先说结论
很多人理解网站优惠活动时,会把它简化成一句话:
- 用户输入优惠码;
- 系统打个折;
- 完成。
但在 Odoo 的 website_sale_loyalty 里,真正的主链路要复杂得多。
它至少同时处理这几件事:
- 优惠码是不是当前网站可用;
- 现在有没有购物车,没购物车时先把 code 暂存起来;
- 这个 code 是直接产生唯一奖励,还是要用户自己选 reward;
- 奖励是不是能自动领取;
- 奖励应用后,配送费要不要重算;
- 用户删行、改数量、换配送方式后,奖励是否仍然成立。
所以 Odoo 电商的优惠逻辑,本质上不是“优惠码输入框”,而是:
围绕购物车生命周期不断重算 programs 与 rewards 的一套引擎。
一、为什么 /coupon/<code> 这种链接很重要
在 website_sale_loyalty/controllers/main.py 里,Odoo 提供了:
/coupon/<string:code>
这类链接不是简单跳转,而是把 code 写进 request.session['pending_coupon_code']。
这个设计很聪明。
因为现实里的营销入口,很多时候不是用户自己手输,而是:
- EDM 邮件里的跳转链接;
- 活动页面按钮;
- 社媒海报上的专属 URL;
- 登录前就能访问的推广页。
此时系统还不一定有购物车。
如果要求“必须先建 cart 才能识别优惠码”,体验就会很糟。
所以 Odoo 的做法是:
- 先记住这个 code,等 cart 出现再尝试应用。
这就是 pending_coupon_code 的意义。
二、为什么 code 不一定立刻生效
模型里 _try_pending_coupon() 会在后续时机读取 session 里的待应用 code,再调用 _try_apply_code()。
这里最关键的不是“能不能输进去”,而是:
- 当前购物车是否满足活动条件;
- 当前用户身份是否允许该 program;
- 当前网站是否匹配该 loyalty program;
- 奖励是否已过期、是否还可领取。
也就是说,优惠码并不是被“保存进订单”就算成功,而是每次都要过资格判断。
这跟很多轻量电商不同。
Odoo 没把优惠码视为一个静态标签,而是把它放在了动态规则引擎里。
三、为什么有些奖励会自动领,有些必须用户手动选
_auto_apply_rewards() 里能看出 Odoo 非常克制。
它只会在比较明确、安全的场景自动应用奖励,比如:
- 这个 program 只有一个 reward;
- 不是 nominative program;
- 不是多商品 reward;
- 该 reward 没被用户主动禁用;
- 当前订单里还没应用过。
换句话说,Odoo 的自动领券策略不是“能自动就自动”,而是:
只有在系统几乎不会误判用户意图时,才自动代劳。
这也是为什么有些活动你会看到自动折扣,有些却需要用户点“领取奖励”。
不是体验不统一,而是不同奖励的歧义程度不一样。
四、为什么 claim reward 不等于简单新增一条折扣行
在 controller 里,claim_reward() 和 _apply_reward() 最终会触发订单侧 _apply_program_reward(),随后再调用 _update_programs_and_rewards()。
这说明应用奖励不是一次性落库动作,而是一次重新结算购物车的起点。
原因很简单:
- 一个 reward 进来,可能影响其他 reward 的可用性;
- 全局折扣可能只能保留最优一个;
- 金额归零后,某些折扣反而不该继续出现;
- 某些 program 需要基于最新订单金额再判断;
- 配送优惠还会改变运费行。
所以奖励应用后的关键动作不是“加一条 line”,而是“重新验证整套促销状态”。
五、为什么配送费会在优惠应用后再次重算
_apply_reward() 里有个很容易被忽略的逻辑:
- 如果当前 carrier 有
free_over规则,且这次 reward 不是 payment program,Odoo 会重新调用rate_shipment(),成功就重设 delivery line,失败则移除配送行。
这一步非常像真实电商。
因为很多优惠活动会让订单金额跨过某个门槛:
- 原本满额包邮,打折后不满;
- 原本不包邮,凑单或奖励后又满足;
- 不同奖励对运费基数的影响也不一样。
如果系统应用优惠后不重算运费,用户看到的总价就可能是错的。
所以 Odoo 在这里坚持“促销和配送必须联动”。
六、为什么删购物车行、改数量、换配送方式,奖励也会跟着变
在 sale_order.py 里,website_sale_loyalty 接管了多个关键节点:
_update_programs_and_rewards()_verify_cart_after_update()_set_delivery_method()_remove_delivery_line()_recompute_cart()
也就是说,购物车只要发生商业意义上的变化:
- 商品数量变化;
- 行被删掉;
- 配送方式变化;
- 运费行变化;
Odoo 都会把 program 和 reward 重新刷新一遍。
这点是网站促销是否靠谱的分水岭。
如果只是“输入 code 时算一次”,后面购物车再变化,优惠状态就很容易失真。
Odoo 选择的是更稳妥但更复杂的路:
- 把优惠引擎嵌进购物车生命周期。
七、为什么前台看到的是“合并后的折扣行”
源码里 _compute_website_order_line() 会把同一 reward 产生的多条 discount line 在网站上“视觉合并”。
为什么要这样?
因为后台为了税务和金额精确,可能需要按不同税率拆成多条奖励行。
但前台购物车如果把这些技术细节全展示出来,用户会非常困惑。
于是 Odoo 采取了一个典型的“后台精确、前台简化”策略:
- 后端保留真实计算结构;
- 网站展示层把同一 reward 汇总成一条更好理解的折扣项。
这也是为什么网站购物车里的促销展示,经常比后台订单行看起来更“干净”。
八、做促销定制时最容易踩的坑
1)只在输入 code 的地方改逻辑
这样一旦用户后续改购物车、改运费、删商品,你的自定义折扣就会和 Odoo 主链路脱节。
2)把 reward line 当普通商品行处理
这会破坏 _cart_find_product_line()、数量统计和前台展示。
3)忽略网站维度
源码明确把 website 维度带进了 program domain。多网站场景下,一个活动不一定应该跨站生效。
4)忽略 session 里的 pending coupon
你会发现营销链接落地页“怎么没有自动带券”,其实不是 Odoo 不支持,而是定制时把这条链路绕丢了。
最后一句
Odoo 电商优惠活动真正厉害的地方,不在于它能“输入优惠码”,而在于它把:
待应用 code、自动奖励、购物车重算、配送联动、网站范围和过期校验
放进了一套持续更新的购物车规则引擎里。
所以理解 website_sale_loyalty,关键不是盯着优惠码输入框,而是看懂“购物车一变,奖励就得跟着重新判定”这条主线。
DISCUSSION
评论区