销售促销

Odoo 优惠券为什么不是“打个折就完了”:Reward Line、Coupon 与 Loyalty Program 的真实协作机制

很多人把 Odoo 优惠券和忠诚度计划理解成销售单上自动多一行折扣,但官方源码里的实现更像一套“奖励编排引擎”:先算资格,再决定 reward,再落成 reward line,并持续回写积分与历史。本文把这套机制讲透。

销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

Odoo 的优惠券 / 忠诚度机制,不是“满足条件就多一行负数”。

它真正做的是:

先判断你有没有资格拿奖励,再判断这次能领什么 reward,然后把 reward 落成可追踪的销售行,并继续维护 coupon points、history 和取消时的回滚。

所以它的本质不是折扣显示,而是奖励状态机 + 销售行落地机制


这篇文章主要参考了哪些源码

主要看的官方实现有:

  • /home/ubuntu/odoo-temp/addons/sale_loyalty/models/sale_order.py
  • /home/ubuntu/odoo-temp/addons/sale_loyalty/models/sale_order_line.py
  • /home/ubuntu/odoo-temp/addons/sale_loyalty/models/loyalty_program.py
  • /home/ubuntu/odoo-temp/addons/sale_loyalty/wizard/sale_loyalty_reward_wizard.py
  • /home/ubuntu/odoo-temp/addons/sale_loyalty/tests/

这里最关键的源码信号是:

  • sale.order.line 上有 is_reward_linereward_idcoupon_id
  • 销售单确认时会 _update_programs_and_rewards()
  • 同时还会 _add_loyalty_history_lines()
  • 取消订单时又会回收 points、删除 reward line、清理 coupon

这说明 reward 不是“算完就消失的临时结果”,而是订单生命周期的一部分。


为什么 Odoo 要把奖励落成一条 sale.order.line

sale_loyalty/models/sale_order_line.py 里,is_reward_line 的计算非常直接:

  • 只要 reward_id 存在,这条 line 就是 reward line

这看上去很普通,但其实非常关键。

因为它意味着 Odoo 并没有把折扣或赠品只存在内存里,而是把奖励显式建模成订单行。

这样做的好处是:

  • 前台能展示
  • 报价 / 订单能打印
  • 开票逻辑能识别
  • 删除、取消、复制时能有明确边界

如果 reward 只是一个“订单总额上的隐藏修正值”,很多后续链路都会变得很模糊。

所以 Odoo 选择把奖励实体化。


reward line 为什么不等于“折扣行”

新手很容易把 reward linediscount line 画等号。

但从源码看,reward 至少可能对应:

  • 折扣
  • 赠品
  • gift card / eWallet 语义

所以 reward line 更准确的理解应该是:

它是“促销奖励在销售订单上的承载体”,折扣只是其中一种表现。

这也解释了为什么有些 reward line 看起来是负金额,有些却是赠送商品数量。

本质上它们都不是普通销售行,而是 program reward 的落地形式。


系统为什么要反复 _update_programs_and_rewards()

sale_loyalty/models/sale_order.py 里,这个方法调用出现得非常频繁:

  • 确认订单前
  • 打开奖励向导前
  • 某些流程变化后

这说明 Odoo 并不把 reward 视作“一次性算完”的结果,而是视作:

  • 订单内容变化后需要重新判资格
  • 已有 reward 可能需要移除或重算
  • 某些 coupon 可能已经变成 ghost coupon,需要清理

也就是说,促销不是静态附属物,而是会随着订单变化重新编排。

这正是很多团队在项目里最容易低估的地方:

你改了订单,不只是改金额,也可能改了促销资格本身。


为什么确认订单时还要写 loyalty history

action_confirm() 里不只是确认订单。

它还会:

  • 检查 reward 是否有效
  • _update_programs_and_rewards()
  • _add_loyalty_history_lines()
  • 最后再发送相关 coupon mail

这说明 Odoo 认为 loyalty 的关键不是“此刻订单长什么样”,而是:

  • 这次订单消耗了什么 points
  • 发出了什么奖励
  • 未来能不能追溯这次奖励从哪来

所以 loyalty history 的角色,类似一层促销审计轨迹。

没有它,你后面很难回答这些问题:

  • 这张单到底用了哪张券
  • 为什么会有这条赠品
  • 这次积分是怎么被扣掉的

为什么取消订单时要做这么多清理动作

_action_cancel() 里,系统会做一串回滚:

  • 删除相关 history
  • 把积分变化扣回去
  • 删除 reward line
  • 清理不该留下的 coupon / coupon_point

这其实很说明设计思想。

Odoo 不把促销当成“你取消单了也无所谓”的外围信息,而是把它当作订单状态的一部分。

换句话说:

如果订单没成交,这次促销消耗、赠送和积分变更,也应该尽量跟着回滚。

这才是业务一致性。


为什么全局折扣计算这么复杂

_discountable_amount()_discountable_order() 这些逻辑里,系统会细分:

  • 哪些 line 可参与折扣
  • 固定税是否应该被折
  • gift card / eWallet 是否按整单总额算
  • 税组怎么聚合

这说明 Odoo 处理的不是“总金额乘个折扣率”这么简单。

它真正要回答的是:

  • 这条奖励适用于订单级还是行级
  • 哪些税语义应该被折进去
  • 哪些金额只是支付工具,不应和普通促销混为一谈

所以 loyalty 逻辑复杂,并不是实现过度,而是因为它试图保持会计和销售语义一致。


新手最容易误解的 5 件事

1. 以为优惠券只是自动加一条负数行

实际上背后还有 reward、coupon、points、history 这些状态。

2. 以为 reward line 就等于折扣行

reward line 也可能是赠品或支付型奖励的承载体。

3. 以为促销资格算一次就结束

订单变化后,资格和 reward 需要重算。

4. 以为取消订单只要删掉折扣展示

积分、history、coupon 使用状态也要一起回滚。

5. 以为 loyalty 只影响销售前台

其实它会继续影响开票、邮件、历史追踪和后续审计。


实战里该怎么理解这套机制

我会把它拆成四层:

第一层:资格判断

客户、规则、金额、产品范围,决定你有没有资格。

第二层:奖励选择

有资格之后,决定本次能领哪种 reward。

第三层:订单落地

把 reward 落成 sale.order.line,让后续链路都认得它。

第四层:历史与回滚

确认时写 history,取消时做回滚,保证积分和奖励不漂移。

如果你按这四层理解,就不会再把 loyalty 看成“页面上一点点折扣魔法”。


一句话记忆法

Odoo Loyalty 不是自动打折,而是在先判资格、再发奖励、再落订单行、最后维护历史与回滚。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。