"""装箱单模板:样式定义 + 文档元素布局。 设计理念:**无边框、单色、字体驱动**的列表式排版。 弃用 ReportBro 表格元素(行高不自适应、行不自动堆叠),改用纯文本元素 + 细横线手工排版。每行明细的高度由 transform 按字段长度估算, 本模块据此计算各元素的精确 y 坐标 —— 因此 docElements 在运行时按数据动态生成。 坐标系:mm。A4 纵向 210 x 297。 """ from __future__ import annotations from typing import Any # ---- 单位说明 ---- # ReportBro 的页面尺寸由 pageFormat=A4 自动换算为 pt(210mm→595pt), # 但 docElements 的 x/y/width/height 与 documentProperties 的边距都按 **pt** 直接使用 # (边距不换算)。因此本文件所有「设计尺寸」以 mm 书写便于阅读,输出前用 PT 换算为 pt。 PT = 2.834645669 # 1mm = 2.834645669pt(72/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.5mm;model 列宽 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 不换算 margin;pageFormat=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),映射到元素 height(reportbro 线条粗细由 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), }