多公司场景里,制造往往不是“多加一个 company_id 字段”那么简单。因为制造会同时碰到:
- 产品和 BOM
- lot / serial
- route / warehouse
- kit / phantom 识别
- 补货触发和 MO 创建
这些对象只要有一个跨公司串了,现场就会出现非常难排查的“系统看得见,但不能用”“能建单,但一确认就炸”的问题。
Odoo 在 test_multicompany.py 里其实把这些边界测得很细,这比只看表结构更能说明它真正想守住什么。
第一层边界:BOM 不是共享模板,而是公司语义对象
测试 test_bom_1() 和 test_bom_2() 非常直接:
- 你不能在 Company A 的 BOM 上直接使用 Company B 的产品作为成品;
- 也不能把 Company B 的产品塞成 Company A BOM 的组件。
这说明 Odoo 眼里,BOM 不是一个纯技术结构,而是有明确业务归属的制造定义。
很多项目早期喜欢把主数据“先全共享”,后面再慢慢分公司。制造模块通常最先把这种想法打回去,因为一旦 BOM 跨公司混用,后面所有成本、库位、补货、工单都会一起串线。
第二层边界:MO 能建出来,不代表能被当前公司合法确认
test_production_1() 说明一个常见误区:你可以构造出一张 company 与 product 不匹配的 MO,但 action_confirm() 会拒绝它。
也就是说,Odoo 并不把“create 成功”当成业务合法性终点,确认阶段还有第二道约束。
这也是为什么很多多公司 bug 不会在录单时暴露,而会在确认、预留、完工时才炸开。
第三层边界:lot / serial 是最敏感的跨公司对象
test_product_produce_1() 和 test_product_produce_2() 都在测同一件事:
- 成品 lot 不能拿别的公司的;
- 组件 lot 也不能偷偷拿别的公司的;
- 即使用户同时能访问多家公司,也不代表 lot 追溯边界可以打穿。
这很合理。因为 lot / serial 不是普通主数据,它是追溯证据。跨公司误用 lot,比跨公司误选普通产品严重得多。
第四层边界:制造路线不是“全局公共规则”,而是会跟公司和仓库一起复制
test_company_specific_routes_and_company_creation() 与 test_company_specific_routes_and_warehouse_creation() 很能体现 Odoo 的制造设计。
测试里做了两件事:
- 把 manufacture route 按公司拆成 company-specific route;
- 新建公司或新建仓库时,验证新的 manufacture route 会正确落到目标公司。
这说明 Odoo 不希望制造路线永远挂在一个“万能全局 route”上。它承认同名制造流程在不同公司、不同仓库里,可能需要不同归属和不同规则集合。
stock_rule._get_matching_bom() 真正体现了“先公司,再兜底”
在 addons/mrp/models/stock_rule.py 里,_get_matching_bom() 的逻辑顺序很值得看:
- 先看 values 里是否显式给了
bom_id; - 再看 orderpoint 上是否有
bom_id; - 再用
_bom_find(..., company_id=company_id.id)找公司内正常 BOM; - 还找不到,再退到 picking_type=False 的同公司查找。
注意这里的兜底不是“全库随便找一个”,而仍然带着 company_id。这说明 Odoo 在补货触发制造时,非常坚持 BOM 归属不能漂移。
_filter_warehouse_routes() 还在防一类隐蔽问题:kit 误判
如果 route 里有 manufacture 规则,_filter_warehouse_routes() 会进一步判断产品是否真的有 normal BOM;如果只有 phantom BOM,就不把它当普通制造路线处理。
这和 test_multi_company_kit_reservation() 刚好呼应。
那个测试验证:同一个产品在 Company A 是 kit,不代表在 Company B 也必须按 kit 处理。换句话说,is_kits 是公司相关属性,不是一个对全环境恒真的标签。
这点特别容易坑多公司实施:
- 总部把某产品定义成 phantom kit;
- 分公司未必沿用同一逻辑;
- 如果系统把 kit 身份全局化,库存预留和发货都会跑偏。
Odoo 选择按公司上下文重新判断,代价是逻辑更复杂,但边界更稳。
_run_manufacture() 说明 MO 也是按公司分桶创建的
在 _run_manufacture() 里,新的 production values 会先按 company_id 分组,再分别 with_company(company_id).create(...)。
这看起来像实现细节,其实是非常关键的安全动作:补货跑制造时,系统不是“先建 MO,后补公司”,而是从创建那一刻起就把公司上下文锁住。
这能避免很多隐蔽问题,例如:
- 默认库位拿错公司;
- picking type 取到别家公司;
- 后续 move / procurement group 归属混乱。
对实施和定制的启发
1. 多公司制造不能只测主流程
要专门测:建单、确认、lot 录入、预留、完工、补货触发、warehouse 新建、route 复制。很多边界都藏在这些“第二步”里。
2. 共享主数据要分层讨论
产品模板共享,不等于 BOM、lot、route、warehouse 行为也能共享。制造相关对象天然更接近执行层,隔离要求通常比销售、CRM 更高。
3. 定制 route / BOM 选择逻辑时,先别碰掉 company guardrail
很多项目为了“自动选最合适 BOM”会改 _get_matching_bom() 或 route 过滤。这个地方一旦把 company 约束绕掉,后面问题会非常难查。
一句话总结
Odoo 多公司制造真正难的,不是多了一家公司,而是 BOM、lot、route、warehouse、kit 身份都要在公司上下文里重新成立。源码和测试之所以写得这么细,就是因为制造一旦串公司,后果远比普通主数据共享严重。
DISCUSSION
评论区