更新登录页及优化相关功能
This commit is contained in:
@@ -193,6 +193,47 @@ class BizProductionLine(Base):
|
||||
)
|
||||
|
||||
|
||||
class BizTunnel(Base):
|
||||
"""客户现场隧道主数据表。"""
|
||||
|
||||
__tablename__ = 'biz_tunnel'
|
||||
__table_args__ = (
|
||||
UniqueConstraint('site_id', 'tunnel_code', name='uq_biz_tunnel_site_code'),
|
||||
CheckConstraint("status IN ('0', '1')", name='ck_biz_tunnel_status'),
|
||||
{'comment': '客户现场隧道主数据表'},
|
||||
)
|
||||
|
||||
tunnel_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='隧道ID')
|
||||
test_batch_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_test_batch.batch_id'),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment='测试数据批次ID;NULL表示正式数据',
|
||||
)
|
||||
site_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_customer_site.site_id'),
|
||||
nullable=False,
|
||||
comment='现场ID',
|
||||
)
|
||||
tunnel_code = Column(String(64), nullable=False, comment='隧道编号')
|
||||
tunnel_name = Column(String(100), nullable=False, comment='隧道名称')
|
||||
current_trace_no = Column(String(64), nullable=True, comment='当前隧道物料溯源号')
|
||||
current_trace_started_at = Column(DateTime, 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 BizPallet(Base):
|
||||
"""托盘资产主数据表。"""
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, CheckConstraint, Column, DateTime, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
BigInteger,
|
||||
CheckConstraint,
|
||||
Column,
|
||||
Date,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
)
|
||||
|
||||
from config.database import Base
|
||||
from config.env import DataBaseConfig
|
||||
@@ -99,6 +110,11 @@ class BizFillingRecord(Base):
|
||||
),
|
||||
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(
|
||||
'(production_date IS NULL AND expiry_date IS NULL) OR '
|
||||
'(production_date IS NOT NULL AND expiry_date IS NOT NULL AND expiry_date >= production_date)',
|
||||
name='ck_biz_filling_record_material_dates',
|
||||
),
|
||||
CheckConstraint(
|
||||
"active_slot IS NULL OR active_slot = 'ACTIVE'",
|
||||
name='ck_biz_filling_record_active_slot',
|
||||
@@ -126,6 +142,8 @@ class BizFillingRecord(Base):
|
||||
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='生产批次')
|
||||
production_date = Column(Date, nullable=True, comment='来源生产日期;历史未登记为空')
|
||||
expiry_date = Column(Date, nullable=True, comment='有效期至;历史未登记为空')
|
||||
package_spec = Column(String(64), nullable=False, comment='包装规格')
|
||||
initial_packages = Column(Integer, nullable=False, comment='初始包数')
|
||||
active_slot = Column(String(16), nullable=True, comment='活动装填槽')
|
||||
@@ -158,6 +176,8 @@ class BizShipmentOrder(Base):
|
||||
name='uq_biz_shipment_order_route_confirm_idempotency',
|
||||
),
|
||||
CheckConstraint("status IN ('DRAFT', 'CONFIRMED')", name='ck_biz_shipment_order_status'),
|
||||
CheckConstraint("receipt_state IN ('PENDING', 'RECEIVED', 'LEGACY')", name='ck_biz_shipment_order_receipt_state'),
|
||||
UniqueConstraint('target_site_id', 'receipt_idempotency_key', name='uq_biz_shipment_receipt_site_key'),
|
||||
{'comment': '整托发货单'},
|
||||
)
|
||||
|
||||
@@ -187,6 +207,11 @@ class BizShipmentOrder(Base):
|
||||
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='确认时间')
|
||||
receipt_state = Column(String(16), nullable=False, server_default='PENDING', comment='收货状态;LEGACY表示历史未登记')
|
||||
received_by = Column(BigInteger, ForeignKey('sys_user.user_id'), nullable=True, comment='收货核对人ID')
|
||||
received_at = Column(DateTime, nullable=True, comment='实际收货核对时间')
|
||||
receipt_idempotency_key = Column(String(64), nullable=True, comment='收货确认幂等键')
|
||||
receipt_request_hash = Column(String(64), 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='更新者')
|
||||
@@ -275,13 +300,23 @@ class BizUsageRecord(Base):
|
||||
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')
|
||||
tunnel_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_tunnel.tunnel_id'),
|
||||
nullable=True,
|
||||
comment='隧道ID;历史记录可能为空',
|
||||
)
|
||||
trace_no = Column(String(64), nullable=True, comment='隧道物料内部溯源号;历史记录可能为空')
|
||||
batch_no_snapshot = Column(String(64), nullable=True, comment='生产批次快照;历史记录可能为空')
|
||||
line_id = Column(BigInteger, ForeignKey('biz_production_line.line_id'), nullable=True, comment='产线ID(可选)')
|
||||
used_packages = Column(Integer, nullable=False, comment='本次使用包数')
|
||||
remaining_packages_after = Column(Integer, nullable=False, comment='使用后剩余包数')
|
||||
idempotency_key = Column(String(64), nullable=False, 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='发生时间')
|
||||
fifo_skip_reason = Column(String(500), nullable=True, comment='跳过先到托盘原因')
|
||||
fifo_recommendation = Column(JSON, nullable=True, comment='跳过时先到托盘身份及到货时间快照')
|
||||
|
||||
|
||||
class BizReturnOrder(Base):
|
||||
@@ -387,6 +422,14 @@ class BizReturnItem(Base):
|
||||
nullable=True,
|
||||
comment='申请时活动装填记录快照',
|
||||
)
|
||||
tunnel_id_snapshot = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_tunnel.tunnel_id'),
|
||||
nullable=True,
|
||||
comment='申请时隧道ID快照',
|
||||
)
|
||||
trace_no_snapshot = Column(String(64), nullable=True, comment='申请时物料溯源号快照')
|
||||
batch_no_snapshot = Column(String(64), 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='创建时间')
|
||||
@@ -399,7 +442,7 @@ class BizPalletInventory(Base):
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"flow_state IN ('UNSTOCKED', 'EMPTY_IN_WAREHOUSE', 'FILLED_IN_WAREHOUSE', "
|
||||
"'AT_SITE_UNUSED', 'AT_SITE_PARTIAL', 'AT_SITE_EMPTY', 'RETURNING')",
|
||||
"'IN_TRANSIT', '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'),
|
||||
@@ -444,6 +487,13 @@ class BizPalletInventory(Base):
|
||||
nullable=True,
|
||||
comment='当前现场ID',
|
||||
)
|
||||
tunnel_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_tunnel.tunnel_id'),
|
||||
nullable=True,
|
||||
comment='当前隧道ID',
|
||||
)
|
||||
trace_no = Column(String(64), nullable=True, comment='当前隧道物料溯源号')
|
||||
current_line_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_production_line.line_id'),
|
||||
@@ -452,6 +502,7 @@ class BizPalletInventory(Base):
|
||||
)
|
||||
flow_state = Column(String(32), nullable=False, server_default='UNSTOCKED', comment='托盘流转状态')
|
||||
remaining_packages = Column(Integer, nullable=False, server_default='0', comment='当前剩余包数')
|
||||
site_arrived_at = Column(DateTime, nullable=True, comment='本轮实际收货核对时间;历史未登记为空')
|
||||
last_event_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_pallet_event.event_id'),
|
||||
@@ -471,7 +522,7 @@ class BizPalletEvent(Base):
|
||||
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')",
|
||||
"'SHIPMENT_RECEIVED', 'TUNNEL_ENTERED', 'USAGE_RECORDED', 'RETURN_REQUESTED', 'RETURN_CONFIRMED')",
|
||||
name='ck_biz_pallet_event_event_type',
|
||||
),
|
||||
{'comment': '托盘只追加流转事件'},
|
||||
@@ -504,6 +555,12 @@ class BizPalletEvent(Base):
|
||||
nullable=True,
|
||||
comment='变更前现场ID',
|
||||
)
|
||||
before_tunnel_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_tunnel.tunnel_id'),
|
||||
nullable=True,
|
||||
comment='变更前隧道ID',
|
||||
)
|
||||
before_line_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_production_line.line_id'),
|
||||
@@ -522,6 +579,12 @@ class BizPalletEvent(Base):
|
||||
nullable=True,
|
||||
comment='变更后现场ID',
|
||||
)
|
||||
after_tunnel_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_tunnel.tunnel_id'),
|
||||
nullable=True,
|
||||
comment='变更后隧道ID',
|
||||
)
|
||||
after_line_id = Column(
|
||||
BigInteger,
|
||||
ForeignKey('biz_production_line.line_id'),
|
||||
@@ -530,6 +593,10 @@ class BizPalletEvent(Base):
|
||||
)
|
||||
before_remaining_packages = Column(Integer, nullable=False, comment='变更前剩余包数')
|
||||
after_remaining_packages = Column(Integer, nullable=False, comment='变更后剩余包数')
|
||||
trace_no = Column(String(64), nullable=True, comment='事件关联物料溯源号')
|
||||
batch_no_snapshot = Column(String(64), nullable=True, comment='事件生产批次快照')
|
||||
fifo_skip_reason = Column(String(500), nullable=True, comment='跳过先到托盘原因')
|
||||
fifo_recommendation = Column(JSON, nullable=True, 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='幂等键')
|
||||
|
||||
@@ -240,6 +240,64 @@ class DeleteLineModel(BaseModel):
|
||||
_validate_unique_ids = field_validator('line_ids')(_ensure_unique_ids)
|
||||
|
||||
|
||||
class TunnelModel(BaseModel):
|
||||
"""隧道主数据写入及展示模型。"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
from_attributes=True,
|
||||
populate_by_name=True,
|
||||
extra='forbid',
|
||||
)
|
||||
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='隧道ID')
|
||||
site_id: int = Field(gt=0, description='现场ID')
|
||||
tunnel_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='隧道编号')
|
||||
tunnel_name: str = Field(min_length=1, max_length=100, pattern=r'\S', description='隧道名称')
|
||||
status: Literal['0', '1'] = Field(default='0', description='状态(0正常 1停用)')
|
||||
current_trace_no: str | None = Field(default=None, max_length=64, description='当前物料溯源号')
|
||||
current_trace_started_at: datetime | None = Field(default=None, description='当前溯源批次开始时间')
|
||||
create_by: str | None = Field(default=None, max_length=64, description='创建者')
|
||||
create_time: datetime | None = Field(default=None, description='创建时间')
|
||||
update_by: str | None = Field(default=None, max_length=64, description='更新者')
|
||||
update_time: datetime | None = Field(default=None, description='更新时间')
|
||||
remark: str | None = Field(default=None, max_length=500, description='备注')
|
||||
|
||||
|
||||
class TunnelPageQueryModel(BaseModel):
|
||||
"""隧道分页查询模型。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
site_id: int | None = Field(default=None, gt=0, description='现场ID')
|
||||
tunnel_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='隧道编号')
|
||||
tunnel_name: str | None = Field(
|
||||
default=None,
|
||||
min_length=1,
|
||||
max_length=100,
|
||||
pattern=r'\S',
|
||||
description='隧道名称',
|
||||
)
|
||||
status: Literal['0', '1'] | None = Field(default=None, description='状态')
|
||||
|
||||
|
||||
class DeleteTunnelModel(BaseModel):
|
||||
"""删除隧道模型。"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
from_attributes=True,
|
||||
populate_by_name=True,
|
||||
extra='forbid',
|
||||
)
|
||||
|
||||
tunnel_ids: list[PositiveId] = Field(min_length=1, description='需要删除的隧道ID')
|
||||
|
||||
_validate_unique_ids = field_validator('tunnel_ids')(_ensure_unique_ids)
|
||||
|
||||
|
||||
class WarehouseModel(BaseModel):
|
||||
"""仓库主数据写入及展示模型。"""
|
||||
|
||||
@@ -329,7 +387,7 @@ class PalletCreateModel(BaseModel):
|
||||
)
|
||||
|
||||
pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号')
|
||||
capacity_packages: int = Field(strict=True, gt=0, description='容量包数')
|
||||
capacity_packages: Literal[100] = Field(description='容量包数(固定 100 包)')
|
||||
remark: str | None = Field(default=None, max_length=500, description='备注')
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
@@ -16,6 +16,10 @@ def _ensure_unique_ids(value: list[int]) -> list[int]:
|
||||
return value
|
||||
|
||||
|
||||
def _normalize_fifo_reason(value: str | None) -> str | None:
|
||||
return (value.strip() or None) if isinstance(value, str) else value
|
||||
|
||||
|
||||
def _parse_page_query_date(value: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.strptime(value, '%Y-%m-%d')
|
||||
@@ -98,10 +102,18 @@ class FillingCreateModel(BaseModel):
|
||||
warehouse_id: int = Field(gt=0, description='仓库ID')
|
||||
strain_id: int = Field(gt=0, description='菌种ID')
|
||||
batch_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='生产批次')
|
||||
production_date: date = Field(description='发货来源生产日期')
|
||||
expiry_date: date = Field(description='有效期至')
|
||||
package_spec: str = Field(min_length=1, max_length=64, pattern=r'\S', description='包装规格')
|
||||
initial_packages: int = Field(strict=True, gt=0, description='初始包数')
|
||||
initial_packages: Literal[100] = Field(description='初始包数(固定 100 包)')
|
||||
idempotency_key: str = Field(min_length=1, max_length=64, pattern=r'\S', description='幂等键')
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_material_dates(self) -> 'FillingCreateModel':
|
||||
if self.expiry_date < self.production_date:
|
||||
raise ValueError('有效期不能早于生产日期')
|
||||
return self
|
||||
|
||||
|
||||
class ShipmentCreateModel(BaseModel):
|
||||
"""整托发货草稿新增模型。"""
|
||||
@@ -149,9 +161,41 @@ class UsageCreateModel(BaseModel):
|
||||
)
|
||||
|
||||
qr_token: str = Field(min_length=1, max_length=128, pattern=r'\S', description='二维码固定标识')
|
||||
line_id: int = Field(gt=0, description='产线ID')
|
||||
tunnel_id: int = Field(gt=0, description='隧道ID')
|
||||
line_id: int | None = Field(default=None, gt=0, description='产线ID(可选)')
|
||||
used_packages: int = Field(strict=True, gt=0, description='本次使用包数')
|
||||
idempotency_key: str = Field(min_length=1, max_length=64, pattern=r'\S', description='幂等键')
|
||||
fifo_skip_reason: str | None = Field(default=None, max_length=500, description='跳过先到托盘原因')
|
||||
|
||||
_normalize_reason = field_validator('fifo_skip_reason', mode='before')(_normalize_fifo_reason)
|
||||
|
||||
|
||||
class TunnelEntryCreateModel(BaseModel):
|
||||
"""托盘扫码入隧命令模型。"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
from_attributes=True,
|
||||
populate_by_name=True,
|
||||
extra='forbid',
|
||||
)
|
||||
|
||||
qr_token: str = Field(min_length=1, max_length=128, pattern=r'\S', description='二维码固定标识')
|
||||
tunnel_id: int = Field(gt=0, description='隧道ID')
|
||||
idempotency_key: str = Field(min_length=1, max_length=64, pattern=r'\S', description='幂等键')
|
||||
fifo_skip_reason: str | None = Field(default=None, max_length=500, description='跳过先到托盘原因')
|
||||
|
||||
_normalize_reason = field_validator('fifo_skip_reason', mode='before')(_normalize_fifo_reason)
|
||||
|
||||
|
||||
class TunnelEntrySuccessResponseModel(BaseModel):
|
||||
"""托盘扫码入隧成功响应模型。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
pallet_id: int = Field(gt=0, description='托盘ID')
|
||||
tunnel_id: int = Field(gt=0, description='隧道ID')
|
||||
trace_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='物料溯源号')
|
||||
|
||||
|
||||
class UsageSuccessResponseModel(BaseModel):
|
||||
@@ -244,6 +288,8 @@ class FillingModel(BaseModel):
|
||||
strain_code_snapshot: str = Field(min_length=1, max_length=64, pattern=r'\S', description='菌种编码快照')
|
||||
strain_name_snapshot: str = Field(min_length=1, max_length=100, pattern=r'\S', description='菌种名称快照')
|
||||
batch_no: str = Field(min_length=1, max_length=64, pattern=r'\S', description='生产批次')
|
||||
production_date: date | None = Field(default=None, description='生产日期;历史未登记时为空')
|
||||
expiry_date: date | None = Field(default=None, description='有效期至;历史未登记时为空')
|
||||
package_spec: str = Field(min_length=1, max_length=64, pattern=r'\S', description='包装规格')
|
||||
initial_packages: int = Field(strict=True, gt=0, description='初始包数')
|
||||
active_slot: Literal['ACTIVE'] | None = Field(default=None, description='活动装填槽')
|
||||
@@ -293,6 +339,9 @@ class ShipmentModel(BaseModel):
|
||||
confirm_request_hash: str | None = Field(default=None, max_length=64, description='确认请求摘要')
|
||||
confirmed_by: int | None = Field(default=None, gt=0, description='确认人ID')
|
||||
confirmed_at: datetime | None = Field(default=None, description='确认时间')
|
||||
receipt_state: Literal['PENDING', 'RECEIVED', 'LEGACY'] | None = Field(default=None, description='收货核对状态')
|
||||
received_by: int | None = Field(default=None, gt=0, description='收货核对人')
|
||||
received_at: datetime | None = Field(default=None, description='实际收货核对时间')
|
||||
create_by: str | None = Field(default=None, max_length=64, description='创建者')
|
||||
create_time: datetime | None = Field(default=None, description='创建时间')
|
||||
update_by: str | None = Field(default=None, max_length=64, description='更新者')
|
||||
@@ -311,13 +360,17 @@ class UsageModel(BaseModel):
|
||||
pallet_id: int = Field(gt=0, description='托盘ID')
|
||||
active_filling_id: int = Field(gt=0, description='活动装填记录ID')
|
||||
site_id: int = Field(gt=0, description='现场ID')
|
||||
line_id: int = Field(gt=0, description='产线ID')
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='隧道ID')
|
||||
trace_no: str | None = Field(default=None, max_length=64, description='物料溯源号')
|
||||
batch_no_snapshot: str | None = Field(default=None, max_length=64, description='生产批次快照')
|
||||
line_id: int | None = Field(default=None, gt=0, description='产线ID(可选)')
|
||||
used_packages: int = Field(strict=True, gt=0, description='本次使用包数')
|
||||
remaining_packages_after: int = Field(strict=True, ge=0, description='使用后剩余包数')
|
||||
idempotency_key: str = Field(min_length=1, max_length=64, pattern=r'\S', description='幂等键')
|
||||
request_hash: str = Field(min_length=1, max_length=64, pattern=r'\S', description='请求摘要')
|
||||
operator_id: int = Field(gt=0, description='操作人ID')
|
||||
occurred_at: datetime = Field(description='发生时间')
|
||||
fifo_skip_reason: str | None = Field(default=None, max_length=500, description='跳过先到托盘原因')
|
||||
|
||||
|
||||
class ReturnItemModel(BaseModel):
|
||||
@@ -329,6 +382,9 @@ class ReturnItemModel(BaseModel):
|
||||
return_id: int = Field(gt=0, description='退回单ID')
|
||||
pallet_id: int = Field(gt=0, description='托盘ID')
|
||||
active_filling_id_snapshot: int | None = Field(default=None, gt=0, description='申请时活动装填记录快照')
|
||||
tunnel_id_snapshot: int | None = Field(default=None, gt=0, description='申请时隧道ID快照')
|
||||
trace_no_snapshot: str | None = Field(default=None, max_length=64, description='申请时物料溯源号快照')
|
||||
batch_no_snapshot: str | None = Field(default=None, max_length=64, description='申请时生产批次快照')
|
||||
remaining_packages_snapshot: int = Field(strict=True, ge=0, description='申请时剩余包数快照')
|
||||
create_by: str | None = Field(default=None, max_length=64, description='创建者')
|
||||
create_time: datetime | None = Field(default=None, description='创建时间')
|
||||
@@ -420,6 +476,8 @@ class UsagePageQueryModel(BaseModel):
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
usage_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='使用记录号')
|
||||
site_id: int | None = Field(default=None, gt=0, description='现场ID')
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='隧道ID')
|
||||
trace_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='物料溯源号')
|
||||
line_id: int | None = Field(default=None, gt=0, description='产线ID')
|
||||
pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号')
|
||||
begin_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='开始时间')
|
||||
@@ -496,15 +554,20 @@ class PalletScanResponseModel(BaseModel):
|
||||
|
||||
pallet_id: int = Field(gt=0, description='托盘ID')
|
||||
pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号')
|
||||
qr_token: str | None = Field(default=None, min_length=1, max_length=128, description='解析后的固定二维码标识,写入时使用')
|
||||
flow_state: PalletFlowState = Field(description='托盘流转状态')
|
||||
warehouse_id: int | None = Field(default=None, gt=0, description='当前仓库ID')
|
||||
site_id: int | None = Field(default=None, gt=0, description='当前现场ID')
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='当前隧道ID')
|
||||
trace_no: str | None = Field(default=None, max_length=64, description='当前物料溯源号')
|
||||
current_line_id: int | None = Field(default=None, gt=0, description='当前绑定产线ID')
|
||||
active_filling_id: int | None = Field(default=None, gt=0, description='当前活动装填记录ID')
|
||||
strain_id: int | None = Field(default=None, gt=0, description='菌种ID')
|
||||
strain_code: str | None = Field(default=None, max_length=64, description='菌种编码')
|
||||
strain_name: str | None = Field(default=None, max_length=100, description='菌种名称')
|
||||
batch_no: str | None = Field(default=None, max_length=64, description='生产批次')
|
||||
production_date: date | None = Field(default=None, description='生产日期')
|
||||
expiry_date: date | None = Field(default=None, description='有效期至')
|
||||
package_spec: str | None = Field(default=None, max_length=64, description='包装规格')
|
||||
capacity_packages: int = Field(strict=True, gt=0, description='容量包数')
|
||||
remaining_packages: int = Field(strict=True, ge=0, description='当前剩余包数')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState, ReturnStatus
|
||||
@@ -28,7 +28,18 @@ def _validate_report_query_date_range(value: BaseModel) -> BaseModel:
|
||||
return value
|
||||
|
||||
|
||||
class PalletInventoryReportQueryModel(BaseModel):
|
||||
class TunnelReportQueryModel(BaseModel):
|
||||
"""隧道报表共享筛选;编号/名称可直接输入,溯源号按完整值匹配。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='隧道ID')
|
||||
tunnel_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='隧道编号')
|
||||
tunnel_name: str | None = Field(default=None, min_length=1, max_length=100, pattern=r'\S', description='隧道名称')
|
||||
trace_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='内部溯源号(精确匹配)')
|
||||
|
||||
|
||||
class PalletInventoryReportQueryModel(TunnelReportQueryModel):
|
||||
"""托盘库存报表查询模型。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
@@ -69,6 +80,12 @@ class PalletInventoryReportModel(BaseModel):
|
||||
initial_packages: int | None = Field(default=None, strict=True, gt=0, description='初始包数')
|
||||
remaining_packages: int = Field(strict=True, ge=0, description='当前剩余包数')
|
||||
updated_at: datetime = Field(description='库存更新时间')
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='当前隧道ID')
|
||||
tunnel_code: str | None = Field(default=None, max_length=64, description='当前隧道编号')
|
||||
tunnel_name: str | None = Field(default=None, max_length=100, description='当前隧道名称')
|
||||
trace_no: str | None = Field(default=None, max_length=64, description='当前内部溯源号')
|
||||
production_date: date | None = Field(default=None, description='当前物料生产日期,历史未登记为空')
|
||||
expiry_date: date | None = Field(default=None, description='当前物料有效期至,历史未登记为空')
|
||||
|
||||
|
||||
class FillingReportQueryModel(BaseModel):
|
||||
@@ -148,8 +165,8 @@ class ShipmentReportModel(BaseModel):
|
||||
package_qty: int = Field(strict=True, gt=0, description='发货时包数快照')
|
||||
|
||||
|
||||
class ConsumptionReportQueryModel(BaseModel):
|
||||
"""产线消耗报表查询模型。"""
|
||||
class ConsumptionReportQueryModel(TunnelReportQueryModel):
|
||||
"""隧道消耗报表查询模型,兼容历史产线筛选。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
@@ -168,7 +185,7 @@ class ConsumptionReportQueryModel(BaseModel):
|
||||
|
||||
|
||||
class ConsumptionReportModel(BaseModel):
|
||||
"""产线消耗报表行模型。"""
|
||||
"""隧道消耗报表行模型。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
@@ -179,7 +196,7 @@ class ConsumptionReportModel(BaseModel):
|
||||
customer_name: str | None = Field(default=None, max_length=100, description='客户名称')
|
||||
site_id: int = Field(gt=0, description='现场ID')
|
||||
site_name: str | None = Field(default=None, max_length=100, description='现场名称')
|
||||
line_id: int = Field(gt=0, description='产线ID')
|
||||
line_id: int | None = Field(default=None, gt=0, description='产线ID(可选)')
|
||||
line_name: str | None = Field(default=None, max_length=100, description='产线名称')
|
||||
pallet_id: int = Field(gt=0, description='托盘ID')
|
||||
pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号')
|
||||
@@ -191,9 +208,16 @@ class ConsumptionReportModel(BaseModel):
|
||||
remaining_packages_after: int = Field(strict=True, ge=0, description='使用后剩余包数')
|
||||
operator_id: int = Field(gt=0, description='操作人ID')
|
||||
operator_name: str | None = Field(default=None, max_length=100, description='操作人名称')
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='使用时隧道ID')
|
||||
tunnel_code: str | None = Field(default=None, max_length=64, description='使用时隧道编号')
|
||||
tunnel_name: str | None = Field(default=None, max_length=100, description='使用时隧道名称')
|
||||
trace_no: str | None = Field(default=None, max_length=64, description='使用时内部溯源号')
|
||||
batch_no_snapshot: str | None = Field(default=None, max_length=64, description='使用时生产批次快照')
|
||||
production_date: date | None = Field(default=None, description='使用时原装填物料生产日期')
|
||||
expiry_date: date | None = Field(default=None, description='使用时原装填物料有效期至')
|
||||
|
||||
|
||||
class ReturnReportQueryModel(BaseModel):
|
||||
class ReturnReportQueryModel(TunnelReportQueryModel):
|
||||
"""退回仓库报表查询模型。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
@@ -207,6 +231,7 @@ class ReturnReportQueryModel(BaseModel):
|
||||
target_warehouse_id: int | None = Field(default=None, gt=0, description='目标仓库ID')
|
||||
pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号')
|
||||
status: ReturnStatus | None = Field(default=None, description='退回状态')
|
||||
batch_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='退回生产批次')
|
||||
|
||||
_validate_date_range = model_validator(mode='after')(_validate_report_query_date_range)
|
||||
|
||||
@@ -229,9 +254,17 @@ class ReturnReportModel(BaseModel):
|
||||
pallet_code: str = Field(min_length=1, max_length=64, pattern=r'\S', description='托盘编号')
|
||||
active_filling_id_snapshot: int | None = Field(default=None, gt=0, description='活动装填记录快照')
|
||||
remaining_packages_snapshot: int = Field(strict=True, ge=0, description='剩余包数快照')
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='退回时隧道ID快照')
|
||||
tunnel_code: str | None = Field(default=None, max_length=64, description='退回隧道编号')
|
||||
tunnel_name: str | None = Field(default=None, max_length=100, description='退回隧道名称')
|
||||
trace_no: str | None = Field(default=None, max_length=64, description='退回时内部溯源号快照')
|
||||
batch_no_snapshot: str | None = Field(default=None, max_length=64, description='退回时生产批次快照')
|
||||
batch_no: str | None = Field(default=None, max_length=64, description='生产批次,历史记录回查原装填记录')
|
||||
production_date: date | None = Field(default=None, description='退回时原装填物料生产日期')
|
||||
expiry_date: date | None = Field(default=None, description='退回时原装填物料有效期至')
|
||||
|
||||
|
||||
class PalletTraceReportQueryModel(BaseModel):
|
||||
class PalletTraceReportQueryModel(TunnelReportQueryModel):
|
||||
"""托盘追溯报表查询模型。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
@@ -242,6 +275,7 @@ class PalletTraceReportQueryModel(BaseModel):
|
||||
end_time: str | None = Field(default=None, min_length=1, pattern=r'\S', description='结束时间')
|
||||
pallet_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='托盘编号')
|
||||
event_type: PalletEventType | None = Field(default=None, description='事件类型')
|
||||
batch_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='事件生产批次快照')
|
||||
|
||||
_validate_date_range = model_validator(mode='after')(_validate_report_query_date_range)
|
||||
|
||||
@@ -277,3 +311,29 @@ class PalletTraceReportModel(BaseModel):
|
||||
operator_id: int = Field(gt=0, description='操作人ID')
|
||||
operator_name: str | None = Field(default=None, max_length=100, description='操作人名称')
|
||||
occurred_at: datetime = Field(description='发生时间')
|
||||
tunnel_id: int | None = Field(default=None, gt=0, description='事件隧道ID,优先变更后隧道')
|
||||
tunnel_code: str | None = Field(default=None, max_length=64, description='事件隧道编号')
|
||||
tunnel_name: str | None = Field(default=None, max_length=100, description='事件隧道名称')
|
||||
trace_no: str | None = Field(default=None, max_length=64, description='事件内部溯源号')
|
||||
batch_no_snapshot: str | None = Field(default=None, max_length=64, description='事件生产批次快照')
|
||||
before_tunnel_id: int | None = Field(default=None, gt=0, description='变更前隧道ID')
|
||||
before_tunnel_code: str | None = Field(default=None, max_length=64, description='变更前隧道编号')
|
||||
before_tunnel_name: str | None = Field(default=None, max_length=100, description='变更前隧道名称')
|
||||
after_tunnel_id: int | None = Field(default=None, gt=0, description='变更后隧道ID')
|
||||
after_tunnel_code: str | None = Field(default=None, max_length=64, description='变更后隧道编号')
|
||||
after_tunnel_name: str | None = Field(default=None, max_length=100, description='变更后隧道名称')
|
||||
production_date: date | None = Field(default=None, description='事件原装填物料生产日期')
|
||||
expiry_date: date | None = Field(default=None, description='事件原装填物料有效期至')
|
||||
|
||||
@computed_field(description='本事件实际使用包数,仅使用事件计入')
|
||||
@property
|
||||
def used_packages(self) -> int:
|
||||
return (
|
||||
self.before_remaining_packages - self.after_remaining_packages
|
||||
if self.event_type == PalletEventType.USAGE_RECORDED else 0
|
||||
)
|
||||
|
||||
@computed_field(description='本事件确认退回包数,退回申请不重复计入')
|
||||
@property
|
||||
def returned_packages(self) -> int:
|
||||
return self.before_remaining_packages if self.event_type == PalletEventType.RETURN_CONFIRMED else 0
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
from module_business.domain.enums import DocumentStatus
|
||||
|
||||
|
||||
class ShipmentLabelModel(BaseModel):
|
||||
"""独立打印载荷,二维码由发货查询授权后生成,无需基础资料权限。"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
shipment_no: str
|
||||
shipment_status: DocumentStatus
|
||||
pallet_code: str
|
||||
customer_name: str
|
||||
site_name: str
|
||||
strain_name: str
|
||||
batch_no: str
|
||||
production_date: date | None = None
|
||||
expiry_date: date | None = None
|
||||
package_qty: int = Field(strict=True, gt=0)
|
||||
qr_code_data_url: str
|
||||
@@ -0,0 +1,38 @@
|
||||
from datetime import date
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
from module_business.entity.vo.operation_vo import PositiveId, ShipmentItemModel, ShipmentModel, ShipmentPageQueryModel
|
||||
|
||||
|
||||
class ShipmentReceiptCommandModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, extra='forbid')
|
||||
|
||||
scanned_pallet_ids: list[PositiveId] = Field(min_length=1, description='实际核对的托盘集合')
|
||||
idempotency_key: str = Field(min_length=1, max_length=64, pattern=r'\S')
|
||||
|
||||
@field_validator('scanned_pallet_ids')
|
||||
@classmethod
|
||||
def unique_pallets(cls, value: list[int]) -> list[int]:
|
||||
if len(value) != len(set(value)):
|
||||
raise ValueError('核对托盘不能重复')
|
||||
return value
|
||||
|
||||
|
||||
class ShipmentReceiptQueryModel(ShipmentPageQueryModel):
|
||||
receipt_state: Literal['PENDING', 'RECEIVED', 'LEGACY'] | None = None
|
||||
|
||||
|
||||
class ShipmentReceiptItemModel(ShipmentItemModel):
|
||||
pallet_code: str = Field(description='原托盘号')
|
||||
qr_token: str = Field(description='规范二维码标识')
|
||||
strain_name: str | None = None
|
||||
batch_no: str | None = None
|
||||
production_date: date | None = None
|
||||
expiry_date: date | None = None
|
||||
|
||||
|
||||
class ShipmentReceiptModel(ShipmentModel):
|
||||
items: list[ShipmentReceiptItemModel] = Field(default_factory=list)
|
||||
@@ -88,6 +88,7 @@ AdoptionRootType = Literal[
|
||||
'customer',
|
||||
'site',
|
||||
'line',
|
||||
'tunnel',
|
||||
'warehouse',
|
||||
'pallet',
|
||||
'inbound',
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
|
||||
class UsageFifoCandidateModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
pallet_id: int
|
||||
pallet_code: str
|
||||
batch_no: str
|
||||
remaining_packages: int
|
||||
site_arrived_at: datetime
|
||||
|
||||
|
||||
class UsageFifoRecommendationModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
recommended: UsageFifoCandidateModel | None = None
|
||||
unknown_arrival_count: int = 0
|
||||
requires_skip_reason: bool = False
|
||||
message: str
|
||||
Reference in New Issue
Block a user