fix(requisition): 模板三级解析,空白模板入库,产物只写会话 artifacts

历史清洗删掉了根目录两份 低值易耗品申购*.xls——它们其实是**已填写的真实申购单**
(半导体型号、单价、项目名都在),不是模板;同时旧实现「项目根 glob 取名字最大者」会把
上一次生成的产物当模板,所以一并修掉:

- 新增 resolve_requisition_template():REQUISITION_TEMPLATE 环境变量 → templates/ → 项目根(兼容旧位置);
  环境变量指向不存在的文件时告警并继续回退,不静默失败
- 入库 templates/低值易耗品申购.xls 空白件(由旧文件删掉第 3 行起的全部内容得到,UsedRange 只剩 2 行:
  标题 + 表头;真实采购明细不进仓库)
- generate_requisition 默认输出改为 <会话>/artifacts/低值易耗品申购<ts>.xls,不再写模板旁边
- mcp/tools/documents.py 的 _find_template 改为复用同一解析函数,避免两处规则漂移
- 模块 docstring 与 CLI help 里 src/web/uploads 的过时示例改为 cases/
- tests:5 个纯单测覆盖解析优先级(env 优先 / templates 胜过根目录 / 根目录取最新 /
  env 指向不存在的文件时回退 / 全都没有时返回 None),不依赖 COM

验证:真实 Excel COM 端到端 2 项 → 产物 UsedRange 5 行(标题/表头/2 明细/合计),第 6 行起为空,
无旧明细残留;fill_requisition_excel 原本就会先删第 3 行起内容,这条也实测确认过。
门禁:ruff / format --check / mypy src / deptry 全绿,pytest 112 passed,mcp_smoke 17 工具。
This commit is contained in:
wandering
2026-10-19 23:57:44 +08:00
parent e1acf2de84
commit 7e900ebdea
5 changed files with 102 additions and 20 deletions
+12 -5
View File
@@ -11,7 +11,12 @@ from typing import Any
from ...cases import ARTIFACTS_DIR_NAME, CACHE_DIR_NAME, PROJECT_ROOT, CaseStore
from ...infra.com_thread import run_com
from ...infra.documents import REQUISITION_FILENAME, generate_requisition, load_requisition_items
from ...infra.documents import (
REQUISITION_FILENAME,
generate_requisition,
load_requisition_items,
resolve_requisition_template,
)
from ..envelope import fail, ok
_store = CaseStore()
@@ -32,7 +37,10 @@ def doc_requisition(case_id: str, exclude: list[str] | None = None, project: str
return fail("没有可申购的普通发票", next_action="确认已抽取普通发票,或本会话是纯差旅无需申购单")
ts_template = _find_template()
if ts_template is None:
return fail(f"未找到申购单模板 {REQUISITION_FILENAME}*.xls", next_action="把模板放到项目根目录")
return fail(
f"未找到申购单空白模板 {REQUISITION_FILENAME}",
next_action="放一份到 templates/ 目录,或设置环境变量 REQUISITION_TEMPLATE 指向模板文件",
)
out = d / ARTIFACTS_DIR_NAME / f"低值易耗品申购_{d.name}.xls"
out.parent.mkdir(parents=True, exist_ok=True)
try:
@@ -65,6 +73,5 @@ def _groups(d: Path) -> dict[str, Any] | None:
def _find_template() -> Path | None:
stem = REQUISITION_FILENAME.replace(".xls", "")
candidates = sorted(PROJECT_ROOT.glob(f"{stem}*.xls"))
return candidates[-1] if candidates else None
"""模板位置统一由 requisition.resolve_requisition_template 决定,不在这里另写一套。"""
return resolve_requisition_template(PROJECT_ROOT)