EH HR Approval Engine
Reusable multi-step approval chains for any HR record, with self-approval blocked at two layers.
Why this module
EH HR Approval Engine
One engine, every record
Any model inherits eh.hr.approvable.mixin and gains submit-for-approval. Chains are rows in eh.hr.approval.chain, not Python re-written per module, so a new approvable document is configuration rather than code.
Self-approval cannot slip through
The subject employee, the record owner and the real submitter are barred from deciding, even when they hold an approver group. The block is enforced at the decide call and re-enforced by a constraint on every decision row, including direct ORM and sudo writes.
Scoped per company and per team
Global record rules confine chains, steps, requests and decisions to the user's companies. Self-service employees see only their own requests, team managers only their reports, and officers keep the company-wide view.
Day in the life
A leave request needs two sign-offs
An employee submits a record that inherits the approvable mixin. The engine opens a request against the configured chain and lands it on the first step. The line manager approves from the inbox, the serial chain advances to the next step, and the second approver signs off. The engine closes the request as approved and fires the follow-up transition on the source record. Every decision is written with who, when and any comment. If a step sits untouched past its escalation window, the hourly cron notifies the step approvers and HR admins once, then flags the request so it is not re-nagged on the next run.
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.
A user who is both an officer and the subject of the request is in the authorized set yet still blocked. The guard runs at decide() and again as an @api.constrains on eh.hr.approval.decision, so an officer creating a decision row directly, or a sudo call, is rejected too.
Because the engine opens requests under sudo, create_uid is the superuser. The real submitter is captured before the sudo and stored in submitted_by, so a user who raised a request about another employee cannot then approve their own submission, even holding an approver group.
Escalation runs hourly but notifies once per step: the request sets an escalated flag and escalated_at after firing, and the flag resets only when the step advances, so approvers are not re-notified every run.
A conditional chain with no policy_code, or whose policy evaluation raises, never auto-advances and stays pending. The safe default is to wait for a human, not to wave the step through.
The parallel all-of strategy is satisfied only when the approving users' groups cover every required approver group on the step, computed by set coverage rather than a raw vote count, so one over-privileged approver cannot stand in for the rest.
Global rules confine every approval model to the user's companies. A company B officer cannot search or read a company A request; a direct read raises AccessError.
On close the engine fires the follow-up transition by convention, but if no matching transition exists out of the record's current state it leaves the record untouched and the request state itself stands as the recorded outcome.
What is inside
Built to do the job, end to end.
- Models it adds. eh.hr.approval.chain, eh.hr.approval.step, eh.hr.approval.request and eh.hr.approval.decision, plus the eh.hr.approvable.mixin that makes a record submittable and the eh.hr.approval.engine ORM-callable surface.
- Approver resolution. Each step lists static approver groups and an optional dynamic rule that resolves the subject's line manager, department manager or any HR officer at runtime, so chains follow the org chart without hard-coded user IDs.
- Approval inbox. A backend Owl component lists the user's pending requests across every approvable model and lets them approve or reject with a comment in place, or drill into the source record. Visibility is enforced by the record rules, not by the client.
- Escalation cron. An hourly ir.cron calls _cron_escalate, which finds pending requests whose current step has been open past its escalation_hours window and notifies the step approvers and HR admins through chatter and the notification engine when present.
Honest about the edges
What this does not do, so nothing surprises you.
- No delegation or out-of-office routing. Approver resolution is static groups plus line manager, department manager or HR officer; there is no delegate-while-away mechanism in this module.
- Escalation notifies, it does not reassign. An overdue step alerts the step approvers and HR admins once; it does not auto-advance the request or hand the step to a different approver.
- The dynamic resolver targets HR org structure (line and department managers, HR officers). Approvals for non-employee records resolve only through static approver groups.
- Conditional steps depend on the policy engine. A conditional chain needs a valid eh.hr.policy code; without one the step never auto-advances by design.
- Built for Odoo 18 Community and depends on eh_hr_core and eh_hr_engine_workflow. It is an engine other modules consume, not a standalone end-user app.
Odoo 18 approval workflow, Odoo HR approval chain, multi-step approval Odoo, parallel approval Odoo, conditional approval policy, manager approval Odoo Community, self-approval prevention, approval escalation cron, multi-company approval, approval audit trail, HR approval inbox, Odoo approval engine, reusable approval mixin, department manager approval, ERP Heritage HR
Please log in to comment on this module