Files
track/backend/app/models/task_outbound_material.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

104 lines
4.6 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.

"""任务挂载的 MOM 出库物料 — 明细级快照
回答「这个任务用了哪些 MOM 出库物料」。口径已与业务确认:
· 选择粒度 = **整张出库单**(outbound_no),该单据的明细一并带入
—— 不存在"单里混了无关物料"的情况
· **纯引用,不记用量**:quantity 是出库单原值,不是"本任务用了多少"
为什么存明细级快照,而不是只存 outbound_no 现查:
MOM 的 trans_outbound 是**明细行**(一行 = 一条物料),物料名/规格要经
COALESCE 三表 JOIN(stock_buy/stock_semi/stock_product → material_base)
才能解析出来。Track 跨库无法 JOIN,若只存单号,每次展示都要打 MOM ——
MOM 挂了就看不到已挂内容。存快照后 Track 自包含,与既有的
product_outbounds 是同一套存档哲学。
成本实测可忽略:517 张单平均 2.64 条明细,65% 是单条明细。
⚠️ mom_line_id 是**跨库逻辑外键**(指向 MOM 库 trans_outbound.id),无物理
约束 —— 与 assignee_id 指向 MOM sys_user 是同一类做法。MOM 库若重建会让
自增 ID 错位,所以同时冗余 outbound_no 供人工核对。
"""
import uuid
from datetime import datetime
from decimal import Decimal
from sqlalchemy import DateTime, ForeignKey, Numeric, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
from app.core.time_utils import get_beijing_time
class TaskOutboundMaterial(Base):
__tablename__ = "task_outbound_materials"
# 防同一条出库明细被重复挂到同一任务(重复提交、前端重放、并发点击)
__table_args__ = (
UniqueConstraint("task_id", "mom_line_id",
name="uq_task_outbound_materials_task_line"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
# ---- 物理外键(关联本库 tasks) ----
task_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("tasks.id"),
nullable=False, index=True, comment="所属任务ID",
)
# ---- 跨库逻辑外键(MOM 库 trans_outbound.id,无物理约束) ----
mom_line_id: Mapped[int] = mapped_column(
nullable=False, comment="MOM 出库明细行ID(逻辑外键→MOM trans_outbound.id)",
)
outbound_no: Mapped[str] = mapped_column(
String(100), nullable=False, index=True,
comment="MOM 出库单号(批量出库多商品共用,故本表按明细行成行)",
)
# ---- MOM 物料/单据快照(挂载时从 MOM 拉取,之后 Track 自包含) ----
sku: Mapped[str | None] = mapped_column(
String(100), nullable=True, comment="物料SKU(MOM trans_outbound.sku 快照)",
)
material_name: Mapped[str | None] = mapped_column(
String(255), nullable=True, comment="物料名称(经 COALESCE 三表 JOIN 解析后快照)",
)
spec_model: Mapped[str | None] = mapped_column(
String(255), nullable=True, comment="规格型号快照",
)
quantity: Mapped[Decimal | None] = mapped_column(
Numeric(19, 4), nullable=True,
comment="出库数量(出库单原值,**不是**本任务用量)",
)
unit_price: Mapped[Decimal | None] = mapped_column(
Numeric(19, 2), nullable=True, comment="出库单价",
)
outbound_type: Mapped[str | None] = mapped_column(
String(50), nullable=True,
comment="出库类型 SALES/USE/PRODUCTION/LOSS/REPAIR(MOM 码表未冻结,只存不判)",
)
consumer_name: Mapped[str | None] = mapped_column(
String(100), nullable=True, comment="领用人/客户(MOM 侧自由填写,非可靠标识)",
)
operator_name: Mapped[str | None] = mapped_column(
String(100), nullable=True, comment="MOM 侧操作员",
)
warehouse_location: Mapped[str | None] = mapped_column(
String(100), nullable=True, comment="出库库位快照",
)
outbound_time: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True,
comment="MOM 记录的出库时间(写入时已按 +08:00 补全时区)",
)
added_by: Mapped[str | None] = mapped_column(
String(64), nullable=True, comment="挂载人ID(逻辑外键→MOM sys_user)",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=get_beijing_time, comment="挂载时间",
)
# ---- 关系 ----
task: Mapped["Task"] = relationship("Task", back_populates="outbound_materials")
def __repr__(self) -> str:
return f"<TaskOutboundMaterial {self.outbound_no} sku={self.sku}>"