"""签收单(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()