销售单优惠边界

Odoo 优惠券为什么不能把网站/POS 经验直接搬到销售单:coupon、promotion、loyalty 在 Sales Order 的应用边界讲透

很多人一说 Odoo 优惠券,就先想到电商结算页或 POS 扫码。但从官方模块依赖和 sale_loyalty 源码看,销售订单上的 loyalty 应用边界非常独立:它有自己的手动领券、奖励线、确认时积分结算和取消回滚逻辑。本文专门讲清 sales order 场景与网站/POS 的边界。

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

先说结论

Odoo 里的 coupon / promotion / loyalty 虽然听起来像一套统一能力,但落到销售订单、网站结算页、POS 收银台时,边界并不一样

从模块依赖就能看出来:

  • sale_loyalty 依赖 sale + loyalty
  • website_sale_loyalty 依赖 website_sale + sale_loyalty
  • pos_loyalty 依赖 loyalty + point_of_sale

这说明:

销售订单不是网站优惠功能的后台入口,也不是 POS 优惠功能的复制品;它是 loyalty 能力在“人工销售报价/订单”场景下的一条独立落地链路。

所以实施里最常见的错误,就是把网站或 POS 的思维原封不动搬来套 Sales Order。


销售订单上的 loyalty,是“销售员可控”的,不是“顾客自助触发”的

sale_loyalty/views/sale_order_views.xml 里,订单表单明确加了两个按钮:

  • Coupon Code
  • Reward

这已经说明销售订单的使用语义:

  • 由销售员主动输入 coupon code
  • 由销售员主动打开 reward wizard
  • 在订单级别确认是否应用奖励线

这和网站/POS 的前台体验明显不同。

网站/POS 更像即时结算交互

用户自己输入 code,系统立刻重算购物车或小票。

Sales Order 更像销售谈判工具

销售员需要根据报价、谈判、审批、客户关系来决定:

  • 这张订单要不要用券
  • 奖励是立刻加,还是先谈好再确认
  • 赠品、折扣、积分发放是否要在 order confirm 时结算

所以 sale order loyalty 的第一层边界就是:

它首先服务的是销售员决策,而不是终端顾客的自助交互。


核心结构不是“订单打了个折扣”,而是多组 loyalty 对象协作

sale_loyalty/models/sale_order.py 上挂了多组字段:

  • applied_coupon_ids
  • code_enabled_rule_ids
  • coupon_point_ids
  • reward_amount
  • loyalty_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_idpoints_cost
  • 若是产品奖励,甚至会生成 discount=100price_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

评论区

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