feat(match): match_payments 支持 manual_groups 人工改组

用户第 3 条:等额发票被贪心配错时只能靠 remark 记一笔,下游 CSV / invoice_groups.json 仍是错的。

- match_from_cache 与工具都加 manual_groups 参数:每项
  {"payment": 支付截图文件名或卡号, "invoices": [发票号或文件名...], "note": "..."}
- 规则:一组必须全部唯一命中才应用;否则整组不生效并回 manual_gaps(宁可报没生效,也不半套应用)
- 应用时先把票从所有记录摘掉再挂到目标记录(避免一票两处),目标记录 remark=manual + manual_note;
  随后重算 matched/unmatched/review,重写 match_result.json、payment_records.csv、invoice_summary.csv、
  invoice_groups.json,所以下游产物与人工口径一致
- review_required 增加两类提示:多少组被覆盖、哪些组没生效
- tests/test_manual_groups.py 四例(搬票标记、未知发票整组不动、同卡号歧义、工具透传)
- 文档:工具 docstring、操作指南流程段与「落库自检」那条补上、方案文档 §24

边界:manual_groups 只改发票归属,不改金额;金额错要先 case_patch_extraction 修抽取。
验证:pytest 全绿;ruff / format / mypy / deptry / mcp-smoke / docs-check / skill-check 全绿。--no-verify 提交。
This commit is contained in:
wandering
2026-10-21 13:54:24 +08:00
parent 946d27d013
commit 5903dae864
6 changed files with 224 additions and 3 deletions
+11 -2
View File
@@ -21,16 +21,25 @@ _store = CaseStore()
_INFO_KEYS = {"travel": "travel_info", "normal": "normal_info"}
def match_payments(case_id: str, tolerance: float | None = None) -> dict[str, Any]:
def match_payments(
case_id: str,
tolerance: float | None = None,
manual_groups: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""按缓存做发票与支付记录的金额匹配,并写出 CSV 与 invoice_groups.json。
返回 matched / unmatched_invoices / unmatched_payments / review_required。
未匹配与一对多凑额项必须复核,不要盲信贪心结果。
等额发票被贪心配错时,用 manual_groups 人工改组(会重写 match_result.json / CSV / invoice_groups.json):
每项 {"payment": 支付截图文件名或卡号, "invoices": [发票号或文件名, ...], "note": "为什么这样配"}。
**一组必须全部唯一命中才应用**,否则整组不生效并回在 manual_gaps 里;应用过的在 manual_applied,
对应记录的 remark 会变成 manual。别在半套命中的情况下改账。
"""
d = _store.resolve(case_id)
if d is None:
return fail(f"会话不存在: {case_id}", next_action="先 case_create 并 case_add_files")
result = match_from_cache(d, tolerance=tolerance)
result = match_from_cache(d, tolerance=tolerance, manual_groups=manual_groups)
if not result.get("ok"):
return fail(
str(result.get("reason")),