fix: 预览按 draft-{id} 命名且重建全量同构 + C1 空台账改判数据待补 + 立项经费结余结论 + CLI 拆分 import-*/list-* + docx 占位黄底

This commit is contained in:
chenfuqiang
2026-09-27 21:47:56 +08:00
parent 7b24521b43
commit db11f22dc8
11 changed files with 508 additions and 107 deletions

View File

@@ -60,6 +60,15 @@ def _camel(name: str) -> str:
return head + "".join(p[:1].upper() + p[1:] for p in rest if p)
def _to_float(value: Any) -> Optional[float]:
if value is None or value == "":
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _snake(name: str) -> str:
return "".join("_" + ch.lower() if ch.isupper() else ch for ch in str(name)).lstrip("_")
@@ -73,6 +82,7 @@ def _norm_project(raw: dict[str, Any]) -> dict[str, Any]:
"program": ("program", "项目类型", "资助渠道"),
"startDate": ("start_date", "执行期开始", "开始日期"),
"endDate": ("end_date", "执行期结束", "结束日期"),
"approvedBudget": ("approved_budget", "立项经费", "批准经费"),
"expected": ("expected_json", "预期成果指标", "预期指标"),
"annualReportExcerpt": ("annual_report_excerpt", "年度报告摘句", "年度报告"),
"status": ("status", "状态"),
@@ -123,11 +133,16 @@ def _norm_expense(raw: dict[str, Any]) -> dict[str, Any]:
def _c1_compare(expected: list[dict[str, Any]], achievements: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""C1:按类型对比预期指标与实际成果。"""
"""C1:按类型对比预期指标与实际成果。
成果台账整表为空时无从对比,一律判"数据待补"(不下"未达标"结论);
台账非空但某类型无记录按实际 0 计(事实陈述)。
"""
counts: dict[str, int] = {}
for ach in achievements:
ach_type = str(ach.get("achievement_type") or "").strip()
counts[ach_type] = counts.get(ach_type, 0) + 1
ledger_empty = not achievements
rows = []
for item in expected:
item_type = str(item.get("type") or "").strip()
@@ -140,8 +155,11 @@ def _c1_compare(expected: list[dict[str, Any]], achievements: list[dict[str, Any
actual = counts.get(item_type)
if target_v is None:
verdict, detail = "数据待补", f"预期指标未配置数值(类型:{item_type})"
elif ledger_empty:
verdict, detail = "数据待补", f"预期 {target_v} {unit},成果清单尚未录入,无法对比(W1)"
elif actual is None:
verdict, detail = "未达标", f"预期 {target_v} {unit},台账中未录入该类成果"
verdict, detail = "未达标", f"预期 {target_v} {unit},实际 0 {unit}(台账中未录入该类成果)"
actual = 0
else:
if actual > target_v:
verdict = "超额完成"
@@ -177,7 +195,11 @@ def _c3_expense_sentences(expenses: list[dict[str, Any]], total_approved: Option
return [], "经费决算明细尚未录入,请在补录后重新生成初稿(W1)。"
tail = f"。项目决算合计支出 {_wan(total)}。"
if total_approved is not None:
tail += "支出总额控制在立项经费范围内。" if total <= total_approved else "支出总额超出立项经费,请补充说明。"
balance = round(total_approved - total, 2)
if balance >= 0:
tail += f"结余 {_wan(balance)},支出总额控制在立项经费({_wan(total_approved)})范围内。"
else:
tail += f"超出立项经费({_wan(total_approved)}){_wan(-balance)},请补充说明。"
return rows, "本项目经费支出按科目如下:" + ";".join(sentences) + "。" + tail
@@ -224,7 +246,8 @@ def build_draft_sections(
expenses = [{_snake(k): v for k, v in _norm_expense(e).items()} for e in expenses]
completion = _c1_compare(expected, achievements)
expense_rows, expense_paragraph = _c3_expense_sentences(expenses, None)
approved_budget = _to_float(project.get("approved_budget") or project.get("approvedBudget"))
expense_rows, expense_paragraph = _c3_expense_sentences(expenses, approved_budget)
excerpt = project.get("annual_report_excerpt") or project.get("annualReportExcerpt") or ""
narrative_points = _c4_narrative_points(achievements, str(excerpt))
@@ -278,16 +301,6 @@ def run_draft(project_raw: dict[str, Any], achievements: list[dict[str, Any]], e
draft_file = write_docx(project, sections, batch_no)
draft = {
"project_no": project_no,
"project_name": str(project.get("project_name") or ""),
"batch_no": batch_no,
"sections": sections,
"completion": sections_data["completion"],
"narrative_points": sections_data["narrative_points"],
}
preview_file = write_preview_for_kernel(draft=draft)
conn = get_conn()
try:
cur = conn.cursor()
@@ -295,7 +308,7 @@ def run_draft(project_raw: dict[str, Any], achievements: list[dict[str, Any]], e
"""
INSERT INTO report_drafts
(batch_no, project_no, project_name, completion_json, narrative_points_json, draft_file, preview_file)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, '')
""",
(
batch_no,
@@ -304,10 +317,20 @@ def run_draft(project_raw: dict[str, Any], achievements: list[dict[str, Any]], e
json.dumps(sections_data["completion"], ensure_ascii=False),
json.dumps(sections_data["narrative_points"], ensure_ascii=False),
draft_file,
preview_file,
),
)
draft_id = int(cur.lastrowid)
# 预览按初稿记录编号命名(draft-{id}.html),多次生成互不覆盖。
draft = {
"project_no": project_no,
"project_name": str(project.get("project_name") or ""),
"batch_no": batch_no,
"sections": sections,
"completion": sections_data["completion"],
"narrative_points": sections_data["narrative_points"],
}
preview_file = write_preview_for_kernel(draft=draft, draft_id=draft_id)
cur.execute("UPDATE report_drafts SET preview_file = ? WHERE id = ?", (preview_file, draft_id))
cur.execute(
"UPDATE closing_projects SET status = '初稿已生成', updated_at = ? WHERE project_no = ?",
(now_unix(), project_no),