from typing import Any from sqlalchemy import and_, 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 BizCustomer, BizCustomerSite, BizWarehouse from module_business.entity.do.operation_do import ( BizFillingRecord, BizPalletInventory, BizShipmentItem, BizShipmentOrder, ) class ShipmentSummaryDao: """发货单列表的只读汇总:名称、托盘数、包数与菌种构成。 只汇总调用方已按数据范围分页得到的单据 ID,所有关联表均受同一测试批次范围约束。 """ @staticmethod async def get_list_summaries( db: AsyncSession, shipment_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = sorted(set(shipment_ids)) if not ids: return {} source_warehouse = aliased(BizWarehouse, name='source_warehouse') target_site = aliased(BizCustomerSite, name='target_site') target_customer = aliased(BizCustomer, name='target_customer') creator = aliased(SysUser, name='creator') name_statement = ( select( BizShipmentOrder.shipment_id, source_warehouse.warehouse_name, target_site.site_name, target_customer.customer_name, creator.nick_name, ) .select_from(BizShipmentOrder) .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), ), ) .outerjoin(creator, and_(creator.user_name == BizShipmentOrder.create_by, creator.del_flag == '0')) .where( BizShipmentOrder.shipment_id.in_(ids), batch_scope_condition(BizShipmentOrder.test_batch_id, scope), ) ) # 已确认单使用确认快照;草稿与详情一致,只取来源仓库内仍可发货的当前余量 draft_inventory = and_( BizShipmentOrder.status == DocumentStatus.DRAFT.value, BizPalletInventory.pallet_id == BizShipmentItem.pallet_id, BizPalletInventory.warehouse_id == BizShipmentOrder.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), ) item_statement = ( select( BizShipmentItem.shipment_id, BizShipmentItem.package_qty, BizPalletInventory.remaining_packages, BizFillingRecord.strain_code_snapshot, BizFillingRecord.strain_name_snapshot, ) .select_from(BizShipmentItem) .join( BizShipmentOrder, and_( BizShipmentOrder.shipment_id == BizShipmentItem.shipment_id, batch_scope_condition(BizShipmentOrder.test_batch_id, scope), ), ) .outerjoin(BizPalletInventory, draft_inventory) .outerjoin( BizFillingRecord, and_( BizFillingRecord.filling_id == func.coalesce(BizShipmentItem.active_filling_id, BizPalletInventory.active_filling_id), BizFillingRecord.pallet_id == BizShipmentItem.pallet_id, batch_scope_condition(BizFillingRecord.test_batch_id, scope), ), ) .where( BizShipmentItem.shipment_id.in_(ids), batch_scope_condition(BizShipmentItem.test_batch_id, scope), ) .order_by(BizShipmentItem.shipment_id, BizShipmentItem.shipment_item_id) ) summaries: dict[int, dict[str, Any]] = { shipment_id: { 'source_warehouse_name': None, 'target_site_name': None, 'customer_name': None, 'creator_name': None, 'pallet_count': 0, 'package_qty': None, 'strains': [], } for shipment_id in ids } for shipment_id, warehouse_name, site_name, customer_name, creator_name in ( await db.execute(name_statement) ).all(): summary = summaries.get(shipment_id) if summary is None: continue summary.update( source_warehouse_name=warehouse_name, target_site_name=site_name, customer_name=customer_name, creator_name=creator_name, ) strain_counts: dict[int, dict[str, int]] = {} for shipment_id, package_qty, remaining_packages, strain_code, strain_name in ( await db.execute(item_statement) ).all(): summary = summaries.get(shipment_id) if summary is None: continue summary['pallet_count'] += 1 quantity = package_qty if package_qty is not None else remaining_packages if quantity is not None: summary['package_qty'] = (summary['package_qty'] or 0) + quantity label = strain_name or strain_code if label: counts = strain_counts.setdefault(shipment_id, {}) counts[label] = counts.get(label, 0) + 1 for shipment_id, counts in strain_counts.items(): summaries[shipment_id]['strains'] = [ {'strain_name': name, 'pallet_count': count} for name, count in sorted(counts.items(), key=lambda entry: (-entry[1], entry[0])) ] return summaries