From 286238811bc5453bb437d035e7b94be609ac8166 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Fri, 11 Sep 2026 16:24:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(cash):=20=E6=99=BA=E4=BA=AB=E9=80=9A?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E5=AF=BC=E5=85=A5=E6=94=BE=E5=BC=80=E8=B4=9F?= =?UTF-8?q?=E6=95=B0=E9=87=91=E9=A2=9D(=E7=BA=A2=E5=AD=97=E8=AE=B0?= =?UTF-8?q?=E8=B4=A6)+=E6=97=A5=E6=9C=9F=E6=9D=A5=E6=BA=90=E4=B8=89?= =?UTF-8?q?=E7=BA=A7=E4=BC=98=E5=85=88(=E8=A1=8C=E5=86=85voucher=5Fdate?= =?UTF-8?q?=E4=BC=98=E5=85=88)=20=E2=80=94=20=E4=BF=AE=E5=A4=8D423?= =?UTF-8?q?=E5=BC=A0=E5=87=AD=E8=AF=81=E6=95=B4=E5=BC=A0=E6=8B=92=E6=94=B6?= =?UTF-8?q?=20(dama-zxt-import-endpoint-fix-20260908)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/cash_vouchers_json.py | 50 ++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/backend/app/api/cash_vouchers_json.py b/backend/app/api/cash_vouchers_json.py index 014da8fa..c7fecbf9 100644 --- a/backend/app/api/cash_vouchers_json.py +++ b/backend/app/api/cash_vouchers_json.py @@ -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, }