diff --git a/backend/app/services/calc_service.py b/backend/app/services/calc_service.py index 3cfd511..8fb740c 100644 --- a/backend/app/services/calc_service.py +++ b/backend/app/services/calc_service.py @@ -60,7 +60,7 @@ from app.services.param_service import parse_fraction, resolve_snapshot logger = logging.getLogger(__name__) # snapshot_json 里的策略点注记(口径检索锚点;#12 已参数化,开关 0 时留此注记) -_NOTE_MANAGER_PENDING = "经理个人户单笔不计提(基数开关=0):聚合口径在月结时由团队聚合计算" +_NOTE_MANAGER_PENDING = "经理个人户单笔不计提(未计入分成基数):绩效在月结时按团队与全公司分成计算" @dataclass(frozen=True, slots=True) @@ -544,7 +544,7 @@ def _personal_rate( if customer_type == "enterprise": # 存储口径 ‰ → 纯比例(漏换算放大 1000 倍,与预览同防线) rate = parse_fraction(snapshot.get("enterprise.rate")) / 1000 - return _RateDecision(rate, "企业户固定计提比例", None, False) + return _RateDecision(rate, "企业户:不按回款天数,统一按固定比例计提", None, False) template = role.calc_template if role else "TPL_SALESPERSON_DECAY" if template == "TPL_MANAGER_SHARE": @@ -554,7 +554,7 @@ def _personal_rate( if snapshot.get("manager.personal.included") != "1": return _RateDecision( Decimal("0"), - "经理个人户:12.5%+5% 聚合计提(月结时计算),单笔落 0", + "经理个人户:绩效按团队与全公司分成在月结时计算,单笔不计提", None, True, ) @@ -563,12 +563,12 @@ def _personal_rate( floor = parse_fraction(snapshot.get("salesperson.rate.floor")) boundary = int(parse_fraction(snapshot.get("salesperson.days.boundary"))) rate = salesperson_rate(days, start=start, slope=slope, floor=floor, boundary_days=boundary) - note = f"经理个人户(基数开关=1):按业务员递减 {start}‰ 起,{days} 天" + note = f"经理个人户(已开启计入分成基数):按业务员递减,回款 {days} 天,起始 {start}‰" return _RateDecision(rate, note, None, False) if template == "TPL_INFO_SALES_FIXED": # 信息员基数=本人销售额(覆盖折算与阈值),‰ 换算同防线 rate = parse_fraction(snapshot.get("info.rate")) / 1000 - return _RateDecision(rate, "信息员:本人销售额 × 计提比例", None, False) + return _RateDecision(rate, "信息员:按本人销售额乘计提比例,不随回款天数递减", None, False) if template == "TPL_SALESPERSON_DECAY": start = parse_fraction(snapshot.get("salesperson.rate.start")) slope = parse_fraction(snapshot.get("salesperson.rate.slope")) @@ -577,9 +577,16 @@ def _personal_rate( # 经 Fraction 转换再取整(INT 型参数入库时已校验分母为 1,取整无损) boundary = int(parse_fraction(snapshot.get("salesperson.days.boundary"))) rate = salesperson_rate(days, start=start, slope=slope, floor=floor, boundary_days=boundary) - note = f"业务员递减:{start}‰ 起,{days} 天(分界 {boundary} 天,超界取 {floor}‰)" + note = ( + f"回款 {days} 天,从起始 {start}‰ 按日递减" + f"(满 {boundary} 天后固定按 {floor}‰ 计算)" + ) return _RateDecision(rate, note, None, False) - raise BadRequestError(f"公式模板 {template} 暂不支持核算") + # 提示给模板中文名而非编码:业务人员在岗位角色页看到的是「营销经理·团队与全公司分成」 + from app.services.calc_templates import TEMPLATES + + tpl_name = TEMPLATES[template].name if template in TEMPLATES else template + raise BadRequestError(f"「{tpl_name}」不支持单笔核算,请检查该人员的岗位设置") def _role_of(session: Session, person): diff --git a/backend/app/services/param_registry.py b/backend/app/services/param_registry.py index 103d720..14a747b 100644 --- a/backend/app/services/param_registry.py +++ b/backend/app/services/param_registry.py @@ -3,6 +3,10 @@ 注册表是**迁移种子与运行期校验的唯一来源**:编码、默认值、取值域在此定义 一次;新增参数 = 改这里 + 生成迁移种子(两者一致性由测试锁定)。 值一律以 Fraction 可解析字符串表示(如 "4/360",禁 0.011111 —— 见算法红线)。 + +`description` 会直接显示在「计提参数」页的参数名下方,**面向业务人员撰写**: +说清这个参数管什么、钱怎么变、什么时候该调它。不写英文枚举名、需求编号、 +内部阶段号,也不写实现层面的精度约束(那些留在代码注释与测试里)。 """ from dataclasses import dataclass @@ -43,7 +47,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="10", value_min="0", value_max=None, - description="线性递减段起始值:比例 = 起始 − 回款天数 × 斜率(≤分界天数)", + description="回款当天能拿到的最高计提比例。回款每晚一天,按日递减斜率往下走,直到分界天数为止。", sort_order=1, ), ParamSpec( @@ -57,7 +61,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="4/360", value_min="0", value_max=None, - description="精确分数 4/360‰/天(=1/90),禁止按 0.011111 截断存储", + description="回款每延后一天,计提比例下降多少。按分数填写(如 4/360)可保持精确,填小数会引入四舍五入误差。", sort_order=2, ), ParamSpec( @@ -71,7 +75,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="4", value_min="0", value_max=None, - description="回款天数超过分界天数后固定取此比例(≥361 天 = 4‰)", + description="回款天数超过分界天数后,计提比例不再下降,固定按这个值计算,即最低计提比例。", sort_order=3, ), ParamSpec( @@ -85,7 +89,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="360", value_min="0", value_max=None, - description="≤分界天数按公式线性递减(360 天=6‰);超过则取下限", + description="回款天数在这个范围内按日递减;超过这个天数就固定按下限计提。默认 360 天。", sort_order=4, ), ParamSpec( @@ -99,7 +103,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="12.5", value_min="0", value_max="100", - description="∑本人带领业务员绩效 × 此比例(限本团队)", + description="经理按本团队业务员的绩效总额提取的分成比例,只统计自己带的业务员。", sort_order=5, ), ParamSpec( @@ -113,7 +117,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="5", value_min="0", value_max="100", - description="∑全部业务员绩效 × 此比例(全公司)", + description="经理按全公司业务员的绩效总额提取的分成比例,与本团队分成叠加计算。", sort_order=6, ), ParamSpec( @@ -127,7 +131,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="3", value_min="0", value_max=None, - description="所有企业户适用;2026-08-13 起由 2‰ 提高至 3‰", + description="企业户客户的回款统一按这个比例计提,不看回款天数。2026-08-13 起由 2‰ 调整为 3‰。", sort_order=7, ), ParamSpec( @@ -141,7 +145,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="1", value_min="0", value_max=None, - description="信息员绩效 = 本人销售额 × 此比例", + description="信息员的绩效按本人销售额乘这个比例计算,不随回款天数递减。", sort_order=8, ), ParamSpec( @@ -155,7 +159,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="10", value_min="0", value_max=None, - description="预付款统一按此比例计提", + description="预付款到账即按这个比例计提绩效,不必等到全额回款。", sort_order=9, ), ParamSpec( @@ -169,7 +173,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="80", value_min="0", value_max="100", - description="以物抵顶回款绩效按应发绩效的此比例折算", + description="以物抵顶的回款,按应发绩效的这个比例折算,即打折后的金额参与绩效。", sort_order=10, ), ParamSpec( @@ -183,7 +187,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="60", value_min="0", value_max="100", - description="单项业务回款额达销售额(不含运费)此比例后起付;达标前累计", + description="单项业务的回款累计达到销售额(不含运费)的这个比例后才开始发放绩效;达标之前只累计、不发放。", sort_order=11, ), ParamSpec( @@ -197,7 +201,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="FULL", value_min=None, value_max=None, - description="FULL=按 R×R 乘当月全额应发绩效;BALANCE=乘余额(语义待 M3 前确认)", + description="选择支付权重乘在哪个基数上:当月全额 = 乘本月全部应发绩效;余额 = 只乘尚未发放的部分。", sort_order=12, ), ParamSpec( @@ -211,7 +215,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="30", value_min="0", value_max="100", - description="折算毛利润额 = 销售额 ×(毛利润率 − 阈值);市政水利 30%、轨交 20%", + description="折算毛利润额 = 销售额 ×(毛利润率 − 阈值),低于阈值的部分不参与绩效。市政水利默认 30%,轨道交通默认 20%。", sort_order=13, ), ParamSpec( @@ -225,7 +229,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="INCLUDE", value_min=None, value_max=None, - description="INCLUDE含基本工资 / DEDUCT扣除基本工资 / EXCLUDE不计", + description="算月工资时怎么处理基本工资:含基本工资 = 与绩效一起发;扣除基本工资 = 从绩效里扣掉已发的基本工资;不计 = 月工资只发绩效。", sort_order=14, ), ParamSpec( @@ -239,7 +243,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="18000", value_min="0", value_max=None, - description="试用期年累计绩效超过此值部分年底一次性结算", + description="试用期绩效先不发放;年内累计超过这个金额的部分,年底一次性结算。", sort_order=15, ), ParamSpec( @@ -253,11 +257,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="1", value_min=None, value_max=None, - description=( - "0=二次分配方案提交即生效(系统自动通过全节点留痕,留痕不断链);" - "1=提交后进入财务复核→领导审核审批流。" - "默认开启(澄清 #5 业务答复 2026-10-19),业务可关闭" - ), + description="二次分配方案提交后是否要审批:开启 = 先经财务复核、再经领导审核,通过后才生效;关闭 = 提交即生效,系统自动记录各节点留痕。", sort_order=17, ), ParamSpec( @@ -271,11 +271,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="1", value_min=None, value_max=None, - description=( - "月度结算审批链开关(④ 月结提交时消费):" - "0=结算生成即生效,1=走财务复核→领导审核审批流。" - "默认开启(澄清 #7 业务答复 2026-10-19,月结涉及工资发放从严),业务可关闭" - ), + description="月度结算提交后是否要审批:开启 = 先经财务复核、再经领导审核;关闭 = 生成即生效。月结涉及工资发放,默认开启。", sort_order=18, ), ParamSpec( @@ -289,11 +285,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="0", value_min=None, value_max=None, - description=( - "0=调整单提交即生效(系统自动通过留痕);1=走财务复核→领导审核审批流。" - "调整单是已锁定月份补发/扣减的唯一通道(澄清 #9:走同一审批链," - "默认值业务未明确,暂按直接生效,业务可开启)" - ), + description="调整单提交后是否要审批:开启 = 先经财务复核、再经领导审核;关闭 = 提交即生效。调整单是已锁定月份补发或扣减的唯一通道。", sort_order=19, ), ParamSpec( @@ -307,11 +299,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="0", value_min=None, value_max=None, - description=( - "经理 12.5%+5% 聚合时是否计入经理本人名下经理个人户业绩(澄清 #12 " - "业务答复 2026-10-19:作为参数配置、手动调整是否纳入):" - "0=不计入(默认,暂口径) 1=计入" - ), + description="经理提取团队分成时,是否把经理本人名下的业绩(经理个人户)也算进分成基数。默认不计入。", sort_order=20, ), # ===== 预警规则(M4 ⑧ 工作台,澄清 #10「做成参数化规则便于调整」)===== @@ -327,7 +315,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="1", value_min=None, value_max=None, - description="工作台风险预警区块总开关:0=关闭(不扫描不展示) 1=开启(默认)", + description="工作台风险预警区块的总开关:关闭后不再扫描、也不展示预警条目。默认开启。", sort_order=21, ), ParamSpec( @@ -341,7 +329,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="90", value_min="1", value_max=None, - description=("发货后超过该天数仍未回款的发货批次触发预警(澄清 #10 建议默认 90 天)"), + description="发货后超过这么多天仍未收到回款的发货批次,会出现在工作台的预警列表里。默认 90 天。", sort_order=22, ), ParamSpec( @@ -355,7 +343,7 @@ PARAM_REGISTRY: dict[str, ParamSpec] = { default_value="3", value_min="1", value_max=None, - description=("待审批单提交后超过该天数未处理触发预警(澄清 #10 建议默认 3 天)"), + description="审批单提交后超过这么多天还没处理,会出现在工作台的预警列表里。默认 3 天。", sort_order=23, ), ] diff --git a/backend/app/services/param_service.py b/backend/app/services/param_service.py index d82c3df..66fd2ae 100644 --- a/backend/app/services/param_service.py +++ b/backend/app/services/param_service.py @@ -44,7 +44,7 @@ from app.schemas.param import ( ParamPreviewOut, ParamUsedOut, ) -from app.services import audit_service, formula +from app.services import audit_service, calc_templates, formula from app.services.audit_service import AuditRecord, RequestMeta from app.services.formula import to_decimal from app.services.no_generator import next_no @@ -557,25 +557,25 @@ def _preview_rate(ctx: _PreviewContext, days: int) -> tuple[Fraction, str, Decim # 存储口径为 ‰ 数(如 "3"),除以 1000 转纯比例 —— performance_amount # 按纯比例相乘,漏换算会把绩效金额放大 1000 倍(与业务员分支同防线) rate = take("enterprise.rate") / 1000 - return rate, "企业户固定计提比例", None + return rate, "企业户:不按回款天数,统一按固定比例计提", None role_code = payload.business_role template = "TPL_SALESPERSON_DECAY" if role_code: role = business_role_repository.get_by_code(ctx.session, role_code) if role is None: - raise BadRequestError(f"岗位 {role_code!r} 不存在") + raise BadRequestError(f"岗位「{role_code}」不存在,请到岗位角色里确认") template = role.calc_template if template == "TPL_MANAGER_SHARE": raise BadRequestError( - "营销经理绩效为团队聚合口径(本团队 12.5% + 全公司 5%),不支持单笔预览" + "营销经理的绩效按团队与全公司分成计算,不单笔试算;请到月度结算里查看" ) if template == "TPL_INFO_SALES_FIXED": # ‰ 存储口径 → 纯比例,同企业户分支(不换算则绩效放大 1000 倍) rate = take("info.rate") / 1000 # 信息员的基数是本人销售额(无折算与阈值),覆盖默认基数 - return rate, "信息员:本人销售额 × 计提比例", Decimal(payload.revenue) + return rate, "信息员:按本人销售额乘计提比例,不随回款天数递减", Decimal(payload.revenue) if template == "TPL_SALESPERSON_DECAY": start = take("salesperson.rate.start") slope = take("salesperson.rate.slope") @@ -584,20 +584,27 @@ def _preview_rate(ctx: _PreviewContext, days: int) -> tuple[Fraction, str, Decim rate = formula.salesperson_rate( days, start=start, slope=slope, floor=floor, boundary_days=boundary ) + # 说明写成业务话:说清这笔为什么是这个比例、什么时候不再降 return ( rate, - f"业务员递减:{to_decimal(start)}‰ 起,{days} 天(分界 {boundary} 天,超界取 {to_decimal(floor)}‰)", + f"回款 {days} 天,从起始 {to_decimal(start)}‰ 按日递减" + f"(满 {boundary} 天后固定按 {to_decimal(floor)}‰ 计算)", None, ) - raise BadRequestError(f"公式模板 {template} 暂不支持预览") + # 提示里给模板中文名而不是编码:编码是内部标识,业务人员在岗位角色页看不到它 + tpl_name = calc_templates.TEMPLATES[template].name if template in calc_templates.TEMPLATES else template + raise BadRequestError( + f"「{tpl_name}」不支持单笔试算,请在岗位角色里改用业务员递减或信息员固定计提" + ) def _preview_rate_decision(ctx: _PreviewContext) -> _PreviewRateDecision: """产品类别校验 + 回款天数 + 计提比例判定。""" payload = ctx.payload if payload.product_category and payload.product_category not in PRODUCT_CATEGORIES: + # 提示里用中文类别名:前端下拉显示的是「市政水利」,报错却回英文编码会让人对不上 raise BadRequestError( - f"产品类别 {payload.product_category!r} 不存在,可选:municipal_water / rail_transit" + f"产品类别「{payload.product_category}」不存在,可选:市政水利、轨道交通" ) days = formula.payment_days(payload.ship_date, payload.receipt_date) rate, rate_text, base_override = _preview_rate(ctx, days) @@ -651,7 +658,10 @@ def _preview_probation_note(ctx: _PreviewContext) -> str | None: if not ctx.payload.probation: return None annual = ctx.take("probation.annual.threshold") - return f"试用期绩效照算不发放;年累计超 {to_decimal(annual)} 元部分年底一次性结算" + return ( + f"试用期人员:绩效照常计算但不发放;年内累计超过 {to_decimal(annual)} 元的部分," + "年底一次性结清。" + ) def preview(session: Session, payload: ParamPreviewIn) -> ParamPreviewOut: diff --git a/backend/app/services/settlement_service.py b/backend/app/services/settlement_service.py index 8a531ed..e356360 100644 --- a/backend/app/services/settlement_service.py +++ b/backend/app/services/settlement_service.py @@ -423,9 +423,10 @@ def _aggregate_row( "members": member_names, "params_as_of": str(share.param_date), "manager_personal_included": 1 if share.include_manager_personal else 0, + # 说明写给业务人员看:说清这一笔的基数是怎么凑出来的 "note": ( - f"#12 参数化:基数{'含' if share.include_manager_personal else '不含'}" - "经理个人户业绩;9.8 方案公式" + f"团队分成:基数{'含' if share.include_manager_personal else '不含'}" + "经理本人名下业绩,按本团队与全公司两级比例合计计提" ), }, ), diff --git a/backend/tests/integration/test_param_management.py b/backend/tests/integration/test_param_management.py index 022d4b0..f7d5070 100644 --- a/backend/tests/integration/test_param_management.py +++ b/backend/tests/integration/test_param_management.py @@ -371,7 +371,11 @@ class TestPreview: } resp = client.post(f"{_PARAMS}/preview", json=payload, headers=admin["headers"]) assert resp.status_code == 400 - assert "聚合" in resp.json()["message"] + # 守两件事:① 聚合口径不允许单笔试算(拒绝而不是算出个错数); + # ② 提示要能指导下一步(点名是谁的口径 + 去哪儿看),不是一句「不支持」。 + message = resp.json()["message"] + assert "营销经理" in message + assert "月度结算" in message def test_回款日早于发货日_400(self, client: TestClient, admin: dict) -> None: payload = { diff --git a/db/init/init-dev.sql b/db/init/init-dev.sql index ee7ca31..404f5aa 100644 --- a/db/init/init-dev.sql +++ b/db/init/init-dev.sql @@ -439,29 +439,29 @@ INSERT INTO param_change_batches (id, version_no, effective_date, change_type, r (1, 'P20260101-00', '2026-01-01', 1, '初始版本(系统初始化种子,口径依据 2026-10-12 业务裁决与 2026-10-19 澄清答复)', 'system', 19); INSERT INTO param_definitions (id, param_code, param_name, param_group, value_type, unit, dimension_type, options_text, default_value, value_min, value_max, description, sort_order) VALUES -(1, 'salesperson.rate.start', '业务员起始计提比例', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '10', '0', NULL, '线性递减段起始值:比例 = 起始 − 回款天数 × 斜率(≤分界天数)', 1), -(2, 'salesperson.rate.slope', '业务员日递减斜率', '业务员个人户', 'DECIMAL', '‰/天', 'BUSINESS_ROLE', NULL, '4/360', '0', NULL, '精确分数 4/360‰/天(=1/90),禁止按 0.011111 截断存储', 2), -(3, 'salesperson.rate.floor', '业务员计提下限', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '4', '0', NULL, '回款天数超过分界天数后固定取此比例(≥361 天 = 4‰)', 3), -(4, 'salesperson.days.boundary', '线性递减分界天数', '业务员个人户', 'INT', '天', 'BUSINESS_ROLE', NULL, '360', '0', NULL, '≤分界天数按公式线性递减(360 天=6‰);超过则取下限', 4), -(5, 'manager.share.team', '经理本团队分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '12.5', '0', '100', '∑本人带领业务员绩效 × 此比例(限本团队)', 5), -(6, 'manager.share.company', '经理全公司分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '5', '0', '100', '∑全部业务员绩效 × 此比例(全公司)', 6), -(7, 'enterprise.rate', '企业户计提比例', '企业户', 'DECIMAL', '‰', 'PRODUCT_CATEGORY', NULL, '3', '0', NULL, '所有企业户适用;2026-08-13 起由 2‰ 提高至 3‰', 7), -(8, 'info.rate', '信息员计提比例', '信息员', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '1', '0', NULL, '信息员绩效 = 本人销售额 × 此比例', 8), -(9, 'prepay.rate', '预付款计提比例', '支付环节', 'DECIMAL', '‰', 'NONE', NULL, '10', '0', NULL, '预付款统一按此比例计提', 9), -(10, 'barter.discount', '以物抵顶折算比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '80', '0', '100', '以物抵顶回款绩效按应发绩效的此比例折算', 10), -(11, 'payment.start.ratio', '支付起付比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '60', '0', '100', '单项业务回款额达销售额(不含运费)此比例后起付;达标前累计', 11), -(12, 'payment.weight.mode', '支付权重应用方式', '支付环节', 'ENUM', NULL, 'NONE', 'FULL|BALANCE', 'FULL', NULL, NULL, 'FULL=按 R×R 乘当月全额应发绩效;BALANCE=乘余额(语义待 M3 前确认)', 12), -(13, 'product.threshold', '折算毛利润额阈值', '产品阈值', 'DECIMAL', '%', 'PRODUCT_CATEGORY', NULL, '30', '0', '100', '折算毛利润额 = 销售额 ×(毛利润率 − 阈值);市政水利 30%、轨交 20%', 13), -(14, 'salary.basic.mode', '月工资基本工资口径', '月工资口径', 'ENUM', NULL, 'BUSINESS_ROLE', 'INCLUDE|DEDUCT|EXCLUDE', 'INCLUDE', NULL, NULL, 'INCLUDE含基本工资 / DEDUCT扣除基本工资 / EXCLUDE不计', 14), -(15, 'probation.annual.threshold', '试用期年累计结算阈值', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '18000', '0', NULL, '试用期年累计绩效超过此值部分年底一次性结算', 15), -(17, 'approval.secondary_allocation.enabled', '二次分配审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '1', NULL, NULL, '0=二次分配方案提交即生效(系统自动通过全节点留痕,留痕不断链);1=提交后进入财务复核→领导审核审批流。默认开启(澄清 #5 业务答复 2026-10-19),业务可关闭', 17), -(18, 'approval.monthly_settlement.enabled', '月度结算审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '1', NULL, NULL, '月度结算审批链开关(④ 月结提交时消费):0=结算生成即生效,1=走财务复核→领导审核审批流。默认开启(澄清 #7 业务答复 2026-10-19,月结涉及工资发放从严),业务可关闭', 18), -(19, 'approval.adjustment.enabled', '调整单审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '0=调整单提交即生效(系统自动通过留痕);1=走财务复核→领导审核审批流。调整单是已锁定月份补发/扣减的唯一通道(澄清 #9:走同一审批链,默认值业务未明确,暂按直接生效,业务可开启)', 19), -(20, 'manager.personal.included', '经理个人户计入聚合基数', '经理分成', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '经理 12.5%+5% 聚合时是否计入经理本人名下经理个人户业绩(澄清 #12 业务答复 2026-10-19:作为参数配置、手动调整是否纳入):0=不计入(默认,暂口径) 1=计入', 20), --- 预警规则参数(M4 ⑧ 工作台,澄清 #10「做成参数化规则便于调整」) -(21, 'warning.enabled', '风险预警总开关', '预警规则', 'BOOL', NULL, 'NONE', NULL, '1', NULL, NULL, '工作台风险预警区块总开关:0=关闭(不扫描不展示) 1=开启(默认)', 21), -(22, 'warning.receipt.overdue.days', '回款逾期天数阈值', '预警规则', 'INT', '天', 'NONE', NULL, '90', '1', NULL, '发货后超过该天数仍未回款的发货批次触发预警(澄清 #10 建议默认 90 天)', 22), -(23, 'warning.approval.timeout.days', '审批超时天数阈值', '预警规则', 'INT', '天', 'NONE', NULL, '3', '1', NULL, '待审批单提交后超过该天数未处理触发预警(澄清 #10 建议默认 3 天)', 23); +(1, 'salesperson.rate.start', '业务员起始计提比例', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '10', '0', NULL, '回款当天能拿到的最高计提比例。回款每晚一天,按日递减斜率往下走,直到分界天数为止。', 1), +(2, 'salesperson.rate.slope', '业务员日递减斜率', '业务员个人户', 'DECIMAL', '‰/天', 'BUSINESS_ROLE', NULL, '4/360', '0', NULL, '回款每延后一天,计提比例下降多少。按分数填写(如 4/360)可保持精确,填小数会引入四舍五入误差。', 2), +(3, 'salesperson.rate.floor', '业务员计提下限', '业务员个人户', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '4', '0', NULL, '回款天数超过分界天数后,计提比例不再下降,固定按这个值计算,即最低计提比例。', 3), +(4, 'salesperson.days.boundary', '线性递减分界天数', '业务员个人户', 'INT', '天', 'BUSINESS_ROLE', NULL, '360', '0', NULL, '回款天数在这个范围内按日递减;超过这个天数就固定按下限计提。默认 360 天。', 4), +(5, 'manager.share.team', '经理本团队分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '12.5', '0', '100', '经理按本团队业务员的绩效总额提取的分成比例,只统计自己带的业务员。', 5), +(6, 'manager.share.company', '经理全公司分成比例', '经理分成', 'DECIMAL', '%', 'BUSINESS_ROLE', NULL, '5', '0', '100', '经理按全公司业务员的绩效总额提取的分成比例,与本团队分成叠加计算。', 6), +(7, 'enterprise.rate', '企业户计提比例', '企业户', 'DECIMAL', '‰', 'PRODUCT_CATEGORY', NULL, '3', '0', NULL, '企业户客户的回款统一按这个比例计提,不看回款天数。2026-08-13 起由 2‰ 调整为 3‰。', 7), +(8, 'info.rate', '信息员计提比例', '信息员', 'DECIMAL', '‰', 'BUSINESS_ROLE', NULL, '1', '0', NULL, '信息员的绩效按本人销售额乘这个比例计算,不随回款天数递减。', 8), +(9, 'prepay.rate', '预付款计提比例', '支付环节', 'DECIMAL', '‰', 'NONE', NULL, '10', '0', NULL, '预付款到账即按这个比例计提绩效,不必等到全额回款。', 9), +(10, 'barter.discount', '以物抵顶折算比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '80', '0', '100', '以物抵顶的回款,按应发绩效的这个比例折算,即打折后的金额参与绩效。', 10), +(11, 'payment.start.ratio', '支付起付比例', '支付环节', 'DECIMAL', '%', 'NONE', NULL, '60', '0', '100', '单项业务的回款累计达到销售额(不含运费)的这个比例后才开始发放绩效;达标之前只累计、不发放。', 11), +(12, 'payment.weight.mode', '支付权重应用方式', '支付环节', 'ENUM', NULL, 'NONE', 'FULL|BALANCE', 'FULL', NULL, NULL, '选择支付权重乘在哪个基数上:当月全额 = 乘本月全部应发绩效;余额 = 只乘尚未发放的部分。', 12), +(13, 'product.threshold', '折算毛利润额阈值', '产品阈值', 'DECIMAL', '%', 'PRODUCT_CATEGORY', NULL, '30', '0', '100', '折算毛利润额 = 销售额 ×(毛利润率 − 阈值),低于阈值的部分不参与绩效。市政水利默认 30%,轨道交通默认 20%。', 13), +(14, 'salary.basic.mode', '月工资基本工资口径', '月工资口径', 'ENUM', NULL, 'BUSINESS_ROLE', 'INCLUDE|DEDUCT|EXCLUDE', 'INCLUDE', NULL, NULL, '算月工资时怎么处理基本工资:含基本工资 = 与绩效一起发;扣除基本工资 = 从绩效里扣掉已发的基本工资;不计 = 月工资只发绩效。', 14), +(15, 'probation.annual.threshold', '试用期年累计结算阈值', '月工资口径', 'DECIMAL', '元', 'NONE', NULL, '18000', '0', NULL, '试用期绩效先不发放;年内累计超过这个金额的部分,年底一次性结算。', 15), +(17, 'approval.secondary_allocation.enabled', '二次分配审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '1', NULL, NULL, '二次分配方案提交后是否要审批:开启 = 先经财务复核、再经领导审核,通过后才生效;关闭 = 提交即生效,系统自动记录各节点留痕。', 17), +(18, 'approval.monthly_settlement.enabled', '月度结算审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '1', NULL, NULL, '月度结算提交后是否要审批:开启 = 先经财务复核、再经领导审核;关闭 = 生成即生效。月结涉及工资发放,默认开启。', 18), +(19, 'approval.adjustment.enabled', '调整单审批开关', '审批开关', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '调整单提交后是否要审批:开启 = 先经财务复核、再经领导审核;关闭 = 提交即生效。调整单是已锁定月份补发或扣减的唯一通道。', 19), +(20, 'manager.personal.included', '经理个人户计入聚合基数', '经理分成', 'BOOL', NULL, 'NONE', NULL, '0', NULL, NULL, '经理提取团队分成时,是否把经理本人名下的业绩(经理个人户)也算进分成基数。默认不计入。', 20), +-- 预警规则参数:工作台预警阈值,可按业务需要调整 +(21, 'warning.enabled', '风险预警总开关', '预警规则', 'BOOL', NULL, 'NONE', NULL, '1', NULL, NULL, '工作台风险预警区块的总开关:关闭后不再扫描、也不展示预警条目。默认开启。', 21), +(22, 'warning.receipt.overdue.days', '回款逾期天数阈值', '预警规则', 'INT', '天', 'NONE', NULL, '90', '1', NULL, '发货后超过这么多天仍未收到回款的发货批次,会出现在工作台的预警列表里。默认 90 天。', 22), +(23, 'warning.approval.timeout.days', '审批超时天数阈值', '预警规则', 'INT', '天', 'NONE', NULL, '3', '1', NULL, '审批单提交后超过这么多天还没处理,会出现在工作台的预警列表里。默认 3 天。', 23); INSERT INTO param_values (id, param_id, dimension_value, effective_date, value_text, batch_id) VALUES (1, 1, 'salesperson', '2026-02-17', '10', 1), diff --git a/frontend/src/views/business/businesses/index.vue b/frontend/src/views/business/businesses/index.vue index ffa270c..26186b9 100644 --- a/frontend/src/views/business/businesses/index.vue +++ b/frontend/src/views/business/businesses/index.vue @@ -171,7 +171,7 @@ const allocationHint = computed(() => { const total = Number(allocationTotal.value.toFixed(2)) if (total === 100) return `二次分配合计 ${total.toFixed(2)}%` const rest = Number((100 - total).toFixed(2)) - return `二次分配合计 ${total.toFixed(2)}%,余额 ${rest.toFixed(2)}%(归属待业务确认)` + return `二次分配合计 ${total.toFixed(2)}%,余额 ${rest.toFixed(2)}%(归负责人)` }) function addAllocationRow(): void { @@ -880,7 +880,7 @@ onMounted(() => {
- 计提比例、系数、阈值、分成与起付比例在此统一维护;变更逐项留痕并支持按日期生效。 + 计提比例、系数、阈值、分成与起付比例都在这里统一维护;每次改动留下旧值,可以指定从哪天起生效。