from datetime import datetime from typing import Any from sqlalchemy import and_, false, func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import aliased from module_admin.entity.do.user_do import SysUser 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 BizPallet, BizWarehouse from module_business.entity.do.operation_do import BizInboundItem, BizInboundOrder, BizPalletInventory class InboundSummaryDao: """首次入库的只读汇总:列表名称与托盘数、详情托盘编号与当前状态、页面概况。 列表与详情只汇总调用方已按数据范围取得的单据,所有关联表均受同一测试批次范围约束。 """ @staticmethod async def get_list_summaries( db: AsyncSession, inbound_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = sorted(set(inbound_ids)) if not ids: return {} creator = aliased(SysUser, name='creator') confirmer = aliased(SysUser, name='confirmer') name_statement = ( select( BizInboundOrder.inbound_id, BizWarehouse.warehouse_name, creator.nick_name, confirmer.nick_name, ) .select_from(BizInboundOrder) .outerjoin( BizWarehouse, and_( BizWarehouse.warehouse_id == BizInboundOrder.warehouse_id, batch_scope_condition(BizWarehouse.test_batch_id, scope), ), ) .outerjoin(creator, and_(creator.user_name == BizInboundOrder.create_by, creator.del_flag == '0')) .outerjoin(confirmer, and_(confirmer.user_id == BizInboundOrder.confirmed_by, confirmer.del_flag == '0')) .where( BizInboundOrder.inbound_id.in_(ids), batch_scope_condition(BizInboundOrder.test_batch_id, scope), ) ) count_statement = ( select(BizInboundItem.inbound_id, func.count(BizInboundItem.inbound_item_id)) .where( BizInboundItem.inbound_id.in_(ids), batch_scope_condition(BizInboundItem.test_batch_id, scope), ) .group_by(BizInboundItem.inbound_id) ) summaries: dict[int, dict[str, Any]] = { inbound_id: { 'warehouse_name': None, 'creator_name': None, 'confirmer_name': None, 'pallet_count': 0, } for inbound_id in ids } for inbound_id, warehouse_name, creator_name, confirmer_name in (await db.execute(name_statement)).all(): summary = summaries.get(inbound_id) if summary is None: continue summary.update( warehouse_name=warehouse_name, creator_name=creator_name, confirmer_name=confirmer_name, ) for inbound_id, pallet_count in (await db.execute(count_statement)).all(): summary = summaries.get(inbound_id) if summary is not None: summary['pallet_count'] = int(pallet_count or 0) return summaries @staticmethod async def get_item_identities( db: AsyncSession, inbound_id: int, scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: """为详情明细补充托盘编号与当前流转状态,只读展示,不参与任何写入。""" statement = ( select(BizInboundItem.inbound_item_id, BizPallet.pallet_code, BizPalletInventory.flow_state) .select_from(BizInboundItem) .outerjoin( BizPallet, and_( BizPallet.pallet_id == BizInboundItem.pallet_id, batch_scope_condition(BizPallet.test_batch_id, scope), ), ) .outerjoin( BizPalletInventory, and_( BizPalletInventory.pallet_id == BizInboundItem.pallet_id, batch_scope_condition(BizPalletInventory.test_batch_id, scope), ), ) .where( BizInboundItem.inbound_id == inbound_id, batch_scope_condition(BizInboundItem.test_batch_id, scope), ) ) return { inbound_item_id: {'pallet_code': pallet_code, 'flow_state': flow_state} for inbound_item_id, pallet_code, flow_state in (await db.execute(statement)).all() } @staticmethod async def get_overview( db: AsyncSession, warehouse_ids: list[int] | None, scope: TestBatchScope, month_start: datetime, ) -> dict[str, Any]: """入库页概况。单据与空托盘按仓库数据范围统计;未入库托盘尚无仓库归属,按测试批次范围统计。""" order_conditions = [batch_scope_condition(BizInboundOrder.test_batch_id, scope)] inventory_conditions = [batch_scope_condition(BizPalletInventory.test_batch_id, scope)] if warehouse_ids is not None: scoped_ids = sorted(set(warehouse_ids)) order_conditions.append(BizInboundOrder.warehouse_id.in_(scoped_ids) if scoped_ids else false()) inventory_conditions.append(BizPalletInventory.warehouse_id.in_(scoped_ids) if scoped_ids else false()) item_join = and_( BizInboundItem.inbound_id == BizInboundOrder.inbound_id, batch_scope_condition(BizInboundItem.test_batch_id, scope), ) draft_statement = ( select( func.count(func.distinct(BizInboundOrder.inbound_id)), func.count(BizInboundItem.inbound_item_id), func.min(BizInboundOrder.create_time), ) .select_from(BizInboundOrder) .outerjoin(BizInboundItem, item_join) .where(*order_conditions, BizInboundOrder.status == DocumentStatus.DRAFT.value) ) month_statement = ( select( func.count(func.distinct(BizInboundOrder.inbound_id)), func.count(BizInboundItem.inbound_item_id), ) .select_from(BizInboundOrder) .outerjoin(BizInboundItem, item_join) .where( *order_conditions, BizInboundOrder.status == DocumentStatus.CONFIRMED.value, BizInboundOrder.confirmed_at >= month_start, ) ) unstocked_statement = ( select(func.count(BizPalletInventory.pallet_id)) .select_from(BizPalletInventory) .join( BizPallet, and_( BizPallet.pallet_id == BizPalletInventory.pallet_id, batch_scope_condition(BizPallet.test_batch_id, scope), ), ) .where( batch_scope_condition(BizPalletInventory.test_batch_id, scope), BizPalletInventory.flow_state == PalletFlowState.UNSTOCKED.value, BizPallet.asset_status == '0', ) ) empty_statement = ( select( BizPalletInventory.warehouse_id, BizWarehouse.warehouse_name, func.count(BizPalletInventory.pallet_id), ) .select_from(BizPalletInventory) .outerjoin( BizWarehouse, and_( BizWarehouse.warehouse_id == BizPalletInventory.warehouse_id, batch_scope_condition(BizWarehouse.test_batch_id, scope), ), ) .where( *inventory_conditions, BizPalletInventory.flow_state == PalletFlowState.EMPTY_IN_WAREHOUSE.value, BizPalletInventory.warehouse_id.is_not(None), ) .group_by(BizPalletInventory.warehouse_id, BizWarehouse.warehouse_name) ) draft_count, draft_pallets, oldest_draft_at = (await db.execute(draft_statement)).one() month_count, month_pallets = (await db.execute(month_statement)).one() unstocked_count = (await db.execute(unstocked_statement)).scalar_one() empty_rows = [ {'warehouse_id': warehouse_id, 'warehouse_name': warehouse_name, 'pallet_count': int(count or 0)} for warehouse_id, warehouse_name, count in (await db.execute(empty_statement)).all() ] empty_rows.sort(key=lambda row: (-row['pallet_count'], row['warehouse_id'])) return { 'draft_count': int(draft_count or 0), 'draft_pallet_count': int(draft_pallets or 0), 'oldest_draft_at': oldest_draft_at, 'month_start': month_start.date(), 'month_confirmed_count': int(month_count or 0), 'month_confirmed_pallet_count': int(month_pallets or 0), 'unstocked_pallet_count': int(unstocked_count or 0), 'empty_pallet_count': sum(row['pallet_count'] for row in empty_rows), 'empty_by_warehouse': empty_rows, }