EH HR Approval Engine
One reusable, multi-step approval engine that any HR record can submit to, with self-approval blocked and every decision on the record.
Why this module
EH HR Approval Engine
One engine, every workflow
Instead of re-coding a manager-approval state machine in each module, a record inherits eh.hr.approvable.mixin, declares a chain code, and calls action_submit_for_approval. The engine creates the request, resolves approvers, advances the steps and fires the follow-up transition when the chain closes.
Self-approval cannot happen
The subject employee, the record owner and the real submitter are all blocked from deciding, even if they hold an approver group. The block is enforced in decide(), re-checked by a model constraint on every create path including sudo, and filtered again in the engine, so it holds however a decision row is produced.
Chains are data, not Python
An administrator defines chains, steps, approver groups, dynamic approver rules and per-step escalation hours from the Approval Chains screen. Strategy choices cover serial order, any one approver, all approver groups, or a conditional policy whose no-code DSL decides when a step is satisfied.
Day in the life
A leave request that approves itself, correctly
An employee submits a record for sign-off. The engine opens a pending request against the configured chain, records who submitted it before any privilege escalation, and resolves the first step's approvers from its groups plus the live line or department manager. An authorized approver clicks Approve from the request list; the engine counts only valid, non-subject decisions, advances a serial chain to the next step, and on the final approval fires the record's follow-up transition. If a step sits unactioned past its escalation window, the hourly cron notifies the step approvers and HR admins once, then flags the request so it is not re-notified until it moves on. Every decision, with its approver, comment and timestamp, stays on the request and in its chatter.
Edge cases
The cases most modules quietly ignore.
In the shipped code today, each one a place where a cheaper module silently does the wrong thing.
The subject employee's user, the underlying record's own user_id, and the captured submitter are all excluded from approving. Because the engine opens requests under sudo, the real submitter is persisted in submitted_by before the sudo so create_uid being the superuser cannot be used to slip a self-approval through.
A model constraint on eh.hr.approval.decision re-validates authorization and the no-self-approval rule on every create, so a direct ORM or sudo create that bypasses decide() still raises ValidationError rather than recording an illegitimate vote.
Global record rules scope chains, steps, requests and decisions to the user's allowed companies; an officer in one company cannot read or directly access another company's requests, verified by a cross-company isolation test.
Each overdue step escalates once. The request carries an escalated flag and timestamp so the hourly cron does not re-notify on every run, and the flag resets to False when the request advances to a new step.
A conditional chain with no policy_code, or whose policy raises, never auto-advances and stays pending. The unconfigured case is the safe default rather than an accidental approval.
Self-service employees see only requests they raised or are the subject of; team managers see only requests about their direct reports plus their own; officers and admins keep the company-wide view, combined through OR-widening rules.
An all-of step is only satisfied when the union of approver groups voting covers every required approver group on the step, computed from the actual group membership of the users who approved, not just a count.
A single reject decision closes the request as rejected and attempts the record's refuse transition; if no such transition exists out of the current state the record is left in place and the rejected request stands as the audit record.
What is inside
Built to do the job, end to end.
- Five models. eh.hr.approvable.mixin (submit-for-approval surface), eh.hr.approval.chain, eh.hr.approval.step, eh.hr.approval.request and eh.hr.approval.decision. The engine itself is both a service for Python callers and an ORM-callable AbstractModel.
- Four chain strategies. Serial steps in order, parallel any-of (any approver clears the step), parallel all-of (every approver group must approve), and conditional, where an eh.hr.policy DSL expression with the approval count and subject employee in context decides when the step is met.
- Static and dynamic approvers. Each step lists approver groups and optionally resolves a dynamic approver at runtime: the subject employee's line manager, their department manager, or any HR officer. The authorized set is the union of both.
- Escalation cron. An hourly ir.cron runs _cron_escalate over pending requests, escalates any step overdue past its escalation_hours to the step approvers and HR admins, posts to chatter, and routes through the notification engine when that module is present.
- Backend views. Admin Approval Chains form with inline step editing, an Approval Requests list and form under the security menus, and decisions tracked on the request through mail.thread. A pending-approvals inbox component ships in the assets bundle.
- Security model. Access rights for HR admin, officer and self-service groups plus global per-company rules and role-scoped read rules, so the engine ships locked down rather than wide open.
Honest about the edges
What this does not do, so nothing surprises you.
- This is an engine and configuration layer. It provides the chains, requests, decisions and the submit-for-approval mixin; the records that submit to it are supplied by the modules that inherit the mixin, not by this module on its own.
- There is no out-of-office delegation or substitute-approver reassignment feature. The only delegation concept in the code is the guard that prevents a user who submitted a request on someone else's behalf from approving their own submission.
- Escalation notifies the step approvers and HR admins; it does not auto-approve or auto-skip an overdue step. The request stays pending until an authorized approver acts.
- The conditional strategy depends on the policy engine. Without a configured eh.hr.policy code, a conditional chain never auto-advances by design.
- Depends on eh_hr_core and eh_hr_engine_workflow and is intended to run as part of the EH HR Platform rather than as a standalone HR application.
odoo 17 approval workflow, odoo hr approval chain, multi step approval odoo, approval matrix odoo, conditional approval policy, approval escalation cron, self approval prevention, multi company hr approvals, dynamic manager approver, approval audit trail odoo
Please log in to comment on this module