from datetime import datetime from typing import Any from sqlalchemy import and_, case, false, func, select from sqlalchemy.ext.asyncio import AsyncSession from module_admin.entity.do.user_do import SysUser from module_business.domain.enums import PalletFlowState from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition from module_business.entity.do.master_do import BizCustomer, BizCustomerSite, BizPallet, BizProductionLine, BizTunnel from module_business.entity.do.operation_do import BizPalletInventory, BizUsageRecord _IN_USE_STATES = (PalletFlowState.AT_SITE_UNUSED.value, PalletFlowState.AT_SITE_PARTIAL.value) class UsageSummaryDao: """客户现场使用的只读汇总:列表名称、现场选项与现场看板。 不返回隧道内部溯源号;所有关联表均受同一测试批次范围约束。 """ @staticmethod def _skipped_pallet_code(snapshot: Any) -> str | None: # 跳过先到托盘时,写入路径保存了当时的先到托盘快照 if isinstance(snapshot, dict): code = snapshot.get('palletCode') return code if isinstance(code, str) and code.strip() else None return None @classmethod async def get_list_summaries( cls, db: AsyncSession, usage_ids: list[int], scope: TestBatchScope, ) -> dict[int, dict[str, Any]]: ids = sorted(set(usage_ids)) if not ids: return {} statement = ( select( BizUsageRecord.usage_id, BizPallet.pallet_code, BizCustomerSite.site_name, BizTunnel.tunnel_code, BizTunnel.tunnel_name, BizProductionLine.line_name, SysUser.nick_name, BizUsageRecord.fifo_recommendation, ) .select_from(BizUsageRecord) .outerjoin( BizPallet, and_( BizPallet.pallet_id == BizUsageRecord.pallet_id, batch_scope_condition(BizPallet.test_batch_id, scope), ), ) .outerjoin( BizCustomerSite, and_( BizCustomerSite.site_id == BizUsageRecord.site_id, batch_scope_condition(BizCustomerSite.test_batch_id, scope), ), ) .outerjoin( BizTunnel, and_( BizTunnel.tunnel_id == BizUsageRecord.tunnel_id, batch_scope_condition(BizTunnel.test_batch_id, scope), ), ) .outerjoin( BizProductionLine, and_( BizProductionLine.line_id == BizUsageRecord.line_id, batch_scope_condition(BizProductionLine.test_batch_id, scope), ), ) .outerjoin(SysUser, and_(SysUser.user_id == BizUsageRecord.operator_id, SysUser.del_flag == '0')) .where( BizUsageRecord.usage_id.in_(ids), batch_scope_condition(BizUsageRecord.test_batch_id, scope), ) ) summaries: dict[int, dict[str, Any]] = { usage_id: { 'pallet_code': None, 'site_name': None, 'tunnel_code': None, 'tunnel_name': None, 'line_name': None, 'operator_name': None, 'skipped_pallet_code': None, } for usage_id in ids } for usage_id, pallet_code, site_name, tunnel_code, tunnel_name, line_name, operator_name, snapshot in ( await db.execute(statement) ).all(): summary = summaries.get(usage_id) if summary is None: continue summary.update( pallet_code=pallet_code, site_name=site_name, tunnel_code=tunnel_code, tunnel_name=tunnel_name, line_name=line_name, operator_name=operator_name, skipped_pallet_code=cls._skipped_pallet_code(snapshot), ) return summaries @staticmethod async def get_site_options( db: AsyncSession, site_ids: list[int] | None, scope: TestBatchScope, ) -> list[dict[str, Any]]: """当前用户有权访问且已启用的现场,附带启用隧道与产线数。""" conditions = [batch_scope_condition(BizCustomerSite.test_batch_id, scope), BizCustomerSite.status == '0'] if site_ids is not None: scoped_ids = sorted(set(site_ids)) conditions.append(BizCustomerSite.site_id.in_(scoped_ids) if scoped_ids else false()) tunnel_counts = ( select(BizTunnel.site_id, func.count(BizTunnel.tunnel_id).label('tunnel_count')) .where(BizTunnel.status == '0', batch_scope_condition(BizTunnel.test_batch_id, scope)) .group_by(BizTunnel.site_id) .subquery() ) line_counts = ( select(BizProductionLine.site_id, func.count(BizProductionLine.line_id).label('line_count')) .where(BizProductionLine.status == '0', batch_scope_condition(BizProductionLine.test_batch_id, scope)) .group_by(BizProductionLine.site_id) .subquery() ) statement = ( select( BizCustomerSite.site_id, BizCustomerSite.site_code, BizCustomerSite.site_name, BizCustomer.customer_name, tunnel_counts.c.tunnel_count, line_counts.c.line_count, ) .select_from(BizCustomerSite) .outerjoin( BizCustomer, and_( BizCustomer.customer_id == BizCustomerSite.customer_id, batch_scope_condition(BizCustomer.test_batch_id, scope), ), ) .outerjoin(tunnel_counts, tunnel_counts.c.site_id == BizCustomerSite.site_id) .outerjoin(line_counts, line_counts.c.site_id == BizCustomerSite.site_id) .where(*conditions) .order_by(BizCustomerSite.site_id) ) return [ { 'id': site_id, 'code': site_code, 'name': site_name, 'customer_name': customer_name, 'tunnel_count': int(tunnel_count or 0), 'line_count': int(line_count or 0), } for site_id, site_code, site_name, customer_name, tunnel_count, line_count in ( await db.execute(statement) ).all() ] @staticmethod async def get_site_board( db: AsyncSession, site: Any, scope: TestBatchScope, today_start: datetime, week_start: datetime, ) -> dict[str, Any]: """调用方已校验现场权限与批次范围;这里只做只读统计。""" site_id = site.site_id customer_statement = select(BizCustomer.customer_name).where( BizCustomer.customer_id == site.customer_id, batch_scope_condition(BizCustomer.test_batch_id, scope), ) inventory_statement = ( select( BizPalletInventory.flow_state, func.count(BizPalletInventory.pallet_id), func.sum(BizPalletInventory.remaining_packages), ) .where( BizPalletInventory.site_id == site_id, BizPalletInventory.warehouse_id.is_(None), BizPalletInventory.flow_state.in_( [*_IN_USE_STATES, PalletFlowState.AT_SITE_EMPTY.value], ), batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(BizPalletInventory.flow_state) ) usage_conditions = [ BizUsageRecord.site_id == site_id, batch_scope_condition(BizUsageRecord.test_batch_id, scope), ] today_statement = ( select( BizUsageRecord.line_id, BizProductionLine.line_name, func.count(BizUsageRecord.usage_id), func.sum(BizUsageRecord.used_packages), ) .select_from(BizUsageRecord) .outerjoin( BizProductionLine, and_( BizProductionLine.line_id == BizUsageRecord.line_id, batch_scope_condition(BizProductionLine.test_batch_id, scope), ), ) .where(*usage_conditions, BizUsageRecord.occurred_at >= today_start) .group_by(BizUsageRecord.line_id, BizProductionLine.line_name) ) skip_statement = select( func.sum(case((BizUsageRecord.occurred_at >= today_start, 1), else_=0)), func.count(BizUsageRecord.usage_id), ).where( *usage_conditions, BizUsageRecord.fifo_skip_reason.is_not(None), BizUsageRecord.occurred_at >= week_start, ) tunnel_statement = ( select( BizTunnel.tunnel_id, BizTunnel.tunnel_code, BizTunnel.tunnel_name, BizTunnel.current_trace_no.is_not(None).label('in_use'), BizTunnel.current_trace_started_at, ) .where( BizTunnel.site_id == site_id, BizTunnel.status == '0', batch_scope_condition(BizTunnel.test_batch_id, scope), ) .order_by(BizTunnel.tunnel_code, BizTunnel.tunnel_id) ) tunnel_load_statement = ( select( BizPalletInventory.tunnel_id, func.count(BizPalletInventory.pallet_id), func.sum(BizPalletInventory.remaining_packages), ) .where( BizPalletInventory.site_id == site_id, BizPalletInventory.tunnel_id.is_not(None), BizPalletInventory.flow_state.in_(_IN_USE_STATES), BizPalletInventory.remaining_packages > 0, batch_scope_condition(BizPalletInventory.test_batch_id, scope), ) .group_by(BizPalletInventory.tunnel_id) ) latest_usage = ( select(BizUsageRecord.tunnel_id, func.max(BizUsageRecord.usage_id).label('usage_id')) .where(*usage_conditions, BizUsageRecord.tunnel_id.is_not(None)) .group_by(BizUsageRecord.tunnel_id) .subquery() ) last_use_statement = ( select(BizUsageRecord.tunnel_id, BizUsageRecord.occurred_at, BizProductionLine.line_name) .select_from(latest_usage) .join(BizUsageRecord, BizUsageRecord.usage_id == latest_usage.c.usage_id) .outerjoin( BizProductionLine, and_( BizProductionLine.line_id == BizUsageRecord.line_id, batch_scope_condition(BizProductionLine.test_batch_id, scope), ), ) ) line_statement = ( select(BizProductionLine.line_id, BizProductionLine.line_code, BizProductionLine.line_name) .where( BizProductionLine.site_id == site_id, BizProductionLine.status == '0', batch_scope_condition(BizProductionLine.test_batch_id, scope), ) .order_by(BizProductionLine.line_id) ) customer_name = (await db.execute(customer_statement)).scalar_one_or_none() stock = { state: (int(count or 0), int(packages or 0)) for state, count, packages in (await db.execute(inventory_statement)).all() } today_rows = (await db.execute(today_statement)).all() today_skips, week_skips = (await db.execute(skip_statement)).one() tunnel_rows = (await db.execute(tunnel_statement)).all() loads = { tunnel_id: (int(count or 0), int(packages or 0)) for tunnel_id, count, packages in (await db.execute(tunnel_load_statement)).all() } last_uses = { tunnel_id: (occurred_at, line_name) for tunnel_id, occurred_at, line_name in (await db.execute(last_use_statement)).all() } lines = [ {'id': line_id, 'code': line_code, 'name': line_name} for line_id, line_code, line_name in (await db.execute(line_statement)).all() ] unused = stock.get(PalletFlowState.AT_SITE_UNUSED.value, (0, 0)) partial = stock.get(PalletFlowState.AT_SITE_PARTIAL.value, (0, 0)) by_line = sorted( ( {'line_id': line_id, 'line_name': line_name, 'used_packages': int(packages or 0)} for line_id, line_name, _, packages in today_rows ), key=lambda row: (-row['used_packages'], row['line_id'] or 0), ) tunnels = [] for tunnel_id, tunnel_code, tunnel_name, in_use, started_at in tunnel_rows: pallet_count, remaining = loads.get(tunnel_id, (0, 0)) last_used_at, last_line_name = last_uses.get(tunnel_id, (None, None)) tunnels.append( { 'tunnel_id': tunnel_id, 'tunnel_code': tunnel_code, 'tunnel_name': tunnel_name, 'in_use': bool(in_use), 'pallet_count': pallet_count, 'remaining_packages': remaining, 'started_at': started_at if in_use else None, 'last_used_at': last_used_at, 'last_line_name': last_line_name, } ) return { 'site_id': site_id, 'site_code': site.site_code, 'site_name': site.site_name, 'customer_name': customer_name, 'pallet_count': unused[0] + partial[0], 'package_sum': unused[1] + partial[1], 'unused_pallet_count': unused[0], 'partial_pallet_count': partial[0], 'empty_pallet_count': stock.get(PalletFlowState.AT_SITE_EMPTY.value, (0, 0))[0], 'today_usage_count': sum(int(row[2] or 0) for row in today_rows), 'today_used_packages': sum(row['used_packages'] for row in by_line), 'today_by_line': by_line, 'tunnel_count': len(tunnels), 'active_tunnel_count': sum(1 for tunnel in tunnels if tunnel['in_use']), 'today_fifo_skip_count': int(today_skips or 0), 'week_fifo_skip_count': int(week_skips or 0), 'tunnels': tunnels, 'lines': lines, }