from datetime import date, datetime, timedelta from typing import Any from sqlalchemy import and_, case, 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, PalletEventType, PalletFlowState, ReturnStatus from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition from module_business.entity.do.master_do import ( BizCustomer, BizCustomerSite, BizProductionLine, BizTunnel, BizUserSite, BizUserWarehouse, BizWarehouse, ) from module_business.entity.do.operation_do import ( BizFillingRecord, BizInboundOrder, BizPalletEvent, BizPalletInventory, BizReturnItem, BizReturnOrder, BizShipmentItem, BizShipmentOrder, BizUsageRecord, ) SITE_STATES = ( PalletFlowState.AT_SITE_UNUSED.value, PalletFlowState.AT_SITE_PARTIAL.value, PalletFlowState.AT_SITE_EMPTY.value, ) FIELD_STOCK_STATES = ( PalletFlowState.IN_TRANSIT.value, PalletFlowState.AT_SITE_UNUSED.value, PalletFlowState.AT_SITE_PARTIAL.value, ) EXPIRY_WARNING_DAYS = 7 RECENT_DAYS = 30 USER_PREVIEW_LIMIT = 5 RECENT_EVENT_LIMIT = 6 PARENT_OPTION_LIMIT = 1000 def _count_when(condition: Any) -> Any: return func.coalesce(func.sum(case((condition, 1), else_=0)), 0) def _sum_when(condition: Any, column: Any) -> Any: return func.coalesce(func.sum(case((condition, column), else_=0)), 0) def _active_user() -> Any: return and_(SysUser.status == '0', SysUser.del_flag == '0') class MasterSummaryDao: """基础资料列表与详情的只读汇总。 只汇总调用方已分页得到的主数据 ID;每张关联表都受同一测试批次范围约束, 结果只用于展示,不参与任何写入校验。 """ @staticmethod def _ids(resource_ids: list[int]) -> list[int]: return sorted(set(resource_ids)) @classmethod async def get_strain_summaries( cls, db: AsyncSession, strain_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = cls._ids(strain_ids) if not ids: return {} in_warehouse = BizPalletInventory.flow_state == PalletFlowState.FILLED_IN_WAREHOUSE.value in_field = BizPalletInventory.flow_state.in_(FIELD_STOCK_STATES) stock_statement = ( select( BizFillingRecord.strain_id, _count_when(in_warehouse), _sum_when(in_warehouse, BizPalletInventory.remaining_packages), _count_when(in_field), _sum_when(in_field, BizPalletInventory.remaining_packages), func.min(case((in_warehouse, BizFillingRecord.expiry_date), else_=None)), ) .select_from(BizPalletInventory) .join( BizFillingRecord, and_( BizFillingRecord.filling_id == BizPalletInventory.active_filling_id, BizFillingRecord.pallet_id == BizPalletInventory.pallet_id, batch_scope_condition(BizFillingRecord.test_batch_id, scope), ), ) .where( BizFillingRecord.strain_id.in_(ids), BizPalletInventory.remaining_packages > 0, batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(BizFillingRecord.strain_id) ) recent_from = datetime.now() - timedelta(days=RECENT_DAYS) filling_statement = ( select( BizFillingRecord.strain_id, func.max(BizFillingRecord.operated_at), _count_when(BizFillingRecord.operated_at >= recent_from), ) .where( BizFillingRecord.strain_id.in_(ids), batch_scope_condition(BizFillingRecord.test_batch_id, scope), ) .group_by(BizFillingRecord.strain_id) ) summaries: dict[int, dict[str, Any]] = {strain_id: {} for strain_id in ids} for strain_id, wh_pallets, wh_packages, field_pallets, field_packages, nearest in ( await db.execute(stock_statement) ).all(): summaries[strain_id].update( warehouse_pallet_count=int(wh_pallets), warehouse_package_qty=int(wh_packages), field_pallet_count=int(field_pallets), field_package_qty=int(field_packages), nearest_expiry_date=nearest, ) for strain_id, last_filled_at, recent_count in (await db.execute(filling_statement)).all(): summaries[strain_id].update(last_filled_at=last_filled_at, filling_count_30d=int(recent_count)) return summaries @classmethod async def _site_pallet_counts( cls, db: AsyncSession, group_column: Any, condition: Any, scope: TestBatchScope, ) -> dict[int, dict[str, int]]: state = BizPalletInventory.flow_state statement = ( select( group_column, _count_when(state == PalletFlowState.AT_SITE_UNUSED.value), _count_when(state == PalletFlowState.AT_SITE_PARTIAL.value), _count_when(state == PalletFlowState.AT_SITE_EMPTY.value), _count_when(state == PalletFlowState.IN_TRANSIT.value), _sum_when(state.in_(SITE_STATES), BizPalletInventory.remaining_packages), ) .select_from(BizPalletInventory) .join( BizCustomerSite, and_( BizCustomerSite.site_id == BizPalletInventory.site_id, batch_scope_condition(BizCustomerSite.test_batch_id, scope), ), ) .where( condition, state.in_((*SITE_STATES, PalletFlowState.IN_TRANSIT.value)), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(group_column) ) return { key: { 'unused_pallet_count': int(unused), 'partial_pallet_count': int(partial), 'empty_pallet_count': int(empty), 'transit_pallet_count': int(transit), 'remaining_package_qty': int(remaining), } for key, unused, partial, empty, transit, remaining in (await db.execute(statement)).all() } @classmethod async def _site_users( cls, db: AsyncSession, site_ids: list[int], scope: TestBatchScope, ) -> dict[int, list[dict[str, Any]]]: if not site_ids: return {} statement = ( select(BizUserSite.site_id, SysUser.user_id, SysUser.user_name, SysUser.nick_name) .select_from(BizUserSite) .join( BizCustomerSite, and_( BizCustomerSite.site_id == BizUserSite.site_id, batch_scope_condition(BizCustomerSite.test_batch_id, scope), ), ) .join(SysUser, and_(SysUser.user_id == BizUserSite.user_id, _active_user())) .where(BizUserSite.site_id.in_(site_ids)) .order_by(BizUserSite.site_id, SysUser.user_id) ) users: dict[int, list[dict[str, Any]]] = {} for site_id, user_id, user_name, nick_name in (await db.execute(statement)).all(): users.setdefault(site_id, []).append({'user_id': user_id, 'user_name': user_name, 'nick_name': nick_name}) return users @classmethod async def _count_by( cls, db: AsyncSession, group_column: Any, count_column: Any, *conditions: Any, join: tuple[Any, Any] | None = None, ) -> dict[int, int]: statement = select(group_column, func.count(count_column)) if join is not None: statement = statement.select_from(count_column.class_).join(*join) statement = statement.where(*conditions).group_by(group_column) return {key: int(count) for key, count in (await db.execute(statement)).all()} @classmethod async def get_customer_summaries( cls, db: AsyncSession, customer_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = cls._ids(customer_ids) if not ids: return {} site_scope = batch_scope_condition(BizCustomerSite.test_batch_id, scope) site_join = ( BizCustomerSite, and_(BizCustomerSite.site_id == BizProductionLine.site_id, site_scope), ) site_counts = await cls._count_by( db, BizCustomerSite.customer_id, BizCustomerSite.site_id, BizCustomerSite.customer_id.in_(ids), site_scope, ) line_counts = await cls._count_by( db, BizCustomerSite.customer_id, BizProductionLine.line_id, BizCustomerSite.customer_id.in_(ids), batch_scope_condition(BizProductionLine.test_batch_id, scope), join=site_join, ) tunnel_counts = await cls._count_by( db, BizCustomerSite.customer_id, BizTunnel.tunnel_id, BizCustomerSite.customer_id.in_(ids), batch_scope_condition(BizTunnel.test_batch_id, scope), join=(BizCustomerSite, and_(BizCustomerSite.site_id == BizTunnel.site_id, site_scope)), ) pallet_counts = await cls._site_pallet_counts( db, BizCustomerSite.customer_id, BizCustomerSite.customer_id.in_(ids), scope ) user_statement = ( select(BizCustomerSite.customer_id, func.count(func.distinct(BizUserSite.user_id))) .select_from(BizUserSite) .join(BizCustomerSite, and_(BizCustomerSite.site_id == BizUserSite.site_id, site_scope)) .join(SysUser, and_(SysUser.user_id == BizUserSite.user_id, _active_user())) .where(BizCustomerSite.customer_id.in_(ids)) .group_by(BizCustomerSite.customer_id) ) user_counts = {key: int(count) for key, count in (await db.execute(user_statement)).all()} summaries: dict[int, dict[str, Any]] = {} for customer_id in ids: pallets = pallet_counts.get(customer_id, {}) summaries[customer_id] = { 'site_count': site_counts.get(customer_id, 0), 'line_count': line_counts.get(customer_id, 0), 'tunnel_count': tunnel_counts.get(customer_id, 0), 'site_pallet_count': pallets.get('unused_pallet_count', 0) + pallets.get('partial_pallet_count', 0) + pallets.get('empty_pallet_count', 0), 'transit_pallet_count': pallets.get('transit_pallet_count', 0), 'user_count': user_counts.get(customer_id, 0), } return summaries @classmethod async def get_customer_site_tree( cls, db: AsyncSession, customer_id: int, scope: TestBatchScope, ) -> list[dict[str, Any]]: site_rows = ( await db.execute( select( BizCustomerSite.site_id, BizCustomerSite.site_code, BizCustomerSite.site_name, BizCustomerSite.address, BizCustomerSite.status, ) .where( BizCustomerSite.customer_id == customer_id, batch_scope_condition(BizCustomerSite.test_batch_id, scope), ) .order_by(BizCustomerSite.site_code, BizCustomerSite.site_id) ) ).all() sites = { row.site_id: { 'site_id': row.site_id, 'site_code': row.site_code, 'site_name': row.site_name, 'address': row.address, 'status': row.status, 'lines': [], 'tunnels': [], 'users': [], } for row in site_rows } if not sites: return [] site_ids = list(sites) line_pallets = ( select( BizPalletInventory.current_line_id, func.count(BizPalletInventory.pallet_id).label('pallet_count'), ) .where( BizPalletInventory.site_id.in_(site_ids), BizPalletInventory.current_line_id.is_not(None), BizPalletInventory.flow_state.in_(SITE_STATES), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(BizPalletInventory.current_line_id) .subquery() ) line_rows = ( await db.execute( select( BizProductionLine.site_id, BizProductionLine.line_id, BizProductionLine.line_code, BizProductionLine.line_name, BizProductionLine.status, func.coalesce(line_pallets.c.pallet_count, 0), ) .outerjoin(line_pallets, line_pallets.c.current_line_id == BizProductionLine.line_id) .where( BizProductionLine.site_id.in_(site_ids), batch_scope_condition(BizProductionLine.test_batch_id, scope), ) .order_by(BizProductionLine.line_code, BizProductionLine.line_id) ) ).all() for site_id, line_id, line_code, line_name, status, pallet_count in line_rows: sites[site_id]['lines'].append( { 'line_id': line_id, 'line_code': line_code, 'line_name': line_name, 'status': status, 'active_pallet_count': int(pallet_count), } ) tunnel_pallets = ( select( BizPalletInventory.tunnel_id, func.count(BizPalletInventory.pallet_id).label('pallet_count'), ) .where( BizPalletInventory.site_id.in_(site_ids), BizPalletInventory.tunnel_id.is_not(None), BizPalletInventory.flow_state.in_(SITE_STATES), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(BizPalletInventory.tunnel_id) .subquery() ) tunnel_rows = ( await db.execute( select( BizTunnel.site_id, BizTunnel.tunnel_id, BizTunnel.tunnel_code, BizTunnel.tunnel_name, BizTunnel.status, BizTunnel.current_trace_no, BizTunnel.current_trace_started_at, func.coalesce(tunnel_pallets.c.pallet_count, 0), ) .outerjoin(tunnel_pallets, tunnel_pallets.c.tunnel_id == BizTunnel.tunnel_id) .where( BizTunnel.site_id.in_(site_ids), batch_scope_condition(BizTunnel.test_batch_id, scope), ) .order_by(BizTunnel.tunnel_code, BizTunnel.tunnel_id) ) ).all() for site_id, tunnel_id, tunnel_code, tunnel_name, status, trace_no, trace_started_at, pallet_count in ( tunnel_rows ): sites[site_id]['tunnels'].append( { 'tunnel_id': tunnel_id, 'tunnel_code': tunnel_code, 'tunnel_name': tunnel_name, 'status': status, 'current_trace_no': trace_no, 'current_trace_started_at': trace_started_at, 'active_pallet_count': int(pallet_count), } ) pallet_counts = await cls._site_pallet_counts( db, BizPalletInventory.site_id, BizPalletInventory.site_id.in_(site_ids), scope ) for site_id, counts in pallet_counts.items(): sites[site_id].update(counts) for site_id, users in (await cls._site_users(db, site_ids, scope)).items(): sites[site_id]['users'] = users return list(sites.values()) @classmethod async def get_site_summaries( cls, db: AsyncSession, site_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = cls._ids(site_ids) if not ids: return {} name_rows = ( await db.execute( select(BizCustomerSite.site_id, BizCustomer.customer_code, BizCustomer.customer_name) .select_from(BizCustomerSite) .join( BizCustomer, and_( BizCustomer.customer_id == BizCustomerSite.customer_id, batch_scope_condition(BizCustomer.test_batch_id, scope), ), ) .where(BizCustomerSite.site_id.in_(ids), batch_scope_condition(BizCustomerSite.test_batch_id, scope)) ) ).all() line_counts = await cls._count_by( db, BizProductionLine.site_id, BizProductionLine.line_id, BizProductionLine.site_id.in_(ids), batch_scope_condition(BizProductionLine.test_batch_id, scope), ) tunnel_counts = await cls._count_by( db, BizTunnel.site_id, BizTunnel.tunnel_id, BizTunnel.site_id.in_(ids), batch_scope_condition(BizTunnel.test_batch_id, scope), ) pallet_counts = await cls._site_pallet_counts( db, BizPalletInventory.site_id, BizPalletInventory.site_id.in_(ids), scope ) users = await cls._site_users(db, ids, scope) summaries: dict[int, dict[str, Any]] = { site_id: { 'line_count': line_counts.get(site_id, 0), 'tunnel_count': tunnel_counts.get(site_id, 0), **pallet_counts.get(site_id, {}), 'user_count': len(users.get(site_id, [])), 'users': users.get(site_id, [])[:USER_PREVIEW_LIMIT], } for site_id in ids } for site_id, customer_code, customer_name in name_rows: summaries[site_id].update(customer_code=customer_code, customer_name=customer_name) return summaries @classmethod async def _child_site_names( cls, db: AsyncSession, model: Any, id_column: Any, ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: statement = ( select(id_column, BizCustomerSite.site_code, BizCustomerSite.site_name, BizCustomer.customer_name) .select_from(model) .join( BizCustomerSite, and_( BizCustomerSite.site_id == model.site_id, batch_scope_condition(BizCustomerSite.test_batch_id, scope), ), ) .outerjoin( BizCustomer, and_( BizCustomer.customer_id == BizCustomerSite.customer_id, batch_scope_condition(BizCustomer.test_batch_id, scope), ), ) .where(id_column.in_(ids), batch_scope_condition(model.test_batch_id, scope)) ) return { resource_id: {'site_code': site_code, 'site_name': site_name, 'customer_name': customer_name} for resource_id, site_code, site_name, customer_name in (await db.execute(statement)).all() } @classmethod async def get_line_summaries( cls, db: AsyncSession, line_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = cls._ids(line_ids) if not ids: return {} names = await cls._child_site_names(db, BizProductionLine, BizProductionLine.line_id, ids, scope) pallet_counts = await cls._count_by( db, BizPalletInventory.current_line_id, BizPalletInventory.pallet_id, BizPalletInventory.current_line_id.in_(ids), BizPalletInventory.flow_state.in_(SITE_STATES), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) recent_from = datetime.now() - timedelta(days=RECENT_DAYS) recent = BizUsageRecord.occurred_at >= recent_from usage_rows = ( await db.execute( select( BizUsageRecord.line_id, _count_when(recent), _sum_when(recent, BizUsageRecord.used_packages), func.max(BizUsageRecord.occurred_at), ) .where(BizUsageRecord.line_id.in_(ids), batch_scope_condition(BizUsageRecord.test_batch_id, scope)) .group_by(BizUsageRecord.line_id) ) ).all() summaries = { line_id: {**names.get(line_id, {}), 'active_pallet_count': pallet_counts.get(line_id, 0)} for line_id in ids } for line_id, usage_count, used_packages, last_used_at in usage_rows: summaries[line_id].update( usage_count_30d=int(usage_count), used_package_qty_30d=int(used_packages), last_used_at=last_used_at, ) return summaries @classmethod async def get_tunnel_summaries( cls, db: AsyncSession, tunnel_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = cls._ids(tunnel_ids) if not ids: return {} names = await cls._child_site_names(db, BizTunnel, BizTunnel.tunnel_id, ids, scope) pallet_rows = ( await db.execute( select( BizPalletInventory.tunnel_id, func.count(BizPalletInventory.pallet_id), func.coalesce(func.sum(BizPalletInventory.remaining_packages), 0), ) .where( BizPalletInventory.tunnel_id.in_(ids), BizPalletInventory.flow_state.in_(SITE_STATES), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(BizPalletInventory.tunnel_id) ) ).all() recent_from = datetime.now() - timedelta(days=RECENT_DAYS) usage_rows = ( await db.execute( select( BizUsageRecord.tunnel_id, _count_when(BizUsageRecord.occurred_at >= recent_from), func.max(BizUsageRecord.occurred_at), ) .where( BizUsageRecord.tunnel_id.in_(ids), batch_scope_condition(BizUsageRecord.test_batch_id, scope), ) .group_by(BizUsageRecord.tunnel_id) ) ).all() summaries: dict[int, dict[str, Any]] = {tunnel_id: dict(names.get(tunnel_id, {})) for tunnel_id in ids} for tunnel_id, pallet_count, package_qty in pallet_rows: summaries[tunnel_id].update(active_pallet_count=int(pallet_count), active_package_qty=int(package_qty)) for tunnel_id, usage_count, last_used_at in usage_rows: summaries[tunnel_id].update(usage_count_30d=int(usage_count), last_used_at=last_used_at) return summaries @classmethod async def get_warehouse_summaries( cls, db: AsyncSession, warehouse_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = cls._ids(warehouse_ids) if not ids: return {} today_start = datetime.combine(date.today(), datetime.min.time()) expiry_limit = date.today() + timedelta(days=EXPIRY_WARNING_DAYS) filled = BizPalletInventory.flow_state == PalletFlowState.FILLED_IN_WAREHOUSE.value stock_rows = ( await db.execute( select( BizPalletInventory.warehouse_id, _count_when(filled), _sum_when(filled, BizPalletInventory.remaining_packages), _count_when(BizPalletInventory.flow_state == PalletFlowState.EMPTY_IN_WAREHOUSE.value), _count_when(and_(filled, BizFillingRecord.expiry_date <= expiry_limit)), ) .select_from(BizPalletInventory) .outerjoin( BizFillingRecord, and_( BizFillingRecord.filling_id == BizPalletInventory.active_filling_id, BizFillingRecord.pallet_id == BizPalletInventory.pallet_id, batch_scope_condition(BizFillingRecord.test_batch_id, scope), ), ) .where( BizPalletInventory.warehouse_id.in_(ids), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(BizPalletInventory.warehouse_id) ) ).all() filling_counts = await cls._count_by( db, BizFillingRecord.warehouse_id, BizFillingRecord.filling_id, BizFillingRecord.warehouse_id.in_(ids), BizFillingRecord.operated_at >= today_start, batch_scope_condition(BizFillingRecord.test_batch_id, scope), ) shipment_counts = await cls._count_by( db, BizShipmentOrder.source_warehouse_id, BizShipmentItem.shipment_item_id, BizShipmentOrder.source_warehouse_id.in_(ids), BizShipmentOrder.status == DocumentStatus.CONFIRMED.value, BizShipmentOrder.confirmed_at >= today_start, batch_scope_condition(BizShipmentItem.test_batch_id, scope), join=( BizShipmentOrder, and_( BizShipmentOrder.shipment_id == BizShipmentItem.shipment_id, batch_scope_condition(BizShipmentOrder.test_batch_id, scope), ), ), ) inbound_drafts = await cls._count_by( db, BizInboundOrder.warehouse_id, BizInboundOrder.inbound_id, BizInboundOrder.warehouse_id.in_(ids), BizInboundOrder.status == DocumentStatus.DRAFT.value, batch_scope_condition(BizInboundOrder.test_batch_id, scope), ) shipment_drafts = await cls._count_by( db, BizShipmentOrder.source_warehouse_id, BizShipmentOrder.shipment_id, BizShipmentOrder.source_warehouse_id.in_(ids), BizShipmentOrder.status == DocumentStatus.DRAFT.value, batch_scope_condition(BizShipmentOrder.test_batch_id, scope), ) incoming_returns = await cls._count_by( db, BizReturnOrder.target_warehouse_id, BizReturnOrder.return_id, BizReturnOrder.target_warehouse_id.in_(ids), BizReturnOrder.status == ReturnStatus.REQUESTED.value, batch_scope_condition(BizReturnOrder.test_batch_id, scope), ) user_rows = ( await db.execute( select(BizUserWarehouse.warehouse_id, SysUser.user_id, SysUser.user_name, SysUser.nick_name) .select_from(BizUserWarehouse) .join( BizWarehouse, and_( BizWarehouse.warehouse_id == BizUserWarehouse.warehouse_id, batch_scope_condition(BizWarehouse.test_batch_id, scope), ), ) .join(SysUser, and_(SysUser.user_id == BizUserWarehouse.user_id, _active_user())) .where(BizUserWarehouse.warehouse_id.in_(ids)) .order_by(BizUserWarehouse.warehouse_id, SysUser.user_id) ) ).all() users: dict[int, list[dict[str, Any]]] = {} for warehouse_id, user_id, user_name, nick_name in user_rows: users.setdefault(warehouse_id, []).append( {'user_id': user_id, 'user_name': user_name, 'nick_name': nick_name} ) summaries: dict[int, dict[str, Any]] = { warehouse_id: { 'today_filling_count': filling_counts.get(warehouse_id, 0), 'today_shipment_pallet_count': shipment_counts.get(warehouse_id, 0), 'draft_count': inbound_drafts.get(warehouse_id, 0) + shipment_drafts.get(warehouse_id, 0), 'incoming_return_count': incoming_returns.get(warehouse_id, 0), 'user_count': len(users.get(warehouse_id, [])), 'users': users.get(warehouse_id, [])[:USER_PREVIEW_LIMIT], } for warehouse_id in ids } for warehouse_id, filled_count, package_qty, empty_count, expiring_count in stock_rows: summaries[warehouse_id].update( filled_pallet_count=int(filled_count), filled_package_qty=int(package_qty), empty_pallet_count=int(empty_count), expiring_pallet_count=int(expiring_count), ) return summaries @classmethod async def get_pallet_summaries( cls, db: AsyncSession, pallet_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = cls._ids(pallet_ids) if not ids: return {} state_rows = ( await db.execute( select( BizPalletInventory.pallet_id, BizPalletInventory.flow_state, BizPalletInventory.remaining_packages, BizWarehouse.warehouse_name, BizCustomer.customer_name, BizCustomerSite.site_name, BizProductionLine.line_name, BizTunnel.tunnel_name, BizFillingRecord.strain_name_snapshot, BizFillingRecord.batch_no, BizFillingRecord.expiry_date, ) .select_from(BizPalletInventory) .outerjoin( BizWarehouse, and_( BizWarehouse.warehouse_id == BizPalletInventory.warehouse_id, batch_scope_condition(BizWarehouse.test_batch_id, scope), ), ) .outerjoin( BizCustomerSite, and_( BizCustomerSite.site_id == BizPalletInventory.site_id, batch_scope_condition(BizCustomerSite.test_batch_id, scope), ), ) .outerjoin( BizCustomer, and_( BizCustomer.customer_id == BizCustomerSite.customer_id, batch_scope_condition(BizCustomer.test_batch_id, scope), ), ) .outerjoin( BizProductionLine, and_( BizProductionLine.line_id == BizPalletInventory.current_line_id, batch_scope_condition(BizProductionLine.test_batch_id, scope), ), ) .outerjoin( BizTunnel, and_( BizTunnel.tunnel_id == BizPalletInventory.tunnel_id, batch_scope_condition(BizTunnel.test_batch_id, scope), ), ) .outerjoin( BizFillingRecord, and_( BizFillingRecord.filling_id == BizPalletInventory.active_filling_id, BizFillingRecord.pallet_id == BizPalletInventory.pallet_id, batch_scope_condition(BizFillingRecord.test_batch_id, scope), ), ) .where( BizPalletInventory.pallet_id.in_(ids), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) ) ).all() shipped = BizPalletEvent.event_type == PalletEventType.SHIPMENT_CONFIRMED.value event_rows = ( await db.execute( select( BizPalletEvent.pallet_id, _count_when(shipped), func.max(case((shipped, BizPalletEvent.occurred_at), else_=None)), func.max(BizPalletEvent.occurred_at), ) .where(BizPalletEvent.pallet_id.in_(ids), batch_scope_condition(BizPalletEvent.test_batch_id, scope)) .group_by(BizPalletEvent.pallet_id) ) ).all() return_rows = ( await db.execute( select(BizReturnItem.pallet_id, BizWarehouse.warehouse_name) .select_from(BizReturnItem) .join( BizReturnOrder, and_( BizReturnOrder.return_id == BizReturnItem.return_id, BizReturnOrder.status == ReturnStatus.REQUESTED.value, batch_scope_condition(BizReturnOrder.test_batch_id, scope), ), ) .join( BizWarehouse, and_( BizWarehouse.warehouse_id == BizReturnOrder.target_warehouse_id, batch_scope_condition(BizWarehouse.test_batch_id, scope), ), ) .where(BizReturnItem.pallet_id.in_(ids), batch_scope_condition(BizReturnItem.test_batch_id, scope)) ) ).all() summaries: dict[int, dict[str, Any]] = {pallet_id: {} for pallet_id in ids} for row in state_rows: summaries[row[0]].update( flow_state=row[1], remaining_packages=row[2] or 0, warehouse_name=row[3], customer_name=row[4], site_name=row[5], line_name=row[6], tunnel_name=row[7], strain_name=row[8], batch_no=row[9], expiry_date=row[10], ) for pallet_id, cycle_count, last_shipped_at, last_event_at in event_rows: summaries[pallet_id].update( cycle_count=int(cycle_count), last_shipped_at=last_shipped_at, last_event_at=last_event_at, ) for pallet_id, warehouse_name in return_rows: summaries[pallet_id]['return_warehouse_name'] = warehouse_name return summaries @classmethod async def get_pallet_recent_events( cls, db: AsyncSession, pallet_id: int, scope: TestBatchScope, ) -> list[dict[str, Any]]: warehouse = aliased(BizWarehouse, name='event_warehouse') site = aliased(BizCustomerSite, name='event_site') customer = aliased(BizCustomer, name='event_customer') line = aliased(BizProductionLine, name='event_line') tunnel = aliased(BizTunnel, name='event_tunnel') operator = aliased(SysUser, name='event_operator') statement = ( select( BizPalletEvent.event_id, BizPalletEvent.event_type, BizPalletEvent.occurred_at, BizPalletEvent.after_state, BizPalletEvent.before_remaining_packages, BizPalletEvent.after_remaining_packages, BizPalletEvent.trace_no, warehouse.warehouse_name, customer.customer_name, site.site_name, line.line_name, tunnel.tunnel_name, operator.nick_name, ) .select_from(BizPalletEvent) .outerjoin( warehouse, and_( warehouse.warehouse_id == func.coalesce(BizPalletEvent.after_warehouse_id, BizPalletEvent.before_warehouse_id), batch_scope_condition(warehouse.test_batch_id, scope), ), ) .outerjoin( site, and_( site.site_id == func.coalesce(BizPalletEvent.after_site_id, BizPalletEvent.before_site_id), batch_scope_condition(site.test_batch_id, scope), ), ) .outerjoin( customer, and_(customer.customer_id == site.customer_id, batch_scope_condition(customer.test_batch_id, scope)), ) .outerjoin( line, and_(line.line_id == BizPalletEvent.after_line_id, batch_scope_condition(line.test_batch_id, scope)), ) .outerjoin( tunnel, and_( tunnel.tunnel_id == BizPalletEvent.after_tunnel_id, batch_scope_condition(tunnel.test_batch_id, scope), ), ) .outerjoin(operator, operator.user_id == BizPalletEvent.operator_id) .where(BizPalletEvent.pallet_id == pallet_id, batch_scope_condition(BizPalletEvent.test_batch_id, scope)) .order_by(BizPalletEvent.event_seq.desc()) .limit(RECENT_EVENT_LIMIT) ) keys = ( 'event_id', 'event_type', 'occurred_at', 'after_state', 'before_remaining_packages', 'after_remaining_packages', 'trace_no', 'warehouse_name', 'customer_name', 'site_name', 'line_name', 'tunnel_name', 'operator_name', ) return [dict(zip(keys, row, strict=True)) for row in (await db.execute(statement)).all()] @classmethod async def list_customer_options(cls, db: AsyncSession, scope: TestBatchScope) -> list[dict[str, Any]]: """新增或修改现场时可选的客户:当前数据范围内状态正常的客户。""" rows = ( await db.execute( select(BizCustomer.customer_id, BizCustomer.customer_code, BizCustomer.customer_name) .where(BizCustomer.status == '0', batch_scope_condition(BizCustomer.test_batch_id, scope)) .order_by(BizCustomer.customer_code, BizCustomer.customer_id) .limit(PARENT_OPTION_LIMIT) ) ).all() return [{'id': row[0], 'code': row[1], 'name': row[2]} for row in rows] @classmethod async def list_site_options(cls, db: AsyncSession, scope: TestBatchScope) -> list[dict[str, Any]]: """新增或修改产线、隧道时可选的现场:状态正常且所属客户也正常的现场。""" rows = ( await db.execute( select( BizCustomerSite.site_id, BizCustomerSite.site_code, BizCustomerSite.site_name, BizCustomer.customer_name, ) .select_from(BizCustomerSite) .join( BizCustomer, and_( BizCustomer.customer_id == BizCustomerSite.customer_id, BizCustomer.status == '0', batch_scope_condition(BizCustomer.test_batch_id, scope), ), ) .where(BizCustomerSite.status == '0', batch_scope_condition(BizCustomerSite.test_batch_id, scope)) .order_by(BizCustomer.customer_code, BizCustomerSite.site_code, BizCustomerSite.site_id) .limit(PARENT_OPTION_LIMIT) ) ).all() return [{'id': row[0], 'code': row[1], 'name': row[2], 'parent_name': row[3]} for row in rows]