Files
WareShipManifest/run.py
Misaka_Company df8f25f6da Add sign-off receipt report and refine packing list: single-line info bar, real-glyph row height, auto-paginated detail table
- Packing list: info bar changed to a single-line three-column layout (schedule no. | order no. | box no.); packing date moved to bottom-right on the same row as the total; date separator changed to slash
- Packing list: range column widened 20->30mm; quantity/position-no./remark start shifted right 10mm; remark column narrowed to compensate; total width unchanged
- Packing list: detail cells now use line_spacing=1.0 + valign=top; separators changed to thin solid lines at row bottom (+1mm line spacing); placeholder empty rows no longer step y
- Packing list: row height estimation now uses fpdf2's same simhei glyph measured width (replacing rough character-count estimate); _clean adds control-character cleanup
- New sign-off receipt report reports/sign_receipt: master+detail linked query, ReportBro auto-paginated table, PyMuPDF stamped footer (document no. / page no. / sign-off column)
- run.py: required params now read from each report's config.yaml; default output path auto-generated; added transform.stamp_footer post-processing hook
- .gitignore: ignore .workbuddy/
2026-08-04 08:55:09 +08:00

164 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""装箱单报表生成 CLI。
用法:
python run.py --report packing_list \
--param paichan_no=R04398 --param box_no=1 \
--output out/R04398_box1.pdf
每个报表位于 reports/<name>/ 目录下,约定包含:
query.sql 参数化查询
transform.py build_context(rows) -> dict模板数据
template.report ReportBro 模板 JSON
报表模块需导出 build_context。本入口负责解析参数 → 查询 → 装配 → 渲染 PDF。
"""
from __future__ import annotations
import argparse
import importlib
import json
import sys
from datetime import datetime
from pathlib import Path
# 确保项目根目录在 sys.path便于 python run.py 直接运行)
PROJECT_ROOT = Path(__file__).resolve().parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from core.db import run_query # noqa: E402
from core.fonts import additional_fonts # noqa: E402
import yaml # noqa: E402
def parse_params(param_strs: list[str]) -> dict:
"""把 ['paichan_no=R04398', 'box_no=1'] 解析为 {'paichan_no':'R04398','box_no':1}。
数值型字符串自动转 int便于 SQL 参数绑定。
"""
params = {}
for item in param_strs or []:
if "=" not in item:
raise ValueError(f"参数格式错误(应为 key=value{item}")
key, value = item.split("=", 1)
key, value = key.strip(), value.strip()
# 尝试转 int箱号等失败保持字符串
try:
params[key] = int(value)
except ValueError:
params[key] = value
return params
def _load_config(report_dir: Path) -> dict | None:
"""读取报表目录下的 config.yaml不存在返回 None"""
cfg_path = report_dir / "config.yaml"
if not cfg_path.exists():
return None
with open(cfg_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def _required_params(report_dir: Path) -> set[str]:
"""从 config.yaml 收集标记为 required 的参数名(兼容各报表)。"""
cfg = _load_config(report_dir)
if not cfg or "params" not in cfg:
return set()
return {
name
for name, spec in cfg["params"].items()
if isinstance(spec, dict) and spec.get("required")
}
def _default_output(report_name: str, params: dict) -> str:
"""根据参数智能生成默认输出路径。"""
if "receipt_no" in params:
return f"out/{params['receipt_no']}.pdf"
if "paichan_no" in params:
bn = params.get("box_no", 0)
return f"out/{params['paichan_no']}_box{bn}.pdf"
return f"out/{report_name}.pdf"
def generate(report_name: str, params: dict, output: str) -> Path:
"""生成指定报表的 PDF。"""
from reportbro import Report # 延迟导入CLI 报错时信息更清晰
report_dir = PROJECT_ROOT / "reports" / report_name
if not report_dir.is_dir():
raise FileNotFoundError(f"报表不存在:{report_dir}(检查 --report 名称)")
# 必需参数校验(从 config.yaml 读取,兼容各报表)
required = _required_params(report_dir)
missing = required - params.keys()
if missing:
raise ValueError(f"缺少必需参数:{', '.join(sorted(missing))}(用 --param key=value 提供)")
# 1. 查询
rows = run_query(report_dir / "query.sql", params)
# 2. 装配模板数据
transform = importlib.import_module(f"reports.{report_name}.transform")
context = transform.build_context(rows)
context["now"] = datetime.now().strftime("%Y-%m-%d %H:%M")
# 3. 渲染:模板元素按数据动态布局(明细行高度自适应)
layout = importlib.import_module(f"reports.{report_name}._build_template")
report_def = layout.build_report_definition(context)
report = Report(
report_definition=report_def,
data=context,
additional_fonts=additional_fonts(),
)
if report.errors:
raise RuntimeError(
f"模板渲染错误({report_name}\n"
+ json.dumps(report.errors, ensure_ascii=False, indent=2, default=str)
)
out_path = Path(output)
if not out_path.is_absolute():
out_path = PROJECT_ROOT / out_path
out_path.parent.mkdir(parents=True, exist_ok=True)
report.generate_pdf(str(out_path))
# 报表级后处理:如报表模块提供 stamp_footer签收单用 PyMuPDF 盖印页脚家具),
# 则在已生成 PDF 上逐页盖印单据号/页码/签收栏等。其它报表无此函数则跳过。
stamp_fn = getattr(transform, "stamp_footer", None)
if stamp_fn is not None:
stamp_fn(str(out_path), context)
return out_path
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="生成装箱单 PDF")
parser.add_argument("--report", default="packing_list", help="报表名(默认 packing_list")
parser.add_argument(
"--param", action="append", default=[],
help="报表参数,格式 key=value可多次指定",
)
parser.add_argument("--output", "-o", help="输出 PDF 路径(默认 out/<paichan_no>_box<box_no>.pdf")
args = parser.parse_args(argv)
try:
params = parse_params(args.param)
output = args.output or _default_output(args.report, params)
out_path = generate(args.report, params, output)
print(f"✅ 已生成:{out_path}")
print(f" 报表:{args.report} 参数:{params}")
return 0
except FileNotFoundError as e:
print(f"{e}", file=sys.stderr)
except ValueError as e:
print(f"{e}", file=sys.stderr)
except Exception as e: # noqa: BLE001
# 找不到箱等业务错误在此给出清晰提示
print(f"❌ 生成失败:{e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())