2026-10-19 04:33:31 -04:00
|
|
|
|
"""经理分成共享口径(月结聚合与报表部门汇总的唯一实现)。
|
|
|
|
|
|
|
|
|
|
|
|
从 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
|
|
|
|
|
|
|
2026-10-19 21:44:43 -04:00
|
|
|
|
from app.core.exceptions import BadRequestError
|
|
|
|
|
|
from app.core.month import parse_month
|
2026-10-19 04:33:31 -04:00
|
|
|
|
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);聚合与月工资口径没有
|
|
|
|
|
|
更细的业务日期,取该月最后可确定的生效时点。
|
|
|
|
|
|
"""
|
2026-10-19 21:44:43 -04:00
|
|
|
|
try:
|
|
|
|
|
|
year, mon = parse_month(month)
|
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
|
# schema 已拦住非法月份;内部调用绕过 schema 时也不能变成 500
|
|
|
|
|
|
raise BadRequestError(str(exc)) from exc
|
2026-10-19 04:33:31 -04:00
|
|
|
|
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"),
|
|
|
|
|
|
)
|