跨店扣佣 · 合伙人分账
> 【使用前必读】请先在 SkillHub 下载并安装本技能,装完才能在对话里触发:
> 搜索「跨店扣佣」点「安装」,或执行 skillhub install partner-commission-deduct
任意门店产生的合伙人分佣,都能指定由另一家门店的应补款里扣,且全月扣走的钱和产生的钱一分不差。
适用什么场景
- 招商合伙 / 加盟模式下,门店要按月核算合伙人分佣
- 合伙人在某店只是挂名,钱实际由他自己的直营店承担
- 总部与合伙人约定「某几家店的分佣合并算到旗舰店」
- 新店还没起量,分佣先由老店垫
- 分佣功能已上线,但账面总额和明细对不上
解决什么问题
- 总额和明细对不上:某家店代扣了他店的分佣,全月扣走的钱和产生的钱不等——缺守恒断言,差额可能永远查不出来
- 分佣凭空消失:指定的扣减门店这个月没参与结算,这笔钱既没扣原店也没扣目标店
- 门店看不懂账单:被转扣的门店看到自己没扣分佣,以为是漏算——账单文字没说明这笔钱去哪了
- 改完不敢上线:几十家店的数字只能靠肉眼核对
优势点
| 对比项 | 把分佣写死在产生门店 | 用本方法 |
|---|---|---|
| 分佣归属 | 只能在产生门店扣 | 产生侧 / 扣减侧两个独立维度 |
| 总额校验 | 靠肉眼对 | 守恒断言,写进回归脚本 |
| 目标配错 | 分佣丢失或算错 | 回退自身 + 分级告警,不丢钱 |
| 门店账单 | 看不出钱去哪了 | 三种措辞自动说明「谁扣的、扣到哪」 |
| 上线验证 | 手工核对 | 一键跑完校验 |
独有的一条:把「指定扣到别人」和「代扣别人」明确拆开——B 指定扣到 A,不等于 A 自己产生的分佣也归 A。这两个概念混在一起,代码必然写乱。
怎么调用 / 触发
第一步:先拿到它
在 SkillHub 搜「跨店扣佣」,点「安装」;或命令行:
skillhub install partner-commission-deduct
装完常驻在你的技能目录,以后在对话里说话就能触发,不用每次重装。
第二步:在对话里直接说
- 「合伙人分佣能不能从别的店扣」
- 「A 店的分佣扣到 B 店头上怎么算」
- 「分佣总额和明细对不上」
- 「跨门店扣款做不了」
AI 会加载本技能,依次给出:数据模型 5 个字段 → 目标解析三步走代码 → 守恒断言 → 对账文案模板 → 可直接运行的校验脚本。
第三步:装了和没装差在哪
不装,AI 也能算个分佣,但「B 指定扣到 A 头上」和「A 自己产生的分佣」这两个概念它很容易揉成一个——一揉就出问题:总额和明细对不上,而这类错通常要到月底对账才浮出来,那时钱已经结完了。
装了,直接拿走把这两个概念拆开的数据模型、目标解析代码,以及一条守恒断言(总额必须等于明细之和),发版前跑一遍就能拦住。
边界
- 只提供工程实现方法(逻辑怎么写对、怎么验对),不含财务口径(分佣比例、封顶规则由你的业务定)
- 前提是「本月参与结算的门店」是一个确定集合(有数据源记录)
- 校验脚本在临时沙盒构造数据,不碰真实数据;接自家系统需替换数据结构
- 不处理分佣的税务与开票
实施细节(给 AI 与开发者)
这个功能在解决什么业务问题
招商合伙模式下,每家门店按月产生「合伙人分佣」(合伙人对门店抽点的分成)。默认这笔钱从产生它的门店应补款里扣。但真实业务经常要求挪到别的门店头上:
- 合伙人在某店只是挂名,钱实际由他自己的直营店承担
- 总部与合伙人约定「某几家店的分佣合并算到旗舰店」
- 新店没起量,分佣先由老店垫
一句话:分佣在哪产生(产生侧)和从谁头上扣(扣减侧)是两个独立维度。系统必须同时建模这两个维度,而不是把「分佣」写死在产生门店的公式里。
数据模型:账单快照上要存的 5 个字段
结算完成后每家门店的账单记录(bill)上必须固化这些字段,展示层、账单文字、Excel 导出全部直接读,不再自行推导:
b["commission"] # 本店产生的分佣金额(基数见下节)
b["commission_deduct_target"] # 实际扣减目标门店名(解析后)
b["commission_deduct_self"] # 从本店扣的部分(目标=自己时 = commission,否则 0)
b["commission_deduct_from_others"] # 本店代扣的他店分佣合计
b["commission_deducted_total"] # 本店实际扣减合计 = self + from_others
b["commission_deducted_elsewhere"] # 本店分佣是否被转扣走了(用于「转扣」角标)
为什么快照要这么细:前端「分佣扣减」一列要显示 -123.00,鼠标悬停要说明「本店分佣 100 + 他店指定代扣 23」,被转扣的店要显示「转扣」角标。如果只存一个总数,这些细节全部做不出来,用户就会来问「这 123 是怎么来的」。
分佣基数:必须用「封顶后」的抽点
分佣普遍按「抽点金额 × 分佣比例」计算。抽点有封顶时,分佣基数必须是封顶后的值:
kickback = min(raw_kickback, cap) if cap > 0 else raw_kickback # 先封顶
commission = round(kickback * commission_ratio, 2) # 再算分佣
顺序反了(先算分佣再封顶抽点)会出现「门店抽点被封了顶、分佣却照全额抽」的矛盾账。任何「按另一个费用的百分比计算的派生费用」,基数都要用该费用参与最终公式时的最终形态。
目标解析三步走(核心逻辑)
每家门店解析自己的扣减目标,任何异常都回退自身并告警,绝不丢扣减:
names = {b["store"] for b in bills} # 本月参与结算的门店
target = {}
for b in bills:
s = stores.get(b["store"]) or {}
tgt = str(s.get("commission_deduct_store") or "").strip()
if not tgt or tgt == b["store"]:
tgt = b["store"] # 未指定 / 指定自己 → 扣自己
elif tgt not in names:
if tgt in stores:
# 指定的门店存在,但本月数据源没它的记录(未参与结算)
warnings.append("门店「%s」指定的分佣扣减门店「%s」本月数据源中没有记录"
"(未参与结算),分佣已改回本店扣减" % (b["store"], tgt))
else:
# 指定的门店压根不在基础信息里(多半是写错字)
warnings.append("门店「%s」指定的分佣扣减门店「%s」不在基础信息中,"
"分佣已改回本店扣减" % (b["store"], tgt))
tgt = b["store"]
target[b["store"]] = tgt
两种无效场景要分开提示:目标店没参与本月结算(可能只是这个月没单)和目标店不存在(配置写错了),用户排查方向完全不同。混成一句「目标无效」用户会抓瞎。
为什么回退而不是报错中止:一次结算涉及几十家店,一家配置错了就全月结不了,代价太大。回退 + 显著告警是更合理的失败模式——但要保证告警真的能被用户看到(见下文)。
未设合伙人的门店,分佣清零必须发生在汇总之前。写校验脚本时实测抓到:若先跑「代扣他方」汇总、再清零未设合伙人的分佣,这笔钱会照常流进目标门店的「代扣他方」里——门店没合伙人,钱却扣出去了。正确顺序:
# 先清零,再进汇总
for b in bills:
if not (b.get("partner") or "").strip():
warnings.append("门店「%s」未设置招商合伙人,分佣记为 0" % b["store"])
b["commission"] = 0.0
# ……然后才是目标解析、from_others 汇总
汇总与入公式
# 1. 按目标累计「代扣他方」
from_others = {}
for b in bills:
tgt = target[b["store"]]
if tgt != b["store"]:
from_others[tgt] = from_others.get(tgt, 0.0) + b["commission"]
# 2. 逐店计算并写入公式
for b in bills:
st, tgt = b["store"], target[b["store"]]
self_deduct = b["commission"] if tgt == st else 0.0
others = round(from_others.get(st, 0.0), 2)
deducted_total = round(self_deduct + others, 2)
b["commission_deduct_target"] = tgt
b["commission_deduct_self"] = round(self_deduct, 2)
b["commission_deduct_from_others"] = others
b["commission_deducted_total"] = deducted_total
b["commission_deducted_elsewhere"] = (tgt != st)
# 应补公式用的是 deducted_total,不是原始 commission!
b["shortfall"] = round(b["kickback"] + b["purchase_fee"] + b["unattended_fee"]
- b["deducted_amount"] - deducted_total
- b["brand_subsidy"] - b["special_subsidy"], 2)
if tgt != st:
warnings.append("门店「%s」的合伙人「%s」分佣 %.2f 元按设置改由「%s」扣减"
% (st, b["partner"] or "-", b["commission"], tgt))
守恒断言:这套逻辑的生死线
所有门店实际扣掉的分佣总额,必须等于所有门店产生的分佣总额。 一分不能多,一分不能少:
generated = sum(b["commission"] for b in bills)
deducted = sum(b["commission_deducted_total"] for b in bills)
assert abs(generated - deducted) < 0.005, "分佣守恒被破坏"
把它写进回归脚本(本 skill 附带的 verify_commission.py 第一个断言就是它)。守恒被破坏的场景几乎都来自:回退分支漏算、四舍五入不一致(有的地方 round 有的不 round)、公式里用了 commission 而不是 deducted_total。
最容易混的概念:「指定扣到别人」≠「代扣别人」
| 说法 | 含义 | 账面表现 |
|---|---|---|
| A 指定扣 B | A 产生的分佣,改由 B 的应补里扣 | A 的自扣 = 0,B 的「代扣他方」+A 的份额 |
| B 代扣他方 | B 的应补里扣着别家店转来的分佣 | B 的扣减明细里能列出「谁家、多少」 |
B 自己门店产生的分佣,仍然扣 B 自己——指定关系只改变「产生侧」的流向,不影响 B 自身分佣的归属。把这两层分开命名(_self / _from_others),代码就不会写乱。
对账说明文字的三种措辞
账单文字直接发给门店老板,三种情况都要用对方看得懂的话讲清楚:
if b["commission_deduct_self"] > 0:
reason += ";已扣减本店招商合伙人「%s」分佣 %.2f 元" % (partner, self_amt)
if b["commission_deduct_from_others"] > 0:
srcs = "、".join("%s(%s %.2f 元)" % (x["store"], x["partner"], x["commission"])
for x in bills if x["store"] != st
and x["commission_deduct_target"] == st)
reason += ";另按设置代扣他店分佣 %.2f 元:%s" % (from_others_amt, srcs)
if b["commission_deducted_elsewhere"]:
reason += ";本店合伙人「%s」分佣 %.2f 元已按设置改由「%s」扣减" % (
partner, b["commission"], b["commission_deduct_target"])
最后一种最容易被漏:被转扣的门店自己也是账单接收方,它看到自己没被扣分佣会以为漏算了——必须主动告诉它「这笔钱去哪了」。这句话是唯一一处说明「为什么我的账和别人不一样」的地方。
合伙人汇总表
按合伙人聚合,给出他名下所有店的分佣去向:
partners = {}
for b in bills:
if not b["partner"]:
continue
p = partners.setdefault(b["partner"], {"store_count": 0, "commission": 0.0,
"deduct_targets": {}})
p["store_count"] += 1
p["commission"] += b["commission"]
tgt = b["commission_deduct_target"]
p["deduct_targets"][tgt] = p["deduct_targets"].get(tgt, 0.0) + b["commission"]
if tgt != b["store"]:
p.setdefault("designated", []).append("%s→%s" % (b["store"], tgt))
导出列:合伙人 / 负责门店数 / 抽点金额合计 / 分佣比例 / 分佣金额 / 分佣扣减门店。最后一列渲染成 碑林店(500.00 元)、未央店(300.00 元),合伙人一眼看到钱从哪些店扣。
配置与告警的配套要求
- 配置入口:门店配置里加下拉「分佣扣减门店」(选项含「本店」+ 所有其他门店),留空 = 扣本店
- 未设合伙人:该店分佣记 0,给 warning 不报错(挂名店合法存在)
- 合伙人不在合伙人列表:同样 warning(多半是改名后没同步)
- 告警必须回传前端展示——后端 warnings 列表随结算结果一起返回,前端渲染成醒目列表。静默吞掉 warnings 等于没做回退机制,用户永远不会知道分佣去了哪
回归校验脚本
scripts/verify_commission.py 在临时沙盒里构造数据跑断言,不碰真实数据:
| 用例组 | 验什么 |
|---|---|
| 守恒 | 产生总额 == 扣减总额(最核心) |
| 默认自身 | 未指定 → 自扣 = commission |
| 指定转扣 | A→B 后 A 自扣 0、B 代扣增加 |
| 混合场景 | B 既被转扣又自己有分佣,两项并存 |
| 目标未参与结算 | 回退自身 + warning |
| 目标不存在 | 回退自身 + 另一种 warning |
| 应补公式 | shortfall 用 deducted_total |
| 未设合伙人 | 分佣 0 + warning,不崩 |
| 基数封顶 | 抽点封顶后分佣跟着变小 |
| 汇总表 | deduct_targets 按(目标,金额)聚合正确 |
运行:python scripts/verify_commission.py。改任何分佣逻辑后先跑它再交付。
开发自查清单
- [ ] 分佣基数 = 封顶后抽点,不是原始抽点
- [ ] 应补公式用
commission_deducted_total,不是commission - [ ] 目标无效两种场景分开告警,且回退自身
- [ ] 守恒断言写进回归脚本
- [ ] 被转扣门店的账单文字里说明去向
- [ ] warnings 随结算结果回传前端展示
- [ ] 合伙人汇总表能看出「A→B」指定关系
- [ ] 加了任何新费用字段,对照「一处改动六处联动」清单(存取/公式/UI/导出/账单文字/测试)