Bläddra i källkod

refactor#明细样式调整.

hefuzheng 1 vecka sedan
förälder
incheckning
a6f60553f8

BIN
code/post_clear_check/src/post_clear_check/__pycache__/dimension_body.cpython-312.pyc


+ 113 - 13
code/post_clear_check/src/post_clear_check/dimension_body.py

@@ -124,7 +124,11 @@ def _body(chain_name: str, views: tuple[_View, ...], business_date: date) -> lis
         and not view.formula_text.startswith(CLEAR_MARK)
         and view.formula_text not in GATE_FORMULAS
     ]
-    clears = [view.calc_text for view in views if view.formula_text.startswith(CLEAR_MARK) and view.calc_text]
+    clears = [
+        _reflow_topic(view.calc_text)
+        for view in views
+        if view.formula_text.startswith(CLEAR_MARK) and view.calc_text
+    ]
     pieces = [_summary(chain_name, views)]
     if chain_name == DIM_ORDER:
         pieces.extend(_order_sections(rest, clears))
@@ -347,23 +351,69 @@ def _report_block(rows: Sequence[_View], business_date: date) -> str:
         (
             f"报表:{label}",
             f"筛选:{screen}",
-            _stacked("期望", names),
+            _expect_line(names),
             f"实据:{actual}",
-            _stacked("依据", bases or ["见各列登记"]),
+            _basis_line(bases or ["见各列登记"]),
             f"结论:{label_text}",
         )
     )
 
 
-def _stacked(label: str, items: Sequence[str]) -> str:
-    """多条依据或期望各占一行。用分号拼在一起时,原型路径会挤成一行。"""
+def _expect_line(items: Sequence[str]) -> str:
+    """多个字段写在同一句里,用逗号分开,句号收尾。"""
 
-    kept = [item for item in items if item]
+    kept: list[str] = []
+    for item in items:
+        text = item.strip().rstrip("。").strip()
+        if text and text not in kept:
+            kept.append(text)
     if not kept:
-        return f"{label}:"
-    if len(kept) == 1:
-        return f"{label}:{kept[0]}"
-    return f"{label}:{kept[0]}\n" + "\n".join(kept[1:])
+        return "期望:"
+    return "期望:" + ",".join(kept) + "。"
+
+
+def _basis_line(items: Sequence[str]) -> str:
+    """同一份原型文件只写一次,后面接它引用的字段。不同文件仍分行。"""
+
+    files: dict[str, list[str]] = {}
+    sequence: list[tuple[str, str]] = []
+    for item in items:
+        text = item.strip()
+        if not text:
+            continue
+        doc, field = _split_basis(text)
+        if not field:
+            sequence.append(("plain", text))
+            continue
+        if doc not in files:
+            files[doc] = []
+            sequence.append(("file", doc))
+        if field not in files[doc]:
+            files[doc].append(field)
+    lines: list[str] = []
+    for kind, key in sequence:
+        if kind == "plain":
+            lines.append(key)
+            continue
+        quoted = "".join(f"「{name}」" for name in files[key])
+        lines.append(f"{key} {quoted}".strip())
+    if not lines:
+        return "依据:"
+    return "依据:" + lines[0] + (("\n" + "\n".join(lines[1:])) if len(lines) > 1 else "")
+
+
+def _split_basis(text: str) -> tuple[str, str]:
+    """从「产品原型 某文件.md 「列名」」里拆出文件和列名。没有列名就不分组。"""
+
+    if "「" not in text or "」" not in text:
+        return "", ""
+    doc, _, rest = text.partition("「")
+    field, _, _tail = rest.partition("」")
+    doc = doc.strip()
+    field = field.strip()
+    if not doc or not field:
+        return "", ""
+    return doc, field
 
 
 def _formula_names(rows: Sequence[_View]) -> list[str]:
@@ -466,9 +516,9 @@ def format_topic(title: str, lines: Sequence[object]) -> str:
     return _lines(
         (
             f"字段:{title}",
-            _stacked("期望", names[:8] or ["该项已登记的公式"]),
+            _expect_line(names or ["该项已登记的公式"]),
             f"实据:{actual}",
-            _stacked("依据", bases[:6] or ["见各列登记"]),
+            _basis_line(bases or ["见各列登记"]),
             f"结论:{label}({counts[VERDICT_CORRECT]}/{len(orders)})",
         )
     )
@@ -486,9 +536,59 @@ def _clear_title(formula: str) -> str:
     return formula
 
 
+def _reflow_topic(text: str) -> str:
+    """已落库的字段块再排一次。期望收成一句,同一份原型只留一次文件名。"""
+
+    labels = ("字段:", "期望:", "实据:", "依据:", "结论:")
+    sections: list[tuple[str, list[str]]] = []
+    label = ""
+    parts: list[str] = []
+    for raw in text.splitlines():
+        line = raw.strip()
+        if not line:
+            continue
+        matched = next((item for item in labels if line.startswith(item)), "")
+        if matched:
+            if label:
+                sections.append((label, parts))
+            label = matched
+            rest = line[len(matched) :].strip()
+            parts = [rest] if rest else []
+            continue
+        if label:
+            parts.append(line)
+    if label:
+        sections.append((label, parts))
+    rows: list[str] = []
+    for name, chunks in sections:
+        if name == "期望:":
+            items: list[str] = []
+            for chunk in chunks:
+                items.extend(_split_items(chunk))
+            rows.append(_expect_line(items))
+            continue
+        if name == "依据:":
+            rows.append(_basis_line(chunks))
+            continue
+        body = "\n".join(chunk for chunk in chunks if chunk)
+        rows.append(f"{name}{body}" if body else name)
+    return "\n".join(rows)
+
+
+def _split_items(text: str) -> list[str]:
+    """把已经用逗号或分号连过的字段再拆开,避免重复加标点。"""
+
+    found: list[str] = []
+    for piece in text.replace(";", ",").split(","):
+        item = piece.strip().rstrip("。").strip()
+        if item:
+            found.append(item)
+    return found
+
+
 def _index_block(text: str) -> str:
     if text.startswith("字段:"):
-        return text
+        return _reflow_topic(text)
     head, _, rest = text.partition(":")
     actual = rest.split("。", 1)[0] if rest else text
     detail = "对应报表卡"

BIN
code/post_clear_check/tests/__pycache__/test_feishu.cpython-312-pytest-9.1.1.pyc


+ 57 - 17
code/post_clear_check/tests/test_feishu.py

@@ -1475,8 +1475,8 @@ def test_dimension_report_body_keeps_correct_rows_on_error_theme() -> None:
     assert "筛选:主订单 ORD001 · 资金账户 FA00000001" in body
 
 
-def test_report_bases_are_one_per_line() -> None:
-    """多条依据各占一行。分号拼在一起时,原型路径会挤成一行。"""
+def test_report_bases_group_same_file_and_expect_uses_commas() -> None:
+    """同一份原型只写一次文件名。期望的多个字段用逗号连成一句。"""
 
     body = render_body(
         business_date=DAY,
@@ -1488,30 +1488,70 @@ def test_report_bases_are_one_per_line() -> None:
             _dim(
                 "股票数量",
                 VERDICT_CORRECT,
-                check="OTC003 共赢票据订单报表.股票数量 = 1",
-                report_code="OTC003",
-                basis="产品原型 订单报表.md 「股票数量」",
+                check="OTC008 共赢票据订单报表.股票数量 = 1",
+                report_code="OTC008",
+                basis="产品原型 8 Notes Trade Order Non-Settlement Report.md 「名义本金(交易币种)」",
             ),
             _dim(
-                "价格",
+                "发行费用(结算币种)",
                 VERDICT_CORRECT,
-                check="OTC003 共赢票据订单报表.价格 = 1",
-                report_code="OTC003",
-                basis="产品原型 订单报表.md 「价格」",
+                check="OTC008 共赢票据订单报表.发行费用 = 1",
+                report_code="OTC008",
+                basis="产品原型 8 Notes Trade Order Non-Settlement Report.md 「发行费用(结算币种)」",
             ),
             _dim(
-                "总金额",
+                "报表用途",
                 VERDICT_CORRECT,
-                check="OTC003 共赢票据订单报表.总金额 = 1",
-                report_code="OTC003",
-                basis="产品原型 订单报表.md 「总金额」",
+                check="OTC008 共赢票据订单报表.用途 = 1",
+                report_code="OTC008",
+                basis="产品原型 7 Notes Trade Order Settlement Report.md 「报表用途」",
             ),
         ),
     )
-    assert "依据:产品原型 订单报表.md 「股票数量」\n产品原型 订单报表.md 「价格」\n产品原型 订单报表.md 「总金额」" in body
-    assert "另有" not in body
-    assert "依据:产品原型 订单报表.md 「股票数量」;" not in body
-    assert "期望:股票数量\n价格\n总金额" in body
+    assert (
+        "依据:产品原型 8 Notes Trade Order Non-Settlement Report.md 「名义本金(交易币种)」「发行费用(结算币种)」\n"
+        "产品原型 7 Notes Trade Order Settlement Report.md 「报表用途」"
+    ) in body
+    assert body.count("8 Notes Trade Order Non-Settlement Report.md") == 1
+    assert "期望:股票数量,发行费用(结算币种),报表用途。" in body
+
+
+def test_stored_topic_collapses_repeated_prototype_files() -> None:
+    """已经落库的字段块在发送前再排。同一份原型只留一次,期望改成一句。"""
+
+    stored = "\n".join(
+        (
+            "字段:TRS 发生额与汇率口径",
+            "期望:发生金额(交易币种)",
+            "发生金额(结算币种)",
+            "实据:TRS 发生额与汇率口径:本轮主单 8 笔,正确 8,错误 0,可疑 0。列核对 78 条。",
+            "依据:产品原型 TRS/2 Master Transaction Order Report.md 「发生金额(交易币种)」",
+            "产品原型 TRS/2 Master Transaction Order Report.md 「发生金额(结算币种)」",
+            "产品原型 TRS/3 Sub Transaction Order Report.md 「发生金额(交易币种)」",
+            "产品原型 TRS/3 Sub Transaction Order Report.md 「发生金额(结算币种)」",
+            "结论:正确(8/8)",
+        )
+    )
+    body = render_body(
+        business_date=DAY,
+        trade_date=DAY,
+        batch_no="IB",
+        chain_name=DIM_ORDER,
+        record_no="15",
+        lines=(
+            _dim(
+                f"{CLEAR_MARK}TRS 发生额与汇率口径",
+                VERDICT_CORRECT,
+                calc=stored,
+                calc_value=None,
+                check_value=None,
+            ),
+        ),
+    )
+    assert "期望:发生金额(交易币种),发生金额(结算币种)。" in body
+    assert body.count("2 Master Transaction Order Report.md") == 1
+    assert body.count("3 Sub Transaction Order Report.md") == 1
+    assert "「发生金额(交易币种)」「发生金额(结算币种)」" in body
 
 
 def test_dimension_long_body_keeps_fields_and_errors() -> None:

+ 1 - 1
doc/业务链路核对设计.md

@@ -506,7 +506,7 @@ TRS 的字段公式不在 Excel 里,在产品原型 `TRS/4 业务引起数据
 无
 ```
 
-章节之间空一行。一张报表或一个字段内部用单个换行,五行连在一起。同一格里有多条期望或多条依据时,每条单独一行,不用分号拼成一行,也不再收成「另有若干条」。空的错误、可疑、无法展开各写一行「:无」。
+章节之间空一行。一张报表或一个字段内部用单个换行,五行连在一起。同一格里有多条期望时,用逗号连成一句,句号收尾。同一份原型文件只写一次,后面接它引用的字段;不同文件仍分行。空的错误、可疑、无法展开各写一行「:无」。
 
 ### 7.3 三张报表
 

+ 1 - 1
doc/使用说明.md

@@ -180,7 +180,7 @@ OTC009、OTC012 是实时导出,没有落库结果,`report_topup_payment`、
 
 三张报表正文是批次门禁、摘要、分报表、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。方向卡是错误主题时,正确列仍留在分报表。
 
-章节之间空一行。一张报表或一个字段内部用单个换行。多条依据、多条期望各占一行。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,`<` `>` 会改成全角。发送失败时日志带上飞书返回的 `msg`。
+章节之间空一行。一张报表或一个字段内部用单个换行。期望的多个字段用逗号连成一句。同一份原型只写一次,字段接在文件名后面。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,`<` `>` 会改成全角。发送失败时日志带上飞书返回的 `msg`。
 
 已有库要先放宽两道 CHECK,再启动。不要重跑 `schema.sql`,那会清空已有轮次。没执行时新的方向卡插不进去,这一轮会整段回滚。