结论先行
“外部税”最容易被误解成一次同步调用:客户到支付页时请求税率服务,算个金额就结束。企业版的实现正好证明不是这样。税务上下文会在网站、销售、会计三个阶段被反复重放,因为每个阶段承担的法律与业务责任不同。
第一层:入口或表面动作
网站阶段,website_sale_external_tax 在 _order_summary_values() 和 _get_shop_payment_errors() 里都会调用 order._get_and_set_external_taxes_on_eligible_records()。前者是为了让订单摘要显示正确税额,后者是为了在支付前发现地址无效等错误。这里最重要的是:网站层只是预演,不是最终承诺。它会把错误反馈给用户,但并不以页面上看到的税额作为最终会计真相。
第二层:真正的业务护栏
到了销售阶段,sale_external_tax.sale_order.action_confirm() 又会重新跑一次 _get_and_set_external_taxes_on_eligible_records()。原因很现实:地址、折扣、行项目、送货方式在客户点支付前后都可能变化,销售单确认时必须再拿一次“当前订单事实”向税务服务求值。_get_line_data_for_external_taxes() 也说明了这一点,它重新从 base lines 组装待税务计算的数据,而不是复用网站缓存。
第三层:状态落点与边界
真正的法律边界在会计。account_move._post() 在发票过账前再次拉外部税;button_draft() 与 unlink() 则通过 _uncommit_external_taxes()、_void_external_taxes() 撤销或作废外部税承诺。也就是说,企业版不是“算出税额”就完,而是把外部税当成会计对象生命周期的一部分:未过账可改、回草稿要反提交、删除要作废。
为什么这套设计更稳
这种多节点 replay 的设计非常像消息系统里的幂等消费者。网站在乎体验,销售在乎订单一致性,会计在乎法定凭证,三者都要拿到同一份税务上下文,但没有任何一层能永久相信上一层算出来的结果。你若把外部税结果缓存成一个固定字段,只在 checkout 算一次,后面任何改价、改地址、拆单、重开票都会让税额失真。
实战启示
所以这类扩展最该学到的不是“怎么接税务接口”,而是“怎么在多个关键节点重放、校验并撤销外部状态”。企业版把 mixin、sale.order、account.move、website controller 各管一段,正是为了让外部税既能穿透前台结账,也能在后台会计生命周期里自洽。
DISCUSSION
评论区