from datetime import datetime, time, timedelta from typing import Any from sqlalchemy import ColumnElement, Select, delete, false, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm.attributes import InstrumentedAttribute from common.vo import PageModel from module_business.entity.do.master_do import BizPallet from module_business.entity.do.operation_do import ( BizFillingRecord, BizInboundItem, BizInboundOrder, 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, ) -> list[ColumnElement[bool]]: if warehouse_ids is None: return [] scoped_warehouse_ids = sorted(set(warehouse_ids)) return [BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()] @staticmethod def _filling_scope_conditions( warehouse_ids: list[int] | None, ) -> list[ColumnElement[bool]]: if warehouse_ids is None: return [] scoped_warehouse_ids = sorted(set(warehouse_ids)) return [BizFillingRecord.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()] @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, ) @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, *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).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, 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), ) if inbound is None: return None # 父单锁定后用 locking read 获取最新明细,避免 MySQL RR 继续读取确认前的旧快照。 item_result = await db.execute( select(BizInboundItem) .where(BizInboundItem.inbound_id == inbound_id) .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, ) -> BizShipmentOrder | None: return await cls._get_for_update( db, BizShipmentOrder, BizShipmentOrder.shipment_id, shipment_id, ) @classmethod async def get_return_for_update( cls, db: AsyncSession, return_id: int, ) -> BizReturnOrder | None: return await cls._get_for_update( db, BizReturnOrder, BizReturnOrder.return_id, return_id, ) @classmethod async def get_active_filling_for_update( cls, db: AsyncSession, filling_id: int, ) -> BizFillingRecord | None: """按已锁库存的 active_filling_id 锁定仍处于活动状态的装填周期。""" return await cls._get_for_update( db, BizFillingRecord, BizFillingRecord.filling_id, filling_id, BizFillingRecord.active_slot == 'ACTIVE', ) @classmethod async def get_inbound_by_confirm_idempotency_key( cls, db: AsyncSession, idempotency_key: str, ) -> BizInboundOrder | None: return await cls._get_by_key( db, BizInboundOrder, BizInboundOrder.confirm_idempotency_key, idempotency_key, ) @classmethod async def get_shipment_by_confirm_idempotency_key( cls, db: AsyncSession, idempotency_key: str, ) -> BizShipmentOrder | None: return await cls._get_by_key( db, BizShipmentOrder, BizShipmentOrder.confirm_idempotency_key, idempotency_key, ) @classmethod async def get_filling_by_idempotency_key( cls, db: AsyncSession, idempotency_key: str, 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), ) @classmethod async def get_usage_by_idempotency_key( cls, db: AsyncSession, idempotency_key: str, ) -> BizUsageRecord | None: return await cls._get_by_key( db, BizUsageRecord, BizUsageRecord.idempotency_key, idempotency_key, ) @classmethod async def get_return_by_request_idempotency_key( cls, db: AsyncSession, idempotency_key: str, ) -> BizReturnOrder | None: return await cls._get_by_key( db, BizReturnOrder, BizReturnOrder.request_idempotency_key, idempotency_key, ) @classmethod async def get_return_by_confirm_idempotency_key( cls, db: AsyncSession, idempotency_key: str, ) -> BizReturnOrder | None: return await cls._get_by_key( db, BizReturnOrder, BizReturnOrder.confirm_idempotency_key, idempotency_key, ) @classmethod async def get_inbound_list( cls, db: AsyncSession, query_object: InboundPageQueryModel, is_page: bool = True, warehouse_ids: list[int] | None = None, ) -> PageModel | list[dict[str, Any]]: conditions = cls._inbound_scope_conditions(warehouse_ids) 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, 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, *cls._inbound_scope_conditions(warehouse_ids), ) @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, is_page: bool = True, warehouse_ids: list[int] | None = None, ) -> PageModel | list[dict[str, Any]]: conditions = cls._filling_scope_conditions(warehouse_ids) 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}%')) conditions.extend( cls._time_conditions( BizFillingRecord.operated_at, query_object.begin_time, query_object.end_time, ) ) statement = select(BizFillingRecord) if query_object.pallet_code: 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, 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), ) @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, is_page: bool = True, ) -> PageModel | list[dict[str, Any]]: conditions = [] 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) conditions.extend( cls._time_conditions( BizShipmentOrder.create_time, query_object.begin_time, query_object.end_time, ) ) statement = select(BizShipmentOrder).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, ) -> BizShipmentOrder | None: return await cls._get_order_detail( db, BizShipmentOrder, BizShipmentOrder.shipment_id, shipment_id, BizShipmentItem, BizShipmentItem.shipment_id, BizShipmentItem.shipment_item_id, ) @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, is_page: bool = True, ) -> PageModel | list[dict[str, Any]]: conditions = [] 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.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}%')) conditions.extend( cls._time_conditions( BizUsageRecord.occurred_at, query_object.begin_time, query_object.end_time, ) ) statement = select(BizUsageRecord) if query_object.pallet_code: 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, ) -> BizUsageRecord | None: return await cls._get_by_id( db, BizUsageRecord, BizUsageRecord.usage_id, usage_id, ) @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, is_page: bool = True, ) -> PageModel | list[dict[str, Any]]: conditions = [] 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).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, ) -> BizReturnOrder | None: return await cls._get_order_detail( db, BizReturnOrder, BizReturnOrder.return_id, return_id, BizReturnItem, BizReturnItem.return_id, BizReturnItem.return_item_id, ) @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)