Files
WareShipManifest/reports/packing_list/transform.py
Misaka_Company 4dc563cdd9 Initial commit
2026-07-30 14:17:18 +08:00

117 lines
4.4 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.
"""装箱单数据装配(纯逻辑,可单测)。
把 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_atdatetime/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