from __future__ import annotations from datetime import date from typing import Dict from fastapi import APIRouter, Depends from sqlalchemy import func from sqlalchemy.orm import Session from app.core.database import get_db from app.core.db_auth import DbUserContext, get_db_current_user from app.core.tenant_context import get_tenant_no from app.models.db_models import DbInboundOutbound, DbWeighingSlip from app.schemas.db.dashboard import ( DashboardCargoStat, DashboardPartnerCargoStat, DashboardStats, ) from app.services.db_data_scope import apply_db_scope router = APIRouter(prefix="/dashboard", tags=["dashboard"]) def _scoped_inventory_query(db: Session, tenant_no: str, user: DbUserContext): query = db.query(DbInboundOutbound).filter( DbInboundOutbound.del_flag == 0, DbInboundOutbound.tenant_no == tenant_no, ) return apply_db_scope(query, DbInboundOutbound, user, db, tenant_no) def _scoped_weighing_query(db: Session, tenant_no: str, user: DbUserContext): query = db.query(DbWeighingSlip).filter( DbWeighingSlip.is_voided == 0, DbWeighingSlip.del_flag == 0, DbWeighingSlip.tenant_no == tenant_no, ) return apply_db_scope(query, DbWeighingSlip, user, db, tenant_no) @router.get("/stats", response_model=DashboardStats) def get_dashboard_stats( db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> DashboardStats: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) today = date.today() today_inbound = float( _scoped_inventory_query(db, tenant_no, user) .filter( DbInboundOutbound.date == today, DbInboundOutbound.business_type == "入库", ) .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0)) .scalar() or 0 ) today_outbound = float( _scoped_inventory_query(db, tenant_no, user) .filter( DbInboundOutbound.date == today, DbInboundOutbound.business_type == "出库", ) .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0)) .scalar() or 0 ) abnormal_orders = int( _scoped_weighing_query(db, tenant_no, user) .with_entities(func.count(DbWeighingSlip.id)) .scalar() or 0 ) return DashboardStats( today_inbound=today_inbound, today_outbound=today_outbound, abnormal_orders=abnormal_orders, ) @router.get("/inbound/by-cargo", response_model=list[DashboardCargoStat]) def get_inbound_by_cargo( db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> list[DashboardCargoStat]: return _cargo_stats(db, user, business_type="入库") @router.get("/inbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat]) def get_inbound_by_partner_cargo( db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> list[DashboardPartnerCargoStat]: return _partner_cargo_stats(db, user, business_type="入库") @router.get("/outbound/by-cargo", response_model=list[DashboardCargoStat]) def get_outbound_by_cargo( db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> list[DashboardCargoStat]: return _cargo_stats(db, user, business_type="出库") @router.get("/outbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat]) def get_outbound_by_partner_cargo( db: Session = Depends(get_db), user: DbUserContext = Depends(get_db_current_user), ) -> list[DashboardPartnerCargoStat]: return _partner_cargo_stats(db, user, business_type="出库") def _weighing_actual_map( db: Session, tenant_no: str, user: DbUserContext, inventory_ids: list[str] ) -> Dict[str, float]: if not inventory_ids: return {} rows = ( _scoped_weighing_query(db, tenant_no, user) .filter(DbWeighingSlip.inbound_outbound_id.in_(inventory_ids)) .all() ) actual_map: Dict[str, float] = {} for row in rows: if not row.inbound_outbound_id: continue actual_map[row.inbound_outbound_id] = actual_map.get(row.inbound_outbound_id, 0.0) + ( float(row.net_weight or 0) / 1000.0 ) return actual_map def _cargo_stats(db: Session, user: DbUserContext, *, business_type: str) -> list[DashboardCargoStat]: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) today = date.today() inventories = ( _scoped_inventory_query(db, tenant_no, user) .filter( DbInboundOutbound.date == today, DbInboundOutbound.business_type == business_type, ) .all() ) actual_map = _weighing_actual_map(db, tenant_no, user, [inv.id for inv in inventories]) agg: Dict[str, Dict[str, float]] = {} for inv in inventories: label = (inv.product or "").strip() or "未填写货物" bucket = agg.setdefault(label, {"planned": 0.0, "actual": 0.0}) bucket["planned"] += float(inv.quantity or 0) bucket["actual"] += actual_map.get(inv.id, 0.0) return [ DashboardCargoStat(label=label, planned=values["planned"], actual=values["actual"]) for label, values in sorted(agg.items(), key=lambda item: item[1]["planned"], reverse=True) ] def _partner_cargo_stats( db: Session, user: DbUserContext, *, business_type: str, ) -> list[DashboardPartnerCargoStat]: tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) today = date.today() inventories = ( _scoped_inventory_query(db, tenant_no, user) .filter( DbInboundOutbound.date == today, DbInboundOutbound.business_type == business_type, ) .all() ) actual_map = _weighing_actual_map(db, tenant_no, user, [inv.id for inv in inventories]) agg: Dict[str, Dict[str, float]] = {} for inv in inventories: product = (inv.product or "").strip() or "未填写货物" label = f"{inv.partner_company or ''} - {product}" bucket = agg.setdefault(label, {"planned": 0.0, "actual": 0.0}) bucket["planned"] += float(inv.quantity or 0) bucket["actual"] += actual_map.get(inv.id, 0.0) return [ DashboardPartnerCargoStat(label=label, planned=values["planned"], actual=values["actual"]) for label, values in sorted( agg.items(), key=lambda item: (item[0].split(" - ", 1)[0], -item[1]["planned"]) ) ]