Worksheet 质检

Odoo 企业版工单 Worksheet 质检为什么不是“打开表单填完就行”:后台 wizard、成功条件与自动跳转链路讲透

很多人第一次在 Odoo 企业版车间端看到 worksheet 质检,会以为它只是把一张表单嵌进工单界面。但从 `quality_control_worksheet`、`quality_mrp_workorder_worksheet` 与 shop floor 前端补丁来看,官方真正做的是一条“双层状态机”:前台打开 worksheet,后台挂着 quality wizard,再根据成功条件决定 pass/fail,并自动跳到下一道质检或下一步工单。

企业 制造
进阶 开发者 4 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

在 Odoo 企业版里,工单里的 worksheet 质检不是“点开一张表、填完保存”这么简单

它实际上是一条三段式链路:

  1. 前台先把 worksheet 表单弹出来,让操作员填写;
  2. 后台同时悄悄创建一个 quality.check.wizard,当作质检游标和上下文容器;
  3. 保存 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_worksheet
  • enterprise/quality_mrp_workorder_worksheet
  • enterprise/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,而是先:

  1. check_id.action_quality_worksheet() 生成 worksheet 的 action;
  2. 再创建一个 quality.check.wizard,写入: - check_ids - current_check_id
  3. 把这些上下文塞回 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.pyaction_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.pyquality.check.wizard 加了两个关键导航方法:

  • action_generate_next_window()
  • action_generate_previous_window()

它们都会判断相邻 check 是否还是 worksheet 类型:

  • 如果是 worksheet,就重新走 action_open_quality_check_wizard(next_check_id.id)
  • 否则才退回父类默认逻辑

这说明 wizard 的价值是:

  1. 记录当前检查位置;
  2. 统一接住上一条 / 下一条的跳转;
  3. 让 worksheet 和非 worksheet 检查能混排在同一条质检链里。

没有这个后台 wizard,前端 worksheet 表单根本不知道:

  • 自己前面是谁;
  • 后面是谁;
  • 失败页应该退回哪一项;
  • 最后一项完成后该怎么收尾。

所以这不是“多此一举的一层包装”,而是 质检导航状态机


第五层:前端 controller 的真正职责,是“先保存,再带上下文回调后端判定”

quality_control_worksheet/static/src/views/quality_worksheet_fromview.js 很值得看。

这个 controller 在 validate() 里不是直接把窗口关掉,而是按下面顺序走:

  1. saveButtonClicked() 先保存 worksheet;
  2. 取当前记录和 context;
  3. 如果存在 quality_wizard_id,就补一个: - from_worksheet = true
  4. 然后 RPC 调: - quality.check.action_worksheet_check([record.x_quality_check_id.id], { context })
  5. 如果后端回了 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(),核心逻辑是:

  1. 如果 context 里还没有 quality_wizard_id,先自己创建一个 wizard;
  2. 调父类 super().with_context(quality_wizard_id=wizard.id).action_worksheet_check()
  3. 如果这条 check 绑定了 workorder_id,并且当前不是 from_worksheet 回来,直接: - return self._next()
  4. 否则返回父类 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 在工单里存在两种入口:

  1. 从 shop floor 点当前质检项进去;
  2. 在 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

它只是:

  1. 打开 worksheet / wizard;
  2. 临时保持 quality_state = none
  3. 等后端真正判定之后,再回到正确状态。

这也是企业版很克制的一点:

  • 点击进入 ≠ 检查通过
  • 填写保存 ≠ 立刻前端自行写 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 非常有代表性。

测试流程大致是:

  1. 建成品和组件;
  2. 建 workcenter、BOM、operation;
  3. 建一个 test_type = worksheetquality.point
  4. 给它挂 worksheet_template_id
  5. 创建并确认 MO;
  6. action_assign()button_plan()
  7. 打开 /odoo/shop-floor 跑 tour;
  8. 最后断言: - 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

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。