fix(cash): 智享通凭证导入放开负数金额(红字记账)+日期来源三级优先(行内voucher_date优先) — 修复423张凭证整张拒收 (dama-zxt-import-endpoint-fix-20260908)

This commit is contained in:
Hermes CI Fix
2026-09-11 16:24:41 +08:00
parent 9822fbd9ba
commit 286238811b
+42 -8
View File
@@ -12,12 +12,12 @@
Bearer token,Bot hook 只有 X-BOT-KEY 到不了,故同前缀另挂本 router(FastAPI 允许多 router 同前缀)。
校验口径(对齐财务BOT映射说明 cma_field_mapping_finance_20260906.md + 任务单验收):
1. 行级:voucher_no 必有;debit/credit 字符串→float round2,非负,借贷不同时0
1. 行级:voucher_no 必有;debit/credit 字符串→float round2,允许负数(红字记账,负数行计数 neg_rows),借贷不同时0
2. 结转行:summary 含"结转" → carry_forward=1(zxt 113行"期间损益结转"100%命中)
3. 凭证级借贷平衡(每 Voucher_ID Σ借=Σ贷 容差0.01)—— 批级平衡对 zxt 快照不适用
(已知单边凭证 30694 仅贷5479.65),不平衡凭证整张拒收记异常清单,不整批拒绝
4. 日期/period:zxt 行无 voucher_date 字段,日期从 summary 前缀 YYYY-MM-DD 解析;
解析不到的行(期间损益结转等)按 pending 拒收,不冒充文件生成时间
4. 日期/period:三级优先——行内 voucher_date/VoucherDate/BillDate 字段 → summary 前缀
YYYY-MM-DD 解析 → 均无则按 pending 拒收;不冒充文件生成时间/当前时间
5. 科目:subject_code/subject_name 全 null(智享通侧未带)。解析优先级:
a) 行内 subject_code/subject_name 有值 → 直用
b) 无 → 查请求体 subject_map {CwAccountID: {subject_code, subject_name}}
@@ -34,7 +34,7 @@ import hashlib
import json
import logging
import re
from datetime import datetime
from datetime import date, datetime
from typing import Any, Optional, Union
from fastapi import APIRouter, Body, Depends, HTTPException, Request
@@ -87,6 +87,33 @@ def _parse_date_from_summary(summary: str):
return None
def _parse_date_value(raw) -> Optional[datetime]:
"""行内日期字段解析(voucher_date/VoucherDate/BillDate)。
datetime/date 对象直用;字符串 strip 后 fromisoformat(YYYY-MM-DD / YYYY-MM-DD[ T]HH:MM:SS),
失败回退 _DATE_RE search(YYYY/MM/DD 等);仍取不到返回 None——严禁冒充当前时间/文件时间。"""
if raw is None:
return None
if isinstance(raw, datetime):
return datetime(raw.year, raw.month, raw.day)
if isinstance(raw, date):
return datetime(raw.year, raw.month, raw.day)
s = str(raw).strip()
if not s:
return None
try:
dt = datetime.fromisoformat(s)
return datetime(dt.year, dt.month, dt.day)
except ValueError:
pass
m = _DATE_RE.search(s)
if not m:
return None
try:
return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)))
except ValueError:
return None
def _is_carry_forward(summary: str) -> bool:
"""结转行识别(subject_name 缺失只能靠 summary):zxt 期间损益结转 100% 命中"""
return "结转" in (summary or "")
@@ -211,17 +238,23 @@ def api_import_vouchers_json(
parsed = []
pending_undated = 0
pending_dict = 0
neg_rows = 0
for idx, raw in enumerate(norm_rows):
voucher_no = _norm_str(raw.get("voucher_no") or raw.get("Voucher_ID"))
if not voucher_no:
errors.append({"row": idx + 1, "field": "voucher_no", "reason": "凭证号为空"})
continue
date_obj = _parse_date_from_summary(_norm_str(raw.get("summary")))
# 三级优先:行内日期字段 → summary 前缀 → 拒收
date_obj = _parse_date_value(
raw.get("voucher_date") or raw.get("VoucherDate") or raw.get("BillDate")
)
if date_obj is None:
date_obj = _parse_date_from_summary(_norm_str(raw.get("summary")))
if date_obj is None:
pending_undated += 1
errors.append({"row": idx + 1, "field": "voucher_date",
"reason": f"summary无日期(待日期来源): {_norm_str(raw.get('summary'))[:40]}"})
"reason": f"行内日期与summary均无日期(待日期来源): {_norm_str(raw.get('summary'))[:40]}"})
continue
debit = _parse_amount(raw.get("debit"))
@@ -233,8 +266,7 @@ def api_import_vouchers_json(
errors.append({"row": idx + 1, "field": "credit", "reason": f"贷方金额不是有效数字: {raw.get('credit')}"})
continue
if debit < 0 or credit < 0:
errors.append({"row": idx + 1, "field": "amount", "reason": "金额不能为负"})
continue
neg_rows += 1 # 红字记账允许负数入库,仅计数不拒收
if debit == 0 and credit == 0:
errors.append({"row": idx + 1, "field": "amount", "reason": "借贷金额不能同时为0"})
continue
@@ -275,6 +307,7 @@ def api_import_vouchers_json(
"entity_id": entity_id,
"pending_undated": pending_undated,
"pending_dict_miss": pending_dict,
"neg_rows": neg_rows,
"hint": "科目字典:请协调智享通推送方补 subject_code/subject_name,或在 body 带 subject_map,或建 zxt_account_dict 表",
}
@@ -406,6 +439,7 @@ def api_import_vouchers_json(
"carry_forward_count": carry_forward_count,
"pending_undated": pending_undated,
"pending_dict_miss": pending_dict,
"neg_rows": neg_rows,
"batch": batch,
"entity_id": entity_id,
}