feat(核算)!: 添加 T+ 正式核算与采集托管
补齐源快照、资料预演、正式导入及工资核算的依赖链路。 新增版本化输入、逐段 FIFO、生成锁和结构化当月调整, 并提供默认关闭的周期采集、有限重试及 Windows 托管。 原始客户资料、私有配置及首页改动不纳入本次提交。 BREAKING CHANGE: 升级前需应用新增迁移,并通过 PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。 旧单批次核算默认关闭;历史兼容需显式启用 PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
This commit is contained in:
1 parent
1b074576fe
commit
108bf87225
154 files changed
+9515
-645
No files matched your search
@@ -154,6 +154,16 @@ def department_summary(session, query) -> dict:
|
||||
manager_of, manager_role, person_info = _team_relations(
|
||||
session, member_map, manager_ids, adjustments
|
||||
)
|
||||
attributed_managers = {mid for a in adjustments.values() for mid in a.get("by_manager", {}) if mid is not None}
|
||||
manager_ids = sorted(set(manager_ids) | attributed_managers)
|
||||
frozen_rows = rows if any((getattr(r, "snapshot_json", None) or {}).get("engine") == "FIFO" for r in rows) or attributed_managers else None
|
||||
for mid in attributed_managers:
|
||||
manager_role[mid] = "sales_manager"
|
||||
if frozen_rows is not None:
|
||||
for row in frozen_rows:
|
||||
snapshot = row.snapshot_json or {}
|
||||
if snapshot.get("engine") == "FIFO" and snapshot.get("manager_id") is not None:
|
||||
manager_role[int(snapshot["manager_id"])] = "sales_manager"
|
||||
# 只有调整单、本月无台账行的人:补成零值成员后参与归属
|
||||
_merge_adjustment_only_members(member_map, adjustments, person_info)
|
||||
|
||||
@@ -167,6 +177,7 @@ def department_summary(session, query) -> dict:
|
||||
"include_mp": include_mp,
|
||||
"share": share,
|
||||
"company_base": company_base,
|
||||
"frozen_rows": frozen_rows,
|
||||
}
|
||||
teams_all = _build_teams(session, manager_ids, context)
|
||||
if query.manager_id is None:
|
||||
@@ -186,7 +197,10 @@ def department_summary(session, query) -> dict:
|
||||
for team in teams_all
|
||||
if query.manager_id is None or team["manager_id"] == query.manager_id
|
||||
]
|
||||
unassigned_all = _unassigned_rows(member_map, manager_of, manager_role, adjustments, include_mp)
|
||||
unassigned_all = (
|
||||
_frozen_member_rows(None, context) if frozen_rows is not None
|
||||
else _unassigned_rows(member_map, manager_of, manager_role, adjustments, include_mp)
|
||||
)
|
||||
# 「仅看某经理团队」时未分配桶不随展示:全公司无经理人员与该团队无关
|
||||
unassigned = [] if query.manager_id is not None else unassigned_all
|
||||
company_share_amount = _company_share(company_base, share.company_pct)
|
||||
@@ -219,6 +233,47 @@ def department_summary(session, query) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _frozen_member_rows(manager_id, context):
|
||||
"""按台账固化的业务分管拆成员,同一业务员可分别出现在多个团队。"""
|
||||
targets = {}
|
||||
source_rows = context["frozen_rows"]
|
||||
for row in source_rows:
|
||||
snap = row.snapshot_json or {}
|
||||
if snap.get("engine") == "FIFO":
|
||||
target = snap.get("manager_id") if not snap.get("manager_own_excluded") else None
|
||||
else:
|
||||
target = _regular_team_of(row.personnel_id, context["manager_of"], context["manager_role"])
|
||||
if row.customer_type == "manager_personal":
|
||||
target = row.personnel_id if context["include_mp"] else None
|
||||
targets[row.id] = target
|
||||
selected = [r for r in source_rows if targets[r.id] == manager_id]
|
||||
members = _member_map(selected)
|
||||
local_adjustments = {}
|
||||
for pid, adjustment in context["adjustments"].items():
|
||||
candidates = {targets[r.id] for r in source_rows if r.personnel_id == pid}
|
||||
present = sorted(t for t in candidates if t is not None)
|
||||
default_target = present[0] if present else (
|
||||
None if candidates else _regular_team_of(pid, context["manager_of"], context["manager_role"])
|
||||
)
|
||||
totals = {"extra": Decimal(0), "deduct": Decimal(0)}
|
||||
selected_adjustment = adjustment.get("by_manager", {}).get(manager_id, {})
|
||||
for field in totals:
|
||||
totals[field] += selected_adjustment.get(field, Decimal(0))
|
||||
if default_target == manager_id:
|
||||
totals[field] += adjustment.get("unattributed", adjustment)[field]
|
||||
local_adjustments[pid] = totals
|
||||
if any(totals.values()) and pid not in members:
|
||||
members[pid] = context["member_map"][pid]
|
||||
output = []
|
||||
for pid, member in members.items():
|
||||
actual = [r for r in selected if r.personnel_id == pid]
|
||||
output.append(_member_row(pid, member, local_adjustments, "regular",
|
||||
performance=sum((r.performance_amount or Decimal(0) for r in actual), Decimal(0)),
|
||||
payable=sum((r.payable_amount or Decimal(0) for r in actual), Decimal(0)),
|
||||
ledger_ids=[r.id for r in actual], include_adjust=True))
|
||||
return output
|
||||
|
||||
|
||||
def _build_teams(session, manager_ids: list[int], context: dict) -> list[dict]:
|
||||
"""按经理枚举生成团队行(含零成员团队)。"""
|
||||
teams = []
|
||||
@@ -243,6 +298,8 @@ def _build_team(session, manager_id: int, context: dict) -> dict | None:
|
||||
context["adjustments"],
|
||||
context["include_mp"],
|
||||
)
|
||||
if context.get("frozen_rows") is not None:
|
||||
member_rows = _frozen_member_rows(manager_id, context)
|
||||
team_extra = sum((m["adjust_extra"] for m in member_rows), Decimal("0"))
|
||||
team_deduct = sum((m["adjust_deduct"] for m in member_rows), Decimal("0"))
|
||||
team_base = _dec(context["buckets"].get(manager_id, {}).get("performance", Decimal("0")))
|
||||
@@ -397,12 +454,17 @@ def _adjustments_by_person(orders) -> dict[int, dict]:
|
||||
"extra": Decimal("0"),
|
||||
"deduct": Decimal("0"),
|
||||
"personnel_name": order.personnel_name,
|
||||
"by_manager": {},
|
||||
"unattributed": {"extra": Decimal(0), "deduct": Decimal(0)},
|
||||
},
|
||||
)
|
||||
if order.direction == ADJUSTMENT_DIRECTION_EXTRA:
|
||||
acc["extra"] += order.amount
|
||||
else:
|
||||
acc["deduct"] += order.amount
|
||||
attribution = getattr(order, "attribution_json", None)
|
||||
bucket = acc["by_manager"].setdefault(attribution.get("manager_id"), {"extra": Decimal(0), "deduct": Decimal(0)}) if attribution else acc["unattributed"]
|
||||
bucket["extra" if order.direction == ADJUSTMENT_DIRECTION_EXTRA else "deduct"] += order.amount
|
||||
return by_person
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user