先说结论
Odoo 里的 coupon / promotion / loyalty 虽然听起来像一套统一能力,但落到销售订单、网站结算页、POS 收银台时,边界并不一样。
从模块依赖就能看出来:
sale_loyalty依赖sale+loyaltywebsite_sale_loyalty依赖website_sale+sale_loyaltypos_loyalty依赖loyalty+point_of_sale
这说明:
销售订单不是网站优惠功能的后台入口,也不是 POS 优惠功能的复制品;它是 loyalty 能力在“人工销售报价/订单”场景下的一条独立落地链路。
所以实施里最常见的错误,就是把网站或 POS 的思维原封不动搬来套 Sales Order。
销售订单上的 loyalty,是“销售员可控”的,不是“顾客自助触发”的
sale_loyalty/views/sale_order_views.xml 里,订单表单明确加了两个按钮:
Coupon CodeReward
这已经说明销售订单的使用语义:
- 由销售员主动输入 coupon code
- 由销售员主动打开 reward wizard
- 在订单级别确认是否应用奖励线
这和网站/POS 的前台体验明显不同。
网站/POS 更像即时结算交互
用户自己输入 code,系统立刻重算购物车或小票。
Sales Order 更像销售谈判工具
销售员需要根据报价、谈判、审批、客户关系来决定:
- 这张订单要不要用券
- 奖励是立刻加,还是先谈好再确认
- 赠品、折扣、积分发放是否要在 order confirm 时结算
所以 sale order loyalty 的第一层边界就是:
它首先服务的是销售员决策,而不是终端顾客的自助交互。
核心结构不是“订单打了个折扣”,而是多组 loyalty 对象协作
sale_loyalty/models/sale_order.py 上挂了多组字段:
applied_coupon_idscode_enabled_rule_idscoupon_point_idsreward_amountloyalty_data
这说明订单上的 loyalty 不是一个简单的布尔开关,而是至少拆成了几层:
1. 手工应用了哪些卡券
applied_coupon_ids
2. 哪些规则是通过 code 触发的
code_enabled_rule_ids
3. 本单会给哪些 coupon/card 记多少 points
coupon_point_ids
4. 当前奖励线合计影响多少金额
reward_amount
5. 已确认后,这张单累计发出多少积分、消耗多少成本
loyalty_data
所以你不能把它理解成“sale order 上多了一个 coupon 字段”。
真正发生的是:
订单成了 loyalty 资格判定、奖励线挂载、积分结算与历史沉淀的协作容器。
销售订单上的奖励线,不等于网站购物车上的前端优惠表现
sale_loyalty 里,reward line 是真实订单行的一部分。
源码里可以看到:
order_line上会出现reward_id- 还会带
coupon_id、points_cost - 若是产品奖励,甚至会生成
discount=100、price_unit=0的免费商品行 - 视图层会把
is_reward_line设成只读保护,防止销售员随手改坏
这说明 Sales Order 里的 reward,不是纯视觉折扣标签,而是正式 order line 建模。
这和网站的“购物车立即重算”或者 POS 的“扫码后立刻显示优惠”相比,更强调:
- 奖励线是销售单结构的一部分
- 要参与后续确认、取消、积分历史、邮件通知
所以如果你用前台结算思维去理解销售订单 reward,就会低估它的后端语义。
确认订单时,sale_loyalty 真正关心的是“积分结算一致性”
action_confirm() 的覆写是这条链路最值钱的地方。
确认前,系统会:
- 检查 coupon 的 real points 是否出现负数
order._update_programs_and_rewards()order._add_loyalty_history_lines()
确认后,还会:
- 清理当前 program 中“没真正领取奖励的 ghost coupon”
- 对 coupon points 做增减
- 如果有可领取但没加到订单的奖励,给出 notification
- 最后
_send_reward_coupon_mail()发送奖励卡券沟通
这表明 Sales Order 场景下的 loyalty,并不只是“成交时少收一点钱”。
它还在做:
- 奖励资格校验
- 积分发行/消耗记账
- 幽灵 coupon 清理
- 奖励卡券邮件发送
这是一个明显偏后台、偏业务一致性的链路,而不是前台即时结算链路。
为什么取消订单时也要回滚 loyalty 历史
_action_cancel() 会额外处理:
- 删除此前生成的
loyalty.history - 回滚 coupon points 变化
- 删除 reward lines
- 清理某些未使用的 coupon/card
- 清空
coupon_point_ids
这一步很关键,因为它说明 sale order loyalty 不是一次性前台动作。
它的正确语义是:
订单确认时把 loyalty 结果正式入账;订单取消时把这套结果系统性回滚。
而网站/POS 场景里,很多团队更容易把优惠理解成“结账时发生的一次 UI 操作”。
在销售订单里,这显然不够。
为什么不能把网站/POS 规则直接照搬到销售订单
从模块边界看就很清楚:
website_sale_loyalty
强调的是: - 电商结算页 - promo link / coupon share - shopper 自助使用 - 与网站商品页、购物车、landing page 协作
pos_loyalty
强调的是: - 门店收银 - 扫码/条码 - 小票现场核销 - 高即时性、高交互频率
sale_loyalty
强调的是: - 报价/订单阶段的销售人工决策 - 奖励线可审阅、可谈判、可确认 - 确认与取消时的积分/历史一致性
所以如果你把 POS 的“收银员扫一下码立刻抵扣”的心智,或者网站的“顾客自助输优惠码”的心智,直接套到销售单,就会在这些地方出问题:
- 权限边界错位
- 审批流程缺失
- 奖励线被手工改坏
- 订单取消后积分没回滚干净
实战里最值得记住的 4 条
1. 销售订单上的 coupon 是销售动作,不是顾客动作
要先设计销售员何时可输入 code、何时可领 reward。
2. 奖励不是只改总金额,而是正式 reward line
后续开票、确认、取消、审计都可能依赖这些结构化订单行。
3. 确认和取消必须成对看
只懂确认不懂取消,就很容易让 loyalty history 和 coupon points 失真。
4. 网站/POS 可以共用 loyalty 基础能力,但业务节奏不同
共用的是 program/rule/reward/card 基础层,不共用的是交互入口和订单控制方式。
一句话总结
Odoo 把 loyalty 能力拆成了多个销售渠道上的不同落地模块。
在 Sales Order 里,它真正做的是:
- 让销售员手工触发 coupon / reward
- 把奖励建模成正式 reward line
- 在确认时结算积分和历史
- 在取消时回滚这些结果
所以 sales order loyalty 的本质不是“网站优惠搬到后台”,而是:
同一套 loyalty 基础能力,在人工销售订单场景中的一条受控、可审计、可回滚的应用链路。
DISCUSSION
评论区