"""《经费执行简报》HTML 组装。
- 每项目一份 `report-{report_id}.html`(含执行率 vs 序时进度 CSS 柱状图)
- 月度播报另生成汇总页 `brief-{batch_no}.html`
写入 `{skill_data}/html/`,宿主数据管理侧栏可预览;重建走固定 Action id `render-html`。
`{{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.logging_config import get_skill_logger
from util.timeutil import unix_to_iso
_PROGRESS_TEMPLATE_REL = "assets/html/progress-report.html"
_BRIEF_TEMPLATE_REL = "assets/html/monthly-brief.html"
_STATUS_BADGE = {
"严重滞后": "tag-lag",
"支出异常偏快": "tag-overshoot",
"正常": "tag-normal",
"待补录": "tag-gray",
"未到账": "tag-gray",
}
def _esc(value: Any) -> str:
return html.escape(str(value if value is not None else "—"), quote=True)
def _pct(value: Any) -> str:
try:
return f"{float(value) * 100:.1f}%"
except (TypeError, ValueError):
return "—"
def _wan(value: Any) -> str:
try:
return f"{float(value) / 10000:.2f} 万元"
except (TypeError, ValueError):
return "—"
def _bar(label: str, value: Any, fill_class: str) -> str:
"""CSS 柱状图行:纯 div 宽度百分比,iframe 安全无 JS。"""
try:
ratio = max(0.0, min(float(value), 1.0))
width = f"{ratio * 100:.1f}%"
except (TypeError, ValueError):
width = "0%"
return (
f'
{_esc(label)}'
f'
'
f'{_pct(value)}
'
)
def _suggestion_list(suggestions: Any) -> str:
if isinstance(suggestions, str):
try:
suggestions = json.loads(suggestions)
except (TypeError, ValueError):
suggestions = []
if not suggestions:
return '
暂无特别建议。
'
rows = "".join(f"
{_esc(s)}
" for s in suggestions)
return f"{rows}"
def _project_info_table(result: dict[str, Any]) -> str:
fields = (
("项目编号", "project_no"),
("项目类别", "project_type"),
("负责人", "pi_name"),
("所属学院", "department"),
("总立项经费", None),
("已到账金额", None),
("已报销金额", None),
("冻结/暂付款", "frozen_amount"),
("执行期", None),
)
rows = []
for label, key in fields:
if label == "总立项经费":
value = _wan(result.get("total_approved"))
elif label == "已到账金额":
value = _wan(result.get("total_received"))
elif label == "已报销金额":
value = _wan(result.get("total_spent"))
elif label == "执行期":
value = f"{result.get('start_date') or '—'} ~ {result.get('end_date') or '—'}"
else:
value = result.get(key) or "—"
rows.append(f"
{_esc(label)}
{_esc(value)}
")
return f'
{"".join(rows)}
'
def _reminder_section(result: dict[str, Any]) -> str:
"""§10 独立「暂付款 / 结题提醒」章节:取 N1/N2 规则命中明细(kernel 与重建同源 items_json)。"""
reminders = [i for i in (result.get("items") or []) if i.get("rule_id") in ("N1", "N2")]
if not reminders:
return ""
rows = []
for item in reminders:
suggestion = item.get("suggestion") or ""
tail = f' {_esc(suggestion)}' if suggestion else ""
rows.append(f"