先说结论
Odoo 的 Sales Grid Entry,不是一个“更方便填数量”的小功能。
它真正解决的是:
当一个产品有大量变体组合时,销售如何用矩阵方式下单,同时又不破坏 sale.order.line 原本的业务语义。
所以 Variant Matrix 的重点,不只是快,而是:
- 快速录入
- 仍然尊重变体逻辑
- 尽量不破坏已有销售行的业务含义
这篇文章主要参考了哪些源码
主要看的是:
/home/ubuntu/odoo-temp/addons/sale_product_matrix/models/sale_order.py/home/ubuntu/odoo-temp/addons/sale_product_matrix/models/product_template.py/home/ubuntu/odoo-temp/addons/sale/wizard/res_config_settings.py
最有意思的一段注释就在 sale_product_matrix/models/sale_order.py:
- 这套 matrix 功能刻意放在 Python / server side 做
- 原因是前端只加载前几行时,不适合处理很大的矩阵
这段注释其实已经把设计取向讲得很清楚:
它优先保证业务正确性和大矩阵可处理性,而不是追求前端炫酷交互。
为什么销售录单需要“矩阵”,而不是一行一行加
如果你卖的是简单商品,一行一行加确实足够。
但如果你卖的是:
- 颜色 × 尺码
- 材质 × 规格
- 型号 × 配置
- 两个甚至三个属性组合
销售面对的就不是几个 SKU,而是一整张二维甚至准多维组合表。
这时候逐行录入的问题不是慢而已,而是:
- 容易漏组合
- 容易重复录
- 很难整体看数量结构
- 客户确认时也看不清变体分布
所以矩阵录单解决的是“组合型商品的下单视图”问题。
Odoo 为什么把它做在服务端
源码里的注释非常诚实:
- 如果在 JS 里做,大矩阵场景下只加载前面若干行
- 后面的数据拿不到,改动就不完整
- 想彻底解决,要 hack 前端框架
于是 Odoo 选择了另一条路:
- 矩阵本身由服务端生成
- 前端只是带着结构回传变更后的 cell
- 服务端再统一应用这些变化
这很“朴素”,但我觉得是对的。
因为销售矩阵这种场景,最怕的是:
- 看起来改成功了
- 实际只有一部分 cell 真写进订单
那会比界面不好看危险得多。
一个 cell 被改动后,系统真正做了什么
在 _apply_grid() 里,系统会对每个 dirty cell 做这些事:
- 根据
ptav_ids找到属性组合 - 调
product_template._create_product_variant(combination)找到或创建对应 variant - 在当前订单行里查找这个 variant 对应的 line
- 比较新数量和旧数量
- 决定是更新、删除还是新增 sale.order.line
这说明 matrix cell 不是独立数据模型。
它最后还是会落回标准的 sale.order.line。
也就是说:
矩阵只是销售录入视图,不是另一套订单存储结构。
这点非常重要。
因为它解释了为什么矩阵功能虽然好用,但不能随便脱离原有业务模型乱搞。
为什么“多条相同变体销售行”会触发报错
源码里有个很关键的判断:
- 如果同一个变体在订单里已经出现了多条 line
- 又想通过 matrix 直接改数量
- 系统会抛
ValidationError
很多人看到这里会嫌 Odoo“保守”。
但我反而觉得这是理性的。
因为当同一变体对应多条销售行时,通常意味着这些行已经带有额外业务语义,比如:
- 不同折扣
- 不同交期
- 不同说明
- 不同促销来源
- 不同组合包关系
这时如果 matrix 粗暴地把它们合成一条或随便改一条,等于把原本行级语义抹平了。
所以 Odoo 宁愿报错,也不愿偷偷替你“猜”该怎么合并。
这正是它的设计边界:
矩阵负责批量录入,但不负责消灭业务差异。
为什么数量改成 0 的处理还分状态
在 _apply_grid() 里,如果某个 cell 的数量被改成 0:
- 订单还在
draft/sent,可以直接移除对应行 - 订单不是草稿态,就改成
product_uom_qty = 0.0
这个细节也很说明问题。
因为草稿阶段,销售行还只是意图;确认之后,销售行往往已经跟:
- 交付
- 开票
- 追踪
- 审计
产生关联。
所以确认后的单据,系统就不再随意物理移除行,而是更保守地保留结构。
这不是麻烦,而是对后续链路负责。
为什么报表里还要保留矩阵视图
get_report_matrixes() 会把订单行重新聚合成 matrix,用于报告展示。
这意味着 Odoo 不只是想让销售录单时方便,还希望:
- 客户看报价时,也能按矩阵理解变体结构
- 团队内部打印单据时,也能看见整张组合分布
这很重要。
如果系统只支持“矩阵录入”,最后报价输出却回到几十条零散销售行,客户体验会很割裂。
所以 Sales Grid Entry 的完整价值是:
- 输入端是矩阵
- 存储端是标准销售行
- 输出端还能重新恢复矩阵视图
这才闭环。
新手最容易误解的 4 件事
1. 以为 matrix 是另一套订单模型
不是。它最终还是写回 sale.order.line。
2. 以为矩阵应该自动帮你合并所有重复行
如果行上已经承载不同业务语义,强行合并反而危险。
3. 以为前端做矩阵一定更先进
在大数据量和复杂组合下,服务端方案反而更稳。
4. 以为 grid 只是为了内部录单快
实际上它也服务于报价展示和客户理解。
什么时候应该启用它
我会把 Sales Grid Entry 用在这些场景:
- 典型服饰 / 家具 / 配件变体销售
- 客户习惯按颜色 × 尺码整体看单
- 一张单里同模板商品会大量重复出现
- 销售更关心组合分布,而不是一条条 SKU 明细
如果你的商品本身没有明显矩阵结构,硬开这个功能反而会让流程更重。
一句话记忆法
Sales Grid Entry 不是批量填数量的小工具,而是让“矩阵化录单”与
sale.order.line的业务语义和平共处。
DISCUSSION
评论区