为 Web 端改版提供名称、统计和看板数据。只新增只读查询、扩展列表与详情 返回字段;写接口、托盘状态机和幂等键规则均未改动,未新增权限标识。 - 基础资料:列表补充名称与汇总,客户详情返回现场树,托盘详情返回最近流转, 托盘列表支持按流转阶段筛选和二维码精确匹配;新增现场、产线、隧道的上级 资料选项接口;Excel 导入支持 preview 只校验不写入 - 发货出库:列表支持 keyword、receiptState 筛选并补充名称、托盘数、包数和 菌种构成;详情明细补充托盘号、菌种、批次 - 首次入库:列表与详情补充名称、托盘数、托盘当前状态;新增 /summary - 托盘退回:列表支持 keyword 并补充名称与托盘汇总,详情补充托盘号和隧道; 新增 /summary、/site-options - 工厂装填:列表支持 keyword 并补充托盘号、仓库、操作人;新增 /summary、 /batches、/batches/detail - 客户现场使用:列表支持 keyword、fifoSkipped 并补充名称和被跳过的先到托盘号; 新增 /site-options、/site-board 以及 usage-fifo 的 /queue - 先到先用 DAO 抽出公共候选条件与排序,建议查询生成的 SQL 与改动前一致 - 新增各汇总 DAO 的单元测试,同步更新路由权限与模型字段契约测试 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1052 lines
37 KiB
Python
1052 lines
37 KiB
Python
from datetime import datetime, time, timedelta
|
|
from typing import Any
|
|
|
|
from sqlalchemy import ColumnElement, Select, and_, delete, false, or_, select, update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import aliased
|
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
|
|
from common.vo import PageModel
|
|
from module_business.domain.enums import DocumentStatus, PalletFlowState
|
|
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
|
|
from module_business.entity.do.master_do import BizCustomer, BizCustomerSite, BizPallet, BizWarehouse
|
|
from module_business.entity.do.operation_do import (
|
|
BizFillingRecord,
|
|
BizInboundItem,
|
|
BizInboundOrder,
|
|
BizPalletInventory,
|
|
BizReturnItem,
|
|
BizReturnOrder,
|
|
BizShipmentItem,
|
|
BizShipmentOrder,
|
|
BizUsageRecord,
|
|
)
|
|
from module_business.entity.vo.operation_vo import (
|
|
FillingPageQueryModel,
|
|
InboundPageQueryModel,
|
|
ReturnPageQueryModel,
|
|
ShipmentPageQueryModel,
|
|
UsagePageQueryModel,
|
|
)
|
|
from utils.page_util import PageUtil
|
|
|
|
|
|
class OperationDao:
|
|
"""托盘入库、装填、发货、使用和退回单据数据库操作层。"""
|
|
|
|
@staticmethod
|
|
def _time_conditions(
|
|
column: InstrumentedAttribute,
|
|
begin_time: str | None,
|
|
end_time: str | None,
|
|
) -> list[ColumnElement[bool]]:
|
|
conditions = []
|
|
if begin_time:
|
|
begin_at = datetime.combine(
|
|
datetime.strptime(begin_time, '%Y-%m-%d').date(),
|
|
time.min,
|
|
)
|
|
conditions.append(column >= begin_at)
|
|
if end_time:
|
|
end_at = datetime.combine(
|
|
datetime.strptime(end_time, '%Y-%m-%d').date() + timedelta(days=1),
|
|
time.min,
|
|
)
|
|
conditions.append(column < end_at)
|
|
return conditions
|
|
|
|
@staticmethod
|
|
def _inbound_scope_conditions(
|
|
warehouse_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
) -> list[ColumnElement[bool]]:
|
|
if warehouse_ids is None:
|
|
return [batch_scope_condition(BizInboundOrder.test_batch_id, scope)]
|
|
scoped_warehouse_ids = sorted(set(warehouse_ids))
|
|
return [
|
|
batch_scope_condition(BizInboundOrder.test_batch_id, scope),
|
|
BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false(),
|
|
]
|
|
|
|
@staticmethod
|
|
def _filling_scope_conditions(
|
|
warehouse_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
) -> list[ColumnElement[bool]]:
|
|
if warehouse_ids is None:
|
|
return [batch_scope_condition(BizFillingRecord.test_batch_id, scope)]
|
|
scoped_warehouse_ids = sorted(set(warehouse_ids))
|
|
return [
|
|
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
|
BizFillingRecord.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false(),
|
|
]
|
|
|
|
@staticmethod
|
|
def _usage_scope_conditions(
|
|
site_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
) -> list[ColumnElement[bool]]:
|
|
if site_ids is None:
|
|
return [batch_scope_condition(BizUsageRecord.test_batch_id, scope)]
|
|
scoped_site_ids = sorted(set(site_ids))
|
|
return [
|
|
batch_scope_condition(BizUsageRecord.test_batch_id, scope),
|
|
BizUsageRecord.site_id.in_(scoped_site_ids) if scoped_site_ids else false(),
|
|
]
|
|
|
|
@staticmethod
|
|
def _shipment_scope_conditions(
|
|
warehouse_ids: list[int] | None,
|
|
site_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
) -> list[ColumnElement[bool]]:
|
|
conditions = [batch_scope_condition(BizShipmentOrder.test_batch_id, scope)]
|
|
if warehouse_ids is not None:
|
|
scoped_warehouse_ids = sorted(set(warehouse_ids))
|
|
conditions.append(
|
|
BizShipmentOrder.source_warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()
|
|
)
|
|
if site_ids is not None:
|
|
scoped_site_ids = sorted(set(site_ids))
|
|
conditions.append(BizShipmentOrder.target_site_id.in_(scoped_site_ids) if scoped_site_ids else false())
|
|
return conditions
|
|
|
|
@staticmethod
|
|
def _return_scope_conditions(
|
|
site_ids: list[int] | None,
|
|
warehouse_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
) -> list[ColumnElement[bool]]:
|
|
if site_ids is None and warehouse_ids is None:
|
|
return [batch_scope_condition(BizReturnOrder.test_batch_id, scope)]
|
|
|
|
visibility_conditions: list[ColumnElement[bool]] = []
|
|
if site_ids is not None:
|
|
scoped_site_ids = sorted(set(site_ids))
|
|
visibility_conditions.append(
|
|
BizReturnOrder.source_site_id.in_(scoped_site_ids) if scoped_site_ids else false()
|
|
)
|
|
if warehouse_ids is not None:
|
|
scoped_warehouse_ids = sorted(set(warehouse_ids))
|
|
visibility_conditions.append(
|
|
BizReturnOrder.target_warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()
|
|
)
|
|
return [batch_scope_condition(BizReturnOrder.test_batch_id, scope), or_(*visibility_conditions)]
|
|
|
|
@staticmethod
|
|
async def _paginate(
|
|
db: AsyncSession,
|
|
statement: Select,
|
|
query_object: Any,
|
|
is_page: bool,
|
|
) -> PageModel | list[dict[str, Any]]:
|
|
return await PageUtil.paginate(
|
|
db,
|
|
statement,
|
|
query_object.page_num,
|
|
query_object.page_size,
|
|
is_page,
|
|
exclude_fields={'test_batch_id'},
|
|
)
|
|
|
|
@staticmethod
|
|
async def _get_for_update(
|
|
db: AsyncSession,
|
|
model: type[Any],
|
|
id_column: InstrumentedAttribute,
|
|
record_id: int,
|
|
*extra_conditions: ColumnElement[bool],
|
|
) -> Any | None:
|
|
result = await db.execute(
|
|
select(model)
|
|
.where(id_column == record_id, *extra_conditions)
|
|
.with_for_update()
|
|
.execution_options(populate_existing=True)
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
@staticmethod
|
|
async def _get_by_key(
|
|
db: AsyncSession,
|
|
model: type[Any],
|
|
key_column: InstrumentedAttribute,
|
|
idempotency_key: str,
|
|
*extra_conditions: ColumnElement[bool],
|
|
) -> Any | None:
|
|
result = await db.execute(select(model).where(key_column == idempotency_key, *extra_conditions))
|
|
return result.scalar_one_or_none()
|
|
|
|
@staticmethod
|
|
async def _get_by_id(
|
|
db: AsyncSession,
|
|
model: type[Any],
|
|
id_column: InstrumentedAttribute,
|
|
record_id: int,
|
|
*extra_conditions: ColumnElement[bool],
|
|
) -> Any | None:
|
|
result = await db.execute(select(model).where(id_column == record_id, *extra_conditions))
|
|
return result.scalar_one_or_none()
|
|
|
|
@classmethod
|
|
async def _get_order_detail(
|
|
cls,
|
|
db: AsyncSession,
|
|
order_model: type[Any],
|
|
order_id_column: InstrumentedAttribute,
|
|
order_id: int,
|
|
item_model: type[Any],
|
|
item_order_id_column: InstrumentedAttribute,
|
|
item_id_column: InstrumentedAttribute,
|
|
scope: TestBatchScope,
|
|
*order_conditions: ColumnElement[bool],
|
|
) -> Any | None:
|
|
order = await cls._get_by_id(
|
|
db,
|
|
order_model,
|
|
order_id_column,
|
|
order_id,
|
|
*order_conditions,
|
|
)
|
|
if order is None:
|
|
return None
|
|
|
|
item_result = await db.execute(
|
|
select(item_model)
|
|
.where(item_order_id_column == order_id, batch_scope_condition(item_model.test_batch_id, scope))
|
|
.order_by(item_id_column)
|
|
)
|
|
order.items = list(item_result.scalars().all())
|
|
return order
|
|
|
|
@staticmethod
|
|
async def _add(db: AsyncSession, record: Any) -> Any:
|
|
db.add(record)
|
|
await db.flush()
|
|
return record
|
|
|
|
@staticmethod
|
|
async def _update(
|
|
db: AsyncSession,
|
|
model: type[Any],
|
|
id_column: InstrumentedAttribute,
|
|
record: Any,
|
|
) -> Any:
|
|
primary_key = id_column.key
|
|
payload = {
|
|
column.key: vars(record)[column.key]
|
|
for column in model.__table__.columns
|
|
if column.key not in {primary_key, 'create_by', 'create_time'} and column.key in vars(record)
|
|
}
|
|
if payload:
|
|
await db.execute(update(model).where(id_column == getattr(record, primary_key)).values(**payload))
|
|
await db.flush()
|
|
return record
|
|
|
|
@staticmethod
|
|
async def _delete(
|
|
db: AsyncSession,
|
|
model: type[Any],
|
|
id_column: InstrumentedAttribute,
|
|
record_id: int,
|
|
) -> None:
|
|
await db.execute(delete(model).where(id_column == record_id))
|
|
await db.flush()
|
|
|
|
@staticmethod
|
|
async def _replace_items(
|
|
db: AsyncSession,
|
|
item_model: type[Any],
|
|
order_id_column: InstrumentedAttribute,
|
|
order_id: int,
|
|
items: list[Any],
|
|
) -> list[Any]:
|
|
await db.execute(delete(item_model).where(order_id_column == order_id))
|
|
for item in items:
|
|
setattr(item, order_id_column.key, order_id)
|
|
if items:
|
|
db.add_all(items)
|
|
await db.flush()
|
|
return items
|
|
|
|
@staticmethod
|
|
async def _add_items(db: AsyncSession, items: list[Any]) -> list[Any]:
|
|
if items:
|
|
db.add_all(items)
|
|
await db.flush()
|
|
return items
|
|
|
|
@classmethod
|
|
async def get_inbound_for_update(
|
|
cls,
|
|
db: AsyncSession,
|
|
inbound_id: int,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
) -> BizInboundOrder | None:
|
|
inbound = await cls._get_for_update(
|
|
db,
|
|
BizInboundOrder,
|
|
BizInboundOrder.inbound_id,
|
|
inbound_id,
|
|
*cls._inbound_scope_conditions(warehouse_ids, scope),
|
|
)
|
|
if inbound is None:
|
|
return None
|
|
|
|
# 父单锁定后用 locking read 获取最新明细,避免 MySQL RR 继续读取确认前的旧快照。
|
|
item_result = await db.execute(
|
|
select(BizInboundItem)
|
|
.where(
|
|
BizInboundItem.inbound_id == inbound_id,
|
|
batch_scope_condition(BizInboundItem.test_batch_id, scope),
|
|
)
|
|
.order_by(BizInboundItem.pallet_id, BizInboundItem.inbound_item_id)
|
|
.with_for_update()
|
|
.execution_options(populate_existing=True)
|
|
)
|
|
inbound.items = sorted(
|
|
item_result.scalars().all(),
|
|
key=lambda item: (item.pallet_id, item.inbound_item_id),
|
|
)
|
|
return inbound
|
|
|
|
@classmethod
|
|
async def get_shipment_for_update(
|
|
cls,
|
|
db: AsyncSession,
|
|
shipment_id: int,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
site_ids: list[int] | None = None,
|
|
) -> BizShipmentOrder | None:
|
|
shipment = await cls._get_for_update(
|
|
db,
|
|
BizShipmentOrder,
|
|
BizShipmentOrder.shipment_id,
|
|
shipment_id,
|
|
*cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
|
|
)
|
|
if shipment is None:
|
|
return None
|
|
|
|
# 父单锁定后读取当前明细,避免 MySQL RR 沿用确认前的快照。
|
|
item_result = await db.execute(
|
|
select(BizShipmentItem)
|
|
.where(
|
|
BizShipmentItem.shipment_id == shipment_id,
|
|
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
|
|
)
|
|
.order_by(BizShipmentItem.pallet_id, BizShipmentItem.shipment_item_id)
|
|
.with_for_update()
|
|
.execution_options(populate_existing=True)
|
|
)
|
|
shipment.items = sorted(
|
|
item_result.scalars().all(),
|
|
key=lambda item: (item.pallet_id, item.shipment_item_id),
|
|
)
|
|
return shipment
|
|
|
|
@classmethod
|
|
async def get_return_for_update(
|
|
cls,
|
|
db: AsyncSession,
|
|
return_id: int,
|
|
scope: TestBatchScope,
|
|
site_ids: list[int] | None = None,
|
|
warehouse_ids: list[int] | None = None,
|
|
) -> BizReturnOrder | None:
|
|
return_order = await cls._get_for_update(
|
|
db,
|
|
BizReturnOrder,
|
|
BizReturnOrder.return_id,
|
|
return_id,
|
|
*cls._return_scope_conditions(site_ids, warehouse_ids, scope),
|
|
)
|
|
if return_order is None:
|
|
return None
|
|
|
|
# 父单锁定后使用 locking read 获取当前明细,避免 MySQL RR 沿用旧快照。
|
|
item_result = await db.execute(
|
|
select(BizReturnItem)
|
|
.where(
|
|
BizReturnItem.return_id == return_id,
|
|
batch_scope_condition(BizReturnItem.test_batch_id, scope),
|
|
)
|
|
.order_by(BizReturnItem.pallet_id, BizReturnItem.return_item_id)
|
|
.with_for_update()
|
|
.execution_options(populate_existing=True)
|
|
)
|
|
return_order.items = sorted(
|
|
item_result.scalars().all(),
|
|
key=lambda item: (item.pallet_id, item.return_item_id),
|
|
)
|
|
return return_order
|
|
|
|
@classmethod
|
|
async def get_active_filling_for_update(
|
|
cls,
|
|
db: AsyncSession,
|
|
filling_id: int,
|
|
scope: TestBatchScope,
|
|
) -> BizFillingRecord | None:
|
|
"""按已锁库存的 active_filling_id 锁定仍处于活动状态的装填周期。"""
|
|
return await cls._get_for_update(
|
|
db,
|
|
BizFillingRecord,
|
|
BizFillingRecord.filling_id,
|
|
filling_id,
|
|
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
|
BizFillingRecord.active_slot == 'ACTIVE',
|
|
)
|
|
|
|
@classmethod
|
|
async def get_inbound_by_confirm_idempotency_key(
|
|
cls,
|
|
db: AsyncSession,
|
|
idempotency_key: str,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
) -> BizInboundOrder | None:
|
|
return await cls._get_by_key(
|
|
db,
|
|
BizInboundOrder,
|
|
BizInboundOrder.confirm_idempotency_key,
|
|
idempotency_key,
|
|
*cls._inbound_scope_conditions(warehouse_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def get_shipment_by_confirm_idempotency_key(
|
|
cls,
|
|
db: AsyncSession,
|
|
idempotency_key: str,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
site_ids: list[int] | None = None,
|
|
) -> BizShipmentOrder | None:
|
|
return await cls._get_by_key(
|
|
db,
|
|
BizShipmentOrder,
|
|
BizShipmentOrder.confirm_idempotency_key,
|
|
idempotency_key,
|
|
*cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def get_filling_by_idempotency_key(
|
|
cls,
|
|
db: AsyncSession,
|
|
idempotency_key: str,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
) -> BizFillingRecord | None:
|
|
return await cls._get_by_key(
|
|
db,
|
|
BizFillingRecord,
|
|
BizFillingRecord.idempotency_key,
|
|
idempotency_key,
|
|
*cls._filling_scope_conditions(warehouse_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def get_usage_by_idempotency_key(
|
|
cls,
|
|
db: AsyncSession,
|
|
idempotency_key: str,
|
|
scope: TestBatchScope,
|
|
site_ids: list[int] | None = None,
|
|
) -> BizUsageRecord | None:
|
|
return await cls._get_by_key(
|
|
db,
|
|
BizUsageRecord,
|
|
BizUsageRecord.idempotency_key,
|
|
idempotency_key,
|
|
*cls._usage_scope_conditions(site_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def get_return_by_request_idempotency_key(
|
|
cls,
|
|
db: AsyncSession,
|
|
idempotency_key: str,
|
|
source_site_id: int,
|
|
target_warehouse_id: int,
|
|
scope: TestBatchScope,
|
|
) -> BizReturnOrder | None:
|
|
return await cls._get_by_key(
|
|
db,
|
|
BizReturnOrder,
|
|
BizReturnOrder.request_idempotency_key,
|
|
idempotency_key,
|
|
BizReturnOrder.source_site_id == source_site_id,
|
|
BizReturnOrder.target_warehouse_id == target_warehouse_id,
|
|
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def get_return_by_confirm_idempotency_key(
|
|
cls,
|
|
db: AsyncSession,
|
|
idempotency_key: str,
|
|
source_site_id: int,
|
|
target_warehouse_id: int,
|
|
scope: TestBatchScope,
|
|
) -> BizReturnOrder | None:
|
|
return await cls._get_by_key(
|
|
db,
|
|
BizReturnOrder,
|
|
BizReturnOrder.confirm_idempotency_key,
|
|
idempotency_key,
|
|
BizReturnOrder.source_site_id == source_site_id,
|
|
BizReturnOrder.target_warehouse_id == target_warehouse_id,
|
|
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def get_inbound_list(
|
|
cls,
|
|
db: AsyncSession,
|
|
query_object: InboundPageQueryModel,
|
|
warehouse_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
is_page: bool = True,
|
|
) -> PageModel | list[dict[str, Any]]:
|
|
conditions = cls._inbound_scope_conditions(warehouse_ids, scope)
|
|
if query_object.inbound_no:
|
|
conditions.append(BizInboundOrder.inbound_no.like(f'%{query_object.inbound_no}%'))
|
|
if query_object.warehouse_id:
|
|
conditions.append(BizInboundOrder.warehouse_id == query_object.warehouse_id)
|
|
if query_object.status:
|
|
conditions.append(BizInboundOrder.status == query_object.status.value)
|
|
conditions.extend(
|
|
cls._time_conditions(
|
|
BizInboundOrder.create_time,
|
|
query_object.begin_time,
|
|
query_object.end_time,
|
|
)
|
|
)
|
|
statement = select(BizInboundOrder).where(*conditions).order_by(BizInboundOrder.inbound_id)
|
|
return await cls._paginate(db, statement, query_object, is_page)
|
|
|
|
@classmethod
|
|
async def get_inbound_by_id(
|
|
cls,
|
|
db: AsyncSession,
|
|
inbound_id: int,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
) -> BizInboundOrder | None:
|
|
return await cls._get_order_detail(
|
|
db,
|
|
BizInboundOrder,
|
|
BizInboundOrder.inbound_id,
|
|
inbound_id,
|
|
BizInboundItem,
|
|
BizInboundItem.inbound_id,
|
|
BizInboundItem.inbound_item_id,
|
|
scope,
|
|
*cls._inbound_scope_conditions(warehouse_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def add_inbound(
|
|
cls,
|
|
db: AsyncSession,
|
|
inbound: BizInboundOrder,
|
|
) -> BizInboundOrder:
|
|
return await cls._add(db, inbound)
|
|
|
|
@classmethod
|
|
async def update_inbound(
|
|
cls,
|
|
db: AsyncSession,
|
|
inbound: BizInboundOrder,
|
|
) -> BizInboundOrder:
|
|
return await cls._update(
|
|
db,
|
|
BizInboundOrder,
|
|
BizInboundOrder.inbound_id,
|
|
inbound,
|
|
)
|
|
|
|
@classmethod
|
|
async def delete_inbound(cls, db: AsyncSession, inbound_id: int) -> None:
|
|
await db.execute(delete(BizInboundItem).where(BizInboundItem.inbound_id == inbound_id))
|
|
await cls._delete(db, BizInboundOrder, BizInboundOrder.inbound_id, inbound_id)
|
|
|
|
@classmethod
|
|
async def replace_inbound_items(
|
|
cls,
|
|
db: AsyncSession,
|
|
inbound_id: int,
|
|
items: list[BizInboundItem],
|
|
) -> list[BizInboundItem]:
|
|
return await cls._replace_items(
|
|
db,
|
|
BizInboundItem,
|
|
BizInboundItem.inbound_id,
|
|
inbound_id,
|
|
items,
|
|
)
|
|
|
|
@classmethod
|
|
async def get_filling_list(
|
|
cls,
|
|
db: AsyncSession,
|
|
query_object: FillingPageQueryModel,
|
|
warehouse_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
is_page: bool = True,
|
|
) -> PageModel | list[dict[str, Any]]:
|
|
conditions = cls._filling_scope_conditions(warehouse_ids, scope)
|
|
if query_object.filling_no:
|
|
conditions.append(BizFillingRecord.filling_no.like(f'%{query_object.filling_no}%'))
|
|
if query_object.warehouse_id:
|
|
conditions.append(BizFillingRecord.warehouse_id == query_object.warehouse_id)
|
|
if query_object.strain_id:
|
|
conditions.append(BizFillingRecord.strain_id == query_object.strain_id)
|
|
if query_object.batch_no:
|
|
conditions.append(BizFillingRecord.batch_no.like(f'%{query_object.batch_no}%'))
|
|
if query_object.pallet_code:
|
|
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
|
|
if query_object.keyword:
|
|
keyword = f'%{query_object.keyword}%'
|
|
conditions.append(
|
|
or_(
|
|
BizFillingRecord.filling_no.like(keyword),
|
|
BizPallet.pallet_code.like(keyword),
|
|
BizFillingRecord.batch_no.like(keyword),
|
|
)
|
|
)
|
|
conditions.extend(
|
|
cls._time_conditions(
|
|
BizFillingRecord.operated_at,
|
|
query_object.begin_time,
|
|
query_object.end_time,
|
|
)
|
|
)
|
|
statement = select(BizFillingRecord)
|
|
if query_object.pallet_code or query_object.keyword:
|
|
statement = statement.join(
|
|
BizPallet,
|
|
BizPallet.pallet_id == BizFillingRecord.pallet_id,
|
|
)
|
|
statement = statement.where(*conditions).order_by(BizFillingRecord.filling_id)
|
|
return await cls._paginate(db, statement, query_object, is_page)
|
|
|
|
@classmethod
|
|
async def get_filling_by_id(
|
|
cls,
|
|
db: AsyncSession,
|
|
filling_id: int,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
) -> BizFillingRecord | None:
|
|
return await cls._get_by_id(
|
|
db,
|
|
BizFillingRecord,
|
|
BizFillingRecord.filling_id,
|
|
filling_id,
|
|
*cls._filling_scope_conditions(warehouse_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def add_filling_record(
|
|
cls,
|
|
db: AsyncSession,
|
|
filling: BizFillingRecord,
|
|
) -> BizFillingRecord:
|
|
return await cls._add(db, filling)
|
|
|
|
@classmethod
|
|
async def update_filling_record(
|
|
cls,
|
|
db: AsyncSession,
|
|
filling: BizFillingRecord,
|
|
) -> BizFillingRecord:
|
|
return await cls._update(
|
|
db,
|
|
BizFillingRecord,
|
|
BizFillingRecord.filling_id,
|
|
filling,
|
|
)
|
|
|
|
@classmethod
|
|
async def get_shipment_list(
|
|
cls,
|
|
db: AsyncSession,
|
|
query_object: ShipmentPageQueryModel,
|
|
warehouse_ids: list[int] | None,
|
|
site_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
is_page: bool = True,
|
|
) -> PageModel | list[dict[str, Any]]:
|
|
conditions = cls._shipment_scope_conditions(warehouse_ids, site_ids, scope)
|
|
if query_object.shipment_no:
|
|
conditions.append(BizShipmentOrder.shipment_no.like(f'%{query_object.shipment_no}%'))
|
|
if query_object.source_warehouse_id:
|
|
conditions.append(BizShipmentOrder.source_warehouse_id == query_object.source_warehouse_id)
|
|
if query_object.target_site_id:
|
|
conditions.append(BizShipmentOrder.target_site_id == query_object.target_site_id)
|
|
if query_object.status:
|
|
conditions.append(BizShipmentOrder.status == query_object.status.value)
|
|
if query_object.receipt_state:
|
|
# 收货状态只在确认后有意义,草稿的默认值不参与筛选
|
|
conditions.append(BizShipmentOrder.status == DocumentStatus.CONFIRMED.value)
|
|
conditions.append(BizShipmentOrder.receipt_state == query_object.receipt_state)
|
|
conditions.extend(
|
|
cls._time_conditions(
|
|
BizShipmentOrder.create_time,
|
|
query_object.begin_time,
|
|
query_object.end_time,
|
|
)
|
|
)
|
|
statement = select(BizShipmentOrder)
|
|
if query_object.keyword:
|
|
keyword = f'%{query_object.keyword}%'
|
|
source_warehouse = aliased(BizWarehouse, name='source_warehouse')
|
|
target_site = aliased(BizCustomerSite, name='target_site')
|
|
target_customer = aliased(BizCustomer, name='target_customer')
|
|
# 名称关联与单据处于同一测试批次范围,跨批次的主数据不会被匹配
|
|
statement = (
|
|
statement.outerjoin(
|
|
source_warehouse,
|
|
and_(
|
|
source_warehouse.warehouse_id == BizShipmentOrder.source_warehouse_id,
|
|
batch_scope_condition(source_warehouse.test_batch_id, scope),
|
|
),
|
|
)
|
|
.outerjoin(
|
|
target_site,
|
|
and_(
|
|
target_site.site_id == BizShipmentOrder.target_site_id,
|
|
batch_scope_condition(target_site.test_batch_id, scope),
|
|
),
|
|
)
|
|
.outerjoin(
|
|
target_customer,
|
|
and_(
|
|
target_customer.customer_id == target_site.customer_id,
|
|
batch_scope_condition(target_customer.test_batch_id, scope),
|
|
),
|
|
)
|
|
)
|
|
conditions.append(
|
|
or_(
|
|
BizShipmentOrder.shipment_no.like(keyword),
|
|
source_warehouse.warehouse_name.like(keyword),
|
|
target_site.site_name.like(keyword),
|
|
target_customer.customer_name.like(keyword),
|
|
)
|
|
)
|
|
statement = statement.where(*conditions).order_by(BizShipmentOrder.shipment_id)
|
|
return await cls._paginate(db, statement, query_object, is_page)
|
|
|
|
@classmethod
|
|
async def get_shipment_by_id(
|
|
cls,
|
|
db: AsyncSession,
|
|
shipment_id: int,
|
|
scope: TestBatchScope,
|
|
warehouse_ids: list[int] | None = None,
|
|
site_ids: list[int] | None = None,
|
|
) -> BizShipmentOrder | None:
|
|
shipment = await cls._get_by_id(
|
|
db,
|
|
BizShipmentOrder,
|
|
BizShipmentOrder.shipment_id,
|
|
shipment_id,
|
|
*cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
|
|
)
|
|
if shipment is None:
|
|
return None
|
|
|
|
if shipment.status == DocumentStatus.DRAFT.value:
|
|
item_result = await db.execute(
|
|
select(BizShipmentItem, BizPalletInventory.remaining_packages)
|
|
.outerjoin(
|
|
BizPalletInventory,
|
|
and_(
|
|
BizPalletInventory.pallet_id == BizShipmentItem.pallet_id,
|
|
BizPalletInventory.warehouse_id == shipment.source_warehouse_id,
|
|
BizPalletInventory.flow_state == PalletFlowState.FILLED_IN_WAREHOUSE.value,
|
|
BizPalletInventory.site_id.is_(None),
|
|
BizPalletInventory.current_line_id.is_(None),
|
|
BizPalletInventory.active_filling_id.is_not(None),
|
|
BizPalletInventory.remaining_packages > 0,
|
|
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
|
),
|
|
)
|
|
.where(
|
|
BizShipmentItem.shipment_id == shipment_id,
|
|
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
|
|
)
|
|
.order_by(BizShipmentItem.shipment_item_id)
|
|
)
|
|
items = []
|
|
for item, remaining_packages in item_result.all():
|
|
item.remaining_packages = remaining_packages
|
|
items.append(item)
|
|
else:
|
|
item_result = await db.execute(
|
|
select(BizShipmentItem)
|
|
.where(
|
|
BizShipmentItem.shipment_id == shipment_id,
|
|
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
|
|
)
|
|
.order_by(BizShipmentItem.shipment_item_id)
|
|
)
|
|
items = list(item_result.scalars().all())
|
|
for item in items:
|
|
item.remaining_packages = None
|
|
shipment.items = items
|
|
return shipment
|
|
|
|
@classmethod
|
|
async def add_shipment(
|
|
cls,
|
|
db: AsyncSession,
|
|
shipment: BizShipmentOrder,
|
|
) -> BizShipmentOrder:
|
|
return await cls._add(db, shipment)
|
|
|
|
@classmethod
|
|
async def update_shipment(
|
|
cls,
|
|
db: AsyncSession,
|
|
shipment: BizShipmentOrder,
|
|
) -> BizShipmentOrder:
|
|
return await cls._update(
|
|
db,
|
|
BizShipmentOrder,
|
|
BizShipmentOrder.shipment_id,
|
|
shipment,
|
|
)
|
|
|
|
@classmethod
|
|
async def delete_shipment(cls, db: AsyncSession, shipment_id: int) -> None:
|
|
await db.execute(delete(BizShipmentItem).where(BizShipmentItem.shipment_id == shipment_id))
|
|
await cls._delete(
|
|
db,
|
|
BizShipmentOrder,
|
|
BizShipmentOrder.shipment_id,
|
|
shipment_id,
|
|
)
|
|
|
|
@classmethod
|
|
async def replace_shipment_items(
|
|
cls,
|
|
db: AsyncSession,
|
|
shipment_id: int,
|
|
items: list[BizShipmentItem],
|
|
) -> list[BizShipmentItem]:
|
|
return await cls._replace_items(
|
|
db,
|
|
BizShipmentItem,
|
|
BizShipmentItem.shipment_id,
|
|
shipment_id,
|
|
items,
|
|
)
|
|
|
|
@classmethod
|
|
async def get_usage_list(
|
|
cls,
|
|
db: AsyncSession,
|
|
query_object: UsagePageQueryModel,
|
|
site_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
is_page: bool = True,
|
|
) -> PageModel | list[dict[str, Any]]:
|
|
conditions = cls._usage_scope_conditions(site_ids, scope)
|
|
if query_object.usage_no:
|
|
conditions.append(BizUsageRecord.usage_no.like(f'%{query_object.usage_no}%'))
|
|
if query_object.site_id:
|
|
conditions.append(BizUsageRecord.site_id == query_object.site_id)
|
|
if query_object.tunnel_id:
|
|
conditions.append(BizUsageRecord.tunnel_id == query_object.tunnel_id)
|
|
if query_object.trace_no:
|
|
conditions.append(BizUsageRecord.trace_no.like(f'%{query_object.trace_no}%'))
|
|
if query_object.line_id:
|
|
conditions.append(BizUsageRecord.line_id == query_object.line_id)
|
|
if query_object.pallet_code:
|
|
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
|
|
if query_object.keyword:
|
|
keyword = f'%{query_object.keyword}%'
|
|
conditions.append(or_(BizUsageRecord.usage_no.like(keyword), BizPallet.pallet_code.like(keyword)))
|
|
if query_object.fifo_skipped:
|
|
conditions.append(BizUsageRecord.fifo_skip_reason.is_not(None))
|
|
conditions.extend(
|
|
cls._time_conditions(
|
|
BizUsageRecord.occurred_at,
|
|
query_object.begin_time,
|
|
query_object.end_time,
|
|
)
|
|
)
|
|
statement = select(BizUsageRecord)
|
|
if query_object.pallet_code or query_object.keyword:
|
|
statement = statement.join(
|
|
BizPallet,
|
|
BizPallet.pallet_id == BizUsageRecord.pallet_id,
|
|
)
|
|
statement = statement.where(*conditions).order_by(BizUsageRecord.usage_id)
|
|
return await cls._paginate(db, statement, query_object, is_page)
|
|
|
|
@classmethod
|
|
async def get_usage_by_id(
|
|
cls,
|
|
db: AsyncSession,
|
|
usage_id: int,
|
|
scope: TestBatchScope,
|
|
site_ids: list[int] | None = None,
|
|
) -> BizUsageRecord | None:
|
|
return await cls._get_by_id(
|
|
db,
|
|
BizUsageRecord,
|
|
BizUsageRecord.usage_id,
|
|
usage_id,
|
|
*cls._usage_scope_conditions(site_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def add_usage_record(
|
|
cls,
|
|
db: AsyncSession,
|
|
usage: BizUsageRecord,
|
|
) -> BizUsageRecord:
|
|
return await cls._add(db, usage)
|
|
|
|
@classmethod
|
|
async def get_return_list(
|
|
cls,
|
|
db: AsyncSession,
|
|
query_object: ReturnPageQueryModel,
|
|
site_ids: list[int] | None,
|
|
warehouse_ids: list[int] | None,
|
|
scope: TestBatchScope,
|
|
is_page: bool = True,
|
|
) -> PageModel | list[dict[str, Any]]:
|
|
conditions = cls._return_scope_conditions(site_ids, warehouse_ids, scope)
|
|
if query_object.return_no:
|
|
conditions.append(BizReturnOrder.return_no.like(f'%{query_object.return_no}%'))
|
|
if query_object.source_site_id:
|
|
conditions.append(BizReturnOrder.source_site_id == query_object.source_site_id)
|
|
if query_object.target_warehouse_id:
|
|
conditions.append(BizReturnOrder.target_warehouse_id == query_object.target_warehouse_id)
|
|
if query_object.status:
|
|
conditions.append(BizReturnOrder.status == query_object.status.value)
|
|
conditions.extend(
|
|
cls._time_conditions(
|
|
BizReturnOrder.requested_at,
|
|
query_object.begin_time,
|
|
query_object.end_time,
|
|
)
|
|
)
|
|
statement = select(BizReturnOrder)
|
|
if query_object.keyword:
|
|
keyword = f'%{query_object.keyword}%'
|
|
source_site = aliased(BizCustomerSite, name='source_site')
|
|
source_customer = aliased(BizCustomer, name='source_customer')
|
|
target_warehouse = aliased(BizWarehouse, name='target_warehouse')
|
|
# 名称关联与单据处于同一测试批次范围,跨批次的主数据不会被匹配
|
|
statement = (
|
|
statement.outerjoin(
|
|
source_site,
|
|
and_(
|
|
source_site.site_id == BizReturnOrder.source_site_id,
|
|
batch_scope_condition(source_site.test_batch_id, scope),
|
|
),
|
|
)
|
|
.outerjoin(
|
|
source_customer,
|
|
and_(
|
|
source_customer.customer_id == source_site.customer_id,
|
|
batch_scope_condition(source_customer.test_batch_id, scope),
|
|
),
|
|
)
|
|
.outerjoin(
|
|
target_warehouse,
|
|
and_(
|
|
target_warehouse.warehouse_id == BizReturnOrder.target_warehouse_id,
|
|
batch_scope_condition(target_warehouse.test_batch_id, scope),
|
|
),
|
|
)
|
|
)
|
|
# 托盘编号按明细存在性匹配,不改变每张单据只出现一行
|
|
pallet_match = (
|
|
select(BizReturnItem.return_item_id)
|
|
.join(
|
|
BizPallet,
|
|
and_(
|
|
BizPallet.pallet_id == BizReturnItem.pallet_id,
|
|
batch_scope_condition(BizPallet.test_batch_id, scope),
|
|
),
|
|
)
|
|
.where(
|
|
BizReturnItem.return_id == BizReturnOrder.return_id,
|
|
batch_scope_condition(BizReturnItem.test_batch_id, scope),
|
|
BizPallet.pallet_code.like(keyword),
|
|
)
|
|
.exists()
|
|
)
|
|
conditions.append(
|
|
or_(
|
|
BizReturnOrder.return_no.like(keyword),
|
|
source_site.site_name.like(keyword),
|
|
source_customer.customer_name.like(keyword),
|
|
target_warehouse.warehouse_name.like(keyword),
|
|
pallet_match,
|
|
)
|
|
)
|
|
statement = statement.where(*conditions).order_by(BizReturnOrder.return_id)
|
|
return await cls._paginate(db, statement, query_object, is_page)
|
|
|
|
@classmethod
|
|
async def get_return_by_id(
|
|
cls,
|
|
db: AsyncSession,
|
|
return_id: int,
|
|
scope: TestBatchScope,
|
|
site_ids: list[int] | None = None,
|
|
warehouse_ids: list[int] | None = None,
|
|
) -> BizReturnOrder | None:
|
|
return await cls._get_order_detail(
|
|
db,
|
|
BizReturnOrder,
|
|
BizReturnOrder.return_id,
|
|
return_id,
|
|
BizReturnItem,
|
|
BizReturnItem.return_id,
|
|
BizReturnItem.return_item_id,
|
|
scope,
|
|
*cls._return_scope_conditions(site_ids, warehouse_ids, scope),
|
|
)
|
|
|
|
@classmethod
|
|
async def add_return_order(
|
|
cls,
|
|
db: AsyncSession,
|
|
return_order: BizReturnOrder,
|
|
) -> BizReturnOrder:
|
|
return await cls._add(db, return_order)
|
|
|
|
@classmethod
|
|
async def update_return_order(
|
|
cls,
|
|
db: AsyncSession,
|
|
return_order: BizReturnOrder,
|
|
) -> BizReturnOrder:
|
|
return await cls._update(
|
|
db,
|
|
BizReturnOrder,
|
|
BizReturnOrder.return_id,
|
|
return_order,
|
|
)
|
|
|
|
@classmethod
|
|
async def add_return_items(
|
|
cls,
|
|
db: AsyncSession,
|
|
items: list[BizReturnItem],
|
|
) -> list[BizReturnItem]:
|
|
return await cls._add_items(db, items)
|