"""add_task_outbound_materials Revision ID: n1o2p3q4r5s6 Revises: m1n2o3p4q5r6 Create Date: 2026-09-23 任务挂载的 MOM 出库物料(task_outbound_materials) -------------------------------------------------------------------------- 回答「这个任务用了哪些 MOM 出库物料」。此前 Track 里创建任务完全无法关联 出库物料;任务进行中若发现第一次领的料不够,也没有地方追加后续出库单。 口径(已与业务确认): · 选择粒度 = **整张出库单**(outbound_no),该单的明细一并带入 · **纯引用,不记用量** —— 不存"本任务用多少" 为什么是明细级(一行 = MOM trans_outbound 的一行)而不是单据级: MOM 的 trans_outbound 是明细行,物料名/规格要经 COALESCE 三表 JOIN (stock_buy/stock_semi/stock_product → material_base) 才能解析。Track 跨库 无法 JOIN,只存单号的话每次展示都要打 MOM —— MOM 挂了就看不到已挂内容。 存快照后 Track 自包含,与既有 product_outbounds 同一套存档哲学。 成本实测可忽略:517 张单平均 2.64 条明细,65% 是单条明细。 ⚠️ 与 product_outbounds 的区别(容易混淆): product_outbounds 是「MOM 出库回调 → 产品详情只读展示」,挂在 **产品** 上、 单向存档;本表是「人在界面上选择 → 挂到 **任务** 上」、可增可删。 唯一约束 (task_id, mom_line_id): 防同一条出库明细被重复挂到同一任务(重复提交 / 前端重放 / 并发点击)。 mom_line_id 是**跨库逻辑外键**(MOM 库 trans_outbound.id),无物理约束 —— 与 assignee_id 指向 MOM sys_user 同一类做法。MOM 库若重建会让自增 ID 错位, 故同时冗余 outbound_no 供人工核对。 本迁移只建表,不写入任何数据。存量任务没有挂载,任务详情上不会显示物料 —— 预期行为。 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.dialects.postgresql import UUID revision: str = "n1o2p3q4r5s6" down_revision: Union[str, None] = "m1n2o3p4q5r6" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "task_outbound_materials", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False, comment="主键"), sa.Column("task_id", UUID(as_uuid=True), sa.ForeignKey("tasks.id"), nullable=False, comment="所属任务ID"), sa.Column("mom_line_id", sa.Integer(), nullable=False, comment="MOM 出库明细行ID(逻辑外键→MOM trans_outbound.id)"), sa.Column("outbound_no", sa.String(100), nullable=False, comment="MOM 出库单号(批量出库多商品共用,故本表按明细行成行)"), sa.Column("sku", sa.String(100), nullable=True, comment="物料SKU"), sa.Column("material_name", sa.String(255), nullable=True, comment="物料名称(经 COALESCE 三表 JOIN 解析后快照)"), sa.Column("spec_model", sa.String(255), nullable=True, comment="规格型号快照"), sa.Column("quantity", sa.Numeric(19, 4), nullable=True, comment="出库数量(出库单原值,不是本任务用量)"), sa.Column("unit_price", sa.Numeric(19, 2), nullable=True, comment="出库单价"), sa.Column("outbound_type", sa.String(50), nullable=True, comment="出库类型 SALES/USE/PRODUCTION/LOSS/REPAIR(只存不判)"), sa.Column("consumer_name", sa.String(100), nullable=True, comment="领用人/客户(自由填写,非可靠标识)"), sa.Column("operator_name", sa.String(100), nullable=True, comment="MOM 侧操作员"), sa.Column("warehouse_location", sa.String(100), nullable=True, comment="出库库位快照"), sa.Column("outbound_time", sa.DateTime(timezone=True), nullable=True, comment="MOM 记录的出库时间(写入时已按 +08:00 补全时区)"), sa.Column("added_by", sa.String(64), nullable=True, comment="挂载人ID(逻辑外键→MOM sys_user)"), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, comment="挂载时间"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("task_id", "mom_line_id", name="uq_task_outbound_materials_task_line"), comment="任务挂载的 MOM 出库物料 — 一行 = MOM 出库单的一条明细", ) # 任务详情按 task_id 拉已挂物料 op.create_index(op.f("ix_task_outbound_materials_task_id"), "task_outbound_materials", ["task_id"], unique=False) # 按出库单号分组展示 / 反查"这张单被哪些任务用过" op.create_index(op.f("ix_task_outbound_materials_outbound_no"), "task_outbound_materials", ["outbound_no"], unique=False) def downgrade() -> None: op.drop_index(op.f("ix_task_outbound_materials_outbound_no"), table_name="task_outbound_materials") op.drop_index(op.f("ix_task_outbound_materials_task_id"), table_name="task_outbound_materials") op.drop_table("task_outbound_materials")