|
|
@@ -1,9 +1,10 @@
|
|
|
from __future__ import annotations
|
|
|
|
|
|
from datetime import date
|
|
|
+from typing import Dict
|
|
|
|
|
|
from fastapi import APIRouter, Depends
|
|
|
-from sqlalchemy import func, text
|
|
|
+from sqlalchemy import func
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
from app.core.database import get_db
|
|
|
@@ -15,10 +16,28 @@ from app.schemas.db.dashboard import (
|
|
|
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),
|
|
|
@@ -28,36 +47,30 @@ def get_dashboard_stats(
|
|
|
today = date.today()
|
|
|
|
|
|
today_inbound = float(
|
|
|
- db.query(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
|
|
|
+ _scoped_inventory_query(db, tenant_no, user)
|
|
|
.filter(
|
|
|
DbInboundOutbound.date == today,
|
|
|
DbInboundOutbound.business_type == "入库",
|
|
|
- DbInboundOutbound.del_flag == 0,
|
|
|
- DbInboundOutbound.tenant_no == tenant_no,
|
|
|
)
|
|
|
+ .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
|
|
|
.scalar()
|
|
|
or 0
|
|
|
)
|
|
|
|
|
|
today_outbound = float(
|
|
|
- db.query(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
|
|
|
+ _scoped_inventory_query(db, tenant_no, user)
|
|
|
.filter(
|
|
|
DbInboundOutbound.date == today,
|
|
|
DbInboundOutbound.business_type == "出库",
|
|
|
- DbInboundOutbound.del_flag == 0,
|
|
|
- DbInboundOutbound.tenant_no == tenant_no,
|
|
|
)
|
|
|
+ .with_entities(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
|
|
|
.scalar()
|
|
|
or 0
|
|
|
)
|
|
|
|
|
|
abnormal_orders = int(
|
|
|
- db.query(func.count(DbWeighingSlip.id))
|
|
|
- .filter(
|
|
|
- DbWeighingSlip.is_voided == 0,
|
|
|
- DbWeighingSlip.del_flag == 0,
|
|
|
- DbWeighingSlip.tenant_no == tenant_no,
|
|
|
- )
|
|
|
+ _scoped_weighing_query(db, tenant_no, user)
|
|
|
+ .with_entities(func.count(DbWeighingSlip.id))
|
|
|
.scalar()
|
|
|
or 0
|
|
|
)
|
|
|
@@ -101,36 +114,49 @@ def get_outbound_by_partner_cargo(
|
|
|
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()
|
|
|
- query = text(
|
|
|
- """
|
|
|
- SELECT
|
|
|
- COALESCE(io.product, ws.cargo_name, '未填写货物') AS label,
|
|
|
- COALESCE(SUM(io.quantity), 0) AS planned,
|
|
|
- COALESCE(SUM(ws.net_weight) / 1000.0, 0) AS actual
|
|
|
- FROM db_inbound_outbound io
|
|
|
- LEFT JOIN db_weighing_slip ws
|
|
|
- ON ws.inbound_outbound_id = io.id
|
|
|
- AND ws.is_voided = 0
|
|
|
- AND ws.del_flag = 0
|
|
|
- AND ws.tenant_no = io.tenant_no
|
|
|
- WHERE io.date = :today
|
|
|
- AND io.business_type = :business_type
|
|
|
- AND io.del_flag = 0
|
|
|
- AND io.tenant_no = :tenant_no
|
|
|
- GROUP BY COALESCE(io.product, ws.cargo_name, '未填写货物')
|
|
|
- ORDER BY planned DESC
|
|
|
- """
|
|
|
+ inventories = (
|
|
|
+ _scoped_inventory_query(db, tenant_no, user)
|
|
|
+ .filter(
|
|
|
+ DbInboundOutbound.date == today,
|
|
|
+ DbInboundOutbound.business_type == business_type,
|
|
|
+ )
|
|
|
+ .all()
|
|
|
)
|
|
|
- rows = db.execute(
|
|
|
- query,
|
|
|
- {"today": today, "business_type": business_type, "tenant_no": tenant_no},
|
|
|
- ).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=row.label, planned=float(row.planned), actual=float(row.actual))
|
|
|
- for row in rows
|
|
|
+ DashboardCargoStat(label=label, planned=values["planned"], actual=values["actual"])
|
|
|
+ for label, values in sorted(agg.items(), key=lambda item: item[1]["planned"], reverse=True)
|
|
|
]
|
|
|
|
|
|
|
|
|
@@ -142,39 +168,27 @@ def _partner_cargo_stats(
|
|
|
) -> list[DashboardPartnerCargoStat]:
|
|
|
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
|
|
today = date.today()
|
|
|
- query = text(
|
|
|
- """
|
|
|
- SELECT
|
|
|
- CONCAT(
|
|
|
- COALESCE(io.partner_company, ''),
|
|
|
- ' - ',
|
|
|
- COALESCE(io.product, ws.cargo_name, '未填写货物')
|
|
|
- ) AS label,
|
|
|
- COALESCE(SUM(io.quantity), 0) AS planned,
|
|
|
- COALESCE(SUM(ws.net_weight) / 1000.0, 0) AS actual
|
|
|
- FROM db_inbound_outbound io
|
|
|
- LEFT JOIN db_weighing_slip ws
|
|
|
- ON ws.inbound_outbound_id = io.id
|
|
|
- AND ws.is_voided = 0
|
|
|
- AND ws.del_flag = 0
|
|
|
- AND ws.tenant_no = io.tenant_no
|
|
|
- WHERE io.date = :today
|
|
|
- AND io.business_type = :business_type
|
|
|
- AND io.del_flag = 0
|
|
|
- AND io.tenant_no = :tenant_no
|
|
|
- GROUP BY io.partner_company, io.product, ws.cargo_name
|
|
|
- ORDER BY io.partner_company, planned DESC
|
|
|
- """
|
|
|
+ inventories = (
|
|
|
+ _scoped_inventory_query(db, tenant_no, user)
|
|
|
+ .filter(
|
|
|
+ DbInboundOutbound.date == today,
|
|
|
+ DbInboundOutbound.business_type == business_type,
|
|
|
+ )
|
|
|
+ .all()
|
|
|
)
|
|
|
- rows = db.execute(
|
|
|
- query,
|
|
|
- {"today": today, "business_type": business_type, "tenant_no": tenant_no},
|
|
|
- ).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=row.label,
|
|
|
- planned=float(row.planned),
|
|
|
- actual=float(row.actual),
|
|
|
+ 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"])
|
|
|
)
|
|
|
- for row in rows
|
|
|
]
|