diff --git a/lib/api/config_api.py b/lib/api/config_api.py index e5a4f69..77838b5 100644 --- a/lib/api/config_api.py +++ b/lib/api/config_api.py @@ -43,6 +43,13 @@ def create_config(): 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 + custom_service_fee_price = data.get('custom_service_fee_price') + if custom_service_fee_price is not None and custom_service_fee_price != '': + custom_service_fee_price = float(custom_service_fee_price) + else: + custom_service_fee_price = None + + custom_service_fee_name = data.get('custom_service_fee_name') if not config_name or not split_type or not split_value: return jsonify({'success': False, 'message': '缺少必填字段'}), 400 @@ -61,8 +68,8 @@ def create_config(): 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, - 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()) + show_monthly_total, custom_service_fee_price, custom_service_fee_name, is_active, sort_order, create_time, update_time) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 1, %s, NOW(), NOW()) """ params = ( @@ -78,6 +85,8 @@ def create_config(): telecom_vehicle_no, json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None, show_monthly_total, + custom_service_fee_price, + custom_service_fee_name if custom_service_fee_name else None, sort_order ) @@ -108,13 +117,21 @@ def update_config(config_id): 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 + custom_service_fee_price = data.get('custom_service_fee_price') + if custom_service_fee_price is not None and custom_service_fee_price != '': + custom_service_fee_price = float(custom_service_fee_price) + else: + custom_service_fee_price = None + + custom_service_fee_name = data.get('custom_service_fee_name') 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, - show_monthly_total = %s, update_time = NOW() + show_monthly_total = %s, custom_service_fee_price = %s, custom_service_fee_name = %s, + update_time = NOW() WHERE id = %s """ @@ -130,6 +147,8 @@ def update_config(config_id): telecom_vehicle_no, json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None, show_monthly_total, + custom_service_fee_price, + custom_service_fee_name if custom_service_fee_name else None, config_id ) @@ -172,8 +191,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, 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())''', + (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, custom_service_fee_price, custom_service_fee_name, is_active, sort_order, create_time, update_time) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())''', ( new_id, new_name, @@ -187,6 +206,8 @@ def copy_config(config_id): original.get('telecom_vehicle_no', ''), original.get('field_custom_names', None), original.get('show_monthly_total', 0), + original.get('custom_service_fee_price'), + original.get('custom_service_fee_name'), original['is_active'], sort_order ) @@ -286,6 +307,8 @@ EXCEL_COLUMNS = [ {'key': 'telecom_vehicle_no', 'name': '特来电自编号', 'required': False}, {'key': 'field_custom_names', 'name': '字段自定义表头(JSON对象)', 'required': False, 'comment': '如: {"charge_degree":"充电量(kWh)"}'}, {'key': 'show_monthly_total', 'name': '是否显示月度总计', 'required': False, 'comment': '0=否, 1=是'}, + {'key': 'custom_service_fee_price', 'name': '自定义服务费单价(元/kWh)', 'required': False, 'comment': '设置后按充电电量×单价计算自定义服务费'}, + {'key': 'custom_service_fee_name', 'name': '自定义服务费表头名称', 'required': False, 'comment': '报表中显示的列名,留空则使用默认名称'}, {'key': 'is_active', 'name': '是否启用', 'required': False, 'comment': '0=禁用, 1=启用'}, {'key': 'sort_order', 'name': '排序号', 'required': False, 'comment': '数字越小越靠前'}, ] @@ -432,6 +455,8 @@ def download_template(): 'telecom_vehicle_no': '', 'field_custom_names': '{"charge_degree":"充电量(kWh)"}', 'show_monthly_total': '1', + 'custom_service_fee_price': '0.8', + 'custom_service_fee_name': '服务费(自定义)', 'is_active': '0', 'sort_order': '1', } @@ -598,12 +623,25 @@ def import_configs(): is_active = config_data.get('is_active', '0').strip() is_active = 1 if is_active in ('1', '是', 'true', 'True') else 0 + custom_service_fee_price_str = config_data.get('custom_service_fee_price', '').strip() + if custom_service_fee_price_str: + try: + custom_service_fee_price = float(custom_service_fee_price_str) + except Exception: + custom_service_fee_price = None + else: + custom_service_fee_price = None + + custom_service_fee_name = config_data.get('custom_service_fee_name', '').strip() + if not custom_service_fee_name: + custom_service_fee_name = None + 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, - 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()) + show_monthly_total, custom_service_fee_price, custom_service_fee_name, is_active, sort_order, create_time, update_time) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) """ params = ( @@ -619,6 +657,8 @@ def import_configs(): config_data.get('telecom_vehicle_no', ''), json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None, show_monthly_total, + custom_service_fee_price, + custom_service_fee_name, is_active, sort_order ) diff --git a/lib/db_init.py b/lib/db_init.py index ea06ba0..d61feb8 100644 --- a/lib/db_init.py +++ b/lib/db_init.py @@ -21,6 +21,9 @@ def init_database_tables(): merge_telecom TINYINT NULL COMMENT '是否合并特来电数据', telecom_vehicle_no VARCHAR(500) NULL COMMENT '特来电车量自编号', field_custom_names VARCHAR(2000) NULL COMMENT '字段自定义报表名头', + show_monthly_total TINYINT NULL COMMENT '是否显示当月充电量总计', + custom_service_fee_price DECIMAL(10,4) NULL COMMENT '自定义服务费单价(元/kWh)', + custom_service_fee_name VARCHAR(100) NULL COMMENT '自定义服务费报表表头名称', is_active TINYINT NULL COMMENT '是否启用', sort_order INT NULL COMMENT '排序顺序', create_time DATETIME NULL COMMENT '创建时间', @@ -118,7 +121,31 @@ def init_database_tables(): if "Duplicate" in str(e) or "duplicate" in str(e) or "Exists" in str(e) or "exists" in str(e): pass else: - print(f"[数据库] 求和数据表新增 sort_order 字段时出错(可能已存在): {e}") + print(f"[数据库] 新增 sort_order 字段时出错(可能已存在): {e}") + + # 给配置表增加 custom_service_fee_price 字段(自定义服务费单价) + try: + execute_update( + "ALTER TABLE t_daily_report_config ADD COLUMN custom_service_fee_price DECIMAL(10,4) NULL COMMENT '自定义服务费单价(元/kWh)' " + ) + print("[数据库] 配置表新增 custom_service_fee_price 字段成功") + 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"[数据库] 新增 custom_service_fee_price 字段时出错(可能已存在): {e}") + + # 给配置表增加 custom_service_fee_name 字段(自定义服务费表头名称) + try: + execute_update( + "ALTER TABLE t_daily_report_config ADD COLUMN custom_service_fee_name VARCHAR(100) NULL COMMENT '自定义服务费报表表头名称' " + ) + print("[数据库] 配置表新增 custom_service_fee_name 字段成功") + 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"[数据库] 新增 custom_service_fee_name 字段时出错(可能已存在): {e}") # 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值 try: diff --git a/lib/report_generator.py b/lib/report_generator.py index 5d12313..3f67593 100644 --- a/lib/report_generator.py +++ b/lib/report_generator.py @@ -475,7 +475,9 @@ def generate_daily_report(config_id, start_time=None, end_time=None): show_monthly_total=config.get('show_monthly_total', 0) == 1, config_id=config['id'], report_date=start_time.strftime('%Y-%m-%d'), - sort_order=config.get('sort_order') + sort_order=config.get('sort_order'), + custom_service_fee_price=config.get('custom_service_fee_price'), + custom_service_fee_name=config.get('custom_service_fee_name') ) print(f"[日报生成] Excel文件已生成: {file_path}") @@ -565,7 +567,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, - show_monthly_total=False, config_id=None, report_date=None, sort_order=None): + show_monthly_total=False, config_id=None, report_date=None, sort_order=None, + custom_service_fee_price=None, custom_service_fee_name=None): """ 生成Excel文件 @@ -697,6 +700,21 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, cell.alignment = header_alignment cell.border = thin_border + # 自定义服务费列表头(如果设置了单价) + has_custom_service_fee = custom_service_fee_price is not None and custom_service_fee_price != '' + if has_custom_service_fee: + custom_fee_col = len(selected_fields) + 2 + cell = ws.cell(row=header_row, column=custom_fee_col) + if custom_service_fee_name and str(custom_service_fee_name).strip(): + header_name = str(custom_service_fee_name).strip() + else: + header_name = f'自定义服务费({custom_service_fee_price}元/kWh)' + cell.value = header_name + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = thin_border + ws.row_dimensions[header_row].height = 25 # 写入数据 @@ -747,6 +765,19 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, if is_even: cell.fill = even_row_fill + # 自定义服务费列(如果设置了单价) + if has_custom_service_fee: + custom_fee_col = len(selected_fields) + 2 + cell = ws.cell(row=row_idx, column=custom_fee_col) + charge_degree = float(order.get('charge_degree', 0) or 0) + custom_fee = round(charge_degree * float(custom_service_fee_price), 2) + cell.value = custom_fee + cell.border = thin_border + cell.alignment = Alignment(horizontal='right', vertical='center') + cell.number_format = '0.00' + if is_even: + cell.fill = even_row_fill + ws.row_dimensions[row_idx].height = 22 # 写入求和行 @@ -769,6 +800,19 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, cell.alignment = Alignment(horizontal='right', vertical='center') cell.number_format = '0.000' + # 自定义服务费合计 + if has_custom_service_fee: + custom_fee_col = len(selected_fields) + 2 + cell = ws.cell(row=sum_row, column=custom_fee_col) + cell.fill = sum_fill + cell.border = thin_border + total_charge_degree = sum_results.get('charge_degree', 0) + total_custom_fee = round(float(total_charge_degree) * float(custom_service_fee_price), 2) + cell.value = total_custom_fee + cell.font = sum_font + cell.alignment = Alignment(horizontal='right', vertical='center') + cell.number_format = '0.00' + ws.row_dimensions[sum_row].height = 25 # 写入当月充电量累计总计行(从求和采集表查询,对账后数据) @@ -807,7 +851,10 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, ws.cell(row=monthly_row, column=1).border = thin_border # 其他列也填充背景色 - for col_idx in range(2, len(selected_fields) + 2): + total_cols = len(selected_fields) + 2 + if has_custom_service_fee: + total_cols += 1 + for col_idx in range(2, total_cols): cell = ws.cell(row=monthly_row, column=col_idx) cell.fill = monthly_fill cell.border = thin_border @@ -848,6 +895,18 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, col_letter = get_column_letter(col_idx) ws.column_dimensions[col_letter].width = min(max_length + 2, 50) + # 自定义服务费列宽度 + if has_custom_service_fee: + custom_fee_col = len(selected_fields) + 2 + if custom_service_fee_name and str(custom_service_fee_name).strip(): + header_name = str(custom_service_fee_name).strip() + else: + header_name = f'自定义服务费({custom_service_fee_price}元/kWh)' + cn_count = sum(1 for c in str(header_name) if '\u4e00' <= c <= '\u9fff') + max_length = len(str(header_name)) + cn_count + col_letter = get_column_letter(custom_fee_col) + ws.column_dimensions[col_letter].width = min(max_length + 2, 50) + # 冻结首行 ws.freeze_panes = 'A2' diff --git a/lib/sum_data_collector.py b/lib/sum_data_collector.py index 1994018..e0feedf 100644 --- a/lib/sum_data_collector.py +++ b/lib/sum_data_collector.py @@ -36,7 +36,7 @@ def collect_sum_data_from_history(history_id=None): collected_count = 0 skipped_count = 0 - config_sort_cache = {} # 缓存配置的排序号 + config_cache = {} # 缓存配置的排序号和自定义服务费单价 for history in history_list: sum_results = history.get('sum_results') @@ -47,23 +47,36 @@ def collect_sum_data_from_history(history_id=None): elif sum_results is None: sum_results = {} - # 获取配置的排序号(缓存) + # 获取配置信息(排序号、自定义服务费单价)(缓存) config_id = history.get('config_id') sort_order = None + custom_service_fee_price = None if config_id: - if config_id in config_sort_cache: - sort_order = config_sort_cache[config_id] + if config_id in config_cache: + sort_order = config_cache[config_id].get('sort_order') + custom_service_fee_price = config_cache[config_id].get('custom_service_fee_price') + custom_service_fee_name = config_cache[config_id].get('custom_service_fee_name') else: try: cfg_result = execute_query( - 'SELECT sort_order FROM t_daily_report_config WHERE id = %s LIMIT 1', + 'SELECT sort_order, custom_service_fee_price, custom_service_fee_name FROM t_daily_report_config WHERE id = %s LIMIT 1', (config_id,) ) - if cfg_result and cfg_result[0]['sort_order'] is not None: - sort_order = cfg_result[0]['sort_order'] - config_sort_cache[config_id] = sort_order + if cfg_result: + sort_order = cfg_result[0].get('sort_order') + custom_service_fee_price = cfg_result[0].get('custom_service_fee_price') + custom_service_fee_name = cfg_result[0].get('custom_service_fee_name') + config_cache[config_id] = { + 'sort_order': sort_order, + 'custom_service_fee_price': custom_service_fee_price, + 'custom_service_fee_name': custom_service_fee_name + } except Exception: - config_sort_cache[config_id] = None + config_cache[config_id] = { + 'sort_order': None, + 'custom_service_fee_price': None, + 'custom_service_fee_name': None + } # 强制确保有充电电量(charge_degree) if 'charge_degree' not in sum_results or sum_results['charge_degree'] is None: @@ -117,6 +130,44 @@ def collect_sum_data_from_history(history_id=None): ) execute_insert(sql, params) collected_count += 1 + + # 如果配置了自定义服务费单价,自动计算并采集自定义服务费 + if custom_service_fee_price is not None and custom_service_fee_price != '' and sum_results.get('charge_degree') is not None: + try: + charge_degree = float(sum_results['charge_degree']) + custom_service_fee = round(charge_degree * float(custom_service_fee_price), 2) + if custom_service_fee_name and str(custom_service_fee_name).strip(): + fee_display_name = str(custom_service_fee_name).strip() + else: + fee_display_name = f'自定义服务费({custom_service_fee_price}元/kWh)' + sum_data_id = int(datetime.now().timestamp() * 1000000) + collected_count + sql = ''' + INSERT INTO t_daily_report_sum_data + (id, report_date, config_id, config_name, split_type, split_value, split_name, + sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark, + create_time, update_time) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) + ''' + params = ( + sum_data_id, + history['report_date'], + history['config_id'], + history.get('config_name', ''), + history.get('split_type', ''), + history.get('split_value', ''), + history.get('split_name', ''), + 'custom_service_fee', + fee_display_name, + custom_service_fee, + history.get('total_orders', 0), + sort_order, + 'auto', + f'自动计算:充电电量{charge_degree}kWh × 单价{custom_service_fee_price}元/kWh' + ) + execute_insert(sql, params) + collected_count += 1 + except Exception as fee_error: + print(f"[采集] 计算自定义服务费失败: {fee_error}") return { 'success': True, diff --git a/public/static/js/config.js b/public/static/js/config.js index d698585..721ecaa 100644 --- a/public/static/js/config.js +++ b/public/static/js/config.js @@ -87,6 +87,14 @@ function showCreateModal() { const showMonthlyTotalCheckbox = document.getElementById('show-monthly-total'); if (showMonthlyTotalCheckbox) showMonthlyTotalCheckbox.checked = false; + // 重置自定义服务费单价 + const customServiceFeePriceInput = document.getElementById('custom-service-fee-price'); + if (customServiceFeePriceInput) customServiceFeePriceInput.value = ''; + + // 重置自定义服务费表头名称 + const customServiceFeeNameInput = document.getElementById('custom-service-fee-name'); + if (customServiceFeeNameInput) customServiceFeeNameInput.value = ''; + document.getElementById('config-modal').classList.add('active'); } @@ -479,6 +487,14 @@ async function saveConfig() { // 获取月度总计开关 const showMonthlyTotal = document.getElementById('show-monthly-total')?.checked || false; + + // 获取自定义服务费单价 + const customServiceFeePriceInput = document.getElementById('custom-service-fee-price'); + const customServiceFeePrice = customServiceFeePriceInput?.value !== '' ? parseFloat(customServiceFeePriceInput.value) : null; + + // 获取自定义服务费表头名称 + const customServiceFeeNameInput = document.getElementById('custom-service-fee-name'); + const customServiceFeeName = customServiceFeeNameInput?.value.trim() || null; if (!configName || !splitType || !splitValue) { alert('请填写所有必填字段'); @@ -519,7 +535,9 @@ async function saveConfig() { merge_telecom: mergeTelecom, telecom_vehicle_no: telecomVehicleNo, field_custom_names: fieldCustomNamesData, - show_monthly_total: showMonthlyTotal + show_monthly_total: showMonthlyTotal, + custom_service_fee_price: customServiceFeePrice, + custom_service_fee_name: customServiceFeeName }; try { @@ -600,6 +618,18 @@ async function editConfig(configId) { showMonthlyTotalCheckbox.checked = config.show_monthly_total === 1 || config.show_monthly_total === true; } + // 加载自定义服务费单价 + const customServiceFeePriceInput = document.getElementById('custom-service-fee-price'); + if (customServiceFeePriceInput) { + customServiceFeePriceInput.value = config.custom_service_fee_price !== null && config.custom_service_fee_price !== undefined ? config.custom_service_fee_price : ''; + } + + // 加载自定义服务费表头名称 + const customServiceFeeNameInput = document.getElementById('custom-service-fee-name'); + if (customServiceFeeNameInput) { + customServiceFeeNameInput.value = config.custom_service_fee_name || ''; + } + document.getElementById('config-modal').classList.add('active'); } } diff --git a/templates/index.html b/templates/index.html index 6202afc..abf0552 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@