- M1 时点报表口径:余额 = 期初(report_opening_balances) + 累计发生额;期初只作用于取数层
(不生成凭证/不写 voucher_details/不改 kpi_values);basis=period 不套期初;受 effective_date 约束
- M2 期初未补 → direction_check_unreliable=true:报表/底稿带"期初未补,方向判定不可信"水印,
重分类触发只进底稿诊断(diagnostic_only),不落地对外行项目
- M7 权益行科目集 = {3001,3002,3101,3103,3104,3105}(含 4001/4002/4101/4103/4104 别名):补实收资本 3001
- 1901 待处理财产损溢过渡列报「其他流动资产」以保恒等式(M6 归因文案)
- 只读验收脚本 + 幂等导入器(dry-run 默认,--commit 门控)
151 lines
7.2 KiB
Python
151 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""账套建账期初余额导入器(M1)—— 幂等 / 默认 dry-run / 显式 --commit 才写库
|
||
|
||
来源清单:`wecom-finance/reports/CMA_酣客建账期初余额_反解清单_v1.csv`(13 行,借贷各 15,110,544.36)
|
||
写入表:`report_opening_balances`(只作用于报表取数;**不生成凭证、不写 voucher_details、不改 kpi_values**)
|
||
|
||
用法:
|
||
cd /root/cma-management/backend
|
||
venv/bin/python3 scripts/import_report_opening_balances.py # dry-run(默认,零写入)
|
||
venv/bin/python3 scripts/import_report_opening_balances.py --commit # 落库(需放行)
|
||
venv/bin/python3 scripts/import_report_opening_balances.py --commit --entity-id 1 --effective-date 2021-03
|
||
|
||
幂等:按 (entity_id, subject_code, effective_date) 判存 —— 新增/等值跳过/差异更新,重复执行结果一致。
|
||
放行前置:本仓 push 会触发部署重启(生产变更),须任总放行;表由 `init_db()`(应用启动 create_all)建出。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import csv
|
||
import io
|
||
import os
|
||
import sys
|
||
from decimal import Decimal
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
DEFAULT_CSV = "/root/.hermes/profiles/wecom-finance/reports/CMA_酣客建账期初余额_反解清单_v1.csv"
|
||
DEFAULT_SOURCE = "reverse-calc-20260916"
|
||
DEFAULT_STATUS = "derived" # 待源系统《科目余额表 2021.03》确认后升级 confirmed
|
||
|
||
|
||
def read_csv(path: str):
|
||
rows = []
|
||
with io.open(path, encoding="utf-8-sig") as f:
|
||
for r in csv.DictReader(f):
|
||
rows.append({
|
||
"entity_id": int((r.get("entity_id") or "1").strip()),
|
||
"subject_code": (r.get("subject_code") or "").strip(),
|
||
"subject_name": (r.get("subject_name") or "").strip(),
|
||
"balance_side": (r.get("balance_side") or "").strip().lower(),
|
||
"amount": Decimal(str((r.get("opening_amount") or "0").strip() or 0)),
|
||
"evidence": (r.get("evidence") or "").strip()[:255],
|
||
"status_note": (r.get("status") or "").strip(),
|
||
})
|
||
return rows
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--csv", default=DEFAULT_CSV)
|
||
ap.add_argument("--entity-id", type=int, default=None, help="覆盖 CSV 内的 entity_id")
|
||
ap.add_argument("--effective-date", default="2021-03", help="建账期 YYYY-MM(期初生效期间)")
|
||
ap.add_argument("--source", default=DEFAULT_SOURCE)
|
||
ap.add_argument("--status", default=DEFAULT_STATUS, choices=["derived", "confirmed"])
|
||
ap.add_argument("--commit", action="store_true", help="真正写库(缺省=dry-run,零写入)")
|
||
args = ap.parse_args()
|
||
|
||
rows = read_csv(args.csv)
|
||
if args.entity_id is not None:
|
||
for r in rows:
|
||
r["entity_id"] = args.entity_id
|
||
|
||
debit = sum((r["amount"] for r in rows if r["balance_side"] == "debit"), Decimal(0))
|
||
credit = sum((r["amount"] for r in rows if r["balance_side"] == "credit"), Decimal(0))
|
||
print(f"[清单] {args.csv}")
|
||
print(f"[清单] {len(rows)} 行 | 借 {debit:,.2f} / 贷 {credit:,.2f} / 差 {debit - credit:,.2f} "
|
||
f"(自平={'是' if abs(debit - credit) < Decimal('0.01') else '否'})")
|
||
for r in rows:
|
||
print(f" e{r['entity_id']} {r['subject_code']:<6}{r['subject_name']:<10}"
|
||
f"{r['balance_side']:<7}{r['amount']:>16,.2f} {r['status_note']}")
|
||
if abs(debit - credit) >= Decimal("0.01"):
|
||
print("[FAIL] 期初借贷不平衡 —— 拒绝导入")
|
||
return 2
|
||
|
||
from app.database import get_session_local
|
||
try:
|
||
from app.models import ReportOpeningBalance
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f"[FAIL] 无法加载模型(表未建?先应用启动 create_all):{exc}")
|
||
return 3
|
||
|
||
db = get_session_local()()
|
||
try:
|
||
# 预检:表是否存在/现有行数(dry-run 也读,只读不写)
|
||
try:
|
||
existing = db.query(ReportOpeningBalance).all()
|
||
table_ready = True
|
||
except Exception as exc: # noqa: BLE001 - 表未建(部署前 create_all 才会建)
|
||
existing = []
|
||
table_ready = False
|
||
print(f"[预检] report_opening_balances 不可读({type(exc).__name__})→ 视为 0 行;"
|
||
f"表由部署后应用启动 `init_db()`/create_all 建出")
|
||
idx = {(r.entity_id, r.subject_code, r.effective_date): r for r in existing}
|
||
print(f"[预检] report_opening_balances 现有 {len(existing)} 行(表就绪={table_ready});"
|
||
f"将按 (entity_id, subject_code, effective_date={args.effective_date}) 幂等导入")
|
||
|
||
ins = upd = skip = 0
|
||
plan = []
|
||
for r in rows:
|
||
key = (r["entity_id"], r["subject_code"], args.effective_date)
|
||
cur = idx.get(key)
|
||
if cur is None:
|
||
ins += 1
|
||
plan.append(("INSERT", r))
|
||
elif (str(cur.balance_side) == r["balance_side"]
|
||
and Decimal(str(cur.amount)) == r["amount"]
|
||
and str(cur.status) == args.status):
|
||
skip += 1
|
||
plan.append(("SKIP", r))
|
||
else:
|
||
upd += 1
|
||
plan.append(("UPDATE", r))
|
||
if args.commit:
|
||
if cur is None:
|
||
db.add(ReportOpeningBalance(
|
||
entity_id=r["entity_id"], subject_code=r["subject_code"],
|
||
subject_name=r["subject_name"], balance_side=r["balance_side"],
|
||
amount=r["amount"], effective_date=args.effective_date,
|
||
source=args.source, evidence=r["evidence"] or args.source,
|
||
status=args.status,
|
||
))
|
||
else:
|
||
cur.subject_name = r["subject_name"]
|
||
cur.balance_side = r["balance_side"]
|
||
cur.amount = r["amount"]
|
||
cur.source = args.source
|
||
cur.evidence = r["evidence"] or args.source
|
||
cur.status = args.status
|
||
print(f"[计划] 新增 {ins} / 更新 {upd} / 跳过(等值){skip}"
|
||
f" | 模式={'COMMIT(写库)' if args.commit else 'DRY-RUN(零写入)'}")
|
||
if not args.commit:
|
||
print("[dry-run] 未写库;确认无误后加 --commit 执行(须任总放行后)")
|
||
return 0
|
||
db.commit()
|
||
after = db.query(ReportOpeningBalance).filter(
|
||
ReportOpeningBalance.entity_id.in_({r["entity_id"] for r in rows})
|
||
).all()
|
||
d2 = sum((Decimal(str(x.amount)) for x in after if x.balance_side == "debit"), Decimal(0))
|
||
c2 = sum((Decimal(str(x.amount)) for x in after if x.balance_side == "credit"), Decimal(0))
|
||
print(f"[回查] 落库后 {len(after)} 行 | 借 {d2:,.2f} / 贷 {c2:,.2f} / 差 {d2 - c2:,.2f}")
|
||
ok = len(after) == len(rows) and abs(d2 - debit) < Decimal("0.01") and abs(c2 - credit) < Decimal("0.01")
|
||
print("[PASS] 落库回查一致" if ok else "[FAIL] 落库回查不一致")
|
||
return 0 if ok else 4
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|