feat(migration): V7 核算台账/月结/调整单四表与核算纯函数

- V7 四表:performance_ledgers(append-only 版本行,uk(receipt_id,calc_version)
  库级防重兜底)/monthly_settlements(月份 uk,通过即锁定终态)/
  settlement_items(按人固化可负工资,uk 三列)/adjustment_orders(TZ 单号)
- registry +1:approval.adjustment.enabled(19 项,BOOL 首组扩至三开关)
- formula 补核算纯函数:clamp_weight_r(从预览内联提取共用)/monthly_salary
  三口径/probation_annual_note/manager_team_share(#12 暂口径:基数不含
  经理个人户业绩,聚合集中一处答复后只改)
- 迁移 downgrade 只保留 drop_table+alter 回滚+种子行反向 DELETE;
  performance_ledgers 表名复数(test_models 红线断言)
- 断言同步:EXPECTED_TABLES 四新表/registry 19/BOOL 三开关/conftest
  清理四新表与 ParamValue 边界 22
- schema_sync+全量单测 460 passed

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-16 16:32:32 +08:00
1 parent 30a179fc4c
commit a5c86640e2
17 files changed
+1464 -51

No files matched your search

+3
View File
@@ -85,6 +85,8 @@ MODULE_RECEIPT = "回款管理"
MODULE_RATING = "客户评级"
MODULE_SYSTEM_SETTINGS = "系统设置"
MODULE_APPROVAL = "审批管理"
MODULE_CALC = "绩效核算"
MODULE_SETTLEMENT = "月度结算"
# ===== 单据编号前缀({前缀}{YYYYMMDD}-{当日4位序号},序号按自然日重置)=====
# 一期为代码常量(业务未提出配置诉求);如需可配置,走 M1 参数体系新增参数组
@@ -93,6 +95,7 @@ NO_PREFIX_SHIPMENT = "SH"
NO_PREFIX_RECEIPT = "HK"
NO_PREFIX_PARAM = "P"
NO_PREFIX_APPROVAL = "AP"
NO_PREFIX_ADJUSTMENT = "TZ"
ACTION_CREATE = "新增"
ACTION_UPDATE = "修改"
+6
View File
@@ -17,16 +17,19 @@ from app.models.customer import Customer, OwnershipChangeLog
from app.models.login_log import LoginLog
from app.models.operation_log import OperationLog
from app.models.param import ParamChangeBatch, ParamDefinition, ParamValue
from app.models.performance_ledger import PerformanceLedger
from app.models.personnel import Personnel, PersonnelPlan, PersonnelRegion
from app.models.product import Product
from app.models.rating import CustomerRatingGrade
from app.models.receipt import Receipt
from app.models.role import Permission, Role, RolePermission
from app.models.settlement import AdjustmentOrder, MonthlySettlement, SettlementItem
from app.models.shipment import Shipment, ShipmentItem
from app.models.system_setting import SystemSetting
from app.models.user import User, UserRole
__all__ = [
"AdjustmentOrder",
"ApprovalOrder",
"ApprovalStep",
"Base",
@@ -37,11 +40,13 @@ __all__ = [
"Customer",
"CustomerRatingGrade",
"LoginLog",
"MonthlySettlement",
"OperationLog",
"OwnershipChangeLog",
"ParamChangeBatch",
"ParamDefinition",
"ParamValue",
"PerformanceLedger",
"Permission",
"Personnel",
"PersonnelPlan",
@@ -51,6 +56,7 @@ __all__ = [
"Role",
"RolePermission",
"SecondaryAllocation",
"SettlementItem",
"Shipment",
"ShipmentItem",
"SystemSetting",
+3 -2
View File
@@ -42,9 +42,10 @@ APPROVAL_ACTION_REJECT = 2 # 人工驳回
APPROVAL_ACTION_AUTO_APPROVE = 3 # 系统自动通过(开关关闭时逐节点留痕)
APPROVAL_ACTION_WITHDRAW = 4 # 撤回(业务重新提交时旧在途单注销)
# 审批对象类型(approval_orders.order_type;2 为 ④ 月结预留)
# 审批对象类型(approval_orders.order_type)
APPROVAL_TYPE_SECONDARY_ALLOCATION = 1
APPROVAL_TYPE_MONTHLY_SETTLEMENT = 2
APPROVAL_TYPE_ADJUSTMENT = 3
class ApprovalOrder(Base, TimestampMixin):
@@ -66,7 +67,7 @@ class ApprovalOrder(Base, TimestampMixin):
comment="审批单号,自动生成 AP{YYYYMMDD}-{4位当日序号}",
)
order_type: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, comment="审批对象类型:1二次分配方案 2月度结算(预留)"
TINYINT_TYPE, nullable=False, comment="审批对象类型:1二次分配方案 2月度结算 3调整单"
)
ref_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
+173
View File
@@ -0,0 +1,173 @@
"""绩效核算台账模型(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) 兜底。
经理聚合行(calc_type=2,receipt_id 为 NULL):月度批处理时按「本团队
业务员绩效 ×12.5% + 全公司 ×5%」落一行;其唯一性由月结生成的
settlement_month 串行化 + 服务层保证(MySQL 无部分唯一索引,docstring
声明即可)。
#12 策略点(待业务答复):经理个人户业绩暂不计入聚合基数——聚合集中
在 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", name="uk_performance_ledgers_receipt_id_calc_version"
),
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_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)",
)
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已按 80% 折算 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="备注")
+197
View File
@@ -0,0 +1,197 @@
"""月度结算与调整单模型(M3 ④:月结审批报表补充需求)。
- 月度结算按「确认回款」和计算快照生成;**通过即锁定**(status=3 已审定
即终态,锁定后源数据改动只产生提示与后续调整)
- settlement_items 明细按人固化(可负=负数工资),锁定后无 UPDATE 路径
(重生成 = 草稿/驳回态删旧插新)
- 调整单是已锁定月份的唯一调整通道(补发/扣减,走审批;生效后不回写
已锁定月结——append-only,报表汇总并入)
"""
from datetime import datetime
from decimal import Decimal
from sqlalchemy import JSON, DateTime, ForeignKey, Numeric, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
# 月结状态(monthly_settlements.status;通过即锁定——两态合一)
SETTLEMENT_STATUS_DRAFT = 1 # 草稿(可重新生成/提交)
SETTLEMENT_STATUS_PENDING = 2 # 待审批(提交后)
SETTLEMENT_STATUS_APPROVED = 3 # 已审定(终态=已锁定)
SETTLEMENT_STATUS_REJECTED = 4 # 已驳回(回草稿可重提)
# 调整单状态(adjustment_orders.status)
ADJUSTMENT_STATUS_PENDING = 1 # 待审批
ADJUSTMENT_STATUS_EFFECTIVE = 2 # 已生效
ADJUSTMENT_STATUS_REJECTED = 3 # 已驳回
ADJUSTMENT_STATUS_WITHDRAWN = 4 # 已撤回
# 调整方向(adjustment_orders.direction)
ADJUSTMENT_DIRECTION_EXTRA = 1 # 补发
ADJUSTMENT_DIRECTION_DEDUCT = 2 # 扣减
class MonthlySettlement(Base, TimestampMixin):
"""月度结算表:按月一行(settlement_month 唯一),快照 + 审批 + 锁定。"""
__tablename__ = "monthly_settlements"
__table_args__ = mysql_table_args("月度结算表(按月一行;通过即锁定,锁定后走调整单)")
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
settlement_month: Mapped[str] = mapped_column(
String(7), nullable=False, unique=True, comment="结算月份(YYYY-MM;全表唯一)"
)
status: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="1",
comment="状态:1草稿 2待审批 3已审定(终态=已锁定)4已驳回(回草稿)",
)
total_performance: Mapped[Decimal | None] = mapped_column(
Numeric(14, 2), comment="绩效金额合计(可负)"
)
total_payable: Mapped[Decimal | None] = mapped_column(
Numeric(14, 2), comment="应付绩效合计(可负)"
)
person_count: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, server_default="0", comment="结算人数"
)
snapshot_json: Mapped[dict | None] = mapped_column(
JSON, comment="月度总览快照(生成时刻的确认回款口径说明与汇总参数)"
)
approval_order_no: Mapped[str | None] = mapped_column(
String(32), comment="关联审批单号(冗余固化;开关关闭自动通过时也有单)"
)
submitter_id: Mapped[int | None] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("users.id", ondelete="SET NULL"),
comment="提交人ID(账号删除后置空)",
)
submitter_name: Mapped[str] = mapped_column(
String(64), nullable=False, comment="提交人用户名(冗余固化)"
)
locked_at: Mapped[datetime | None] = mapped_column(
DateTime, comment="锁定时间(status=3 时回填)"
)
locked_by: Mapped[str | None] = mapped_column(String(64), comment="锁定操作人(冗余固化)")
class SettlementItem(Base):
"""月结明细表:按人按核算类型固化金额(锁定后只读;重生成=草稿态删旧插新)。"""
__tablename__ = "settlement_items"
__table_args__ = mysql_table_args(
"月结明细表(按人固化;支持负数工资;锁定后只读)",
)
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
settlement_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("monthly_settlements.id", ondelete="CASCADE"),
nullable=False,
comment="月结ID(monthly_settlements.id;头删明细随删)",
)
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="岗位编码(主岗,聚合锚点)"
)
calc_type: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, comment="核算类型:1单笔回款 2经理团队聚合"
)
performance_sum: Mapped[Decimal] = mapped_column(
Numeric(14, 2),
nullable=False,
comment="绩效金额合计(可负;聚合行排除经理个人户业绩,#12 暂口径)",
)
payable_sum: Mapped[Decimal] = mapped_column(
Numeric(14, 2), nullable=False, comment="应付绩效合计(可负)"
)
basic_amount: Mapped[Decimal | None] = mapped_column(
Numeric(14, 2), comment="月工资标准(personnel.salary_standard 当月快照)"
)
salary_mode: Mapped[str | None] = mapped_column(
String(16), comment="月工资口径快照:INCLUDE/DEDUCT/EXCLUDE"
)
net_amount: Mapped[Decimal | None] = mapped_column(
Numeric(14, 2), comment="净工资 = 绩效±基本工资(按口径;试用期 0)"
)
is_probation: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="0",
comment="试用期:1绩效照算不发放(net=0,超年累计记 note)0否",
)
probation_note: Mapped[str | None] = mapped_column(
String(255), comment="试用期说明(年累计超阈值部分年底一次性结算)"
)
snapshot_json: Mapped[dict | None] = mapped_column(
JSON, comment="个人明细快照(台账行引用列表)"
)
class AdjustmentOrder(Base, TimestampMixin):
"""调整单:已锁定月份的唯一调整通道(补发/扣减,走审批;不回写月结)。"""
__tablename__ = "adjustment_orders"
__table_args__ = mysql_table_args(
"调整单表(已锁定月份的补发/扣减;走审批;生效后不回写月结快照)",
)
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
order_no: Mapped[str] = mapped_column(
String(32),
nullable=False,
unique=True,
comment="调整单号,自动生成 TZ{YYYYMMDD}-{4位当日序号}",
)
settlement_month: Mapped[str] = mapped_column(
String(7), nullable=False, comment="调整月份(YYYY-MM;跨月规则待澄清 #9 前不限制)"
)
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="调整人员姓名(冗余固化)"
)
direction: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, comment="方向:1补发 2扣减"
)
amount: Mapped[Decimal] = mapped_column(
Numeric(14, 2), nullable=False, comment="调整金额(恒正,方向表达语义)"
)
reason: Mapped[str] = mapped_column(
String(255), nullable=False, comment="扣补说明(必填,审批与追溯锚点)"
)
status: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="1",
comment="状态:1待审批 2已生效 3已驳回 4已撤回",
)
submitter_id: Mapped[int | None] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("users.id", ondelete="SET NULL"),
comment="提交人ID(账号删除后置空)",
)
submitter_name: Mapped[str] = mapped_column(
String(64), nullable=False, comment="提交人用户名(冗余固化)"
)
+60
View File
@@ -95,3 +95,63 @@ def start_payment_ok(received: Decimal, revenue: Decimal, ratio: Fraction) -> bo
if revenue == 0:
return False
return Fraction(received) / Fraction(revenue) >= ratio / 100
def clamp_weight_r(received: Decimal, revenue: Decimal) -> int:
"""支付权重 R:回款占销售额百分比,封顶封底 [0, 100](对照表 #22)。
received 非负时 int() 即向下取整;min/max 同时是红字冲销(负数回款)
接入时的方向防线。从计算预览的内联实现提取 —— ④ 核算与预览共用。
"""
if revenue <= 0:
return 0
return max(0, min(100, int(Fraction(received) / Fraction(revenue) * 100)))
def monthly_salary(performance: Decimal, basic: Decimal | None, mode: str) -> Decimal | None:
"""月工资分岗口径(对照表 #8,salary.basic.mode 按角色维度取值)。
- INCLUDE:绩效 + 基本工资(业务员默认——绩效之外照发基本工资)
- DEDUCT:绩效 − 基本工资(经理默认——基本工资从绩效分成中扣除)
- EXCLUDE:仅绩效,基本工资不参与
basic 缺失时按 0 参与(INCLUDE/EXCLUDE 等价于纯绩效)。
"""
basic_value = basic if basic is not None else Decimal("0")
if mode == "INCLUDE":
return performance + basic_value
if mode == "DEDUCT":
return performance - basic_value
if mode == "EXCLUDE":
return performance
raise ValueError(f"未知的月工资口径:{mode}")
def probation_annual_note(year_total: Decimal, threshold: Decimal) -> str | None:
"""试用期年累计说明:超阈值部分年底一次性结算(对照表 #17)。
未超阈值返回 None(无需说明);阈值按元展示(Decimal 直出)。
"""
if year_total <= threshold:
return None
excess = (year_total - threshold).quantize(Decimal("0.01"), rounding="ROUND_HALF_UP")
return f"试用期绩效照算不发放;年累计超 {threshold} 元部分年底一次性结算(当前超 {excess} 元)"
def manager_team_share(
team_performance: Decimal,
company_performance: Decimal,
*,
team_pct: Fraction,
company_pct: Fraction,
) -> Decimal:
"""经理绩效 = 本团队业务员绩效 × 12.5% + 全公司业务员绩效 × 5%(裁决 #6/#15/#16)。
**#12 策略点(待业务答复,2026-10-16 暂定口径)**:基数**不含**经理本人
名下经理个人户业务的业绩——该部分按经理个人户规则另行处理(单笔落台账
rate=0/绩效 0,待答复后若需并入,只改调用方传入的基数集合)。
团队口径:manager_id=该经理且岗位非「营销经理」的业务员绩效行之和;
全公司口径:全部业务员(非经理岗)绩效行之和。
"""
return _to_decimal(Fraction(team_performance) * team_pct / 100) + _to_decimal(
Fraction(company_performance) * company_pct / 100
)
+17
View File
@@ -290,6 +290,23 @@ PARAM_REGISTRY: dict[str, ParamSpec] = {
),
sort_order=18,
),
ParamSpec(
code="approval.adjustment.enabled",
name="调整单审批开关",
group="审批开关",
value_type="BOOL",
unit=None,
dimension_type="NONE",
options=None,
default_value="0",
value_min=None,
value_max=None,
description=(
"0=调整单提交即生效(系统自动通过留痕);1=走财务复核→领导审核审批流。"
"调整单是已锁定月份补发/扣减的唯一通道"
),
sort_order=19,
),
]
}
+2 -5
View File
@@ -614,11 +614,8 @@ def preview(session: Session, payload: ParamPreviewIn) -> ParamPreviewOut:
start_ratio = take("payment.start.ratio")
start_ok = formula.start_payment_ok(received, revenue, start_ratio)
# R 按「回款占销售额百分比向下取整」口径封顶封底:received 非负时 int()
# 即向下取整;min/max 同时是红字冲销(负数回款)接入时的方向防线
weight_r = (
max(0, min(100, int(Fraction(received) / Fraction(revenue) * 100))) if revenue > 0 else 0
)
# R 钳制已提取为纯函数 clamp_weight_r(④ 核算与预览共用同一实现)
weight_r = formula.clamp_weight_r(received, revenue)
weight = formula.payment_weight(weight_r)
base_amount = after_barter if barter_applied else performance
payable = to_decimal(Fraction(base_amount) * weight)