到店自提

Odoo 到店自提为什么不是“换个配送方式”这么简单:门店库存、税位重算与提货点选择链路讲透

很多人把 Odoo 的 Click & Collect 当成配送方式列表里多一个选项,但 website_sale_collect 实际把门店选择、仓库切换、门店库存校验、税位重算和不可付款拦截串成了一条完整链路。

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

先说结论

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_idfiscal_position_id

这一步非常必要。

因为只要配送方式语义变了,订单就不能继续带着“自提门店仓库”和“自提税位”往下跑。

否则你会遇到特别典型的脏数据问题:

  • 页面上看是快递;
  • 订单底层却还拿着门店仓和门店税位。

七、实施时最容易误解的地方

1. 以为 Click & Collect 只是 delivery carrier 多一个选项

不对。它会影响仓库、库存、税位和支付前校验。

2. 以为库存可以沿用网站默认仓逻辑

不对。关键是所选提货门店对应仓库的可用量。

3. 以为门店只影响前台展示,不影响订单语义

不对。warehouse_id 和 fiscal position 都会跟着变。

4. 以为自提改单成快递只要改 carrier 就行

不对。还要把仓库和税位上下文一起恢复。


最后总结

Odoo 的 website_sale_collect 真正做的是:

  • 让用户在商品页和结账页明确选择提货门店;
  • 把门店映射成订单仓库;
  • 用门店地址重算 fiscal position;
  • 按门店库存而不是全站库存判断是否可付款;
  • 在自提与普通配送切换时保持订单语义一致。

所以它更像:

一套围绕门店库存与税务上下文构建的官网自提订单机制。

DISCUSSION

评论区

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