Initial commit

This commit is contained in:
Misaka_Company
2026-07-30 14:17:18 +08:00
commit 4dc563cdd9
16 changed files with 1144 additions and 0 deletions

128
run.py Normal file
View File

@@ -0,0 +1,128 @@
"""装箱单报表生成 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
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/<paichan_no>_box<box_no>.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())