Initial commit
This commit is contained in:
116
reports/packing_list/transform.py
Normal file
116
reports/packing_list/transform.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""装箱单数据装配(纯逻辑,可单测)。
|
||||
|
||||
把 run_query 返回的行(list[dict])转换成 ReportBro 模板所需的数据结构。
|
||||
|
||||
排版策略:弃用 ReportBro 表格元素(其行高不自适应、行不自动堆叠),
|
||||
改为「Python 端逐行计算高度 + 预展开为标量参数」,模板用纯文本元素
|
||||
在精确坐标渲染。本模块负责:
|
||||
- 计算 box 头信息、合计件数
|
||||
- 估算每行明细的高度(依据型号/位号等长字段的字符数),用于模板定位
|
||||
模板与展示逻辑严格分离:合计/日期/行高在此算好,模板只渲染。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
MAX_ROWS = 8 # 模板预留的最大明细行数
|
||||
|
||||
|
||||
class EmptyBoxError(ValueError):
|
||||
"""查询结果为空 —— 该排产号 + 箱号不存在或无明细。"""
|
||||
|
||||
|
||||
def _to_date(value: Any) -> str:
|
||||
"""把 created_at(datetime/iso str)格式化为 YYYY-MM-DD;失败则原样返回。"""
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, datetime):
|
||||
return value.strftime("%Y-%m-%d")
|
||||
if isinstance(value, date):
|
||||
return value.strftime("%Y-%m-%d")
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00")).strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
return value[:10]
|
||||
return str(value)
|
||||
|
||||
|
||||
def _clean(value: Any) -> str:
|
||||
"""None → 空串;其余去除首尾空白。位号/型号等空值统一留白。"""
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _estimate_row_height(row: dict[str, Any]) -> float:
|
||||
"""估算单行明细的高度(mm)。
|
||||
|
||||
长字段(产品名称/型号/位号/备注)会换行,行高取决于换行后最多的行数。
|
||||
按各列宽度与字符宽估算行数,取最大值,每文本行约 4.2mm。
|
||||
列宽与模板 COLS 保持一致(A5 横向)。
|
||||
"""
|
||||
col_caps = { # field: (宽度mm, 约每行字符数)
|
||||
"product_name": (30, 18),
|
||||
"model": (40, 24),
|
||||
"weihao": (24, 16),
|
||||
"remark": (24, 16),
|
||||
"range_": (18, 12),
|
||||
}
|
||||
max_lines = 1
|
||||
for field, (_w, cap) in col_caps.items():
|
||||
text = _clean(row.get(field))
|
||||
if not text:
|
||||
continue
|
||||
# 按容量向上取整;长串(含分隔符)按容量分段
|
||||
lines = max(1, -(-len(text) // cap)) # ceil division
|
||||
max_lines = max(max_lines, lines)
|
||||
# 基础行高 + 每文本行高度
|
||||
return 3.0 + max_lines * 4.2
|
||||
|
||||
|
||||
def build_context(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""把查询行装配成 ReportBro 模板数据。
|
||||
|
||||
:param rows: query.sql 的结果。
|
||||
:return: dict,含箱头标量参数 + 预展开的明细行标量参数(r0_*..r7_*)+
|
||||
每行 y 坐标(row_y0..)+ 合计。
|
||||
:raises EmptyBoxError: rows 为空。
|
||||
"""
|
||||
if not rows:
|
||||
raise EmptyBoxError("找不到该箱:排产号与箱号无装箱明细,请核对参数。")
|
||||
|
||||
first = rows[0]
|
||||
total_qty = sum(int(r.get("box_qty") or 0) for r in rows)
|
||||
|
||||
ctx: dict[str, Any] = {
|
||||
"paichan_no": _clean(first.get("paichan_no")),
|
||||
"box_no": int(first.get("box_no") or 0),
|
||||
"pack_date": _to_date(first.get("created_at")),
|
||||
"order_no": _clean(first.get("order_no")),
|
||||
"total_qty": total_qty,
|
||||
}
|
||||
|
||||
# 预展开明细行:r{i}_{field},并在模板列出的行范围内填值(超出则留空)。
|
||||
# 每行的 y 偏移由模板侧按预估行高累加;此处也输出每行高度供模板使用。
|
||||
row_heights = []
|
||||
for i in range(MAX_ROWS):
|
||||
if i < len(rows):
|
||||
r = rows[i]
|
||||
row_heights.append(_estimate_row_height(r))
|
||||
else:
|
||||
row_heights.append(0.0)
|
||||
src = rows[i] if i < len(rows) else {}
|
||||
ctx[f"r{i}_seq"] = str(i + 1) if i < len(rows) else ""
|
||||
ctx[f"r{i}_product_name"] = _clean(src.get("product_name"))
|
||||
ctx[f"r{i}_model"] = _clean(src.get("model"))
|
||||
ctx[f"r{i}_range_"] = _clean(src.get("range_"))
|
||||
ctx[f"r{i}_qty"] = str(int(src.get("box_qty") or 0)) if i < len(rows) else ""
|
||||
ctx[f"r{i}_weihao"] = _clean(src.get("weihao"))
|
||||
ctx[f"r{i}_remark"] = _clean(src.get("remark"))
|
||||
# 该行是否有数据(模板用 printIf 控制空行不渲染)
|
||||
ctx[f"r{i}_show"] = "1" if i < len(rows) else ""
|
||||
|
||||
ctx["row_heights"] = row_heights
|
||||
return ctx
|
||||
Reference in New Issue
Block a user