#!/usr/bin/env python3 """ Excel数据导入脚本 从docs/example.xls导入工程项目数据到数据库 使用SQLAlchemy """ import sys import xlrd from datetime import datetime, date import os from decimal import Decimal import asyncio # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import declarative_base from sqlalchemy import text # 配置 EXCEL_FILE = '/home/xsl/code/ocean_project_manager/docs/example.xls' DATABASE_URL = "mysql+aiomysql://root:rootpassword@localhost:3306/project_manager?charset=utf8mb4" # 创建引擎 engine = create_async_engine(DATABASE_URL, echo=False) AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) def parse_date_value(cell_value, workbook): """解析Excel日期值""" if cell_value == '': return None # 检查是否是日期类型 try: date_tuple = xlrd.xldate_as_tuple(cell_value, workbook.datemode) if date_tuple[0] < 1900: return None return f'{date_tuple[0]}-{date_tuple[1]:02d}-{date_tuple[2]:02d}' except: pass # 检查特殊值 if cell_value in ['未到期', '可退质保金', '未开工']: return None # 尝试解析字符串日期 try: date_obj = datetime.strptime(str(cell_value), '%Y-%m-%d') return date_obj.strftime('%Y-%m-%d') except: pass return None def parse_number_value(cell_value): """解析数字值""" if cell_value == '': return None try: return float(cell_value) except: return None def parse_int_value(cell_value): """解析整数值""" num = parse_number_value(cell_value) if num is not None: return int(num) return None def parse_enum_value(cell_value, allowed_values): """解析枚举值""" if cell_value == '': return None if cell_value in allowed_values: return cell_value return None def clean_text_value(cell_value): """清理文本值""" if cell_value == '': return None return str(cell_value).strip() def parse_decimal_value(cell_value): """解析Decimal值""" num = parse_number_value(cell_value) if num is not None: return Decimal(str(num)) return None async def import_projects_from_sheet(sheet, sheet_name, admin_id): """从工作表导入项目数据""" print(f'\n正在导入工作表: {sheet_name}') print(f'总行数: {sheet.nrows}') async with AsyncSessionLocal() as session: success_count = 0 skip_count = 0 error_count = 0 # 从第6行开始(前5行是表头) for row_idx in range(5, sheet.nrows): try: # 读取数据 row_data = [sheet.cell_value(row_idx, col_idx) for col_idx in range(sheet.ncols)] # 检查是否是合计行或空行(检查第1列,索引0) if len(row_data) == 0 or row_data[0] == '' or str(row_data[0]).strip() == '': skip_count += 1 continue # 准备SQL插入语句 sql = """ INSERT INTO projects ( project_no, power_contract_no, name, subitem_count, subitem_code, total_investment, contract_amount, warranty_ratio, settlement_amount, total_cost_estimated, voltage_level, engineering_type, owner_unit, owner_contact, bidding_type, signing_date, start_date, planned_end_date, actual_end_date, warranty_amount, warranty_expiry_date, actual_warranty_refund_date, project_department, project_leader, payment_method, total_cost_control, is_adjusted, labor_cost_control, labor_cost_planned, labor_cost_paid, material_cost_control, material_cost_payable, material_cost_actual, material_cost_paid, other_cost_control, other_cost_payable, other_cost_actual, tax_amount, profit, actual_profit, cost_settlement_amount, cumulative_progress, receivable_amount, invoice_amount, actual_receipt_amount, receipt_completion_rate, payable_amount, actual_payment_amount, unpaid_amount, payment_completion_rate, labor_debt_amount, settlement_cost_amount, settlement_labor_cost, settlement_material_cost, settlement_other_cost, due_settlement_count, unsettlement_count, problems, suggestions, remarks, status, created_by ) VALUES ( :project_no, :power_contract_no, :name, :subitem_count, :subitem_code, :total_investment, :contract_amount, :warranty_ratio, :settlement_amount, :total_cost_estimated, :voltage_level, :engineering_type, :owner_unit, :owner_contact, :bidding_type, :signing_date, :start_date, :planned_end_date, :actual_end_date, :warranty_amount, :warranty_expiry_date, :actual_warranty_refund_date, :project_department, :project_leader, :payment_method, :total_cost_control, :is_adjusted, :labor_cost_control, :labor_cost_planned, :labor_cost_paid, :material_cost_control, :material_cost_payable, :material_cost_actual, :material_cost_paid, :other_cost_control, :other_cost_payable, :other_cost_actual, :tax_amount, :profit, :actual_profit, :cost_settlement_amount, :cumulative_progress, :receivable_amount, :invoice_amount, :actual_receipt_amount, :receipt_completion_rate, :payable_amount, :actual_payment_amount, :unpaid_amount, :payment_completion_rate, :labor_debt_amount, :settlement_cost_amount, :settlement_labor_cost, :settlement_material_cost, :settlement_other_cost, :due_settlement_count, :unsettlement_count, :problems, :suggestions, :remarks, :status, :created_by ) """ # 准备参数(使用安全的索引访问) def safe_get(idx, default=None): return row_data[idx] if idx < len(row_data) else default params = { 'project_no': clean_text_value(safe_get(1)), 'power_contract_no': clean_text_value(safe_get(2)), 'name': clean_text_value(safe_get(3)), 'subitem_count': parse_int_value(safe_get(4)), 'subitem_code': clean_text_value(safe_get(5)), 'total_investment': parse_decimal_value(safe_get(6)), 'contract_amount': parse_decimal_value(safe_get(7)), 'warranty_ratio': parse_decimal_value(safe_get(8)), 'settlement_amount': parse_decimal_value(safe_get(9)), 'total_cost_estimated': parse_decimal_value(safe_get(10)), 'voltage_level': clean_text_value(safe_get(11)), 'engineering_type': clean_text_value(safe_get(12)), 'owner_unit': clean_text_value(safe_get(13)), 'owner_contact': clean_text_value(safe_get(14)), 'bidding_type': clean_text_value(safe_get(15)), 'signing_date': parse_date_value(safe_get(16), sheet.book), 'start_date': parse_date_value(safe_get(17), sheet.book), 'planned_end_date': parse_date_value(safe_get(18), sheet.book), 'actual_end_date': parse_date_value(safe_get(19), sheet.book), 'warranty_amount': parse_decimal_value(safe_get(20)), 'warranty_expiry_date': parse_date_value(safe_get(21), sheet.book), 'actual_warranty_refund_date': parse_date_value(safe_get(22), sheet.book), 'project_department': clean_text_value(safe_get(23)), 'project_leader': clean_text_value(safe_get(24)), 'payment_method': clean_text_value(safe_get(25)), 'total_cost_control': parse_decimal_value(safe_get(26)), 'is_adjusted': parse_enum_value(safe_get(27), ['是', '否']), 'labor_cost_control': parse_decimal_value(safe_get(28)), 'labor_cost_planned': parse_decimal_value(safe_get(29)), 'labor_cost_paid': parse_decimal_value(safe_get(30)), 'material_cost_control': parse_decimal_value(safe_get(31)), 'material_cost_payable': parse_decimal_value(safe_get(32)), 'material_cost_actual': parse_decimal_value(safe_get(33)), 'material_cost_paid': parse_decimal_value(safe_get(34)), 'other_cost_control': parse_decimal_value(safe_get(35)), 'other_cost_payable': parse_decimal_value(safe_get(36)), 'other_cost_actual': parse_decimal_value(safe_get(37)), 'tax_amount': parse_decimal_value(safe_get(38)), 'profit': parse_decimal_value(safe_get(39)), 'actual_profit': parse_decimal_value(safe_get(40)), 'cost_settlement_amount': parse_decimal_value(safe_get(41)), 'cumulative_progress': (lambda x: x if x is None else min(x, Decimal('999.99')))(parse_decimal_value(safe_get(42))), 'receivable_amount': parse_decimal_value(safe_get(43)), 'invoice_amount': parse_decimal_value(safe_get(44)), 'actual_receipt_amount': parse_decimal_value(safe_get(45)), 'receipt_completion_rate': parse_decimal_value(safe_get(46)), 'payable_amount': parse_decimal_value(safe_get(47)), 'actual_payment_amount': parse_decimal_value(safe_get(48)), 'unpaid_amount': parse_decimal_value(safe_get(49)), 'payment_completion_rate': parse_decimal_value(safe_get(50)), 'labor_debt_amount': parse_decimal_value(safe_get(51)), 'settlement_cost_amount': parse_decimal_value(safe_get(52)), 'settlement_labor_cost': parse_decimal_value(safe_get(53)), 'settlement_material_cost': parse_decimal_value(safe_get(54)), 'settlement_other_cost': parse_decimal_value(safe_get(55)), 'due_settlement_count': parse_int_value(safe_get(56)), 'unsettlement_count': parse_int_value(safe_get(57)), 'problems': clean_text_value(safe_get(58)), 'suggestions': clean_text_value(safe_get(59)), 'remarks': clean_text_value(safe_get(60)), 'status': '新建', 'created_by': admin_id } # 跳过没有项目名称的行 if not params['name']: skip_count += 1 continue await session.execute(text(sql), params) success_count += 1 except Exception as e: print(f'第{row_idx+1}行导入错误: {e}', file=sys.stderr) error_count += 1 continue await session.commit() print(f'导入完成: 成功 {success_count} 条, 跳过 {skip_count} 条, 错误 {error_count} 条') return True async def main(): """主函数""" print('='*60) print('工程项目数据导入脚本') print('='*60) # 检查Excel文件是否存在 if not os.path.exists(EXCEL_FILE): print(f'错误: Excel文件不存在: {EXCEL_FILE}', file=sys.stderr) sys.exit(1) # 打开Excel文件 try: workbook = xlrd.open_workbook(EXCEL_FILE) print(f'已打开Excel文件: {EXCEL_FILE}') print(f'工作表数量: {len(workbook.sheet_names())}') except Exception as e: print(f'错误: 无法打开Excel文件: {e}', file=sys.stderr) sys.exit(1) # 获取管理员用户ID async with AsyncSessionLocal() as session: result = await session.execute(text("SELECT id FROM users WHERE username='admin' LIMIT 1")) row = result.fetchone() if not row: print('错误: 未找到管理员用户', file=sys.stderr) sys.exit(1) admin_id = row[0] print(f'管理员用户ID: {admin_id}') try: # 导入主要工作表 main_sheets = [ '1-1基建', '1-2业扩', '1-3客户', '1-4营销', '2检修、技改、应急抢修项目' ] for sheet_name in main_sheets: if sheet_name in workbook.sheet_names(): sheet = workbook.sheet_by_name(sheet_name) await import_projects_from_sheet(sheet, sheet_name, admin_id) else: print(f'警告: 工作表不存在: {sheet_name}') # 显示统计信息 async with AsyncSessionLocal() as session: result = await session.execute(text("SELECT COUNT(*) FROM projects")) total_count = result.scalar() print(f'\n数据库中共有 {total_count} 个项目') result = await session.execute(text("SELECT engineering_type, COUNT(*) as count FROM projects GROUP BY engineering_type")) print('\n按工程类别统计:') for row in result.fetchall(): print(f' {row[0]}: {row[1]} 个') print('\n' + '='*60) print('数据导入完成!') print('='*60) except Exception as e: print(f'错误: {e}', file=sys.stderr) import traceback traceback.print_exc() sys.exit(1) finally: await engine.dispose() if __name__ == '__main__': asyncio.run(main())