DB 列表/仪表盘 scoped 聚合,操作日志按 user_id 过滤,工作流实例详情与审批前校验 scope。 Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
6.4 KiB
Python
195 lines
6.4 KiB
Python
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"])
|
|
)
|
|
]
|