Files
ocean_project_manager/backend/config/import-excel-data-v2.py
T
2026-01-26 23:45:58 +08:00

310 lines
14 KiB
Python

#!/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 = '../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': 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())