PayrollManagementSystem/backend/app/repositories/dashboard_repository.py
liuwenxin 6ff202d637 feat(dashboard): 工作台四区块实时聚合与风险预警参数
- 后端 /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>
2026-10-19 18:44:15 +08:00

210 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""工作台仓储:四区块的只读聚合查询;不 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()
)