- 部门汇总全公司 5% 按每位有台账聚合行的营销经理各计一次, 团队应发小计取聚合额 + 调整净额;与月结 total_payable 对拍 (有调整单时 + adjust_net_total),分列舍入差单列 - 人员归属按行类型拆分(普通/经理个人户/仅调整单),调整单全量并入, 团队与未分配不重复;按经理筛选只汇总展示团队 - 导出改单 sheet 分块签字稿,审计覆盖取数与超限;筛选页眉 ID→名称 - 月份校验抽 core/month,报表/月结/调整单非法月份统一 400; 岗位只读选项供财务/领导筛选 - 前端修复:部门下钻带入筛选、回款多桶独立状态、打印截断判定、 summary 请求序号保护 Co-Authored-By: Claude Code <noreply@anthropic.com>
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
"""经理分成共享口径(月结聚合与报表部门汇总的唯一实现)。
|
||
|
||
从 settlement_service 迁出(行为零变化):经理 12.5%/5% 的取参、团队
|
||
分桶、公司基数计算,月结生成与部门汇总报表都要用——抽为单一实现后
|
||
「报表汇总 == 月结聚合行」不靠两处代码各自对齐,而是同源函数的直接
|
||
结果。分成金额仍走 formula.manager_team_share 纯函数。
|
||
"""
|
||
|
||
from calendar import monthrange
|
||
from dataclasses import dataclass
|
||
from datetime import date
|
||
from decimal import Decimal
|
||
from fractions import Fraction
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.exceptions import BadRequestError
|
||
from app.core.month import parse_month
|
||
from app.models import Personnel
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class ManagerShareParams:
|
||
"""经理分成取参与参数原文(同一次计算共用一份,随月月末时点化)。"""
|
||
|
||
month: str
|
||
param_date: date
|
||
team_text: str
|
||
company_text: str
|
||
team_pct: Fraction
|
||
company_pct: Fraction
|
||
include_manager_personal: bool
|
||
|
||
|
||
def month_end(month: str) -> date:
|
||
"""YYYY-MM → 所属月月末(月结与报表取参锚点)。
|
||
|
||
月结是对过去某月的金额计算,取参必须锚定业务月份——按 date.today()
|
||
会让补生成历史月份的结果随操作日期漂移(违背「参数按生效日期时点化」
|
||
红线)。单笔台账行按回款日取参(裁决 #11);聚合与月工资口径没有
|
||
更细的业务日期,取该月最后可确定的生效时点。
|
||
"""
|
||
try:
|
||
year, mon = parse_month(month)
|
||
except ValueError as exc:
|
||
# schema 已拦住非法月份;内部调用绕过 schema 时也不能变成 500
|
||
raise BadRequestError(str(exc)) from exc
|
||
return date(year, mon, monthrange(year, mon)[1])
|
||
|
||
|
||
def manager_share_params(session: Session, month: str) -> ManagerShareParams:
|
||
"""解析经理分成参数(锚定所属月月末,取参与台账行口径同源)。
|
||
|
||
参数按百分数存储(12.5 表示 12.5%):parse_fraction 得 12.5,
|
||
manager_team_share 内部再 ÷100 —— 调用方不得再乘 100(否则放大 100 倍)。
|
||
#12 开关(manager.personal.included)与分成比例同日取参,答复后调整可追溯。
|
||
"""
|
||
from app.services.param_service import parse_fraction, resolve_snapshot
|
||
|
||
param_date = month_end(month)
|
||
snapshot = resolve_snapshot(session, param_date)
|
||
team_text = snapshot.get("manager.share.team")
|
||
company_text = snapshot.get("manager.share.company")
|
||
return ManagerShareParams(
|
||
month=month,
|
||
param_date=param_date,
|
||
team_text=team_text,
|
||
company_text=company_text,
|
||
team_pct=parse_fraction(team_text),
|
||
company_pct=parse_fraction(company_text),
|
||
include_manager_personal=snapshot.get("manager.personal.included") == "1",
|
||
)
|
||
|
||
|
||
def add_team_performance(managers: dict[int, dict], manager_id: int, row) -> None:
|
||
"""把一行业绩并入经理的团队桶(首行固化业务锚点,成员名去重收集)。"""
|
||
bucket = managers.setdefault(
|
||
manager_id,
|
||
{
|
||
"performance": Decimal("0"),
|
||
"business_id": row.business_id,
|
||
"business_no": row.business_no,
|
||
"names": set(),
|
||
},
|
||
)
|
||
bucket["performance"] += row.performance_amount or Decimal("0")
|
||
bucket["names"].add(row.personnel_name)
|
||
|
||
|
||
def collect_team_buckets(
|
||
session: Session, ledger_rows: list, include_manager_personal: bool
|
||
) -> dict[int, dict]:
|
||
"""按经理采集团队业绩。
|
||
|
||
业务员行经 manager_id 上溯到其经理;经理个人户行(#12 开关计入时)
|
||
归属经理本人——行 personnel_id 即经理,不走上溯(经理没有上级,
|
||
按上溯会被跳过漏计)。
|
||
"""
|
||
managers: dict[int, dict] = {}
|
||
for row in ledger_rows:
|
||
if row.customer_type == "manager_personal":
|
||
if include_manager_personal:
|
||
add_team_performance(managers, row.personnel_id, row)
|
||
continue # 默认不计入聚合基数(#12 参数化)
|
||
person = session.get(Personnel, row.personnel_id)
|
||
manager_id = person.manager_id if person else None
|
||
if manager_id is not None:
|
||
add_team_performance(managers, manager_id, row)
|
||
return managers
|
||
|
||
|
||
def company_base_of(ledger_rows: list, include_manager_personal: bool) -> Decimal:
|
||
"""全公司基数(5% 的底):默认排除经理个人户行(#12 开关计入时不过滤)。
|
||
|
||
口径逐行固定,调用方在循环外算一次。
|
||
"""
|
||
return sum(
|
||
(
|
||
r.performance_amount or Decimal("0")
|
||
for r in ledger_rows
|
||
if include_manager_personal or r.customer_type != "manager_personal"
|
||
),
|
||
Decimal("0"),
|
||
)
|