包邮奖励

Odoo 包邮为什么不是把运费改成 0:免费配送奖励行、门槛排除与税映射边界讲透

很多人把包邮理解成“把 delivery line 价格改成 0”;但标准 Odoo 在 sale_loyalty_delivery 里真正做的是保留原始运费行,再新增一条 shipping reward 负行,并把 delivery line 排除在积分门槛与积分累计之外,还限制同一时间只能有一个 shipping reward。本文把这套边界设计讲透。

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

先抓主线

在很多团队的直觉里,“包邮”最简单的做法就是:

  • 找到 delivery line
  • 把价格改成 0
  • 结束

但标准 Odoo 没这么做。

sale_loyalty_delivery 的核心思路是:

  1. 保留原始 delivery line
  2. 再生成一条 reward_type == 'shipping' 的负向奖励行
  3. 这条奖励行的金额通常等于运费,必要时受 discount_max_amount 限制
  4. 同时把 delivery line 从积分门槛和积分累计逻辑里排除
  5. 并限制同一时刻只能存在一个 shipping reward

所以标准方案不是“改掉运费”,而是:

保留运费事实,再用奖励语义去抵消它。

这套设计对税、审计、促销门槛、积分累计都更稳。


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

核心参考文件是:

  • /home/ubuntu/odoo-temp/addons/sale_loyalty_delivery/models/sale_order.py

最关键的方法包括:

  • _compute_amount_total_without_delivery()
  • _get_no_effect_on_threshold_lines()
  • _get_not_rewarded_order_lines()
  • _get_reward_values_free_shipping()
  • _get_reward_line_values()
  • _get_claimable_rewards()

文件不大,但每个 override 都非常说明标准 Odoo 对“包邮”边界的态度。


第一层:为什么标准不直接把运费改零

因为 delivery line 本身是一个业务事实:

  • 选了哪个承运逻辑
  • 原始运费是多少
  • 运费适用什么税
  • 订单小计里原本发生了什么

如果你直接把运费改成 0,这些语义会被抹平。

标准方案保留 delivery line,再新增负向 reward line,意味着你在订单上能同时看到:

  • 原本运费是多少
  • 因为什么促销/积分被抵掉了多少

这在业务解释上明显更强,也更接近“补贴”而不是“历史上从来没产生过运费”。


第二层:包邮奖励行到底怎么构造

_get_reward_values_free_shipping() 是这篇文章的核心。

标准逻辑会:

  1. 找当前订单第一条 delivery line
  2. 取 delivery 产品税并按公司过滤
  3. 再通过 fiscal_position_id.map_tax() 做税映射
  4. 计算 max_discount
  5. 生成一条新的 reward line,其 price_unit 为运费的负数,且不超过上限

这条 reward line 里通常会写入:

  • name = Free Shipping - xxx
  • reward_id
  • coupon_id
  • points_cost
  • product_id = reward.discount_line_product_id
  • price_unit = -min(max_discount, delivery_line.price_unit or 0)
  • product_uom_qty = 1
  • is_reward_line = True
  • tax_ids 为映射后的税

这说明标准 Odoo 对包邮的理解非常明确:

  • 不是改原运费行
  • 而是新增一条“负向运费补贴行”

第三层:为什么包邮奖励也要做税映射

这正是很多自定义最容易做错的地方。

标准逻辑不会偷懒沿用“零税”或“随便复制一下税”,而是:

  • 先拿 delivery 产品税
  • 再按公司过滤
  • 再过 fiscal position

原因很简单:

包邮奖励抵消的是运费这个商业事实,所以税语义也应该沿着运费上下文走。

如果你直接做一条无税负行,或者用错税,最后订单税额、开票口径、跨国销售税位都可能变形。

所以 reward line 不是只关心金额,还关心“你在抵消什么税语义下的运费”。


第四层:为什么 delivery line 会被排除出门槛和积分累计

标准还有两个配套 override:

1)_get_no_effect_on_threshold_lines()

它会把:

  • delivery line
  • reward_type == 'shipping' 的奖励行

都加入“不影响门槛”的集合。

2)_get_not_rewarded_order_lines()

它会把 delivery line 从可累计奖励积分的订单行里排除。

这两步特别重要。

因为如果不排除,你会得到一堆很怪的促销结果:

  • 运费也被算进“满 100 包邮”的门槛
  • 包邮本身又反过来产生积分
  • 奖励叠奖励,逻辑递归失真

标准 Odoo 在这里的态度很明确:

  • delivery 是可被补贴的对象
  • 但它不应该成为进一步薅奖励的基础

第五层:为什么同一时间只允许一个 shipping reward

_get_claimable_rewards() 会做一道额外过滤:

  • 如果订单上已经有 reward_type == 'shipping' 的奖励行
  • 那么新的 claimable rewards 里,shipping 奖励会被过滤掉

源码注释也很直白:

  • Allow only one reward of type shipping at the same time

这背后的业务原因很简单:

  • 运费本身通常只有一条主对象
  • 允许多个 shipping reward 同时叠加,很容易出现负运费、重复抵扣或规则冲突

所以标准不是“能领几个券就领几个”,而是把 shipping reward 当成一个互斥型优惠位。


第六层:为什么 _compute_amount_total_without_delivery() 还要特殊排除钱包/礼卡

这个方法表面看和包邮有点远,其实也在维护边界。

它在父类基础上,进一步扣掉:

  • coupon.program_type in ['ewallet', 'gift_card']

对应订单行的金额。

这说明标准在处理“总额里哪些属于 delivery、哪些属于奖励抵扣、哪些属于钱包/礼卡抵扣”时,是逐层拆口径的。

也就是说,包邮从来不是单点 hack,而是嵌在整套 loyalty / delivery / reward 的金额口径体系里。


新手最容易误解的 5 件事

1. 以为包邮就是把 delivery price 改成 0

标准不是这么做的,而是保留运费事实,再新增负向奖励行。

2. 以为包邮奖励不需要税

不是。标准会沿运费税语义做公司过滤和 fiscal position 映射。

3. 以为运费也应该参与满减门槛

标准明确把 delivery line 排除出 threshold 计算。

4. 以为包邮奖励还能继续帮你累计积分

标准也排除了 delivery line 的积分累计资格。

5. 以为多个包邮券可以同时叠加

标准只允许一个 shipping reward 同时生效。


实战调试顺序

如果你排查“为什么包邮后税额不对”,建议看:

  1. delivery line 的产品税是什么
  2. reward line 用的是不是映射后的税
  3. fiscal position 有没有变更税
  4. 自定义代码有没有直接写 tax_ids = False

如果你排查“为什么包邮门槛怪怪的”,重点看:

  1. delivery line 是否被纳入 threshold 计算
  2. reward line 是否被误算进门槛
  3. 是否有钱包/礼卡类 coupon 同时生效
  4. 是否已存在一个 shipping reward 导致后续奖励被过滤

一句话记忆法

Odoo 的包邮不是“把运费清零”,而是保留 delivery line 这条运费事实,再用一条带正确税语义的 shipping reward 负行去抵消它,同时把运费排除出门槛和积分累计。

DISCUSSION

评论区

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