PayrollManagementSystem/backend/app/models/performance_ledger.py
liuwenxin a4794489b1 fix(T+与核算): 对齐抵顶口径和托管说明
根据审查结果统一以物抵顶的线下核实口径,移除当前模型、初始化脚本和前端接口中的自动折算误导文案。\n补充业务接入必须持有共享 T+ 命名锁的执行契约,明确来源快照稳定性依赖。\nWindows 部署说明区分新 supervisor 与旧任务迁移检查,避免现场并行启动旧 worker。\n\n影响范围:T+ 托管文档、业务接入契约、回款/台账模型注释和参数试算说明。\n\nCo-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-06 14:49:06 +08:00

187 lines
8.2 KiB
Python
Raw Permalink 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.

"""绩效核算台账模型(M3 ④:每笔回款仅核算一次的固化点)。
**append-only 版本行**:重算 = 插入 calc_version+1 的新行,旧行仅把
is_active 翻 0 —— 这是全表唯一的受控 UPDATE(模型注释与 test_models
均已声明例外),行内容永不改写;历史版本可回溯(重算授权留痕的需求口径)。
防重三层:服务层 CAS(UPDATE ... WHERE settlement_status=0,rowcount=0
即 400)+ FOR UPDATE 行锁 + 库级 uk(receipt_id, calc_version, personnel_id)
兜底。uk 含 personnel_id:二次分配分摊(澄清 #1 答复 2026-10-19,分配
绩效——按方案比例拆给参与人、余额归合同负责人)后同一回款同版本按人
各落一行,拆分仍是「一次核算」的组成部分;聚合行 receipt_id 为 NULL,
MySQL 唯一索引对 NULL 不去重,天然不约束。
经理聚合行(calc_type=2,receipt_id 为 NULL):月度批处理时按「本团队
业务员绩效 ×12.5% + 全公司 ×5%」落一行;其唯一性由月结生成的
settlement_month 串行化 + 服务层保证(MySQL 无部分唯一索引,docstring
声明即可)。
#12 答复(2026-10-19):经理个人户业绩是否计入聚合基数改为参数
``manager.personal.included``(默认 0=不计入)——消费点在 settlement_service
的聚合函数。
"""
from datetime import date
from decimal import Decimal
from sqlalchemy import (
JSON,
Date,
ForeignKey,
Index,
Numeric,
String,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
# 核算类型(performance_ledgers.calc_type)
CALC_TYPE_RECEIPT = 1 # 单笔回款核算
CALC_TYPE_MANAGER_AGGREGATE = 2 # 经理团队聚合(月度批处理)
class PerformanceLedger(Base, TimestampMixin):
"""绩效核算台账表:核算事实的固化点,报表 2.4.1 的数据源。"""
__tablename__ = "performance_ledgers"
__table_args__ = mysql_table_args(
"绩效核算台账表(append-only 版本行;重算插新版本旧行仅翻 is_active)",
# 库级防重兜底:同一回款同版本按人仅一行(分摊拆行后负责人+参与人
# 各占一行;服务层 CAS+行锁之外的第二道闸)。聚合行 receipt_id 为
# NULL,MySQL 唯一索引对 NULL 不去重,天然不约束聚合行
UniqueConstraint(
"receipt_id",
"calc_version",
"personnel_id",
"allocation_key",
name="uk_performance_ledgers_receipt_id_calc_version_personnel",
),
Index("idx_performance_ledgers_month_active", "settlement_month", "is_active"),
Index(
"idx_performance_ledgers_month_personnel_type",
"settlement_month",
"personnel_id",
"calc_type",
),
Index("idx_performance_ledgers_personnel_id", "personnel_id"),
Index("idx_performance_ledgers_receipt_id", "receipt_id"),
Index("idx_performance_ledgers_business_id", "business_id"),
)
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
receipt_id: Mapped[int | None] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("receipts.id", ondelete="RESTRICT"),
comment="回款ID(receipts.id;聚合行可为 NULL)",
)
calc_version: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="1",
comment="核算版本(重算+1,旧行 is_active=0)",
)
allocation_key: Mapped[str] = mapped_column(
String(64), nullable=False, server_default="''", comment="正式FIFO事件键;历史单笔核算为空串",
)
is_active: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="1",
comment="生效标记:1当前生效 0已被重算替代(重算受控 UPDATE 例外)",
)
calc_type: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="1",
comment="核算类型:1单笔回款 2经理团队聚合(月度)",
)
business_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
comment="业务ID(固化无外键;聚合行为该经理名下首行业务ID,报表下钻锚点)",
)
business_no: Mapped[str] = mapped_column(
String(32), nullable=False, comment="业务编号(冗余固化,列表免联查)"
)
personnel_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("personnel.id", ondelete="RESTRICT"),
nullable=False,
comment="归属营销人员ID(personnel.id;核算归属锚点)",
)
personnel_name: Mapped[str] = mapped_column(
String(64), nullable=False, comment="归属营销人员姓名(冗余固化)"
)
role_code: Mapped[str] = mapped_column(
String(32), nullable=False, comment="岗位编码(business_roles.role_code,计提公式锚点)"
)
customer_type: Mapped[str] = mapped_column(
String(32), nullable=False, comment="客户分类:personal/manager_personal/enterprise"
)
product_category: Mapped[str] = mapped_column(
String(32),
nullable=False,
comment="产品类别:municipal_water/rail_transit/mixed(混合业务,行级明细入 snapshot_json)",
)
ship_date: Mapped[date | None] = mapped_column(Date, comment="发货日期(回款天数基准)")
receipt_date: Mapped[date] = mapped_column(
Date, nullable=False, comment="回款日期(参数版本匹配日,裁决 #11)"
)
payment_days: Mapped[int | None] = mapped_column(
comment="回款天数(发货日首日计第 1 天;未指定批次为 NULL;先款后货照实负值)"
)
rate_text: Mapped[str] = mapped_column(
String(32),
nullable=False,
comment="计提比例原文(如 4/360,防 Fraction 截断;聚合行存 12.5%/5% 结构说明)",
)
converted_profit: Mapped[Decimal | None] = mapped_column(
Numeric(14, 2), comment="折算毛利润额(可负;混合业务为各行汇总)"
)
performance_amount: Mapped[Decimal | None] = mapped_column(
Numeric(14, 2), comment="绩效金额(可负,裁决:负绩效照算不归零)"
)
barter_applied: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="0",
comment="以物抵顶处理标记:当前系统不自动折算,1表示由线下规则处理 0表示未标记",
)
weight_r: Mapped[int | None] = mapped_column(comment="支付权重 R(回款占销售额百分比向下取整)")
payment_weight: Mapped[Decimal | None] = mapped_column(
Numeric(7, 6), comment="支付权重系数 R×R(展示用)"
)
payable_amount: Mapped[Decimal | None] = mapped_column(
Numeric(14, 2), comment="当期应付绩效(绩效 × 支付权重;聚合行为当月汇总)"
)
settlement_month: Mapped[str] = mapped_column(
String(7), nullable=False, comment="归属月(回款日期所在月,YYYY-MM;月结与报表维度)"
)
is_probation: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="0",
comment="试用期:1照算不发放 0否",
)
missing_cost: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="0",
comment="成本缺失:1有明细行缺成本(该行毛利按 0 计)0否",
)
snapshot_json: Mapped[dict | None] = mapped_column(
JSON, comment="计算快照(所用参数版本与行级折算明细,重算可回溯)"
)
settlement_id: Mapped[int | None] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("monthly_settlements.id", ondelete="RESTRICT"),
comment="所属月结ID(月结生成时回填;NULL=未入结)",
)
calc_reason: Mapped[str | None] = mapped_column(
String(255), comment="核算/重算原因(重算必填留痕)"
)
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")