158 lines
7.6 KiB
Python
158 lines
7.6 KiB
Python
|
|
"""设备出库明细 — 一台设备对应 MOM 的**每一条**出库明细
|
|||
|
|
|
|||
|
|
一行 = 设备上的一条 MOM 出库明细(`trans_outbound` 的一行)。
|
|||
|
|
|
|||
|
|
═══ 为什么是这张表(合并了原先的两张)═══
|
|||
|
|
本表合并了原来的 `product_outbounds`(产品 ↔ 出库**单**,单据级)与
|
|||
|
|
`task_outbound_materials`(任务 ↔ 出库**明细**,明细级)。
|
|||
|
|
|
|||
|
|
它们本来就是**同一件事**——「这台设备对应 MOM 的哪些出库单、领了哪些料」——
|
|||
|
|
却因为粒度不同被拆成两张表、界面上显示成两张卡,用户要面对两个入口、
|
|||
|
|
两个删除按钮,还要猜「我刚才在那边挂的怎么这边看不见」。那是设计失误。
|
|||
|
|
|
|||
|
|
统一到**明细级**,因为只有明细级带 `mom_line_id`(MOM `trans_outbound.id`),
|
|||
|
|
而报废必须靠它定位到具体哪一条出库明细。单据级的信息(申请单号、备注、撤回)
|
|||
|
|
作为**冗余列**落在每一条明细上 —— 同单内必然一致,多存几份换取单表自包含。
|
|||
|
|
|
|||
|
|
═══ 两种来源(source)═══
|
|||
|
|
· `manual` —— 人在界面上挂的(网页端/移动端选 MOM 出库单)
|
|||
|
|
· `webhook` —— MOM 出库回调自动存档(按 SN 匹配到设备后写入)
|
|||
|
|
两者语义不同、删除规则也不同(本表的 `manual` 可删;`webhook` 是系统事实,
|
|||
|
|
要撤得去 MOM 撤回,由回调置 `is_revoked`),所以保留 `source` 区分。
|
|||
|
|
|
|||
|
|
═══ task_id 为什么可空 ═══
|
|||
|
|
人工挂载时要选「挂到哪条任务」(一线按任务领料),webhook 存档则**不知道任务**。
|
|||
|
|
但任务只是**溯源信息**,不再是组织维度 —— 展示、报废、删除一律按**设备**维度走。
|
|||
|
|
"""
|
|||
|
|
import uuid
|
|||
|
|
from datetime import datetime
|
|||
|
|
from decimal import Decimal
|
|||
|
|
|
|||
|
|
from sqlalchemy import (
|
|||
|
|
Boolean, DateTime, ForeignKey, Index, Numeric, String, Text, text,
|
|||
|
|
)
|
|||
|
|
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 ProductOutboundMaterial(Base):
|
|||
|
|
__tablename__ = "product_outbound_materials"
|
|||
|
|
__table_args__ = (
|
|||
|
|
# 同一条 MOM 出库明细不能在一台设备上出现两次(重复提交、前端重放、并发点击、
|
|||
|
|
# webhook 重推)。**部分索引**:mom_line_id 为空的行(MOM 查不到明细的存档)
|
|||
|
|
# 不参与 —— NULL 之间不相等,带上它等于给这类行开了后门。
|
|||
|
|
Index("uq_pom_product_line", "product_id", "mom_line_id",
|
|||
|
|
unique=True, postgresql_where=text("mom_line_id IS NOT NULL")),
|
|||
|
|
# 没有明细行的存档:一张单在一台设备上只留一行
|
|||
|
|
Index("uq_pom_product_no_noline", "product_id", "outbound_no",
|
|||
|
|
unique=True, postgresql_where=text("mom_line_id IS NULL")),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|||
|
|
|
|||
|
|
# ---- 物理外键:设备(组织维度,展示/报废/删除都按它走) ----
|
|||
|
|
product_id: Mapped[uuid.UUID] = mapped_column(
|
|||
|
|
UUID(as_uuid=True), ForeignKey("products.id"),
|
|||
|
|
nullable=False, index=True, comment="所属设备ID",
|
|||
|
|
)
|
|||
|
|
# 冗余序列号:按 SN 对账/排查时不必 join products
|
|||
|
|
serial_number: Mapped[str | None] = mapped_column(
|
|||
|
|
String(16), nullable=True, index=True, comment="设备序列号(冗余,便于按SN对账)",
|
|||
|
|
)
|
|||
|
|
# ---- 物理外键:任务(可空,仅溯源用,不参与展示与权限) ----
|
|||
|
|
task_id: Mapped[uuid.UUID | None] = mapped_column(
|
|||
|
|
UUID(as_uuid=True), ForeignKey("tasks.id"),
|
|||
|
|
nullable=True, index=True,
|
|||
|
|
comment="人工挂载时选的任务(可空,仅溯源用,不参与展示/报废/删除)",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---- 跨库逻辑外键(MOM 库 trans_outbound.id,无物理约束) ----
|
|||
|
|
mom_line_id: Mapped[int | None] = mapped_column(
|
|||
|
|
nullable=True, index=True,
|
|||
|
|
comment="MOM 出库明细行ID(逻辑外键→MOM trans_outbound.id)。为空=MOM 查不到明细的单据存档",
|
|||
|
|
)
|
|||
|
|
outbound_no: Mapped[str] = mapped_column(
|
|||
|
|
String(100), nullable=False, index=True,
|
|||
|
|
comment="MOM 出库单号(批量出库多商品共用,故本表按明细行成行)",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---- 单据级信息(同单内一致,冗余在每条明细上换取单表自包含) ----
|
|||
|
|
request_no: Mapped[str | None] = mapped_column(
|
|||
|
|
String(100), nullable=True, comment="MOM 出库申请单号",
|
|||
|
|
)
|
|||
|
|
applicant_name: Mapped[str | None] = mapped_column(
|
|||
|
|
String(100), nullable=True, comment="申请人姓名(MOM 侧解析后传来,不做 ID 反查)",
|
|||
|
|
)
|
|||
|
|
remark: Mapped[str | None] = mapped_column(
|
|||
|
|
Text, nullable=True, comment="出库单备注",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---- 明细级快照(挂载/回调时从 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 补全时区)",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---- 来源 ----
|
|||
|
|
# manual —— 人在界面上挂的(网页端/移动端选 MOM 出库单)→ **可删**
|
|||
|
|
# webhook —— MOM 出库回调自动存档 → 系统事实,要撤得去 MOM 撤回,**不可删**
|
|||
|
|
source: Mapped[str] = mapped_column(
|
|||
|
|
String(16), nullable=False, default="manual", server_default="manual",
|
|||
|
|
comment="来源: manual(人工挂载,可删) | webhook(MOM回调自动存档,不可删)",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---- 撤回(只置位不删行)----
|
|||
|
|
is_revoked: Mapped[bool] = mapped_column(
|
|||
|
|
Boolean, nullable=False, default=False, server_default="false",
|
|||
|
|
comment="该次出库是否已被 MOM 撤回",
|
|||
|
|
)
|
|||
|
|
revoked_at: Mapped[datetime | None] = mapped_column(
|
|||
|
|
DateTime(timezone=True), nullable=True, comment="撤回时间",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
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="本行写入时间",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---- 关系 ----
|
|||
|
|
product: Mapped["Product"] = relationship("Product", lazy="selectin")
|
|||
|
|
|
|||
|
|
def __repr__(self) -> str:
|
|||
|
|
return (f"<ProductOutboundMaterial {self.outbound_no} "
|
|||
|
|
f"line={self.mom_line_id} sku={self.sku}>")
|