Add sign-off receipt report and refine packing list: single-line info bar, real-glyph row height, auto-paginated detail table

- Packing list: info bar changed to a single-line three-column layout (schedule no. | order no. | box no.); packing date moved to bottom-right on the same row as the total; date separator changed to slash
- Packing list: range column widened 20->30mm; quantity/position-no./remark start shifted right 10mm; remark column narrowed to compensate; total width unchanged
- Packing list: detail cells now use line_spacing=1.0 + valign=top; separators changed to thin solid lines at row bottom (+1mm line spacing); placeholder empty rows no longer step y
- Packing list: row height estimation now uses fpdf2's same simhei glyph measured width (replacing rough character-count estimate); _clean adds control-character cleanup
- New sign-off receipt report reports/sign_receipt: master+detail linked query, ReportBro auto-paginated table, PyMuPDF stamped footer (document no. / page no. / sign-off column)
- run.py: required params now read from each report's config.yaml; default output path auto-generated; added transform.stamp_footer post-processing hook
- .gitignore: ignore .workbuddy/
This commit is contained in:
Misaka_Company
2026-08-04 08:55:09 +08:00
parent 22b764ef19
commit df8f25f6da
11 changed files with 1503 additions and 70 deletions

View File

@@ -0,0 +1,131 @@
"""签收单 20260803001 压力测试数据。
目的:观察当前「单页绝对定位」模板在【内容 > 1 页】时的表现:
* 明细行 > MAX_ROWS(20) 时,超出部分被静默截断(合计仍按全量计)。
* 即便在 20 行内,若累计高度超过内容区,行会越过页脚、与签收栏重叠。
* 发货/收货地址为固定高度(12mm)文本,加长后会换行并压到下一行(电话)上。
可重复运行STRESS_BOXES 为显式列表,运行前先 DELETE 这些箱,再 INSERT
地址直接 UPDATE幂等
运行:
.venv/Scripts/python.exe reports/sign_receipt/add_stress_rows.py
注意:本脚本只动 20260803001重新跑 setup_db.py 会清空 202608030% 测试
数据(含本脚本追加内容),恢复干净状态。
"""
from __future__ import annotations
import sys
from pathlib import Path
# 让脚本能 import core.*
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from core.db import get_engine
from sqlalchemy import text
RECEIPT_NO = "20260803001"
# 显式选取一批真实箱(来自 finished_goods_box单箱明细行数较多确保 > 20 行)。
# 格式:(paichan_no, box_no)
STRESS_BOXES = [
("R07200", 1), ("R06994", 1), ("R06994", 2),
("R06626", 1), ("R06687", 1), ("R06884", 1),
("R05541", 1), ("R06006", 87),
]
LONG_SHIPPER_ADDR = (
"北京市朝阳区南三环成寿寺路甲135号院1号楼 北京布莱迪仪器仪表有限公司 "
"仓储物流中心收发室近地铁成寿寺站B口工作日8:30-17:30可收货"
"节假日及夜间到货请提前与仓管预约货车限高3.2米,卸货区在厂区西北角)"
)
LONG_RECEIVER_ADDR = (
"上海市浦东新区张江高科技园区博云路2号三期厂房B栋5层收货平台 "
"收货联系人王女士电话15230028484请走北门货运通道"
"货车需登记后入厂卸货位B1-07到货前1小时请短信通知"
)
# 静态 VALUES 列表STRESS_BOXES 为硬编码常量,无注入风险)
VALUES_SQL = ", ".join(f"(:p{i}, :b{i})" for i in range(len(STRESS_BOXES)))
PAIR_VALS = {f"p{i}": p for i, (p, _) in enumerate(STRESS_BOXES)} | \
{f"b{i}": b for i, (_, b) in enumerate(STRESS_BOXES)}
def main() -> None:
eng = get_engine()
with eng.begin() as conn:
# 1) 清掉上一轮追加的箱VALUES + EXISTS兼容 SQL Server
conn.execute(
text(
f"DELETE FROM warehouseOutbound.delivery_receipt_item "
f"WHERE receipt_no = :r AND EXISTS ("
f" SELECT 1 FROM (VALUES {VALUES_SQL}) v(pa, bo) "
f" WHERE v.pa = paichan_no AND v.bo = box_no)"
),
{**PAIR_VALS, "r": RECEIPT_NO},
)
# 2) 仅当箱真实存在才插入
exist = {
(p, b) for (p, b) in conn.execute(
text(
f"SELECT v.pa, v.bo FROM (VALUES {VALUES_SQL}) v(pa, bo) "
f"WHERE EXISTS (SELECT 1 FROM CargoTrace.finished_goods_box b "
f" WHERE b.paichan_no = v.pa AND b.box_no = v.bo)"
),
PAIR_VALS,
).fetchall()
}
missing = [pb for pb in STRESS_BOXES if pb not in exist]
if missing:
print(" 警告:以下箱在 finished_goods_box 不存在,已跳过:", missing)
for (p, b) in STRESS_BOXES:
if (p, b) in exist:
conn.execute(
text(
"INSERT INTO warehouseOutbound.delivery_receipt_item "
"(receipt_no, paichan_no, box_no) VALUES (:r, :p, :b)"
),
{"r": RECEIPT_NO, "p": p, "b": b},
)
# 3) 拉长地址(幂等 UPDATE
conn.execute(
text(
"UPDATE warehouseOutbound.delivery_receipt "
"SET shipper_address = :sa, receiver_address = :ra "
"WHERE receipt_no = :r"
),
{"sa": LONG_SHIPPER_ADDR, "ra": LONG_RECEIVER_ADDR, "r": RECEIPT_NO},
)
# 校验
with eng.connect() as conn:
n = conn.execute(
text("SELECT COUNT(*) FROM warehouseOutbound.delivery_receipt_item "
"WHERE receipt_no = :r"), {"r": RECEIPT_NO}).scalar()
rows = conn.execute(
text(
"SELECT COUNT(*) FROM ("
"SELECT bi.id FROM warehouseOutbound.delivery_receipt_item di "
"JOIN CargoTrace.finished_goods_box b ON b.paichan_no=di.paichan_no AND b.box_no=di.box_no "
"JOIN CargoTrace.finished_goods_box_item bi ON bi.box_id=b.id "
"WHERE di.receipt_no = :r) t"
), {"r": RECEIPT_NO}).scalar()
sa = conn.execute(
text("SELECT LEN(shipper_address) FROM warehouseOutbound.delivery_receipt "
"WHERE receipt_no=:r"), {"r": RECEIPT_NO}).scalar()
ra = conn.execute(
text("SELECT LEN(receiver_address) FROM warehouseOutbound.delivery_receipt "
"WHERE receipt_no=:r"), {"r": RECEIPT_NO}).scalar()
print(f" {RECEIPT_NO} 明细箱数={n},预计明细行数(产品粒度)={rows}")
print(f" 发货地址长度={sa}字符,收货地址长度={ra}字符")
print(f" MAX_ROWS=20 → 超出 {max(0, rows - 20)} 行将被截断(合计仍按 {rows} 行计)。")
if __name__ == "__main__":
main()