结账地址

Odoo 电商结账地址为什么老“自己变了”:地址表单、子联系人复用与账单/收货同步主链路讲透

Odoo 网站结账里的地址不是简单写回 partner,而是围绕匿名购物车、主联系人、子地址、账单与收货地址同步、国家语言归属等规则做了一整套地址决策。

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

先说结论

很多人调 Odoo 电商结账时,最容易产生的错觉是:

  • 页面上填一个地址;
  • 提交后把字段写回客户;
  • 完成。

实际完全不是这样。

website_sale 里的地址流程,本质上是在回答几个连续问题:

  1. 当前购物车是不是匿名 cart;
  2. 用户是在新建地址,还是编辑已有地址;
  3. 这是账单地址还是收货地址;
  4. 当前地址能不能直接复用现有子联系人;
  5. 是否要把收货地址同时当账单地址;
  6. 这个地址该归到哪个网站、公司、销售员语境里。

所以你看到的“地址自己变了”“怎么又生成了一个子联系人”“为什么账单地址跟着一起改了”,大多不是 bug,而是 Odoo 在执行一套很明确的地址决策逻辑。


一、/shop/address 不是单纯表单页,而是 checkout 状态机的一环

website_sale/controllers/main.py 里,/shop/address 会先拿当前 request.cart,然后先做购物车校验,再决定本次地址表单到底是在干什么。

核心不是“渲染模板”,而是先判定:

  • 当前要编辑的是哪个 partner;
  • 这个 partner 的地址类型是 billing 还是 delivery
  • 是否需要把 use_delivery_as_billing 打开。

这意味着地址页不是孤立存在的,它依附于当前 cart 上的:

  • partner_id
  • partner_invoice_id
  • partner_shipping_id

所以你在地址页看到的内容,本质上是订单当前地址状态的一个编辑视图


二、为什么匿名购物车和登录用户的地址行为完全不一样

_prepare_address_update() 里有个非常关键的分支:

  • 匿名 cart:允许从空 partner 语义出发创建地址;
  • 非匿名 cart:只能在订单已有主联系人、账单地址、收货地址及其合法子联系人范围内编辑。

这背后的目的很明确:

对匿名用户

系统必须允许他从零开始填一份地址,所以 partner 可以视作“待创建”。

对已登录用户

系统不能让他随便通过 URL 改到别人的联系人记录,所以会检查:

  • 指定的 partner 是否已经关联到当前订单;
  • 当前客户是否有权限编辑这条地址。

这就是为什么 Odoo 的地址页虽然看起来很普通,但其实已经带着访问边界。


三、地址提交后为什么不一定新建联系人

真正提交走的是 /shop/address/submit

很多人会以为:

  • 填一个地址;
  • 就 create 一个 res.partner

但 Odoo 实际会先区分:

  • 是新地址还是旧地址;
  • 是匿名还是已登录;
  • 是账单还是收货;
  • 是否只卖服务型产品;
  • 是否勾选“收货地址同账单地址”。

而且在 express checkout 和普通 checkout 里,都能看到一个核心思想:

尽量复用已有子联系人,只有找不到匹配地址时才新建。

源码里的 _find_child_partner() 会遍历商业主体下面的子联系人,只要地址字段一致,就直接复用那条联系人,而不是继续制造重复地址。

这点很关键,因为很多企业客户会反复下单。

如果每次网站订单都机械新建一个地址联系人,后台客户档案会很快爆炸。


四、为什么 Odoo 要把国家/州先转成对象,再做地址比较

在 express checkout 相关逻辑里,Odoo 会先调用 _include_country_and_state_in_address(),把前端传来的 ISO 国家/州编码转成 country_idstate_id

看起来像小事,其实非常重要。

因为地址去重和比较必须基于统一结构。

否则会出现这种问题:

  • 前端传 US
  • 后台存的是美国记录 id;
  • 两边直接比较永远不相等。

所以 Odoo 先做标准化,再用 _are_same_addresses()_find_child_partner() 比较。

这说明它关心的不只是“页面有没有填值”,而是:

  • 地址能不能被稳定识别;
  • 已有联系人能不能被复用;
  • 后续税、配送、客户档案能不能建立在同一份地址事实上。

五、账单地址、收货地址、主联系人,三者到底怎么联动

地址提交成功后,Odoo 不会只写 partner 本身,而是更新订单上的 partner 字段集合。

shop_address_submit() 里可以看到:

  • 如果是主地址更新,强制重算 partner_id 相关字段;
  • 如果是账单地址,更新 partner_invoice_id
  • 如果是收货地址,更新 partner_shipping_id
  • 如果勾选 use_delivery_as_billing,则收货地址也会同步给账单地址。

这意味着网站结账地址不是一张表单,而是一个订单地址指针重绑定过程

所以有时你觉得“我只是改了收货地址,为什么账单也跟着变”,原因通常不是它乱来,而是:

  • 本次流程确实要求把配送地址兼作账单地址;
  • 或当前订单仍处于共用同一 partner 的模式。

六、为什么只卖服务时,地址跳转逻辑又不一样

源码里还有一个很容易忽略的细节:

  • 如果订单 only_services,成功提交地址后更倾向直接回 /shop/checkout?try_skip_step=true

这反映出 Odoo 对电商结账的理解不是死板的“三步走”,而是:

  • 按订单是否真的需要配送来裁剪流程。

也就是说,地址流程虽然存在,但其存在目的不是“网站都得有地址页”,而是为了满足订单履约。

如果没有可配送商品,配送相关步骤就应该尽量跳过。

这也是 Odoo checkout 体验比较顺的一个原因:

  • 它的流程是条件驱动的,不是模板驱动的。

七、为什么新建地址时会带上网站、公司和销售员语境

_complete_address_values() 做了几件很业务化的事:

  • 语言不在网站可用语言里就移除;
  • 公司用当前网站所属公司;
  • user_id 绑定到网站销售员;
  • 某些场景还会写入 website_id

这说明网站地址不是孤立主数据,而是带着网站上下文创建的客户数据。

这会影响什么?

  • 后续销售归属;
  • 多网站客户识别;
  • 语言与邮件模板;
  • 地址后续是否被别的网站或别的公司复用。

所以如果你做多网站或多公司电商,地址逻辑不能只盯住 res.partner 表面字段,还要看它是在哪个网站语境里创建的。


八、做结账地址定制时,最容易踩的坑

1)前端加字段,却没接入地址标准化

结果页面看着提交成功,后台却无法正确复用联系人,或者配送税费计算异常。

2)强行每次都新建联系人

短期感觉“最安全”,长期一定把客户档案搞脏。

3)把账单和收货逻辑混在一起改

最后常见结果是:

  • 订单 partner 指针错乱;
  • 税务地址和履约地址对不上;
  • 支付页、配送页来回跳。

4)忽略匿名购物车与登录用户的边界差异

这会直接带来权限问题,或者让已登录客户改到不该改的联系人记录。


最后一句

Odoo 电商里的地址流程,真正处理的不是“填表单”,而是:

购物车身份识别、地址标准化、子联系人复用、账单/收货指针更新,以及网站上下文归属。

你一旦把它看成这条主链路,就能明白为什么很多所谓“地址异常”,其实只是系统在认真维护订单和客户数据的一致性。

DISCUSSION

评论区

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