dashboard.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. from __future__ import annotations
  2. from datetime import date
  3. from typing import List
  4. from fastapi import APIRouter, Depends
  5. from sqlalchemy import func, text
  6. from sqlmodel import select
  7. from sqlmodel.ext.asyncio.session import AsyncSession
  8. from app.db.session import get_async_session
  9. from app.models.inventory import InboundOutbound
  10. from app.models.weighing import WeighingSlip
  11. from app.schemas.dashboard import DashboardStats, DashboardCargoStat, DashboardPartnerCargoStat
  12. router = APIRouter(prefix="/dashboard", tags=["dashboard"])
  13. @router.get("/stats", response_model=DashboardStats)
  14. async def get_dashboard_stats(
  15. session: AsyncSession = Depends(get_async_session),
  16. ) -> DashboardStats:
  17. """获取仪表盘统计数据"""
  18. today = date.today().strftime("%Y-%m-%d")
  19. # 计算今日入库总量(从inbound_outbound表)
  20. today_inbound_query = select(func.coalesce(func.sum(InboundOutbound.quantity), 0)).where(
  21. InboundOutbound.date == today,
  22. InboundOutbound.business_type == "入库",
  23. InboundOutbound.is_deleted == 0,
  24. )
  25. today_inbound_result = await session.exec(today_inbound_query)
  26. today_inbound = float(today_inbound_result.one() or 0)
  27. # 计算今日出库总量(从inbound_outbound表)
  28. today_outbound_query = select(func.coalesce(func.sum(InboundOutbound.quantity), 0)).where(
  29. InboundOutbound.date == today,
  30. InboundOutbound.business_type == "出库",
  31. InboundOutbound.is_deleted == 0,
  32. )
  33. today_outbound_result = await session.exec(today_outbound_query)
  34. today_outbound = float(today_outbound_result.one() or 0)
  35. # 计算异常订单数(待入库/待出库状态的订单)
  36. abnormal_query = select(func.count(WeighingSlip.id)).where(
  37. WeighingSlip.is_voided == 0,
  38. )
  39. abnormal_result = await session.exec(abnormal_query)
  40. abnormal_orders = abnormal_result.one() or 0
  41. return DashboardStats(
  42. today_inbound=today_inbound,
  43. today_outbound=today_outbound,
  44. abnormal_orders=abnormal_orders,
  45. )
  46. @router.get("/inbound/by-cargo", response_model=List[DashboardCargoStat])
  47. async def get_inbound_by_cargo(
  48. session: AsyncSession = Depends(get_async_session),
  49. ) -> List[DashboardCargoStat]:
  50. """获取今日入库量按货物统计(计划量 vs 实际量)"""
  51. today = date.today().strftime("%Y-%m-%d")
  52. # 出入库单表左联地磅单表,按货物统计
  53. # 计划量来自出入库单表的quantity,实际量来自地磅单表的net_weight(转换为吨)
  54. query = """
  55. SELECT
  56. COALESCE(io.product, ws.cargo_name, '未填写货物') as label,
  57. COALESCE(SUM(io.quantity), 0) as planned,
  58. COALESCE(SUM(ws.net_weight) / 1000.0, 0) as actual
  59. FROM inbound_outbound io
  60. LEFT JOIN weighing_slips ws ON ws.inbound_outbound_id = io.id
  61. AND ws.is_voided = 0
  62. WHERE io.date = :today
  63. AND io.business_type = '入库'
  64. AND io.is_deleted = 0
  65. GROUP BY COALESCE(io.product, ws.cargo_name, '未填写货物')
  66. ORDER BY planned DESC
  67. """
  68. result = await session.execute(text(query), {"today": today})
  69. rows = result.all()
  70. return [
  71. DashboardCargoStat(label=row.label, planned=float(row.planned), actual=float(row.actual))
  72. for row in rows
  73. ]
  74. @router.get("/inbound/by-partner-cargo", response_model=List[DashboardPartnerCargoStat])
  75. async def get_inbound_by_partner_cargo(
  76. session: AsyncSession = Depends(get_async_session),
  77. ) -> List[DashboardPartnerCargoStat]:
  78. """获取今日入库量按往来单位+货物统计(计划量 vs 实际量)"""
  79. today = date.today().strftime("%Y-%m-%d")
  80. query = """
  81. SELECT
  82. CONCAT(COALESCE(io.partner_company, ''), ' - ', COALESCE(io.product, ws.cargo_name, '未填写货物')) as label,
  83. COALESCE(SUM(io.quantity), 0) as planned,
  84. COALESCE(SUM(ws.net_weight) / 1000.0, 0) as actual
  85. FROM inbound_outbound io
  86. LEFT JOIN weighing_slips ws ON ws.inbound_outbound_id = io.id
  87. AND ws.is_voided = 0
  88. WHERE io.date = :today
  89. AND io.business_type = '入库'
  90. AND io.is_deleted = 0
  91. GROUP BY io.partner_company, io.product, ws.cargo_name
  92. ORDER BY io.partner_company, planned DESC
  93. """
  94. result = await session.execute(text(query), {"today": today})
  95. rows = result.all()
  96. return [
  97. DashboardPartnerCargoStat(label=row.label, planned=float(row.planned), actual=float(row.actual))
  98. for row in rows
  99. ]
  100. @router.get("/outbound/by-cargo", response_model=List[DashboardCargoStat])
  101. async def get_outbound_by_cargo(
  102. session: AsyncSession = Depends(get_async_session),
  103. ) -> List[DashboardCargoStat]:
  104. """获取今日出库量按货物统计(计划量 vs 实际量)"""
  105. today = date.today().strftime("%Y-%m-%d")
  106. query = """
  107. SELECT
  108. COALESCE(io.product, ws.cargo_name, '未填写货物') as label,
  109. COALESCE(SUM(io.quantity), 0) as planned,
  110. COALESCE(SUM(ws.net_weight) / 1000.0, 0) as actual
  111. FROM inbound_outbound io
  112. LEFT JOIN weighing_slips ws ON ws.inbound_outbound_id = io.id
  113. AND ws.is_voided = 0
  114. WHERE io.date = :today
  115. AND io.business_type = '出库'
  116. AND io.is_deleted = 0
  117. GROUP BY COALESCE(io.product, ws.cargo_name, '未填写货物')
  118. ORDER BY planned DESC
  119. """
  120. result = await session.execute(text(query), {"today": today})
  121. rows = result.all()
  122. return [
  123. DashboardCargoStat(label=row.label, planned=float(row.planned), actual=float(row.actual))
  124. for row in rows
  125. ]
  126. @router.get("/outbound/by-partner-cargo", response_model=List[DashboardPartnerCargoStat])
  127. async def get_outbound_by_partner_cargo(
  128. session: AsyncSession = Depends(get_async_session),
  129. ) -> List[DashboardPartnerCargoStat]:
  130. """获取今日出库量按往来单位+货物统计(计划量 vs 实际量)"""
  131. today = date.today().strftime("%Y-%m-%d")
  132. query = """
  133. SELECT
  134. CONCAT(COALESCE(io.partner_company, ''), ' - ', COALESCE(io.product, ws.cargo_name, '未填写货物')) as label,
  135. COALESCE(SUM(io.quantity), 0) as planned,
  136. COALESCE(SUM(ws.net_weight) / 1000.0, 0) as actual
  137. FROM inbound_outbound io
  138. LEFT JOIN weighing_slips ws ON ws.inbound_outbound_id = io.id
  139. AND ws.is_voided = 0
  140. WHERE io.date = :today
  141. AND io.business_type = '出库'
  142. AND io.is_deleted = 0
  143. GROUP BY io.partner_company, io.product, ws.cargo_name
  144. ORDER BY io.partner_company, planned DESC
  145. """
  146. result = await session.execute(text(query), {"today": today})
  147. rows = result.all()
  148. return [
  149. DashboardPartnerCargoStat(label=row.label, planned=float(row.planned), actual=float(row.actual))
  150. for row in rows
  151. ]