第十八章 工作流与状态机
篇别:第三篇 数据库与数据管理
本章学习目标
- 能用
Selection+ 专用方法 实现 可审计的状态迁移。 - 能配合
tracking与 chatter 保留状态变更痕迹。 - 了解 审批链 与
mail.activity的结合方式及局限。 - 能讨论 并发点击、重复提交 下的幂等与锁策略(基础级)。
导读:状态机是业务规则的骨架
订单、借阅单、请假单等常有 草稿 → 提交 → 完成/取消 路径。用 单一字段 state + 显式 Python 方法 切换,比散落在多处的 write({'state':...}) 更易 维护、测试与审计。
18.1 状态字段设计
18.1.1 知识要点
Selection:选项 稳定字符串 存库;勿随意改 key,否则历史数据与报表需迁移。default:新建默认状态。tracking=True(需mail.thread):变更写入 chatter,利于合规。group_expand(列表/看板分组用,可选):控制空阶段是否显示。
18.1.2 案例
state = fields.Selection([
('draft', '草稿'),
('submitted', '已提交'),
('approved', '已批准'),
('running', '进行中'),
('done', '完成'),
('cancel', '已取消'),
], string='状态', default='draft', required=True, tracking=True)
18.1.3 截图占位
![]()
18.1.4 本节练习
- 实操:修改
Selection的 显示名(value 不变)与 value(改变 key)各做一次,观察历史消息差异。 - 简答:为何生产库改
Selectionkey 需要迁移脚本? - 判断:
required=True阻止将state设为False。( )
参考答案提示:3. 对(通常无空状态)。
18.2 状态机模式
18.2.1 知识要点
- 每个合法转移 对应
action_*方法:内部校验 前置状态、角色、业务条件。 - 统一出口:避免在 十个按钮 里直接
write;例外情况(导入、迁移)单独方法并打日志。 - 拒绝:
UserError/ValidationError给出 可翻译 说明。
18.2.2 案例
def action_submit(self):
for rec in self:
if rec.state != 'draft':
raise UserError(_('仅草稿可提交'))
rec.state = 'submitted'
def action_approve(self):
for rec in self:
if rec.state != 'submitted':
raise UserError(_('仅已提交可审批'))
rec.state = 'approved'
def action_cancel(self):
for rec in self:
if rec.state in ('done', 'cancel'):
raise UserError(_('当前状态不可取消'))
rec.state = 'cancel'
视图:header 中按钮 invisible 与状态一致。
<button name="action_submit" type="object" string="提交" invisible="state != 'draft'"/>
18.2.3 截图占位

18.2.4 本节练习
- 简答:为何避免多处直接
write({'state':...})? - 实操:增加 「驳回」:
approved→draft并message_post说明。 - 简答:
super().write钩子中拦截非法state变更的利弊?
参考答案提示:1. 难审计、易绕过校验、难国际化错误信息。
18.3 审批流程(新增)
18.3.1 知识要点
- 活动(activity):
mail.activity.mixin;activity_schedule指定 类型、用户、截止日期、备注。 - 多级审批:上一节点
done后schedule下一负责人;或用 审批/签署 企业模块。 - 与状态同步:建议在
action_*里同时改state与 关闭/创建活动,避免「状态已批但活动仍在」。
18.3.2 案例
def action_submit(self):
for rec in self:
if rec.state != 'draft':
raise UserError(_('仅草稿可提交'))
rec.state = 'submitted'
manager = self.env.ref('base.user_admin') # 演示:换为真实组用户
rec.activity_schedule(
'mail.mail_activity_data_todo',
user_id=manager.id,
note=_('请审批此借阅申请'),
)
18.3.3 截图占位

18.3.4 本节练习
- 实操:提交后 经理组 某用户收到待办;审批按钮清除待办并
state=approved。 - 简答:activity 与 message 在通知渠道上的差异?
- 情景:审批人离职,活动挂死——如何设计 代理或超时升级?
18.4 并发与幂等(基础)
18.4.1 知识要点
- 两用户 同时点「确认」:可能 两次
write;若业务要求 仅一次生效,可用 SQLSELECT FOR UPDATE、唯一约束、或 状态检查在单事务内完成。 - 双点提交:前端 禁用按钮 + 后端 幂等(重复请求返回相同结果)。
18.4.2 本节练习
- 简答:
write_date乐观锁思路(比较前后时间戳)适用场景? - 拓展:搜索 Odoo
_mail_post_access或 bus 通知与状态刷新关系(可选)。
本章综合练习
- 绘图:状态图含 draft / submitted / approved / done / cancel 及允许转移箭头。
- 并发:描述 两用户同时提交 时的一种 数据异常 与一种 防护手段。
- 合规:哪些行业需要 不可篡改的状态日志?
mail.thread是否足够? - 综合:为 借阅单 写 状态 + 活动 + 超时提醒(cron) 的文字方案(不必全码)。
本章对应白皮书目录:第十八章 工作流与状态机。