"""运输管理 - 侧栏菜单角标统计""" from __future__ import annotations from typing import Dict, List, Optional from sqlalchemy.orm import Session from app.models.bx_models import BxWorkflowNode from app.models.pl_models import ( PlDocumentReminder, PlFreightSettlement, PlPlan, PlVehicleMaintenanceApply, PlVehicleRechargeApply, PlVehicleUreaApply, ) from app.services.pl_cert_service import scan_cert_expiry from app.services.pl_data_scope import apply_pl_scope from app.services.pl_freight_settlement_service import can_user_approve_settlement MENU_CODE_DOCUMENT = "pl:document:list" MENU_CODE_FREIGHT_APPROVAL = "pl:freight-approval:list" MENU_CODE_FREIGHT_DISBURSE = "pl:freight-disburse:list" MENU_CODE_RECHARGE_APPROVAL = "pl:vehicle-recharge-approval:list" MENU_CODE_RECHARGE_RECORD = "pl:vehicle-recharge-record:list" MENU_CODE_MAINT_PROCESS = "pl:vehicle-maintenance-process:list" MENU_CODE_UREA_DISBURSE = "pl:vehicle-urea-disburse:list" MENU_CODE_PLAN_CERT = "pl:plan-cert:list" MENU_CODE_LEGAL_USE_DOCUMENT = "pl:legal-use-document:list" MENU_CODE_PLAN_ARCHIVE = "pl:plan-archive:list" MENU_CODE_PLAN_CONTRACT = "pl:plan-contract:list" SUPPORTED_MENU_CODES = { MENU_CODE_DOCUMENT, MENU_CODE_FREIGHT_APPROVAL, MENU_CODE_FREIGHT_DISBURSE, MENU_CODE_RECHARGE_APPROVAL, MENU_CODE_RECHARGE_RECORD, MENU_CODE_MAINT_PROCESS, MENU_CODE_UREA_DISBURSE, MENU_CODE_PLAN_CERT, MENU_CODE_LEGAL_USE_DOCUMENT, MENU_CODE_PLAN_ARCHIVE, MENU_CODE_PLAN_CONTRACT, } def _count_pending_applies(db: Session, tenant_no: str, user, model, *extra_filters) -> int: query = db.query(model).filter( model.tenant_no == tenant_no, model.del_flag == 0, model.status == "pending", *extra_filters, ) query = apply_pl_scope(query, model, user, db, tenant_no) return query.count() def count_document_reminder_expired_pending(db: Session, tenant_no: str, user) -> int: scan_cert_expiry(db, tenant_no) db.flush() query = db.query(PlDocumentReminder).filter( PlDocumentReminder.tenant_no == tenant_no, PlDocumentReminder.del_flag == 0, PlDocumentReminder.status == "pending", PlDocumentReminder.days_remaining <= 0, ) query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no) return query.count() def count_freight_approval_pending(db: Session, tenant_no: str, user_id: str) -> int: if not user_id: return 0 rows = ( db.query(PlFreightSettlement.id) .filter( PlFreightSettlement.tenant_no == tenant_no, PlFreightSettlement.del_flag == 0, PlFreightSettlement.settlement_status == "approving", ) .all() ) total = 0 for (settlement_id,) in rows: if can_user_approve_settlement(db, settlement_id, user_id): total += 1 return total def count_plan_cert_pending(db: Session, tenant_no: str, user) -> int: """待处理用证配置或结转异常;不再读取人工齐备标记。""" from sqlalchemy import or_, cast, String from app.models.pl_models import PlPlanProduct from app.models.pl_quota_models import PlPlanCertUsage toxic = db.query(PlPlanProduct.id).filter( PlPlanProduct.plan_id == PlPlan.id, PlPlanProduct.tenant_no == tenant_no, PlPlanProduct.del_flag == 0, PlPlanProduct.is_toxic == 1).exists() exception = db.query(PlPlanCertUsage.id).filter( PlPlanCertUsage.plan_id == PlPlan.id, PlPlanCertUsage.tenant_no == tenant_no, PlPlanCertUsage.del_flag == 0, or_(PlPlanCertUsage.usage_status == 'exception', PlPlanCertUsage.confirmation_snapshot['settlementError'].as_string().isnot(None))).exists() query = db.query(PlPlan).filter( PlPlan.tenant_no == tenant_no, PlPlan.del_flag == 0, PlPlan.follow_status != 'cancelled', toxic, or_(PlPlan.cert_configuration.is_(None), cast(PlPlan.cert_configuration, String).in_(['null', '{}']), exception), or_(PlPlan.archive_status != 'archived', exception), ) return apply_pl_scope(query, PlPlan, user, db, tenant_no).count() def count_legal_use_document_pending(db: Session, tenant_no: str, user) -> int: """独立资料待补数;归档不免除资料后补,不参与运输额度判断。""" from sqlalchemy import or_ from app.models.pl_models import PlPlanProduct pending = db.query(PlPlanProduct.id).filter( PlPlanProduct.plan_id == PlPlan.id, PlPlanProduct.tenant_no == tenant_no, PlPlanProduct.del_flag == 0, PlPlanProduct.is_explosive == 1, or_(PlPlanProduct.legal_use_cert_url.is_(None), PlPlanProduct.legal_use_cert_url == ''), ).exists() query = db.query(PlPlan).filter(PlPlan.tenant_no == tenant_no, PlPlan.del_flag == 0, pending) return apply_pl_scope(query, PlPlan, user, db, tenant_no).count() def count_plan_archive_pending(db: Session, tenant_no: str, user) -> int: """待归档角标:与归档列表一致,仅统计已安排运输且未归档的计划。""" query = db.query(PlPlan).filter( PlPlan.tenant_no == tenant_no, PlPlan.del_flag == 0, PlPlan.archive_status == "unarchived", PlPlan.follow_status != "cancelled", PlPlan.transport_status == "arranged", ) query = apply_pl_scope(query, PlPlan, user, db, tenant_no) return query.count() def count_plan_contract_gaps(db: Session, tenant_no: str, user) -> int: from app.services.pl_plan_contract_gap import plan_has_contract_gap_exists query = db.query(PlPlan).filter( PlPlan.tenant_no == tenant_no, PlPlan.del_flag == 0, PlPlan.follow_status != "cancelled", plan_has_contract_gap_exists(tenant_no), ) query = apply_pl_scope(query, PlPlan, user, db, tenant_no) return query.count() def count_freight_disburse_pending(db: Session, tenant_no: str, user) -> int: query = db.query(PlFreightSettlement).filter( PlFreightSettlement.tenant_no == tenant_no, PlFreightSettlement.del_flag == 0, PlFreightSettlement.settlement_status == "approved", ) query = apply_pl_scope(query, PlFreightSettlement, user, db, tenant_no) return query.count() def count_recharge_approval_pending(db: Session, tenant_no: str, user_id: str) -> int: """与充值审批待办列表一致:当前用户为当前节点审批人的审批中单据。""" if not user_id: return 0 return ( db.query(PlVehicleRechargeApply) .join( BxWorkflowNode, BxWorkflowNode.id == PlVehicleRechargeApply.current_node, ) .filter( PlVehicleRechargeApply.tenant_no == tenant_no, PlVehicleRechargeApply.del_flag == 0, PlVehicleRechargeApply.status == "in_approval", BxWorkflowNode.tenant_no == tenant_no, BxWorkflowNode.del_flag == 0, BxWorkflowNode.approver_id == user_id, ) .count() ) def build_menu_badge_counts( db: Session, *, tenant_no: str, user, user_id: str, menu_codes: Optional[List[str]] = None, ) -> Dict[str, int]: requested = {c.strip() for c in (menu_codes or []) if c and c.strip()} if not requested: requested = set(SUPPORTED_MENU_CODES) requested &= SUPPORTED_MENU_CODES counts: Dict[str, int] = {} if MENU_CODE_DOCUMENT in requested: counts[MENU_CODE_DOCUMENT] = count_document_reminder_expired_pending(db, tenant_no, user) if MENU_CODE_FREIGHT_APPROVAL in requested: counts[MENU_CODE_FREIGHT_APPROVAL] = count_freight_approval_pending(db, tenant_no, user_id) if MENU_CODE_FREIGHT_DISBURSE in requested: counts[MENU_CODE_FREIGHT_DISBURSE] = count_freight_disburse_pending(db, tenant_no, user) if MENU_CODE_RECHARGE_APPROVAL in requested: counts[MENU_CODE_RECHARGE_APPROVAL] = count_recharge_approval_pending( db, tenant_no, user_id ) if MENU_CODE_RECHARGE_RECORD in requested: counts[MENU_CODE_RECHARGE_RECORD] = _count_pending_applies( db, tenant_no, user, PlVehicleRechargeApply ) if MENU_CODE_MAINT_PROCESS in requested: counts[MENU_CODE_MAINT_PROCESS] = _count_pending_applies( db, tenant_no, user, PlVehicleMaintenanceApply ) if MENU_CODE_UREA_DISBURSE in requested: counts[MENU_CODE_UREA_DISBURSE] = _count_pending_applies( db, tenant_no, user, PlVehicleUreaApply ) if MENU_CODE_PLAN_CERT in requested: counts[MENU_CODE_PLAN_CERT] = count_plan_cert_pending(db, tenant_no, user) if MENU_CODE_LEGAL_USE_DOCUMENT in requested: counts[MENU_CODE_LEGAL_USE_DOCUMENT] = count_legal_use_document_pending(db, tenant_no, user) if MENU_CODE_PLAN_ARCHIVE in requested: counts[MENU_CODE_PLAN_ARCHIVE] = count_plan_archive_pending(db, tenant_no, user) if MENU_CODE_PLAN_CONTRACT in requested: counts[MENU_CODE_PLAN_CONTRACT] = count_plan_contract_gaps(db, tenant_no, user) return counts