BambooForge Labs

Approval Workflow for Odoo Community

Multi-step approval workflow for Odoo Community, now with rules on any model: a condition picks the records, the ladder decides, a gate holds the confirm button, and the decision sets a field, schedules an activity, sends a template, runs a server action or calls a signed webhook. SLA clocks, parallel steps, a decision trail nobody can edit.

Buy on the Odoo Apps StoreOpen the live demoHuman Resources€89Community & Enterprise

Available for Odoo 16.0, Odoo 17.0, Odoo 18.0, Odoo 19.0, Odoo 20.0. Technical name bambooforge_approvals.

Odoo 16.0Odoo 17.0Odoo 18.0Odoo 19.0Odoo 20.0
Screens and walkthrough for
Transcript of the walkthrough (Odoo 20)

Approval Workflow for Odoo Community. Multi-step approval workflow for Odoo Community, now with rules on any model: a condition picks the records, the ladder decides, a gate holds the confirm button, and the decision sets a field, schedules an activity, sends a template, runs a server action or calls a signed webhook. SLA clocks, parallel steps, a decision trail nobody can edit. Said before you buy, because it is the half that decides whether the app fits: In the next few minutes: approval workflow for odoo community, then a type, and the ladder under it, then the amount matrix, then the mailbox, then nobody approves their own request.

Then what this app deliberately does not do, so you can tell before you buy whether it fits. Everything lives under Approvals then Waiting on Me then All Requests then Delegation. 1. Copy bambooforge_approvals into your addons path and update the app list, or install the ZIP from the Odoo Apps Store.

2. Install Approval Workflow for Odoo Community. 3. Open Approvals - its menus are Waiting on Me, All Requests, Delegation, Configuration.

Approval Workflow for Odoo Community. Odoo Community has no approvals app. What people do instead is an email thread — and six weeks later nobody can say who agreed to the fifteen thousand, or whether the number they agreed to is the number that was spent. This is multi-step approvals for Community, built so that the trail it leaves is worth reading a year later.

A type, and the ladder under it. Approvals › Configuration › Approval Types. A type is a kind of thing that needs signing off — spending, time off, a new starter, access to a system. It says what its form asks for (a period, an amount, a quantity, a contact, a reference, an attachment), and carries the ladder of approvers underneath.

The amount matrix. A step can carry a floor: From amount. The manager signs everything, finance from 2,000, the board from 20,000 - three steps on one type, not three types. A request below a floor skips that rung, and the rung stays on the trail marked Skipped, with the floor written next to it, so "why did finance not sign this" has an answer six months later.

The floor is read in the company currency, converted on the day the request is submitted: a rate that moves while the request waits must not move the request between ladders. A request that no step applies to cannot be submitted - a ladder nobody has to climb would come out the other end approved, and the module says so instead. A floor on a type that never asks for an amount is refused for the same reason: it would skip its step on every request, silently, for ever. The mailbox.

When a step opens, everybody who can clear it gets the activity - and, if the type says Email approvers, a mail with an Approve button in it. The button is a signed link: it is bound to that person, that step and the word approve, it asks them to log in as themselves, and it stops working the moment the step is decided. Forwarding the mail gives the next reader a link that answers "this was sent to somebody else". Refusing needs a reason, so the mail's second link opens the request instead.

Nobody approves their own request. Not by being in the approving group, not by being their own manager, not through a delegation, and not by being added by hand afterwards. The requester is removed from every step's approvers when the ladder is built, and the same rule is repeated as a database constraint — because a rule enforced only where the buttons are is a rule that holds until somebody imports a file. If removing the requester leaves a step with nobody on it, the request cannot be submitted and says so.

That is better than the alternative, which is a request that sits in a queue nobody can clear. The ladder is frozen when it is submitted. Each step is copied onto the request at submission, with the people it resolved to at that moment. A reorganisation, somebody leaving the approving group, or an administrator editing the type afterwards does not rewrite who was asked.

An approval trail that changes retrospectively is not a trail. It is a report of what the org chart looks like today. What was approved cannot change under the approval. Once anybody has said yes, the amount, the currency, the dates, the quantity, the contact and the reference are locked.

Change them and the approval that was granted quietly becomes an approval of something else — which is the single most common way one of these systems turns out to have been decorative. To change them, withdraw the request and raise it again. That costs somebody thirty seconds and leaves a trail that is true. The decision log is read-only to everybody.

Including the approvers. Nobody has write access to bf.approval.entry at all; every change goes through the workflow, which writes it under sudo. An approval log that approvers can edit answers "who agreed to this?" with whatever suits today. Delegation ends, and does not pass on.

Approvals › Delegation. Hand your approvals to somebody while you are away. It has an end date, because a delegation without one is how somebody is still signing things off two years after the holiday. It cannot be chained.

If B is covering for A and then B goes away and hands it to C, C ends up with A's say — and nobody chose C. That is refused when it is set up, not discovered afterwards. Confidential types. A confidential type is seen only by the person who raised it, the people it is waiting on, and an approval administrator.

Use it for anything about money or people. A confidential type cannot be delegated, and that is a deliberate pairing rather than a missing feature: lending somebody your say would lend them the reading of it, to somebody nobody chose. The complete manual is at docs dot bambooforge dot dev, the demo database is linked from the store page, and support answers within twenty-four hours. Links are in the description.

Amount matrix - bambooforge_approvals
Amount matrix
Decisions - bambooforge_approvals
Decisions
Ladder - bambooforge_approvals
Ladder
Portal - bambooforge_approvals
Portal
Questions - bambooforge_approvals
Questions
Queue - bambooforge_approvals
Queue
Request - bambooforge_approvals
Request
Rules - bambooforge_approvals
Rules

Key facts

What it is
Multi-step approval workflow for Odoo Community, now with rules on any model: a condition picks the records, the ladder decides, a gate holds the confirm button, and the decision sets a field, schedules an activity, sends a template, runs a server action or calls a signed webhook. SLA clocks, parallel steps, a decision trail nobody can edit.
For
Human Resources
Odoo versions
16.0, 17.0, 18.0, 19.0, 20.0 - Community and Enterprise
Runs on
Odoo.sh or Odoo you host yourself - not Odoo Online, which accepts no third-party apps
Price
89 EUR, one-time, on the Odoo Apps Store
Technical name
bambooforge_approvals
Not for
Said before you buy, because it is the half that decides whether the app fits:; Approve anything by itself: a request that no step applies to is refused, not passed.; Let a requester approve their own request - not as a member of the group, not as their own manager, not through a delegation.; Watch records on its own: a rule raises a request from the Action menu or from the button it blocks, not from a save. A record edited after its approval is not re-approved unless you raise it again.
Manual
https://docs.bambooforge.dev/bambooforge_approvals/
Live demo
https://demo20.bambooforge.dev/odoo/action-bambooforge_approvals.action_bf_approval_todo
Store page
https://apps.odoo.com/apps/modules/20.0/bambooforge_approvals
Manual last updated
2026-09-25

Approval Workflow for Odoo Community

Odoo Community has no approvals app. What people do instead is an email thread — and six weeks later nobody can say who agreed to the fifteen thousand, or whether the number they agreed to is the number that was spent.

This is multi-step approvals for Community, built so that the trail it leaves is worth reading a year later.

A type, and the ladder under it

Approvals › Configuration › Approval Types.

A type is a kind of thing that needs signing off — spending, time off, a new starter, access to a system. It says what its form asks for (a period, an amount, a quantity, a contact, a reference, an attachment), and carries the ladder of approvers underneath.

A step is a group, some named people, the requester's manager, or all three at once — plus how many of them have to say yes. Steps run in order: the second opens when the first is satisfied. One refusal at any step refuses the request, with a reason that the person who raised it reads.

Two worked examples are installed, built on groups every database already has, so they do something on the first run.

The amount matrix

A step can carry a floor: From amount. The manager signs everything, finance from 2,000, the board from 20,000 - three steps on one type, not three types. A request below a floor skips that rung, and the rung stays on the trail marked Skipped, with the floor written next to it, so "why did finance not sign this" has an answer six months later.

The floor is read in the company currency, converted on the day the request is submitted: a rate that moves while the request waits must not move the request between ladders. A request that no step applies to cannot be submitted - a ladder nobody has to climb would come out the other end approved, and the module says so instead. A floor on a type that never asks for an amount is refused for the same reason: it would skip its step on every request, silently, for ever.

The mailbox

When a step opens, everybody who can clear it gets the activity - and, if the type says Email approvers, a mail with an Approve button in it. The button is a signed link: it is bound to that person, that step and the word approve, it asks them to log in as themselves, and it stops working the moment the step is decided. Forwarding the mail gives the next reader a link that answers "this was sent to somebody else". Refusing needs a reason, so the mail's second link opens the request instead.

Nobody approves their own request

Not by being in the approving group, not by being their own manager, not through a delegation, and not by being added by hand afterwards. The requester is removed from every step's approvers when the ladder is built, and the same rule is repeated as a database constraint — because a rule enforced only where the buttons are is a rule that holds until somebody imports a file.

If removing the requester leaves a step with nobody on it, the request cannot be submitted and says so. That is better than the alternative, which is a request that sits in a queue nobody can clear.

The ladder is frozen when it is submitted

Each step is copied onto the request at submission, with the people it resolved to at that moment. A reorganisation, somebody leaving the approving group, or an administrator editing the type afterwards does not rewrite who was asked.

An approval trail that changes retrospectively is not a trail. It is a report of what the org chart looks like today.

What was approved cannot change under the approval

Once anybody has said yes, the amount, the currency, the dates, the quantity, the contact and the reference are locked. Change them and the approval that was granted quietly becomes an approval of something else — which is the single most common way one of these systems turns out to have been decorative.

To change them, withdraw the request and raise it again. That costs somebody thirty seconds and leaves a trail that is true.

The decision log is read-only to everybody

Including the approvers. Nobody has write access to bf.approval.entry at all; every change goes through the workflow, which writes it under sudo.

An approval log that approvers can edit answers "who agreed to this?" with whatever suits today.

Each decision records who was asked and who actually clicked — they differ when somebody was standing in.

Delegation ends, and does not pass on

Approvals › Delegation. Hand your approvals to somebody while you are away. It has an end date, because a delegation without one is how somebody is still signing things off two years after the holiday.

It cannot be chained. If B is covering for A and then B goes away and hands it to C, C ends up with A's say — and nobody chose C. That is refused when it is set up, not discovered afterwards.

A stand-in never inherits the right to approve their own request either.

Confidential types

A confidential type is seen only by the person who raised it, the people it is waiting on, and an approval administrator. Use it for anything about money or people.

A confidential type cannot be delegated, and that is a deliberate pairing rather than a missing feature: lending somebody your say would lend them the reading of it, to somebody nobody chose.

It chases

Give a type a number of days and any step that has been waiting longer puts an activity on everybody who can clear it — an activity, not an email, because an activity is the only thing in Odoo that shows up as a number somebody has to clear.

An approval queue nobody is reminded of is a drawer.

Rules: any record climbs a ladder

A type is a ladder. A rule (Configuration → Approval Rules) says which records climb it:

  • the model - sales orders, purchase orders, invoices, transfers, or anything with a form;

  • the condition, written in the same editor as a filter: amount above 5,000, margin below 20 %, a tag on the customer, a company;

  • the ladder it climbs, and the amount field on the record that the steps' floors are compared with, the contact field that names the contact on the request;

  • what the record's own button is called, when the approval should stand in front of it (Blocks, below).

Every model with an active rule gets Request approval in its Action menu. Pick the records, choose it, and a request is raised for each one on every rule that matches - named after the type and the record, carrying the amount and the contact, linked both ways: the request has a Record button, the record's chatter says it was sent. A record with a request already open is left alone.

What the decision does

On the rule, two lists: When approved and When refused. Each line is one of:

  • Set a field on the record - a stage, a flag, a text; the value is typed as text and coerced to the field.

  • Schedule an activity for a named user, with a summary.

  • Send a mail template of the record's model.

  • Run a server action of the record's model.

  • Post a message on the record.

  • Call a webhook: a JSON body with the request, the record and the rule, signed with HMAC-SHA256 in X-BF-Signature when a secret is set, so the receiver can check it is yours.

They run in order, the moment the request is decided. One that fails - a webhook that is down, a template that errors - is written to the request's chatter and never undoes the decision it follows.

The gate in front of a button

A rule whose Blocks names the record's own button - action_confirm on a sales order, button_confirm on a purchase, action_post on an invoice, button_validate on a transfer - stands in front of that button:

  • the first press raises the request and stops, saying so and naming the step it went to;

  • while the request waits, the button stops and names the step;

  • once it is approved, the button works;

  • a refused request is named when somebody tries again.

The request is written before the button is stopped, on a transaction of its own, so the stop cannot roll it back. Four small modules ship alongside this app and install by themselves when the other app is there: the gates for Sales, Purchase, Invoicing and Inventory. No context flag skips a gate - a browser can send any flag with a call. The seed runner that writes demo history carries a server-side marker instead (SEEDING in bf_approval_rule, an object no request can produce), and the gate steps aside for that alone.

A type asks its own questions

Period, amount, quantity, contact, reference and attachment are the standard fields a type can ask for. On the type's Questions tab you add the rest: a cost centre as a choice, a laptop model as text, a start date, a yes/no, a number, a contact, a user. Each question has a label, a type, whether it is required, and a hint.

A request of that type carries one row per question, right above the details, typed by the question: a date picker for a date, a dropdown for a choice, a contact picker for a contact. A required question stops the request being submitted until it is answered - the message names the question. The answers travel with the request, are read-only once it is submitted, and are on the decision trail with everything else. From code, request._bf_answers() gives them as a dictionary of label to text, for a mail template or a webhook.

No model is created and no view is generated: the questions are rows, which is why they need no upgrade, no Studio, and no developer. What this is not: a form for people without an Odoo login. Requests are raised in Odoo, and Odoo's own screens work on a phone.

SLA clocks and parallel rungs

Every step of a type can carry an SLA in hours. The clock starts when the step opens; when it runs out, everybody who can act on the step is chased with an activity, the request's chatter says the step passed its SLA, and the request is flagged Past its SLA - a filter on the list, so the overdue ones are one click.

A step ticked Together with the step above opens at the same time as the one before it instead of after it. Both are asked at once; the request moves on when both are done. Three steps - the team, legal in parallel, then the board - is the shape most contracts want.

Who can do what

  • Approvals › User — everybody has it. Raise requests, act on the ones waiting on you, arrange your own cover.

  • Approvals › Administrator — defines the types and the steps, sees every request including confidential ones, and can withdraw one on somebody's behalf.

An administrator cannot edit a decision. Nobody can.

Nothing from Enterprise

The module depends on mail and nothing else. hr is used if it happens to be installed, to find who somebody reports to.

Without it — and for anybody at the top of the tree, who reports to nobody — "the requester's manager" resolves to nobody. A step is asked to name somebody who is not the manager as well, and a step that ends up with nobody on it stops the request from being submitted and says why. Both examples shipped with the module are built that way.

How we make sure it works

Before every release, 117 automated checks run against this app on Odoo 16, 17, 18, 19 and 20. Among them:

  • One person cannot cast two votes on the same step.

  • A type that is not offered is refused by the model.

  • A request cannot be raised in somebody else's name.

  • A type offered on the portal cannot require the unaskable.

  • A visitor cannot name a contact that is not theirs.

The complete list is in the source you receive.

What it does not do

Said before you buy, because it is the half that decides whether the app fits:

  • Approve anything by itself: a request that no step applies to is refused, not passed.

  • Let a requester approve their own request - not as a member of the group, not as their own manager, not through a delegation.

  • Watch records on its own: a rule raises a request from the Action menu or from the button it blocks, not from a save. A record edited after its approval is not re-approved unless you raise it again.

Install

  1. Copy bambooforge_approvals into your addons path and update the app list, or install the ZIP from the Odoo Apps Store.

  2. Install Approval Workflow for Odoo Community.

  3. Open Approvals - its menus are Waiting on Me, All Requests, Delegation, Configuration.

Questions people ask

Which Odoo versions does it run on?

Odoo 16.0, 17.0, 18.0, 19.0, 20.0, on the series you buy it for; each series is its own listing.

Community or Enterprise?

Both. It needs no Enterprise app.

What does it need?

The standard Discuss app. No extra Python package.

Where does it show up?

Under the Approvals menu (Waiting on Me, All Requests, Delegation, Configuration).

Does anything leave my server?

No. It makes no call outside your Odoo server: no telemetry, no licence check, no third-party service.

What happens if I uninstall it?

Odoo drops the app's own tables and fields; the standard records it worked on (contacts, orders, products) stay as they are.

Raising one from the portal

The people who ask for things often have no Odoo login worth paying for: a contractor asking for site access, a subcontractor asking for a purchase order, a new starter asking for a laptop. Tick Offered on the portal on a request type and it appears at /my/approvals, on a phone as readily as a desk.

What the visitor gets:

  • The type's own form - the standard fields it asks for (period, amount, quantity, contact, reference, attachment) and every question you put on it, in the order you put them.

  • Their own requests, and nobody else's: what they raised, what it is waiting on, who has decided, and why if it was refused.

  • Their decision, if the ladder waits on them - approve, or refuse with a reason. The refusal needs the reason; the person who asked reads it.

  • Withdraw, while it is still theirs to withdraw.

What they never get: somebody else's request, a request they were not named on, or a way to set a field the form does not show. Every route hands the form to the model, which whitelists what a form may write, forces the requester to whoever is logged in, and resolves each answer against the questions that type actually asks. A contact question offers the visitor their own company's contacts and nothing else.

Two things the portal cannot ask, and says so:

  • a question whose kind is User - a visitor has no back end to pick an internal user in;

  • a question you marked Ask on the portal: no - the cost centre the office fills in.

Make either of them required on a type that is offered on the portal and the type refuses to save, rather than offering a form nobody can submit.

The ladder, the SLA clock, the escalation activities and the trail are the same as for a request raised in the back end. A portal request is not a lesser request: it is the same record, raised by the person who wanted the thing.

What changed in 1.4

  • A portal form: a request type offered on the portal is raised at /my/approvals by anybody with a portal login - the type's own questions, their own requests, approve or refuse from a phone, and nothing of anybody else's.

What changed in 1.3

  • Questions on a request type: text, number, date, yes/no, choice, contact, user - required or not - answered on the request, checked on submit, on the trail. No model, no generated view.