先抓主线
Sales Product Matrix 最容易被低估成一个 UI 小功能:
- 选一个产品模板
- 弹一个颜色 × 尺码矩阵
- 填数量
- 自动生成销售行
但标准 Odoo 的源码一开头就已经把重点说出来了:
矩阵功能放在 python / server side 完成,而不是只在 JS 里做。
原因也写得很直白:
- Web 客户端不会把订单里的所有行都加载到前端
- 大矩阵和长订单下,前端只能看到前面一部分
- 如果只靠 JS 直接改行,很容易漏掉第 41 行之后的数据
所以这个功能真正解决的不是“显示一个表格”,而是:
如何在大订单场景下,仍然可靠地把矩阵输入变成正确的 sale.order.line 变化。
这篇文章主要参考哪些源码
核心参考文件是:
/home/ubuntu/odoo-temp/addons/sale_product_matrix/models/sale_order.py
最关键的方法包括:
_set_grid_up()_apply_grid()_get_matrix()get_report_matrixes()
这个文件很适合讲“前端交互外观之下,其实是服务端数据编排”。
第一层:为什么矩阵不是纯前端逻辑
源码注释已经给出答案:
- JS framework 只加载当前显示的前几行
- 大矩阵、大量销售行时,前端并不拥有完整订单状态
- 如果强行只在浏览器里改,后面的销售行会失真或被忽略
因此标准设计选择:
- 前端只负责把矩阵选择结果存进字段
- 服务端负责真正解释这些变化,并更新
order_line
这类设计很典型:
- UI 看起来像前端功能
- 但可靠性要求决定了核心逻辑必须在后端收口
第二层:打开矩阵时,系统先把“当前真实状态”序列化出去
_set_grid_up() 在 grid_product_tmpl_id onchange 时触发。
它做的事情是:
- 把
grid_update = False - 再把
_get_matrix(product_template)的结果 JSON 序列化到grid
也就是说,打开矩阵不是只画一个模板空表,而是先把:
- 这个产品模板有哪些组合
- 当前订单里这些组合已录了多少数量
一起打包给前端。
所以矩阵界面显示的,不是“产品模板定义”,而是“产品模板定义 + 当前订单真实落单状态”。
第三层:真正落单时,标准只处理 dirty cells,而不是整张表重算
_apply_grid() 会在:
self.grid有值- 且
self.grid_update == True
时工作。
它读取 JSON 后,重点只取:
grid['changes']
也就是本次用户修改过的 cells。
这一步很重要,因为它说明矩阵更新不是暴力重建全部订单行,而是:
基于脏单元格增量更新订单。
这样做的好处是:
- 降低改动面
- 少碰没变化的行
- 避免已有业务字段、备注、组合项被无谓重写
第四层:变体不是预先假定存在,而是按组合即时创建/获取
对每个 dirty cell,标准会:
- 读取
ptav_ids - 通过
product.template.attribute.value还原组合 - 分离出
no_variant_attribute_values - 调用
product_template._create_product_variant(combination)
这里非常关键。
矩阵录单并不是只在现成 SKU 里选,而是基于属性组合去定位/创建对应变体,然后再找订单里有没有同款行。
这解释了两个常见现象:
- 为什么有的矩阵组合第一次录入时系统会“长出”变体
- 为什么 no-variant 属性也会影响找行逻辑
因为标准并不只按 product_id 粗暴匹配,还会同时比较:
product_idproduct_no_variant_attribute_value_ids- 并排除
combo_item_id
第五层:矩阵修改的是“目标总数量”,不是“加减一笔动作”
源码会先算:
- 订单里该组合已有的
old_qty - 本次 cell 目标数量
qty - 然后取差值
diff = qty - old_qty
如果 diff == 0,就直接跳过。
这说明矩阵不是在记录“本次新增了几件”,而是在表达:
这个组合在订单里的期望总数量现在应该是多少。
因此:
- 若已有行且目标数量非零,系统会尝试更新该行数量
- 若已有行且目标数量变成 0,在 draft/sent 才允许直接删行
- 若订单已确认,就不删,只把
product_uom_qty设成 0 - 若本来没有对应行,则新建 sale order line
这正是矩阵功能在业务边界上很成熟的地方:
- 草稿阶段可以更激进地重排
- 已确认阶段则尽量保留交易痕迹
第六层:为什么“同一变体出现多条销售行”会直接报错
这是标准最值得点赞的保护之一。
如果矩阵发现:
- 同一个 product variant
- 同一组 no-variant 属性
- 在订单中匹配出 多条 普通销售行
它不会尝试“智能合并”,而是直接抛:
You cannot change the quantity of a product present in multiple sale lines.
源码注释解释得非常诚实:
- 理论上可以把第一条改数量、删除其余几条
- 但这样会丢掉其他行背后绑定的业务逻辑
所以标准宁可报错,也不装作自己能安全合并。
这其实是一种很成熟的保守策略:
- 发现语义不唯一,就停止自动化
- 让用户先把多条业务行梳理干净
第七层:新建行时为什么还要拿默认值和 sequence
如果矩阵里某个组合之前没有销售行,_apply_grid() 不会裸造一条最小数据行。
它会先:
- 取
sale.order.line的default_get() - 继承必要默认值
- 并读取最后一条 order line 的
sequence
然后再构造新行。
这表示矩阵新建行不是绕过标准销售行生命周期,而是尽量在标准默认值体系里生成。
好处很明显:
- 税、单位、默认描述、默认行为更容易保持一致
- 自定义 default 逻辑也更容易一起生效
第八层:为什么报表里还能把矩阵状态反填回来
_get_matrix() 除了给前端开矩阵,还负责从当前订单里把数量反填回矩阵结构。
而 get_report_matrixes() 则进一步:
- 只在
report_grids开启时输出 - 只处理矩阵模式产品模板
- 只有某模板在订单里出现多行时才有展示价值
- 并过滤掉数量全为 0 的行
这意味着矩阵不是录单时的一次性交互,而是还能变成报价/报表的展示语义。
换句话说:
- 前端录的不是临时 UI 数据
- 而是一种可被再次渲染的订单结构
新手最容易误解的 5 件事
1. 以为矩阵录单只是前端功能
标准明确把关键更新逻辑放在服务端。
2. 以为矩阵提交会重建整张订单
不是。它只处理 dirty cells。
3. 以为匹配订单行时只看 product_id
不是,还要看 no-variant 属性,并排除 combo item。
4. 以为重复销售行可以自动安全合并
标准宁可报错,也不冒险吞掉业务语义。
5. 以为矩阵只是录单时临时界面
它还能被反填到报表矩阵里继续展示。
实战调试顺序
如果你排查“矩阵更新后数量不对”,建议看:
- 前端传回的
grid['changes']是什么 - 组合对应的
product_id是否被正确创建/定位 no_variant_attribute_values是否与已有销售行一致- 订单里是否已经存在同组合的多条销售行
- 当前订单状态是 draft/sent 还是已确认,决定了删行还是归零
如果你排查“报表矩阵为什么没显示”,重点看:
report_grids是否开启- 产品模板是否真的是 matrix add mode
- 该模板在订单里是否有多条销售行
- 矩阵各行是否全为 0,导致被过滤
一句话记忆法
Odoo 矩阵录单不是前端多一个表格,而是把用户改动的脏单元格交给服务端去解释:创建/定位变体、比较目标总量、保护重复销售行,并把最终结构再反填到报表矩阵。
DISCUSSION
评论区