feat(核算)!: 添加 T+ 正式核算与采集托管

补齐源快照、资料预演、正式导入及工资核算的依赖链路。
新增版本化输入、逐段 FIFO、生成锁和结构化当月调整,
并提供默认关闭的周期采集、有限重试及 Windows 托管。

原始客户资料、私有配置及首页改动不纳入本次提交。

BREAKING CHANGE: 升级前需应用新增迁移,并通过
PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。
旧单批次核算默认关闭;历史兼容需显式启用
PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
This commit is contained in:
liuwenxin committed 2026-11-02 09:42:53 +08:00
1 parent 1b074576fe
commit 108bf87225
154 files changed
+9515 -645

No files matched your search

+63 -1
View File
@@ -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