- 后端 /api/v1/dashboard 单端点返回四区块:经营总览(绩效/应付/人数/团队数/月结状态/近 6 个月趋势)、待办(待审批 + 回款未指定批次 + 同步显式降级)、风险预警(回款逾期/审批超时,阈值参数化)、最近动态(操作日志) - dashboard_service 实时派生(不落表、不引调度器);参数快照一次解析;总览只做 SQL 聚合,不复用报表的重查询 - V10 迁移新增预警参数三项(warning.enabled / warning.receipt.overdue.days=90 / warning.approval.timeout.days=3),参数注册表 19→22,init-dev 与 V10 SQL 双轨同步 - 测试:test_dashboard 5 例(权限文案/总览对账+非法月份 400/待办三类/预警命中与参数关闭/动态来源);全量 852 passed - 依赖闭包并入:core/month.py 月份校验、api/v1/base/business_roles.py(router 已挂载的岗位只读端点)、test_month_validation - 修复:alembic --sql 在 Windows 按控制台 GBK 输出,V10 SQL 以 UTF-8 重新导出(README 已记) Co-Authored-By: Claude Code <noreply@anthropic.com>
210 lines
6.6 KiB
Python
210 lines
6.6 KiB
Python
"""工作台仓储:四区块的只读聚合查询;不 commit、不抛业务异常。
|
||
|
||
总览只做 SQL 聚合(SUM/COUNT DISTINCT),不复用报表的
|
||
``department_summary``——后者会加载整月台账并逐团队查人员,适合报表页,
|
||
不适合每次打开工作台。
|
||
"""
|
||
|
||
from datetime import date, datetime
|
||
|
||
from sqlalchemy import exists, func, select
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.models import (
|
||
ApprovalOrder,
|
||
Business,
|
||
BusinessRole,
|
||
MonthlySettlement,
|
||
PerformanceLedger,
|
||
Personnel,
|
||
Receipt,
|
||
Shipment,
|
||
)
|
||
from app.models.approval import (
|
||
APPROVAL_STATUS_PENDING_FINANCE,
|
||
APPROVAL_STATUS_PENDING_LEADER,
|
||
)
|
||
|
||
_ACTIVE_SINGLE = (PerformanceLedger.calc_type == 1, PerformanceLedger.is_active == 1)
|
||
_PENDING_STATUSES = (
|
||
APPROVAL_STATUS_PENDING_FINANCE,
|
||
APPROVAL_STATUS_PENDING_LEADER,
|
||
)
|
||
|
||
|
||
# ==== 经营绩效总览 ====
|
||
|
||
def sum_month_performance(session: Session, month: str) -> dict:
|
||
"""当月单笔生效行合计与人员数(拆行求和不失真,人数需 DISTINCT)。"""
|
||
return session.execute(
|
||
select(
|
||
func.coalesce(func.sum(PerformanceLedger.performance_amount), 0).label(
|
||
"performance_total"
|
||
),
|
||
func.coalesce(func.sum(PerformanceLedger.payable_amount), 0).label("payable_total"),
|
||
func.count(func.distinct(PerformanceLedger.personnel_id)).label("person_count"),
|
||
).where(
|
||
PerformanceLedger.settlement_month == month,
|
||
*_ACTIVE_SINGLE,
|
||
)
|
||
).mappings().one()
|
||
|
||
|
||
def performance_trend(session: Session, months: list[str]) -> dict[str, object]:
|
||
"""近 N 个月绩效合计(按台账归属月)。"""
|
||
rows = session.execute(
|
||
select(
|
||
PerformanceLedger.settlement_month,
|
||
func.coalesce(func.sum(PerformanceLedger.performance_amount), 0),
|
||
)
|
||
.where(
|
||
PerformanceLedger.settlement_month.in_(months),
|
||
*_ACTIVE_SINGLE,
|
||
)
|
||
.group_by(PerformanceLedger.settlement_month)
|
||
).all()
|
||
return dict(rows)
|
||
|
||
|
||
def receipt_trend(session: Session, months: list[str]) -> dict[str, object]:
|
||
"""近 N 个月回款合计(按到账月,含未核算回款——与台账口径不同,页面注明)。"""
|
||
month_expr = func.date_format(Receipt.receipt_date, "%Y-%m")
|
||
rows = session.execute(
|
||
select(month_expr, func.coalesce(func.sum(Receipt.amount), 0))
|
||
.where(month_expr.in_(months))
|
||
.group_by(month_expr)
|
||
).all()
|
||
return dict(rows)
|
||
|
||
|
||
def count_active_managers(session: Session) -> int:
|
||
"""在职营销经理数(团队数口径,与部门汇总的团队枚举一致)。"""
|
||
return int(
|
||
session.execute(
|
||
select(func.count())
|
||
.select_from(Personnel)
|
||
.join(BusinessRole, Personnel.business_role_id == BusinessRole.id)
|
||
.where(
|
||
BusinessRole.role_code == "sales_manager",
|
||
Personnel.status == 1,
|
||
)
|
||
).scalar_one()
|
||
)
|
||
|
||
|
||
def get_month_settlement(session: Session, month: str) -> MonthlySettlement | None:
|
||
"""当月月结头(状态与锁定时间角标)。"""
|
||
return session.execute(
|
||
select(MonthlySettlement).where(MonthlySettlement.settlement_month == month)
|
||
).scalar_one_or_none()
|
||
|
||
|
||
# ==== 待办事项 ====
|
||
|
||
def list_pending_approvals(session: Session, *, limit: int) -> list[ApprovalOrder]:
|
||
"""待处理审批单(按提交时间升序——最久的排前面)。"""
|
||
return list(
|
||
session.execute(
|
||
select(ApprovalOrder)
|
||
.where(ApprovalOrder.status.in_(_PENDING_STATUSES))
|
||
.order_by(ApprovalOrder.create_time.asc(), ApprovalOrder.id.asc())
|
||
.limit(limit)
|
||
).scalars()
|
||
)
|
||
|
||
|
||
def count_pending_approvals(session: Session) -> int:
|
||
return int(
|
||
session.execute(
|
||
select(func.count())
|
||
.select_from(ApprovalOrder)
|
||
.where(ApprovalOrder.status.in_(_PENDING_STATUSES))
|
||
).scalar_one()
|
||
)
|
||
|
||
|
||
def list_unassigned_receipts(session: Session, *, limit: int) -> list:
|
||
"""回款未指定发货批次(shipment_id IS NULL,核算前置缺失)。"""
|
||
return list(
|
||
session.execute(
|
||
select(
|
||
Receipt,
|
||
Business.business_no.label("business_no"),
|
||
)
|
||
.outerjoin(Business, Business.id == Receipt.business_id)
|
||
.where(Receipt.shipment_id.is_(None))
|
||
.order_by(Receipt.receipt_date.asc(), Receipt.id.asc())
|
||
.limit(limit)
|
||
).all()
|
||
)
|
||
|
||
|
||
def count_unassigned_receipts(session: Session) -> int:
|
||
return int(
|
||
session.execute(
|
||
select(func.count())
|
||
.select_from(Receipt)
|
||
.where(Receipt.shipment_id.is_(None))
|
||
).scalar_one()
|
||
)
|
||
|
||
|
||
# ==== 风险预警 ====
|
||
|
||
def list_overdue_shipments(session: Session, *, cutoff: date, limit: int) -> list:
|
||
"""发货后超过阈值且该批次无任何回款的发货单(回款逾期规则)。"""
|
||
has_receipt = exists().where(Receipt.shipment_id == Shipment.id)
|
||
return list(
|
||
session.execute(
|
||
select(
|
||
Shipment,
|
||
Business.business_no.label("business_no"),
|
||
)
|
||
.outerjoin(Business, Business.id == Shipment.business_id)
|
||
.where(Shipment.ship_date <= cutoff, ~has_receipt)
|
||
.order_by(Shipment.ship_date.asc())
|
||
.limit(limit)
|
||
).all()
|
||
)
|
||
|
||
|
||
def count_overdue_shipments(session: Session, *, cutoff: date) -> int:
|
||
has_receipt = exists().where(Receipt.shipment_id == Shipment.id)
|
||
return int(
|
||
session.execute(
|
||
select(func.count())
|
||
.select_from(Shipment)
|
||
.where(Shipment.ship_date <= cutoff, ~has_receipt)
|
||
).scalar_one()
|
||
)
|
||
|
||
|
||
def list_overdue_approvals(
|
||
session: Session, *, cutoff: datetime, limit: int
|
||
) -> list[ApprovalOrder]:
|
||
"""提交后超过阈值仍未处理的审批单(审批超时规则)。"""
|
||
return list(
|
||
session.execute(
|
||
select(ApprovalOrder)
|
||
.where(
|
||
ApprovalOrder.status.in_(_PENDING_STATUSES),
|
||
ApprovalOrder.create_time < cutoff,
|
||
)
|
||
.order_by(ApprovalOrder.create_time.asc())
|
||
.limit(limit)
|
||
).scalars()
|
||
)
|
||
|
||
|
||
def count_overdue_approvals(session: Session, *, cutoff: datetime) -> int:
|
||
return int(
|
||
session.execute(
|
||
select(func.count())
|
||
.select_from(ApprovalOrder)
|
||
.where(
|
||
ApprovalOrder.status.in_(_PENDING_STATUSES),
|
||
ApprovalOrder.create_time < cutoff,
|
||
)
|
||
).scalar_one()
|
||
)
|