Files
WareShipManifest/reports/sign_receipt/setup_db.py
Misaka_Company df8f25f6da 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/
2026-08-04 08:55:09 +08:00

342 lines
12 KiB
Python
Raw 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.
"""签收单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()