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,341 @@
"""签收单delivery_receipt建表 + 测试假数据。
用法:
.venv/Scripts/python.exe reports/sign_receipt/setup_db.py
逻辑:
1. 若 warehouseOutbound.delivery_receipt / delivery_receipt_item 不存在则建表
(含 schema、主键、唯一约束、外键级联、按天索引、updated_at 触发器、列备注)。
2. 清理旧的测试签收单receipt_no 以 202608030 开头),再插入:
- 1 张单排产号签收单
- 1 张多排产号签收单
明细引用真实存在的 CargoTrace.finished_goods_box (paichan_no, box_no)。
3. 打印验证结果。
可重复运行:建表幂等(先判断存在),测试数据先删后插。
"""
from __future__ import annotations
import os
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 sqlalchemy import text # noqa: E402
from core.db import get_engine # noqa: E402
ENGINE = get_engine()
SCHEMA = "warehouseOutbound"
# ---------------------------------------------------------------- DDL
DDL_STATEMENTS = [
# 1) schemaCREATE SCHEMA 必须是批首语句,用动态 SQL 包裹以兼容 IF 判断)
"IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'warehouseOutbound') "
"EXEC('CREATE SCHEMA warehouseOutbound')",
# 2) 主表
"""CREATE TABLE warehouseOutbound.delivery_receipt (
receipt_no VARCHAR(11) NOT NULL,
receipt_date DATE NOT NULL,
seq INT NOT NULL,
shipper_name NVARCHAR(200),
shipper_address NVARCHAR(500),
shipper_phone NVARCHAR(50),
receiver_name NVARCHAR(100),
receiver_address NVARCHAR(500),
receiver_phone NVARCHAR(50),
customer_order_no VARCHAR(100),
customer_total_no VARCHAR(50),
transport_mode NVARCHAR(50),
ship_date DATE,
sign_date DATE,
signed_by NVARCHAR(100),
maker NVARCHAR(100),
remark NVARCHAR(1000),
created_at DATETIME2 NOT NULL DEFAULT GETDATE(),
updated_at DATETIME2 NOT NULL DEFAULT GETDATE(),
CONSTRAINT PK_delivery_receipt PRIMARY KEY (receipt_no),
CONSTRAINT UQ_delivery_receipt_date_seq UNIQUE (receipt_date, seq)
)""",
# 3) 明细表(仅最小关联键)
"""CREATE TABLE warehouseOutbound.delivery_receipt_item (
receipt_no VARCHAR(11) NOT NULL,
paichan_no VARCHAR(50) NOT NULL,
box_no INT NOT NULL,
CONSTRAINT PK_delivery_receipt_item PRIMARY KEY (receipt_no, paichan_no, box_no),
CONSTRAINT FK_delivery_receipt_item_receipt FOREIGN KEY (receipt_no)
REFERENCES warehouseOutbound.delivery_receipt (receipt_no) ON DELETE CASCADE
)""",
# 4) 按天查询辅助索引
"CREATE INDEX IX_delivery_receipt_date ON warehouseOutbound.delivery_receipt (receipt_date)",
# 5) updated_at 自动维护触发器
"""CREATE TRIGGER warehouseOutbound.TR_delivery_receipt_updated
ON warehouseOutbound.delivery_receipt AFTER UPDATE AS
BEGIN
UPDATE r SET updated_at = GETDATE()
FROM warehouseOutbound.delivery_receipt r
JOIN inserted i ON i.receipt_no = r.receipt_no;
END""",
]
# 主表列备注:(列名, 备注)
RECEIPT_COL_COMMENTS = [
("receipt_no", "签收单序列号YYYYMMDD+3位当日序号唯一主键"),
("receipt_date", "序列号中的日期部分(冗余,便于按天查询)"),
("seq", "当日顺序号从1起、按天重置、步长1"),
("shipper_name", "发货单位名称"),
("shipper_address", "发货单位地址"),
("shipper_phone", "发货单位联系电话"),
("receiver_name", "收货人姓名"),
("receiver_address", "收货人地址"),
("receiver_phone", "收货人联系电话"),
("customer_order_no", "客户订单编号"),
("customer_total_no", "客户提供的外部总排行号(与内部排产号完全无关)"),
("transport_mode", "运输方式"),
("ship_date", "发货日期"),
("sign_date", "签收日期(客户签收后回写)"),
("signed_by", "签收人(客户签收后回写)"),
("maker", "制单人"),
("remark", "备注"),
("created_at", "记录创建时间"),
("updated_at", "记录最后更新时间(触发器自动维护)"),
]
ITEM_COL_COMMENTS = [
("receipt_no", "签收单序列号(关联主表 delivery_receipt"),
("paichan_no", "内部排产号(用于定位箱子,与客户总排行号无关)"),
("box_no", "箱号在排产号下从1开始"),
]
def _table_exists(conn, name: str) -> bool:
return conn.execute(
text(
"SELECT 1 FROM sys.tables t "
"JOIN sys.schemas s ON s.schema_id = t.schema_id "
"WHERE s.name = :sch AND t.name = :tbl"
),
{"sch": SCHEMA, "tbl": name},
).first() is not None
def _add_col_comment(conn, table: str, col: str, desc: str) -> None:
conn.execute(
text(
"EXEC sys.sp_addextendedproperty "
"N'MS_Description', :desc, "
"N'SCHEMA', N'warehouseOutbound', "
"N'TABLE', :tbl, N'COLUMN', :col"
),
{"desc": desc, "tbl": table, "col": col},
)
def _build_schema(conn) -> None:
print("[1/3] 建表warehouseOutbound schema...")
for stmt in DDL_STATEMENTS:
conn.execute(text(stmt))
if not _table_exists(conn, "delivery_receipt"):
# 理论上上面已建,防御性检查
pass
for col, desc in RECEIPT_COL_COMMENTS:
_add_col_comment(conn, "delivery_receipt", col, desc)
for col, desc in ITEM_COL_COMMENTS:
_add_col_comment(conn, "delivery_receipt_item", col, desc)
print(" 表与列备注已就绪。")
def _probe_boxes(conn):
"""返回可用的 (paichan_no, [box_no,...]) 列表(最多取 3 个排产号)。"""
rows = conn.execute(
text(
"SELECT DISTINCT TOP 3 paichan_no "
"FROM CargoTrace.finished_goods_box ORDER BY paichan_no"
)
).fetchall()
result = []
for (p,) in rows:
boxes = [
r[0]
for r in conn.execute(
text(
"SELECT box_no FROM CargoTrace.finished_goods_box "
"WHERE paichan_no = :p ORDER BY box_no"
),
{"p": p},
).fetchall()
]
if boxes:
result.append((p, boxes))
return result
def _seed(conn, boxes_by_paichan) -> None:
print("[2/3] 写入测试假数据...")
# 清理旧测试数据(幂等)
conn.execute(
text(
"DELETE FROM warehouseOutbound.delivery_receipt_item "
"WHERE receipt_no LIKE '202608030%'"
)
)
conn.execute(
text(
"DELETE FROM warehouseOutbound.delivery_receipt "
"WHERE receipt_no LIKE '202608030%'"
)
)
# 收货/发货方示范信息(仿用户提供的签收单样式)
shipper = {
"shipper_name": "北京布莱迪仪器仪表有限公司",
"shipper_address": "北京市朝阳区南三环成寿寺路甲135号",
"shipper_phone": "010-67690053",
"transport_mode": "汽运",
"maker": "王制单",
}
if not boxes_by_paichan:
print(" !! 警告CargoTrace.finished_goods_box 无数据,"
"将使用合成 (paichan_no, box_no) 作为明细引用(报表 JOIN 可能无产品明细)。")
boxes_by_paichan = [("R09999", [1, 2])]
# ---- 签收单 1单排产号 ----
p1, boxes1 = boxes_by_paichan[0]
item_boxes_1 = boxes1[:3] # 最多 3 箱
conn.execute(
text(
"""INSERT INTO warehouseOutbound.delivery_receipt
(receipt_no, receipt_date, seq, shipper_name, shipper_address,
shipper_phone, receiver_name, receiver_address, receiver_phone,
customer_order_no, customer_total_no, transport_mode, ship_date,
maker, remark)
VALUES
(:receipt_no, :receipt_date, :seq, :shipper_name, :shipper_address,
:shipper_phone, :receiver_name, :receiver_address, :receiver_phone,
:customer_order_no, :customer_total_no, :transport_mode, :ship_date,
:maker, :remark)"""
),
{
"receipt_no": "20260803001",
"receipt_date": "2026-08-03",
"seq": 1,
**shipper,
"receiver_name": "李家",
"receiver_address": "北京市昌平区南口地区李流路三一产业园三一重能1号厂房D1",
"receiver_phone": "15230028484",
"customer_order_no": "123321",
"customer_total_no": "202589472",
"ship_date": "2025-12-16",
"remark": "单排产号示例",
},
)
for b in item_boxes_1:
conn.execute(
text(
"INSERT INTO warehouseOutbound.delivery_receipt_item "
"(receipt_no, paichan_no, box_no) VALUES (:r, :p, :b)"
),
{"r": "20260803001", "p": p1, "b": b},
)
# ---- 签收单 2多排产号 ----
if len(boxes_by_paichan) > 1:
p2, boxes2 = boxes_by_paichan[1]
else:
p2, boxes2 = p1, boxes1
box2 = boxes2[0]
conn.execute(
text(
"""INSERT INTO warehouseOutbound.delivery_receipt
(receipt_no, receipt_date, seq, shipper_name, shipper_address,
shipper_phone, receiver_name, receiver_address, receiver_phone,
customer_order_no, customer_total_no, transport_mode, ship_date,
maker, remark)
VALUES
(:receipt_no, :receipt_date, :seq, :shipper_name, :shipper_address,
:shipper_phone, :receiver_name, :receiver_address, :receiver_phone,
:customer_order_no, :customer_total_no, :transport_mode, :ship_date,
:maker, :remark)"""
),
{
"receipt_no": "20260803002",
"receipt_date": "2026-08-03",
"seq": 2,
**shipper,
"receiver_name": "张收货",
"receiver_address": "上海市浦东新区张江高科技园区博云路2号",
"receiver_phone": "13800001111",
"customer_order_no": "PO-2026-7788",
"customer_total_no": "202589999",
"ship_date": "2026-08-02",
"remark": "多排产号示例(含两个排产号)",
},
)
# 第一个排产号的第 1 箱
conn.execute(
text(
"INSERT INTO warehouseOutbound.delivery_receipt_item "
"(receipt_no, paichan_no, box_no) VALUES (:r, :p, :b)"
),
{"r": "20260803002", "p": p1, "b": boxes1[0]},
)
# 第二个排产号的第 1 箱(若与第一个相同则跳过重复主键)
if (p2, box2) != (p1, boxes1[0]):
conn.execute(
text(
"INSERT INTO warehouseOutbound.delivery_receipt_item "
"(receipt_no, paichan_no, box_no) VALUES (:r, :p, :b)"
),
{"r": "20260803002", "p": p2, "b": box2},
)
print(f" 已插入 2 张签收单;明细引用排产号: "
f"{[p for p, _ in boxes_by_paichan][:2]}")
def _verify(conn) -> None:
print("[3/3] 验证...")
heads = conn.execute(
text(
"SELECT receipt_no, seq, receiver_name, customer_total_no "
"FROM warehouseOutbound.delivery_receipt ORDER BY receipt_no"
)
).fetchall()
for r in heads:
items = conn.execute(
text(
"SELECT paichan_no, box_no FROM warehouseOutbound.delivery_receipt_item "
"WHERE receipt_no = :r ORDER BY paichan_no, box_no"
),
{"r": r[0]},
).fetchall()
print(f" {r[0]} | seq={r[1]} | 收货人={r[2]} | 客户总排行号={r[3]}"
f" | 明细 {len(items)} 箱: {[(p, b) for p, b in items]}")
def main() -> None:
with ENGINE.begin() as conn:
need_build = not _table_exists(conn, "delivery_receipt")
if need_build:
_build_schema(conn)
else:
print("[1/3] 表已存在,跳过建表。")
boxes_by_paichan = _probe_boxes(conn)
_seed(conn, boxes_by_paichan)
_verify(conn)
print("完成。")
if __name__ == "__main__":
main()