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:
341
reports/sign_receipt/setup_db.py
Normal file
341
reports/sign_receipt/setup_db.py
Normal 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) schema(CREATE 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()
|
||||
Reference in New Issue
Block a user