先说结论
Odoo 电商里的购物车,从来就不是一个临时前台篮子。
website_sale 的设计是:
购物车本身就是一张处在特定生命周期里的
sale.order。
所以从用户第一次点“加入购物车”开始,系统已经在做销售单层面的工作:
- 找现有行还是新建行;
- 校验数量是否合法;
- 变体组合是否存在;
- 配送费要不要重算;
- 会话里的购物车数量要不要刷新;
- 这张单是不是匿名 cart;
- 后续是否会进入 abandoned cart 恢复邮件链路。
这就是为什么很多电商定制一旦只盯着前台 JS,最后总会踩坑。真正的主链路在 sale.order 模型里。
一、为什么 Odoo 把购物车直接建模成 sale.order
从 website_sale/models/sale_order.py 看,核心方法全都挂在 sale.order 上:
_cart_add()_cart_find_product_line()_cart_update_line_quantity()_verify_cart_after_update()_check_cart_is_ready_to_be_paid()_filter_can_send_abandoned_cart_mail()
这说明 Odoo 的思路不是“前台先随便存一下,结账时再生成销售单”,而是:
- 从一开始就用销售订单承载电商状态。
这么做的好处很明显:
- 价格、税、配送、优惠、付款都能沿用销售链路;
- 购物车不是独立平行世界,而是销售订单的前置阶段;
- 弃单、恢复、重算、支付校验都更容易串起来。
二、加入购物车时,系统先想的是“合并行”而不是“新建行”
_cart_add() 的第一件大事,不是 create,而是:
- 先找有没有可复用的现有行。
源码会先走 _cart_find_product_line(),匹配条件不只是 product,还包括:
- UoM;
linked_line_id;- 自定义属性值;
no_variant属性值;- combo 场景下的额外边界。
也就是说,Odoo 不会简单认为“同产品就合并”。
它真正想判断的是:
这次加购,是否还是同一份可合并配置。
这对电商很重要,因为一旦产品存在:
- 变体;
- no_variant 属性;
- 可选组合;
- 套餐/组合商品;
“是不是同一行”就不能只看 product_id 了。
三、为什么数量校验放在更新前,而不是支付前才做
无论是新增还是改数量,源码都会先走 _verify_updated_quantity()。
这背后的思路很清楚:
- 不要让明显非法的数量先落到购物车里,最后再在支付页统一报错。
好处是:
- 用户反馈更即时;
- 购物车状态更可信;
- 后续推荐、配送、库存提示也能基于更真实的 cart。
而且 _cart_update_line_quantity() 里还有一个很容易忽略的细节:
- 如果某条 line 因为多标签页并发、参数错误或已被删除而找不到,系统不会悄悄吞掉,而是直接提示刷新页面。
这说明 Odoo 默认承认:
- 网站购物车会发生并发修改;
- 多标签页是正常用户行为;
- 所以反馈必须偏保守,而不是假装一切顺利。
四、为什么更新购物车后还要做一次全局校验
很多人看到 _cart_add() 或 _cart_update_line_quantity() 已经做过数量检查,就以为流程结束了。
其实后面还有 _verify_cart_after_update()。
这个方法做的是“全局整理”,典型包括:
- 如果整车只剩服务,移除配送行;
- 如果已经选了 carrier,就重新计算运费;
- 把 session 里的
website_sale_cart_quantity刷新成最新值。
这说明 Odoo 把购物车更新分成两层:
- 行级校验:你这次更新是否合法;
- 车级校验:这次更新对整张订单造成了什么连锁变化。
很多定制问题就出在这里:
- 只改了 line,没触发 order 级整理;
- 结果配送费、顶部购物车数字、整车状态全部失真。
五、匿名购物车到底是什么
源码里的 _is_anonymous_cart() 很直白:
- 如果订单 partner 还是网站 public user 对应的 partner,就视为匿名 cart。
这说明匿名购物车的本质不是“没 session”,而是:
- 订单还没真正绑定到具体客户身份上。
这点特别重要,因为很多人以为匿名与登录只是前台状态。
但从订单模型视角看,真正关键的是:
- partner 是否还是公共用户;
- 地址是否已经补齐;
- 是否已经进入可恢复、可追踪、可继续营销的客户链路。
也正因为如此,弃单邮件会非常关心:
- 有没有真实 email;
- 后续有没有成功下过单;
- 支付过程是否出现 error;
- 购物车里是不是全免费商品。
六、为什么 abandoned cart 不是“超时就发邮件”这么简单
_filter_can_send_abandoned_cart_mail() 的条件比很多人想得严。
系统会过滤掉这些情况:
- 客户没有邮箱;
- 支付交易已经报错;
- 购物车全是零价行;
- 这个客户在弃单之后已经完成了新的 sale order。
而 _cart_recovery_email_send() 还会先确保 portal token 存在,再把恢复链接直接指向:
/shop/cart?id=<order_id>&access_token=<token>
这说明 Odoo 对恢复邮件的理解不是“营销自动化发一封提醒”,而是:
给客户一个能安全、直接、低摩擦回到原购物车的入口。
所以弃单恢复链路其实同时包含:
- 订单有效性;
- 访问安全性;
- 客户状态判断;
- 营销时机判断。
七、最容易误解的 5 件事
1. 以为购物车只是前台 session 数据
不对。核心状态落在 sale.order。
2. 以为加入购物车一定新建一行
不对。系统会先判断是否应该合并到现有行。
3. 以为数量问题只在支付页才校验
不对。加购和改数量时就会先验证。
4. 以为匿名购物车只是“未登录”
不对。关键是订单是否仍绑定 public partner。
5. 以为弃单邮件只看“多久没付款”
不对。源码还会校验邮箱、后续成交、支付错误、零价订单等条件。
八、实战定制最该注意什么
1. 不要绕开 _cart_add() / _cart_update_line_quantity() 直接乱写订单行
你会丢掉合并逻辑、数量校验和整车整理。
2. 改购物车数量规则时,先想 combo、optional products、多标签页并发
电商不是单一 SKU 世界。
3. 做弃单恢复时,不要只盯邮件模板
真正决定“该不该发、能不能恢复”的核心都在模型层。
一句话记忆
Odoo 电商购物车不是“报价单之前的临时容器”,而是“从第一次加购起就已经以
sale.order运行的前置销售订单生命周期”。
DISCUSSION
评论区