先说结论
在 Odoo 企业版里,工单里的 worksheet 质检不是“点开一张表、填完保存”这么简单。
它实际上是一条三段式链路:
- 前台先把 worksheet 表单弹出来,让操作员填写;
- 后台同时悄悄创建一个
quality.check.wizard,当作质检游标和上下文容器; - 保存 worksheet 后,系统再按
worksheet_success_conditions判断这次填写到底算pass还是fail,然后自动切到下一条检查,甚至直接推动工单继续流转。
一句话概括:
Odoo 把 worksheet 质检设计成“表单录入 UI + 后台质检状态机”的组合,而不是一个孤立文档。
这也是为什么很多团队会困惑:
- 明明看见的是 worksheet 表单,为什么背后还会冒出 wizard;
- 明明已经保存了内容,为什么状态还要再判定一次;
- 为什么在 shop floor 上点一下 worksheet,会自动跳去下一道检查,而不是停在原地。
这些现象都不是偶然,而是官方源码故意这样拼起来的。
这篇文章和站内已有制造文章有什么区别
站内已经有几篇制造相关内容,比如:
- 工单与工序怎么拆;
- 工单依赖怎么阻塞;
- 质量检查如何嵌进制造步骤;
- 条码制造为什么会被质量闸门拦住。
但这篇切口更细,重点不是“工单里有质检”本身,而是:
当质检类型是 worksheet 时,Odoo 为什么要同时维护 worksheet 表单、quality wizard、success conditions 和 shop floor 自动跳转。
这条链主要落在:
enterprise/quality_control_worksheetenterprise/quality_mrp_workorder_worksheetenterprise/mrp_workorder/static/src/...
所以它不是泛讲 quality_mrp,也不是泛讲工单,而是在讲 企业版车间 worksheet 质检的实际执行机制。
第一层:worksheet 质检不是直接打开记录,而是先借 action_open_quality_check_wizard() 起链
在 quality_control_worksheet/models/quality.py 里,quality.check.action_open_quality_check_wizard() 对 worksheet 类型做了特殊分流。
如果当前 check 的 test_type == 'worksheet',它不会直接走普通质检 wizard,而是先:
- 调
check_id.action_quality_worksheet()生成 worksheet 的 action; - 再创建一个
quality.check.wizard,写入: -check_ids-current_check_id - 把这些上下文塞回 worksheet action:
-
default_check_ids-default_current_check_id-quality_wizard_id-from_failure_form = False
这一步很关键。
因为它说明 Odoo 的思路不是:
- worksheet = 一个独立表单;
而是:
- worksheet = 当前质检链上的一个前台录入壳;
- wizard = 这条质检链真正的游标和状态容器。
所以你表面上是在填表,实际上系统已经先把“这条检查在整串 check_ids 里的位置”记住了。
这正是后面能实现“上一条 / 下一条 / 失败回退 / 自动续跳”的基础。
第二层:action_quality_worksheet() 打开的不是静态文档,而是绑定 x_quality_check_id 的业务记录
继续看 action_quality_worksheet()。
它会读取 worksheet_template_id.action_id,然后按模板对应模型去搜:
x_quality_check_id = self.id
如果已经有 worksheet 记录,就带 res_id 打开;
如果还没有,就靠 context 里的:
default_x_quality_check_id = self.id
让新建记录自动挂到当前 quality.check。
这意味着 worksheet 在 Odoo 里不是附件,也不是临时 JSON,而是一个正经的业务对象,只不过它永远围绕:
- 一条具体的
quality.check
来展开。
所以企业现场经常会犯的第一个误解是:
worksheet 是“补充说明页”。
其实不对。
在源码层面,它更像:
质检结果承载模型。
你填入的数据不是给人看看而已,而是后面要参与 pass/fail 判定的。
第三层:保存 worksheet 不等于质检通过,真正的判定靠 worksheet_success_conditions
还是在 quality_control_worksheet/models/quality.py,action_worksheet_check() 才是真正决定结果的地方。
它先做一个特别硬的判断:
worksheet_count == 0就直接报错:Please fill in the worksheet.
也就是说:
- 没有 worksheet 记录,连“进入判定”资格都没有。
但更关键的是下一步。
源码会把 point_id.worksheet_success_conditions 转成 Domain(...),然后在 worksheet 模型里搜索:
- success conditions
- 且
x_quality_check_id = self.id
只要命中,就 quality_wizard.do_pass();
否则就 quality_wizard.do_fail()。
这意味着 Odoo 的设计不是:
- 填了表 = 通过;
而是:
- 填了表之后,还要看表里的字段是否满足质量点定义的成功条件。
这是 worksheet 模式最容易被忽略、但也最有企业味的一层。
因为企业现场真正想要的通常不是“员工确实填过表”,而是:
- 数值在容差内;
- 必填项目已填;
- 检查项组合满足某种合格标准;
- 记录已经形成且满足放行规则。
所以 worksheet 在这里不是文档化,而是 规则化录入。
第四层:为什么还要有 quality.check.wizard 这一层后台对象
很多人第一次看这段源码都会问:
- 既然 worksheet 自己能保存、也能判定 pass/fail,为什么还要再建一个 wizard?
答案很简单:
因为 Odoo 要维护的不只是“这一张表填好了没”,而是“这一串质量检查现在走到哪一步”。
quality_control_worksheet/wizard/quality_check_wizard.py 给 quality.check.wizard 加了两个关键导航方法:
action_generate_next_window()action_generate_previous_window()
它们都会判断相邻 check 是否还是 worksheet 类型:
- 如果是 worksheet,就重新走
action_open_quality_check_wizard(next_check_id.id) - 否则才退回父类默认逻辑
这说明 wizard 的价值是:
- 记录当前检查位置;
- 统一接住上一条 / 下一条的跳转;
- 让 worksheet 和非 worksheet 检查能混排在同一条质检链里。
没有这个后台 wizard,前端 worksheet 表单根本不知道:
- 自己前面是谁;
- 后面是谁;
- 失败页应该退回哪一项;
- 最后一项完成后该怎么收尾。
所以这不是“多此一举的一层包装”,而是 质检导航状态机。
第五层:前端 controller 的真正职责,是“先保存,再带上下文回调后端判定”
quality_control_worksheet/static/src/views/quality_worksheet_fromview.js 很值得看。
这个 controller 在 validate() 里不是直接把窗口关掉,而是按下面顺序走:
saveButtonClicked()先保存 worksheet;- 取当前记录和 context;
- 如果存在
quality_wizard_id,就补一个: -from_worksheet = true - 然后 RPC 调:
-
quality.check.action_worksheet_check([record.x_quality_check_id.id], { context }) - 如果后端回了 action,就继续
doAction(action)。
这段代码的意思非常明确:
- 前端只负责把 worksheet 数据落稳;
- 真正的 pass/fail 和下一步跳转,仍然让后端说了算。
这是一种很稳的分工。
因为 worksheet 成功条件往往是业务规则,不该只写死在 JS 里。
如果把“保存后是否通过”完全交给前端判断,就会有几个问题:
- 规则不好复用;
- 容易被前端定制改坏;
- 非前端入口无法共享同一套判定。
而 Odoo 的方案是:
前端提交事实,后端判定语义。
这比“前端表单自己决定质检结果”靠谱得多。
第六层:进入工单场景后,quality_mrp_workorder_worksheet 又多包了一层“自动续跳”
如果只看 quality_control_worksheet,你会觉得它已经够完整了。
但到制造车间还不够。
quality_mrp_workorder_worksheet/models/quality.py 又重写了 action_worksheet_check(),核心逻辑是:
- 如果 context 里还没有
quality_wizard_id,先自己创建一个 wizard; - 调父类
super().with_context(quality_wizard_id=wizard.id).action_worksheet_check(); - 如果这条 check 绑定了
workorder_id,并且当前不是from_worksheet回来,直接: -return self._next() - 否则返回父类 action。
这段逻辑第一次看很绕,但它其实是在解决车间端一个很现实的问题:
工单流里不能每做完一步都停下来等人手动找“下一项”,而要尽量自动推进。
也就是说,企业版制造把 worksheet 质检又往前推了一步:
- 不只是让你填表;
- 还要把它变成 shop floor 节拍的一部分。
第七层:为什么 from_worksheet 这个 context 标记特别重要
上面那段逻辑里,from_worksheet 是关键小开关。
在前端 controller 里,validate 前会显式设置:
context['from_worksheet'] = true
然后后端 quality_mrp_workorder_worksheet 会判断:
- 如果是工单 check,且 不是
from_worksheet - 才调用
self._next()
这个小细节是在避免链路打架。
因为 worksheet 在工单里存在两种入口:
- 从 shop floor 点当前质检项进去;
- 在 worksheet 自己的表单里点 validate 回来。
如果不区分这两种入口,就很容易出现:
- 还没保存完 worksheet,工单游标已经跳了;
- 或者保存后重复跳两次;
- 或者 wizard 和工单当前 check 指针不同步。
所以 from_worksheet 的作用可以直译成:
这次调用是“从 worksheet 表单内部回来的收尾动作”,别再按外层工单入口重新跳一遍。
这是很典型的 Odoo 上下文协调技巧:
- 不是靠全局状态,
- 而是靠一次调用链里的上下文旗标,
- 去避免多层 UI 和状态机互相踩踏。
第八层:shop floor 前端为什么点一下 worksheet 就会自动走 doActionAndNext()
再看 quality_mrp_workorder_worksheet/static/src/quality_check.js。
它 patch 了工单 shop floor 里的 QualityCheck 组件,主要做三件事:
1. 给 worksheet 类型单独换图标
get icon() {
return this.type === "worksheet" ? "fa fa-file-text" : super.icon;
}
这只是识别层,告诉操作员这不是普通 pass/fail 检查,而是一张可填写的 worksheet。
2. 把 worksheet 纳入 passFailTypes
get passFailTypes() {
return [...super.passFailTypes, "worksheet"];
}
这一步非常关键。
它说明在 shop floor 看来,worksheet 虽然交互方式不同,但业务语义仍然属于“要给出 pass/fail 结论”的那一类检查。
3. 点击时不直接判 pass,而是先打开 wizard
clicked() {
return this.type === "worksheet"
? this.doActionAndNext("action_open_quality_check_wizard", "none")
: super.clicked();
}
注意这里第二个参数是:
"none"
这恰好说明点击 worksheet 时,前端不会自作主张把状态改成 pass。
它只是:
- 打开 worksheet / wizard;
- 临时保持
quality_state = none; - 等后端真正判定之后,再回到正确状态。
这也是企业版很克制的一点:
- 点击进入 ≠ 检查通过
- 填写保存 ≠ 立刻前端自行写 pass
真正结果仍以后台业务规则为准。
第九层:为什么这条链能支撑“上一条 / 下一条 / 失败回退”
quality_worksheet_fromview.js 里还有三组动作:
discard()next()previous()
它们分别回调:
quality.check.action_worksheet_discard()quality.check.wizard.action_generate_next_window()quality.check.wizard.action_generate_previous_window()
这就说明 worksheet 表单并不是一个“保存即结束”的死胡同,而是被嵌进了一个完整导航序列。
尤其是:
- 下一条可能还是 worksheet;
- 上一条也可能是 worksheet;
- 失败表单回退时又和普通上一条不完全一样。
所以只有把导航统一收进 wizard,系统才能保证各种检查类型混排时仍然顺滑。
这也解释了为什么企业现场常常会有一种感觉:
- worksheet 明明长得像独立表单,
- 但实际体验更像“工序里的一个步骤页”。
因为从源码设计上,它本来就是后者。
第十层:官方测试其实已经把产品意图写得很明白
quality_mrp_workorder_worksheet/tests/test_shopfloor.py 非常有代表性。
测试流程大致是:
- 建成品和组件;
- 建 workcenter、BOM、operation;
- 建一个
test_type = worksheet的quality.point; - 给它挂
worksheet_template_id; - 创建并确认 MO;
action_assign()、button_plan();- 打开
/odoo/shop-floor跑 tour; - 最后断言:
-
mo.workorder_ids.state == "done"
这个测试最有价值的地方不在“能跑通”,而在于它明确表达了产品假设:
worksheet 质检不是工单之外的补充动作,而是可以直接把工单推到 done 的标准执行链一部分。
换句话说,官方并不是把 worksheet 当成“可选记录层”,而是把它当成 shop floor 正常完工路径里的一个节点。
这套设计到底解决了什么问题
我觉得它主要解决了三个企业场景难题。
1. 让“填表留痕”和“质量放行”不再脱节
很多工厂会要求:
- 既要填检查记录;
- 又要按规则决定是否合格;
- 还要让系统知道下一步能不能继续。
如果 worksheet 只是附件或备注,这三件事会完全分离。
Odoo 把 worksheet 绑到 quality.check,再用 success conditions 做 pass/fail,这就把:
- 记录
- 判定
- 放行
三者串起来了。
2. 让 shop floor 端保持节拍,不让人迷路
车间端最怕的是“填完了但不知道下一步在哪”。
wizard + _next() + next/previous 导航,本质上是在把质检链做成可连续执行的步骤流,而不是一堆分散表单。
3. 让规则定义留在后端,而不是散在前端页面逻辑里
真正的合格条件在 worksheet_success_conditions,不是写死在 JS。
这意味着:
- 规则更集中;
- 行为更可审计;
- 前端换界面时不必重写一整套判定。
实施和二开时最容易踩的坑
坑 1:把 worksheet 当“说明文档”而不是“判定载体”
如果你只关心表单长什么样,不关心 worksheet_success_conditions,就会出现:
- 表单填得很漂亮;
- 但系统永远判不了 pass;
- 或者总是 fail。
坑 2:前端自定义时误把点击进入当作自动通过
shop floor patch 很明确:worksheet 点击时状态先保持 none。
如果你自定义前端时偷懒,直接把 worksheet 点开就当 pass,会把整条质量链搞坏。
坑 3:忘记保留 quality_wizard_id
没有这个上下文,worksheet 页面就只剩一张孤立表单:
- 下一条 / 上一条失效;
- 失败回退失效;
- 整条检查链上下文丢失。
坑 4:忽略 from_worksheet,导致跳转重复或错位
这个坑很隐蔽。
一旦你自定义保存逻辑,却没把 from_worksheet 带回去,就可能看到:
- 自动跳两次;
- 当前工单游标错位;
- wizard 里是 A,工单里高亮却是 B。
坑 5:只测单一 worksheet,不测“worksheet + 普通检查混排”
官方之所以要用 wizard 来兜底,正是为了兼容混排。
所以二开测试不要只测:
- 只有一张 worksheet 能不能保存;
还要测:
- worksheet 前后接 pass/fail;
- 失败回退;
- 连续多张 worksheet;
- 工单 done 前的最后一跳。
我对这段设计的判断
我挺喜欢这套实现,因为它没有把 worksheet 做成一个“超级大对象”,而是很克制地复用了现有质量链。
官方做的事其实不多:
- 给 worksheet 一个真实业务记录;
- 用 wizard 保持检查链位置;
- 用 success conditions 做通过判定;
- 在工单场景下再补一层自动续跳。
但就是这几个点拼起来,车间端体验就从:
- “打开一个表填完再自己找回去”
变成了:
- “这是工单质检流里的标准一步,填完系统自己接着走”。
这才是企业版真正值钱的地方:
不是多一张表,而是把表、规则、状态和节拍合成一条执行链。
一句话总结
Odoo 企业版的工单 worksheet 质检,本质上不是“嵌入式表单”,而是“由 worksheet 记录承载数据、由 quality wizard 承载导航、由 success conditions 承载判定、由工单
_next()承载节拍推进”的组合状态机。
DISCUSSION
评论区