功能:新增托盘业务数据模型
This commit is contained in:
@@ -0,0 +1,233 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
CHAR,
|
||||||
|
BigInteger,
|
||||||
|
CheckConstraint,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
from config.env import DataBaseConfig
|
||||||
|
from utils.common_util import SqlalchemyUtil
|
||||||
|
|
||||||
|
|
||||||
|
class BizStrain(Base):
|
||||||
|
"""菌种主数据表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_strain'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("status IN ('0', '1')", name='ck_biz_strain_status'),
|
||||||
|
{'comment': '菌种主数据表'},
|
||||||
|
)
|
||||||
|
|
||||||
|
strain_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='菌种ID')
|
||||||
|
strain_code = Column(String(64), nullable=False, unique=True, comment='菌种编码')
|
||||||
|
strain_name = Column(String(100), nullable=False, comment='菌种名称')
|
||||||
|
status = Column(CHAR(1), nullable=False, server_default='0', comment='状态(0正常 1停用)')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizWarehouse(Base):
|
||||||
|
"""仓库主数据表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_warehouse'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("status IN ('0', '1')", name='ck_biz_warehouse_status'),
|
||||||
|
{'comment': '仓库主数据表'},
|
||||||
|
)
|
||||||
|
|
||||||
|
warehouse_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='仓库ID')
|
||||||
|
warehouse_code = Column(String(64), nullable=False, unique=True, comment='仓库编码')
|
||||||
|
warehouse_name = Column(String(100), nullable=False, comment='仓库名称')
|
||||||
|
address = Column(String(500), nullable=True, comment='仓库地址')
|
||||||
|
status = Column(CHAR(1), nullable=False, server_default='0', comment='状态(0正常 1停用)')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizCustomer(Base):
|
||||||
|
"""客户主数据表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_customer'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("status IN ('0', '1')", name='ck_biz_customer_status'),
|
||||||
|
{'comment': '客户主数据表'},
|
||||||
|
)
|
||||||
|
|
||||||
|
customer_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='客户ID')
|
||||||
|
customer_code = Column(String(64), nullable=False, unique=True, comment='客户编码')
|
||||||
|
customer_name = Column(String(100), nullable=False, comment='客户名称')
|
||||||
|
status = Column(CHAR(1), nullable=False, server_default='0', comment='状态(0正常 1停用)')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizCustomerSite(Base):
|
||||||
|
"""客户现场主数据表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_customer_site'
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('customer_id', 'site_code', name='uq_biz_customer_site_customer_code'),
|
||||||
|
CheckConstraint("status IN ('0', '1')", name='ck_biz_customer_site_status'),
|
||||||
|
{'comment': '客户现场主数据表'},
|
||||||
|
)
|
||||||
|
|
||||||
|
site_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='现场ID')
|
||||||
|
customer_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer.customer_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='客户ID',
|
||||||
|
)
|
||||||
|
site_code = Column(String(64), nullable=False, comment='现场编码')
|
||||||
|
site_name = Column(String(100), nullable=False, comment='现场名称')
|
||||||
|
address = Column(String(500), nullable=True, comment='现场地址')
|
||||||
|
status = Column(CHAR(1), nullable=False, server_default='0', comment='状态(0正常 1停用)')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizProductionLine(Base):
|
||||||
|
"""客户现场产线主数据表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_production_line'
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('site_id', 'line_code', name='uq_biz_production_line_site_code'),
|
||||||
|
CheckConstraint("status IN ('0', '1')", name='ck_biz_production_line_status'),
|
||||||
|
{'comment': '客户现场产线主数据表'},
|
||||||
|
)
|
||||||
|
|
||||||
|
line_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='产线ID')
|
||||||
|
site_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer_site.site_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='现场ID',
|
||||||
|
)
|
||||||
|
line_code = Column(String(64), nullable=False, comment='产线编码')
|
||||||
|
line_name = Column(String(100), nullable=False, comment='产线名称')
|
||||||
|
status = Column(CHAR(1), nullable=False, server_default='0', comment='状态(0正常 1停用)')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizPallet(Base):
|
||||||
|
"""托盘资产主数据表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_pallet'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint('capacity_packages > 0', name='ck_biz_pallet_capacity_packages'),
|
||||||
|
CheckConstraint("asset_status IN ('0', '1')", name='ck_biz_pallet_asset_status'),
|
||||||
|
{'comment': '托盘资产主数据表'},
|
||||||
|
)
|
||||||
|
|
||||||
|
pallet_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='托盘ID')
|
||||||
|
pallet_code = Column(String(64), nullable=False, unique=True, comment='托盘编号')
|
||||||
|
qr_token = Column(String(128), nullable=False, unique=True, comment='二维码固定标识')
|
||||||
|
capacity_packages = Column(Integer, nullable=False, comment='容量包数')
|
||||||
|
asset_status = Column(CHAR(1), nullable=False, server_default='0', comment='资产状态(0正常 1停用)')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizUserWarehouse(Base):
|
||||||
|
"""用户与仓库关联表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_user_warehouse'
|
||||||
|
__table_args__ = {'comment': '用户与仓库关联表'}
|
||||||
|
|
||||||
|
user_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('sys_user.user_id'),
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
comment='用户ID',
|
||||||
|
)
|
||||||
|
warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
comment='仓库ID',
|
||||||
|
)
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
|
||||||
|
|
||||||
|
class BizUserSite(Base):
|
||||||
|
"""用户与客户现场关联表。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_user_site'
|
||||||
|
__table_args__ = {'comment': '用户与客户现场关联表'}
|
||||||
|
|
||||||
|
user_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('sys_user.user_id'),
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
comment='用户ID',
|
||||||
|
)
|
||||||
|
site_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer_site.site_id'),
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
comment='现场ID',
|
||||||
|
)
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, CheckConstraint, Column, DateTime, ForeignKey, Integer, String, UniqueConstraint
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
from config.env import DataBaseConfig
|
||||||
|
from utils.common_util import SqlalchemyUtil
|
||||||
|
|
||||||
|
|
||||||
|
class BizInboundOrder(Base):
|
||||||
|
"""首次入库单。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_inbound_order'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("status IN ('DRAFT', 'CONFIRMED')", name='ck_biz_inbound_order_status'),
|
||||||
|
{'comment': '首次入库单'},
|
||||||
|
)
|
||||||
|
|
||||||
|
inbound_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='入库单ID')
|
||||||
|
inbound_no = Column(String(64), nullable=False, unique=True, comment='入库单号')
|
||||||
|
warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='仓库ID',
|
||||||
|
)
|
||||||
|
status = Column(String(32), nullable=False, server_default='DRAFT', comment='单据状态')
|
||||||
|
confirm_idempotency_key = Column(String(64), nullable=True, unique=True, comment='确认幂等键')
|
||||||
|
confirm_request_hash = Column(String(64), nullable=True, comment='确认请求摘要')
|
||||||
|
confirmed_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=True, comment='确认人ID')
|
||||||
|
confirmed_at = Column(DateTime, nullable=True, comment='确认时间')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizInboundItem(Base):
|
||||||
|
"""首次入库单托盘明细。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_inbound_item'
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('inbound_id', 'pallet_id', name='uq_biz_inbound_item_order_pallet'),
|
||||||
|
{'comment': '首次入库单托盘明细'},
|
||||||
|
)
|
||||||
|
|
||||||
|
inbound_item_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment='入库明细ID',
|
||||||
|
)
|
||||||
|
inbound_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_inbound_order.inbound_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='入库单ID',
|
||||||
|
)
|
||||||
|
pallet_id = Column(BigInteger, ForeignKey('biz_pallet.pallet_id'), nullable=False, comment='托盘ID')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
|
||||||
|
|
||||||
|
class BizFillingRecord(Base):
|
||||||
|
"""托盘装填记录。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_filling_record'
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('pallet_id', 'active_slot', name='uq_biz_filling_record_pallet_active_slot'),
|
||||||
|
CheckConstraint('initial_packages > 0', name='ck_biz_filling_record_initial_packages'),
|
||||||
|
CheckConstraint(
|
||||||
|
"active_slot IS NULL OR active_slot = 'ACTIVE'",
|
||||||
|
name='ck_biz_filling_record_active_slot',
|
||||||
|
),
|
||||||
|
{'comment': '托盘装填记录'},
|
||||||
|
)
|
||||||
|
|
||||||
|
filling_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='装填记录ID')
|
||||||
|
filling_no = Column(String(64), nullable=False, unique=True, comment='装填单号')
|
||||||
|
pallet_id = Column(BigInteger, ForeignKey('biz_pallet.pallet_id'), nullable=False, comment='托盘ID')
|
||||||
|
warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='仓库ID',
|
||||||
|
)
|
||||||
|
strain_id = Column(BigInteger, ForeignKey('biz_strain.strain_id'), nullable=False, comment='菌种ID')
|
||||||
|
strain_code_snapshot = Column(String(64), nullable=False, comment='菌种编码快照')
|
||||||
|
strain_name_snapshot = Column(String(100), nullable=False, comment='菌种名称快照')
|
||||||
|
batch_no = Column(String(64), nullable=False, comment='生产批次')
|
||||||
|
package_spec = Column(String(64), nullable=False, comment='包装规格')
|
||||||
|
initial_packages = Column(Integer, nullable=False, comment='初始包数')
|
||||||
|
active_slot = Column(String(16), nullable=True, comment='活动装填槽')
|
||||||
|
closed_at = Column(DateTime, nullable=True, comment='装填周期关闭时间')
|
||||||
|
idempotency_key = Column(String(64), nullable=False, unique=True, comment='幂等键')
|
||||||
|
request_hash = Column(String(64), nullable=False, comment='请求摘要')
|
||||||
|
operator_id = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=False, comment='操作人ID')
|
||||||
|
operated_at = Column(DateTime, nullable=False, comment='操作时间')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizShipmentOrder(Base):
|
||||||
|
"""整托发货单。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_shipment_order'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("status IN ('DRAFT', 'CONFIRMED')", name='ck_biz_shipment_order_status'),
|
||||||
|
{'comment': '整托发货单'},
|
||||||
|
)
|
||||||
|
|
||||||
|
shipment_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='发货单ID')
|
||||||
|
shipment_no = Column(String(64), nullable=False, unique=True, comment='发货单号')
|
||||||
|
source_warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='来源仓库ID',
|
||||||
|
)
|
||||||
|
target_site_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer_site.site_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='目标现场ID',
|
||||||
|
)
|
||||||
|
status = Column(String(32), nullable=False, server_default='DRAFT', comment='单据状态')
|
||||||
|
confirm_idempotency_key = Column(String(64), nullable=True, unique=True, comment='确认幂等键')
|
||||||
|
confirm_request_hash = Column(String(64), nullable=True, comment='确认请求摘要')
|
||||||
|
confirmed_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=True, comment='确认人ID')
|
||||||
|
confirmed_at = Column(DateTime, nullable=True, comment='确认时间')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizShipmentItem(Base):
|
||||||
|
"""整托发货明细。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_shipment_item'
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('shipment_id', 'pallet_id', name='uq_biz_shipment_item_order_pallet'),
|
||||||
|
CheckConstraint(
|
||||||
|
'package_qty IS NULL OR package_qty > 0',
|
||||||
|
name='ck_biz_shipment_item_package_qty',
|
||||||
|
),
|
||||||
|
{'comment': '整托发货明细'},
|
||||||
|
)
|
||||||
|
|
||||||
|
shipment_item_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment='发货明细ID',
|
||||||
|
)
|
||||||
|
shipment_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_shipment_order.shipment_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='发货单ID',
|
||||||
|
)
|
||||||
|
pallet_id = Column(BigInteger, ForeignKey('biz_pallet.pallet_id'), nullable=False, comment='托盘ID')
|
||||||
|
active_filling_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_filling_record.filling_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='确认时活动装填记录ID',
|
||||||
|
)
|
||||||
|
package_qty = Column(Integer, nullable=True, comment='确认时包数快照')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
|
||||||
|
|
||||||
|
class BizUsageRecord(Base):
|
||||||
|
"""客户现场使用事实记录。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_usage_record'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint('used_packages > 0', name='ck_biz_usage_record_used_packages'),
|
||||||
|
CheckConstraint(
|
||||||
|
'remaining_packages_after >= 0',
|
||||||
|
name='ck_biz_usage_record_remaining_packages_after',
|
||||||
|
),
|
||||||
|
{'comment': '客户现场使用事实记录'},
|
||||||
|
)
|
||||||
|
|
||||||
|
usage_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='使用记录ID')
|
||||||
|
usage_no = Column(String(64), nullable=False, unique=True, comment='使用记录号')
|
||||||
|
pallet_id = Column(BigInteger, ForeignKey('biz_pallet.pallet_id'), nullable=False, comment='托盘ID')
|
||||||
|
active_filling_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_filling_record.filling_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='活动装填记录ID',
|
||||||
|
)
|
||||||
|
site_id = Column(BigInteger, ForeignKey('biz_customer_site.site_id'), nullable=False, comment='现场ID')
|
||||||
|
line_id = Column(BigInteger, ForeignKey('biz_production_line.line_id'), nullable=False, comment='产线ID')
|
||||||
|
used_packages = Column(Integer, nullable=False, comment='本次使用包数')
|
||||||
|
remaining_packages_after = Column(Integer, nullable=False, comment='使用后剩余包数')
|
||||||
|
idempotency_key = Column(String(64), nullable=False, unique=True, comment='幂等键')
|
||||||
|
request_hash = Column(String(64), nullable=False, comment='请求摘要')
|
||||||
|
operator_id = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=False, comment='操作人ID')
|
||||||
|
occurred_at = Column(DateTime, nullable=False, comment='发生时间')
|
||||||
|
|
||||||
|
|
||||||
|
class BizReturnOrder(Base):
|
||||||
|
"""托盘退回单。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_return_order'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("status IN ('REQUESTED', 'CONFIRMED')", name='ck_biz_return_order_status'),
|
||||||
|
{'comment': '托盘退回单'},
|
||||||
|
)
|
||||||
|
|
||||||
|
return_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='退回单ID')
|
||||||
|
return_no = Column(String(64), nullable=False, unique=True, comment='退回单号')
|
||||||
|
source_site_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer_site.site_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='来源现场ID',
|
||||||
|
)
|
||||||
|
target_warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='目标仓库ID',
|
||||||
|
)
|
||||||
|
status = Column(String(32), nullable=False, server_default='REQUESTED', comment='退回状态')
|
||||||
|
request_idempotency_key = Column(String(64), nullable=False, unique=True, comment='申请幂等键')
|
||||||
|
request_hash = Column(String(64), nullable=False, comment='申请请求摘要')
|
||||||
|
confirm_idempotency_key = Column(String(64), nullable=True, unique=True, comment='确认幂等键')
|
||||||
|
confirm_request_hash = Column(String(64), nullable=True, comment='确认请求摘要')
|
||||||
|
requested_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=False, comment='申请人ID')
|
||||||
|
requested_at = Column(DateTime, nullable=False, comment='申请时间')
|
||||||
|
confirmed_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=True, comment='确认人ID')
|
||||||
|
confirmed_at = Column(DateTime, nullable=True, comment='确认时间')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
|
||||||
|
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
|
||||||
|
remark = Column(
|
||||||
|
String(500),
|
||||||
|
nullable=True,
|
||||||
|
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
|
||||||
|
comment='备注',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BizReturnItem(Base):
|
||||||
|
"""托盘退回明细。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_return_item'
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('return_id', 'pallet_id', name='uq_biz_return_item_order_pallet'),
|
||||||
|
CheckConstraint(
|
||||||
|
'remaining_packages_snapshot >= 0',
|
||||||
|
name='ck_biz_return_item_remaining_packages_snapshot',
|
||||||
|
),
|
||||||
|
{'comment': '托盘退回明细'},
|
||||||
|
)
|
||||||
|
|
||||||
|
return_item_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment='退回明细ID',
|
||||||
|
)
|
||||||
|
return_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_return_order.return_id'),
|
||||||
|
nullable=False,
|
||||||
|
comment='退回单ID',
|
||||||
|
)
|
||||||
|
pallet_id = Column(BigInteger, ForeignKey('biz_pallet.pallet_id'), nullable=False, comment='托盘ID')
|
||||||
|
active_filling_id_snapshot = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_filling_record.filling_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='申请时活动装填记录快照',
|
||||||
|
)
|
||||||
|
remaining_packages_snapshot = Column(Integer, nullable=False, comment='申请时剩余包数快照')
|
||||||
|
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
|
||||||
|
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
|
||||||
|
|
||||||
|
|
||||||
|
class BizPalletInventory(Base):
|
||||||
|
"""托盘当前库存、位置和流转状态。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_pallet_inventory'
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(
|
||||||
|
"flow_state IN ('UNSTOCKED', 'EMPTY_IN_WAREHOUSE', 'FILLED_IN_WAREHOUSE', "
|
||||||
|
"'AT_SITE_UNUSED', 'AT_SITE_PARTIAL', 'AT_SITE_EMPTY', 'RETURNING')",
|
||||||
|
name='ck_biz_pallet_inventory_flow_state',
|
||||||
|
),
|
||||||
|
CheckConstraint('remaining_packages >= 0', name='ck_biz_pallet_inventory_remaining_packages'),
|
||||||
|
CheckConstraint('version >= 0', name='ck_biz_pallet_inventory_version'),
|
||||||
|
CheckConstraint(
|
||||||
|
'NOT (warehouse_id IS NOT NULL AND site_id IS NOT NULL)',
|
||||||
|
name='ck_biz_pallet_inventory_location',
|
||||||
|
),
|
||||||
|
{'comment': '托盘当前库存、位置和流转状态'},
|
||||||
|
)
|
||||||
|
|
||||||
|
pallet_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_pallet.pallet_id'),
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=False,
|
||||||
|
comment='托盘ID',
|
||||||
|
)
|
||||||
|
active_filling_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_filling_record.filling_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='当前活动装填记录ID',
|
||||||
|
)
|
||||||
|
warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='当前仓库ID',
|
||||||
|
)
|
||||||
|
site_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer_site.site_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='当前现场ID',
|
||||||
|
)
|
||||||
|
current_line_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_production_line.line_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='当前驻场周期绑定产线ID',
|
||||||
|
)
|
||||||
|
flow_state = Column(String(32), nullable=False, server_default='UNSTOCKED', comment='托盘流转状态')
|
||||||
|
remaining_packages = Column(Integer, nullable=False, server_default='0', comment='当前剩余包数')
|
||||||
|
last_event_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_pallet_event.event_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='最后事件ID',
|
||||||
|
)
|
||||||
|
version = Column(Integer, nullable=False, server_default='0', comment='并发版本号')
|
||||||
|
updated_at = Column(DateTime, nullable=False, default=datetime.now(), comment='更新时间')
|
||||||
|
|
||||||
|
|
||||||
|
class BizPalletEvent(Base):
|
||||||
|
"""托盘只追加流转事件。"""
|
||||||
|
|
||||||
|
__tablename__ = 'biz_pallet_event'
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('pallet_id', 'event_seq', name='uq_biz_pallet_event_pallet_seq'),
|
||||||
|
UniqueConstraint('pallet_id', 'idempotency_key', name='uq_biz_pallet_event_pallet_idempotency'),
|
||||||
|
CheckConstraint(
|
||||||
|
"event_type IN ('INBOUND_CONFIRMED', 'FILLING_CONFIRMED', 'SHIPMENT_CONFIRMED', "
|
||||||
|
"'USAGE_RECORDED', 'RETURN_REQUESTED', 'RETURN_CONFIRMED')",
|
||||||
|
name='ck_biz_pallet_event_event_type',
|
||||||
|
),
|
||||||
|
{'comment': '托盘只追加流转事件'},
|
||||||
|
)
|
||||||
|
|
||||||
|
event_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='事件ID')
|
||||||
|
pallet_id = Column(BigInteger, ForeignKey('biz_pallet.pallet_id'), nullable=False, comment='托盘ID')
|
||||||
|
event_seq = Column(BigInteger, nullable=False, comment='托盘内事件序号')
|
||||||
|
event_type = Column(String(32), nullable=False, comment='事件类型')
|
||||||
|
source_type = Column(String(32), nullable=False, comment='来源类型')
|
||||||
|
source_id = Column(BigInteger, nullable=False, comment='来源记录ID')
|
||||||
|
before_state = Column(String(32), nullable=False, comment='变更前状态')
|
||||||
|
after_state = Column(String(32), nullable=False, comment='变更后状态')
|
||||||
|
before_warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='变更前仓库ID',
|
||||||
|
)
|
||||||
|
before_site_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer_site.site_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='变更前现场ID',
|
||||||
|
)
|
||||||
|
before_line_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_production_line.line_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='变更前产线ID',
|
||||||
|
)
|
||||||
|
after_warehouse_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_warehouse.warehouse_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='变更后仓库ID',
|
||||||
|
)
|
||||||
|
after_site_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_customer_site.site_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='变更后现场ID',
|
||||||
|
)
|
||||||
|
after_line_id = Column(
|
||||||
|
BigInteger,
|
||||||
|
ForeignKey('biz_production_line.line_id'),
|
||||||
|
nullable=True,
|
||||||
|
comment='变更后产线ID',
|
||||||
|
)
|
||||||
|
before_remaining_packages = Column(Integer, nullable=False, comment='变更前剩余包数')
|
||||||
|
after_remaining_packages = Column(Integer, nullable=False, comment='变更后剩余包数')
|
||||||
|
operator_id = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=False, comment='操作人ID')
|
||||||
|
occurred_at = Column(DateTime, nullable=False, comment='发生时间')
|
||||||
|
idempotency_key = Column(String(64), nullable=False, comment='幂等键')
|
||||||
|
request_hash = Column(String(64), nullable=False, comment='请求摘要')
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from sqlalchemy import CHAR, BigInteger, CheckConstraint, Integer, String, Table, UniqueConstraint
|
||||||
|
|
||||||
|
from config.database import Base
|
||||||
|
from module_business.entity.do import master_do, operation_do
|
||||||
|
|
||||||
|
EXPECTED_COLUMNS = {
|
||||||
|
'biz_strain': {
|
||||||
|
'strain_id',
|
||||||
|
'strain_code',
|
||||||
|
'strain_name',
|
||||||
|
'status',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_warehouse': {
|
||||||
|
'warehouse_id',
|
||||||
|
'warehouse_code',
|
||||||
|
'warehouse_name',
|
||||||
|
'address',
|
||||||
|
'status',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_customer': {
|
||||||
|
'customer_id',
|
||||||
|
'customer_code',
|
||||||
|
'customer_name',
|
||||||
|
'status',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_customer_site': {
|
||||||
|
'site_id',
|
||||||
|
'customer_id',
|
||||||
|
'site_code',
|
||||||
|
'site_name',
|
||||||
|
'address',
|
||||||
|
'status',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_production_line': {
|
||||||
|
'line_id',
|
||||||
|
'site_id',
|
||||||
|
'line_code',
|
||||||
|
'line_name',
|
||||||
|
'status',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_pallet': {
|
||||||
|
'pallet_id',
|
||||||
|
'pallet_code',
|
||||||
|
'qr_token',
|
||||||
|
'capacity_packages',
|
||||||
|
'asset_status',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_user_warehouse': {'user_id', 'warehouse_id', 'create_by', 'create_time'},
|
||||||
|
'biz_user_site': {'user_id', 'site_id', 'create_by', 'create_time'},
|
||||||
|
'biz_inbound_order': {
|
||||||
|
'inbound_id',
|
||||||
|
'inbound_no',
|
||||||
|
'warehouse_id',
|
||||||
|
'status',
|
||||||
|
'confirm_idempotency_key',
|
||||||
|
'confirm_request_hash',
|
||||||
|
'confirmed_by',
|
||||||
|
'confirmed_at',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_inbound_item': {
|
||||||
|
'inbound_item_id',
|
||||||
|
'inbound_id',
|
||||||
|
'pallet_id',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
},
|
||||||
|
'biz_filling_record': {
|
||||||
|
'filling_id',
|
||||||
|
'filling_no',
|
||||||
|
'pallet_id',
|
||||||
|
'warehouse_id',
|
||||||
|
'strain_id',
|
||||||
|
'strain_code_snapshot',
|
||||||
|
'strain_name_snapshot',
|
||||||
|
'batch_no',
|
||||||
|
'package_spec',
|
||||||
|
'initial_packages',
|
||||||
|
'active_slot',
|
||||||
|
'closed_at',
|
||||||
|
'idempotency_key',
|
||||||
|
'request_hash',
|
||||||
|
'operator_id',
|
||||||
|
'operated_at',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_shipment_order': {
|
||||||
|
'shipment_id',
|
||||||
|
'shipment_no',
|
||||||
|
'source_warehouse_id',
|
||||||
|
'target_site_id',
|
||||||
|
'status',
|
||||||
|
'confirm_idempotency_key',
|
||||||
|
'confirm_request_hash',
|
||||||
|
'confirmed_by',
|
||||||
|
'confirmed_at',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_shipment_item': {
|
||||||
|
'shipment_item_id',
|
||||||
|
'shipment_id',
|
||||||
|
'pallet_id',
|
||||||
|
'active_filling_id',
|
||||||
|
'package_qty',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
},
|
||||||
|
'biz_usage_record': {
|
||||||
|
'usage_id',
|
||||||
|
'usage_no',
|
||||||
|
'pallet_id',
|
||||||
|
'active_filling_id',
|
||||||
|
'site_id',
|
||||||
|
'line_id',
|
||||||
|
'used_packages',
|
||||||
|
'remaining_packages_after',
|
||||||
|
'idempotency_key',
|
||||||
|
'request_hash',
|
||||||
|
'operator_id',
|
||||||
|
'occurred_at',
|
||||||
|
},
|
||||||
|
'biz_return_order': {
|
||||||
|
'return_id',
|
||||||
|
'return_no',
|
||||||
|
'source_site_id',
|
||||||
|
'target_warehouse_id',
|
||||||
|
'status',
|
||||||
|
'request_idempotency_key',
|
||||||
|
'request_hash',
|
||||||
|
'confirm_idempotency_key',
|
||||||
|
'confirm_request_hash',
|
||||||
|
'requested_by',
|
||||||
|
'requested_at',
|
||||||
|
'confirmed_by',
|
||||||
|
'confirmed_at',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
'update_by',
|
||||||
|
'update_time',
|
||||||
|
'remark',
|
||||||
|
},
|
||||||
|
'biz_return_item': {
|
||||||
|
'return_item_id',
|
||||||
|
'return_id',
|
||||||
|
'pallet_id',
|
||||||
|
'active_filling_id_snapshot',
|
||||||
|
'remaining_packages_snapshot',
|
||||||
|
'create_by',
|
||||||
|
'create_time',
|
||||||
|
},
|
||||||
|
'biz_pallet_inventory': {
|
||||||
|
'pallet_id',
|
||||||
|
'active_filling_id',
|
||||||
|
'warehouse_id',
|
||||||
|
'site_id',
|
||||||
|
'current_line_id',
|
||||||
|
'flow_state',
|
||||||
|
'remaining_packages',
|
||||||
|
'last_event_id',
|
||||||
|
'version',
|
||||||
|
'updated_at',
|
||||||
|
},
|
||||||
|
'biz_pallet_event': {
|
||||||
|
'event_id',
|
||||||
|
'pallet_id',
|
||||||
|
'event_seq',
|
||||||
|
'event_type',
|
||||||
|
'source_type',
|
||||||
|
'source_id',
|
||||||
|
'before_state',
|
||||||
|
'after_state',
|
||||||
|
'before_warehouse_id',
|
||||||
|
'before_site_id',
|
||||||
|
'before_line_id',
|
||||||
|
'after_warehouse_id',
|
||||||
|
'after_site_id',
|
||||||
|
'after_line_id',
|
||||||
|
'before_remaining_packages',
|
||||||
|
'after_remaining_packages',
|
||||||
|
'operator_id',
|
||||||
|
'occurred_at',
|
||||||
|
'idempotency_key',
|
||||||
|
'request_hash',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECTED_PRIMARY_KEYS = {
|
||||||
|
'biz_strain': ('strain_id',),
|
||||||
|
'biz_warehouse': ('warehouse_id',),
|
||||||
|
'biz_customer': ('customer_id',),
|
||||||
|
'biz_customer_site': ('site_id',),
|
||||||
|
'biz_production_line': ('line_id',),
|
||||||
|
'biz_pallet': ('pallet_id',),
|
||||||
|
'biz_user_warehouse': ('user_id', 'warehouse_id'),
|
||||||
|
'biz_user_site': ('user_id', 'site_id'),
|
||||||
|
'biz_inbound_order': ('inbound_id',),
|
||||||
|
'biz_inbound_item': ('inbound_item_id',),
|
||||||
|
'biz_filling_record': ('filling_id',),
|
||||||
|
'biz_shipment_order': ('shipment_id',),
|
||||||
|
'biz_shipment_item': ('shipment_item_id',),
|
||||||
|
'biz_usage_record': ('usage_id',),
|
||||||
|
'biz_return_order': ('return_id',),
|
||||||
|
'biz_return_item': ('return_item_id',),
|
||||||
|
'biz_pallet_inventory': ('pallet_id',),
|
||||||
|
'biz_pallet_event': ('event_id',),
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECTED_FOREIGN_KEYS = {
|
||||||
|
'biz_strain': set(),
|
||||||
|
'biz_warehouse': set(),
|
||||||
|
'biz_customer': set(),
|
||||||
|
'biz_customer_site': {('customer_id', 'biz_customer.customer_id')},
|
||||||
|
'biz_production_line': {('site_id', 'biz_customer_site.site_id')},
|
||||||
|
'biz_pallet': set(),
|
||||||
|
'biz_user_warehouse': {
|
||||||
|
('user_id', 'sys_user.user_id'),
|
||||||
|
('warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
},
|
||||||
|
'biz_user_site': {
|
||||||
|
('user_id', 'sys_user.user_id'),
|
||||||
|
('site_id', 'biz_customer_site.site_id'),
|
||||||
|
},
|
||||||
|
'biz_inbound_order': {
|
||||||
|
('warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
('confirmed_by', 'sys_user.user_id'),
|
||||||
|
},
|
||||||
|
'biz_inbound_item': {
|
||||||
|
('inbound_id', 'biz_inbound_order.inbound_id'),
|
||||||
|
('pallet_id', 'biz_pallet.pallet_id'),
|
||||||
|
},
|
||||||
|
'biz_filling_record': {
|
||||||
|
('pallet_id', 'biz_pallet.pallet_id'),
|
||||||
|
('warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
('strain_id', 'biz_strain.strain_id'),
|
||||||
|
('operator_id', 'sys_user.user_id'),
|
||||||
|
},
|
||||||
|
'biz_shipment_order': {
|
||||||
|
('source_warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
('target_site_id', 'biz_customer_site.site_id'),
|
||||||
|
('confirmed_by', 'sys_user.user_id'),
|
||||||
|
},
|
||||||
|
'biz_shipment_item': {
|
||||||
|
('shipment_id', 'biz_shipment_order.shipment_id'),
|
||||||
|
('pallet_id', 'biz_pallet.pallet_id'),
|
||||||
|
('active_filling_id', 'biz_filling_record.filling_id'),
|
||||||
|
},
|
||||||
|
'biz_usage_record': {
|
||||||
|
('pallet_id', 'biz_pallet.pallet_id'),
|
||||||
|
('active_filling_id', 'biz_filling_record.filling_id'),
|
||||||
|
('site_id', 'biz_customer_site.site_id'),
|
||||||
|
('line_id', 'biz_production_line.line_id'),
|
||||||
|
('operator_id', 'sys_user.user_id'),
|
||||||
|
},
|
||||||
|
'biz_return_order': {
|
||||||
|
('source_site_id', 'biz_customer_site.site_id'),
|
||||||
|
('target_warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
('requested_by', 'sys_user.user_id'),
|
||||||
|
('confirmed_by', 'sys_user.user_id'),
|
||||||
|
},
|
||||||
|
'biz_return_item': {
|
||||||
|
('return_id', 'biz_return_order.return_id'),
|
||||||
|
('pallet_id', 'biz_pallet.pallet_id'),
|
||||||
|
('active_filling_id_snapshot', 'biz_filling_record.filling_id'),
|
||||||
|
},
|
||||||
|
'biz_pallet_inventory': {
|
||||||
|
('pallet_id', 'biz_pallet.pallet_id'),
|
||||||
|
('active_filling_id', 'biz_filling_record.filling_id'),
|
||||||
|
('warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
('site_id', 'biz_customer_site.site_id'),
|
||||||
|
('current_line_id', 'biz_production_line.line_id'),
|
||||||
|
('last_event_id', 'biz_pallet_event.event_id'),
|
||||||
|
},
|
||||||
|
'biz_pallet_event': {
|
||||||
|
('pallet_id', 'biz_pallet.pallet_id'),
|
||||||
|
('before_warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
('before_site_id', 'biz_customer_site.site_id'),
|
||||||
|
('before_line_id', 'biz_production_line.line_id'),
|
||||||
|
('after_warehouse_id', 'biz_warehouse.warehouse_id'),
|
||||||
|
('after_site_id', 'biz_customer_site.site_id'),
|
||||||
|
('after_line_id', 'biz_production_line.line_id'),
|
||||||
|
('operator_id', 'sys_user.user_id'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECTED_UNIQUE_SETS = {
|
||||||
|
'biz_strain': {frozenset({'strain_code'})},
|
||||||
|
'biz_warehouse': {frozenset({'warehouse_code'})},
|
||||||
|
'biz_customer': {frozenset({'customer_code'})},
|
||||||
|
'biz_customer_site': {frozenset({'customer_id', 'site_code'})},
|
||||||
|
'biz_production_line': {frozenset({'site_id', 'line_code'})},
|
||||||
|
'biz_pallet': {frozenset({'pallet_code'}), frozenset({'qr_token'})},
|
||||||
|
'biz_user_warehouse': set(),
|
||||||
|
'biz_user_site': set(),
|
||||||
|
'biz_inbound_order': {
|
||||||
|
frozenset({'inbound_no'}),
|
||||||
|
frozenset({'confirm_idempotency_key'}),
|
||||||
|
},
|
||||||
|
'biz_inbound_item': {frozenset({'inbound_id', 'pallet_id'})},
|
||||||
|
'biz_filling_record': {
|
||||||
|
frozenset({'filling_no'}),
|
||||||
|
frozenset({'idempotency_key'}),
|
||||||
|
frozenset({'pallet_id', 'active_slot'}),
|
||||||
|
},
|
||||||
|
'biz_shipment_order': {
|
||||||
|
frozenset({'shipment_no'}),
|
||||||
|
frozenset({'confirm_idempotency_key'}),
|
||||||
|
},
|
||||||
|
'biz_shipment_item': {frozenset({'shipment_id', 'pallet_id'})},
|
||||||
|
'biz_usage_record': {
|
||||||
|
frozenset({'usage_no'}),
|
||||||
|
frozenset({'idempotency_key'}),
|
||||||
|
},
|
||||||
|
'biz_return_order': {
|
||||||
|
frozenset({'return_no'}),
|
||||||
|
frozenset({'request_idempotency_key'}),
|
||||||
|
frozenset({'confirm_idempotency_key'}),
|
||||||
|
},
|
||||||
|
'biz_return_item': {frozenset({'return_id', 'pallet_id'})},
|
||||||
|
'biz_pallet_inventory': set(),
|
||||||
|
'biz_pallet_event': {
|
||||||
|
frozenset({'pallet_id', 'event_seq'}),
|
||||||
|
frozenset({'pallet_id', 'idempotency_key'}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _unique_column_sets(table: Table) -> set[frozenset[str]]:
|
||||||
|
return {
|
||||||
|
frozenset(column.name for column in constraint.columns)
|
||||||
|
for constraint in table.constraints
|
||||||
|
if isinstance(constraint, UniqueConstraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_check_sql(table: Table) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
constraint.name: ''.join(str(constraint.sqltext).split())
|
||||||
|
for constraint in table.constraints
|
||||||
|
if isinstance(constraint, CheckConstraint) and constraint.name is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BusinessModelContractTest(unittest.TestCase):
|
||||||
|
def test_exact_business_tables_are_registered(self) -> None:
|
||||||
|
expected = {
|
||||||
|
'biz_strain',
|
||||||
|
'biz_warehouse',
|
||||||
|
'biz_customer',
|
||||||
|
'biz_customer_site',
|
||||||
|
'biz_production_line',
|
||||||
|
'biz_pallet',
|
||||||
|
'biz_user_warehouse',
|
||||||
|
'biz_user_site',
|
||||||
|
'biz_inbound_order',
|
||||||
|
'biz_inbound_item',
|
||||||
|
'biz_filling_record',
|
||||||
|
'biz_shipment_order',
|
||||||
|
'biz_shipment_item',
|
||||||
|
'biz_usage_record',
|
||||||
|
'biz_return_order',
|
||||||
|
'biz_return_item',
|
||||||
|
'biz_pallet_inventory',
|
||||||
|
'biz_pallet_event',
|
||||||
|
}
|
||||||
|
actual = {name for name in Base.metadata.tables if name.startswith('biz_')}
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
def test_exact_model_classes_and_columns(self) -> None:
|
||||||
|
models = {
|
||||||
|
master_do.BizStrain: 'biz_strain',
|
||||||
|
master_do.BizWarehouse: 'biz_warehouse',
|
||||||
|
master_do.BizCustomer: 'biz_customer',
|
||||||
|
master_do.BizCustomerSite: 'biz_customer_site',
|
||||||
|
master_do.BizProductionLine: 'biz_production_line',
|
||||||
|
master_do.BizPallet: 'biz_pallet',
|
||||||
|
master_do.BizUserWarehouse: 'biz_user_warehouse',
|
||||||
|
master_do.BizUserSite: 'biz_user_site',
|
||||||
|
operation_do.BizInboundOrder: 'biz_inbound_order',
|
||||||
|
operation_do.BizInboundItem: 'biz_inbound_item',
|
||||||
|
operation_do.BizFillingRecord: 'biz_filling_record',
|
||||||
|
operation_do.BizShipmentOrder: 'biz_shipment_order',
|
||||||
|
operation_do.BizShipmentItem: 'biz_shipment_item',
|
||||||
|
operation_do.BizUsageRecord: 'biz_usage_record',
|
||||||
|
operation_do.BizReturnOrder: 'biz_return_order',
|
||||||
|
operation_do.BizReturnItem: 'biz_return_item',
|
||||||
|
operation_do.BizPalletInventory: 'biz_pallet_inventory',
|
||||||
|
operation_do.BizPalletEvent: 'biz_pallet_event',
|
||||||
|
}
|
||||||
|
self.assertEqual(set(EXPECTED_COLUMNS), set(models.values()))
|
||||||
|
for model, table_name in models.items():
|
||||||
|
with self.subTest(table_name=table_name):
|
||||||
|
self.assertEqual(table_name, model.__tablename__)
|
||||||
|
self.assertEqual(EXPECTED_COLUMNS[table_name], set(model.__table__.columns.keys()))
|
||||||
|
|
||||||
|
def test_primary_keys_and_foreign_keys_match_contract(self) -> None:
|
||||||
|
for table_name, expected_primary_key in EXPECTED_PRIMARY_KEYS.items():
|
||||||
|
table = Base.metadata.tables[table_name]
|
||||||
|
with self.subTest(table_name=table_name, constraint='primary_key'):
|
||||||
|
self.assertEqual(expected_primary_key, tuple(table.primary_key.columns.keys()))
|
||||||
|
|
||||||
|
actual_foreign_keys = {
|
||||||
|
(foreign_key.parent.name, foreign_key.target_fullname) for foreign_key in table.foreign_keys
|
||||||
|
}
|
||||||
|
with self.subTest(table_name=table_name, constraint='foreign_keys'):
|
||||||
|
self.assertEqual(EXPECTED_FOREIGN_KEYS[table_name], actual_foreign_keys)
|
||||||
|
|
||||||
|
for table_name, primary_key in EXPECTED_PRIMARY_KEYS.items():
|
||||||
|
for column_name in primary_key:
|
||||||
|
with self.subTest(table_name=table_name, column_name=column_name):
|
||||||
|
self.assertIsInstance(Base.metadata.tables[table_name].c[column_name].type, BigInteger)
|
||||||
|
|
||||||
|
def test_all_required_unique_constraints_match_contract(self) -> None:
|
||||||
|
for table_name, expected_unique_sets in EXPECTED_UNIQUE_SETS.items():
|
||||||
|
with self.subTest(table_name=table_name):
|
||||||
|
self.assertEqual(expected_unique_sets, _unique_column_sets(Base.metadata.tables[table_name]))
|
||||||
|
|
||||||
|
def test_pallet_identifiers_are_unique(self) -> None:
|
||||||
|
table = Base.metadata.tables['biz_pallet']
|
||||||
|
unique_columns = {column.name for column in table.columns if column.unique}
|
||||||
|
self.assertEqual({'pallet_code', 'qr_token'}, unique_columns)
|
||||||
|
|
||||||
|
def test_checks_lock_quantities_states_active_slot_and_location(self) -> None:
|
||||||
|
expected_checks = {
|
||||||
|
'biz_strain': {'ck_biz_strain_status': "statusIN('0','1')"},
|
||||||
|
'biz_warehouse': {'ck_biz_warehouse_status': "statusIN('0','1')"},
|
||||||
|
'biz_customer': {'ck_biz_customer_status': "statusIN('0','1')"},
|
||||||
|
'biz_customer_site': {'ck_biz_customer_site_status': "statusIN('0','1')"},
|
||||||
|
'biz_production_line': {'ck_biz_production_line_status': "statusIN('0','1')"},
|
||||||
|
'biz_pallet': {
|
||||||
|
'ck_biz_pallet_capacity_packages': 'capacity_packages>0',
|
||||||
|
'ck_biz_pallet_asset_status': "asset_statusIN('0','1')",
|
||||||
|
},
|
||||||
|
'biz_inbound_order': {
|
||||||
|
'ck_biz_inbound_order_status': "statusIN('DRAFT','CONFIRMED')",
|
||||||
|
},
|
||||||
|
'biz_filling_record': {
|
||||||
|
'ck_biz_filling_record_initial_packages': 'initial_packages>0',
|
||||||
|
'ck_biz_filling_record_active_slot': "active_slotISNULLORactive_slot='ACTIVE'",
|
||||||
|
},
|
||||||
|
'biz_shipment_order': {
|
||||||
|
'ck_biz_shipment_order_status': "statusIN('DRAFT','CONFIRMED')",
|
||||||
|
},
|
||||||
|
'biz_shipment_item': {
|
||||||
|
'ck_biz_shipment_item_package_qty': 'package_qtyISNULLORpackage_qty>0',
|
||||||
|
},
|
||||||
|
'biz_usage_record': {
|
||||||
|
'ck_biz_usage_record_used_packages': 'used_packages>0',
|
||||||
|
'ck_biz_usage_record_remaining_packages_after': 'remaining_packages_after>=0',
|
||||||
|
},
|
||||||
|
'biz_return_order': {
|
||||||
|
'ck_biz_return_order_status': "statusIN('REQUESTED','CONFIRMED')",
|
||||||
|
},
|
||||||
|
'biz_return_item': {
|
||||||
|
'ck_biz_return_item_remaining_packages_snapshot': 'remaining_packages_snapshot>=0',
|
||||||
|
},
|
||||||
|
'biz_pallet_inventory': {
|
||||||
|
'ck_biz_pallet_inventory_flow_state': (
|
||||||
|
"flow_stateIN('UNSTOCKED','EMPTY_IN_WAREHOUSE','FILLED_IN_WAREHOUSE',"
|
||||||
|
"'AT_SITE_UNUSED','AT_SITE_PARTIAL','AT_SITE_EMPTY','RETURNING')"
|
||||||
|
),
|
||||||
|
'ck_biz_pallet_inventory_remaining_packages': 'remaining_packages>=0',
|
||||||
|
'ck_biz_pallet_inventory_version': 'version>=0',
|
||||||
|
'ck_biz_pallet_inventory_location': 'NOT(warehouse_idISNOTNULLANDsite_idISNOTNULL)',
|
||||||
|
},
|
||||||
|
'biz_pallet_event': {
|
||||||
|
'ck_biz_pallet_event_event_type': (
|
||||||
|
"event_typeIN('INBOUND_CONFIRMED','FILLING_CONFIRMED','SHIPMENT_CONFIRMED',"
|
||||||
|
"'USAGE_RECORDED','RETURN_REQUESTED','RETURN_CONFIRMED')"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for table_name, expected in expected_checks.items():
|
||||||
|
with self.subTest(table_name=table_name):
|
||||||
|
self.assertEqual(expected, _normalized_check_sql(Base.metadata.tables[table_name]))
|
||||||
|
|
||||||
|
def test_status_fields_use_portable_string_types(self) -> None:
|
||||||
|
char_statuses = {
|
||||||
|
'biz_strain': 'status',
|
||||||
|
'biz_warehouse': 'status',
|
||||||
|
'biz_customer': 'status',
|
||||||
|
'biz_customer_site': 'status',
|
||||||
|
'biz_production_line': 'status',
|
||||||
|
'biz_pallet': 'asset_status',
|
||||||
|
}
|
||||||
|
for table_name, column_name in char_statuses.items():
|
||||||
|
status_type = Base.metadata.tables[table_name].c[column_name].type
|
||||||
|
with self.subTest(table_name=table_name, column_name=column_name):
|
||||||
|
self.assertIs(type(status_type), CHAR)
|
||||||
|
self.assertEqual(1, status_type.length)
|
||||||
|
|
||||||
|
string_statuses = {
|
||||||
|
'biz_inbound_order': {'status'},
|
||||||
|
'biz_shipment_order': {'status'},
|
||||||
|
'biz_return_order': {'status'},
|
||||||
|
'biz_pallet_inventory': {'flow_state'},
|
||||||
|
'biz_pallet_event': {'event_type', 'before_state', 'after_state'},
|
||||||
|
}
|
||||||
|
for table_name, column_names in string_statuses.items():
|
||||||
|
for column_name in column_names:
|
||||||
|
status_type = Base.metadata.tables[table_name].c[column_name].type
|
||||||
|
with self.subTest(table_name=table_name, column_name=column_name):
|
||||||
|
self.assertIs(type(status_type), String)
|
||||||
|
self.assertEqual(32, status_type.length)
|
||||||
|
|
||||||
|
def test_package_counts_use_integer_and_nullable_snapshots_are_preserved(self) -> None:
|
||||||
|
integer_columns = {
|
||||||
|
'biz_pallet': {'capacity_packages'},
|
||||||
|
'biz_filling_record': {'initial_packages'},
|
||||||
|
'biz_shipment_item': {'package_qty'},
|
||||||
|
'biz_usage_record': {'used_packages', 'remaining_packages_after'},
|
||||||
|
'biz_return_item': {'remaining_packages_snapshot'},
|
||||||
|
'biz_pallet_inventory': {'remaining_packages', 'version'},
|
||||||
|
'biz_pallet_event': {'before_remaining_packages', 'after_remaining_packages'},
|
||||||
|
}
|
||||||
|
for table_name, column_names in integer_columns.items():
|
||||||
|
for column_name in column_names:
|
||||||
|
with self.subTest(table_name=table_name, column_name=column_name):
|
||||||
|
self.assertIs(type(Base.metadata.tables[table_name].c[column_name].type), Integer)
|
||||||
|
|
||||||
|
nullable_columns = {
|
||||||
|
'biz_inbound_order': {
|
||||||
|
'confirm_idempotency_key',
|
||||||
|
'confirm_request_hash',
|
||||||
|
'confirmed_by',
|
||||||
|
'confirmed_at',
|
||||||
|
},
|
||||||
|
'biz_filling_record': {'active_slot', 'closed_at'},
|
||||||
|
'biz_shipment_order': {
|
||||||
|
'confirm_idempotency_key',
|
||||||
|
'confirm_request_hash',
|
||||||
|
'confirmed_by',
|
||||||
|
'confirmed_at',
|
||||||
|
},
|
||||||
|
'biz_shipment_item': {'active_filling_id', 'package_qty'},
|
||||||
|
'biz_return_order': {
|
||||||
|
'confirm_idempotency_key',
|
||||||
|
'confirm_request_hash',
|
||||||
|
'confirmed_by',
|
||||||
|
'confirmed_at',
|
||||||
|
},
|
||||||
|
'biz_return_item': {'active_filling_id_snapshot'},
|
||||||
|
'biz_pallet_inventory': {
|
||||||
|
'active_filling_id',
|
||||||
|
'warehouse_id',
|
||||||
|
'site_id',
|
||||||
|
'current_line_id',
|
||||||
|
'last_event_id',
|
||||||
|
},
|
||||||
|
'biz_pallet_event': {
|
||||||
|
'before_warehouse_id',
|
||||||
|
'before_site_id',
|
||||||
|
'before_line_id',
|
||||||
|
'after_warehouse_id',
|
||||||
|
'after_site_id',
|
||||||
|
'after_line_id',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for table_name, column_names in nullable_columns.items():
|
||||||
|
for column_name in column_names:
|
||||||
|
with self.subTest(table_name=table_name, column_name=column_name):
|
||||||
|
self.assertTrue(Base.metadata.tables[table_name].c[column_name].nullable)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user