"""《初审意见单》HTML 报告组装。 写入 `{skill_data}/html/report-{report_id}.html`,宿主数据管理侧栏可预览; 重新生成走固定 Action id `render-html`(见 references/ACTIONS.md)。 `{{BODY}}` 内数据一律经 html.escape 转义。 """ from __future__ import annotations import html import json from typing import Any from service.html_page_service import load_html_template, render_html_placeholders, write_html_page from util.timeutil import unix_to_iso _TEMPLATE_REL = "assets/html/prereview-report.html" _LEVEL_BADGE = {"pass": "✅", "warn": "⚠️", "reject": "❌"} def _esc(value: Any) -> str: return html.escape(str(value if value is not None else ""), quote=True) def _items_rows(items_json: str) -> str: try: items = json.loads(items_json) if items_json else [] except (TypeError, ValueError): items = [] if not items: return '无' rows = [] for item in items: badge = _LEVEL_BADGE.get(str(item.get("level") or ""), "•") ref = str(item.get("rule_ref") or "") ref_cell = f'依据:{_esc(ref)}' if ref else "" rows.append( "" f'{badge} {_esc(item.get("rule_id"))}' f'{_esc(item.get("name"))}
{_esc(item.get("detail"))}{ref_cell}' f'{_esc(item.get("level"))}' "" ) return "".join(rows) def _info_table(inv: dict[str, Any]) -> str: fields = ( ("发票类型", "invoice_type"), ("发票代码", "invoice_code"), ("发票号码", "invoice_number"), ("开票日期", "issue_date"), ("金额(元)", "amount"), ("购买方名称", "buyer_name"), ("购买方税号", "buyer_tax_no"), ("销售方名称", "seller_name"), ("销售方税号", "seller_tax_no"), ("费用类别", "expense_category"), ("消费城市", "city"), ) rows = "".join( f"{_esc(label)}{_esc(inv.get(key) if inv.get(key) not in (None, '') else '—')}" for label, key in fields ) return f'{rows}
' def _context_table(context: dict[str, Any]) -> str: fields = ( ("报销事由", "purpose"), ("预算项目编号", "project_no"), ("出差开始", "trip_start"), ("出差结束", "trip_end"), ("目的地", "trip_city"), ("职级", "staff_title"), ) rows = [] for label, key in fields: value = str(context.get(key) or "").strip() if value: rows.append(f"{_esc(label)}{_esc(value)}") if not rows: return "" return f'{"".join(rows)}
' def build_report_html( *, report: dict[str, Any], invoice: dict[str, Any], context: dict[str, Any], ) -> str: """组装完整《初审意见单》HTML 文本。""" verdict = str(report.get("verdict") or "") badge = {"通过": "verdict-pass", "待确认": "verdict-warn", "驳回": "verdict-reject"}.get(verdict, "verdict-warn") mark = {"通过": "✅ 通过", "待确认": "⚠️ 待确认", "驳回": "❌ 驳回"}.get(verdict, verdict) verify_status = str(report.get("verify_status") or "待查验") verify_note = str(report.get("verify_note") or "") created = unix_to_iso(report.get("created_at")) body = f"""
{mark} 批次 {_esc(report.get("batch_no"))} · 报告编号 {_esc(report.get("id"))} · 生成于 {_esc(created or "")}

{_esc(report.get("summary"))}

发票信息

{_info_table(invoice)}

报销上下文

{_context_table(context) or '

未提供差旅/项目上下文。

'}

❌ 驳回项(必须整改)

{_items_rows(report.get("reject_json") or "")}
规则说明级别

⚠️ 待确认项(请核实)

{_items_rows(report.get("warn_json") or "")}
规则说明级别

✅ 通过项

{_items_rows(report.get("pass_json") or "")}
规则说明级别

真伪查验

当前状态:{_esc(verify_status)}{(" · 备注:" + _esc(verify_note)) if verify_note else ""}

请经办人到全国增值税发票查验平台(inv-veri.chinatax.gov.cn)核验票面真伪后,在数据管理“初审意见”表回填查验结果。

""" template = load_html_template(_TEMPLATE_REL) return render_html_placeholders( template, { "TITLE": f"初审意见单 · 发票 {report.get('invoice_number') or ''}", "GENERATED_AT": created or "", "BODY": body, }, ) def build_report_for_invoice(report_id: int, batch_no: str = "") -> str: """按报告编号从库中取数并写出 HTML,返回文件绝对路径(重建入口用)。""" from db.connection import get_conn from util.logging_config import get_skill_logger log = get_skill_logger() log.info("report_render_start report_id=%s stage=render", report_id) conn = get_conn() try: cur = conn.cursor() cur.execute("SELECT * FROM prereview_reports WHERE id = ?", (report_id,)) row = cur.fetchone() if not row: raise ValueError(f"初审报告不存在:{report_id}") report = _row_to_dict(cur, "prereview_reports", row) invoice = {} if report.get("invoice_id"): cur.execute("SELECT * FROM invoice_records WHERE id = ?", (report["invoice_id"],)) irow = cur.fetchone() if irow: invoice = _row_to_dict(cur, "invoice_records", irow) context: dict[str, Any] = {} try: context = json.loads(invoice.get("trip_context_json") or "{}") except (TypeError, ValueError): context = {} html_text = build_report_html(report=report, invoice=invoice, context=context) finally: conn.close() filename = f"report-{report_id}.html" path = write_html_page(filename, html_text) log.info("report_render_done report_id=%s path=%s stage=render", report_id, path) return path def write_report_for_kernel( *, report_id: int, report: dict[str, Any], invoice: dict[str, Any], context: dict[str, Any], ) -> str: """内核路径:用事务内内存数据直接组装并写出 HTML(不回查未提交的行)。""" html_text = build_report_html(report=report, invoice=invoice, context=context) return write_html_page(f"report-{report_id}.html", html_text) def _row_to_dict(cursor, table_name: str, row) -> dict[str, Any]: columns = [desc[0] for desc in cursor.description] return dict(zip(columns, row))