kocr/core/excel_writer.py

328 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""KOCR v4 — Excel 模板填充与合并"""
import os
from datetime import datetime
from collections import defaultdict
def fill_template(voucher_data, template_path, output_path, verbose=False):
"""填充金蝶 K3 导入模板
Args:
voucher_data: K3VoucherParser.parse() 的输出
template_path: 模板 .xls 文件路径
output_path: 输出 .xls 文件路径
verbose: 是否输出详细信息
"""
import xlrd
import xlwt
wb = xlrd.open_workbook(template_path, formatting_info=True)
schema_sheet = wb.sheet_by_index(0)
page_sheet = wb.sheet_by_index(1)
out_wb = xlwt.Workbook(encoding='utf-8')
# Sheet 1: t_Schema
out_ws = out_wb.add_sheet('t_Schema', cell_overwrite_ok=True)
for r in range(schema_sheet.nrows):
for c in range(schema_sheet.ncols):
val = schema_sheet.cell_value(r, c)
if isinstance(val, float) and val == int(val):
val = int(val)
out_ws.write(r, c, val)
# Sheet 2: Page1
out_page = out_wb.add_sheet('Page1', cell_overwrite_ok=True)
headers = []
for c in range(page_sheet.ncols):
val = page_sheet.cell_value(0, c)
out_page.write(0, c, val)
headers.append(val)
def col_idx(predicate, default=999):
for i, h in enumerate(headers):
if predicate in str(h):
return i
return default
# 列索引
col_date = col_idx('凭证日期')
col_year = col_idx('会计年度')
col_period = col_idx('会计期间')
col_vword = col_idx('凭证字')
col_vno = col_idx('凭证号')
col_code = col_idx('科目代码')
col_name = col_idx('科目名称')
col_currency = col_idx('币别')
col_cur_name = col_idx('币别名称')
col_debit = col_idx('借方')
col_credit = col_idx('贷方')
col_desc = col_idx('摘要')
col_preparer = col_idx('制单')
col_checker = col_idx('审核')
col_approve = col_idx('核准')
col_cashier = col_idx('出纳')
col_handler = col_idx('经办')
col_settle_type = col_idx('结算方式')
col_attachments = col_idx('附件数')
col_entry_id = col_idx('分录序号')
col_amount_for = col_idx('原币金额')
col_biz_date = col_idx('业务日期')
col_measure_unit = col_idx('数量单位')
col_unit_price = col_idx('单价')
col_exchange_rate = col_idx('汇率')
date = voucher_data['date'] or datetime.now()
row = 1
entry_id = 0
for ei, entry in enumerate(voucher_data['entries']):
debit = entry.get('debit', 0) or 0
credit = entry.get('credit', 0) or 0
amount = max(debit, credit)
out_page.write(row, col_date, f'{date.year}-{date.month}-{date.day}')
out_page.write(row, col_year, float(voucher_data['year'] or date.year))
out_page.write(row, col_period, float(voucher_data['period'] or date.month))
out_page.write(row, col_vword, voucher_data['voucher_word'] or '')
out_page.write(row, col_vno, float(voucher_data['voucher_no'] or 0))
out_page.write(row, col_biz_date, f'{date.year}-{date.month}-{date.day}')
out_page.write(row, col_code, entry['account_code'])
out_page.write(row, col_name, entry['account_name'])
out_page.write(row, col_currency, 'RMB')
out_page.write(row, col_cur_name, '人民币')
out_page.write(row, col_amount_for, float(amount))
out_page.write(row, col_debit, float(debit))
out_page.write(row, col_credit, float(credit))
out_page.write(row, col_desc, entry.get('description', ''))
out_page.write(row, col_preparer, voucher_data['preparer'] or 'Manager')
out_page.write(row, col_checker, 'NONE')
out_page.write(row, col_approve, 'NONE')
out_page.write(row, col_cashier, 'NONE')
out_page.write(row, col_handler, voucher_data['handler'] or '')
out_page.write(row, col_settle_type, '*')
out_page.write(row, col_attachments, float(0))
out_page.write(row, col_measure_unit, '*')
out_page.write(row, col_unit_price, float(0))
out_page.write(row, col_entry_id, float(entry_id))
out_page.write(row, col_exchange_rate, float(1))
row += 1
entry_id += 1
os.makedirs(os.path.dirname(output_path), exist_ok=True)
out_wb.save(output_path)
if verbose:
total_entries = len(voucher_data['entries'])
print(f"\n✅ Excel 输出: {output_path}")
print(f" 凭证: {voucher_data['voucher_word']}-{voucher_data['voucher_no']}")
print(f" 日期: {date.strftime('%Y-%m-%d')}")
print(f" 分录: {total_entries}")
print(f" 合计: 借 {sum(e.get('debit',0) or 0 for e in voucher_data['entries']):.2f}{sum(e.get('credit',0) or 0 for e in voucher_data['entries']):.2f}")
def merge_output(results, template_path, output_dir, review=False):
"""合并多张凭证为一个排序好的 Excel
Args:
results: [(fname, voucher_data, output_path, quality), ...]
template_path: 模板路径
output_dir: 输出目录
review: 是否生成校验报告
Returns:
merge_path: 合并后的 Excel 路径
"""
import xlrd
import xlwt
# 按 (日期, 凭证号) 分组
groups = defaultdict(list)
for item in results:
v = item[1]
key = (
v.get('date').strftime('%Y-%m-%d') if v.get('date') else '0000',
v.get('voucher_no') or 0
)
groups[key].append(item)
sorted_keys = sorted(groups.keys())
src_wb = xlrd.open_workbook(template_path, formatting_info=True)
schema_sheet = src_wb.sheet_by_index(0)
all_entries = []
merged_report = []
for key in sorted_keys:
group = groups[key]
if len(group) == 1:
# 单页
fname, voucher, out_path, quality = group[0]
wb = xlrd.open_workbook(out_path)
ws = wb.sheet_by_name('Page1')
for r in range(1, ws.nrows):
row_data = [ws.cell_value(r, c) for c in range(ws.ncols)]
all_entries.append(row_data)
merged_report.append(([fname], fname, voucher, None))
else:
# 多页合并
page_names = [g[0] for g in group]
voucher = group[0][1]
total_dr = 0
total_cr = 0
pages_entries = []
for fname, v, out_path, quality in group:
wb = xlrd.open_workbook(out_path)
ws = wb.sheet_by_name('Page1')
page_rows = []
for r in range(1, ws.nrows):
row_data = [ws.cell_value(r, c) for c in range(ws.ncols)]
page_rows.append(row_data)
dr_val = float(row_data[10] or 0) if isinstance(row_data[10], (int, float)) else 0.0
cr_val = float(row_data[11] or 0) if isinstance(row_data[11], (int, float)) else 0.0
total_dr += dr_val
total_cr += cr_val
pages_entries.append((fname, page_rows))
merged_ok = abs(total_dr - total_cr) < 0.01
for fname, page_rows in pages_entries:
for row_data in page_rows:
all_entries.append(row_data)
pages_str = ' + '.join(page_names)
merged_report.append((page_names, pages_str, voucher, merged_ok))
# 写入合并文件
# ---- 科目名称归一化:同一科目代码取最高频名称 ---- #
from collections import Counter
name_counter = Counter()
for row_data in all_entries:
code = str(row_data[5]) if len(row_data) > 5 else ''
name = str(row_data[6]) if len(row_data) > 6 else ''
if name and len(name) >= 2:
name_counter[(code, name)] += 1
code_best = {}
for (code, name), cnt in name_counter.items():
if code not in code_best or cnt > code_best[code][1]:
code_best[code] = (name, cnt)
fixed_count = 0
for row_data in all_entries:
code = str(row_data[5]) if len(row_data) > 5 else ''
if code in code_best:
best_name = code_best[code][0]
if len(row_data) > 6 and str(row_data[6]) != best_name:
row_data[6] = best_name
fixed_count += 1
if fixed_count > 0:
print(f' 🔄 科目名称归一化:修正 {fixed_count} 处不一致')
# ---- 标准映射覆盖:用户提供的正确对照表 ---- #
STANDARD_NAMES = {
'1002.01.01': '淇县支行1710020809200182975',
'1002.02.01': '漓江支行4105016228400000750',
'1012.01': 'POS刷卡未到账',
'2203.01': '物业管理费',
'2241.01.01': '装修押金',
'6051.02': '办证工本收入',
'6051.08': '转售水电费收入',
'6401.02.02': '办公费',
'6401.51.09': '日常维修费',
}
override_count = 0
for row_data in all_entries:
code = str(row_data[5]) if len(row_data) > 5 else ''
if code in STANDARD_NAMES and len(row_data) > 6:
correct_name = STANDARD_NAMES[code]
if str(row_data[6]) != correct_name:
row_data[6] = correct_name
override_count += 1
if override_count > 0:
print(f' 📋 标准映射覆盖:修正 {override_count}')
out_wb = xlwt.Workbook(encoding='utf-8')
out_ws = out_wb.add_sheet('t_Schema', cell_overwrite_ok=True)
for r in range(schema_sheet.nrows):
for c in range(schema_sheet.ncols):
val = schema_sheet.cell_value(r, c)
if isinstance(val, float) and val == int(val):
val = int(val)
out_ws.write(r, c, val)
out_page = out_wb.add_sheet('Page1', cell_overwrite_ok=True)
for c in range(len(all_entries[0]) if all_entries else 0):
out_page.write(0, c, src_wb.sheet_by_index(1).cell_value(0, c))
for r, row_data in enumerate(all_entries):
for c, val in enumerate(row_data):
out_page.write(r + 1, c, val)
merge_path = os.path.join(output_dir, '合并凭证_按顺序.xls')
out_wb.save(merge_path)
return merge_path
def merge_voucher_data(results):
"""按(日期,凭证号)合并凭证数据,返回合并后的凭证列表
多页凭证(同一凭证号的多张照片)合并 entries单页不变。
合并后的数据用于统一的借贷平衡 + 大小写金额校验。
Args:
results: [(fname, voucher_data, out_path, quality), ...]
Returns:
[voucher_dict, ...] 每个 voucher_dict 包含合并后的 entries
"""
from collections import defaultdict
groups = defaultdict(list)
for item in results:
v = item[1]
key = (
v.get('date').strftime('%Y-%m-%d') if v.get('date') else '0000',
v.get('voucher_no') or 0
)
groups[key].append(item)
merged_list = []
for key in sorted(groups.keys()):
group = groups[key]
page_names = [g[0] for g in group]
base = group[0][1] # 第一页的元数据(日期、凭证号等)
# 合并所有页的 entries
all_entries = []
for fname, v, out_path, quality in group:
all_entries.extend(v.get('entries', []))
# 重新编号
for i, e in enumerate(all_entries):
e['id'] = i + 1
merged_list.append({
'date': base.get('date'),
'voucher_word': base.get('voucher_word', ''),
'voucher_no': base.get('voucher_no'),
'entries': all_entries,
'total_str': base.get('total_str', ''),
'total_amount': base.get('total_amount', 0),
'pages': page_names,
'is_multi_page': len(group) > 1,
'page_count': len(group),
})
return merged_list