"""装箱单报表生成 CLI。 用法: python run.py --report packing_list \ --param paichan_no=R04398 --param box_no=1 \ --output out/R04398_box1.pdf 每个报表位于 reports// 目录下,约定包含: 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 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 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 名称)") # 必需参数校验 required = {"paichan_no", "box_no"} 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)) 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/_box.pdf)") args = parser.parse_args(argv) try: params = parse_params(args.param) output = args.output if not output: pc = params.get("paichan_no", "report") bn = params.get("box_no", 0) output = f"out/{pc}_box{bn}.pdf" 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())