第十八章 工作流与状态机

篇别:第三篇 数据库与数据管理

本章学习目标

  • 能用 Selection + 专用方法 实现 可审计的状态迁移
  • 能配合 trackingchatter 保留状态变更痕迹。
  • 了解 审批链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 Chatter 中状态变更

18.1.4 本节练习

  1. 实操:修改 Selection显示名(value 不变)与 value(改变 key)各做一次,观察历史消息差异。
  2. 简答:为何生产库改 Selection key 需要迁移脚本?
  3. 判断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 header 按钮与 statusbar

18.2.4 本节练习

  1. 简答:为何避免多处直接 write({'state':...})
  2. 实操:增加 「驳回」approveddraftmessage_post 说明。
  3. 简答super().write 钩子中拦截非法 state 变更的利弊?

参考答案提示:1. 难审计、易绕过校验、难国际化错误信息。


18.3 审批流程(新增)

18.3.1 知识要点

  • 活动(activity)mail.activity.mixinactivity_schedule 指定 类型、用户、截止日期、备注
  • 多级审批:上一节点 doneschedule 下一负责人;或用 审批/签署 企业模块。
  • 与状态同步:建议在 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 活动链与日历

18.3.4 本节练习

  1. 实操:提交后 经理组 某用户收到待办;审批按钮清除待办并 state=approved
  2. 简答:activity 与 message 在通知渠道上的差异?
  3. 情景:审批人离职,活动挂死——如何设计 代理或超时升级

18.4 并发与幂等(基础)

18.4.1 知识要点

  • 两用户 同时点「确认」:可能 两次 write;若业务要求 仅一次生效,可用 SQL SELECT FOR UPDATE唯一约束、或 状态检查在单事务内完成
  • 双点提交:前端 禁用按钮 + 后端 幂等(重复请求返回相同结果)。

18.4.2 本节练习

  1. 简答write_date 乐观锁思路(比较前后时间戳)适用场景?
  2. 拓展:搜索 Odoo _mail_post_access 或 bus 通知与状态刷新关系(可选)。

本章综合练习

  1. 绘图:状态图含 draft / submitted / approved / done / cancel 及允许转移箭头。
  2. 并发:描述 两用户同时提交 时的一种 数据异常 与一种 防护手段
  3. 合规:哪些行业需要 不可篡改的状态日志mail.thread 是否足够?
  4. 综合:为 借阅单状态 + 活动 + 超时提醒(cron) 的文字方案(不必全码)。

本章对应白皮书目录:第十八章 工作流与状态机。