Files
track/backend/alembic/versions/n1o2p3q4r5s6_add_task_outbound_materials.py
duxingchen e45c97bd1f feat: 组织隔离(IRIS 单实例)与出料功能基础
本轮之前累积的未提交工作,一并固化:

- 组织隔离:同一份代码部署给不同部门只需改 config 的 ORG_DEPARTMENT 与
  MATERIAL_CATEGORY_PREFIX。过滤点在登录/人员列表/物料/MOM 出库单四处,
  全部服务端钉死,客户端传什么都放不大。
  ★ 物料必须用**前缀** LIKE,不能反推成 ILIKE '%IRIS%':MOM 里 LICA 的物料是
  `LICA/<中文>`,而本部门分类树里另有 `IRIS/成品/LICA/…`(本就属于本部门),
  前缀匹配天然区分得开。
- MOM 出库单只读查询(直连 MOM 库):不走 MOM 现成的 /outbound 接口 ——
  那个要 JWT + permission_required,且对非特权账号按 consumer_name 做行级
  隔离,服务账号只能拿到自己名下的单。分页必须两段式(先按单号 GROUP BY
  分页,再 IN 捞明细),对宽表直接分页会得到明细行数而不是单据数。
- 出料功能:产品 ↔ 出库单存档(product_outbounds)与任务 ↔ 出库明细
  (task_outbound_materials),供「这台设备对应 MOM 哪张单」的展示。
  ⚠️ 快照一律由后端拿 ID 去 MOM 现查,不接受前端传入,否则前端可伪造单据。
2026-09-23 15:18:06 +08:00

100 lines
5.3 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.

"""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")