美化表格

This commit is contained in:
2026-07-14 14:39:38 +08:00
parent ccc041604a
commit fdd232989b
7 changed files with 356 additions and 49 deletions

View File

@@ -42,6 +42,7 @@ def create_config():
merge_telecom = 1 if data.get('merge_telecom') else 0
telecom_vehicle_no = data.get('telecom_vehicle_no', '')
field_custom_names = data.get('field_custom_names', {})
show_monthly_total = 1 if data.get('show_monthly_total') else 0
if not config_name or not split_type or not split_value:
return jsonify({'success': False, 'message': '缺少必填字段'}), 400
@@ -59,8 +60,9 @@ def create_config():
sql = """
INSERT INTO t_daily_report_config
(id, config_name, split_type, split_value, split_name, selected_fields,
sum_fields, time_periods, merge_telecom, telecom_vehicle_no, field_custom_names, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 1, %s, NOW(), NOW())
sum_fields, time_periods, merge_telecom, telecom_vehicle_no, field_custom_names,
show_monthly_total, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 1, %s, NOW(), NOW())
"""
params = (
@@ -75,6 +77,7 @@ def create_config():
merge_telecom,
telecom_vehicle_no,
json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None,
show_monthly_total,
sort_order
)
@@ -104,12 +107,14 @@ def update_config(config_id):
merge_telecom = 1 if data.get('merge_telecom') else 0
telecom_vehicle_no = data.get('telecom_vehicle_no', '')
field_custom_names = data.get('field_custom_names', {})
show_monthly_total = 1 if data.get('show_monthly_total') else 0
sql = """
UPDATE t_daily_report_config
SET config_name = %s, split_type = %s, split_value = %s, split_name = %s,
selected_fields = %s, sum_fields = %s, time_periods = %s,
merge_telecom = %s, telecom_vehicle_no = %s, field_custom_names = %s, update_time = NOW()
merge_telecom = %s, telecom_vehicle_no = %s, field_custom_names = %s,
show_monthly_total = %s, update_time = NOW()
WHERE id = %s
"""
@@ -124,6 +129,7 @@ def update_config(config_id):
merge_telecom,
telecom_vehicle_no,
json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None,
show_monthly_total,
config_id
)
@@ -166,8 +172,8 @@ def copy_config(config_id):
# 插入新配置
execute_insert(
'''INSERT INTO t_daily_report_config
(id, config_name, split_type, split_value, split_name, selected_fields, sum_fields, time_periods, merge_telecom, telecom_vehicle_no, field_custom_names, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())''',
(id, config_name, split_type, split_value, split_name, selected_fields, sum_fields, time_periods, merge_telecom, telecom_vehicle_no, field_custom_names, show_monthly_total, is_active, sort_order, create_time, update_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())''',
(
new_id,
new_name,
@@ -180,6 +186,7 @@ def copy_config(config_id):
original.get('merge_telecom', 0),
original.get('telecom_vehicle_no', ''),
original.get('field_custom_names', None),
original.get('show_monthly_total', 0),
original['is_active'],
sort_order
)

View File

@@ -8,7 +8,8 @@ from lib.sum_data_collector import (
add_sum_data,
update_sum_data,
delete_sum_data,
batch_delete_sum_data
batch_delete_sum_data,
get_monthly_charge_degree_total
)
from . import sum_data_bp
@@ -128,3 +129,26 @@ def batch_delete_sum_data_api():
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500
@sum_data_bp.route('/sum-data/monthly-total', methods=['GET'])
def monthly_charge_degree_total():
"""获取指定配置当月充电电量累计从sum_data表查询对账后的数据
参数:
config_id: 配置ID可选不传则查所有配置总和
month: 月份,格式 YYYY-MM可选默认当月
"""
try:
config_id = request.args.get('config_id')
month = request.args.get('month')
result = get_monthly_charge_degree_total(config_id, month)
if result['success']:
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': str(e)}), 500

View File

@@ -94,6 +94,19 @@ def init_database_tables():
execute_update(create_sum_data_table)
print("[数据库] t_daily_report_sum_data 表已就绪")
# 给配置表增加 show_monthly_total 字段
try:
execute_update(
"ALTER TABLE t_daily_report_config ADD COLUMN show_monthly_total TINYINT NULL COMMENT '是否显示当月充电量总计' "
)
print("[数据库] 配置表新增 show_monthly_total 字段成功")
except Exception as e:
# 字段已存在会报错,忽略
if "Duplicate" in str(e) or "duplicate" in str(e) or "Exists" in str(e) or "exists" in str(e):
pass
else:
print(f"[数据库] 新增 show_monthly_total 字段时出错(可能已存在): {e}")
# 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值
try:
null_count = execute_query(

View File

@@ -471,7 +471,10 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
end_time=end_time_str,
id_name_maps=id_name_maps,
field_custom_names=config.get('field_custom_names'),
has_supplement=has_supplement
has_supplement=has_supplement,
show_monthly_total=config.get('show_monthly_total', 0) == 1,
config_id=config['id'],
report_date=start_time.strftime('%Y-%m-%d')
)
print(f"[日报生成] Excel文件已生成: {file_path}")
@@ -560,7 +563,8 @@ def generate_daily_report(config_id, start_time=None, end_time=None):
def generate_excel(orders, selected_fields, sum_fields, sum_results,
config_name, split_name, start_time, end_time,
id_name_maps=None, field_custom_names=None, has_supplement=False):
id_name_maps=None, field_custom_names=None, has_supplement=False,
show_monthly_total=False, config_id=None, report_date=None):
"""
生成Excel文件
@@ -568,6 +572,9 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
id_name_maps: ID到名称的映射字典格式为 {field_name: {id_value: name_value}}
field_custom_names: 字段自定义名头字典,格式为 {field_key: custom_name}
has_supplement: 是否有补单记录
show_monthly_total: 是否显示当月充电量总计
config_id: 配置ID用于查询月度累计
report_date: 报表日期,格式 YYYY-MM-DD
Returns:
str: 文件路径
@@ -596,40 +603,91 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
ws = wb.active
ws.title = '日报'
# 写入标题
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(selected_fields))
title_cell = ws.cell(row=1, column=1)
title_cell.value = f'{config_name} - {split_name} 日报'
title_cell.font = Font(size=16, bold=True)
title_cell.alignment = Alignment(horizontal='center')
# 写入时间范围
ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=len(selected_fields))
time_cell = ws.cell(row=2, column=1)
time_cell.value = f'统计时间: {start_time}{end_time}'
time_cell.alignment = Alignment(horizontal='center')
# 写入表头(中文)
header_row = 4
# 表头从第1行开始
header_row = 1
field_custom_names = field_custom_names or {}
# 导入边框样式
from openpyxl.styles import Border, Side
thin_border = Border(
left=Side(style='thin', color='000000'),
right=Side(style='thin', color='000000'),
top=Side(style='thin', color='000000'),
bottom=Side(style='thin', color='000000')
)
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_font = Font(bold=True, color='FFFFFF', size=11)
header_alignment = Alignment(horizontal='center', vertical='center')
even_row_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
sum_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
sum_font = Font(bold=True, size=11)
monthly_fill = PatternFill(start_color='D6E4F0', end_color='D6E4F0', fill_type='solid')
monthly_font = Font(bold=True, color='1F4E78', size=11)
# 数字字段列表(靠右对齐,保留三位小数)
numeric_fields = {
# 电量类
'charge_degree', 'charge_begin_degree', 'charge_end_degree', 'charge_ah',
'sharp_electricity', 'peak_electricity', 'flat_electricity', 'valley_electricity',
'charge_times_degree', 'subsidy_degree',
# 金额类(应收)
'receivable_electric_fee', 'receivable_service_fee', 'receivable_total_fee',
# 金额类(实际)
'charge_elecfee_amount', 'charge_elecfee_cost_amount', 'charge_service_amount', 'charging_amt',
# 金额类(支付)
'actual_pay_amount', 'pay_amount',
# 金额类(单价)
'charge_unit_price', 'charge_unit_cost', 'charge_unit_service_fee',
# 金额类(结算)
'settle_electric_fee', 'settle_service_fee', 'settle_fee', 'settle_coupon_amount',
# 金额类(优惠券)
'coupon_amount', 'coupon_total_amount', 'elecfee_coupon_amount', 'servicefee_coupon_amount',
# 金额类(发票)
'invoice_fee',
# 金额类(活动)
'activity_electric_fee', 'activity_service_fee', 'activity_total_fee',
'company_activity_total_fee', 'company_activity_service_fee', 'company_activity_electric_fee',
# 金额类(补贴/运营商/佣金)
'subsidy_fee', 'operator_income', 'plat_service_fee',
'com_service_fee', 'com_electric_fee', 'com_total_fee', 'com_charge_discount',
# 兼容字段(旧版命名)
'total_money', 'total_amount', 'pay_money', 'discount_money', 'service_money',
'electricity_money', 'parking_money', 'other_money', 'peak_valley_diff_money',
# 时长类
'total_duration', 'charging_duration', 'charge_duration',
# SOC类
'start_soc', 'end_soc', 'total_soc', 'charge_begin_soc', 'charge_end_soc', 'charge_cur_soc',
}
# 保留三位小数的字段(金额、电量等)
decimal3_fields = numeric_fields
# 写入表头
for col_idx, field in enumerate(selected_fields, 1):
cell = ws.cell(row=header_row, column=col_idx)
# 优先使用自定义名头
if field in field_custom_names and field_custom_names[field]:
display_name = field_custom_names[field]
# 如果是ID字段且有名称映射表头显示为名称而不是ID
elif field in id_name_maps and id_name_maps[field]:
# 将"用户ID"改为"用户""企业ID"改为"企业"等
display_name = get_field_display_name(field).replace('ID', '')
else:
display_name = get_field_display_name(field)
cell.value = display_name
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
cell.alignment = Alignment(horizontal='center')
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
ws.row_dimensions[header_row].height = 25
# 写入数据
for row_idx, order in enumerate(orders, header_row + 1):
data_start_row = header_row + 1
for row_idx, order in enumerate(orders, data_start_row):
is_even = (row_idx - data_start_row) % 2 == 1
for col_idx, field in enumerate(selected_fields, 1):
cell = ws.cell(row=row_idx, column=col_idx)
value = order.get(field, '')
@@ -647,33 +705,123 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results,
value = name_value
cell.value = value
cell.border = thin_border
# 数字字段靠右对齐,其他居中或靠左
if field in numeric_fields:
cell.alignment = Alignment(horizontal='right', vertical='center')
if isinstance(value, (int, float)):
if field in decimal3_fields:
cell.number_format = '0.000'
elif field in ['order_no', 'vehicle_no', 'gun_no', 'connector_no']:
cell.alignment = Alignment(horizontal='center', vertical='center')
else:
cell.alignment = Alignment(horizontal='left', vertical='center')
# 斑马纹(偶数行)
if is_even:
cell.fill = even_row_fill
ws.row_dimensions[row_idx].height = 22
# 写入求和行
last_data_row = header_row + len(orders)
sum_row = last_data_row + 1
if sum_fields:
sum_row = header_row + len(orders) + 1
ws.cell(row=sum_row, column=1).value = '合计'
ws.cell(row=sum_row, column=1).font = Font(bold=True)
ws.cell(row=sum_row, column=1).font = sum_font
ws.cell(row=sum_row, column=1).fill = sum_fill
ws.cell(row=sum_row, column=1).alignment = Alignment(horizontal='center', vertical='center')
ws.cell(row=sum_row, column=1).border = thin_border
for col_idx, field in enumerate(selected_fields, 1):
if field in sum_fields and field in sum_results:
cell = ws.cell(row=sum_row, column=col_idx)
cell.fill = sum_fill
cell.border = thin_border
if field in sum_fields and field in sum_results:
cell.value = sum_results[field]
cell.font = Font(bold=True)
cell.number_format = '0.000' # 保留三位小数
cell.font = sum_font
cell.alignment = Alignment(horizontal='right', vertical='center')
cell.number_format = '0.000'
# 调整列宽
ws.row_dimensions[sum_row].height = 25
# 写入当月充电量累计总计行(从求和采集表查询,对账后数据)
if show_monthly_total and config_id and report_date:
try:
from lib.sum_data_collector import get_monthly_charge_degree_total
month = report_date[:7]
monthly_result = get_monthly_charge_degree_total(config_id, month)
if monthly_result['success'] and monthly_result['data']:
total_degree = monthly_result['data']['total_degree']
total_days = monthly_result['data']['total_days']
today_degree_from_table = monthly_result['data'].get('today_degree', 0)
# 如果当天的数据还没采集进 sum_data 表,手动加上当天的
today_charge_degree = sum_results.get('charge_degree', 0)
if today_degree_from_table == 0 and today_charge_degree > 0:
total_degree = round(total_degree + today_charge_degree, 3)
total_days = total_days + 1
# 月度总计行(在合计行下面空一行)
monthly_row = sum_row + 2 if sum_fields else sum_row + 1
# 找 charge_degree 字段所在的列
charge_degree_col = None
for col_idx, field in enumerate(selected_fields, 1):
max_length = len(get_field_display_name(field))
for row in ws.iter_rows(min_row=header_row + 1, max_row=header_row + len(orders),
if field == 'charge_degree':
charge_degree_col = col_idx
break
# 第一列显示标题
ws.cell(row=monthly_row, column=1).value = f'当月累计充电量(共{total_days}天)'
ws.cell(row=monthly_row, column=1).font = monthly_font
ws.cell(row=monthly_row, column=1).fill = monthly_fill
ws.cell(row=monthly_row, column=1).alignment = Alignment(horizontal='center', vertical='center')
ws.cell(row=monthly_row, column=1).border = thin_border
# 其他列也填充背景色
for col_idx in range(2, len(selected_fields) + 1):
cell = ws.cell(row=monthly_row, column=col_idx)
cell.fill = monthly_fill
cell.border = thin_border
if charge_degree_col:
cell = ws.cell(row=monthly_row, column=charge_degree_col)
cell.value = total_degree
cell.font = monthly_font
cell.alignment = Alignment(horizontal='right', vertical='center')
cell.number_format = '0.000'
ws.row_dimensions[monthly_row].height = 25
except Exception as e:
print(f"[日报生成] 写入月度总计时出错: {e}")
# 调整列宽中文按2个字符宽度计算
for col_idx, field in enumerate(selected_fields, 1):
header_name = get_field_display_name(field)
if field in field_custom_names and field_custom_names[field]:
header_name = field_custom_names[field]
cn_count = sum(1 for c in str(header_name) if '\u4e00' <= c <= '\u9fff')
max_length = len(str(header_name)) + cn_count
for row in ws.iter_rows(min_row=data_start_row, max_row=last_data_row,
min_col=col_idx, max_col=col_idx):
for cell in row:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
# 使用 openpyxl 的列字母转换函数
if cell.value is not None and cell.value != '':
cell_str = str(cell.value)
cn_count = sum(1 for c in cell_str if '\u4e00' <= c <= '\u9fff')
cell_len = len(cell_str) + cn_count
max_length = max(max_length, cell_len)
from openpyxl.utils import get_column_letter
col_letter = get_column_letter(col_idx)
ws.column_dimensions[col_letter].width = min(max_length + 2, 50)
# 冻结首行
ws.freeze_panes = 'A2'
# 保存文件
wb.save(file_path)

View File

@@ -407,3 +407,96 @@ def batch_delete_sum_data(ids):
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}
def get_monthly_charge_degree_total(config_id=None, month=None):
"""
获取当月充电电量累计(从求和数据表查询,对账后的准确数据)
注意:
- 数据来源于 t_daily_report_sum_data 表(线下对账后的数据,可能被手动修改过)
- 只统计 sum_field_key = 'charge_degree' 的记录
- 如果当天已经生成了日报并采集,自动包含在内,不会重复计算
Args:
config_id: 配置ID可选不传则统计所有配置
month: 月份,格式 YYYY-MM可选默认当月
Returns:
dict: {
success: bool,
data: {
total_degree: float, // 当月累计充电电量(kWh)
total_days: int, // 有数据的天数
total_orders: int, // 累计订单数
today_degree: float, // 当天充电电量(kWh)(如果有的话)
month: str, // 统计月份
detail: list // 按天明细
},
message: str
}
"""
try:
# 默认当月
if not month:
month = datetime.now().strftime('%Y-%m')
# 拼接日期模糊匹配前缀
date_prefix = month + '-'
# 构建查询SQL
sql = """
SELECT
report_date,
SUM(sum_value) as total_degree,
SUM(total_orders) as total_orders
FROM t_daily_report_sum_data
WHERE sum_field_key = 'charge_degree'
AND report_date LIKE %s
"""
params = [date_prefix + '%']
if config_id:
sql += ' AND config_id = %s'
params.append(config_id)
sql += ' GROUP BY report_date ORDER BY report_date ASC'
rows = execute_query(sql, tuple(params))
total_degree = 0.0
total_orders = 0
total_days = len(rows)
today_degree = 0.0
today_str = datetime.now().strftime('%Y-%m-%d')
detail = []
for row in rows:
degree = float(row['total_degree'] or 0)
orders = int(row['total_orders'] or 0)
total_degree += degree
total_orders += orders
detail.append({
'report_date': row['report_date'],
'degree': round(degree, 3),
'orders': orders
})
if row['report_date'] == today_str:
today_degree = degree
return {
'success': True,
'data': {
'total_degree': round(total_degree, 3),
'total_days': total_days,
'total_orders': total_orders,
'today_degree': round(today_degree, 3),
'month': month,
'detail': detail
},
'message': '查询成功'
}
except Exception as e:
import traceback
traceback.print_exc()
return {'success': False, 'message': str(e)}

View File

@@ -80,6 +80,11 @@ function showCreateModal() {
loadFields();
initTimePeriodEditor();
clearAllTimePeriods();
// 重置月度总计开关
const showMonthlyTotalCheckbox = document.getElementById('show-monthly-total');
if (showMonthlyTotalCheckbox) showMonthlyTotalCheckbox.checked = false;
document.getElementById('config-modal').classList.add('active');
}
@@ -470,6 +475,9 @@ async function saveConfig() {
const mergeTelecom = document.getElementById('merge-telecom')?.checked || false;
const telecomVehicleNo = document.getElementById('telecom-vehicle-no')?.value.trim() || '';
// 获取月度总计开关
const showMonthlyTotal = document.getElementById('show-monthly-total')?.checked || false;
if (!configName || !splitType || !splitValue) {
alert('请填写所有必填字段');
return;
@@ -508,7 +516,8 @@ async function saveConfig() {
time_periods: timePeriods,
merge_telecom: mergeTelecom,
telecom_vehicle_no: telecomVehicleNo,
field_custom_names: fieldCustomNamesData
field_custom_names: fieldCustomNamesData,
show_monthly_total: showMonthlyTotal
};
try {
@@ -583,6 +592,12 @@ async function editConfig(configId) {
telecomVehicleNoInput.value = config.telecom_vehicle_no || '';
}
// 加载月度总计开关
const showMonthlyTotalCheckbox = document.getElementById('show-monthly-total');
if (showMonthlyTotalCheckbox) {
showMonthlyTotalCheckbox.checked = config.show_monthly_total === 1 || config.show_monthly_total === true;
}
document.getElementById('config-modal').classList.add('active');
}
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单日报系统</title>
<link rel="stylesheet" href="/public/static/css/style.css?v=20260714">
<link rel="stylesheet" href="/public/static/css/style.css?v=2026071402">
</head>
<body>
<div class="container">
@@ -245,6 +245,13 @@
<div class="time-period-summary" id="time-period-summary"></div>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="show-monthly-total">
报表底部显示当月充电量累计总计(数据来源于求和采集表,对账后数据)
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal()">取消</button>
@@ -325,11 +332,11 @@
</div>
<!-- JavaScript 文件 -->
<script src="/public/static/js/common.js?v=20260714"></script>
<script src="/public/static/js/config.js?v=20260714"></script>
<script src="/public/static/js/entity.js?v=20260714"></script>
<script src="/public/static/js/report.js?v=20260714"></script>
<script src="/public/static/js/history.js?v=20260714"></script>
<script src="/public/static/js/sum_data.js?v=20260714"></script>
<script src="/public/static/js/common.js?v=2026071402"></script>
<script src="/public/static/js/config.js?v=2026071402"></script>
<script src="/public/static/js/entity.js?v=2026071402"></script>
<script src="/public/static/js/report.js?v=2026071402"></script>
<script src="/public/static/js/history.js?v=2026071402"></script>
<script src="/public/static/js/sum_data.js?v=2026071402"></script>
</body>
</html>