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

275 lines
12 KiB
Python
Raw Permalink 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.
"""装箱单模板:样式定义 + 文档元素布局。
设计理念:**无边框、单色、字体驱动**的列表式排版。
弃用 ReportBro 表格元素(行高不自适应、行不自动堆叠),改用纯文本元素
+ 细横线手工排版。每行明细的高度由 transform 按字段长度估算,
本模块据此计算各元素的精确 y 坐标 —— 因此 docElements 在运行时按数据动态生成。
坐标系mm。A4 纵向 210 x 297。
"""
from __future__ import annotations
from typing import Any
# ---- 单位说明 ----
# ReportBro 的页面尺寸由 pageFormat=A4 自动换算为 pt210mm→595pt
# 但 docElements 的 x/y/width/height 与 documentProperties 的边距都按 **pt** 直接使用
# (边距不换算)。因此本文件所有「设计尺寸」以 mm 书写便于阅读,输出前用 PT 换算为 pt。
PT = 2.834645669 # 1mm = 2.834645669pt72/25.4
def mm(v: float) -> int:
"""mm → pt取整。所有坐标/尺寸输出前必须经过此换算。"""
return round(v * PT)
# ---- 页面与边距mm设计值----
# A5 横向210mm(宽) × 148mm(高)。ReportBro 用 orientation=landscape + pageFormat=A5。
PAGE_W = 210 # 横向时的宽度
PAGE_H = 148 # 横向时的高度
MARGIN_L = 12
MARGIN_R = 12
CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R # 186mm
CJK = "simhei"
INK = "#1a1a1a" # 主文字(近黑)
MUTE = "#8a8a8a" # 辅助文字/列头(中灰)
FAINT = "#b5b5b5" # 行间细线(浅灰)
RULE = "#1a1a1a" # 区段加重线(与主文字同色)
# ---- 列定义:序号/产品名称/产品型号/量程/数量/位号/备注 ----
# (字段后缀, 表头, x偏移mm, 宽度mm, 对齐)
# 宽度合计 = 186mm。产品名称与型号给宽列其余窄列。
COLS = [
("seq", "序号", 0, 12, "center"),
("product_name", "产品名称", 12, 30, "left"),
("model", "产品型号", 42, 44, "left"),
("range_", "量程", 86, 20, "left"),
("qty", "数量", 106, 14, "right"),
("weihao", "位号", 120, 26, "left"),
("remark", "备注", 146, 40, "left"),
]
# ---------- 样式 ----------
def _debug_border() -> bool:
"""从配置文件读取 debug_border 开关config/settings.yaml → report.debug_border"""
try:
from core.settings import settings
return settings.report.debug_border
except Exception:
return False
def _text_style(id_, *, size=10, bold=False, color=INK, halign="left",
valign="middle", name=None, pad_l=0, pad_r=0, font=CJK,
debug_border=None):
"""文本样式。padding 默认 0 让坐标精确可控;可经 pad_l/pad_r 加左右内边距mm
debug_border 由配置文件 report.debug_border 决定True 时给元素加边框便于核对占位)。
"""
db = _debug_border() if debug_border is None else debug_border
return {
"id": id_, "type": "text", "name": name or f"s{id_}",
"font": font, "fontSize": size, "bold": bold, "italic": False,
"underline": False, "strikethrough": False,
"horizontalAlignment": halign, "verticalAlignment": valign,
"textColor": color, "backgroundColor": "",
"lineSpacing": 1.25,
"paddingLeft": pad_l, "paddingTop": 0, "paddingRight": pad_r, "paddingBottom": 0,
"borderColor": FAINT,
"borderWidth": 0.3 if db else 0,
"borderRadius": 0,
"borderAll": db,
"borderLeft": db, "borderTop": db,
"borderRight": db, "borderBottom": db,
}
def styles() -> list[dict]:
return [
_text_style(101, size=18, bold=True, halign="center", name="title"),
_text_style(102, size=8, color=MUTE, halign="center", name="subtitle"),
_text_style(103, size=8, color=INK, name="info_lbl"),
# 信息条值Bahnschrift 字体。排产号/订单号放大1.5倍(28.5),箱号/装箱日期保持19。
_text_style(104, size=19, bold=True, color=INK, name="info_val", font="bahnschrift"),
_text_style(112, size=28.5, bold=True, color=INK, name="info_val_big", font="bahnschrift"),
_text_style(105, size=9.5, bold=True, color=INK, halign="center", name="colhdr"),
_text_style(106, size=8.5, color=INK, name="cell_l"),
_text_style(107, size=8.5, color=INK, halign="center", name="cell_c"),
_text_style(108, size=8.5, bold=True, color=INK, halign="right", name="cell_r"),
_text_style(109, size=11.5, bold=True, color=INK, halign="center", name="total"),
# 合计数量:居中 + 加大两号(13.5) + 加粗
_text_style(113, size=13.5, bold=True, color=INK, halign="center", name="total_qty"),
# 产品名称/型号:居中 + 左右各 5% 边距按列宽算padding 单位 mm
# product_name 列宽 30mm → 5%≈1.5mmmodel 列宽 44mm → 5%≈2.2mm
_text_style(110, size=8.5, color=INK, halign="center", name="cell_name",
pad_l=1.5, pad_r=1.5),
_text_style(111, size=8.5, color=INK, halign="center", name="cell_model",
pad_l=2.2, pad_r=2.2),
{"id": 201, "type": "line", "name": "rule_faint",
"color": FAINT, "borderWidth": 0.3},
{"id": 202, "type": "line", "name": "rule_strong",
"color": RULE, "borderWidth": 2.5},
]
def document_properties() -> dict:
# 边距按 pt 给出ReportBro 不换算 marginpageFormat=A5 自动换算页面尺寸为 pt
return {
"pageFormat": "A5", "orientation": "landscape",
"marginLeft": mm(MARGIN_L), "marginRight": mm(MARGIN_R),
"marginTop": mm(10), "marginBottom": mm(10),
"headerDisplay": "never", "headerSize": 0,
"footerDisplay": "never", "footerSize": 0,
"patternLocale": "zh", "patternCurrencySymbol": "",
"patternNumberGroupSymbol": "",
}
def parameters() -> list[dict]:
"""模板参数。明细行预展开为 r0_*..r7_* 标量。"""
params = [
{"id": 1, "name": "paichan_no", "type": "string", "nullable": False},
{"id": 2, "name": "box_no", "type": "number", "nullable": False},
{"id": 3, "name": "pack_date", "type": "string", "nullable": False},
{"id": 4, "name": "order_no", "type": "string", "nullable": False},
{"id": 5, "name": "total_qty", "type": "number", "nullable": False},
{"id": 6, "name": "now", "type": "string", "nullable": False},
]
pid = 100
for i in range(8):
for field, _l, _x, _w, _a in COLS:
pid += 1
# 行字段统一用 string空行为 ""、有数据行由 transform 转成字符串,
# 避免 number 参数收到空串报错。
params.append({"id": pid, "name": f"r{i}_{field}", "type": "string", "nullable": True})
# printIf 标记本行是否渲染
params.append({"id": pid + 1, "name": f"r{i}_show", "type": "string", "nullable": True})
return params
# ---------- 元素工厂 ----------
def _text(id_, x, y, w, h, content, *, style_id, print_if=""):
return {
"id": id_, "elementType": "text", "containerId": "0_content",
"x": x, "y": y, "width": w, "height": h,
"content": content, "styleId": style_id, "eval": False,
"printIf": print_if, "removeEmptyElement": False, "alwaysPrintOnSamePage": False,
"link": "", "pattern": "", "cs_condition": "",
"richText": False, "richTextHtml": "",
"spreadsheet_hide": True, "spreadsheet_column": 0,
"spreadsheet_colspan": 1, "spreadsheet_addEmptyRow": False,
}
def _line(id_, x, y, w, *, style_id, weight=0):
"""线条元素。weight=线粗(mm),映射到元素 heightreportbro 线条粗细由 height 决定,
样式的 borderWidth 对线条无效。0 = 细线fpdf 默认最细)。"""
return {
"id": id_, "elementType": "line", "containerId": "0_content",
"x": x, "y": y, "width": w, "height": mm(weight),
"styleId": style_id, "printIf": "", "removeEmptyElement": False,
"spreadsheet_hide": True, "spreadsheet_column": 0,
"spreadsheet_addEmptyRow": False,
}
def build_doc_elements(context: dict[str, Any]) -> list[dict]:
"""按数据动态生成所有文档元素(标题/信息条/列头/明细行/合计/页脚)。
所有坐标/尺寸以 mm 设计书写,输出时统一经 mm() 换算为 pt
ReportBro 元素坐标按 pt 使用,详见文件头单位说明)。
明细行高度取自 context['row_heights'](单位 mm逐行累加 y 坐标。
注意:元素 x/y 是相对内容容器的坐标(容器原点=左边距),故 x 从 0 起,
不要再加左边距(左边距由 reportbro 在渲染时统一偏移)。
"""
L = 0 # 元素相对内容容器左边,从 0 开始
CW = mm(CONTENT_W)
els: list[dict] = []
nid = [2000]
def nid_():
nid[0] += 1
return nid[0]
# 详情数据行各字段样式:默认居中(107);产品名称(110)/产品型号(111)居中+左右5%边距。
# 表头仍用 colhdr 样式 105不受影响。
col_style = {"left": 107, "center": 107, "right": 107}
field_style = {"product_name": 110, "model": 111}
# ===== 标题(紧贴内容区顶部,容器原点已在 marginTop 处)=====
y = mm(1)
els.append(_text(nid_(), L, y, CW, mm(10), "装箱单", style_id=101))
els.append(_line(nid_(), L, y + mm(12), CW, style_id=202))
# ===== 信息条2×2 网格,标签灰 + 值用 Bahnschrift=====
# 行1排产号 | 箱号行2订单号 | 装箱日期(订单号在排产号正下方)
# 排产号、订单号用大号(112, 28.5pt);箱号、装箱日期用普通号(104, 19pt)。
y = y + mm(15)
info = [("排产号", "${paichan_no}", 112), ("箱号", "${box_no}", 104),
("订单号", "${order_no}", 112), ("装箱日期", "${pack_date}", 104)]
col_w = CW / 2
lbl_w = mm(28)
rh = mm(13) # 行高放高,容纳放大后的值字号
for lbl, val, val_style in info:
row = info.index((lbl, val, val_style)) // 2
col = info.index((lbl, val, val_style)) % 2
x = L + col * col_w
yy = y + row * (rh + mm(2))
els.append(_text(nid_(), x, yy, lbl_w, rh, lbl, style_id=103))
els.append(_text(nid_(), x + lbl_w, yy, col_w - lbl_w, rh, val, style_id=val_style))
y = y + 2 * (rh + mm(2)) + mm(2)
els.append(_line(nid_(), L, y, CW, style_id=201))
# ===== 列头 =====
y = y + mm(4)
hdr_h = mm(6)
for _f, label, xoff, w, _a in COLS:
els.append(_text(nid_(), L + mm(xoff), y, mm(w), hdr_h, label, style_id=105))
y = y + hdr_h + mm(2)
els.append(_line(nid_(), L, y, CW, style_id=202, weight=0.45)) # 表头下横线加粗
# ===== 明细行(逐行定位,高度自适应)=====
y = y + mm(2)
row_heights: list[float] = context.get("row_heights", [])
for i in range(8):
h = mm(row_heights[i]) if i < len(row_heights) else 0
show = f"${{r{i}_show}}" # printIf: r{i}_show 为 "1" 才渲染
for field, _label, xoff, w, align in COLS:
els.append(_text(
nid_(), L + mm(xoff), y, mm(w), h,
"${r%d_%s}" % (i, field),
style_id=field_style.get(field, col_style[align]), print_if=show,
))
y += h
# 行间极细线(仅在有数据的行之后)
if i < 7 and row_heights[i] > 0:
els.append(_line(nid_(), L, y + mm(1), CW, style_id=201))
y += mm(2)
# ===== 合计 =====
# 标签框宽度对齐量程列x=86-106宽20数量框与数量列同宽(x=106-120)、居中。
els.append(_line(nid_(), L, y + mm(2), CW, style_id=202, weight=0.45))
y = y + mm(4)
range_x = COLS[3][2] # 量程列起点 86
range_w = COLS[3][3] # 量程列宽 20
qty_x = COLS[4][2] # 数量列起点 106
qty_w = COLS[4][3] # 数量列宽 14
els.append(_text(nid_(), L + mm(range_x), y, mm(range_w), mm(8), "合计", style_id=109))
els.append(_text(nid_(), L + mm(qty_x), y, mm(qty_w), mm(8), "${total_qty}", style_id=113))
return els
def build_report_definition(context: dict[str, Any]) -> dict:
"""组装完整 report_definition运行时调用按数据动态布局"""
return {
"version": 6,
"documentProperties": document_properties(),
"parameters": parameters(),
"styles": styles(),
"docElements": build_doc_elements(context),
}