dashboard.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. from __future__ import annotations
  2. from datetime import date
  3. from typing import Dict
  4. from fastapi import APIRouter, Depends
  5. from sqlalchemy import func
  6. from sqlalchemy.orm import Session
  7. from app.core.database import get_db
  8. from app.core.db_auth import DbUserContext, get_db_current_user
  9. from app.core.tenant_context import get_tenant_no
  10. from app.models.db_models import DbInboundOutbound, DbWeighingSlip
  11. from app.schemas.db.dashboard import (
  12. DashboardCargoStat,
  13. DashboardPartnerCargoStat,
  14. DashboardStats,
  15. )
  16. from app.services.db_data_scope import apply_db_scope
  17. router = APIRouter(prefix="/dashboard", tags=["dashboard"])
  18. def _scoped_inventory_query(db: Session, tenant_no: str, user: DbUserContext):
  19. query = db.query(DbInboundOutbound).filter(
  20. DbInboundOutbound.del_flag == 0,
  21. DbInboundOutbound.tenant_no == tenant_no,
  22. )
  23. return apply_db_scope(query, DbInboundOutbound, user, db, tenant_no)
  24. def _scoped_weighing_query(db: Session, tenant_no: str, user: DbUserContext):
  25. query = db.query(DbWeighingSlip).filter(
  26. DbWeighingSlip.is_voided == 0,
  27. DbWeighingSlip.del_flag == 0,
  28. DbWeighingSlip.tenant_no == tenant_no,
  29. )
  30. return apply_db_scope(query, DbWeighingSlip, user, db, tenant_no)
  31. @router.get("/stats", response_model=DashboardStats)
  32. def get_dashboard_stats(
  33. db: Session = Depends(get_db),
  34. user: DbUserContext = Depends(get_db_current_user),
  35. ) -> DashboardStats:
  36. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  37. today = date.today()
  38. today_inbound = float(
  39. _scoped_inventory_query(db, tenant_no, user)
  40. .filter(
  41. DbInboundOutbound.date == today,
  42. DbInboundOutbound.business_type == "入库",
  43. )
  44. .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
  45. .scalar()
  46. or 0
  47. )
  48. today_outbound = float(
  49. _scoped_inventory_query(db, tenant_no, user)
  50. .filter(
  51. DbInboundOutbound.date == today,
  52. DbInboundOutbound.business_type == "出库",
  53. )
  54. .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
  55. .scalar()
  56. or 0
  57. )
  58. abnormal_orders = int(
  59. _scoped_weighing_query(db, tenant_no, user)
  60. .with_entities(func.count(DbWeighingSlip.id))
  61. .scalar()
  62. or 0
  63. )
  64. return DashboardStats(
  65. today_inbound=today_inbound,
  66. today_outbound=today_outbound,
  67. abnormal_orders=abnormal_orders,
  68. )
  69. @router.get("/inbound/by-cargo", response_model=list[DashboardCargoStat])
  70. def get_inbound_by_cargo(
  71. db: Session = Depends(get_db),
  72. user: DbUserContext = Depends(get_db_current_user),
  73. ) -> list[DashboardCargoStat]:
  74. return _cargo_stats(db, user, business_type="入库")
  75. @router.get("/inbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat])
  76. def get_inbound_by_partner_cargo(
  77. db: Session = Depends(get_db),
  78. user: DbUserContext = Depends(get_db_current_user),
  79. ) -> list[DashboardPartnerCargoStat]:
  80. return _partner_cargo_stats(db, user, business_type="入库")
  81. @router.get("/outbound/by-cargo", response_model=list[DashboardCargoStat])
  82. def get_outbound_by_cargo(
  83. db: Session = Depends(get_db),
  84. user: DbUserContext = Depends(get_db_current_user),
  85. ) -> list[DashboardCargoStat]:
  86. return _cargo_stats(db, user, business_type="出库")
  87. @router.get("/outbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat])
  88. def get_outbound_by_partner_cargo(
  89. db: Session = Depends(get_db),
  90. user: DbUserContext = Depends(get_db_current_user),
  91. ) -> list[DashboardPartnerCargoStat]:
  92. return _partner_cargo_stats(db, user, business_type="出库")
  93. def _weighing_actual_map(
  94. db: Session, tenant_no: str, user: DbUserContext, inventory_ids: list[str]
  95. ) -> Dict[str, float]:
  96. if not inventory_ids:
  97. return {}
  98. rows = (
  99. _scoped_weighing_query(db, tenant_no, user)
  100. .filter(DbWeighingSlip.inbound_outbound_id.in_(inventory_ids))
  101. .all()
  102. )
  103. actual_map: Dict[str, float] = {}
  104. for row in rows:
  105. if not row.inbound_outbound_id:
  106. continue
  107. actual_map[row.inbound_outbound_id] = actual_map.get(row.inbound_outbound_id, 0.0) + (
  108. float(row.net_weight or 0) / 1000.0
  109. )
  110. return actual_map
  111. def _cargo_stats(db: Session, user: DbUserContext, *, business_type: str) -> list[DashboardCargoStat]:
  112. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  113. today = date.today()
  114. inventories = (
  115. _scoped_inventory_query(db, tenant_no, user)
  116. .filter(
  117. DbInboundOutbound.date == today,
  118. DbInboundOutbound.business_type == business_type,
  119. )
  120. .all()
  121. )
  122. actual_map = _weighing_actual_map(db, tenant_no, user, [inv.id for inv in inventories])
  123. agg: Dict[str, Dict[str, float]] = {}
  124. for inv in inventories:
  125. label = (inv.product or "").strip() or "未填写货物"
  126. bucket = agg.setdefault(label, {"planned": 0.0, "actual": 0.0})
  127. bucket["planned"] += float(inv.quantity or 0)
  128. bucket["actual"] += actual_map.get(inv.id, 0.0)
  129. return [
  130. DashboardCargoStat(label=label, planned=values["planned"], actual=values["actual"])
  131. for label, values in sorted(agg.items(), key=lambda item: item[1]["planned"], reverse=True)
  132. ]
  133. def _partner_cargo_stats(
  134. db: Session,
  135. user: DbUserContext,
  136. *,
  137. business_type: str,
  138. ) -> list[DashboardPartnerCargoStat]:
  139. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  140. today = date.today()
  141. inventories = (
  142. _scoped_inventory_query(db, tenant_no, user)
  143. .filter(
  144. DbInboundOutbound.date == today,
  145. DbInboundOutbound.business_type == business_type,
  146. )
  147. .all()
  148. )
  149. actual_map = _weighing_actual_map(db, tenant_no, user, [inv.id for inv in inventories])
  150. agg: Dict[str, Dict[str, float]] = {}
  151. for inv in inventories:
  152. product = (inv.product or "").strip() or "未填写货物"
  153. label = f"{inv.partner_company or ''} - {product}"
  154. bucket = agg.setdefault(label, {"planned": 0.0, "actual": 0.0})
  155. bucket["planned"] += float(inv.quantity or 0)
  156. bucket["actual"] += actual_map.get(inv.id, 0.0)
  157. return [
  158. DashboardPartnerCargoStat(label=label, planned=values["planned"], actual=values["actual"])
  159. for label, values in sorted(
  160. agg.items(), key=lambda item: (item[0].split(" - ", 1)[0], -item[1]["planned"])
  161. )
  162. ]