先说结论
Odoo 网站里的 Click & Collect,并不是“配送方式从快递换成自提”这么轻量。
website_sale_collect 真正在处理的是一条跨配送、库存、仓库和税务的复合链路:
- 商品页要不要显示选店按钮;
- 订单里当前提货门店是谁;
- 订单仓库要不要跟着改;
- 改门店后税位是否需要重算;
- 该门店库存够不够;
- 如果不够,支付前能不能继续;
- 切回普通配送时,又该如何把自提状态撤掉。
所以 Click & Collect 不是 UI 开关,而是:
把“我在哪家店拿货”变成订单语义的一部分。
一、为什么商品页就要提前介入门店选择
controllers/main.py 里的 _prepare_product_values() 会为商品页补几类数据:
- 当前已选提货点;
- 是否显示选择门店按钮;
- 用什么 zip code 去初始化门店查找。
而且如果当前站点的 in-store delivery method 只有一个仓库,系统会直接把这家店作为默认门店。
这说明 Odoo 认为“去哪家店拿”不是 checkout 末尾才想起的问题,而是:
- 在商品页就会影响可得性与用户决策。
这和普通快递配送很不一样。因为快递往往先决定送货地址,再算仓库; 但自提是先决定门店,门店本身就决定库存和税务上下文。
二、为什么选中提货点后,订单仓库要跟着改
sale.order._set_pickup_location() 里会把前端传来的 pickup_location_data 解析后写到订单上。
如果当前 carrier 是 in_store,源码会进一步:
- 把
warehouse_id改成所选门店对应仓库; - 然后触发
_compute_fiscal_position_id()。
这一步非常关键。
因为到店自提不是“商品还从默认仓发,只是客户自己去拿”。
在 Odoo 语义里,它更像:
- 订单本身已经绑定到某个提货仓库;
- 后续库存、税、拣货都应该以这个仓为中心。
所以如果实现里只记一个 pickup point 名字,不同步仓库,后面很多计算都会错。
三、为什么 fiscal position 也要跟着提货门店重新匹配
_compute_fiscal_position_id() 的覆写,是这个模块最容易被忽视、但最业务化的一段。
对于 in-store 订单,系统不是按普通送货地址去算 fiscal position,而是调用:
AccountFiscalPosition._get_fiscal_position(order.partner_id, delivery=order.warehouse_id.partner_id)
也就是说,它把“提货门店的地址主体”当成 delivery 语境的一部分。
这很合理,因为线下提货常常会影响:
- 适用税区;
- 本地税规则;
- 门店所属公司或地区带来的税务判断。
所以 Click & Collect 不是纯物流问题,它会直接碰到财税逻辑。
四、为什么商品数量校验在自提模式下要延后到门店维度
_verify_updated_quantity() 在这里选择:
- 如果是可库存商品;
- 不允许超卖;
- 且网站启用了 in-store delivery;
- 那么购物车更新时先不做普通
website_sale_stock的校验。
为什么?
因为普通库存校验只知道“网站默认仓或总体可卖量”,但不知道用户最终选的是哪家门店。
真正可靠的校验必须等到提货点确定后,按该仓库看库存。
所以 Odoo 把更严格的判断放到了:
_get_insufficient_stock_data(wh_id)_is_in_stock(wh_id)_check_cart_is_ready_to_be_paid()
这套以门店仓库为中心的方法里。
换句话说:
自提模式下,库存边界不是“全站还有没有”,而是“你选的这家店够不够”。
五、为什么支付前还要再拦一次
_get_shop_payment_errors() 和 _check_cart_is_ready_to_be_paid() 两层都做了拦截。
如果订单有可配送商品,且 carrier 是 in_store:
- 没选门店,会提示必须选择门店;
- 选了门店但部分商品在这家店没货,会提示不能继续。
这说明 Odoo 对支付页的态度非常明确:
- 不能把“提货点未定”或“门店库存不足”的单子放进付款环节再说。
因为一旦已经付款,后续改门店、拆单、退款、补货的成本都会变高。
六、为什么切回普通配送时,还要把仓库和税位再重算一遍
_set_delivery_method() 还处理了一个容易漏掉的边界:
- 如果订单原来是自提;
- 现在改成非自提配送方式;
- 系统会重新计算
warehouse_id和fiscal_position_id。
这一步非常必要。
因为只要配送方式语义变了,订单就不能继续带着“自提门店仓库”和“自提税位”往下跑。
否则你会遇到特别典型的脏数据问题:
- 页面上看是快递;
- 订单底层却还拿着门店仓和门店税位。
七、实施时最容易误解的地方
1. 以为 Click & Collect 只是 delivery carrier 多一个选项
不对。它会影响仓库、库存、税位和支付前校验。
2. 以为库存可以沿用网站默认仓逻辑
不对。关键是所选提货门店对应仓库的可用量。
3. 以为门店只影响前台展示,不影响订单语义
不对。warehouse_id 和 fiscal position 都会跟着变。
4. 以为自提改单成快递只要改 carrier 就行
不对。还要把仓库和税位上下文一起恢复。
最后总结
Odoo 的 website_sale_collect 真正做的是:
- 让用户在商品页和结账页明确选择提货门店;
- 把门店映射成订单仓库;
- 用门店地址重算 fiscal position;
- 按门店库存而不是全站库存判断是否可付款;
- 在自提与普通配送切换时保持订单语义一致。
所以它更像:
一套围绕门店库存与税务上下文构建的官网自提订单机制。
DISCUSSION
评论区