# ABC-XYZ Inventory Analysis & Reorder Rules > ABC and XYZ classification for your catalogue, plus Min/Max reorder rules generated from a service level and safety-stock days. - **What it is:** ABC and XYZ classification for your catalogue, plus Min/Max reorder rules generated from a service level and safety-stock days. - **For:** Warehouse - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_abc_analysis - **Manual:** https://docs.bambooforge.dev/bambooforge_abc_analysis/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_abc_analysis.abc_analysis_line_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_abc_analysis - **Manual last updated:** 2026-10-15 --- # ABC-XYZ Inventory Analysis & Reorder Rules Stop treating every product the same. This app ranks your catalogue by **value** (ABC / Pareto) and by **demand predictability** (XYZ), combines the two into an actionable **ABC-XYZ matrix** with a stocking recommendation per cell, keeps a snapshot of every run, and writes the classes back onto your products so you can filter and act on them across Inventory. This page is the complete manual. Follow it top to bottom and you can install, run your first analysis, read the matrix, tune the thresholds, automate refreshes and fix the common issues **without contacting support**. ## Overview Two complementary classifications, combined: - **ABC (value)** — the Pareto principle applied to inventory. A small share of products usually drives most of the value. Class **A** is the vital few, **B** the middle, **C** the trivial many. - **XYZ (demand variability)** — how predictable a product's demand is. **X** is stable and easy to forecast, **Y** is variable, **Z** is erratic. Cross them and you get a 9-cell **ABC-XYZ matrix**. An *AX* product (high value, stable) deserves tight, automated replenishment with low safety stock; a *CZ* product (low value, erratic) is a phase-out or make-to-order candidate. The app spells out a recommended policy for every cell. It gives you: - **Re-runnable analyses** — each keeps its own snapshot of results. - **Configurable ABC** — choose the value basis and the A/B thresholds. - **Configurable XYZ** — choose the number of demand buckets and the variability cut-offs. - **Write-back to products** — ABC, XYZ and the combined cell stored on each product for filtering and grouping across Inventory. - **Matrix pivot, graph and list** of the results, plus a per-cell policy. - **Optional weekly cron** to keep the latest analysis fresh. ## Requirements - Odoo 20.0. - The **Inventory** app (`stock`). Installing this app pulls it in automatically. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_abc_analysis` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **ABC-XYZ** and click *Install*. A new top-level **ABC-XYZ** app appears, and an **ABC-XYZ Analysis** entry is added under *Inventory > Reporting*. ## Quick start 1. Open **ABC-XYZ > Analyses** and click *New*. 2. Set the **Reference Date** and the **Period (days)** to analyse (365 is a good default). 3. Choose the **ABC Based On** metric: - *Consumption Value* — quantity delivered to customers × cost (the classic choice). - *Consumption Quantity* — units delivered, ignoring cost. - *On-hand Stock Value* — current quantity on hand × cost. 4. Leave **Compute XYZ** ticked to also classify demand variability. 5. Click **Compute**. The analysis switches to *Computed*, the **Products** stat fills in, and the **Results** tab lists every product with its value, classes and combined cell. ## Reading the matrix Open an analysis and use the **Products** stat button, or go to **ABC-XYZ > Products**: - **Pivot** — ABC classes down the side, XYZ classes across the top, value in each cell. This is your ABC-XYZ matrix: where does value sit, and how predictable is it? - **Graph** — value per ABC-XYZ cell at a glance. - **List** — every product with its value, cumulative share, ABC, XYZ, combined cell and the recommended stocking policy. ## How classification works **ABC.** Products are sorted by the chosen metric, highest first. Running totals give each product a cumulative share. Products are class **A** until the cumulative share reaches the **A Threshold** (default 80%), class **B** up to the A + **B Threshold** (default 80 + 15 = 95%), and **C** beyond. **XYZ.** The period is split into **Demand Buckets** (default 12, i.e. monthly over a year). For each product the coefficient of variation (CV = standard deviation / mean) of demand across the buckets is measured. CV up to **X max CV** is **X** (stable), up to **Y max CV** is **Y** (variable), above is **Z** (erratic). Products with no movement fall into **Z**. All values are expressed in the company currency. ## Tuning the thresholds Everything is on the analysis form: - **A / B Thresholds** — move the Pareto cut-offs (their sum must stay below 100). - **Demand Buckets** — more buckets = finer variability detection (needs enough history). - **X max CV / Y max CV** — tighten or loosen what counts as stable vs erratic. Re-run the analysis after any change; results update immediately. ## Acting on the classes When **Write Back to Products** is on, every product gets an **ABC-XYZ** tab showing its classes and the analysis it came from, plus filterable fields. In the product list (Inventory > Products): 1. Group by **ABC Class** or **XYZ Class** (search panel > Group By). 2. Filter to **ABC Class A** or **Erratic Demand (Z)**. 3. Act on the selection — set reordering rules, adjust safety stock, review suppliers, or plan a phase-out. ## Recommended policies Each result line carries a recommended stocking policy for its cell, for example: - **AX** — high value, stable: tight control, automate continuous replenishment, low safety stock. - **AZ** — high value, erratic: make-to-order where possible, careful forecasting. - **CZ** — low value, erratic: consider phase-out or pure make-to-order. ## Automating refreshes A scheduled action **ABC-XYZ: Recompute Latest Analysis** ships **disabled**. Enable it under *Settings > Technical > Scheduled Actions* to recompute the most recent analysis per company on a schedule (weekly by default), moving the reference date to today each run. ## Frequently asked questions ****A product is missing from the results.**** For consumption bases, only products with deliveries to customers in the period appear. For the on-hand stock value basis, products with stock on hand are included. ****Everything is class A (or class C).**** The ABC split depends on spread. Check the A/B thresholds and that your cost prices are set, since value = quantity × cost. ****All products are class Z.**** XYZ needs demand spread across several buckets. With little history, or demand concentrated in one bucket, the variation is high by definition. Lower the bucket count or widen the CV cut-offs. ****Does it work with product variants?**** Classification is computed from delivery moves (per variant) and written back to the product. For variant-heavy catalogues, treat the class as a product-level signal. ****Does it work with multiple companies?**** Yes. Each analysis is scoped to one company, and results are kept separate per company. ## How we make sure it works Before every release, **21 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Defaults do not write orderpoints by themselves. - Preview does not write when auto off. - Manual orderpoint not touched. - A shipment after the company's midnight stays out. - Reapply updates no duplicate. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## Usage This section walks through a full working cycle: run an analysis, read the results, define reorder policies and let the app maintain your Min/Max reorder rules. It all lives under the top-level **ABC-XYZ** menu (Inventory users), with manager-only pieces called out below. ### Run an analysis 1. Open **ABC-XYZ > Analyses** and click *New*. The reference is filled from an `ABC/#####` sequence, so you never have to name it. 2. In the **Scope** group set: - **Reference Date** — the analysis is measured up to this day (defaults to today). - **Period (days)** — how far back demand and consumption are measured; 365 is the usual choice. - **ABC Based On** — *Consumption Value* (delivered qty x cost), *Consumption Quantity*, or *On-hand Stock Value*. - **Write Back to Products** — leave on to store the classes on each product. 3. Tune the **ABC Thresholds** and, if **Compute XYZ** is ticked, the **Demand Buckets** and **X max CV / Y max CV** on the right. 4. Click **Compute**. The status moves to *Computed*, the **Products** stat button fills in, the **Results** tab lists every product (A rows highlighted green, C rows muted), and a note is logged in the chatter. To refresh an existing analysis, open it and click **Recompute** (it re-reads moves up to the current reference date); **Reset to Draft** clears the results. ### Read the results Open **ABC-XYZ > Products** (or the **Products** stat button on an analysis) to explore the same result lines as a **Pivot**, **Graph** and **List**, grouped by ABC class by default. The pivot with ABC down the side and XYZ across the top is your ABC-XYZ matrix; each list line carries the product, **Value**, **Cumulative %**, **ABC**, **XYZ**, combined **ABC-XYZ** cell and a plain-language stocking **recommendation**. Products also gain an **ABC-XYZ** tab and filterable ABC/XYZ fields, so in **Inventory > Products** you can group by *ABC Class* or *XYZ Class* and act on, say, every A item or every erratic (Z) item at once. ### Define reorder policies A manager opens **ABC-XYZ > Reorder Policies** (requires *Inventory / Manager*) and creates one policy per class cell: 1. Under **Applies To**, pick the **ABC Class** (required), optionally an **XYZ Class** (blank = every XYZ class in that ABC class), the **Warehouse**, and optionally a **Product Category**. A more specific policy (pinned XYZ and/or category) wins over a wildcard one. 2. Under **Reorder Parameters**, set the **Service Level (%)**, **Safety Stock (days)**, **Lead Time (days)**, **Target Cover (days)** and **Qty Multiple**. The app sizes the reorder point from lead-time demand plus a safety buffer (the larger of the service-level buffer and the days-of-cover floor), and the Max from the target cover. 3. Leave **Auto-create Reorder Rules** off to stay in preview, or turn it on to let the app write real rules for matching products. *Example.* An AX policy with service level 99%, 3 safety days and a 14-day target cover keeps high-value stable items tightly stocked, while a CZ policy left in preview merely flags low-value erratic items without touching any rule. ### Apply the rules On a *Computed* analysis, click **Preview Reorder Rules** to see the **Suggested Min** / **Suggested Max** and matched **Policy** for every line without changing anything. When you are happy, click **Apply Reorder Rules** and confirm: the app creates or updates `stock.warehouse.orderpoint` Min/Max rules only for products matched by a policy with auto-create on. It is completely safe and idempotent - it only ever touches the reorder rules it created, so manually-created rules are counted as *left untouched* and never overwritten. A notification reports how many rules were created, updated and skipped. ### Automate the cycle Two scheduled actions ship **disabled** under *Settings > Technical > Scheduled Actions*: **ABC-XYZ: Recompute Latest Analysis** re-runs the most recent analysis per company (moving the reference date to today), and **ABC-XYZ: Sync Reorder Rules** keeps auto-create policies' reorder rules aligned to the latest demand. Enable them to run the whole classify-then-replenish loop hands-off. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_abc_analysis -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (3 models, prefixed `abc.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## 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 Inventory app. No extra Python package. ****Where does it show up?**** Under the **ABC-XYZ** menu (Products, Analyses, Reorder Policies). ****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. ## Changelog ### 20.0.1.1.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Auto-generate ABC-driven reorder rules (Min/Max orderpoints) from a service level and safety-stock days, opt-in and never touching manual rules. Also classify products with ABC (Pareto value) and XYZ (demand variability) analysis, combine them into a 9-cell ABC-XYZ matrix with stocking policies, snapshot over time and write the classes back onto products. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # Access Rights All in One with an Audit Trail > Access reviews with the evidence attached: SoD conflicts, time-boxed grants and every rights change since the last snapshot, on one line. - **What it is:** Access reviews with the evidence attached: SoD conflicts, time-boxed grants and every rights change since the last snapshot, on one line. - **For:** Extra Tools - **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:** 458 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_access_audit_bridge - **Manual:** https://docs.bambooforge.dev/bambooforge_access_audit_bridge/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_audit_pro.action_audit_dashboard - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_access_audit_bridge - **Manual last updated:** 2026-10-15 --- # Access Rights All in One with an Audit Trail ## What it does Audit Pro runs the access review campaign: a snapshot of who holds what, a named reviewer, a decision per user, and revocations that are actually applied. Access Manager knows the story behind those rights: which grants were only ever meant to last a fortnight, which combinations break segregation of duties, and every change since the last campaign. Install both and this module puts one in front of the other. ## On every review line - **SoD conflicts** - how many segregation-of-duties violations stand against this user right now, in red when there are any, with a button that opens them. - **Time-boxed grants** - how many grants are still running and the date the first one ends. Access somebody already agreed should stop is not a reason to keep it. - **Changes since the snapshot** - how many rights changes landed after the snapshot was frozen. A reviewer signing a stale list should know that is what they are doing; one button opens the full history. ## When revocations are applied Applying a campaign's revocations writes into Access Manager's change log with the source **Access review decision**. Without this bridge the same removals are filed as "edited outside the wizards" - the entry an auditor cannot reconcile with the campaign that ordered it. ## Design Nothing is duplicated and neither module is patched: the campaign stays in Audit Pro, the history stays in Access Manager. This addon only reads from one side and adds context to the other, so both keep shipping and upgrading on their own. It installs itself automatically once both are present. ## How we make sure it works Before every release, **8 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Finished grant is not counted. - Clean user shows nothing. - Revocation is logged as a review decision. - Changes after the snapshot are flagged. - History action targets the user. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_access_audit_bridge` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Access Rights & Audit Suite**. ## 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 Web; access manager, audit pro (BambooForge) must be installed first app. No extra Python package. ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - answered within two business days. This module is licensed OPL-1; one purchase covers one production database plus its staging copies. --- # User Access Management & Access Rights Manager > Grant access by named role instead of by guesswork, copy one user's rights to another, hide the menus, fields, buttons, tabs and reports a role should not see - no code - and get told exactly which rule blocked someone. - **What it is:** Grant access by named role instead of by guesswork, copy one user's rights to another, hide the menus, fields, buttons, tabs and reports a role should not see - no code - and get told exactly which rule blocked someone. - **For:** Extra Tools - **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:** 229 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_access_manager - **Compared with the alternatives:** https://docs.bambooforge.dev/bambooforge_access_manager/compare/ - **Manual:** https://docs.bambooforge.dev/bambooforge_access_manager/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_access_manager.action_bf_access_role - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_access_manager - **Manual last updated:** 2026-10-15 --- # User Access Management & Access Rights Odoo's access rights are precise and unreadable. This app makes them manageable — roles instead of guesswork, a preview before anything is written, an explainer that says *which* rule blocked a user, and a log of every change. This page is the complete manual. ## Install 1. Install **BambooForge Access Rights Manager** (depends on `base` only). 2. The **Access Manager** menu appears for users in the **Access Manager** group, which implies Settings — the people who already manage users. ## Roles *Access Manager → Roles* A role is a named bundle of groups: "Warehouse operator", "Accountant, read-only", "Field sales". Give it a description in the words the person doing the hiring uses, and put the groups in. The role card shows how many users currently hold **every** group of the role, and the button opens that list — so "who is a warehouse operator?" has an answer again. **Apply to users** opens the preview. ## Applying, copying, bulk editing All three tools work the same way: pick the targets, read the preview, apply. Nothing is written until you press the button, and everything that is written lands in the change log. **Apply a role** — *Add* leaves the user's other groups alone; *Replace* strips everything the role does not contain. Replace is right when a role is the whole job, and dangerous otherwise: the preview shows exactly what it would take away, in red. **Copy access** — the day-one task. Point at somebody who already works, copy to the new hire. *Replace* makes the access identical; *Add* only fills the gaps. The source user is skipped if they are also in the target list. **Bulk edit** — add or remove a few groups across a selection of users. It is also bound to the Users list: select users, *Action → Bulk access edit*. Each preview lists, per user, the groups gained in green and the groups lost in red — or says plainly that nothing changes. ## Explain access *Access Manager → Tools → Explain access* The feature the "Access Error" dialog never gives you. Pick: - **User** — whose access is in question. - **Model** — what they are trying to reach. - **Operation** — read, write, create or delete. - **Record id** *(optional)* — the exact record, when the problem is "everything works except this one". The verdict comes back in words: **Model access** — the access-control lines that apply to this user's groups and grant the operation, named with their group. When none do, it says so, and lists the groups whose lines *would* grant it — which is usually the answer to "what should I give them?". **Record rules** — every rule in force for this user on this model, with its scope (global, or the groups it belongs to) and its domain. With a record id, each rule is marked as *allows this record* or *excludes this record* — the one in red is the reason. If a rule's domain cannot be parsed at all — a bad module data file, a hand-edited row — the verdict says the check could not run and points at the rule instead of failing silently. ## Change log *Access Manager → Change Log* records every change to a user's groups: who was changed, who changed them, how, and the exact groups added and removed. A change made through this module's wizards says which (role, copy, bulk) and names the role or source user involved. Since 1.1 a change made anywhere else that writes the user - the Users form, RPC, a script, another module - is logged too, as "Edited outside the wizards", unless the module names its own road (an access review decision, a time-boxed grant ending). Nobody can edit or delete a line. Three roads stay outside it: a user created with groups already set, groups changed from the group's side (its Users tab, or a group made to imply another) and SQL run on the database. It is a log of users' rights, not an audit of the whole database. ## Interface restrictions *Access Manager -> Interface Restrictions* Groups decide what a user **may** do. This decides what a user **sees**. The two are kept apart on purpose, and the difference is stated on every screen of it: a hidden field is not a secret, it is a tidier screen for somebody whose job never touches it. A rule names **who** and **what**. **Who** - any mix of: - *Roles* - every user holding every group of the role; - *Groups* - every user in any of them; - *Users* - named one by one; - *Except* - users the rule never touches, whatever their groups. The superuser is never restricted, so an administrator cannot lock the database. **What** - one tab each: - **Menus** - a hidden menu takes every menu under it along, apps included. - **Fields** - *Hidden* fields leave every form, list, kanban and search view the user opens, embedded lists and kanbans included, and are refused on export. *Read-only* fields stay visible and cannot be edited on screen. - **Buttons, tabs, blocks** - a button (by its method or action name), a notebook tab (by name or title), any named block, or the chatter. Press **Pick from a view**, choose the model and the view, read it, tick the ones to hide: no XML to read. - **Actions & reports** - entries removed from the Action and Print menus of their model. - **Per-model switches** - no create, no edit, no delete, no duplicate, no export, no import, or the whole model read-only. **What is enforced where.** Menus, fields, buttons, tabs and the create / edit / delete switches shape the screen. A user who knows the model name can still reach the record over RPC with whatever their groups allow - use groups for that, and *Explain access* to check them. Three switches are refused on the server as well, because they are the roads a screen cannot close: **export** (of a locked model, or of any hidden field), **import** and **duplicate**. **See it as a user** (*Tools*, or the button on the rule) answers the question every administrator asks after saving: what does this person actually see now? Pick the user and, optionally, a model: the rules that reach them, the menus gone, and per model the fields, elements and switches in force. Rules take effect the moment they are saved, for every user, without a restart; the next screen the user opens is the restricted one. Menus refresh on the user's next page load. ## Troubleshooting ****A role shows zero users though people clearly have those groups**** The count is users holding *every* group of the role. A role with a group almost nobody has will count almost nobody. ****Replace removed more than expected**** Replace leaves only the role's groups. Check the preview before applying; the change log has the exact list, so the groups can be put back. ****Explain says the user may read, but they still see an error**** Check the record id: model access can be fine while a record rule excludes that one record. That is what the record-level answer is for. ****Explain says a rule "excludes this record" but the user sees it**** Rules of the same group are OR-ed together in Odoo, and rules of different groups are AND-ed. The wizard reports each rule on its own; one red line among several green ones of the same group is not a block. ****Nothing appears in the change log**** The log sees changes made to a user: the wizards, the Users form, RPC, another module. A user created with groups already set, groups changed from the group's own form and SQL run on the database leave no line. ****A hidden field is still in the export dialog**** The dialog lists what the model has; the export itself is refused for a hidden field. Hide the export entirely with the model switch. ****A user still sees the menu after the rule was saved**** Menus are loaded once per page; the user reloads the page. ****The administrator cannot see something either**** Administrators are ordinary users to a rule that names their group. Put them in *Except*, or write the rule against a role instead of against a group every administrator happens to hold. ## What changed in 1.1 - **Segregation of duties.** Name the pairs of groups one person must never hold together (create vendor bills / approve payments). Blocking rules refuse the grant whatever road it takes - wizards, the Users form, RPC; warning rules let it through but keep the person on the violation list. *Access Manager -> SoD Rules*, plus a one-click scan of everybody already in violation. - **The log cannot be walked around.** Group changes made outside the wizards are captured too, and the log itself became append-only - nobody edits or deletes history, deleting a user keeps their trail. - Names are escaped everywhere the wizards render previews. ## What changed in 1.2 - **Time-boxed grants.** The Apply and Bulk wizards take an optional end date: every group granted with it is revoked automatically on that day by a daily job, and the change log says the clock did it. Revoke early from *Access Manager -> Time-boxed Grants*. ## How we make sure it works Before every release, **78 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Explain refuses an id that does not exist. - Explain survives a domain that cannot be parsed. - Explain reports a restriction it cannot evaluate. - Log cannot be edited or deleted. - Preview says what the user does not see. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — questions, bug reports and feature requests are answered by the developers who wrote the module. ## What changed in 1.3 - **The change log can be told where a change came from.** Another module (the Audit Pro bridge, for one) can set `bf_access_source` in the context, and rights changes it makes are filed under that source instead of "edited outside the wizards" - the entry nobody can reconcile at audit time. Unknown sources still fall back to a plain direct edit, so the log cannot be spoofed into a value it does not define. ## What changed in 1.4 - **Interface restrictions.** Hide menus, fields, buttons, tabs, blocks, the chatter, Action and Print entries per role, group or user; make fields or whole models read-only; switch off create, edit, delete, duplicate, export and import. Elements are picked from the view, not typed. Export, import and duplicate are refused on the server as well. - **See it as a user.** The screen as one person will find it, in words, before they open it. --- # Activity Menu Performance Fix > Fixes the slow and timing-out activity menu on databases with hundreds of thousands of activities: same numbers, one fast query. - **What it is:** Fixes the slow and timing-out activity menu on databases with hundreds of thousands of activities: same numbers, one fast query. - **For:** Extra Tools - **Odoo versions:** 16.0 - Community and Enterprise - **Runs on:** Odoo.sh or Odoo you host yourself - not Odoo Online, which accepts no third-party apps - **Price:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_activity_perf - **Manual:** https://docs.bambooforge.dev/bambooforge_activity_perf/ - **Live demo:** https://demo20.bambooforge.dev/odoo - **Store page:** https://apps.odoo.com/apps/modules/16.0/bambooforge_activity_perf - **Manual last updated:** 2026-09-06 --- # Activity Menu Performance Fix The clock icon in the top bar loads on **every page** and, on databases with a very large `mail.activity` table, it can take **minutes** or return *504 Gateway Timeout* for the affected users. This module replaces the counting logic with a single fast SQL query per model that returns the **exact same badge numbers** — no upgrade, no data migration, no configuration. This page is the complete manual. Read it top to bottom and you can install, verify the fix and understand exactly what changed **without contacting support**. ## The problem The activity systray (the clock in the top bar) calls `res.users.systray_get_activities` on every page load. Standard Odoo 16: 1. `search()` **all** of the current user's scheduled activities into memory; 2. builds a dictionary keyed by each activity's *record*; 3. filters the records the user may see with `record not in allowed_records` — an **O(n²)** membership test on a recordset. For most users this is instant. But a common failure mode on aged databases is an automated action or server action that has quietly created activities on records that never close — piling **hundreds of thousands of activities onto a single user**. On a real production database one user had **514,789** activities on Manufacturing Orders. For that user the activity menu materialised the whole set in Python on every page load, took **600+ seconds**, pinned a worker process and returned a 504 timeout — degrading the whole instance. Upgrading Odoo is not a fix you can schedule around a live timeout, and it does not address the accumulated data. This module fixes the algorithm in place. ## What this module does It overrides `res.users.systray_get_activities` to count the badges with **one grouped SQL query per model** instead of loading the activity table into Python: - `COUNT(*) FILTER (...)` computes the overdue / today / planned buckets in a single indexed aggregate; - each model's **record rules** are compiled to SQL (via `_apply_ir_rules`) and pushed into the `WHERE` clause, so the badge counts only the activities on records the user is actually allowed to read; - a model-level `check_access_rights` gate skips models the user cannot read. The returned data structure is **identical** to standard Odoo — same keys, same per-model dictionaries, including the `actions` entry the systray JavaScript expects — so the menu renders exactly as before, just instantly. ## Why the numbers stay identical This is a performance change, not a behaviour change: - **Same permissions.** Record rules and model access are applied in SQL exactly as the standard membership test applied them. A user who cannot read a model still sees zero for it; records hidden by a record rule (for example the multi-company rule) are not counted. - **Same buckets.** Overdue / today / planned are computed from `date_deadline` against the user's *today*, the same classification the standard menu uses. - **Same shape.** The result is the same list of per-model dictionaries with the same keys. The bundled automated tests assert this directly: exact per-bucket counts on a mixed dataset, correct filtering through the standard multi-company record rule, and no entry for models with no activities. ## Installation 1. Copy `bambooforge_activity_perf` into your addons path. 2. Update the apps list and install **BambooForge Activity Menu Performance**. 3. There is nothing to configure. The override is active immediately. Dependencies: `mail` (standard). No Enterprise modules are needed. There is no schema change and no new stored column, so the module is uninstall-clean. ## Verifying the fix Before installing, you can confirm whether you are affected. Run (read-only): ``` SELECT u.login, a.res_model, count(*) FROM mail_activity a JOIN res_users u ON u.id = a.user_id GROUP BY u.login, a.res_model ORDER BY count(*) DESC LIMIT 10; ``` If any single user has tens or hundreds of thousands of activities on one model, that user's activity menu is the bottleneck this module fixes. After installing, open that user's session — the clock menu opens immediately, with the same badge numbers. You should **also** investigate *why* those activities were created (usually an automated action) and clean up the stale ones; this module makes the menu fast regardless, but a runaway rule will keep growing the table. ## Frequently asked questions ****Will my badge numbers change?**** No. See *Why the numbers stay identical* — permissions, buckets and shape are all preserved. ****Is it safe on multi-company databases?**** Yes. The multi-company record rule is compiled into the SQL, so counts are correct per company, exactly as before. ****Do I need to upgrade Odoo or delete data?**** No. The module is an in-place algorithmic fix. Cleaning up the source of the activity pile-up is recommended but independent of this module. ****What happens on uninstall?**** The override is removed and standard behaviour returns. There is no schema change to reverse. ## How we make sure it works Before every release, **8 automated checks** run against this app on Odoo 16. Among them: - It is one query per call not one per model. - An activity for somebody else is not counted. - A user with nothing scheduled sees nothing. - No activities no entry. - The shape matches what the web client expects. The complete list is in the source you receive. ## Questions people ask ****Which Odoo versions does it run on?**** Odoo 16.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. ****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. ## Support BambooForge Labs — [support@bambooforge.dev](mailto:support@bambooforge.dev) --- # Agile Sprints & Board for Projects > Scrum and Kanban on Odoo Project: backlog, sprints, story points, epics, releases, a board with swimlanes and WIP limits, velocity, burndown and cumulative flow - the half of Jira a team misses. - **What it is:** Scrum and Kanban on Odoo Project: backlog, sprints, story points, epics, releases, a board with swimlanes and WIP limits, velocity, burndown and cumulative flow - the half of Jira a team misses. - **For:** Project - **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:** 199 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_agile - **Not for:** Every stage has a WIP limit (Configuration → Stages). Zero means none. With a limit, the column header shows the count in the whole project against the limit: amber at the limit, red over it.; Tick Enforce and the move is refused instead - on the board, in the kanban, in the list, from an import - with the stage's name and the limit in the message. The count is per project, so two projects sharing a stage do not count against each other. Moving a task that is already in the stage never counts as a new entry. - **Manual:** https://docs.bambooforge.dev/bambooforge_agile/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_agile.action_bf_agile_board - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_agile - **Manual last updated:** 2026-09-21 --- # Agile Sprints & Board for Projects Odoo Project has tasks, stages and a kanban. A software or product team that leaves Jira misses the rest. This app adds it, on project.task, so the hours the team logs still land on the same invoices. ## What you get - A **backlog** of every task not yet in a sprint, ranked by dragging the handle, with an issue type (story, bug, task, spike), **story points**, an **epic**, a **release** and a blocked flag - all editable in the row. - **Sprints** with a goal, two dates and a capacity in points. One active sprint per project. Close one and the **velocity** is frozen, unfinished work goes back to the backlog, and the chatter says what was left. - A **board**: the project's own stages as columns, **swimlanes** by epic, assignee or issue type, drag between columns and to re-rank, a sprint bar with goal, points and days left. - **WIP limits** per stage: the column turns amber when full and red when over; tick *Enforce* and the move is refused. - **Epics** and **releases** with points-based progress. - **Burndown** per sprint (remaining, ideal and scope), **velocity** per closed sprint, a **cumulative flow diagram** rebuilt from the tasks' own stage history, and created-vs-resolved per week. - The project form carries the mean velocity of the last three sprints - the number to plan the next one against. - Written in OWL like the rest of Odoo. No Jira licence, no bundled board library, nothing phoning home. ## Install 1. Install **BambooForge Agile** (depends on Project only). 2. An **Agile** menu appears in the Project app: Board, Backlog, Sprints, Epics, Releases and Agile Reports. Every project form gets **Board**, **Sprints** and **Backlog** buttons. 3. Open *Configuration → Stages* and give a stage a **WIP limit** if you run Kanban; leave it at zero otherwise. ## The backlog *Agile → Backlog* lists every open task with no sprint, in rank order. Drag the handle to reorder; the rank is stored on the task, so the board and the sprint planning kanban show the same order. Type, points, epic, sprint, release and the blocked flag are edited in the row. Switch to the **kanban**: cards are grouped by sprint, with the backlog as the first column. Dragging a card into a sprint column plans it; the day it entered is stamped on the task, and the burndown reads it. ## Sprints A sprint belongs to one project and has a **goal** (one sentence the team can say yes or no to at the review), a start and an end date, and a **capacity** in points that the form compares with the points committed. - **Start** marks it active. Only one sprint per project may be active; starting a second one is refused with the first one's name. Missing dates are filled: today, and two weeks later. - **Close** freezes the **velocity** (points done) and the committed total as they stand, sends every unfinished task back to the backlog, and posts what went back in the chatter. Reopening a task later changes the task, not the velocity the next sprint was planned on. - **Reopen** puts a closed sprint back to planned; the frozen numbers stay until it is closed again. Story points are typed by the team. The app never derives them from hours. ## The board *Agile → Board* (or the **Board** button on a project or a sprint) shows the project's stages as columns and, by default, the active sprint's tasks. Pick another sprint, or *Whole project*, in the second selector. - **Swimlanes** by epic, assignee or issue type split every column into rows; each row shows its card and point count. - **Drag a card** to another column to change its stage; drop it above another card to put it before that card in the rank. - **Click** a card to open the task; the ban icon toggles *blocked*. - **Search** filters by name or epic; **Only mine** keeps the cards assigned to you. - The **sprint bar** shows the goal, points done of committed, a progress bar and the days left (red once the end date has passed). ## WIP limits Every stage has a **WIP limit** (*Configuration → Stages*). Zero means none. With a limit, the column header shows the count in the whole project against the limit: amber at the limit, red over it. Tick **Enforce** and the move is refused instead - on the board, in the kanban, in the list, from an import - with the stage's name and the limit in the message. The count is per project, so two projects sharing a stage do not count against each other. Moving a task that is already in the stage never counts as a new entry. ## Epics and releases An **epic** is a body of work too big for a sprint: tasks point at it, and its progress is the story points done over the points total. It has a colour (shown on the cards), a target date and an open/done state. A **release** groups the tasks that ship together, with a planned date. *Mark Released* records the actual date and posts in the chatter how many tasks were done - and which were not. ## Reports - **Burndown** (button on the sprint, or on the board): one point per day from the sprint's start to its end. *Remaining* is the points of tasks in the sprint and not yet done on that day; *Ideal* is the straight line from the committed total to zero; *Scope* is the committed total on that day, so a task pulled in mid-sprint shows as a step up rather than as history rewritten. The chart stops at today: the ideal line's slope says where the sprint is heading, and a remaining line that pretended to know tomorrow would be a lie. - **Velocity** (*Agile Reports → Velocity*): a bar per closed sprint, committed beside done. - **Cumulative flow** (button on the project): for each of the last thirty days, how many tasks sat in each stage. It is rebuilt from the chatter's tracking of stage changes, which Odoo has always recorded, so it works on tasks that existed before this app was installed. - **Created per week** and **Resolved per week**: two graphs over the tasks themselves; resolved reads the done date this app stamps. ## Coming from Jira *Agile -> Import from Jira*. In Jira: Filters, search the project, Export, CSV (all fields). Pick the Odoo project, the file, and: - **epics** become epics, with their state; - **stories, bugs and tasks** become tasks with their points, priority (Highest/High become starred), status, assignee, labels as tags, description and the comments, in the chatter as notes; - **sub-tasks** hang under their parent; - **sprints** are created by name, in the planned state, for you to date and start; - a Jira **status** with no stage of that name becomes a stage (folded when it is Done/Closed/Resolved), unless you untick it; - an **assignee** is matched to a user by name, email or login; unmatched ones are listed and go to the user you name, or nobody. The Jira key stays on every task as its *External key*, so the same file imported again updates instead of duplicating - run the export twice a week during the transition - and a commit that says `PROJ-12` still lands on the right task after the move. ## Commits and pull requests on the task On the project form, tab **Git**: pick GitHub or GitLab, press *New secret*, copy the webhook address and the secret into the repository's webhook settings (GitHub: content type JSON, events *push* and *pull requests*; GitLab: *push* and *merge request* events, the secret as the token). From then on: - a commit message, a pull request title or body that names a task - `#123`, `task-123`, or its Jira key - leaves a note on the task with the author and a link to the commit or the PR; - `fixes #123`, `closes task-123`, `resolves PROJ-12` moves the task to the project's folded stage - when the commit reaches the default branch you named, or when the pull request is merged; on a feature branch, or an open PR, it only leaves the note; - a payload that does not carry the right signature (GitHub) or token (GitLab) is refused and writes nothing. ## Break a story into subtasks with AI With **AI Assistant** installed and configured, the story gains *Break into subtasks with AI*: the model reads the title and the description and proposes subtasks with a point estimate each. You untick what you do not want, edit the rest, and press Create; the subtasks land under the story, in its sprint and epic. Installed by the *Agile: break a story into subtasks with AI* bridge, by itself. ## Where the done date comes from A task is *done* when its stage is folded or, on Odoo 17 and later, when its state is Done or Cancelled. The first time that happens the app stamps **Done on**; reopening the task clears it. The burndown, the velocity and the resolved chart read that stamp, so a task done and reopened twice is counted where it ended up, not three times. ## Limits, stated plainly - One sprint per task at a time; the closed sprint remembers returned work in its chatter, not in its points. - The board does not create stages: use the project's own. - Story points are typed, never guessed from timesheets or hours. - The cumulative flow diagram covers thirty days and reads only stage changes that went through the chatter tracking (every ordinary write does). ## How we make sure it works Before every release, **35 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - One pull request naming the task twice leaves one note. - A file that is not jira is refused. - A merged pull request closes and an open one does not. - A signed push leaves a note and fixes closes. - An unknown status becomes a stage or not. The complete list is in the source you receive. ## What changed in 1.1 - **Import from Jira**: the CSV export becomes epics, tasks, sub-tasks, sprints, points, tags and comments; imported twice, it updates. - **Commits and pull requests on the task**: a GitHub or GitLab webhook per project; a commit that names a task leaves a note, "fixes #123" moves it to done on the default branch or when the PR is merged. - **Break into subtasks with AI**, with the AI Assistant bridge. --- # ChatGPT for Odoo | AI Assistant in Discuss > ChatGPT inside Odoo Discuss - or a model on your own server with Ollama: a private assistant per employee, with conversation history and as many separate chats as they need. - **What it is:** ChatGPT inside Odoo Discuss - or a model on your own server with Ollama: a private assistant per employee, with conversation history and as many separate chats as they need. - **For:** Discuss - **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:** 69 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_ai_assistant - **Run for real:** Tested for real against Ollama 0.33.3 (model qwen2.5:0.5b): the Settings button reported the model's answer, and a question typed in Discuss came back answered by the local model, with both requests visible in the Ollama log and none leaving the network. - **Not for:** What is sent to OpenAI: the text of the messages in the current conversation. When you send a message, the whole thread of that conversation is transmitted to OpenAI's chat completion API so it can answer in context. Nothing else from your Odoo database is sent.; Where the key lives: the API key is stored in Odoo's system parameters and is readable only by administrators.; Costs are yours: usage is billed to your own OpenAI account at OpenAI's rates for the model you choose. BambooForge does not resell or mark up OpenAI usage.; OpenAI's terms apply: data you send is processed by OpenAI under OpenAI's data and privacy policies. Do not send secrets or regulated data you are not allowed to share with a third-party API.; Out of scope: the assistant does not query Odoo records, does not perform Odoo actions, has no per-user keys, and provides one shared model selection for the whole database. - **Manual:** https://docs.bambooforge.dev/bambooforge_ai_assistant/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-mail.action_discuss - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_ai_assistant - **Manual last updated:** 2026-10-15 --- # ChatGPT for Odoo | AI Assistant in Discuss Chat with ChatGPT without leaving Odoo. This module adds a **ChatGPT** contact to your Discuss app: open a conversation, type a question, and the answer comes back as a normal Discuss message. Each chat keeps its own history, and you can run several separate conversations at once — just like talking to a colleague. This page is the complete manual. If you follow it top to bottom you can install, configure, use, and fix the common issues **without contacting support**. ## Overview The app connects Odoo Discuss to the **OpenAI API** so your employees can use ChatGPT inside the tools they already work in. It covers: - **A ChatGPT contact in Discuss** — search for *ChatGPT* in Discuss and start a direct message with it, the same way you message a coworker. It always shows as *online*. - **Real answers from OpenAI** — each message you send is forwarded to OpenAI's chat completion API and the reply is posted back into the conversation. - **Conversation memory** — the whole thread is sent with every new message, so ChatGPT remembers the earlier turns of *that* conversation. - **Multiple separate chats** — start as many ChatGPT conversations as you like; each is named automatically after its first message and kept in your Discuss history. What it is **not**: it does not read your Odoo business records, does not run actions in Odoo, and is not an "agent". It is a chat window to ChatGPT, wired into Discuss. ## Requirements - **Odoo**: 20.0, Community or Enterprise. - **OpenAI account**: an OpenAI API key from https://platform.openai.com with available credit or billing enabled. The cost of every request is charged to **your** OpenAI account (see *Data, privacy & limits*). - **Python libraries**: `openai` and `markdown` must be installed in the same environment as Odoo (`pip install openai markdown`). Odoo will refuse to install the module until both are present. - **Network**: outbound HTTPS from the Odoo server to `api.openai.com`. - **Odoo access**: any internal user can chat with the ChatGPT contact in Discuss. Only a **Settings / Administration** user can enter the OpenAI API key and pick the model. ## Installation 1. Install the Python dependencies on the Odoo server: ``` pip install openai markdown ``` 2. Copy `bambooforge_ai_assistant` into your Odoo addons path. 3. Restart the Odoo service. 4. Open **Apps**, click **Update Apps List**, search for *ChatGPT* (or *BambooForge AI Assistant*), and press **Activate / Install**. The base dependencies (*General Settings* / `base_setup` and *Discuss* / `mail`) install automatically. Installing the module also creates a hidden internal account named **ChatGPT**. This is the "author" of the assistant's replies in Discuss; it is inactive and is not a person you pay for. ## Configuration All settings live on one screen: **Settings ▸ General Settings ▸ Integrations ▸ Odoo ChatGPT Integration**. There is no separate menu — the two fields are added to the standard General Settings page. ![Odoo ChatGPT Integration settings (API key and model)](shot_settings.png) ### Step 1 — Get an OpenAI API key 1. Sign in at https://platform.openai.com. 2. Go to **API keys** (https://platform.openai.com/account/api-keys). The settings screen also has a **Generate an OpenAPI API Key** link that opens this page. 3. Click **Create new secret key**, copy the value (it starts with `sk-`) — it is shown only once. 4. Make sure your OpenAI account has billing set up or available credit, otherwise requests will fail with a quota error (see *Troubleshooting*). ### Step 2 — Enter the key and pick a model in Odoo 1. Open **Settings ▸ General Settings**, scroll to the **Integrations** section, and find **Odoo ChatGPT Integration**. 2. Paste your key into the **OpenAI API Key** field. 3. As soon as you enter a valid key, the app contacts OpenAI, validates the key, and fills the **ChatGPT Model** dropdown with the models your account can use. It also pre-selects a sensible default if available (it tries `chatgpt-4o-latest`, then `gpt-4o`, `gpt-4o-mini`, then `gpt-3.5-turbo`). 4. Pick the model you want from **ChatGPT Model**. Pricing and capability differ per model — see OpenAI's pricing page. 5. Click **Save**. | Field | What it does | |---|---| | OpenAI API Key | The `sk-…` secret key used for every request. Stored as the system parameter `bambooforge_ai_assistant.openapi_api_key`. There is one key for the whole database (system-wide, not per user). | | ChatGPT Model | The OpenAI model used to answer. The list is refreshed from your account when you enter the key. Stored as `bambooforge_ai_assistant.chatgp_model_id`. If no model is chosen, the app falls back to `gpt-3.5-turbo`. | If the key is wrong, Odoo shows the warning *"The OpenAI API Key is not valid"* and does not fill the model list. Re-check the key and try again. ### Step 3 (optional) — Run it on your own server with Ollama The assistant speaks the OpenAI chat API, and so do Ollama and most local model servers. Pick **Ollama / local server** as the provider, give the URL (Ollama's default is `http://localhost:11434/v1`), the model name as the server knows it (`ollama pull llama3.1` first), and press **Test the connection**. From then on the conversation never leaves your network, and the provider-storage switch does not apply - there is no provider. Any other OpenAI-compatible server (vLLM, LM Studio, a hosted gateway) is the third option, with a key if it asks for one. Tested for real against Ollama 0.33.3 (model qwen2.5:0.5b): the Settings button reported the model's answer, and a question typed in Discuss came back answered by the local model, with both requests visible in the Ollama log and none leaving the network. ## Usage Once a key and model are saved, any internal user can use the assistant: 1. Open the **Discuss** app. 2. In the **Direct messages** search (the **+** next to *Direct messages*, or the *New message* search), type `ChatGPT` and select the **ChatGPT** contact. It appears as online. 3. A conversation opens. Type your question and send it like any Discuss message. 4. The reply from ChatGPT is posted back into the same conversation, formatted from Markdown (lists, code blocks and bold text render properly). ![A ChatGPT answer formatted inside a Discuss conversation](shot_format.png) **Multiple conversations.** Start a new chat with the ChatGPT contact whenever you want a fresh, separate thread. Each conversation is named automatically after its first message, so you can tell them apart in your Discuss sidebar. **History & context.** Every message in a conversation is kept in Discuss. When you send a new message, the *entire* current conversation is sent to OpenAI so the assistant answers with the earlier turns in mind. Different conversations do **not** share context with each other. **Deleting a conversation.** Unpinning (closing) a ChatGPT conversation removes it — the chat and its history are deleted from your Discuss. Start a new chat to begin again. ## Customization This is a deliberately small module. The pieces you can change: | Item | Where / how | |---|---| | API key | **Settings ▸ General Settings ▸ Integrations ▸ Odoo ChatGPT Integration**. One key per database, set by an administrator. | | Model | Same screen, **ChatGPT Model**. Switch models any time; the change applies to the next message in every conversation. | | Available models | The model list (model `chatgpt.model`) is rebuilt from OpenAI each time you enter the API key. Internal users can read it but not edit it. | | Who can chat | Any internal user (group *Internal User*). The assistant is a Discuss contact, so Discuss access is all that is required. | There is no per-user API key: the whole database shares the single key entered in Settings, and all OpenAI usage is billed to that one account. ## Troubleshooting | Symptom | Cause and fix | |---|---| | "The OpenAI API Key is not valid" when saving Settings | The key is wrong, revoked, or copied with extra spaces. Re-copy it from https://platform.openai.com/account/api-keys and paste it again. | | The model list stays empty after entering the key | The key could not reach OpenAI (invalid key or the server cannot make outbound HTTPS calls to `api.openai.com`). Verify the key, then check the server's network and firewall. | | I send a message but get no reply | No model is selected, the key is blank/invalid, or OpenAI returned an error. Open **Settings ▸ General Settings** and confirm both **OpenAI API Key** and **ChatGPT Model** are set. If they are, the error text from OpenAI is usually posted back into the conversation — read it for the exact cause. | | Reply says "insufficient quota" / billing error | Your OpenAI account has no credit or no active billing. Add a payment method or credit at https://platform.openai.com — costs are charged to your OpenAI account, not to BambooForge. | | Reply says "rate limit" / 429 | You are sending requests faster than your OpenAI plan allows, or the model is momentarily overloaded. Wait a moment and resend; consider a higher OpenAI tier for heavy use. | | "model not found" / the chosen model errors | Your OpenAI account cannot access that model, or the model id changed on OpenAI's side. Open Settings, re-enter the key to refresh the list, and pick a model your account can use (e.g. `gpt-4o-mini` or `gpt-3.5-turbo`). | | ChatGPT does not appear when I search in Discuss | The module is not fully installed, or you are not an internal user. Confirm the app is installed and that you log in as an internal (not portal/public) user. | | The app will not install | The Python libraries are missing. Run `pip install openai markdown` in the Odoo environment and restart, then install again. | | I can see Settings but the fields are read-only / missing | You are not a Settings administrator. Only **Settings / Administration** users can enter the API key and model. | ## Frequently asked questions **Do I need my own OpenAI account?** Yes. The module talks to OpenAI on your behalf using *your* API key, and every request is billed to your OpenAI account. The module itself does not include any OpenAI usage. **Where do I put the API key?** **Settings ▸ General Settings ▸ Integrations ▸ Odoo ChatGPT Integration ▸ OpenAI API Key.** Only an administrator can set it; it applies to the whole database. **Which model does it use?** Whatever you pick in **ChatGPT Model** on the same screen. The list is populated from your OpenAI account. If nothing is selected, it falls back to `gpt-3.5-turbo`. **Can each user use their own key?** No. There is a single, database-wide key. All conversations from all users use it. **Does it remember my previous chats?** Within one conversation, yes — the full thread is sent to OpenAI each time. Separate conversations are independent and do not share context. **Can it read or edit my Odoo data?** No. It only sends the text you type in the conversation. It does not access records, reports or actions in Odoo. **How do I start a new, clean conversation?** Start a new direct message with the **ChatGPT** contact in Discuss. Each one is a separate thread with its own history. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - **What is sent to OpenAI**: the text of the messages in the current conversation. When you send a message, the whole thread of *that* conversation is transmitted to OpenAI's chat completion API so it can answer in context. Nothing else from your Odoo database is sent. - **Where the key lives**: the API key is stored in Odoo's system parameters and is readable only by administrators. - **Costs are yours**: usage is billed to your own OpenAI account at OpenAI's rates for the model you choose. BambooForge does not resell or mark up OpenAI usage. - **OpenAI's terms apply**: data you send is processed by OpenAI under OpenAI's data and privacy policies. Do not send secrets or regulated data you are not allowed to share with a third-party API. - **Out of scope**: the assistant does not query Odoo records, does not perform Odoo actions, has no per-user keys, and provides one shared model selection for the whole database. ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 line and the OpenAI API. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_ai_assistant -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (1 model, prefixed `chatgpt.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.3.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.1 Current release for Odoo 20.0. This build includes: - Boost Employee Productivity with ChatGPT Integrated into Odoo. With conversation history tracking and the ability to create multiple separate chats, employees get a virtual assistant that supports them in handling tasks quickly and effectively. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **13 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The bot partner exists and is not a person. - Channel get twice is one chat. - Ollama points the client at the local server. - The history limit is configurable and defaults sanely. - The settings button reports the answer. Tested for real against Ollama 0.33.3 (model qwen2.5:0.5b): the Settings button reported the model's answer, and a question typed in Discuss came back answered by the local model, with both requests visible in the Ollama log and none leaving the network. The complete list is in the source you receive. --- # API Gateway for Odoo | REST API, OpenAPI, Webhooks & MCP Server > One key, three doors: a REST API with an OpenAPI file for Postman, n8n, Make and Zapier; signed webhooks that retry until delivered; and an MCP server for Claude and ChatGPT - each key limited to the models, fields and rows you list, with a review queue for writes and a log of every call. - **What it is:** One key, three doors: a REST API with an OpenAPI file for Postman, n8n, Make and Zapier; signed webhooks that retry until delivered; and an MCP server for Claude and ChatGPT - each key limited to the models, fields and rows you list, with a review queue for writes and a log of every call. - **For:** Extra Tools - **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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_ai_bridge - **Run for real:** Tested for real against n8n 2.34.6, Node-RED 5.0.6, Postman (newman), HTTPie and openapi-python-client: a contact created through REST fired a signed webhook into an n8n workflow that wrote a phone number back through REST; an order confirmed through a REST action reached a Node-RED flow with a verifiable signature; a 15-request Postman collection passed 24 of 24 assertions; the OpenAPI document validated with openapi-spec-validator and Redocly and generated a typed Python client that listed the orders - 0 errors. - **Compared with the alternatives:** https://docs.bambooforge.dev/bambooforge_ai_bridge/compare/ - **Manual:** https://docs.bambooforge.dev/bambooforge_ai_bridge/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_ai_bridge.action_bf_ai_token - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_ai_bridge - **Manual last updated:** 2026-10-15 --- # API Gateway for Odoo | REST API, OpenAPI, Webhooks & MCP One key, three doors. A **REST API** with an **OpenAPI** file that Postman, n8n, Make, Zapier and code generators read as-is; **webhooks** that tell another system the moment something happens, signed and retried until they get through; and an **MCP server** for Claude, ChatGPT and any other agent. Every door opens exactly as far as the key was granted: the models, the fields, the rows and the calls a person listed - nothing else exists for it. This page is the complete manual. Follow it top to bottom and you can install the app, issue a key, connect a tool, a system or an agent, understand the guard rails and diagnose the usual problems **without contacting support**. ## Why this exists Odoo already has an API. What it does not have is a way to hand a piece of it to somebody else safely: - an API key is the **whole** user - a partner who reads orders also reads payroll, and nothing tells you what they read; - XML-RPC and JSON-RPC need a developer who knows model names, field names and domain syntax; the people who build automations in n8n or Make do not; - Odoo's JSON-2 endpoint (19.0 and later) and `/doc` page describe the *database*, not a scope, and there is no OpenAPI file to import; - the webhook action in Automation Rules fires once and forgets - no signature, no retry, no record of what was delivered. The gateway inverts the default. A key starts with **zero** models. A person lists what it may read, what it may change, whether changes apply at once or wait for review, and how much it may call. The same list drives the REST API, the OpenAPI document, the webhooks and the MCP tools, and every call through any door is one line in the query log. ## Install 1. Copy `bambooforge_ai_bridge` into your addons path and update the app list, or install the ZIP from the Odoo Apps store. 2. Install the app **API Gateway for Odoo**. 3. Give yourself the group **AI Bridge / Manager** (*Settings → Users → your user → Other*). Without it the menu stays hidden. 4. The menu **API Gateway** appears with *Keys*, *Proposals*, *Query Log*, *Webhooks* and *Deliveries*. There is no external service, no outbound call of ours and no account with anyone: every endpoint lives inside your own Odoo. ## Issue a key *API Gateway → Keys → New* | Field | Meaning | |---|---| | **Name** | What the key is for, e.g. *n8n - warehouse* or *Claude Desktop - sales team*. It shows on every log line. | | **Runs as user** | The Odoo user whose record rules and access rights apply to every call. Use a dedicated, low-privilege user - the allowlist is a second fence, not a replacement for ACLs. | | **Reviewers** | Who is asked to apply or refuse the changes the key proposes. Empty: the key's user. | | **Expires on** | Optional. After this date the key stops authenticating - and so do its access tokens. | | **Max rows per call** | Hard ceiling on rows returned by one call (default 200). A per-model limit can lower it. | | **Max calls per day / per minute** | Rolling caps shared by every door. Past them every call is refused with the reason, and REST answers `429` with `Retry-After`. | | **Answers REST / Answers MCP** | Two switches: turn one door off without touching the other. | | **OAuth2 client credentials** | For systems that prefer short-lived access tokens over a long-lived key (see below). | Add at least one line under **Allowed models**, then press **Generate secret**. The secret is shown **once**, in a banner at the top of the form, and looks like: ``` bfai_9f2c1e7ad4b3... ``` Only a SHA-256 digest is stored; nobody - including you - can read the secret back afterwards. Lost it? Press *Generate secret* again: the previous secret stops working immediately. **Revoke** stops the key, its client secret and every access token it issued, at once. ## Allowed models: the keyhole Each line under **Allowed models** answers the questions every door asks: ****Model**** The only models the key can name. Anything else is refused before a query is built, and the refusal is logged. The answer does not say whether the model exists. ****Fields**** A comma-separated allowlist, e.g. `name,email,city,country_id`. Leave it empty to expose every *stored, non-binary* field of the model. Regardless of what you type, these are always stripped: `password`, `password_crypt`, `api_key`, `totp_secret`, `access_token` and any field whose name contains *password*, *token* or *secret*; binary fields are excluded too. ****Extra filter (domain)**** An Odoo domain the caller can never widen, e.g. `[('company_id','=',1)]` or `[('state','=','sale')]`. It is **AND**-ed with whatever the caller sends. This is how you expose *confirmed orders of one company* rather than *the sale.order table*. A record outside it does not exist: reading it is a 404, changing it is a 404, a webhook never mentions it. ****Row limit**** Optional per-model ceiling, lower than the key's own. ****Allow aggregate**** Whether grouped counts and sums may run on this model. ****May propose changes**** The key may ask for a record to be created or changed; the ask lands under *Proposals* and is applied by a person, under that person's rights. This is how MCP agents write, and how REST writes unless the next switch is on. ****Write directly**** REST `POST` and `PATCH` apply at once, under the key's user (`201` / `200` instead of `202`). For systems, not for agents. ****Writable fields**** What a proposal or a direct write may touch. Empty means the readable fields minus anything that decides ownership, company, security or the archive flag - those are never writable, whatever is typed. ****May delete**** REST `DELETE` removes the record under the key's user. Off by default. ****Callable actions**** Comma-separated `action_*` or `button_*` methods REST may call on one record, e.g. `action_confirm`. Empty means none. > **Tip:** A good starting allowlist for an automation tool is `res.partner` > (`name,email,phone,city`; write directly on `email,phone`), > `sale.order` (`name,partner_id,date_order,amount_total,state`, extra > filter `[('state','in',('sale','done'))]`, may propose) and > `product.template` (`name,list_price,default_code`). ## Give a system access: REST Every allowed model is a resource. Send the key as a bearer token: ``` GET https:///api/v1/sale.order?fields=name,amount_total,state&limit=20 Authorization: Bearer bfai_ ``` | Call | Answer | |---|---| | `GET /api/v1/models` | What this key may use: fields, writable fields, row limit, write mode, actions. | | `GET /api/v1/me` | The key, its user, its limits and calls left. | | `GET /api/v1/` | A page: `?fields=a,b` `?filter=` `?order=write_date desc` `?page=` `?limit=` `?count=1` (adds `total`). | | `GET /api/v1//` | One record, with an `ETag`. | | `GET /api/v1//aggregate` | `?group_by=partner_id&measure=amount_total` | | `POST /api/v1/` | Create: `201` with the record, or `202` with a `proposal_id` when the rule says review. `Idempotency-Key` header optional. | | `PATCH /api/v1//` | Change: `200` or `202`. `If-Match` with the ETag you read gives `412` if the record changed since. | | `DELETE /api/v1//` | `204` - only when *May delete* is on. | | `POST /api/v1///actions/` | Call a listed `action_*` method; the body is its keyword arguments. | | `GET /api/v1/proposals/` | What became of a proposal this key made. | Values come back the way a tool expects them: a many2one is `{"id": 7, "display_name": "Ada Lovelace"}`, one2many and many2many are lists of ids, dates are ISO 8601 and datetimes end in `Z` (Odoo stores UTC). To write a many2one, send the id. Add `"_reason": "..."` to a `POST` or `PATCH` body and the reviewer sees it next to the proposal. Errors are `application/problem+json`: ``` {"type": "about:blank", "title": "Field not writable", "status": 400, "detail": "This key may not change these fields on res.partner: comment", "instance": "/api/v1/res.partner/17", "request_id": "9c95...", "fields": ["comment"]} ``` `400` malformed, `401` no valid key, `403` not granted (the same answer whether the model exists or not), `404` outside the key's view, `409` an `Idempotency-Key` reused with a different body, `412` stale `If-Match`, `422` refused by Odoo's own constraints, `429` over a cap. Every answer carries `X-Request-Id` and, when a per-minute cap is set, `X-RateLimit-Limit` / `-Remaining` / `-Reset`. A first session with curl: ``` curl -s https://your-odoo.example.com/api/v1/res.partner \ -H "Authorization: Bearer bfai_xxx" -H "Content-Type: application/json" \ -H "Idempotency-Key: 5c2d0a1e" \ -d '{"name": "Round Trip Ltd", "city": "Leeds", "email": "rt@example.test"}' curl -s "https://your-odoo.example.com/api/v1/sale.order?filter=[[\"state\",\"=\",\"sale\"]]&fields=name,amount_total&count=1" \ -H "Authorization: Bearer bfai_xxx" ``` ## Give an automation tool access: OpenAPI `GET /api/openapi.json` with the key returns an **OpenAPI 3.1** document built from that key's rules: exactly its models, fields, writable fields, limits and actions, with an example taken from the first record the key may see. Import it into Postman, Insomnia, Bruno, n8n, Make, or a code generator - the paths, schemas and the bearer scheme are already there. A revoked key has no document. `/api/doc` is a built-in **API explorer**: paste the key, see every operation it may perform, try each one, copy the curl line. It ships in the module - no CDN, nothing loaded from outside. On Odoo 20 the document also points at the core `/json/2//` endpoint, which accepts the same bearer key for any method of the listed models; `/doc` describes those. > **Note:** n8n: add an *HTTP Request* node, choose *Import cURL* or paste the URL, > set *Authentication: Generic - Header Auth* with > `Authorization: Bearer bfai_...`. Make and Zapier: their generic HTTP > modules take the same header. A Python client generated with > `openapi-python-client generate --path openapi.json` lists orders in > four lines; the typed client is in the module's examples. ## Tell another system: webhooks *API Gateway → Webhooks → New* | Field | Meaning | |---|---| | **Key** | The payload never carries more than this key may read; the model must be on its allowlist. | | **Model / events** | On create, on change, on delete. | | **Only when these fields change** | On change: fire only when one of them changed. Empty: any change of an exposed field. | | **Domain** | Only records matching it fire, e.g. `[('state', '=', 'sale')]`. | | **URL / Secret** | Where to POST; the secret is generated and shared with the receiver to verify signatures. | Each event becomes a **delivery** written in the same transaction as the change, so a committed change is never lost. A cron sends deliveries every minute; **Send a test** sends one now. While setting up, point the URL at your own Odoo's `/api/echo`: it answers `200` with `{"received": true, "signature_valid": true}` and stores nothing, so you can see a green delivery before the real receiver exists. The body: ``` {"event": "write", "model": "sale.order", "id": 3, "occurred_at": "2026-09-05T14:19:08.512Z", "webhook": "Order changes", "data": {"id": 3, "name": "S00003", "state": "sale", "amount_total": 529.58, ...}, "changed": ["date_order", "state"], "previous": {"state": "draft"}} ``` Headers: `X-BF-Event`, `X-BF-Model`, `X-BF-Delivery-Id`, `X-BF-Webhook-Id` and `X-BF-Signature: t=,v1=`, where `v1` is `HMAC-SHA256(secret, t + "." + raw body)`. Verify it in five lines: ``` import hmac, hashlib def verify(secret, header, raw_body, tolerance=300): t, v1 = [p.split("=", 1)[1] for p in header.split(",")] expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(v1, expected) and abs(time.time() - int(t)) < tolerance ``` A `2xx` answer settles the delivery. Anything else is retried after 1, 5 and 30 minutes, then 2, 6 and 24 hours, and is then **dead**; a `4xx` (other than 408, 425, 429) is dead at once - the receiver said no. Ten dead deliveries in a row **pause** the webhook and leave an activity for its owner; *Resume* clears the streak. Every delivery is under *Deliveries* with attempt count, HTTP status, duration, the receiver's answer and a **Replay** button. Deliveries are kept 30 days (`bf_ai_bridge.delivery_retention_days`). ## OAuth2 client credentials Some systems will not store a long-lived key. Press **Generate client secret** on the key: it gets a `client_id` and a secret shown once. The system then exchanges them for an access token that lives one hour (tunable per key, 60 s to 24 h): ``` POST /api/oauth/token grant_type=client_credentials&client_id=bfc_...&client_secret=bfcs_... {"access_token": "bfa_...", "token_type": "Bearer", "expires_in": 3600, "scope": "product.template res.partner sale.order"} ``` The access token is a bearer token like the key and opens exactly the same doors; the `scope` only reports what the key was granted. Revoking the key revokes every access token. MCP clients that speak OAuth2 can use the same endpoint. ## Give an AI agent access: MCP The endpoint is: ``` POST https:///api/mcp Authorization: Bearer bfai_ Content-Type: application/json ``` It speaks **JSON-RPC 2.0** and implements the MCP methods `initialize`, `tools/list`, `tools/call` and `ping`. There is no session cookie and no CSRF token involved: the bearer token is the whole authentication. Check it with curl before touching any client: ``` curl -s https://your-odoo.example.com/api/mcp \ -H "Authorization: Bearer bfai_xxx" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' ``` A healthy answer names the server and lists exactly the models you allowed: ``` {"jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "serverInfo": {"name": "BambooForge Odoo Bridge", "version": "1.0.0"}, "capabilities": {"tools": {"listChanged": false}}, "instructions": "This Odoo bridge is read-only. Allowed models: product.template, res.partner, sale.order"}} ``` A `tools/call` answers in MCP shape — human-readable `content` plus machine-readable `structuredContent`: ``` {"jsonrpc": "2.0", "id": 4, "result": {"isError": false, "content": [{"type": "text", "text": "..."}], "structuredContent": {"model": "sale.order", "groups": [{"count": 73, "state": "sale", "amount_total": 116313.91}]}}} ``` Note the group list: the rule on `sale.order` carried the extra filter `[('state','in',('sale','done'))]`, so draft quotations never enter the answer — the agent cannot see they exist, whatever it asks. **Claude Desktop / Claude Code.** Clients that speak HTTP MCP take the URL and header directly. For a client that only speaks stdio, put a standard remote bridge in front — no code of ours is involved: ``` { "mcpServers": { "odoo": { "command": "npx", "args": ["-y", "mcp-remote", "https://your-odoo.example.com/api/mcp", "--header", "Authorization: Bearer bfai_xxx"] } } } ``` Use HTTPS. A bearer token on plain HTTP is a bearer token in everyone's logs. ### The MCP tools **`odoo_list_models`** What may I read? Returns each allowed model with its exposed fields, row limit and whether aggregates are allowed. Agents call this first. **`odoo_describe_model` — `{"model": "sale.order"}`** Field names, types, labels and selection values for one allowed model, so the agent writes correct domains instead of guessing. **`odoo_search` — `{"model", "domain", "fields", "limit", "order"}`** The workhorse. The domain is validated (see below), AND-ed with the rule's filter, and the limit is clamped. **`odoo_read` — `{"model", "ids", "fields"}`** Specific records by id, same field allowlist. **`odoo_aggregate` — `{"model", "domain", "group_by", "measure"}`** Grouped count, or sum of one numeric field. This is what makes "how much did we sell per salesperson last month" cost 12 rows instead of 12,000. Nothing in this list writes. The three tools below can only *propose*. ### Proposing a change From 1.2 the agent can ask for a record to be created or updated. It cannot do it. Three tools: - `odoo_propose_create` - a model, the values, and a reason. - `odoo_propose_write` - a model, a record id, the values, a reason. - `odoo_proposal_status` - what became of the proposals it made. A proposal lands under **API Gateway > Proposals** as an activity for the token's **reviewers** (or its owner, if none are named). The reviewer sees the values as a table, the agent's reason as the agent's words, and two buttons. **Apply** writes the values under the reviewer's own user and access rights - so a reviewer who may not edit contacts cannot apply a contact change, whatever the token was allowed. The chatter of the record says which proposal, which token and which person. **Refuse** takes an optional note, which the agent can read back. What the agent may propose is narrower than what it may read. A rule has to say **May propose changes**, and the fields it may touch are listed separately; empty means the readable fields minus anything that decides ownership, company, security or the archive flag - those are never proposable, whatever is typed. A record outside the rule's domain does not exist as far as a proposal is concerned. The grant is read again when the proposal is applied, not only when it was made: a field the token lost in between is not written on the strength of an older permission. Every proposal is a line in the query log, like every read. #### Running it against a local model The bridge speaks MCP over HTTP, so any MCP client can use it - Claude Desktop, ChatGPT, and the open clients that drive a model served by **Ollama** or another OpenAI-compatible server on your own machine. Point the client at `/api/mcp` with the token, exactly as for a hosted model. The proposals door is the same door: a local model gets no more than a hosted one. ## What the gateway refuses - **A model that is not on the allowlist** - refused before any SQL runs, with an answer that does not reveal whether the model exists. - **A field that is not on the allowlist** - the call is refused rather than silently trimmed, and the refusal names the field. - **A write to a field outside the writable list** - `400` naming the field; ownership, company, security and archive fields are never writable. - **A domain sent as a string** - string domains are executable Python in Odoo; only list (JSON) domains are accepted, every condition three parts, only `&` `|` `!` as operators. - **A record outside the rule's domain** - `404`; it does not exist for the key, and no webhook mentions it. - **Anything above the caps** - row counts are clamped, and past the daily or per-minute cap every call is refused until the window moves. - **An expired, revoked or unknown key** - `401` on REST, error `-32001` on MCP. - **Anything the key's user could not do anyway** - reads and writes run as that user, so Odoo's own access rights and record rules are the second lock; a write they refuse is a `403`. ## The query log *API Gateway → Query Log* records, per call and per door (*channel*: REST, MCP, OAuth2, OpenAPI): key, operation, model, the exact arguments, HTTP method, path and status, row count, duration, an **estimated token cost** of the payload, the caller's IP and user agent, and the message of any refusal. Use it three ways: 1. **Cost and load** - group by key, sum rows or estimated tokens; a runaway automation shows up as a wall of identical calls. 2. **Security review** - filter on *Refused*. A healthy integration refuses almost nothing; a burst of refusals means a badly configured client or an agent being talked into wandering. 3. **Kill switch** - the **Revoke** button on a log line disables the key that produced it, immediately. Logs expire after 180 days (`bf_ai_bridge.log_retention_days`, 0 keeps everything). ## Compared with the other doors into Odoo | Door | What it means for you | |---|---| | Odoo's XML-RPC / JSON-RPC | Built in, on every version. A login and password (or API key) with the user's full rights; no allowlist, no OpenAPI file, no webhooks, no per-call log of what an automation read. | | Odoo's JSON-2 API | Built in from 19.0, cleaner than XML-RPC, same scope: the caller's full rights. No webhooks. | | Odoo 20's MCP server | Built into Odoo 20 Enterprise. It acts with the rights of the user behind the API key, like the other doors Odoo ships. | | REST API apps on the store | Several, listed at roughly 87 to 122 EUR at the time of writing; they add REST endpoints. Check each listing for an OpenAPI file, webhooks, per-key model and field allowlists, and a query log before comparing. | | MCP connector apps on the store | Listed at about 173 EUR at the time of writing for the MCP door alone. | | This app | One key with three doors - MCP, REST with OpenAPI 3.1, signed webhooks - and, on every door, an allowlist of models, fields, rows and actions, propose-then-approve writes, per-key quotas, a query log with token estimates and a revoke button. 129 EUR per Odoo series, source included, no subscription. | The store prices quoted are those shown on the Odoo Apps Store at the time of writing; check the listings for today's figures and features. ## Security notes - Keys, client secrets and access tokens are stored as SHA-256 digests; the plaintext exists only in the banner shown at creation. - Every read and write runs `with_user()` as the key's user: record rules, multi-company rules and access rights apply. The allowlist narrows that - it never widens it. Nothing is `sudo` except reading the key itself and writing the log. - A new key has no rules: it answers `403` to everything until a person lists a model. There is no "allow all". - Fields protected by `groups` that the key's user is not in are absent from the schema and from every answer. - Controllers are `auth="none"` with `save_session=False`: no session is created, so a stolen bearer token cannot be turned into a browser session. Bodies are limited to 1 MB. - Webhook bodies are signed with a per-webhook secret and a timestamp; replaying an old delivery to the receiver is detectable. - Nothing leaves your server but the webhooks you configured. No telemetry, no vendor SDK, no CDN. ## Troubleshooting ****401 "No valid key"**** The secret is wrong, the key was revoked, or *Expires on* has passed. Send the whole secret including the `bfai_` prefix, or a live `bfa_` access token. ****403 "This key may not use X"**** Add X under *Allowed models*. `GET /api/v1/models` lists what the key has. ****400 "This key may not change these fields"**** Add the field to *Writable fields*, or leave that empty to allow every readable, non-sensitive field. ****202 instead of 201**** The rule says *May propose changes* but not *Write directly*: the change is waiting under *Proposals*. Switch on *Write directly* for systems that should apply changes themselves. ****429 with Retry-After**** The per-minute or daily cap. Raise it on the key, or find the loop in the query log. ****Deliveries stay "Retrying"**** The receiver is not answering `2xx`; open the delivery to see its answer. A `4xx` goes straight to *Dead* - fix the receiver and press *Replay*. ****A webhook fires for a record I did not expect**** Its domain is AND-ed with the key's rule domain; check both. On change, set *Only when these fields change*. ****The OpenAPI file has no paths**** The key has no rules yet. ****404 on /api/... or /api/mcp**** The app is not installed on *that* database, or your reverse proxy strips the path. Test on the Odoo port directly. ****The menu is missing**** You are not in the *AI Bridge / Manager* group. ## Tested against the real thing Automated tests run against Odoo's own test server on every release; the sentence below is the part no test suite can give you - the gateway pointed at real tools, with real records. Tested for real against n8n 2.34.6, Node-RED 5.0.6, Postman (newman), HTTPie and openapi-python-client: a contact created through REST fired a signed webhook into an n8n workflow that wrote a phone number back through REST; an order confirmed through a REST action reached a Node-RED flow with a verifiable signature; a 15-request Postman collection passed 24 of 24 assertions; the OpenAPI document validated with openapi-spec-validator and Redocly and generated a typed Python client that listed the orders - 0 errors. ## What changed in 1.3 - **REST API** on every allowed model: list, read, aggregate, create, change, delete and listed actions, with paging, filters, ETags, idempotency keys and RFC 9457 errors. - **OpenAPI 3.1** per key, and a built-in API explorer at `/api/doc`. - **Webhooks** on create, change and delete, signed, retried on a schedule, with a deliveries log and replay. - **OAuth2 client credentials** with short-lived access tokens. - `/api/mcp` is the MCP endpoint's address (the old address `/bf_ai/mcp` still answers), and `/api/echo` as a test receiver for webhooks. - Rules gained *Write directly*, *May delete* and *Callable actions*; keys gained per-door switches; the query log gained the channel, path, HTTP status and user agent. ## What changed in 1.2 - Proposals: `odoo_propose_create`, `odoo_propose_write` and `odoo_proposal_status`. Nothing is written until a reviewer applies it under their own rights. - Reviewers on the token; a **To review** counter on its form. ## What changed in 1.1 - **A real cost ceiling**: cap the estimated LLM tokens per rolling day, not only the number of calls. Plus a per-minute rate limit for runaway agent loops. - **Grants are audited too**: every change to a token's allowlist lands in the token's chatter, next to the queries it produced. - **The query log expires** (180 days by default, tunable with the `bf_ai_bridge.log_retention_days` system parameter). - Tokens, rules and logs are **isolated per company**, and the query log is readable by AI Bridge managers only. ## How we make sure it works Before every release, **115 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A read only rule cannot propose. - A model without a rule is forbidden not described. - Rule domain cannot be widened by the agent. - A bystander cannot apply or refuse. - A bystander cannot even read it. Tested for real against n8n 2.34.6, Node-RED 5.0.6, Postman (newman), HTTPie and openapi-python-client: a contact created through REST fired a signed webhook into an n8n workflow that wrote a phone number back through REST; an order confirmed through a REST action reached a Node-RED flow with a verifiable signature; a 15-request Postman collection passed 24 of 24 assertions; the OpenAPI document validated with openapi-spec-validator and Redocly and generated a typed Python client that listed the orders - 0 errors. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - questions, bug reports and feature requests are answered by the developers who wrote the module. --- # 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. - **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. --- # Document Retention & Immutable Archive > Keep issued documents as long as the law requires and prove they were never touched: hash chain, retention dates, deletion blocked. - **What it is:** Keep issued documents as long as the law requires and prove they were never touched: hash chain, retention dates, deletion blocked. - **For:** Document Management - **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:** 169 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_archive - **Not for:** The chain is per company and per Odoo database. It proves internal consistency, not that a document existed on a given date — that needs a timestamping authority, which is a different product.; Archiving works on attachments. A document that was never attached to a record is not archived, because there is nothing to archive.; Verifying re-reads every file. On a large filestore that is real I/O, which is why the scheduled check runs weekly rather than hourly. - **Manual:** https://docs.bambooforge.dev/bambooforge_archive/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_archive.action_bf_archive_entry - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_archive - **Manual last updated:** 2026-09-25 --- # Document Retention & Immutable Archive ## What this is for Most EU jurisdictions require accounting documents to be produceable for ten years, unaltered. Odoo stores attachments; it does not prove anything about them, and anyone with delete rights can remove one. ## Policies *Archive ▸ Retention Policies* A policy says what is kept, for how long, and on what legal basis: - the document type and an optional filter — posted invoices only, one journal; - a file-name filter, for when only the issued PDF matters and the working files do not; - the retention period in years; - the legal basis you are relying on. An auditor asks for this before they ask for the documents. Retention runs from the **document's own date**, not from the day somebody got round to archiving it. ## The chain Every entry stores the SHA-256 of the file and commits to the entry before it. A stored hash proves a file has not changed; the chain proves something more useful — that no entry has been removed or reordered either. Verification reports the two failures separately, because they mean different things: - **Altered** — the file's content no longer matches. Somebody edited it in place. - **File missing** — the attachment row is there but the bytes are not. That is what a filestore restored from a bad backup looks like. - **Chain break** — the entry's own hash does not recompute. Something rewrote the archive table directly. Verification runs weekly on its own. Silent corruption is otherwise found months late. ## Deletion While the retention period runs, neither the archive entry nor the underlying attachment can be deleted. Retention a user can undo with the bin icon is not retention. After the period, deletion is allowed — deliberately. Keeping personal data longer than the rule permits is its own breach. ## Handing it to an auditor *Archive ▸ Export Packages* builds a zip holding the files, a manifest with every hash and chain value, and a README explaining how to redo the check with nothing but `sha256sum`. An archive that can only prove itself inside the system holding it proves very little. For an archive too large to move, build the package with **include files** turned off: the manifest and the hashes still change hands. ## Known limits - The chain is per company and per Odoo database. It proves internal consistency, not that a document existed on a given date — that needs a timestamping authority, which is a different product. - Archiving works on attachments. A document that was never attached to a record is not archived, because there is nothing to archive. - Verifying re-reads every file. On a large filestore that is real I/O, which is why the scheduled check runs weekly rather than hourly. ## How we make sure it works Before every release, **27 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The same file is not archived twice. - An archived attachment cannot be deleted. - A plain employee does not see the menu. - An employee reads the archive and touches nothing. - Zero years is not a policy. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_archive` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Document Retention & Immutable Archive**. 3. Open **Archive** - its menus are Archived Documents, Retention Policies, Export Packages. ## 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, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Archive** menu (Archived Documents, Retention Policies, Export Packages). ****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. --- # Audit Log & Audit Trail for Odoo > Audit log and audit trail with field-level tracking, hash chaining, anomaly detection, alerts, PII masking, retention and a compliance PDF. - **What it is:** Audit log and audit trail with field-level tracking, hash chaining, anomaly detection, alerts, PII masking, retention and a compliance PDF. - **For:** Extra Tools - **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:** 229 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_audit_pro - **Not for:** What is stored — for each audited operation: the model, record id, method, acting user, timestamp, HTTP request/session reference, and (for Full log) a before/after value per field. Login events store the attempted login, user, state, IP, user agent and a hashed session token. PII fields you mark are masked at capture.; Where data goes — audit data stays in your Odoo database unless you configure an archive target (file/syslog/S3) or expose the pull API. Webhooks send only the alert summary payload to the URL you configure.; Multi-company — logs are isolated by company; users see only the companies they belong to.; In scope — change/login auditing, anomaly rules, approval workflow, alerts with email + Slack/Discord/Teams/generic webhooks, archive targets, retention, hash-chain verification, GDPR anonymization, XLSX export, JSON-Lines pull API.; Out of scope — the approval workflow intercepts write on sensitive fields, not create or delete. The S3 backend needs boto3. Webhook URLs must be public. - **Manual:** https://docs.bambooforge.dev/bambooforge_audit_pro/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_audit_pro.action_audit_dashboard - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_audit_pro - **Manual last updated:** 2026-10-15 --- # Audit Log & Audit Trail for Odoo A complete audit trail for Odoo 20: log every create, write, delete and read on the models you choose, watch logins, raise alerts on suspicious activity, gate sensitive edits behind an approval workflow, push alerts to Slack/Discord/Teams, and archive logs off-system before they are purged — all from one **Audit** app. This page is the complete manual. If you follow it top to bottom you can install, choose what to audit, read the logs, wire up alerts and approvals, ship logs out, and fix the common issues **without contacting support**. ## Overview Audit Pro hooks the Odoo ORM so that operations on the models you subscribe are recorded, then layers compliance tooling on top. It covers: - **Audit trail of record changes** — create, write, delete and (optionally) read events on any model you pick, with a per-field before/after diff. - **Login tracking** — every successful, failed and blocked login is recorded with IP, user agent and a hashed session token, automatically, with no rule setup. - **Anomaly rules** — raise an alert when too many events of one kind happen in a short window (for example a single user doing hundreds of writes or deletes in minutes). - **Approval workflows** — writes that touch fields you mark *Sensitive* are intercepted and parked as approval requests until an approver in the configured group accepts them. - **Alerts, email & webhooks** — alert rules match a slice of the trail and notify by email and/or HTTP webhook (Slack, Discord, Microsoft Teams, or generic JSON). - **Archive targets & retention** — ship logs to a local file, syslog or S3-compatible storage before a retention cron deletes them. - **Dashboard & timeline** — a KPI dashboard over a date window, plus a single chronological stream that merges record events and field changes. - **Tamper-evidence & privacy** — each log is chained to the previous one with a hash you can verify, PII fields are masked at capture, and logs can be anonymized for GDPR. Everything lives under one top-level **Audit** menu, grouped into **Audit Logs**, **Security**, **Alerts** and **Configuration**. ## Requirements | Item | Detail | |---|---| | Odoo | 20.0, Community or Enterprise. | | Python | `xlsxwriter` (used by **Export Logs** to write XLSX). It is the only extra dependency. The S3 archive backend also needs `boto3`, but only if you actually configure an S3 target — it is never required at install. | | Depends | `base`, `web`, `mail` (all standard). No third-party Odoo modules. | | Access rights | The **Auditlog User** group can read logs, dashboard and timeline. The **Auditlog Manager** group can configure rules, approvals, webhooks and archives, and may anonymize logs. Odoo *Administration / Settings* users (`base.group_system`) get Manager automatically. | | Network | Outbound HTTPS is only needed if you use webhooks or the S3 archive backend. Webhook URLs must be public — internal/loopback/private hosts are refused (SSRF guard). | ## Installation 1. Copy `bambooforge_audit_pro` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *Audit Pro*, and press **Activate / Install**. The dependencies install automatically. 4. After install, an **Audit** application appears in the main app drawer. A post-install hook re-applies any active audit rules so logging starts immediately. A guided **Getting Started** wizard is available under **Audit ▸ Configuration ▸ Getting Started**: three steps that subscribe a few rules, optionally wire one notification channel, and set a retention window. You can skip it and configure everything by hand using the sections below. ### Access snapshots and reviews *Audit ▸ Security ▸ Access Snapshots* freezes what every internal user may do: their groups, a fingerprint of those groups, and whether they hold administrator rights. Take one on demand or leave the scheduled action to do it, then open a later snapshot and press **Compare** to see who gained rights, who lost them, who joined and who left — users whose fingerprint is unchanged are deliberately not listed. *Audit ▸ Security ▸ Access Reviews* turns a snapshot into the evidence an auditor asks for: one decision per user — keep or revoke — each stamped with the reviewer and the moment they answered. A campaign cannot be completed while a user is unanswered. **Apply revocations** strips only the groups the reviewer refused (leave the group list empty to remove everything the user held at snapshot time), records who applied it, and refuses to let a reviewer revoke their own access in a campaign they are signing. ## Configuration All configuration lives under the **Audit** app. The menu groups are: | Menu group | Contains | |---|---| | **Audit Logs** | Logs, Log Lines, Timeline, Export Logs. | | **Security** | Login Events, Anomaly Rules. | | **Alerts** | Alerts, Alert Rules. | | **Configuration** | Getting Started, Rules, Audit Models, Compliance Presets, Webhooks, Webhook Queue, Archive Targets. Approvals sit directly under **Audit**. | ### Step 1 — Choose what to audit (audit rules) Open **Audit ▸ Configuration ▸ Rules** and create a record (one rule per model — the model is unique across rules). ![Audit rule form — model, operations, exclusions and the Phase B tabs](rule_form.png) | Field | What it does | |---|---| | **Model** | The model to track. Locked once the rule is *Subscribed*. | | **Type** | *Full log* makes a before/after diff of every field (richer, slower); *Fast log* records only the values passed to create/write (less detail, faster). | | **Log Reads / Writes / Deletes / Creates** | Which operations generate logs. Writes, Deletes and Creates are on by default; Reads is off (it is the noisiest). | | **Capture Record** | On *Full log* + *Log Deletes*, keep a snapshot of a record's values when it is deleted. | | **Users to Exclude** | Operations by these users are never logged (for example an integration user). | | **Fields to Exclude** | Fields that never appear in the diff. | The rule form also carries three tabs: - **PII Masking** — *Fields to Mask (PII)*. Values of these fields are masked when stored in audit lines (emails become `a***@b***.c`, phone numbers keep the country code and last two digits, everything else becomes `***`). - **Approval Workflow** — *Sensitive Fields (require approval)*, *Approver Group* and *Approval Timeout (hours)* (default 72). See Step 4. - **Retention & Archive** — *Retention Days* (0 = use the global autovacuum, 180 days by default) and *Archive Target*. See Step 6. Click **Subscribe** in the header to activate the rule. The state moves to *Subscribed* and an **Audit History** shortcut is added to the Action menu of the audited model's form. Click **Unsubscribe** to stop logging; you must unsubscribe before changing the model, operations or masking/sensitive fields. Faster starts: - **Audit Models** (**Configuration ▸ Audit Models**) subscribes several models at once with the same settings. - **Compliance Presets** (**Configuration ▸ Compliance Presets**) bootstraps **GDPR**, **SOX** or **PCI-DSS** in one click — it creates and subscribes the right rules with masking, sensitive fields and retention already filled in, and skips models that are not installed on your database. ### Step 2 — Anomaly rules Open **Audit ▸ Security ▸ Anomaly Rules**. An anomaly rule raises an alert when the number of matching audit events crosses a threshold inside a time window. ![Anomaly rule form — scope, threshold and window](anomaly_rule_form.png) | Field | What it does | |---|---| | **Model** | Scope to one model. Empty = all models. | | **Method** | Scope to Create / Write / Read / Delete. Empty = all. | | **User** | Scope to one user. Empty = any user. | | **Threshold (events)** | Raise an alert when this many matching events occur within the window (default 100). | | **Window (minutes)** | The rolling window length (default 10). | | **Severity** | Low / Medium / High / Critical (labelling only). | | **Alert channel** | The alert rule whose email + webhook channels fire on a breach. Leave empty and one is auto-created on the first breach. | A cron (**Audit anomaly — evaluate thresholds**, every 5 minutes) walks the active rules. The *Last evaluation* fields show the last run time and event count. Breaches are deduplicated so you do not get repeat alerts inside the same window. ### Step 3 — Alert rules, email and webhooks An **Alert Rule** (**Audit ▸ Alerts ▸ Alert Rules**) matches a slice of the trail and raises an **Alert**. Combine **Model**, **User**, **Method**, **Field** and **Keyword** (the alert fires only when the changed values contain that keyword) to narrow it. On the alert rule, the **Notification Channels** section controls how a match is shipped: - **Send Email** — uses an email template (defaults to the bundled audit-alert template) and an optional comma-separated **Email Recipients** override. Mail is queued, not sent inline, so a slow SMTP server never blocks the user write that triggered the alert. - **Webhooks** — one or more webhook records to call. Dispatch is queued (see Step 5). To create a webhook, open **Audit ▸ Configuration ▸ Webhooks**. ![Webhook form — backend, URL, headers and HMAC secret](webhook_form.png) | Field | What to enter | |---|---| | **Backend** | *Slack*, *Discord*, *Microsoft Teams* or *Generic JSON*. This shapes the payload to the receiver's expected format. | | **URL** | The incoming webhook URL. Must be a public host — internal/loopback/private addresses are rejected. | | **Timeout** | Per-call timeout in seconds (default 5). | | **Extra Headers (JSON)** | Optional JSON object merged into the request headers, e.g. `{"Authorization": "Bearer xxx"}`. | | **HMAC Signature Secret** | If set, each request carries an `X-BFAudit-Signature: sha256=…` header so the receiver can verify the body was not altered. | Use the **Send Test** button to POST a synthetic payload and confirm connectivity before attaching the webhook to an alert rule. The form shows the last status, last call time and call count. ### Step 4 — Approval workflow Approvals turn a risky edit into a reviewed action. On an audit rule's **Approval Workflow** tab, add fields to **Sensitive Fields (require approval)**, pick an **Approver Group** and set an **Approval Timeout (hours)**. From then on, when any user tries to `write` one of those fields, the write is blocked and a **bf.audit.approval.request** is created instead. Approvers find pending requests under **Audit ▸ Approvals**. ![Approval request form — side-by-side current vs requested diff](approval_form.png) The request shows a side-by-side **Current vs Requested** diff. An approver in the group clicks **Approve** to apply the change for real, **Reject** to discard it, or **Open Record** to inspect the target. Requests left pending past the rule's timeout are auto-expired by an hourly cron (**Audit approval — expire pending**). If applying an approved change fails (the record was deleted, the value is invalid…), the request moves to *Apply Error* with the reason recorded. ### Step 5 — Webhook queue Webhook dispatch is asynchronous: when an alert fires, each webhook call is enqueued in **Audit ▸ Configuration ▸ Webhook Queue** and a cron (**Audit webhook — drain queue**, every minute) sends it with retry/back-off. This guarantees a slow or failing endpoint can never block the user write that triggered the alert. Each queue row shows its state (pending / sending / sent / failed), retry count and last error; **Retry Now** forces an immediate attempt. ### Step 6 — Archive targets & retention Logs are deleted on a schedule; archive targets let you keep a durable copy first. Create a target under **Audit ▸ Configuration ▸ Archive Targets**, then set it as the **Archive Target** on the relevant audit rule. ![Archive target form — file, syslog or S3 backend](archive_target_form.png) | Backend | What to configure | |---|---| | **Local File** | **File Path** — a directory on the Odoo host. One JSON-Lines file is written per batch. For safety the path must sit inside the base directory set by the `bambooforge_audit_pro.archive_base_dir` system parameter (defaults to `/bf_audit_archive`); paths outside it are rejected. | | **Syslog** | **Syslog Host**, **Port** (514) and **Facility** (local0–local7). | | **S3 / S3-compatible** | **Bucket**, **Prefix**, **Region**, optional **Endpoint URL** (for MinIO / Wasabi / R2), and access/secret keys. Requires the `boto3` package. | Use **Send Test** to ship one synthetic record and confirm the target works; the form records the last status, time and shipped count. Retention is driven by two crons: | Cron | Default | What it does | |---|---|---| | **Auto-vacuum audit logs** | daily, **off** | Deletes logs, HTTP requests, HTTP sessions, login events and old webhook-queue rows older than 180 days. Disabled out of the box — enable it to use the global window. | | **Audit per-rule retention vacuum** | daily, on | Deletes logs older than each subscribed rule's **Retention Days** (0 = skip). | | **Audit approval — expire pending** | hourly, on | Auto-expires approval requests past their timeout. | | **Audit webhook — drain queue** | every minute, on | Sends queued webhook calls with retry/back-off. | | **Audit anomaly — evaluate thresholds** | every 5 min, on | Evaluates anomaly rules. | Both vacuums ship logs to their rule's archive target *before* deleting them, so a configured archive is always durable first. ## Usage ### Dashboard Open **Audit ▸ Dashboard**. Pick a **From** / **To** window; the KPIs recompute live: Total Logs, Field Changes, Active Users, Triggered Alerts, Open Alerts, Pending Approvals, Logins OK and Logins Failed, plus the Top Model and Top User for the window. ![Audit dashboard — KPI tiles and highlights over a date window](dashboard.png) The action bar drills into the same window: **Open Logs**, **Timeline**, **Alerts** and **Approvals**. **Refresh** reloads the figures. ### Logs **Audit ▸ Audit Logs ▸ Logs** lists every captured operation (default filter: last 7 days). Each row is one operation on one record; open it to see the **Changed Fields** tab with the before/after value of each field and a per-field diff preview. ![Audit logs list with method, model, user and resource columns](logs.png) Group by **User**, **Model**, **Method**, **Type**, **User session** or **HTTP Request** to slice the trail. The log form also links to its **Alerts** and a record-scoped **Timeline**. From the list, two server actions are available in the Action menu: **Anonymize (GDPR)** (managers only — wipes field values to `***` but keeps the envelope) and **Verify Hash Chain** (confirms the tamper-evidence chain is intact). **Log Lines** (**Audit Logs ▸ Log Lines**) is the same data flattened to one row per field change. **Export Logs** writes a filtered range to XLSX. ### Login events **Audit ▸ Security ▸ Login Events** records every authentication attempt with IP, user agent, a hashed session token and (on failure) the reason — no rule needed. Rows are colour-coded: green for success, red for failed, amber for blocked. ![Login events list, colour-coded by success / failed / blocked](login_events.png) Default filter is the last 7 days; filter by Success / Failed / Blocked or group by User, State or IP. ### Timeline **Audit ▸ Audit Logs ▸ Timeline** merges record events and individual field changes into one chronological stream. Each entry shows the time, user, method, model, the changed field (if any) and a summary with a change type of *Added*, *Removed* or *Changed*. ![Activity timeline — record events and field changes in one stream](timeline.png) Use the **Field Changes** / **Record Events** filters to focus, or switch to the **pivot** view to chart events by day and model. ## Customization - **Field coverage** — override `auditlog.rule.get_auditlog_fields` to change which fields a rule captures (by default all stored, non-computed/related fields). - **Generic webhooks** — set a webhook's backend to *Generic JSON* to receive the full alert payload, and use **Extra Headers** and **HMAC Signature Secret** to integrate with any HTTP receiver. - **SIEM / log shipper pull** — a read endpoint at `GET /api/bf_audit/logs` streams logs as JSON Lines for Splunk, ELK/Logstash, Datadog, Loki or Grafana Agent. It accepts `since`, `limit` (default 1000, max 10000) and `model` query parameters. Authenticate with the `X-Audit-API-Key` header, matching either the `bambooforge_audit_pro.api_key` system parameter (full export) or a per-user key set under a user's **Audit API** tab (results are then scoped to that user's access rights). - **Compliance presets** — the GDPR / SOX / PCI-DSS presets are a starting point; after applying one, edit the generated rules to fit your data model. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Nothing is logged | The rule is still in *Draft*. Open it and click **Subscribe**. Logging only starts once the state is *Subscribed*. | | A specific operation is not logged | The matching **Log Reads / Writes / Deletes / Creates** toggle is off, the acting user is in **Users to Exclude**, or the field is in **Fields to Exclude**. Reads are off by default. | | Too much is logged / the system feels slow | *Full log* and especially **Log Reads** are heavy. Switch the rule to *Fast log*, turn Reads off, exclude noisy fields/users, and scope anomaly rules tightly. The dashboard's Top Model / Top User show where the volume comes from. | | Logs grow without bound | Retention is opt-in. Set **Retention Days** per rule (the per-rule cron is on), or enable the **Auto-vacuum audit logs** cron for the global 180-day window. | | A webhook never fires | Check the alert rule actually lists the webhook, that the **Webhook Queue** row is not stuck in *failed* (read its last error), and that the **Audit webhook — drain queue** cron is active. Use **Send Test** on the webhook to isolate connectivity. | | Saving a webhook is refused | The URL points at an internal/loopback/private host. Webhooks must use a public `http(s)://` endpoint (SSRF guard). Slack/Discord/Teams cloud URLs are fine. | | An approval is stuck / a user "cannot save" | The field is marked **Sensitive**; the write became an approval request under **Audit ▸ Approvals**. An approver in the rule's **Approver Group** must Approve or Reject. Requests past the timeout auto-expire hourly. If approval shows *Apply Error*, the target record changed — re-do the edit. | | Approve/Reject buttons do nothing for me | You are not in the rule's **Approver Group**. Add yourself, or have an **Auditlog Manager** decide. | | "Permission denied" on Anonymize | Anonymizing requires the **Auditlog Manager** group. | | The API endpoint returns 401 / 403 | 401 = missing or wrong `X-Audit-API-Key` (check the system parameter or the per-user key). 403 = a per-user key whose user lacks audit-log read rights, or whose **Audit API** scope is empty. | | S3 archive test fails with a boto3 error | The S3 backend needs the `boto3` package installed in the Odoo environment. Install it, or use the **Local File** or **Syslog** backend instead. | | A user sees fewer logs than expected | Logs are multi-company isolated: a user only sees logs of companies they belong to (company-less logs stay visible to all). | ## Frequently asked questions **Does this slow Odoo down?** Logging adds work to each audited operation, so only subscribe the models and operations you need. *Fast log* is lighter than *Full log*, and **Log Reads** is the heaviest option (off by default). Webhooks and alert emails are queued, never inline. **Can I audit reads, not just changes?** Yes — turn on **Log Reads** on the rule. Expect high volume; reserve it for sensitive models. **How do I prove the logs were not tampered with?** Every log is hash-chained to the previous one. Select logs and run **Verify Hash Chain** from the Action menu to confirm the chain is intact. **How do I comply with a GDPR erasure request?** Select the relevant logs and run **Anonymize (GDPR)** (managers only). Field values become `***` while the date/user/method/model envelope is kept for forensic integrity. You can also mask PII at capture time via the rule's **PII Masking** tab. **Can alerts go to Slack / Teams / Discord?** Yes. Create a webhook with the matching backend, test it, and attach it to an alert rule's **Webhooks** field. *Generic JSON* covers any other HTTP receiver. **Where do archived logs go?** To the archive target on each rule: a local JSON-Lines file, syslog, or an S3-compatible bucket. Archiving happens before the retention cron deletes the rows. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - **What is stored** — for each audited operation: the model, record id, method, acting user, timestamp, HTTP request/session reference, and (for Full log) a before/after value per field. Login events store the attempted login, user, state, IP, user agent and a *hashed* session token. PII fields you mark are masked at capture. - **Where data goes** — audit data stays in your Odoo database unless you configure an archive target (file/syslog/S3) or expose the pull API. Webhooks send only the alert summary payload to the URL you configure. - **Multi-company** — logs are isolated by company; users see only the companies they belong to. - **In scope** — change/login auditing, anomaly rules, approval workflow, alerts with email + Slack/Discord/Teams/generic webhooks, archive targets, retention, hash-chain verification, GDPR anonymization, XLSX export, JSON-Lines pull API. - **Out of scope** — the approval workflow intercepts `write` on sensitive fields, not create or delete. The S3 backend needs `boto3`. Webhook URLs must be public. ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_audit_pro -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (17 models, prefixed `auditlog.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - The security groups this module defined (user assignments to them are dropped). ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.5.8.5 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.5.5.0 Current release for Odoo 20.0. This build includes: - Contextual anomaly detection, one-click GDPR/SOX compliance attestation PDF and a daily tamper tripwire on top of hash-chained audit logs, alerts, webhooks, PII masking, approval workflow, retention policy and external archiving. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **122 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Review cannot complete while users are unanswered. - Reviewer cannot revoke their own access. - A cached id from another database is not believed. - Diff without an earlier snapshot is refused. - Archive test action does not raise. The complete list is in the source you receive. --- # Backend Theme for Odoo Community > A modern, fully responsive backend theme for Odoo Community: dark mode, per-user accent colours, drag-to-reorder apps and pinned favourites. - **What it is:** A modern, fully responsive backend theme for Odoo Community: dark mode, per-user accent colours, drag-to-reorder apps and pinned favourites. - **For:** Extra Tools - **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_backend_theme - **Not for:** Community backend only. The theme targets Odoo 20 Community and excludes the Enterprise web client; it does not run side-by-side with Enterprise.; Backend, not website. It restyles the internal web client; it is not a website or eCommerce theme and does not change the public-facing site.; Personalization is per user. There is no single global theme switch or forced company-wide color; defaults are set per user.; One backend theme at a time. Run only one backend theme; multiple themes that restyle the same web client will conflict. - **Manual:** https://docs.bambooforge.dev/bambooforge_backend_theme/ - **Live demo:** https://demo20.bambooforge.dev/odoo - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_backend_theme - **Manual last updated:** 2026-10-15 --- # Backend Theme for Odoo Community A modern, fully responsive backend theme for Odoo 20 Community that lets **every user make Odoo their own** — a one-click dark mode, a personal accent color, a full-screen app grid with fuzzy search, plus drag-to-reorder and pinned favorite apps. It is a polished fork of OCA *web_responsive* with a per-user appearance engine on top. This page is the complete manual. If you follow it top to bottom you can install the theme, enable it, set every option, and fix the common issues **without contacting support**. ## Overview The theme restyles the Odoo 20 **backend** (the internal web client) and adds the personalization features usually reserved for Enterprise: - **Dark mode (per user)** — a true backend dark theme. Each user chooses **Light**, **Dark**, or **Auto (follow system)**, and the choice is saved on their own account. - **Personal accent color** — pick one of six ready-made palettes or any custom hex. Buttons, links, active menus, tabs, toggles and the selected status-bar stage all recolor live, with no recompiling and no administrator involved. - **Full-screen app grid with fuzzy search** — a card grid of apps with a Fuse.js fuzzy search across apps and deep menus, full keyboard navigation and hotkeys. - **Drag-to-reorder apps** — rearrange the home apps grid by drag & drop; the order is remembered per user. - **Pinned favorite apps** — star the apps you use most and they float to the top of the grid. - **Fully responsive layout** — a mobile-first layout for phone, tablet and desktop, a touch-tuned command palette and chatter, sticky control panel and list headers, roomier forms and a polished file viewer. ![Home apps grid in light mode](shot_home_light.png) ![Home apps grid in dark mode](shot_home_dark.png) Every appearance choice is stored on the user's own record (`res.users`) and is exposed to the web client at login, so the theme is applied before the first paint — there is no flash of the old style on reload. ## Requirements | Item | Detail | |---|---| | Odoo | 19.0 **Community Edition**. The module declares `excludes: web_enterprise`, so it does not install alongside the Enterprise web client. | | Scope | This is a **backend** theme: it restyles the internal web client only. It is not a website/eCommerce theme and does not change the public website. | | Dependencies | The Odoo **web** and **mail** modules (both part of a standard install). No extra Python libraries are required. | | Access | Any internal user can set their own appearance. No special group is needed. | | Browser | A current evergreen browser (Chrome, Firefox, Edge, Safari). Dark mode uses the CSS `prefers-color-scheme` media query for the *Auto* option. | ## Installation 1. Copy `bambooforge_backend_theme` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for `bambooforge_backend_theme` and press **Activate / Install**. The **web** and **mail** dependencies install automatically. The theme **applies automatically** the moment it is installed: it is loaded as part of the backend asset bundle (`web.assets_backend`), so there is no switch to flip to "turn the theme on". On your next page load the backend already uses the new layout. From there, each user personalizes their own appearance (next section). There is nothing to configure on a second platform — this is a self-contained Odoo theme with no companion app and no external service. ## Options & personalization Every option below is **per user** — your choice never changes anyone else's screen. ### Where you set them There are two places, plus a one-click systray shortcut: 1. **Preferences** (the avatar menu, top-right ▸ **Preferences**). This is the main place. Three fields are added right under your **Timezone**: - **Backend Appearance** — Light / Dark / Auto (follow system). - **Accent Preset** — one of six palettes, or *Custom*. - **Accent Color** — the live hex color picker (used when the preset is *Custom*, or to fine-tune any preset). ![Appearance fields on the Preferences dialog](shot_prefs.png) 2. **Settings ▸ Users ▸ (a user)** — an administrator sees the same three fields on any user's form and can set a starting appearance for them. 3. **The systray dark-mode toggle** — a sun / moon button in the top bar that flips between **Light** and **Dark** instantly and saves the choice to your account. ![Accent preset and color picker in Settings](shot_settings_accent.png) ### Dark mode **Backend Appearance** has three values: | Value | Behavior | |---|---| | Light | The default. Standard light backend. | | Dark | A true dark theme (dark backgrounds, light text) scoped so the light theme is never touched. | | Auto (follow system) | Follows your operating system's light/dark setting and flips live when the OS setting changes. | The systray **sun / moon** button is the fast path: it toggles Light ⇄ Dark and persists the choice immediately — no need to open Preferences. ![A form view rendered in dark mode](shot_form_dark.png) ![A list/kanban view rendered in dark mode](shot_crm_dark.png) ### Accent color The **Accent Preset** field offers six ready-made palettes plus *Custom*: | Preset | Hex | Notes | |---|---|---| | Seagreen | `#1A7D4C` | The default accent. | | Ocean | `#1F6FEB` | A clear blue. | | Violet | `#7B3FE4` | A purple accent. | | Sunset | `#E8590C` | A warm orange. | | Ruby | `#C2255C` | A pink/red accent. | | Graphite | `#3A4047` | A neutral, low-key gray. | | Custom | any hex | Keeps whatever color you set in **Accent Color**. | Picking a preset fills the **Accent Color** hex automatically; choosing *Custom* leaves the hex as-is so you can dial in any brand color with the picker. The accent drives the highlight color of buttons, links, active menus, notebook tabs, checkboxes, switches, the focused search box and the selected status-bar stage. Recoloring happens **live** — the bundle is compiled once and the color is applied at runtime as a CSS variable, so there is no SCSS recompile and no administrator step. ![Backend recolored with the Ocean accent](shot_accent_ocean.png) ![Backend recolored with the Seagreen accent](shot_accent_seagreen.png) ![Backend recolored with the Violet accent](shot_accent_violet.png) While you drag the color picker, the whole backend previews the color live; it is saved to your account only when you save the Preferences form. ## Usage ### The apps menu Click the **apps / home** button in the top-left to open a full-screen card grid of every app you can access. ![Full-screen apps grid](shot_apps.png) - **Fuzzy search** — start typing in the search box at the top. It searches across app names *and* their deep menu entries with a forgiving (fuzzy) match, so a near-miss or typo still finds the menu. Results update as you type. ![Fuzzy search across apps and deep menus](shot_apps_search.png) - **Keyboard navigation** — use the arrow keys to move between cards and **Esc** to close the grid, so you can drive the launcher without the mouse. ### Reorder and pin apps - **Drag to reorder** — with the apps grid open, drag any card to a new position. The new order is saved to your account and used every time you open the grid. A newly installed app that has no saved position simply appears at the end. - **Pin a favorite** — hover a card and click its **star** to pin it. Pinned apps float to the top of the grid (keeping their relative order); click the star again to unpin. ![Pinned favorite apps floating to the top of the grid](shot_pinned.png) Both the personal order and the pinned set are stored on your own user record, so they follow you across devices and sessions. ### On mobile The layout is mobile-first: the navigation collapses for small screens, the control panel and list headers stay sticky as you scroll, and the command palette and chatter are tuned for touch. Dark mode and your accent apply on mobile exactly as on desktop. ![The backend on a phone-sized screen](shot_mobile.png) ## Customization For administrators who want to influence defaults: - **Default appearance for new users** — the out-of-the-box defaults are **Light** appearance and the **Seagreen** accent. To start a user somewhere else, open **Settings ▸ Users ▸ (the user)** and set **Backend Appearance**, **Accent Preset** and **Accent Color** on their form. - **A company brand color** — set the **Accent Preset** to *Custom* and enter your brand hex in **Accent Color** on each user (or as part of your user-provisioning template). Because the accent is a runtime CSS variable, no SCSS edit is needed for a brand color. - **Deeper SCSS changes** — the dark palette and the accent surfaces live in `static/src/scss/bf_dark.scss` and `static/src/scss/bf_accent.scss`, and the full **OPL-1 source** ships with the module, so a developer can adjust the SCSS and rebuild the assets if a deeper restyle is required. The shipped behavior needs no such edit. Note: appearance is intentionally **per user**. There is no single global on/off switch — the theme is always active once installed, and each user controls their own look. ## Troubleshooting | Symptom | Cause and fix | |---|---| | The theme does not look applied after install | The backend asset bundle is cached. Hard-refresh the browser (Ctrl/Cmd+Shift+R), then, if needed, regenerate assets: **Settings ▸ (developer mode) ▸ Technical ▸ Regenerate Assets Bundles**, or restart Odoo with `--dev=assets` while testing. | | Dark mode does not stick after reload | The toggle saves to your own user record. If you are an impersonated/demo session or a public user, the write may be blocked — sign in as your real internal user. Also confirm your browser is not clearing localStorage on exit (used only to avoid a flash; the saved preference still lives on your account). | | Auto mode never switches to dark | *Auto* follows the operating system. Set your OS/browser to dark and ensure the browser supports `prefers-color-scheme`; otherwise pick **Dark** explicitly. | | The module will not install ("excludes web_enterprise") | This is a **Community** backend theme and excludes the Enterprise web client. On an Enterprise database it will not install; use it on Community Edition. | | Conflicts with another backend theme | Two backend themes that both restyle the web client will fight over the same components. Install only one backend theme at a time; uninstall the other, then regenerate assets. | | The accent color does not change | The accent is saved when you **save** the Preferences form (the picker only *previews* while dragging). Save the form; if it still looks unchanged, hard-refresh to clear the asset cache. A *Custom* preset uses exactly the hex in **Accent Color**. | | Mobile layout looks broken / zoomed | A stale cached bundle from before install. Hard-refresh on the device and confirm the page is not pinned-zoomed; the layout is responsive once fresh assets load. | | A drag reorder or pin did not save | Reordering and pinning write to your own user record. If you are in a read-only or impersonated session the write is rejected — use your real internal user and retry. | ## Frequently asked questions **Which Odoo versions does it support?** Odoo 20.0 Community Edition. It excludes the Enterprise web client by design. **Is this a website theme?** No. It restyles the **backend** (internal web client) only. The public website is unchanged. **Do I have to switch the theme on after installing?** No. It applies automatically as part of the backend assets. Each user then personalizes their own dark mode and accent. **Is each user's choice private?** Yes. Appearance, accent, app order and pinned apps are all stored per user, so your settings never change anyone else's screen. **Which accent colors are available?** Six presets — Seagreen (default), Ocean, Violet, Sunset, Ruby, Graphite — plus a *Custom* hex you can set with the color picker. **Where do I turn on dark mode?** Either the sun / moon button in the systray (instant toggle) or **Preferences ▸ Backend Appearance**, where you can also choose *Auto (follow system)*. **Can an admin set a company-wide brand color or default?** An admin can set each user's starting appearance and accent on **Settings ▸ Users**. There is no single forced global color, because appearance is per user by design; for a brand color, set *Custom* + your hex on the users (or your provisioning template). **Does it work on phones and tablets?** Yes. The layout is mobile-first, with a touch-tuned command palette and chatter, and dark mode plus accent apply on mobile too. **What happens when I uninstall it?** Uninstall is clean and reversible — the backend returns to standard Odoo styling. **What support and refund policy do I get?** Every request is answered within 24 hours. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Limits - **Community backend only.** The theme targets Odoo 20 Community and excludes the Enterprise web client; it does not run side-by-side with Enterprise. - **Backend, not website.** It restyles the internal web client; it is not a website or eCommerce theme and does not change the public-facing site. - **Personalization is per user.** There is no single global theme switch or forced company-wide color; defaults are set per user. - **One backend theme at a time.** Run only one backend theme; multiple themes that restyle the same web client will conflict. ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if it is not resolved within 15 days, you get a full refund. - Full **OPL-1** source is included. Updates track the supported Odoo 20 Community line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_backend_theme -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The fields, views and analysis definitions this module added. - The menus, actions, views and reports this module installed. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.3.0 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.1.0 Current release for Odoo 20.0. This build includes: - Dark mode, per-user accent colors, drag-to-reorder apps & pinned favorites. - A modern, fully responsive backend theme for Odoo 20 Community. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **24 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A corrupted preference cannot break the grid. - Pinned apps are not folded into a section. - Auto without a browser is light. - A section made in the browser is still there after a reload. - A very long name is cut rather than stored. The complete list is in the source you receive. --- # Backend Theme All in One with Website Styling > The apps button on your website opens the Home Menu you arranged, instead of a dropdown list of every app. - **What it is:** The apps button on your website opens the Home Menu you arranged, instead of a dropdown list of every app. - **For:** Website - **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_backend_theme_website - **Manual:** https://docs.bambooforge.dev/bambooforge_backend_theme_website/ - **Live demo:** https://demo20.bambooforge.dev/web/login?redirect=%2F - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_backend_theme_website - **Manual last updated:** 2026-10-15 --- # Backend Theme All in One with Website Styling While you are logged in, Odoo draws a small grid button on every page of your website, titled *Go to your Odoo Apps*. Clicking it opens a dropdown that lists every root menu on the database. With eight apps that is a menu. With eighty it is a single column taller than the screen, in no order you chose, with no icons and no search — and the Home Menu you already arranged is one navigation away, unused. This module makes the button do what its title says: it takes you to the Home Menu. ![The apps button on a website page, in the top-left corner](01-website-button.png) Clicking it lands you on the Home Menu, the grid you already arranged: ![The Home Menu the button now opens](02-home-menu.png) ## What you get - The grid button on any website page opens the Home Menu. - You land on your own arrangement: your app order, your named sections, your pinned apps at the top, and the search box. - Odoo's dropdown of every root menu is gone from that button. Nothing else changes. No JavaScript is added to the website, no model is created, and nothing runs on a page a customer sees — the button this adjusts is rendered only for logged-in staff. ## Requirements - **BambooForge Backend Theme** — the Home Menu, the sections and the pinned shelf live there. - Odoo's **Website** app. The module installs itself once both are present and does nothing at all before that, which is why it is a separate module: a backend theme should not drag the Website app in behind it. ## Installing 1. Copy the module into your addons path. 2. Update the apps list. 3. It installs on its own if Backend Theme and Website are both installed; otherwise it waits. ## Uninstalling Odoo's dropdown comes back exactly as it was. The module stores nothing and changes no data, so there is nothing to clean up. ## How it works One inherited view over `website.layout`. It sets the button's `href` to the backend, removes the `data-bs-toggle` that made it a dropdown toggler, and drops the generated list of menus. That is the whole module, and you can read it in `views/website_templates.xml`. ## How we make sure it works Before every release, **8 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - It owns one view and nothing else. - It adds no javascript to the website. - It installs itself only with both apps. - The button no longer opens a dropdown. - The public never sees the button. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_backend_theme_website` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Backend Theme: Website Bridge**. ## 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 Website; backend theme (BambooForge) must be installed first app. No extra Python package. ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) --- # Bagisto Connector > Two-way Bagisto integration: products, customers, orders, refunds and stock over the Bagisto Admin REST API, with a resilient sync queue. - **What it is:** Two-way Bagisto integration: products, customers, orders, refunds and stock over the Bagisto Admin REST API, with a resilient sync queue. - **For:** eCommerce - **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:** 199 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_bagisto_connector - **Run for real:** Tested for real against Bagisto 2.0 and the current Bagisto 2 release: products created through the Admin REST API and three orders placed at the storefront checkout came into Odoo with totals identical to the store, 0 sync errors. - **Not for:** The connector reads products, customers, orders and order slips (refunds) from your store and writes the corresponding Odoo records. It can also export product changes back to Bagisto. Credentials and the webhook secret are stored in admin-only fields.; In scope today: product/customer/order import, product export, order-status mapping, refund discovery, scheduled sync, signed webhooks.; Out of scope / best-effort: tax auto-matching (off by default, name-based, never auto-creates taxes); multi-warehouse stock routing; subscription/booking order types. - **Manual:** https://docs.bambooforge.dev/bambooforge_bagisto_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_bagisto_connector.action_bagisto_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_bagisto_connector - **Manual last updated:** 2026-10-15 --- # Bagisto Connector A two-way bridge between Odoo 20 and your Bagisto store: import products, customers and orders, push product changes back, and discover refunds — with a resilient job queue, dry-run safety, validation and rollback so you stay in control. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first sync, automate it, and fix the common issues **without contacting support**. ## Matching the storefront total A storefront charges shipping and grants discounts outside the product lines, so an order rebuilt from those lines alone is worth less than the order the customer paid for: the sale looks smaller than it was and never reconciles against the platform payout. With **Match Storefront Totals** on the instance (on by default): - the shipping charge is imported as its own order line, on a clearly named service product; - whatever still separates Odoo from the platform total — platform discounts, fees, rounding — is posted as a single adjustment line rather than dropped; - the platform total and the remaining difference are stored on the order and shown on its connector tab; - an order that still does not match within **Total Tolerance** raises a validation warning, so the gap is visible instead of silent. Imported lines deliberately carry no Odoo tax: the storefront has already computed tax and it is kept on the order's own tax amount field. Switch the option off if you would rather Odoo recompute everything from your own price lists and tax rules. ### Overview The connector keeps Odoo and Bagisto in sync over the Bagisto Admin REST API (the `bagisto/rest-api` package). It covers: - **Products** — import Bagisto products into Odoo product templates (optionally with stock, images, categories and tax group), and export Odoo product changes back to the store. - **Customers** — import Bagisto customers (and their billing/shipping addresses) into Odoo contacts. - **Orders** — import Bagisto orders into Odoo sale orders, mapping each order's Bagisto status to a sale-order action (keep draft, confirm, cancel, or ignore). - **Refunds** — optionally discover Bagisto order slips (credit notes) attached to an order and create matching draft customer credit notes in Odoo. Direction of sync: **Odoo ↔ Bagisto** (import is the primary flow; product export and webhook-driven updates are supported). Bagisto returns flat, already-resolved rows and prices in decimal major units, so no currency conversion is needed. Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ### Tested against the real thing Automated tests run against a bundled mock store on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records and real order totals. Tested for real against Bagisto 2.0 and the current Bagisto 2 release: products created through the Admin REST API and three orders placed at the storefront checkout came into Odoo with totals identical to the store, 0 sync errors. ### Requirements - **Odoo**: 20.0, Community or Enterprise. - **Bagisto**: a Bagisto store with the official `bagisto/rest-api` package installed and the Admin REST API enabled. Bagisto must be reachable from the Odoo server. - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can use the connector screens. Bagisto credentials are stored in system-only fields, so only the **Settings / Administration** user can read or change the admin password or bearer token. - **Network**: outbound HTTPS from Odoo to your store. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ### Installation 1. Copy `bambooforge_bagisto_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *Bagisto*, and press **Activate / Install**. Dependencies (*Sales*, *Contacts*, *Invoicing*, *Product*) install automatically. No Bagisto store is required to evaluate the connector: a mock Bagisto Admin REST API ships inside the module, so you can install, explore and run the full import flow against sample data before pointing it at a real store. Point an instance's **Base URL** at `/bagisto/mock_api` with auth type *Admin Email + Password* (any email/password) to try it. ### Step 1 — Obtain a Bagisto Admin API token (Sanctum) The connector talks to the Bagisto Admin REST API, which authenticates with a Laravel Sanctum bearer token. You have two ways to provide it. **Option A — let Odoo log in for you (recommended).** Give the connector a Bagisto admin email and password. On connect it calls `POST /api/v1/admin/login` with `{email, password, device_name}`; Bagisto returns a Sanctum token at the top-level `token` key, and the connector sends it as `Authorization: Bearer ` on every request. The token is cached and automatically refreshed if the store returns 401. To prepare this in Bagisto: 1. Make sure the `bagisto/rest-api` package is installed and the Admin REST API is enabled. 2. Use an existing admin account (or create one under **Settings ▸ Users**) that has permission to read products, customers and orders. 3. Note its email and password — you will enter them in Odoo in Step 2. **Option B — paste a pre-issued token.** If you already have a Sanctum token (for example one minted by an automation account), you can paste it directly. To mint one manually, call the login endpoint once, e.g.: ``` curl -X POST https://store.example.com/api/v1/admin/login \ -H "Accept: application/json" \ -d "email=admin@example.com" \ -d "password=YOUR_PASSWORD" \ -d "device_name=odoo" ``` Copy the `token` value from the JSON response. In Odoo, choose auth type *Bearer Token* and paste it. ### Step 2 — Create the connection in Odoo Open **Bagisto Connector ▸ Configuration ▸ Instances** and create a record. ![Bagisto instance / connection cockpit](01-connection-cockpit.png) Key fields: | Field | What to enter | |---|---| | Name | A label for this store, e.g. *My Live Store*. | | Base URL | Your store root, e.g. `https://store.example.com` (or `http://localhost:9000` for a self-hosted test store). | | Authentication | *Admin Email + Password* (Odoo logs in for you — Option A) or *Bearer Token* (you paste a pre-issued Sanctum token — Option B). | | Admin Email | The Bagisto admin email used by the login flow (Email + Password auth). | | Admin Password | The Bagisto admin password (login auth). Visible to administrators only. | | Bearer Token | A pre-issued Sanctum token (Bearer Token auth). Visible to administrators only. | | Admin path | Leave the default `api/v1/admin` unless your store differs (requires the `bagisto/rest-api` package). | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach a store on localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. A green *Connected* state means the credentials and URL are correct. The first successful connection also seeds the default order-state mapping rows. If it fails, the exact error is shown on the form and recorded in **Logs** (see *Troubleshooting*). Tip: use **Quick Setup** (button on the instance) to pick a solution pack (Catalog Only, B2C Full Sync, B2B Order-First, Retail Starter), seed default field mappings and starter flows, and apply a recommended safety profile in one step. ### Step 3 — First sync (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import and delete jobs *simulate* writes: instead of changing data they produce **Validation Results** you can review under **Bagisto Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, click **Import Products** (and/or **Import Customers**, **Import Orders**, **Import Refunds**). This enqueues jobs; it does not block the UI. 2. Jobs are processed by the **Bagisto Queue Processor** scheduled action (every minute), or immediately if you run it manually from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land in the standard Odoo apps: **Sales ▸ Products**, **Contacts**, and **Sales ▸ Orders**. Each carries its Bagisto reference so re-imports update the same record instead of duplicating it. ### Order status mapping Bagisto exposes a string order status on every order (`pending`, `pending_payment`, `processing`, `completed`, `canceled`, `closed`, `fraud`, plus any shop-specific custom ones). Under **Configuration ▸ Order State Mappings** each instance gets a default table that decides what happens to the Odoo sale order when an order arrives in a given status: ![Bagisto order state mapping table](04-order-state-mapping.png) | Bagisto status | Default Odoo action | |---|---| | pending | Keep draft | | pending_payment | Keep draft | | processing | Confirm sale order | | completed | Confirm sale order | | canceled | Cancel sale order | | closed | Confirm sale order | | fraud | Keep draft | Change any row to **Ignore (do nothing)**, **Keep draft**, **Confirm sale order** or **Cancel sale order**. Unknown/custom statuses default to *Ignore*, so a status you have not mapped never triggers a destructive transition. Add a row for any custom status your shop introduces (use the exact Bagisto status string). ### Field mapping & customization - **Field Mappings** (Configuration) map Bagisto fields to Odoo fields per model. Use **Generate suggested mappings** on the instance to seed the business-critical ones, then adjust. The mapping board flags fields that need attention. - **Schema Fields** lists the discovered Bagisto fields per resource. Run **Schema Introspection** on the instance to refresh it from your live store (it falls back to the bundled sample if the store is unreachable). - **Stock / images / taxes / refunds** are opt-in toggles on the instance: *Sync stock*, *Sync images* (and *max images*), *Upload images on export*, *Sync taxes* (and *Auto-match taxes on import* — name-based, never auto-creates a tax), *Sync refunds* (and *Auto-post refund*). They are off by default; turn on only what you need. ### Automation (scheduled actions & webhooks) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | Bagisto Queue Processor | every 1 min | Processes queued import/export/delete jobs. | | Bagisto Reconciliation | every 15 min | Pulls recent remote changes for enabled models. | | Bagisto Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs. | | Bagisto Flow Scheduler | every 5 min | Runs scheduled sync flows. | | Bagisto Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | Bagisto Auto Recover | every 15 min | Reopens a tripped circuit breaker once the store is healthy. | Turn on **Auto import** / **Auto reconcile** per model on the instance to let the scheduled actions keep things in sync hands-free. **Real-time webhooks** (optional): point your Bagisto store's webhook/event delivery at: - **Delivery URL**: `https:///bagisto/webhook` - **Secret**: the instance **Webhook Secret** (admin-only field on the instance). The webhook is enabled per instance via the **Webhook Enabled** toggle (on by default). Each delivery must carry an HMAC-SHA256 signature over the raw body in the `X-Bagisto-Signature` header; Odoo verifies it and rejects anything without a valid signature. The signed body must include a fresh `emitted_at` timestamp (within 15 minutes past / 5 minutes future), and each signature is accepted only once, so captured requests cannot be replayed. Verified deliveries enqueue a safe import (or a delete, for delete events). Without webhooks the scheduled reconciliation still keeps data current. ### Safety features - **Dry-run mode** — simulate writes and review Validation Results before going live (`Dry-run` field, ON by default). - **Business validation profiles** — *Minimal / Standard / Strict* gate risky writes (`Business validation profile` field). - **Safety profiles** — *Conservative / Balanced / Aggressive* apply a coherent set of dry-run, delete-strategy and batch-size defaults in one click. - **Resilient queue** — every remote action is a job with retry and exponential back-off, plus a dead-letter state for jobs that exhaust their retries. - **Circuit breaker** — after repeated failures an instance auto-pauses (*Tripped*); the Auto Recover action reopens it once the store responds again and no blocking validation issues remain. - **Rollback snapshots** — when enabled (`Rollback enabled`), imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots**. - **SSRF guard** — the connector refuses internal/loopback/private hosts unless *Allow internal host* is explicitly enabled. - **Admin-only credentials** — admin password, bearer token and webhook secret live in `base.group_system` fields, hidden from ordinary internal users. ### Where Bagisto shows up in Odoo The connector stays out of the way on records that have nothing to do with Bagisto: - **Sales orders** - the *Bagisto* tab, *Queue Bagisto Export* and *Bagisto Binding* appear only on orders that came from Bagisto, are bound to Bagisto, or belong to a customer bound to Bagisto. - **Products and contacts** - the *Bagisto* tab appears once an active Bagisto connection exists; the *Bagisto Binding* button inside it appears once the record is bound. - List views, the sync queue and the control tower are unchanged. ### Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with "login failed" / 401 | Wrong admin email or password, or the bearer token is invalid/expired. Re-check Step 1. For Bearer Token auth, mint a fresh token; for Email + Password auth, confirm the admin account can sign in to Bagisto. | | "Admin email and password are required" on Test Connection | Auth type is *Admin Email + Password* but one field is blank. Fill both, or switch to *Bearer Token* and paste a token. | | "the URL must start with http:// or https://" / "is an internal host" | Base URL points at localhost or a private IP. Enable *Allow internal host* on the instance (test/self-hosted only), or use a public store URL. | | SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off *Verify SSL* for testing only. | | "requires the bagisto/rest-api package" / 404 on resources | The Admin REST API is not installed/enabled, or *Admin path* is wrong. Install `bagisto/rest-api` and leave *Admin path* at `api/v1/admin`. | | Orders import but never confirm | The order's Bagisto status is mapped to *Keep draft* or *Ignore*. Adjust **Order State Mappings**. | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check **Scheduled Actions** is active and the instance is not *Paused*. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures. Fix the store/credentials; Auto Recover reopens it, or click **Resume**. | | Webhook deliveries rejected (401 / 400 / 409) | 401 = missing or wrong `X-Bagisto-Signature` (check the shared secret); 400 = missing/stale `emitted_at` timestamp; 409 = duplicate (replay) already processed. Confirm both sides share the same secret and their clocks are roughly in sync. | | Records imported twice | Imports are keyed by the Bagisto reference, so this should not happen. If it does, check that two instances do not point at the same store. | | Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | For anything else, **Operations ▸ Logs** records every API call, payload and error with a timestamp. ### Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the store side the connector targets the Bagisto Admin REST API (the `bagisto/rest-api` package). Validate your exact build with the bundled mock first. **Do I need Bagisto installed to evaluate it?** No. A mock Bagisto Admin REST API ships inside, so you can install, explore and demo the full import flow before connecting a real store — point the Base URL at `/bagisto/mock_api`. **How does authentication work?** Bagisto uses a Laravel Sanctum bearer token. Either let the connector log in for you (it calls `POST /api/v1/admin/login` with your admin email/password and caches the returned token), or paste a pre-issued token and choose *Bearer Token* auth. The token is refreshed automatically on 401. **How does real-time sync work?** Point your store's webhook delivery at `/bagisto/webhook` with the shared secret; Odoo verifies the HMAC-SHA256 signature, rejects stale or replayed deliveries, and enqueues a safe import. Without it, scheduled reconciliation keeps things in sync. **Are prices and currency converted?** No conversion is needed: Bagisto returns prices in decimal major units and the connector reads them as-is. **Is it safe to run against production data?** Dry-run is ON by default, validation blocks risky writes, and rollback snapshots let you undo. You decide when to go live. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ### Data, privacy & limits - The connector reads products, customers, orders and order slips (refunds) from your store and writes the corresponding Odoo records. It can also export product changes back to Bagisto. Credentials and the webhook secret are stored in admin-only fields. - **In scope today**: product/customer/order import, product export, order-status mapping, refund discovery, scheduled sync, signed webhooks. - **Out of scope / best-effort**: tax auto-matching (off by default, name-based, never auto-creates taxes); multi-warehouse stock routing; subscription/booking order types. ### Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / Bagisto Admin REST API line. ### Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ## Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_bagisto_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ## Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ### Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ## What is removed - The module's own tables and every record in them (20 models, prefixed `bagisto.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ## What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ### Changelog ## 20.0.1.2.5 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ## 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Two-way Odoo 20 <-> Bagisto connector: products, customers, orders and refunds over the Bagisto Admin REST API (Sanctum token). - Two-way stock sync, live Sync Control Tower, resilient queue and dry-run safety. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ### How we make sure it works Before every release, **102 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A domain that does not parse is refused. - Nothing at all is an empty row not a crash. - A product shows its binding button only once bound. - A record outside the filter is not queued. - Import to odoo true but dry run never moves stock. Tested for real against Bagisto 2.0 and the current Bagisto 2 release: products created through the Admin REST API and three orders placed at the storefront checkout came into Odoo with totals identical to the store, 0 sync errors. The complete list is in the source you receive. --- # Bakery & Food Production Management > Bakery production in bakers' percentages: allergens computed from the recipe, batches with use-by and lot trace, the round, and the waste. - **What it is:** Bakery production in bakers' percentages: allergens computed from the recipe, batches with use-by and lot trace, the round, and the waste. - **For:** Industries - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_bakery - **Manual:** https://docs.bambooforge.dev/bambooforge_bakery/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_bakery.action_bf_bakery_plan - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_bakery - **Manual last updated:** 2026-10-14 --- # Bakery & Food Production Management Built around the three things a bakery is judged on: the label, the trace, and the number nobody writes down. ## The label - Every ingredient carries what it **contains** and what it **may contain**, entered from the supplier's specification and signed off with a name and a date. - The recipe's declaration is **computed** from its lines, and travels up through preferments and fillings into whatever uses them. - An allergen that is definitely present never also appears as a "may contain". - A recipe **cannot be released for sale** while any ingredient in it has no specification on file. - Changing what an ingredient carries **drops the sign-off**, and the name and date on it, so a reformulation takes every recipe using it **off sale by itself** - nobody has to notice. - Changing a **released** recipe - an ingredient, the weight, the shelf life - puts it back to draft with a line saying why. A label signed off against a recipe is only signed off against that recipe. - The declaration is written in the **bakery's own language** and names the allergens in the **order the regulation lists them**, not in alphabetical order. - The fourteen regulated allergens ship with the module. A bakery adds its own beside them. ## The trace - **Lots are recorded when the sack is opened**: the code exactly as printed, the supplier, the best-before. - A batch records **which lots went into it**. A lot past its date, under a block, emptier than the line says, or of something that is not in the recipe cannot go in - however the row is written. - **Blocking a lot stops the bread**: a batch mixed with it does not come out of the oven for sale, and nothing made with it loads onto a van. Every batch and every round holding it is told on its own record, with how many are still in the bakery. - Once a batch is baked, its lots **cannot be edited or deleted**, the batch itself cannot be deleted, and a lot that has been used cannot be deleted at all. - Every batch keeps **the label it was baked with**. A recipe reformulated next month does not change what was on last month's loaf. - One screen answers a recall: the batches, **how many reached each customer** after returns, and who they are. Blocking the lot is the same button, and the answer is written on the lot. - A lot within a week of its date lands on **somebody's** list - the person who booked it in, or a manager. Never a robot's. ## The bakery - Recipes in **bakers' percentages** against the flour, with hydration, bake loss, shelf life, yield and cost per item. - **Scaling** to tonight's numbers is arithmetic the system does - the mixing sheet is the recipe multiplied by what the plan says. - A **production plan** per night: standing orders per weekday added up, plus what the counter expects. Confirming it raises a batch per line and totals the flour. - A **cut-off** decides which night a late order lands on, and it is recorded on the plan. It is counted in the **bakery's own time** - fourteen hours before Tuesday is four o'clock on Monday afternoon where the bakery is - and an order lands on the next night that is still open, never on one already confirmed. A cancelled night does not block that day for ever. - Pulling the round again **drops the lines nobody wants any more**, and records the cut-off as the moment it was really pulled. - **Use-by** is out of the oven plus the shelf life. A round cannot carry a batch that is out of date before the end of the delivery day, counted in the bakery's own time. - **Bake loss** is measured against the recipe's figure, and a bake two points out says so. - A batch **cannot come out of the oven** without a count and without its lots. ## The round - **Standing orders** per customer per weekday, with a price and whether the customer is sale or return. - A delivery note carries **what went out and what came back**, and the invoice bills the difference. - Every line names the **batch** it came from, which is the other half of a recall. A line without one cannot be loaded. - What is left of a batch is counted **across the whole note and with the batch held**, so two lines cannot take the same bread twice and two vans loading at the same moment cannot both have the last twelve. A delivery still being written up holds nothing back. - Once the van has gone, the batch, the item and the quantity on a line **stop being editable**, the line cannot be deleted, and the delivery itself cannot be deleted. After the invoice, what came back is fixed too - it is the number that was charged. - A delivery is invoiced once, in the **company that made the bread**, and an invoiced delivery cannot be cancelled. Cancelling a round that went out puts the bread back. ## The waste - Waste is a **record against the batch**, with a reason and what it cost to make - the recipe scaled to the batch, corrected by the sacks that were actually weighed, so an untraced ingredient does not simply cost nothing. - More cannot be written off than is **still in the bakery**: what went out and stayed out is on a customer's shelf, not in the bin. Nothing is written off before it is baked. - Made, less what went out, plus what came back, less what was thrown away, is what is left - which is the number tomorrow's plan should be argued with. A batch with nothing left says so. - A batch over the **waste target** says so on its own record. ## Roles - **Baker and driver** - mixes, bakes, records lots and waste, pulls the round into a plan, loads and delivers. Cannot write a recipe, sign off a specification, release an item for sale or lift a recall block. - **Bakery manager** - writes recipes, signs off specifications, releases items, sets standing orders, lifts a block and invoices the round. The role carries the billing rights that invoicing needs. Each company numbers its own batches, plans and delivery notes. ## Installation 1. Copy `bambooforge_bakery` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. Community only; it depends on `base`, `base_setup`, `mail`, `product`, `uom` and `account`. 2. Install **Bakery & Food Production Management**. 3. Enter the ingredients with their allergens, and sign off their specifications. 4. Build the recipes; mark the flour lines as flour, or the percentages cannot be worked out. Release the recipes. 5. Enter the standing orders. 6. Set the two settings: the order cut-off and the waste target. ## How we make sure it works Before every release, **137 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Two lines cannot take the same bread twice. - A delivery cannot load a batch past its use by. - A lot of something not in the recipe is refused. - A lot row cannot be moved onto a blocked lot. - A delivery cannot take more than the batch made. The complete list is in the source you receive. ## 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, Invoicing, Products, Settings, Units of Measure apps. No extra Python package. ****Where does it show up?**** Under the **Bakery** menu (Production plans, Batches, Deliveries, Ingredient lots). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) --- # Appointment Booking All in One with Calendar Sync > Appointment booking with calendar sync: a subscription link for staff without an Odoo login, and their own calendar read back to block slots. - **What it is:** Appointment booking with calendar sync: a subscription link for staff without an Odoo login, and their own calendar read back to block slots. - **For:** Website - **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:** 228 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_booking_calendar_sync - **Manual:** https://docs.bambooforge.dev/bambooforge_booking_calendar_sync/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_booking_calendar_sync.action_bf_booking_busy - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_booking_calendar_sync - **Manual last updated:** 2026-10-15 --- # Appointment Booking All in One with Calendar Sync An add-on for **BambooForge Booking Pro**, for the staff who will never have an Odoo login. ## What Odoo already does Odoo Community ships `google_calendar` and `microsoft_calendar`, and Booking Pro already refuses a slot when a staff member's **Odoo user** has a calendar event in the way. If your staff are Odoo users, connect their accounts in their own preferences. That is free, it is two-way, and this module does not try to sell it to you again. ## What this adds The freelance trainer, the visiting dentist, the photographer who works Thursdays - none of them are Odoo users, and none of them will be. ### A subscription link On the staff member's form, **Create a subscription link**. It produces a URL ending in `.ics`: paste it into Google Calendar ("From URL"), Outlook ("Subscribe from web"), Apple Calendar or a phone. Their bookings appear and keep updating, because that is what a subscribed calendar does. **Revoke the link** stops every calendar that has it - for the day a phone is lost. A revoked link and a wrong link answer identically. ### Their calendar, read back Paste the secret iCal address their own calendar publishes: - **Google Calendar** - Settings for my calendars → *Secret address in iCal format*. - **Outlook / Microsoft 365** - Settings → Calendar → Shared calendars → *Publish a calendar* → ICS link. - **Apple iCloud** - share the calendar publicly and copy the webcal address. Events found there **block slots**. Odoo never writes to that calendar: the sync is one-way in, one-way out, and nothing of yours ends up in their personal diary. Refreshed every fifteen minutes. A calendar checked once a day is a double booking waiting to happen. ## What the reader understands Deliberately small, and deliberately explicit about it: | Understood | Not understood | |---|---| | Timed events with a zone | Monthly and yearly repeats - imported as a single event rather than expanded wrongly | | All-day events, in the | Exceptions to a repeat (EXDATE) - the | | calendar's own zone | occurrence stays blocked | | Cancelled events (skipped) | Attendee status: an invitation you have not answered still blocks | | Events marked *free* | Attachments, alarms, anything not a time | | (skipped) | | | Daily and weekly repeats, | | | with COUNT and UNTIL | | A wrong busy block silently costs a booking; a missing one costs a manual check. Where the two conflict, this errs towards blocking. ## Housekeeping - Imported busy time older than two days is deleted by the standard autovacuum. - A pull replaces that member's imported blocks - no duplicates build up. - A calendar that fails to answer is reported on the staff form and does not stop the other members from being refreshed. ## How we make sure it works Before every release, **29 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A url that is not a url is refused. - A monthly repeat is imported once not guessed. - The link serves a calendar and a wrong one does not. - A comma in a summary does not split the line. - An event marked free is not busy. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_booking_calendar_sync` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Appointment Booking Suite: Google & Outlook Sync**. 3. Open **Imported Busy Time**. ## 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 Web; booking pro (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Imported Busy Time** menu. ****Does anything leave my server?**** It makes calls outside your server only where its manual says so - read the section on that service before you connect it. ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - the people who wrote it. --- # Appointment Booking All in One with Deposits > Appointment booking with deposits: take part of the price up front, refund tiers by cancellation notice, no-show fees and a no-show register. - **What it is:** Appointment booking with deposits: take part of the price up front, refund tiers by cancellation notice, no-show fees and a no-show register. - **For:** Website - **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:** 228 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_booking_deposit - **Manual:** https://docs.bambooforge.dev/bambooforge_booking_deposit/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_booking_deposit.action_bf_booking_money - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_booking_deposit - **Manual last updated:** 2026-10-15 --- # Appointment Booking All in One with Deposits An add-on for **BambooForge Booking Pro**. Booking Pro can ask for the whole price up front; this asks for enough that an empty chair on a Saturday is not free for the person who booked it. ## Install 1. Install **BambooForge Booking: Deposit & No-show** (it needs Booking Pro). Nothing changes until a service is given a deposit or a no-show fee. 2. For the online part - collecting the deposit - enable a provider in Odoo's **Payment** app, exactly as Booking Pro's prepayment needs. 3. Open the two new menus under Booking Pro: *Money Owed* and *No-show Register*. ## On the service A new *Deposit & no-show* section on the service form. ### Deposit - **Percentage of the price** or **fixed amount**. A fixed amount above the price is refused: that is a surcharge, not a deposit. - The deposit is collected through Booking Pro's **online prepayment**: tick it, and the Pay-now link asks for the deposit instead of the whole price. The booking stays *Pending* until it is paid, as before. - The rest is **due on the day**, and the booking says how much. ### Refund tiers "Cancel at least 48 hours before: 100 % of the deposit back. At least 24 hours: 50 %." The earliest tier the cancellation qualifies for applies; no tier matching means no refund. These tiers are shown to the visitor on the booking page before they book. When the **business** cancels, the customer always gets the whole deposit back, whatever the tiers say. ### No-show fee - **Keep the deposit** - the usual choice, and the default. - **Percentage of the price** or **fixed amount** - when the fee is covered by the deposit already paid, it is marked collected; when it is not, it is due. - **Refuse online booking after N no-shows** in the last twelve months: the visitor is asked to call. The desk can still book them by hand, because the desk knows things the form does not. Zero never refuses anybody. ## On the booking The booking shows the deposit, whether it was paid and when, and the balance due on the day. Three things a human does: ****Deposit received**** The desk took it in cash or by phone. A confirmation that came from a paid online transaction marks it by itself. ****Refunded**** A refund due was executed. Recorded with who and when. Nothing moves money automatically - a refund is a decision, and the decision is the record. ****Fee collected / Waive the fee**** Same for a no-show fee that was due. *Money Owed* lists everything still due in either direction. ## The register Every no-show is a booking in state *No-show*. *No-show Register* groups them by customer e-mail; the count over the last twelve months is also on the contact card. Counting is by e-mail, case-insensitive, because that is what a walk-in customer gives you. ## What it will not pretend It does **not** place a hold on a card. That needs a payment provider with manual capture and a promise about when the hold is released, and a wrong promise there costs more than a deposit ever will. The deposit is the honest version, and it is what most places actually run. ## How we make sure it works Before every release, **36 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A cancelled booking cannot become a paid no show. - Cancelling twice does not reopen a refund already paid. - A booking user cannot waive or refund. - A limit of zero refuses nobody. - Keeping an unpaid deposit keeps nothing. The complete list is in the source you receive. ## 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 Web; booking pro (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Money Owed** menu (No-show Register). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - the people who wrote it. --- # Appointment Booking All in One for Many Locations > Appointment booking across branches: the visitor picks the location, only staff who work there are offered, and travel time prevents clashes. - **What it is:** Appointment booking across branches: the visitor picks the location, only staff who work there are offered, and travel time prevents clashes. - **For:** Website - **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:** 228 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_booking_locations - **Not for:** Travel time is a number you set, not a route somebody computed. Two branches ten minutes apart at 11:00 and forty at 17:00 are one number here - pick the one you would rather explain.; A staff member's working hours are still one calendar per person (Booking Pro's Working hours). Different hours at different branches is not modelled; the common approach is a member per branch. - **Manual:** https://docs.bambooforge.dev/bambooforge_booking_locations/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_booking_locations.action_bf_booking_location - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_booking_locations - **Manual last updated:** 2026-10-15 --- # Appointment Booking All in One for Many Locations An add-on for **BambooForge Booking Pro**. Booking Pro's location is one line of text on the service; this makes the branch a record, with the people who work there and the time it takes to reach the next one. ## Install 1. Install **BambooForge Booking: Multi-location** (it needs Booking Pro). Nothing changes until a service is given branches; a service with none keeps the page and the behaviour it had. 2. Open the new menu *Booking Pro → Locations*. ## Locations *Booking Pro → Locations*. A location is: | Field | What it is for | |---|---| | Name, address, phone | Shown to the visitor under the branch selector, in the confirmation page, and in the LOCATION line of the .ics invitation. | | Directions | Free text for the confirmation: parking, entrance, which floor. | | Staff working here | Who can take bookings at this branch. A member listed under **no** branch works everywhere - the mobile hairdresser, the online consultant. | | Travel time to other | Minutes to reach or leave this branch, used | | locations | when no explicit pair is defined. | | Travel time to specific | Per pair. One row serves both directions. | | branches | | Then, on each service: **Offered at** - one branch, some, or none. ## What the visitor sees A *Where?* selector above the calendar, with the address and directions of the chosen branch under it. The first branch is preselected, so the calendar is never shown unfiltered. Changing the branch reloads the slots and hides, in the *With* dropdown, the staff who do not work there. The branch travels with the booking: into the record, into the manage page, into the calendar event and into the invitation - because a customer with two branches in their calendar and no address on the event drives to the wrong one. ## Travel time A stylist finishing in Altona at 12:00 cannot start in the city centre at 12:00. When a slot is computed for one branch, every booking that person has at **another** branch blocks the calendar for the booking **plus the journey** on both sides. Which journey: - the explicit pair between the two branches, if there is one, in either direction; - otherwise the **larger** of the two branches' default travel times. The larger, not the smaller, because the pessimistic side of that choice is a gap in the diary and the optimistic side is a customer waiting in the wrong branch. A booking at the **same** branch needs no journey; the service's own buffers apply as before. ## Honest limits - Travel time is a number you set, not a route somebody computed. Two branches ten minutes apart at 11:00 and forty at 17:00 are one number here - pick the one you would rather explain. - A staff member's working hours are still one calendar per person (Booking Pro's *Working hours*). Different hours at different branches is not modelled; the common approach is a member per branch. ## Multi-company Locations belong to a company and are filtered by it. Staff and services are Booking Pro's and follow its rules. ## How we make sure it works Before every release, **30 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Travel time pads only the journey not the day. - A branch with upcoming bookings cannot be deleted. - An archived branch is not offered on the page. - Moving a booking to another branch updates the invitation. - A branch of another service is ignored. The complete list is in the source you receive. ## 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 Web; booking pro (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Locations** menu. ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - the people who wrote it. --- # Appointment Booking & Online Scheduling > Online appointment booking for Odoo Community: services on staff hours or bookable resources, timezone-aware slot picker, buffers and capacity. - **What it is:** Online appointment booking for Odoo Community: services on staff hours or bookable resources, timezone-aware slot picker, buffers and capacity. - **For:** Website - **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:** 179 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_booking_pro - **Manual:** https://docs.bambooforge.dev/bambooforge_booking_pro/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_booking_pro.action_bf_booking - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_booking_pro - **Manual last updated:** 2026-10-15 --- # Appointment Booking & Online Scheduling Calendly-class online booking for the Odoo 20 website, running entirely on Community: bookable services scheduled on staff with real working hours **or on resources** (tables, meeting rooms, courts…) with seat capacities and combinable tables, a timezone-aware month-grid slot picker on `/booking`, buffers and group capacity, custom questions per service, optional online prepayment and CRM leads, instant or manual confirmation, e-mail confirmations and reminders with `.ics` files, self-service cancellation and staff calendar sync — with the staff's other calendar events (including externally synced ones) blocking slots. This page is the complete manual. Follow it top to bottom and you can install, publish your first service, take real bookings and fix the common issues **without contacting support**. ## Overview - **Services** — each service defines duration, slot granularity (*Slot every*), buffers before/after, capacity (group seats), price, location (on site / online meeting / phone call), minimum notice, booking horizon, cancellation deadline, one or several reminder offsets, auto-confirmation, optional online prepayment and optional custom confirmation/cancellation templates. Name, description and location are translatable, and the module ships its `.pot` template so any language can be added. - **Custom questions** — per service, free text or dropdown, with a required flag. Answers are validated server-side (required and dropdown choices), stored on the booking and echoed into the CRM lead description. - **Staff** — booking staff members with their own **working hours** (standard `resource.calendar` records) and timezone, optionally linked to an Odoo user so confirmed bookings appear in that user's calendar. - **Schedule mode** — each service is scheduled either on **staff members** (the default, everything below applies unchanged) or on **resources**: tables, meeting rooms, courts… In resource mode the visitor books *seats* instead of a person. - **Resources** — bookable resources with a **seat capacity**, a **shareable** flag (several bookings share the resource at the same time until the capacity is reached — a large shared table; off = one booking takes the whole resource) and **Can be combined with** links (adjacent tables merged for big parties). The website asks the visitor **how many people**, proposes only slots where the party fits, and automatically assigns the smallest free resource — or the smallest combination of combinable resources when no single one fits. Opening hours come from a working-hours calendar on the service; capacity double-booking is blocked server-side. - **Slot engine** — free slots are computed from the staff working hours minus existing bookings (with buffers), aligned to the slot step, filtered by minimum notice and booking horizon, and labelled in the **visitor's timezone** (validated — an unknown zone name falls back to UTC instead of breaking the page). When the staff member is linked to a user, the user's **other calendar events** — including events fed by the Google / Outlook calendar synchronisation apps — block slots outright. - **Booking page** — `/booking` lists published services; `/booking/` shows a **month-grid slot picker** (free days highlighted, first free day pre-selected, month navigation), a staff selector including *Anyone available*, and a short form (name, e-mail, phone, notes, the service's custom questions and, when enabled, guest e-mails). Every submit is re-validated server-side, so a stale browser can never double-book — and the re-check plus the booking are **atomic per slot**, so two visitors clicking the same slot at the same instant cannot both take it: the second one is told *Sorry, that slot has just been taken*. **Drafts stay private** — an unpublished service is refused by `/booking`, by `/booking/`, by the slot endpoint and by submit alike, so nobody can book it by guessing its id. - **Prepayment** — optionally require online payment per service (needs the standard Payment app): the booking stays *Pending* with a Pay-now link through the standard payment flow and a cron confirms it once the transaction succeeds. - **Lifecycle** — pending → confirmed → done, plus cancelled and no-show. Auto-confirm per service, or receive requests and confirm by hand. A database constraint rejects overlapping bookings beyond capacity even when created from the backend. - **E-mails** — confirmation (with `.ics` attachment), cancellation and reminder templates; per service you can substitute your own confirmation and cancellation templates and add extra reminder offsets. A cron sends reminders; another marks past bookings *Done*. The reminder and payment-confirmation crons isolate failures **per booking**, so one odd record cannot stop the reminders for everybody else. - **Self-service** — every booking has a private tokenised manage page with the details, an *Add to calendar (.ics)* download and a cancel button active until the per-service deadline. - **Contacts & calendar** — the customer is matched to an existing contact by e-mail or a contact is created; each confirmed booking creates a `calendar.event` for the staff member's user. When the service allows guests, the visitor's guest e-mails are invited to that event as attendees. - **CRM** — optionally create a lead per website booking (needs the CRM app), carrying the service, slot, staff member and the visitor's answers. ## Requirements | Item | Detail | |---|---| | Odoo | 20.0, Community or Enterprise. Only the standard `website`, `calendar` and `resource` apps are required — the Enterprise *Appointments* app is **not** needed. | | Optional apps | *Require online prepayment* needs the standard **Payment** app with an enabled provider; *Create a CRM lead per booking* needs the free **CRM** app. External-calendar awareness works with any `calendar.event` of the staff user — including events created by the Google / Outlook calendar synchronisation apps when they are installed. | | Python / JS | No extra libraries. The slot picker is framework-free vanilla JS, so it renders on any website theme. | | Outgoing e-mail | Confirmations, cancellations and reminders use standard mail templates — configure an outgoing mail server as usual. | | External services | None. No SaaS scheduler, no external calls; everything runs and stays in your database. | ## Getting started 1. Install **BambooForge Booking Pro**. 2. Open **Booking Pro ▸ Staff** and create your staff members. Pick the *Working hours* for each — create dedicated calendars under **Booking Pro ▸ Working Hours** if bookable hours differ from company hours. Link the Odoo user if the person should see bookings in their own calendar. 3. Open **Booking Pro ▸ Services**, create a service (duration, slot step, price, staff…) and **publish** it (website *Published* toggle). 4. Visit `/booking` on your website: the service is listed, the slot picker shows free times. Make a test booking — the confirmation e-mail arrives with a `.ics` file and the booking shows in **Booking Pro ▸ Bookings**. For a table / room / court service, the staff step is replaced by resources: create them under **Booking Pro ▸ Resources**, set the service's *Schedule mode* to *Resources* and pick the resources and the *Opening hours* calendar — see the *Resource mode* section below. ## Usage ### The booking flow Visitors open `/booking` (add it to your website menu), pick a service, then a day in a **month-grid calendar**: days with free slots are highlighted, the first free day is pre-selected and the arrows move month by month. The day's slots are labelled in the **visitor's timezone** (detected from the browser). With several staff on the service, a *With* selector offers each person plus *Anyone available* — in that case the engine assigns a staff member who is actually free. The short form asks for name, e-mail, phone and notes, plus the service's **custom questions** (required questions are enforced in the browser and re-validated server-side; dropdown answers are checked against the configured choices) and, when *Allow guests* is on, guest e-mail addresses — guests are invited to the calendar event. After submitting, the visitor is redirected to their private manage page. Depending on the service's *Auto-confirm* setting the booking is confirmed immediately (confirmation e-mail + `.ics`) or arrives as *Pending confirmation* for you to confirm with one click. On services with *Require online prepayment* the booking always starts as *Pending*: the manage page shows a **Pay now** link through Odoo's standard payment flow (`/payment/pay`), and the cron *Booking Pro: confirm paid bookings* (every 10 minutes) confirms the booking as soon as the payment transaction is done. With *Create a CRM lead per booking* on, every website booking also creates a lead carrying the service, slot, staff member and the visitor's answers. ### Working with bookings **Booking Pro ▸ Bookings** opens a calendar view (week mode, coloured and filterable per staff member); list and form views are one click away. On each booking you can *Confirm*, mark *Done*, *No-show*, or *Cancel* (the customer is notified). The visitor's answers to the custom questions are shown on the booking form. The chatter tracks state changes. Bookings created in the backend obey the same overlap and capacity rules as the website. Three scheduled actions run automatically: *Booking Pro: send reminders* (every 15 minutes, sends every configured reminder offset per service), *Booking Pro: mark past bookings done* (hourly) and *Booking Pro: confirm paid bookings* (every 10 minutes, confirms prepayment bookings whose transaction succeeded). ### Group classes Set the service **Capacity** above 1: the same slot accepts several bookings for the same service and start time until the seats are gone. Any overlapping booking of a *different* service or start time still blocks the slot outright. ### Resource mode (tables, rooms, courts…) Some businesses do not book *people* — they book **tables, meeting rooms or courts**. Set the service's **Schedule mode** to *Resources* and configure: 1. **Resources** (**Booking Pro ▸ Resources**, manager only) — one record per table / room / court with: - **Capacity** — the seats it offers (a table for 4, a room for 10…). - **Shareable** — on: several bookings can share this resource at the same time until the capacity is reached (a large shared table). Off: one booking takes the whole resource, whatever the party size. - **Can be combined with** — resources that can be merged with this one when a party is too big for it alone (adjacent tables). A resource cannot be combined with itself. 2. **On the service** — pick the participating **Resources** and the **Opening hours (resource mode)**: a standard working-hours calendar (`resource.calendar`) that generates the slots — resource mode does not use staff working hours. On the website the flow gains one field: **How many people?** — a seats selector up to the largest party a single booking can seat (combinations included). The engine proposes **only slots where the party fits** and, on submit, automatically assigns the **smallest free resource** — or the **smallest combination** of combinable resources when no single one fits. Double-booking beyond capacity is blocked server-side, exactly like in staff mode. The assigned resources and the party size are visible everywhere: on the booking form in the backend, in the confirmation and reminder e-mails and on the self-service manage page. Staff mode is completely unchanged — you can run staff-scheduled and resource-scheduled services side by side. ### Self-service manage page Every confirmation e-mail links to `/booking/manage/` — a private page (unguessable token, not indexed) where the customer sees the appointment in their own timezone, downloads the `.ics` file and can cancel until the service's cancellation deadline. Cancellation frees the slot immediately and removes the staff calendar event. ### Rescheduling A customer who cannot make the slot can move the booking from the same manage page they use to cancel — set **Let customers reschedule** on the service and a **Reschedule notice (hours)** window. Past slots and slots already taken are refused; the calendar entry follows the new time and the reminder is armed again. After the customer window closes, staff can still move the booking from the backend. ### Staff assignment A service with several people offers three strategies under **Assign staff by**: - **First free member** (default) — the person highest in the list takes it. - **Round robin** — bookings rotate through the team. - **Least busy member** — the member with the fewest upcoming bookings takes it; past and cancelled bookings do not count as load. ## Configuration All rules live on the service form (**Booking Pro ▸ Services**): | Setting (per service) | Effect | |---|---| | Duration / Slot every | Appointment length and the grid of proposed start times (e.g. 60-minute sessions offered every 30 minutes). | | Buffer before / after | Preparation and wrap-up time blocked around each booking. | | Capacity | Seats per slot and staff member. 1 = individual appointments, more = group classes. | | Minimum notice / Bookable days ahead | Visitors cannot book sooner than the notice or beyond the horizon. | | Auto-confirm | On: instant confirmation. Off: bookings arrive as requests. | | Cancellation deadline | How long before the start customers may self-cancel online. | | Email reminder (hours before) | Main reminder delay; 0 disables the reminder for this service. | | Extra reminders (hours before) | Comma-separated additional offsets (e.g. `48, 2`) — each booking gets one reminder per offset, on top of the main one. | | Require online prepayment | The booking stays *Pending* with a Pay-now link until the payment is confirmed. Needs the Payment app with an enabled provider. | | Custom confirmation / cancellation e-mail | Optional `mail.template` records (on model `bf.booking.booking`) that replace the standard templates for this service. | | Custom questions | Ordered list of per-service questions — free text or dropdown (one choice per line), each optionally required. Answers are stored on the booking and echoed into the CRM lead. | | Allow guests | Visitors may add guest e-mails (up to 10); guests are invited to the calendar event. | | Create a CRM lead per booking | Every website booking also creates a lead with the service, slot and answers. Needs the CRM app. | | Price / Location | Shown on the booking page; the price is recorded on each booking, the location (address or meeting link) is included in the confirmation. | | Company | The company this service belongs to. | | Staff | Who can be booked; each staff member brings their own working hours and timezone. Link the Odoo user to sync bookings into their calendar and to let their other calendar events block slots. | | Schedule mode | *Staff members* (default): the visitor books a person, whose working hours and calendar rule the slots. *Resources*: the visitor books seats and the engine assigns tables / rooms / courts. | | Resources | Resource mode only: the resources this service can book — each with its capacity, shareable flag and combinable links (**Booking Pro ▸ Resources**). | | Opening hours (resource mode) | Resource mode only: the working-hours calendar that generates the slots. | Access rights: **Booking Pro / User** sees and manages bookings; **Booking Pro / Manager** additionally configures services, staff, resources and working hours. ## Install 1. Copy `bambooforge_booking_pro` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Appointment Booking & Online Scheduling**. 3. Open **Booking Pro** - its menus are Bookings, Services, Staff, Resources. ## Troubleshooting ****The slot picker shows no free times.**** Check, in order: the service has active staff assigned; the staff working hours contain attendance lines on those weekdays; the *Minimum notice* and *Bookable days ahead* window is not empty (e.g. notice 48 h + horizon 1 day = nothing); existing bookings plus buffers are not filling the calendar; and the staff user's own calendar events (synced ones included) are not blocking the whole period. ****The service is not listed on /booking.**** It must be **published** (website toggle) and, with multiple websites, available on the current one. An unpublished service is also unreachable through its direct `/booking/` link and cannot be booked — drafts stay private until you publish them. ****Slots are shifted by a few hours.**** Slot labels are correct for the *visitor's* browser timezone — open the page from another machine and you will see other labels, by design. If the underlying times are wrong, fix the **timezone of the staff working hours** calendar: it defines when the staff member actually works. ****No confirmation e-mail arrives.**** Configure an outgoing mail server and check **Settings ▸ Technical ▸ Emails** for the message. Bookings on services with *Auto-confirm* off only send the confirmation once you confirm them. ****No reminder was sent.**** The reminder delay is per service (0 disables it), the booking must be *Confirmed*, and the cron *Booking Pro: send reminders* must be active (it runs every 15 minutes). ****Visitors book a colleague who is busy elsewhere.**** The engine blocks slots for its own bookings **and** for the staff user's other Odoo calendar events — link the staff member to a user, and install the Google / Outlook calendar synchronisation apps if the busy times live in an external calendar, so they feed `calendar.event` records. Appointments that exist nowhere in Odoo remain invisible — block those times with a backend booking or calendar event. ## FAQ ****Does it need Odoo Enterprise or the Appointments app?**** No. It runs on Community with only `website`, `calendar` and `resource` — that is the reason it exists. It works unchanged on Enterprise too. ****Can customers pay online while booking?**** Yes — enable *Require online prepayment* on the service (needs the standard Payment app with an enabled provider). The booking stays *Pending* with a **Pay now** link on the manage page, routed through Odoo's standard `/payment/pay` flow, and the cron *Booking Pro: confirm paid bookings* confirms it once the transaction is done. The price is recorded on every booking either way, so you can still invoice the auto-created contact with standard Odoo. ****Can visitors book a table or a room instead of a person?**** Yes — set the service's *Schedule mode* to *Resources* and define tables / rooms / courts with seat capacities, optionally shareable and optionally combinable for big parties. The visitor picks the party size and the engine assigns the smallest free resource or combination. Staff-based services are unaffected. ****Can two people book the same slot?**** Only when you allow it: capacity above 1 sells group seats for the same service and start time. Otherwise buffers, the database overlap constraint and the server-side re-validation of every submit guarantee zero double bookings. Even the classic race — two visitors clicking the same last slot in the same second — is handled: the availability check and the booking are **atomic per slot**, so exactly one of them wins and the other is told *Sorry, that slot has just been taken*. ****Is it tested?**** Yes: unit tests for the slot engine and booking flow, an HTTP suite for the controllers, and a real-browser frontend tour. ****Multi-website?**** Yes. Services are published per website and each booking records the website it came from. ****Which languages?**** All visitor-facing texts are translatable. English ships with the module, and the exportable `.pot` template is there for any other. ****Does any data leave my server?**** No. No external scheduler, no CDN, no API calls — bookings, contacts and calendar events live only in your database. ## Upgrade 1. Back up the database. 2. Replace the module folder, restart, then: ``` odoo-bin -d -u bambooforge_booking_pro --stop-after-init ``` 3. Review the Changelog below for new settings worth enabling. ## Uninstall 1. Export **Booking Pro ▸ Bookings** first if you need the history — bookings, services and staff records are removed with the module (standard Odoo behaviour). 2. **Apps ▸ BambooForge Booking Pro ▸ Uninstall.** 3. Contacts and calendar events created from bookings remain in your database. Remove the `/booking` link from your website menu if you added one. ## Changelog ### 20.0.1.5.7 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. A staff member's timezone is their own setting, falling back to the company: Odoo 20 took the timezone off working-hours calendars. ### 20.0.1.3.1 (2026-08-15) - Concurrency: the availability re-check and the booking that follows are now **atomic per slot**. Two visitors clicking the same slot at the same instant can no longer both take it — the second one gets *Sorry, that slot has just been taken — please pick another one.* - Only **published** services are bookable: `/booking/`, the slot endpoint and submit all refuse an unpublished service, so a draft can no longer be booked by guessing its id. Drafts stay private. - Visitor timezones are validated — an unknown zone name falls back to UTC instead of breaking the slot list or the crons. - The reminder and payment-confirmation crons isolate failures per booking, so one odd record cannot stop the reminders for everyone else. - New **Company** on the service. ### 20.0.1.3.0 (2026-08-14) - Resource scheduling: a service can now be scheduled on **resources** (tables, meeting rooms, courts…) instead of staff. Resources carry a seat capacity, a *shareable* flag (several bookings share the resource until the capacity is reached) and *Can be combined with* links (adjacent tables merged for big parties). - Website flow in resource mode: the visitor picks the party size (*How many people?*), the engine proposes only slots where the party fits and automatically assigns the smallest free resource — or the smallest combination of combinable resources when no single one fits. - Opening hours in resource mode come from a working-hours calendar on the service; capacity double-booking is blocked server-side; the backend gains a Resources menu (manager), bookings show the assigned resources and seats, and the confirmation / reminder e-mails and the manage page include the reserved resources and party size. Staff mode is unchanged. ### 20.0.1.2.0 (2026-08-14) - Month-grid slot picker: a calendar month view with available days highlighted, first free day auto-selected, month navigation. - Online prepayment (optional, needs the Payment app): the booking holds as Pending with a Pay-now link through the standard payment flow and confirms automatically once the transaction succeeds. - Multiple reminder offsets per service and optional custom confirmation/cancellation templates. ### 20.0.1.1.0 (2026-08-14) - Custom questions per service (free text or dropdown, required flag), answered on the booking page, stored on the booking, echoed into the CRM lead description. - External-calendar awareness: the staff user's other calendar events (including those fed by the Google/Outlook synchronisation apps) now block slots. - Optional CRM lead per website booking. - Guest invitees: visitors can add guest e-mails; guests join the calendar event. ### 20.0.1.0.0 (2026-08-14) - Initial release: bookable services with duration, slot step, buffers, capacity (group seats), price, location types, minimum notice, booking horizon, cancellation deadline and per-service reminder delay; staff with resource-calendar working hours and optional user link; timezone-aware slot engine and public booking pages (`/booking`, slot picker with week navigation and *Anyone available*); server-side slot re-validation and overlap/capacity constraint; booking lifecycle pending / confirmed / done / cancelled / no-show with auto or manual confirmation; automatic contact creation and `calendar.event` sync; confirmation, cancellation and reminder mail templates with `.ics` attachment; tokenised self-service manage page with `.ics` download and cancellation deadline; reminder and mark-done crons; backend calendar / list / form views with staff colours; user and manager access groups; multi-website support; exportable translation template; browser-tested (frontend tour) with unit and HTTP test suites. ## How we make sure it works Before every release, **85 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Cancelled booking cannot be moved by the customer. - A plain employee does not see the menu. - Own booking event not double counted. - Customer reschedule respects the notice. - The user configures nothing. The complete list is in the source you receive. --- # Appointment Booking All in One with Skill Routing > Appointment booking with skill-based routing: weighted share, daily and weekly caps, backup members and a visitor preference. - **What it is:** Appointment booking with skill-based routing: weighted share, daily and weekly caps, backup members and a visitor preference. - **For:** Website - **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:** 228 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_booking_routing - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Let a service be saved whose required skills no member of its team holds.; Override a visitor's explicit choice of staff member: the router assigns only when they leave No preference. - **Manual:** https://docs.bambooforge.dev/bambooforge_booking_routing/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_booking_routing.action_bf_booking_skill - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_booking_routing - **Manual last updated:** 2026-10-15 --- # Appointment Booking All in One with Skill Routing An add-on for **BambooForge Booking Pro** (17.0). Skills as records, a weighted share of the work, daily and weekly caps, backup members, and a preference the visitor can ask for on the booking page. ## What Booking Pro already does Booking Pro hands a booking to a member in one of three ways: *first free member*, *round robin* and *least busy*. All three treat every member as interchangeable. This add-on is for teams where they are not. ## Skills *Booking Pro ▸ Skills.* A skill is a record: *Colour*, *English*, *Paediatric*. Give each a short description - it is what the visitor reads when a service exposes it as a preference. *Booking Pro ▸ Staff* gains a **Skills** column. Tick what each member holds. On a service (*Who / what is booked*): - **Required skills** - only members who hold *every* one of them are offered for this service, on the website and by the router. Saving a service whose required skills no member of its team holds is refused: the alternative is a page with no slots and nobody knowing why. - **Visitor may ask for** - the skills the booking page shows as a preference. The visitor picks one or leaves *No preference*; the slot grid is recomputed for the members who hold it, the staff dropdown hides the others, and the booking records what was asked for. ## Weighted share A fourth assignment mode on the service, **Weighted share**, with a **Share window (days)** next to it. Each member has a **Weight** (staff list, default 1). When the visitor leaves *Anyone available*, the next booking goes to the free member whose bookings over the window, divided by their weight, is lowest - the person furthest below their share. Part-timers at 1, the senior at 3, the apprentice at 2. *Booking Pro ▸ Routing report* shows the team with **Target share** (from the weights) next to **Actual share** (from the bookings in the window), so a rota that drifts is seen, not suspected. ## Caps **Daily cap** and **Weekly cap** on a member (0 = none) count *appointments*: a group class with eight seats is one appointment. When a member reaches the cap, the rest of that day - or that week, Monday to Sunday in the member's own timezone - stops being offered for them. Seats in an appointment already on the diary stay open, so a yoga class at its teacher's third-and-last slot of the day still fills. ## Backup members Tick **Backup member** and the router only hands that member a booking when nobody else is free at that time. The visitor can still choose them by name. ## Back office Caps, skills and weights shape what the *website* offers. A receptionist placing a booking by hand sees a **Skill gap** warning when the member lacks what the service requires or the visitor asked for, and can still save. The desk knows things the rules do not. ## Technical notes - Extends Booking Pro's hooks only: `_slot_staff` (who is offered), `_pick_staff` (who is picked) and `_busy_intervals` (when a member is full). The double-booking lock in `booking_submit` is untouched. - The visitor's preference travels as `skill_id` on `/bf_booking/slots` and `/bf_booking/submit`; the controller accepts only skills the service exposes and ignores the rest. - Composes with *BambooForge Booking: Multi-location*: both filter the staff dropdown by data attributes and both add parameters on the wire. - Community only. No Enterprise module, no external service. ## How we make sure it works Before every release, **33 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A backup is picked only when nobody else is free. - A service nobody can serve is refused. - The last holder cannot drop the skill. - An unexposed preference is ignored not honoured. - Cancelled appointments do not fill the cap. 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: - Let a service be saved whose required skills no member of its team holds. - Override a visitor's explicit choice of staff member: the router assigns only when they leave No preference. ## Install 1. Copy `bambooforge_booking_routing` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Appointment Booking Suite: Skill-based Routing**. 3. Open **Skills** - its menus are Routing report. ## 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 Web; booking pro (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Skills** menu (Routing report). ****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. --- # Multi-branch Operations & Branch Reporting > Run several branches, shops or depots inside one company with their own document numbers, journals and reporting - no second company needed. - **What it is:** Run several branches, shops or depots inside one company with their own document numbers, journals and reporting - no second company needed. - **For:** Accounting - **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:** 169 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_branch - **Not for:** It does not rewrite invoice numbers. See above.; It does not make branches into companies. Stock still moves between two of your own depots as an internal transfer, not as a sale.; It does not restrict anybody until you put them in the group.; It does not fence journal entries, payments or bank statement lines. See above — that is a decision, not a gap.; It does not stop a sequence number being consumed by a quotation that then fails to save. ir.sequence draws from a Postgres sequence, which is not rolled back, so a validation error can leave a gap in a branch's quotation numbers. Quotation numbers are a reference, not a legal series; invoice numbers, which are, are left to Odoo. - **Manual:** https://docs.bambooforge.dev/bambooforge_branch/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_branch.action_bf_branch_sale - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_branch - **Manual last updated:** 2026-09-21 --- # Multi-branch Operations & Branch Reporting A company with four shops is still **one** company. One VAT number, one chart of accounts, one P&L for the tax office. Odoo's answer to "run them separately" is to make them separate companies, and that answer costs four charts of accounts to keep in step, an inter-company sale every time stock moves between two of your own depots, consolidated reporting you have to build yourself, and a partner maintained four times. This module adds a **branch** instead: a dimension that sits below the company and above the document. ## The branch itself **Branches › Configuration › Branches.** A branch is a shop, a depot or an office. It has a name, a **code**, a company, an address that can go on its documents, a manager, and a list of who works there. Keep the code short and keep it stable: it is what goes into the document numbers, so `LDS` is a better answer than `LEEDS-SHOP-01`. Two branches of one company cannot share a code, because the code is what tells their numbers apart. A branch that is in use cannot be deleted. Archive it instead — the documents that were filed under it keep pointing at something that still has a name. ## It goes on the documents that matter Quotations and orders, purchase orders, invoices and bills, transfers. Also on warehouses, and on people. A new document starts at **your** branch. If you work at exactly one, it is that one, without you ever opening your own user form — because somebody who has to pick their own branch on every quotation is somebody who leaves the field empty, and a dimension that is empty on half the database reports nothing at all. If you work at several, set a default on your user; if you work at none, documents start empty and stay visible to everybody. ## It propagates This is the part that decides whether a dimension gets filled in or not. - The branch on a **quotation** reaches its **invoice** and its **delivery**. - The branch on a **purchase order** reaches its **bill** and its **receipt**. - A transfer that came from no order takes the branch of the **warehouse** it is happening in. - A branch somebody chose by hand is never overruled. The delivery is the interesting one. A procurement's values are not copied wholesale onto the stock move it creates — `stock.rule._get_stock_move_values` builds the move dictionary key by key — so this module tells the rule to carry the branch across, and the transfer that `_assign_picking` builds out of those moves inherits it from them. ## Its own document numbers Tick **Own Document Numbers** on a branch and its quotations become `LDS/SO/00042` instead of `S00042`, from a real `ir.sequence` belonging to that branch, so two shops never race for the same number. Purchase orders get the same treatment. A quotation that already carries a reference — a duplicate, or an import from somebody else's system — keeps it. Core only reaches for a sequence when the name is still *New*, and so does this. Turning the switch back off does not destroy the sequence. The numbers it already handed out are on paper somewhere, and a sequence that restarts at one after somebody flicks a checkbox twice is a duplicate reference waiting to happen. **Invoice numbers are deliberately left alone.** An invoice number is not a naming convention; it is a legal sequence the tax office expects to be unbroken, and Odoo derives each one from the previous move in the same journal. A module that rewrites `account.move.name` produces gaps somebody has to explain to an auditor. So a branch that wants its own invoice numbering gets its **own journal** — one button on the branch form creates a sales journal and a purchase journal for it, with a free code, and new invoices from that branch are posted there. ## Restriction is opt-in, and it never blinds anybody Put somebody in **Restricted to their branches** and they see the documents of the branches they work at — **plus everything that has no branch on it yet**. That second half is not an oversight. A live database has years of documents with no branch on them. A rule that hides those on the day it is switched on takes half the pipeline away from the person who was told this would help them, and the rollout gets rolled back before anybody assigns a single branch. **What the fence covers.** Quotations and orders, purchase orders, invoices and bills, transfers — and, just as importantly, their *lines* and the analysis views built on top of them: `sale.order.line` (where margin lives), `account.move.line`, `stock.move`, and the three pivots people actually open — Sales Analysis, Purchase Analysis, Invoice Analysis. Fencing the header alone answers a checklist rather than the promise: Sales › Reporting › Sales reads `sale.report`, whose own "All Orders Analysis" rule is "everything", and a rule on `sale.order` does not touch it. **What it deliberately does not cover: journal entries.** Payments and bank statement lines both delegate to `account.move`, and record rules follow that delegation. A fence over every move would take the Leeds cashier's payment out of the York accountant's reconciliation widget — silently, because rules filter searches rather than raising, so the line would simply not be there. Branch reporting is about invoices and bills; the general ledger is one company's and stays that way. Nobody is restricted until somebody restricts them. The list of branches on a user is a statement of where they work, not a fence. Moving somebody between branches takes effect immediately: the rule domains are cached per user, and both doorways — the user form and the branch's *Who works here* tab — flush that cache. The rules are **global rules with a condition**, not rules attached to the restricted group. Record rules belonging to different groups are OR-ed together, so a rule attached to the restricted group would simply be ignored for anybody who also holds Sales / *See all leads* — which is most of the people worth restricting in the first place. Automation is never blocked. A scheduler that cannot create a delivery for the Leeds shop is a scheduler that stops the Leeds shop. ## Driving the unassigned half to zero The **Not filed yet** page on a branch counts the documents in that company with no branch on them at all, per model. Use **File the old ones under this branch** rather than selecting a whole list and setting the branch by hand. The branch is a tracked field, so a multi-edit over three hundred thousand invoices writes three hundred thousand chatter messages inside one request, and the request is killed long before it commits — leaving the count exactly where it was. The button works in batches, with tracking off, committing as it goes, and it takes its answer from what the document already knows: a transfer belongs to its warehouse's branch, an invoice or a bill to the branch of the journal it was posted in. What is left after that is what genuinely has to be decided by a person. Open a list, filter it down, and set the branch on a few thousand at a time. It is counted when you press the button and never on page load: it is four `search_count` calls over the four biggest tables in the database, which is not something a list view may do once per row. ## Reporting **Branches › By Branch** opens the lists people already know — sales, purchases, invoices and bills, transfers — grouped by branch. Same views, same filters, same columns anybody has already set up. There is also a *No branch yet* filter in each of those searches. ## What it does not do - It does not rewrite invoice numbers. See above. - It does not make branches into companies. Stock still moves between two of your own depots as an internal transfer, not as a sale. - It does not restrict anybody until you put them in the group. - It does not fence journal entries, payments or bank statement lines. See above — that is a decision, not a gap. - It does not stop a sequence number being consumed by a quotation that then fails to save. `ir.sequence` draws from a Postgres sequence, which is not rolled back, so a validation error can leave a gap in a branch's quotation numbers. Quotation numbers are a reference, not a legal series; invoice numbers, which are, are left to Odoo. ## How we make sure it works Before every release, **67 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - They cannot file under a branch they do not work at. - A document cannot be filed under another companys branch. - They cannot move a document out of their branch. - Two branches of one company cannot share a code. - A restricted user cannot un file a document. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_branch` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Multi-branch Operations & Branch Reporting**. 3. Open **Branches** - its menus are By Branch, 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, Inventory, Invoicing, Purchase, Sales, purchase_stock, sale_stock apps. No extra Python package. ****Where does it show up?**** Under the **Branches** menu (By Branch, 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. --- # Car Dealership Management > Used-car dealership stock with true cost per unit, days-in-stock ageing, and a deal that shows gross after discount, over-allowance and recon. - **What it is:** Used-car dealership stock with true cost per unit, days-in-stock ageing, and a deal that shows gross after discount, over-allowance and recon. - **For:** Industries - **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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_car_dealership - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Run a dealer network: it is built for a single dealership's forecourt.; Rewrite a preparation line that has been paid once the car is sold.; Let a deal under the floor through without a manager's signed-off action. - **Manual:** https://docs.bambooforge.dev/bambooforge_car_dealership/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_car_dealership.action_bf_stock_unit - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_car_dealership - **Manual last updated:** 2026-10-15 --- # Car Dealership Management For a single dealership - the forecourt, not a dealer network. Built around the two numbers a used-car business lives on: what a unit stands you in at, and how long it has been sitting there. ## Stock - VIN, plate, model, year, mileage, colour, photo, and where on the site it actually is. Cars go missing on forecourts more often than anybody admits. - The cost is the buying price **plus** transport **plus** every preparation line. That total, not the buying price, is what a deal is judged against. - **Days in stock** is computed and banded: under 30, 30-60, 60-90, over 90 - coloured on the forecourt board. A scheduled job puts the ninety-day cars on the list of whoever bought them - never on OdooBot's, where nobody would ever see it - because they are the ones nobody mentions in the morning meeting. The note it leaves says how long the car has stood there and nothing about what it cost: everyone who can read the car can read its notes. - A preparation line that has been paid is not rewritten once the car is sold. A late bill goes on as its own line, and it moves the unit's cost without moving the gross of a deal that is already done. - A salesperson may say where a car is, what the odometer reads and what it needs. What it cost, what it is priced at and when it came in belong to the manager who bought it - field by field, not just on the screen. ## The deal The gross is shown after the **three ways money quietly leaves**, and before the one that comes back: 1. the **discount** off the screen price; 2. the **over-allowance** on the part exchange - what was allowed for the customer's car less what it is really worth; 3. the **cost of the extras**, so a warranty sold at cost shows as the favour it is - and the cost comes off the product on the server, so an extra added by a salesperson who cannot see costs still carries one; 4. plus the **finance commission**, because a car sold on finance makes more than the same car sold for cash. - The **floor price is enforced on the server**, not in the button bar. A deal under it needs a *sign below the floor* action that refuses anyone but a manager - no context flag, no hidden button, and who signed it off goes on the record. The message deliberately does not name the figure, because a salesperson who knows the floor negotiates to it. **The over-allowance counts against the floor**: money given away on the trade-in is money off the car. - A **part exchange is taken into stock** at what it is really worth, as its own unit, through a short form that asks what the customer's car actually is. Only a manager puts the value on it; taking the car in and signing the deal are both refused while an allowance has been promised for a car nobody has appraised. - **A deposit holds the car**, and nothing else does: a deal cannot be reserved without one and it is refundable until signature. After signature it is part of the price, and a manager either **keeps it** or **releases it** - either way it is on the record, and either way the deal can then be lost. A signed deal that falls through is not left standing for ever with the car marked sold. - **What a deal was signed on stops moving.** Price, allowance, car, customer and extras are locked at signature; a manager who needs to change one **reopens** the deal in the open and signs it again. A live deal is lost, not deleted. - **The gross is frozen at signature** on what the car stood us in at that day, so a bill that turns up a fortnight later moves the unit's cost without rewriting a month that has already been reported. - **One car, one live deal** - held by a unique index as well as by the check, because two salespeople in two tabs on a Saturday is exactly when the check is too late. ## Delivery Cars leave against paperwork: delivery is refused until a **posted** invoice exists that has not been credited - a reversed invoice is no paperwork, and it lets the deal be invoiced again. Invoicing and handing the car over are the manager's, on the server and not only on the screen. The invoice carries the part exchange as a **negative line**, so the customer reads one figure rather than two documents, the extras sit on it with their own tax, and the vehicle line is raised on a **product chosen once in the settings**, so it has a tax and an income account like every other line in the ledger. A **lost deal gives the customer their car back**: the part exchange comes off the forecourt with it. ## Roles A **salesperson** quotes, reserves and signs within the floor, takes a part exchange in, and sees the screen price but never the cost, the floor, the over-allowance or the gross - all of them are field-level rules, and none of the figures appear in a chatter note either. A **sales manager** buys stock, sets the floor, values a part exchange, signs off deals below it, reopens a signed deal, settles a deposit after signature, invoices, hands the car over, and sees every number. Every one of those is checked on the server. A hidden button is not a rule. ## Community only Depends on `product` and `account` - both Community. Tested on 16.0, 17.0, 18.0, 19.0 and 20.0. ## How we make sure it works Before every release, **60 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A deal with a deposit held cannot simply be lost. - A quote can be deleted a delivered deal cannot. - A car does not leave without paperwork. - A credit note stays on the deal. - Recon cannot be rewritten after the sale. 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: - Run a dealer network: it is built for a single dealership's forecourt. - Rewrite a preparation line that has been paid once the car is sold. - Let a deal under the floor through without a manager's signed-off action. ## Install 1. Copy `bambooforge_car_dealership` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Car Dealership Management**. 3. Open **Dealership** - its menus are Stock, Deals, 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, Invoicing, Products, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Dealership** menu (Stock, Deals, 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. --- # Cash Flow Forecast & Treasury Projection > Cash flow forecast from open receivables and payables by due date plus manual items, with a running projected balance, pivot and chart. - **What it is:** Cash flow forecast from open receivables and payables by due date plus manual items, with a running projected balance, pivot and chart. - **For:** Accounting - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_cash_forecast - **Manual:** https://docs.bambooforge.dev/bambooforge_cash_forecast/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_cash_forecast.cash_forecast_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_cash_forecast - **Manual last updated:** 2026-10-15 --- # Cash Flow Forecast & Treasury Projection See where your cash is heading. This app buckets your **open customer invoices** and **vendor bills** by due date over a horizon you choose, adds any **manual known items**, and projects a **running cash balance** — with a chart — so you can spot a squeeze before it happens. This page is the complete manual. Follow it top to bottom and you can install, build a forecast, read the projection and fix the common issues **without contacting support**. ## Overview A forecast is built from three sources, period by period: - **Expected in** — the open (unpaid) amounts on your customer invoices, placed in the period of their due date. - **Expected out** — the open amounts on your vendor bills, by due date. - **Manual items** — known future movements not yet in Odoo (payroll, rent, a loan drawdown), positive for money in, negative for money out. Starting from your **opening cash**, each period's net (in − out + manual) rolls forward into a **projected balance**, shown as a table and a line chart. It honours payment terms: an invoice paid in installments is split across the periods of each installment's due date. ## Requirements - Odoo 20.0. - The **Invoicing/Accounting** app (`account`). Installing this app pulls it in automatically. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_cash_forecast` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **Cash Forecast** and click *Install*. A new top-level **Cash Forecast** app appears, and a **Cash Flow Forecast** entry is added under *Accounting > Reporting*. ## Quick start 1. Open **Cash Forecast > Forecasts** and click *New*. 2. Set the **Start Date**, the **Period** (weekly or monthly) and the **Number of Periods** to project. 3. Click **Compute Opening** to pull the current balance of your bank and cash accounts (or type an opening figure). 4. Optionally add **Manual Items** on the second tab. 5. Click **Compute**. The forecast switches to *Computed*. The **Projection** tab shows each period's opening, expected in/out, manual, net and projected balance; the **Projection Chart** button plots the balance over time. ## Reading the projection Each period row shows: - **Opening** — the projected balance carried in from the previous period. - **Expected In / Out** — open receivables and payables due in the period. - **Manual** — your manual items for the period. - **Net** — In − Out + Manual. - **Projected Balance** — Opening + Net, carried forward. A negative projected balance is highlighted, and the chart makes a coming squeeze obvious at a glance. ## Opening balance **Compute Opening** sums the balance of your liquidity (bank and cash) accounts up to the start date. You can override the figure if you want to forecast from a different starting position. ## Overdue amounts With **Include Overdue** on (the default), amounts already past due are lumped into the first period — they are cash you still expect to collect or pay. Turn it off to forecast only amounts coming due from the start date onward. ## Manual items On the **Manual Items** tab, add a date, a description and a signed amount for any known future movement that is not yet an invoice or bill — payroll, rent, tax payments, a planned loan drawdown. Positive amounts are inflows, negative are outflows. They are bucketed by date like everything else. ## Automating refreshes A scheduled action **Cash Forecast: Recompute Latest** ships **disabled**. Enable it under *Settings > Technical > Scheduled Actions* to roll the most recent forecast forward to today and recompute on a schedule (weekly by default). ## Frequently asked questions ****Amounts are split across several periods.**** That is the payment terms at work: an invoice paid in installments contributes to the period of each installment's due date. This is the realistic behaviour. ****Items beyond my horizon don't appear.**** Correct — only amounts due within the projected periods are shown. Increase the number of periods to look further ahead. ****The opening balance looks wrong.**** Compute Opening uses your bank and cash accounts. If you keep cash elsewhere, type the opening figure manually. ****Does it forecast in foreign currency?**** Figures are presented in your company currency, summed from the company-currency amounts of each entry. ****Does it work with multiple companies?**** Yes. Each forecast is scoped to one company's receivables, payables and cash. ## How we make sure it works Before every release, **14 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Bands may not overlap. - No list column hidden with invisible. - An empty contact falls back to odoo 20's company timezone. - The rolling forecast starts on the company's day. - Bucketing is date against date in any timezone. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## Usage This section walks through the day-to-day treasury routine: how a controller builds a forecast on Monday morning, reads it, keeps it honest through the week, and re-uses it every period. It assumes the app is installed and that you have the *Billing* access right (`account.group_account_invoice`), which the menu requires. ### Where to find it Two menu paths open the same list of forecasts: - **Cash Forecast > Forecasts** — the dedicated top-level app. - **Accounting > Reporting > Cash Flow Forecast** — for accountants who live in the Accounting app. Both open the *Cash Flow Forecasts* list. Computed forecasts show a green **Computed** badge; a projected ending balance below zero is shown in red so a looming shortfall is visible before you even open the record. ### Building the weekly forecast 1. Click **New**. The name is assigned automatically from the *Cash Forecast* sequence (e.g. `CF/2026/0007`) and is read-only. 2. Under **Scope**, set the **Start Date** (typically today), the **Period** (*Weekly* for a rolling 13-week treasury view, or *Monthly* for a longer horizon), and the **Number of Periods**. Leave **Include Overdue** on so amounts already past due are collected into the first period — they are still cash you expect to move. 3. Press **Compute Opening**. This sums the posted balance of your bank and cash accounts (account type *Bank and Cash*) dated before the start date and writes it into **Opening Balance**. If you hold cash the ledger does not know about, type the figure over it. 4. Open the **Manual Items** tab and add any known movement that is not yet an invoice or bill — payroll run, quarterly VAT payment, rent, a planned loan drawdown. Enter a **Date**, a **Description** and a signed **Amount** (positive = money in, negative = money out). 5. Press **Compute**. The record moves to *Computed*, the **Projection** tab fills with one row per period, and a chatter note records how many periods were projected. ### Reading the result The **Projection** tab lists, for each period: **Opening**, **Expected In** (open receivables due in the period), **Expected Out** (open payables), **Manual**, **Net** (In − Out + Manual) and **Projected Balance** (Opening + Net, carried into the next row). Any period whose projected balance turns negative is highlighted in red. The **Position** group at the top summarises **Total Inflow**, **Total Outflow** and the final **Ending Balance**. Click the **Projection Chart** stat button to open the same data as a line graph (with a supporting list view) so the trajectory — and any dip below the line — is obvious at a glance. ### A worked example A distributor with **€40,000** opening cash builds a weekly forecast over eight periods. Open customer invoices total €95,000 spread across their due dates; open vendor bills total €78,000. On the Manual Items tab the controller adds `-12,000` on the 28th (monthly payroll) and `+20,000` mid-month (an expected grant). After **Compute**, week 4 shows Expected In €8,000, Expected Out €22,000 and the €12,000 payroll, giving a Net of −€26,000 and a **Projected Balance** that dips red. The controller pulls two large customer payments forward and re-runs the forecast to confirm the dip is cleared. ### Keeping it current Through the week, edit the Manual Items or change the Scope and press **Recompute** — the period rows are rebuilt from scratch each time. Use **Reset to Draft** to clear the projection and start over. To roll your latest forecast forward automatically, enable the scheduled action **Cash Forecast: Recompute Latest** under *Settings > Technical > Scheduled Actions*; it re-bases the most recent computed forecast per company to today and recomputes on its schedule. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_cash_forecast -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (3 models, prefixed `cash.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## 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 Invoicing app. No extra Python package. ****Where does it show up?**** Under the **Cash Forecast** menu (Forecasts, Collection Profiles). ****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. ## Changelog ### 20.0.1.1.3 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Project your cash position over the coming weeks or months from open receivables and payables (by due date), plus manual known items. - With a running projected balance, pivot and chart. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # Chatter Tools: Search, Pin & Bulk Messages > Search every chatter at once, pin what matters, post a template onto hundreds of records, resend failed notifications and purge dead rows. - **What it is:** Search every chatter at once, pin what matters, post a template onto hundreds of records, resend failed notifications and purge dead rows. - **For:** Discuss - **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_chatter_tools - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Send a message a template cannot render: it posts nothing rather than half a sentence.; Read chatters the user cannot read - search runs with the user's own rights. - **Manual:** https://docs.bambooforge.dev/bambooforge_chatter_tools/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_chatter_tools.action_bf_chatter_resend - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_chatter_tools - **Manual last updated:** 2026-10-15 --- # Chatter Tools: Search, Pin & Bulk Messages The chatter is Odoo's best idea and its least maintained screen. Five things it cannot do, and every desk needs. ## Search every chatter at once **Settings › Chatter Tools › Search the Chatter.** Every message on every record: by text, by author, by model, by type, by month, grouped however you like. The chatter itself can only be read one record at a time, which is no use at all when somebody asks "who told the customer that, and when?". Two filters do most of the work. *Typed by a person* keeps `comment` and `email` — what somebody wrote and what arrived — and gets the twenty million system rows out of the way. *Written by the system* does the opposite, and is how you look at what a cleanup rule is about to throw away before you let it. ## Templates The paragraph somebody types forty times a week — "rang them about the delay", "credit checked, on hold" — written once. Placeholders are the ones a mail template uses. `{{ object.name }}` works in the rich-text box, because QWeb interpolates `{{ }}` in attributes but never in a text node — which is why a mail template writes `{{ }}` in the subject and `` in the body — and the template is converted before rendering, exactly the way core converts it for its own preview. A `t-out` written by hand keeps working. A template says whether it posts an **internal note** (the default) or a **message to the followers** (which sends email). It can be limited to a role, and to a company. **Posting one at a time** is the *Post from template* entry in the Action menu of that model's list. Five templates on `sale.order` put **one** line in that menu, not five: the entry belongs to the model. **Posting on two hundred records at once** is the same entry with two hundred rows ticked. The wizard renders the **preview against the first record you selected, by name** — a placeholder typo shows up there rather than in two hundred chatters where nobody can take it back — and says, in as many words, how many emails a *message* would send. A template that does not render posts nothing at all rather than half a sentence, and one record that cannot be posted to does not lose the other hundred and ninety-nine. Each template counts how often it has been used. A template nobody uses is a template to delete. ## Pinning One message on a record that the next person has to read before they do anything. One click in the message's hover menu; the pin records who did it and when, and pinned messages are never deleted by a cleanup rule. Odoo 20's chatter does not take extra actions in that menu, so pinning there is done from **Search the Chatter** instead. Everything else in this module is the same on every series but the newest, whose build differs only where Odoo moved what it hangs off. ## Resending what failed A notification in exception is invisible: the record says the customer was told, and the customer was not. **Settings › Chatter Tools › Failed Notifications** lists them with the reason and the date, and puts the ones you tick back in the queue. A notification on a record you may not **write** to is left alone whatever is ticked, and the wizard says how many it skipped. Sending somebody's mail again is writing on their record. ## Throwing the rest away Every stage change and every tracked field writes a `mail_message` row. Nobody reads the ones from 2019 and everybody pays for them in backup time, in restore time and in the index. A cleanup rule says: on this model, messages of this kind, older than this many months. The kinds are - **Notifications only** — stage moves, tracking, "created by"; - **Everything the system wrote** — the above plus automated comments and user notifications; - **Empty** — a notification with no body and no file, which is the most common row of all. **Three things survive every rule, whatever it is set to.** That is not three checkboxes; it is the shape of the rule, and it is decided in one method so there is one place to read and one place to argue with. 1. *A message somebody typed.* The kinds are an allow-list of one message type, `notification`, so `comment` and `email` are outside every rule — and so is a message type invented by a future version, which is kept rather than guessed at. 2. *A message whose body was ever visible outside the company.* This is not the same as "somebody typed it", and it is the wall that matters most. The copy of the quotation the customer was sent, the delivery confirmation, the portal invite — core writes all of them into the chatter through `message_post_with_source`, as `message_type='notification'` with the customer-facing `mail.mt_comment` subtype. A rule that trusted the message type alone would throw away the only record of what the customer was promised, and the tracking values and delivery receipts would cascade away with it. 3. *A message an email actually went out from.* If somebody was sent it, it is evidence of what they were told. A tracked-field note has no body at all and falls through all three, which is the point: those are the rows there are twenty million of. Files, pinned messages and starred messages are kept unless you say otherwise — and if you say the files may go, the files really go, rather than the row alone while the blob stays in the filestore. **Show me what would go** counts the rows and opens exactly those rows, by id, so the list and the number are the same thing. The nightly job ships **switched off**: a cleanup rule that starts deleting the night it is installed is an incident, not a feature. Turn it on in Scheduled Actions once a preview has told you the truth. It runs oldest rule first with a budget per night, in batches, and one rule that fails does not stop the others. ## Who can do what Every internal user is a *Chatter Tools user*: posts from a template, pins a message, sends a failed notification again — always on records they may already write to. A *manager* writes the templates everybody uses and the cleanup rules that throw chatter away. ## What it needs `mail` only. Community. No Enterprise module is required, and none is used. ## How we make sure it works Before every release, **66 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A note an email actually went out from is never deleted. - A portal customer never sees a pinned internal note. - A template that is not offered cannot be posted. - Records the caller cannot read are dropped. - A broken placeholder shows in the preview not the chatter. 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: - Send a message a template cannot render: it posts nothing rather than half a sentence. - Read chatters the user cannot read - search runs with the user's own rights. ## Install 1. Copy `bambooforge_chatter_tools` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Chatter Tools: Search, Pin & Bulk Messages**. 3. Open **Failed Notifications** - its menus are Search the Chatter, Templates, Cleanup Rules. ## 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, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Failed Notifications** menu (Search the Chatter, Templates, Cleanup Rules). ****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. --- # Hospital & Clinic Management with Light EMR > Clinic and hospital management on Odoo Community: patient files with allergies, a day sheet, consultations with vitals, and prescriptions. - **What it is:** Clinic and hospital management on Odoo Community: patient files with allergies, a day sheet, consultations with vitals, and prescriptions. - **For:** Industries - **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:** 149 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_clinic - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Book the same practitioner twice at the same time.; Sign off an encounter without a diagnosis. - **Manual:** https://docs.bambooforge.dev/bambooforge_clinic/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_clinic.action_bf_clinic_appointment - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_clinic - **Manual last updated:** 2026-10-15 --- # Hospital & Clinic Management with Light EMR ## What it is A small clinic runs on three things Odoo Community does not have: a patient file, a day sheet, and a consultation that turns into money. This app adds exactly those three, and deliberately stops there - it is not a hospital information system and does not pretend to be one. ## The patient file One file per person, attached to the Odoo contact rather than replacing it, so the invoice still goes where invoices go. - A permanent **patient number** (`P-00042`) assigned once. The front desk quotes it on the phone; it never gets reused. - Date of birth with the age computed the way a clinician reads it - years for adults, months for infants, days for a newborn. - Blood type, insurance details, emergency contact, standing conditions. - **Allergies**, shown as a red banner on every consultation and every prescription. That banner is the reason this model exists. ## The day sheet Not online booking - that is a customer picking a slot on a website. This is the list the front desk works from: - **Booked → arrived → in consultation → done**, plus no-show and cancelled. One click checks somebody in and starts the clock. - A **waiting-time column**, because how long people sit there is the number a clinic is judged on. - Overlapping bookings for the same practitioner are **refused** - two people cannot have the same doctor at the same time. Cancel one and the slot frees itself. - An **overnight job closes the day** once it is over, in the clinic's own timezone: still booked and never came is a no-show; checked in and never written up is *left without being seen*, which is not the same thing and must not be counted as one; a visit the doctor saw and wrote up next morning closes itself. A clinic running ninety minutes late keeps its waiting room. - The same data as a day calendar, coloured per practitioner. ## The consultation - Chief complaint in the patient's own words, examination, diagnosis with an **ICD-10** field, and a plan. - **Vitals**: blood pressure, pulse, temperature, respiratory rate, SpO2, weight, height - with **BMI computed and classified**. - Vitals are checked against plausibility bands. A temperature typed as `370` is refused, and so is a blood pressure entered the wrong way round (diastolic above systolic). These are the two mistakes that otherwise reach a chart nobody re-reads. - Sign-off requires a diagnosis: an encounter with no finding is not a record, it is a gap. - A follow-up date schedules a real Odoo activity for the practitioner, so a review is not left to memory. ## Prescriptions - Print with the **practitioner's licence number**, the patient's allergies and a validity date - the three things the pharmacist on the other side actually needs. - Issuing is refused when the practitioner has no licence number on file, because such a prescription is not valid at a pharmacy. - Once issued a prescription is **read-only**. Correcting one means cancelling it and writing another: a document that can change after printing is not a record. ## Billing - Billable items sit on the consultation - a consultation fee, a dressing, a test - and become **one customer invoice in one click**. - The invoice can only follow the record, never lead it: an unsigned encounter cannot be billed. - A **"To invoice"** list shows every signed-off visit that was never billed, and a visit whose invoice was credited comes back onto it. That list is the usual clinic leak, and it is one menu item. - **Reception raises and posts the bill**, without holding a single accounting right: the module writes the journal entry itself, behind the same permission check the visit makes. The front desk reads back the invoices it raised and nothing else in the ledger. Crediting one is an accounting decision and stays with the clinic manager, who does hold Odoo's invoicing rights. ## Who sees what - **Front desk** books, checks in, invoices and takes payment - and cannot open clinical notes or prescriptions. Reception is not a clinical role, and the access rights say so. - **Practitioners** hold the clinical record. - **Managers** configure rooms, practitioners and allergies, are the only ones who can reopen a signed-off encounter, and hold the invoicing rights the practice's books need. Multi-company record rules throughout: a patient file belongs to the clinic that opened it. ## Requirements Odoo Community 16.0, 17.0, 18.0, 19.0 or 20.0. Depends only on `mail`, `product` and `account` - all Community. No Enterprise module, no external service, nothing phoning home. ## How we make sure it works Before every release, **98 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The front desk is not shown buttons that can only fail. - A repeat of an expired script is written not refused. - The front desk cannot read the rest of the ledger. - A posted invoice cannot be detached by an import. - The state cannot be moved by a plain write. 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: - Book the same practitioner twice at the same time. - Sign off an encounter without a diagnosis. ## Install 1. Copy `bambooforge_clinic` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Hospital & Clinic Management with Light EMR**. 3. Open **Clinic** - its menus are Day sheet, Patients, Clinical, Billing. ## 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, Invoicing, Products, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Clinic** menu (Day sheet, Patients, Clinical, Billing). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - answered within two business days. This module is licensed OPL-1; one purchase covers one production database plus its staging copies. ## What changed in 1.3 A second operations review, this time against the roles rather than the records. Everything below has a test that was written to fail against the old code first: - **Reception can raise the bill, post it and read it back** - which is what the listing always said and what no clinic group could actually do: creating an invoice needs Odoo's invoicing right, and no clinic group implied it. The module now writes the journal entry with its own rights behind the same permission check the visit makes, and the front desk sees the invoices it raised and nothing else in the ledger. Crediting one stays with the clinic manager, who does hold the accounting groups. - **The buttons match the rights.** Sign off, Prescribe, Start consultation and the prescriptions counter are practitioners' work and are no longer offered to the desk, where they could only ever raise an access error. - **The workflow is the only way a visit changes state.** An import or an RPC call could write `state` and `move_id` directly: the posted invoice was orphaned, the visit came back onto the "To invoice" list and the next click billed the same consultation twice. - **A signed-off or invoiced visit cannot be deleted**, nor can an issued prescription. Freezing edits and leaving deletion open is not freezing anything. - **"Not billable" needs a reason** on every path, not only where the form asked for one. - **Only a manager reopens a visit** - in the record, not only on the screen. - **Re-signing a visit no longer stacks follow-up reminders**, and clearing the review date clears the reminder. - **The day closes in the clinic's own timezone**, an hour that means something in the building. A patient who arrived and was never written up is filed as *left without being seen*, not as a no-show: she came, and the practice should not phone her to ask why she did not. One historical double booking no longer rolls back the whole night, every night. - **Overbooking is checked both ways**: unticking the flag puts the slot back under the rule, and an emergency still reserves the room it is in - a doctor can run late, a room cannot hold two consultations. - **Clash messages are in the reader's timezone.** A receptionist in Brussels was told about an appointment at 02:00. - **"Write a repeat" works on the prescriptions people repeat**: the validity window comes forward instead of refusing the copy. - **Two branches are two clinics**: a practitioner, room, patient or product from the other branch is refused rather than quietly booked. - Booking a slot and raising an invoice both take a lock first, so two receptionists in the same second cannot make two of either. ## What changed in 1.1 An operations review against a real three-doctor practice found nine things that bite in the first week. All of them are fixed and each has a test: - **Reception could not open a single patient file** - the visit counters dragged the desk through an access check on the clinical record. Counted with sudo now; the desk sees *how many* visits, never what happened in them. - **Reception can now invoice**, which is what a front desk does, while every clinical field (complaint, examination, diagnosis, plan, all vitals, allergies, standing conditions) is blocked **at ORM level**, not merely hidden in a notebook page. Clinical fields are no longer tracked either: a tracked field prints itself into a chatter the desk can read. - **A cancelled or deleted invoice** used to leave the visit looking billed for ever, invisible and unbillable. It now returns to the "To invoice" list and says why in the chatter. - **A signed-off visit is frozen.** Changing a price on an invoiced encounter used to leave the ledger and the record disagreeing. Reopen it deliberately, or not at all. - **"Not billable"** is a state: the free re-check, the staff visit, the insurer-direct case. Without it the leak list filled with noise and people stopped reading it. - **Tax.** The counter now quotes the number the invoice will print, and the invoice carries the product's taxes through the patient's fiscal position. - **The day job closes days, not hours.** A clinic running ninety minutes late keeps its waiting room; yesterday's leftovers are closed overnight, including visits the doctor saw but wrote up next morning. - **Overbooking is a decision**: tick "Overbooked" to squeeze in a walk-in instead of lying about the time. Two consultations can no longer share a room. - **Waiting time is searchable**, so "who has waited longest" is a filter rather than a column read down by eye. A cancelled prescription no longer prints as a valid one, and "Write a repeat" creates a new editable prescription instead of duplicating a frozen one. --- # Sales Commission Management > Sales commission that survives an audit: tiered quotas, commission on margin, pay-on-payment with clawback, one traceable line per invoice line. - **What it is:** Sales commission that survives an audit: tiered quotas, commission on margin, pay-on-payment with clawback, one traceable line per invoice line. - **For:** Sales - **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:** 119 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_commission - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Accept overlapping bands on a plan, or a band that ends before it starts.; Compute margin without product costs - a plan on margin reads the cost from the product. - **Manual:** https://docs.bambooforge.dev/bambooforge_commission/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_commission.action_bf_commission_run - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_commission - **Manual last updated:** 2026-10-15 --- # Sales Commission Management Commissions that survive an audit: tiered quota ladders, commission on margin as well as revenue, pay-on-payment with automatic clawback on credit notes, and one traceable line per invoice line — generated, reviewed, confirmed and paid in runs. Odoo ships no commission engine in Community or Enterprise. This is that engine, and this page is its complete manual. ## The three records **Plan** — who is paid, on what, and when. **Run** — a period: generate, review, confirm, pay. **Commission line** — one earning, tied to the invoice line that produced it. Nothing is computed on the fly at reporting time: a line is a record with the base, the rate, the paid share and the resulting amount, so a payout can be explained six months later. ## Install 1. Install **BambooForge Commission Engine** (depends on Accounting and Sales). 2. Give the people who own the plans the **Commissions: Manager** group. Salespeople with the standard Sales user group can read commission lines. 3. The **Commissions** menu appears with *Commission Runs*, *Commissions* and *Plans*. ## Write a plan *Commissions → Plans → New* **Who it pays** | The invoice's salesperson | Whoever is on the invoice. The usual case. | |---|---| | The sales team's leader | Pays the manager of the team on the invoice. | | One person | A fixed beneficiary, whoever sold it — for an overlay or a house account. | | A partner acting as agent | Pays a contact rather than a user: dealers, resellers, introducers. | **What it pays on** - **Revenue (untaxed)** — the invoice line's subtotal. - **Margin** — subtotal minus quantity × product cost. No extra module: the cost comes from the product, so a plan on margin works out of the box. **When it pays** - **When the invoice is posted** — the commission exists as soon as the invoice does. - **When the customer pays** — the commission follows the money: a half-paid invoice pays half, and nothing is earned until something is collected. **Filters** — product categories (children included) and specific customers. Leave them empty for everything. **Bands** — the ladder. Each band covers a range of the *cumulative amount the beneficiary has reached in the period*: 0–50,000 at 2%, 50,000–100,000 at 3%, above 100,000 at 4% is three bands; leave the last band's *To* empty. A band can also carry a **fixed amount** paid once per invoice line — a per-deal bonus on top of the rate. Bands are validated: one cannot end before it starts, and two cannot overlap. ## Run a period *Commissions → Commission Runs → New* — pick the dates, optionally restrict to some plans, then **Generate**. The run walks every posted customer invoice and credit note whose invoice date falls in the period, applies each matching plan, and creates one commission line per invoice line. Then: - **Confirm** freezes the lines (they move to *Confirmed*). - **Mark paid** closes the run once the money has left. - **Reset to draft** re-opens a confirmed run; a *paid* run stays paid. Generation is **idempotent and non-overlapping**: an invoice line that already carries a commission line for a plan is never paid again, in this run or any other. Regenerate a draft run as often as you like as invoices are posted or paid. ## How the numbers come out **The base** is the invoice line's untaxed subtotal, or its margin when the plan says so. A credit note produces the same base with the opposite sign. **The band** is chosen by what the beneficiary had already earned *before* this line inside the run, so a ladder actually ratchets. **The paid share** applies on pay-on-payment plans: the amount collected divided by the invoice total, clamped to 0–100%. Rerun the period after more money arrives and the remaining share follows. **The clawback** is a normal commission line with a negative amount, flagged as a clawback, dated in the credit note's period. Nothing is deleted, so the history stays readable. ## Reading the result *Commissions → Commissions* lists every line, grouped by beneficiary by default, with filters for draft / confirmed / paid and for clawbacks. The pivot view (beneficiary against month) is what most people export for payroll. Every line has a button to open the invoice it came from, and carries the band's name so the rate can be traced to the plan. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Accept overlapping bands on a plan, or a band that ends before it starts. - Compute margin without product costs - a plan on margin reads the cost from the product. ## Troubleshooting ****A line is missing**** Check in order: is the invoice posted, is its invoice date inside the period, does the plan's category or customer filter exclude it, does the plan have at least one band, and — for pay-on-payment — has any money arrived. ****Nothing is generated at all**** A confirmed run refuses to regenerate on purpose, so amounts people have already agreed to do not move. Reset it to draft first. ****The rate looks too low**** The ladder is cumulative *within the run*: the first invoices of a period sit in the lowest band. A monthly run and a yearly run therefore pay differently, which is exactly what a quota ladder means. ****Margin is negative**** The line was sold below the product's cost. The commission is negative, which is the honest answer; exclude those products with a category filter if that is not your policy. ****Two plans pay the same invoice**** That is allowed — an overlay plan on top of a base plan is a common design. Each plan produces its own line. ## What changed in 1.1 - **Ladders keep counting.** Regenerating a run - or running a second run over the same period - no longer restarts the quota ladder at the bottom band: everything already commissioned still counts. - **Late payments are paid.** On pay-on-payment plans, money that arrives after a run was confirmed becomes a *payment true-up* line in the next run - flagged, auditable, never double-counted. - **Foreign-currency invoices convert** to company currency at the invoice date before any rate is applied. - **Margin uses the deal's cost** (the sale order's cost field) when the sale margin feature is installed, so later cost changes never rewrite commission history. - A monthly scheduled job drafts last month's run; a salesperson now sees **only their own** commission lines; plans and runs carry a chatter that tracks every change. ## What changed in 1.2 - **Statements**: print all beneficiaries at once, or mail each person a PDF holding only their lines - true-ups and clawbacks labelled as such. - **Agent bills**: one click turns the confirmed lines of every agent into a real vendor bill (one per partner, one bill line per commission line, never billed twice), in the purchase journal you choose on the run. ## How we make sure it works Before every release, **60 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A paid line cannot be cancelled. - A paid run cannot be reset. - An empty run cannot be confirmed. - Generating twice does not pay twice. - A second run does not pay the first run again. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — questions, bug reports and feature requests are answered by the developers who wrote the module. --- # Cookie Consent Banner & GDPR Consent Mode v2 > GDPR cookie consent banner with Google Consent Mode v2, script blocking until consent, a tamper-proof log and presets for GA4 and Meta Pixel. - **What it is:** GDPR cookie consent banner with Google Consent Mode v2, script blocking until consent, a tamper-proof log and presets for GA4 and Meta Pixel. - **For:** Website - **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_consent_pro - **Manual:** https://docs.bambooforge.dev/bambooforge_consent_pro/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_consent_pro.action_bf_consent_script - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_consent_pro - **Manual last updated:** 2026-09-21 --- # Cookie Consent Banner & GDPR Consent Mode v2 A complete consent-management platform (CMP) for the Odoo 20 website: per-category cookie banner with a preferences modal, Google Consent Mode v2 signals, marketing/analytics scripts held back until the visitor agrees, an audit-ready consent register, EU geo-targeting and one-click presets for the common trackers. This page is the complete manual. Follow it top to bottom and you can install, brand the banner, wire your trackers, prove consent to an auditor and fix the common issues **without contacting support**. ## Overview - **Per-category banner** — Essential (always on), Functional, Analytics, Marketing. Accept all / Reject non-essential / granular preferences, all equally easy, as the GDPR requires. - **Google Consent Mode v2** — a static `gtag('consent','default',…)` emitted at the very top of `` (before any Google tag) and `consent update` pushes on every choice, covering `ad_storage`, `ad_user_data`, `ad_personalization`, `analytics_storage`, `functionality_storage` and `personalization_storage`. Odoo's built-in GA4 integration is governed automatically. - **Script blocking** — register trackers as *Managed Scripts* and they are only injected after their category is granted. Visitors who already consented get the scripts server-rendered (no flicker, no lost pageviews). Hand-placed tags can use the ` ``` Google tags (GA4, Google Ads) need **no** managed script when Consent Mode is enabled: keep using Odoo's built-in GA4 field — Google's own tags read the consent signals and stay dormant until granted. ### The consent register **Website ▸ Configuration ▸ Consent Pro ▸ Consent Register** lists every choice: timestamp, salted visitor key, action, granted categories, policy version, country. Filter and group by action/country/day; export to XLSX with the standard exporter for an audit bundle. Entries older than the per-website retention (Settings, default 13 months — CNIL guidance) are vacuumed automatically. **One entry per distinct choice.** An entry is identified by *website + visitor key + policy version + action + granted categories*. When the same visitor submits **exactly the same choice again** — reloading the page, clicking *Accept all* twice, a script replaying the request — the existing entry is refreshed in place and **no row is added**. A **genuine change of mind** differs in the action or in the categories (accept-all, then withdraw; analytics on, then analytics off) and is therefore stored as **its own entry**, so the register still shows what changed and when. This is deliberate: `/bf_consent/save` is a public endpoint, so without it anyone could inflate the register into unbounded growth by replaying one request. Deduplication keeps the register an accurate picture of the choices actually made — and **retention is unchanged** (13 months by default). One consequence worth knowing: a refreshed entry keeps the timestamp of the moment that choice was **first** recorded, so the register dates the choice, not the last page view that repeated it. ### Changing your cookie policy Bump **Policy version** in Settings. Existing consent cookies no longer match, so every visitor is prompted again and new register entries carry the new version. ### Withdrawing consent The floating 🍪 badge (and the `window.bfConsent.open()` JS API for custom footer links) reopens the preferences modal at any time, as the GDPR requires. Newly revoked categories stop loading from the next page view; Google tags react immediately via `consent update`. ## Configuration | Setting (per website) | Effect | |---|---| | Consent Pro banner | Master switch. Off = no banner, no scripts blocked, no logging. | | Audience mode | *Ask every visitor*, or *EU-only* (implied consent elsewhere — scripts load, no banner, badge still available). | | Google Consent Mode v2 | Emit default + update signals. Leave on if you use any Google tag. | | Reject button | Shows *Reject non-essential* on the first layer. Keep it on for GDPR compliance. | | Floating badge | The persistent 🍪 button that reopens preferences. | | Policy version / Retention | Re-prompt trigger and register retention in months. | | Banner title / message | First-layer texts, translatable per language. | Access: website designers manage scripts and read the register; deleting register entries is reserved to administrators. ## Install 1. Copy `bambooforge_consent_pro` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Cookie Consent Banner & GDPR Consent Mode v2**. 3. Open **Managed Scripts** - its menus are Consent Register, Consent Pro. ## Troubleshooting ****Two banners show.**** The core cookies bar was re-enabled. Switch *Cookies Bar* off in website settings — Consent Pro replaces it. ****My tracker fires before consent.**** It is not registered: move the tag into Managed Scripts or wrap it in the `text/plain` pattern. Tags pasted into Website ▸ Custom head code run outside any CMP's control. ****GA4 shows no data after installing.**** That is consent working: Google tags stay dormant until a visitor grants Analytics. Expect *consented* traffic only — that is the compliant baseline everywhere in the EU. ****Non-EU visitors still see the banner in EU-only mode.**** Your server has no GeoIP database, so geolocation is unknown and the safe default applies. Install MaxMind GeoLite2 (see Odoo docs, `--geoip-city-db`) to enable the distinction. ****I consented twice but the register shows one row.**** That is the deduplication: the same choice (same visitor, policy version, action and categories) refreshes its entry instead of adding a row. Change something real — withdraw, or untick a category — and the new choice appears as its own entry. ****The banner does not show for me.**** You already chose (badge instead), or you are in the website editor (the banner hides while editing), or your cookie predates a policy bump on another browser profile. ****Does it slow pages down?**** No measurable impact: the consent-default script is inline and static (cache-friendly), consented scripts are server-rendered, and the banner assets ship inside the normal website bundle. ## FAQ ****Is this enough for GDPR/ePrivacy compliance?**** It implements the technical requirements: prior blocking, granular opt-in, equal reject, withdrawal, proof of consent, re-prompt on change. Pair it with an accurate cookie policy page — legal texts remain your responsibility. ****Does it work with Odoo's GA4 integration?**** Yes — that is the flagship case. Consent Mode v2 governs it without any extra configuration. ****Multi-website?**** Yes: all settings, scripts (optionally shared), register entries and retention are per-website. ****Does any data leave my server?**** No. No CDN, no external CMP service, no fees per pageview. The register stores a salted hash, never the raw IP. ****Can visitors change their mind?**** Always — the floating badge or `window.bfConsent.open()` reopens preferences; revocation is logged like any other choice, as its own register entry. Only an **identical** re-submission is deduplicated onto the existing entry. ****Community edition?**** Yes, CE and EE alike. ## Upgrade 1. Back up the database. 2. Replace the module folder, restart, then: ``` odoo-bin -d -u bambooforge_consent_pro --stop-after-init ``` 3. Review the Changelog; bump the policy version if a new release adds categories you start using. ## Uninstall 1. **Apps ▸ BambooForge Consent Pro ▸ Uninstall.** 2. Removed: managed scripts, the consent register, banner templates and settings fields. Visitors' `bf_consent` cookies simply expire. 3. Re-enable the core *Cookies Bar* in website settings if you still want a basic banner afterwards. 4. Remember: without a CMP your trackers fire unconditionally again — check your compliance posture before removing. ## Changelog ### 20.0.1.2.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.1 (2026-08-15) - **The consent register keeps one entry per distinct choice.** Re-submitting the *same* choice (same visitor, policy version, action and granted categories) now refreshes the existing entry instead of adding a row; a genuine change of mind (different action or different categories — accept-all then withdraw, a category toggled) is still stored as its own entry, so the audit trail of what changed and when is preserved. - Rationale: `/bf_consent/save` is a public endpoint, and this keeps the register an accurate record instead of something a replayed request can grow without bound. Retention (13 months by default) is unchanged. ### 20.0.1.0.0 (2026-08-14) - Initial release: per-category banner + preferences modal, Google Consent Mode v2 (default in head + updates), managed scripts with prior blocking and server-side rendering after consent, `text/plain` unblock pattern, audit-ready consent register with salted visitor keys and automatic retention, EU/EEA/UK/CH geo mode, floating re-consent badge and JS API, presets for Meta Pixel, TikTok, LinkedIn, Hotjar and Clarity, policy-version re-prompt, multi-website support, browser-tested (frontend tour) with a full HTTP test suite. ## How we make sure it works Before every release, **19 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Repeat post does not inflate register. - Disabled website renders nothing. - Rejected visitor never gets script. - Core ga back when consent disabled. - Core ga is blocked until consent. The complete list is in the source you receive. --- # Financial Reports All in One with Consolidation > Consolidate several companies into one statement on Odoo Community: the group's P&L or balance sheet from the Financial Reports Builder, each member translated to the parent's currency, intercompany balances and trading eliminated by partner, manual adjustments as a second book, an intercompany matching check - and a company-by-company matrix you can drill and export. - **What it is:** Consolidate several companies into one statement on Odoo Community: the group's P&L or balance sheet from the Financial Reports Builder, each member translated to the parent's currency, intercompany balances and trading eliminated by partner, manual adjustments as a second book, an intercompany matching check - and a company-by-company matrix you can drill and export. - **For:** Accounting - **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:** 318 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_consolidation - **Not for:** The group's accounts are matched by code and type through the statement's lines; a member whose chart uses other codes needs a statement written for that.; One rate per currency per run. Transaction-date rates, hedging and a translation reserve are not modelled.; Eliminations rest on the partner being on the journal item. Full consolidation of a subsidiary's equity against the parent's investment is an adjustment you write, not one the app guesses.; Runs are not stored: consolidate again, or keep the Excel workbook. - **Manual:** https://docs.bambooforge.dev/bambooforge_consolidation/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_consolidation.action_bf_consol_group - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_consolidation - **Manual last updated:** 2026-10-15 --- # Financial Reports All in One with Consolidation A group of companies on Odoo Community has as many balance sheets as it has companies and no way to read them as one. This app reads them as one, on the statements you already built in the Financial Reports Builder. ## What you get - A **consolidation group**: the parent company, the statement to consolidate, the members with their ownership share and method. - Every member's figures computed on its **own chart of accounts** by the statement's line definitions - prefixes, types and formulas match by code, so the group's accounts need not be identical. - **Currency translation** to the parent's currency: closing rate on the last day for a balance sheet, the average rate over the period for a P&L. The rates used are printed on the run. - **Eliminations** read from the books: every journal item a member posted against another member's partner - the intercompany receivable and payable, the intercompany sale and purchase - is taken out of the total, line by line, and shown in its own column. - A **second book of adjustments** per line - goodwill, minority interest, an audit reclassification - dated, with a reason, applied to the runs whose period they fall in. - An **intercompany check**: what A carries as owed by B against what B carries as owed to A, for every pair, with the difference. - The run as a **matrix**: one column per member, eliminations, adjustments, group total - a list on the run, a pivot you can flip, an Excel workbook, and a drill-down into any member's journal items on any line. ## Install Needs **Financial Reports Builder** by BambooForge; the store price of this page includes it. A **Consolidation** menu appears under the Report Builder. The people who may read financial reports may read groups and run them; the report designers may build groups. ## Building a group 1. Build the statement once, in the parent company, with the Financial Reports Builder - a P&L on prefixes `70`, `60`, `61` and a *Result* formula, or a balance sheet on account types. 2. *Consolidation → New*: the parent company, the statement, and the members. Each member has an **ownership share** and a **method**: *Full* takes the member whole; *Proportional* takes the share of every line; *Excluded* keeps it on the list and out of the figures. 3. Leave **Eliminate intercompany** ticked unless you have a reason. 4. Press **Consolidate**, give the period, press **Consolidate** again. The parent itself is a member like any other: add it to the list. ## How a member's figure is computed For each member, the run reads that company's chart of accounts and asks every line of the statement which accounts it claims - a prefix line claims by code, a type line by account type - sums the balances of those accounts over the period (or up to the date, for a position), applies the line's sign, and resolves the formulas over the line codes. Branch companies post under their parent and are counted with it, the way the Financial Reports Builder counts them. The result is in the member's currency; the group's rate for that currency turns it into the parent's. ## Rates - A **position** (balance sheet) is translated at the closing rate: the rate in force on the last day of the run. - A **period** (P&L) is translated at the average of the rates in force over the period, one reading a day up to a month long and sparser beyond that. Rates come from *Accounting → Configuration → Currencies*, read for the parent company. The run prints every rate it used. Because a whole run uses one rate per currency, a balance sheet still balances after translation; no translation-reserve line is invented. ## Eliminations The members are, to each other, partners. Every journal item a member posted with another member's partner on it is intercompany: the receivable the parent holds on the subsidiary, the payable the subsidiary holds on the parent, the sale and the purchase between them. The run computes each member's statement a second time over those items only, and subtracts the result - so the elimination is exact for the lines your statement defines, and it is drawn in its own column. What is not eliminated: an intercompany transaction posted without the partner. The intercompany check is where those show up. ## Adjustments The adjustments tab of the group is the second book: a date, a statement line, an amount in the parent's currency with the line's own sign, and a reason. An adjustment counts in a P&L run whose period contains its date, and in a balance-sheet run dated on or after it. A formula line cannot be adjusted - adjust what it adds up and the formula follows. ## The intercompany check For every ordered pair of members the run shows what the first carries on receivable accounts against the second's partner, and what the second carries on payable accounts against the first's partner, both in the parent's currency at the closing rate, and the difference. A red row is an invoice one side has not booked, a rate difference, or a partner missing on a line. ## Limits, stated plainly - The group's accounts are matched by code and type through the statement's lines; a member whose chart uses other codes needs a statement written for that. - One rate per currency per run. Transaction-date rates, hedging and a translation reserve are not modelled. - Eliminations rest on the partner being on the journal item. Full consolidation of a subsidiary's equity against the parent's investment is an adjustment you write, not one the app guesses. - Runs are not stored: consolidate again, or keep the Excel workbook. ## How we make sure it works Before every release, **15 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Formula line cannot be adjusted. - Excluded member stays out. - Rate average for a period closing for a position. - Adjustment lands on its line and period. - Consolidated totals translated and eliminated. The complete list is in the source you receive. --- # Construction All in One with BOQ and Job Costing > Bill of quantities with rate build-ups, interim valuations that cannot over-measure, approved variations only, and material reconciliation. - **What it is:** Bill of quantities with rate build-ups, interim valuations that cannot over-measure, approved variations only, and material reconciliation. - **For:** Project - **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:** 278 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_construction_boq - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Turn certified work back into work to be paid for again when a revision is made.; Price an item on a revision below what has already been certified against it. - **Manual:** https://docs.bambooforge.dev/bambooforge_construction_boq/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_construction_boq.action_bf_boq - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_construction_boq - **Manual last updated:** 2026-10-15 --- # Construction All in One with BOQ and Job Costing An **extension** of BambooForge Construction Job Costing, not a second copy of it. The job, the phases, the cost budget, the change order register, the AIA payment application and the retainage already live there. This adds the side of the trade that works by measurement. ## The bill - Sections and items, each with a code, a **unit**, a quantity and a rate. A rate without a unit is a number nobody can check. - Revisions **supersede** each other rather than overwrite, so the tender bill is still readable after six variations. A new revision copies the sections and items and re-points each item at the new revision's section. Each copied item **remembers the item it came from**, so what was certified on revision 1 is still certified on revision 2: a revision does not turn measured work back into work to be paid for again. For the same reason a revision cannot price an item below what has already been certified against it, and neither a revision nor an agreement happens while a valuation is open. - Once a bill is **agreed it stops being editable**. The quantity, the rate and the rate build-up are what was signed; the cost phase still moves, because that is bookkeeping rather than contract. Changing the work is a variation, or a new revision. - The bill's total is checked against the **job's contract value**, and any gap is a field on the form. Working to two different contract sums is how a claim is lost. - A bill with an unpriced item cannot be agreed. A bill with holes in it cannot be valued. ## Rate build-ups An item's rate assembled from **labour, material, plant, subcontract and overhead**, each with a quantity per unit and a wastage percentage. The difference between the rate charged and the rate built up is a field, not a spreadsheet somebody keeps at home. The priced bill can be **pushed into the job's cost budget**, split by cost type from the build-up. Pricing a bill and then typing the budget again is how a contractor ends up with two numbers and no idea which is real. It goes **once** - the date is on the bill and a second push is refused - and approved variation items stay out of it, because their cost already moved with the change order their approval raised. ## Interim valuations A valuation is **cumulative**. The sheet carries the quantity done *to date*; this month's money is that minus what was already certified, per item. - A quantity **cannot exceed the bill**. Extra work belongs in a variation, where somebody has to approve it. Over-measuring is the single most common overpayment on a construction contract. - A quantity **cannot go backwards**: a valuation does not un-measure work. That is an omission variation. - **Two open valuations at once are refused** on the whole job, not just on one revision of the bill - that is how the same wall gets measured twice. The number runs per job, and a cancelled valuation keeps the number it was given. - Only the **agreed** revision is certified against, and every line is checked again at certification, not only as it is typed. - A **certified sheet is a signed document**. After certification the quantities are frozen and only the note can be edited; lines cannot be added or removed, the sheet cannot be deleted or cancelled, and the difference is measured next time. The sheet keeps its own copy of the bill quantity and the rate, so it still reads as it was signed after the bill moves on. - A valuation that measures less than what was already certified is refused: that needs a credit, not a certificate. ## The money leaves through the job A certified valuation raises the job's own `bf.job.billing` - with its retainage and its AIA sheet, grouped by cost phase - and marks itself billed. There is **no second invoicing path** and no second set of numbers to reconcile. An item with no cost phase stops the application, because its money would have nowhere to go. An application works out what was billed before from the job's **invoiced** applications, so a second one is refused while the first is still a draft: raising both would bill the same work twice. ## Variations Priced items - added, omitted or re-measured - that **count for nothing until they are approved**. A pending item is not in the contract sum, not on the valuation sheet and not in the budget; a rejected one is marked omitted, so it stays readable and is worth nothing. - Approval writes the items into the bill and raises the job's own **change order**, so the contract value moves in exactly one place. What was approved - or refused - is then **frozen**: omitting the work later does not rewrite what somebody signed. - A client instruction must carry the **client's own reference**, or nobody can match the payment to it later. - A variation prices and omits items **on its own bill**, never somebody else's. - An **omission cannot take out work already certified** - or already measured on the valuation being written - and paid for. Omitting the whole quantity marks the item omitted; it stays on the bill so the history reads, but it is worth nothing. - An approved variation cannot be rejected. Raise an omission variation instead. - *Our error* is on the list of origins on purpose. A variation register that only records the client's changes is a register nobody believes. ## Provisional sums Marked, kept out of measurement - an allowance is not work anybody did - and **adjusted at what they really cost** before the final account. ## Material reconciliation What the rate build-ups say should have been used for the quantity **measured**, beside what was actually bought against the job. The difference is wastage, theft, or a rate that was priced wrong - all three worth knowing before the job finishes. Materials are matched by name - ignoring case and stray spaces, so *Cement* and `cement` are one line - because a rate build-up is written by a quantity surveyor in words; the report says so rather than pretending otherwise. The reconciliation is a company's own, like everything else here. ## Roles A **quantity surveyor** measures, prices variations and raises valuations. The **commercial manager** agrees the bill, approves variations and turns a certified valuation into money. Those three gates are checked on the server, not only hidden in the form, and a state cannot be written by hand around the buttons. The **rate build-up and the margin on an item are the surveyor's**: a site user reads quantities and rates, not what the work costs us. ## Community only Needs BambooForge Job Costing, which needs `account`, `purchase`, `project` and `hr_timesheet`. Tested on 16.0, 17.0, 18.0, 19.0 and 20.0. ## How we make sure it works Before every release, **73 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A bill with an unpriced item cannot be agreed. - An item with no phase cannot reach the application. - A new revision cannot undercut certified work. - A surveyor cannot delete a valuation. - A valuation cannot un measure work. 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: - Turn certified work back into work to be paid for again when a revision is made. - Price an item on a revision below what has already been certified against it. ## Install 1. Copy `bambooforge_construction_boq` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Construction BOQ & Measurement**. 3. Open **Bills of quantities** - its menus are Valuations, Variations, Material requirement. ## 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 Units of Measure; job costing (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Bills of quantities** menu (Valuations, Variations, Material requirement). ****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. --- # SEO All in One with Cookie Scanner and Consent > Scan your own pages, list the third parties they really load and the cookies you really set, and see which ones your banner never declared. - **What it is:** Scan your own pages, list the third parties they really load and the cookies you really set, and see which ones your banner never declared. - **For:** Website - **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:** 287 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_cookie_scanner - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Fetch any host but your own website: a scanner that can be pointed elsewhere would be a proxy.; Declare a tracker for you: findings are a work list for Consent Pro, a person decides.; Run for a read-only SEO user - starting a scan needs the SEO manager role. - **Manual:** https://docs.bambooforge.dev/bambooforge_cookie_scanner/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_cookie_scanner.action_bf_cookie_scan - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_cookie_scanner - **Manual last updated:** 2026-10-15 --- # SEO All in One with Cookie Scanner and Consent An add-on for **BambooForge Consent Pro**. A cookie banner lists what somebody typed into it eighteen months ago; a website loads what its pages actually contain. This shows you the gap. ## Install 1. Install **BambooForge Cookie Scanner** (it needs SEO Pro). Sixteen known third parties are created and a monthly scan is scheduled for every website; nothing is fetched until the first scan runs. 2. Open *SEO Pro → Trackers Found*, and under Configuration, *Cookie Scans* and *Known Third Parties*. ## Running a scan *Configuration → Cookie Scans → New → Scan the site*. The scan fetches this website's own pages - the ones SEO Pro has indexed, at most **sixty**, one at a time, each with a twelve second timeout and a two megabyte cap. It never fetches another host: a scanner that can be pointed elsewhere is a proxy. For each page it records: - **third parties**, matched against the knowledge base; - **cookies your server set**, read from the response headers. The log lists the pages that failed and why, capped at fifty lines so it stays readable. ## What it will not claim It does **not run JavaScript**. That matters, and it is stated on every screen: it reports which vendor is present and which cookies that vendor is *known* to set - not that it observed them. Cookies from the response headers are labelled separately, because those really were observed. That is the version of the report you can defend. A tool that shows a cookie list it inferred is a tool that will be wrong in a meeting. ## The work list *SEO Pro → Trackers Found* opens on **Not declared**: findings that Consent Pro has never been told about. A finding is treated as declared when a Consent Pro script has the same name, or contains one of the vendor's fingerprints in its URL, code or preset. **Declare in Consent Pro** creates the entry with the vendor's category, so the banner and the register agree with the site. **Ignore** is for the ones you have decided about - a first-party analytics script, say. ## The knowledge base *Configuration → Known Third Parties*. A vendor is: - a **name**, as it should appear in the banner; - a **category** - essential, functional, analytics or marketing; - **patterns**: comma-separated fragments to look for in the page source, usually a host or a script path; - the **cookies it is known to set**, used to fill in a declaration; - optionally a privacy policy URL and a note. They are ordinary records. Add the one your agency uses; the next scan knows it. Two notes worth reading: - **Google Fonts** sets no cookie, and is listed anyway: the visitor's IP address reaches Google, which a German court has had an opinion about. - **Stripe** is filed as essential because that is what it is on a checkout. If it is on every page of your site, that is the finding. ## The monthly scan Every website with *Scan for undeclared trackers monthly* on is scanned at 02:20, so a script somebody added in a hurry shows up in the report before it shows up in an audit. A website whose scan fails does not stop the others. ## How we make sure it works Before every release, **22 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A read only seo user cannot start a scan. - Declaring twice does not duplicate the entry. - It never fetches another host. - A page that answers 404 is not scanned for trackers. - A page that errors does not stop the scan. 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: - Fetch any host but your own website: a scanner that can be pointed elsewhere would be a proxy. - Declare a tracker for you: findings are a work list for Consent Pro, a person decides. - Run for a read-only SEO user - starting a scan needs the SEO manager role. ## 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 Web; consent pro, seo pro (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Cookie Scans** menu (Known Third Parties, Trackers Found). ****Does anything leave my server?**** It makes calls outside your server only where its manual says so - read the section on that service before you connect it. ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - the people who wrote it. --- # Product Configurator (CPQ) with Price Rules > Product configurator and CPQ: ask the questions, enforce the rules on the server, price by real size in the real unit, freeze what was quoted. - **What it is:** Product configurator and CPQ: ask the questions, enforce the rules on the server, price by real size in the real unit, freeze what was quoted. - **For:** Sales - **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:** 149 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_cpq - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Drop an implication silently: a rule that cannot land is reported, on the server, on every path an order can arrive by.; Price outside the rules you wrote - there is no hidden pricing engine.; Reprice the customer's line when you edit its components. The cost follows the build; the agreed price does not.; Show the customer what the line is made of as priced lines. One line, one price, and the answers as text underneath.; Let anyone change the build after the order is confirmed. - **Manual:** https://docs.bambooforge.dev/bambooforge_cpq/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_cpq.action_bf_cpq_configuration - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_cpq - **Manual last updated:** 2026-10-15 --- # Product Configurator (CPQ) with Price Rules For anybody who sells something that is made to order — windows, fencing, signage, cable assemblies, furniture, flight cases. Odoo Community has product variants, which answer "which of these twelve do you want". It does not have a configurator, which answers "how big, in what, with which handle, and what does that come to". ## The questions A configurable product is a list of questions asked in the order you ask them on the phone. Three kinds: - **Pick one** — the handle, the finish, the glazing. - **Pick any number** — the extras. - **A number** — how many trickle vents, how many metres of trim. Every answer can add a flat amount, add an amount **per unit of size** (for a coating charged by the square metre), and carry a **component**. A question can be marked required: a required question left blank stops the configuration being priced, which is better than a quotation that silently forgot the glazing. A question with no answers on it yet is not an error — somebody is still building the product. Trying to *quote* one is, and that is where it is caught. **Duplicate works.** Copying a configurable product copies its questions, its answers, its size bands — and rebuilds the rules so they point at the *copied* answers, not the originals. "Tilt-and-turn, same as the casement" is one button, and the copy quotes the same money the original would. ## The rules, on the server Three kinds of rule between answers: - **cannot be combined with** — the brass handle does not clear a triple-glazed sash. - **needs** — triple glazing needs the reinforced frame. - **automatically adds** — triple glazing brings the long screws with it. All three are checked **in the model**, not in the browser. A rule that only lives in the screen stops existing the first time an order arrives by import, by RPC, or by duplicating last year's quotation. Two promises about "automatically adds": - **It never displaces an answer a human chose.** If a rule wants the hand-oiled finish and the salesperson picked the cheap one on purpose, nothing is swapped silently — the clash is reported in plain words and the configuration will not price until a person settles it. - **It never drops an implication silently.** The pass runs to the end of the chain, bounded by the number of answers, and anything that could not land is reported as a problem. A window built without its reinforcement because a chain was eleven rules deep is not a configurator, it is a liability. Say *why* in the message, not *what*. "The brass handle does not clear a triple-glazed sash" is a sentence a salesperson can read out loud. ## Size, in the unit the bands are written in Turn on length, width×height or width×height×depth, and state **two** units: the one the shop floor types (*Sizes Typed In* — millimetres for joinery) and the one the bands are priced in (*Sizes Priced In* — square metres). A joiner types **1200 × 900**; the bands are read on **1.08**. The two units are stated separately on purpose. "Convert to the reference unit of the family" sounds cleaner and is wrong on a moving target: Odoo 16–20 make the metre the reference for length, Odoo 20 rebuilt units as a chain whose root is the **millimetre** — the same code would divide by a thousand on one series and not on the other, and nothing would look wrong until a quotation went out. Two units that cannot be converted into each other are refused at configuration time, on every series — including 19 and 20, where the standard conversion no longer raises and would happily price kilograms per metre. Bands are read in order and the first one that contains the size wins, **compared at six decimal places** — 4100 mm × 100 mm is 0.41 square metres and lands in the "up to 0.41" band, not three billionths past it at five times the price. Each band has a rate, a flat amount and a minimum charge, because a window 200 mm wide still costs a man an afternoon. Two bands ending at the same size are refused outright. **A size above the largest band is refused, not guessed.** The everyday misconfiguration is somebody widening what the shop makes and forgetting the band that covers it; the configurator says so and asks for the job to be quoted by hand. ## The price, and when it stops moving - The **preview in the configurator is the price that lands on the quotation** — implications included, in the order's currency. The salesperson reads it out loud on the phone; those two numbers being different is the fastest way this class of software gets switched off. - The **base product goes through the pricelist**, at the line's unit of measure and the order's date — so a dozen is priced as a dozen, the customer's agreed discount applies, and a quotation dated next month prices off next month's pricelist. - **Options and size are cost build-up on top** and are not discounted again. - **Taxes are never written by this module** — and a product whose tax is *included in the price* is handled the way core handles it: the net figure the engineer typed is grossed up onto the line, so a 20% included tax does not quietly eat a sixth of every window. - The price is **frozen the moment it is quoted**, with the date it was worked out. Retuning a band next month does not rewrite what the customer was sent. Typing the quantity afterwards — which is the order the work actually happens in — does not either, and neither does the *Update Prices* button. - The freeze covers the **answers as well as the number**: once quoted, the answers cannot be rewritten, added to or deleted, by anybody, through any door — form, import or RPC. What the works order is built to is what the customer was charged for. - A configuration on a **confirmed order cannot be edited at all**. Cancelling the order makes it readable again as a quotation. - **Duplicating a quotation duplicates the configuration.** - A question or an answer that has been sold cannot be deleted, only archived. A deleted answer makes an old quotation lie about what was ordered. Housekeeping is honest too: an abandoned wizard (opened, closed, never answered) is cleaned up when the order confirms rather than frozen at a price of zero for ever, and deleting a quotation line archives its quoted configuration — kept for the audit trail, no longer a reason the product can never be retired. ## On the quotation A configurable product on a quotation line shows a small slider icon for anyone in the Sales app — the group is implied by Salesperson, and a user without it can still open every quotation (the injected fields are group-scoped and the flag is computed in one query per order, not one per line). Press it, answer the questions, and the line gets the configured price and a description that reads like something a customer would recognise: ``` Casement window, made to size Size: 1200.0 x 1500.0 Frame material: Accoya Glazing: Triple glazed, 4-12-4-12-4 Ironmongery: Brushed stainless ``` Changing the product on a configured line is refused — the description and the price would be describing something else. Delete the line and start again. ## What the line is made of, and what it costs you Every answer can carry a component and a quantity. The first time a line is priced, those components are written onto the configuration's **Made of** tab as an ordinary editable list - and from there they belong to that quotation line. Add a component, remove one, swap one, change a quantity: this job takes two long brackets, not four; this customer supplies their own glass. Nothing you do there is written back to the template, and no other quotation line changes. The next quotation starts from the standard build again, and **Rebuild from answers** puts this one back to it. Under the list is **Component Cost**: the components' own cost prices, added up, read as the configuration's company - so a two-company database quotes with its own costs and not the other company's. It follows the list, immediately: swap a brass handle for steel and the number moves. The same number reaches the quotation line as **Cost**, in the order's currency, next to the price. If Odoo's own *Margin* feature is installed, its Cost and Margin columns are filled from here too, so the margin a salesperson reads is the margin of what is actually on the line. Two things deliberately do not happen. The customer's price does not move when you change a component - a quotation says what it said, and the build is your business, not theirs. And the customer never sees the components as priced lines: the line stays one line, with one price, and the chosen answers printed underneath as plain text. Once the order is confirmed the list is closed: changing what a customer agreed to, after they agreed to it, is not an edit but a different sale. With Manufacturing installed and *Build a Bill of Materials* ticked on the product, a manager can create the BoM from the list as it stands. It is created **once** per configuration, **inactive**, and named after the configuration — because sixty active BoMs a week on the same product variant means `_bom_find` picks one at random and the shop floor cuts brass handles for a steel-handle order. Somebody activates the one they mean, for the job they mean. ## Companies Templates, questions, answers, rules, bands and configurations all carry the company and a record rule. The products an answer carries are checked against the template's company — the fencing contractor's engineer cannot wire the joinery's hinge into their product by picking it from a list that forgot to filter. ## What it needs `sale_management`, `sales_team`, `product` and `uom`. All Community. No Enterprise module is required, and none is used; Manufacturing is optional and only ever additive. ## How we make sure it works Before every release, **66 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A size nobody priced is refused not guessed. - Two units that cannot be converted are refused. - A generated bill of materials cannot be picked by accident. - An abandoned draft is not frozen by confirming the order. - An answer cannot be added to an ordered configuration. 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: - Drop an implication silently: a rule that cannot land is reported, on the server, on every path an order can arrive by. - Price outside the rules you wrote - there is no hidden pricing engine. - Reprice the customer's line when you edit its components. The cost follows the build; the agreed price does not. - Show the customer what the line is made of as priced lines. One line, one price, and the answers as text underneath. - Let anyone change the build after the order is confirmed. ## Install 1. Copy `bambooforge_cpq` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Product Configurator (CPQ) with Price Rules**. 3. Open **Configurations** - its menus are Configurable Products, Configurator. ## 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, Products, Sales, Settings, Units of Measure, sales_team apps. No extra Python package. ****Where does it show up?**** Under the **Configurations** menu (Configurable Products, Configurator). ****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. --- # Credit Limit & Credit Hold for Sales Orders > Put customers on credit hold automatically from their overdue receivables and release them when they pay down - on a daily schedule. - **What it is:** Put customers on credit hold automatically from their overdue receivables and release them when they pay down - on a daily schedule. - **For:** Sales - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_credit_limit - **Not for:** Odoo warns you when a customer is over their credit limit — then lets the order through anyway. This app turns that warning into real control: it blocks confirmation of orders that would push a customer over the limit, lets you put a customer on hold, and gives you a live credit-exposure overview.; This page is the complete manual. Follow it top to bottom and you can install, set limits and run the workflow without contacting support. - **Manual:** https://docs.bambooforge.dev/bambooforge_credit_limit/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_credit_limit.credit_overview_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_credit_limit - **Manual last updated:** 2026-10-15 --- # Credit Limit & Credit Hold for Sales Orders Odoo warns you when a customer is over their credit limit — then lets the order through anyway. This app turns that warning into real control: it **blocks confirmation** of orders that would push a customer over the limit, lets you put a customer **on hold**, and gives you a live **credit-exposure overview**. This page is the complete manual. Follow it top to bottom and you can install, set limits and run the workflow **without contacting support**. ## Overview - **Hard block** — a quotation that would bring a customer's outstanding balance over their credit limit cannot be confirmed until a **Credit Manager** overrides. - **Credit hold** — flag a customer to block all of their orders, regardless of the limit. - **Exposure overview** — one screen listing every customer with a limit, their outstanding AR, available credit and exposure, with over-limit rows in red. It builds on Odoo's standard credit fields (the customer's *credit limit* and *outstanding balance*), so nothing is duplicated. ## Requirements - Odoo 20.0 with **Sales** (`sale_management`). Installing this app pulls it and Invoicing in automatically. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_credit_limit` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **Credit Limit** and click *Install*. ## Setting it up 1. Turn on **Settings > Invoicing > Customer Invoices > Credit Limit** (Odoo's standard credit-limit feature) and, just below it, enable **Block Over-Limit Orders** (added by this app). 2. On each customer, set their **Credit Limit** (Accounting tab). 3. Assign the **Credit Manager** role (Settings > Users) to whoever may override blocks. ## The exposure overview Open **Sales > Reporting > Credit Overview** for a live table of every customer with a credit limit: their limit, outstanding balance, available credit and exposure percentage. Rows over the limit show in red, near-limit in orange, and on-hold customers are muted. Toggle **Credit Hold** straight from the list. ## How the block works When a quotation's *projected exposure* — the customer's current outstanding balance plus this order's total — exceeds their credit limit, the order is marked **Credit Blocked** and confirmation is refused with a clear reason. A customer on **Credit Hold** is blocked regardless of amounts. A **Credit Manager** opens the order and clicks **Override Credit Block**; the order is then cleared to confirm, and the override is logged on the chatter. ## Frequently asked questions ****It only warns, it doesn't block.**** Make sure **Block Over-Limit Orders** is enabled in Settings. Odoo's own credit-limit option only shows a warning; this app adds the block. ****Who can override?**** Only users with the **Credit Manager** role. Others see the block but cannot clear it. ****What counts towards the limit?**** The customer's outstanding receivable balance plus the order being confirmed. ****Can I block a specific customer entirely?**** Yes — tick **Credit Hold** on the customer (or from the overview) to block all their orders. ****Does it work with multiple companies?**** Yes. The block setting is per company, and exposure is the customer's balance in that company. ## How we make sure it works Before every release, **30 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Non manager cannot set override via create. - Non manager cannot write override directly. - Salesperson cannot write hold directly. - Non manager cannot override. - Rep can create normal order no regression. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Only warn: the hard block stops confirmation over the limit until a Credit Manager overrides - unless you leave blocking off in Settings. - Let anyone override: only the Credit Manager role can. ## 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 Sales app. No extra Python package. ****Where does it show up?**** Under the **Credit Overview** menu (Credit Holds). ****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. ## Troubleshooting This section lists the problems administrators run into most often, with the cause, how to confirm it, and how to fix it. Every item refers only to fields and roles this app actually adds. ### An order is blocked and nobody expected it *Cause.* A quotation is blocked in two situations: the customer is on **Credit Hold**, or the *projected exposure* (their current outstanding balance plus this order's total) exceeds their **Credit Limit** while **Block Over-Limit Orders** is on. *Diagnosis.* Open the order and read the **Credit Blocked** warning; its reason line tells you which case applies. If it mentions "credit hold", check the customer form — the hold may have been applied automatically by the nightly cron because their overdue receivables crossed the configured threshold or days past due. If it mentions the limit, compare the customer's outstanding balance plus the order total against their limit on the **Credit Overview**. *Resolution.* Have the customer pay down their balance, raise their limit, or ask a **Credit Manager** to click **Override Credit Block** on the order. For a hold, a Credit Manager releases the customer. ### A user who should be able to override cannot *Cause.* The **Override Credit Block** action and the customer hold/release buttons are restricted to the **Credit Manager** group. A salesperson also cannot clear the block with a direct write — `credit_override` on the order and the hold fields on the customer are guarded and raise an access error. *Diagnosis.* Under **Settings > Users**, open the user and check the **Credit Control** section for the **Credit Manager** role. If clicking the override raises "Only a Credit Manager can override a credit block", the role is missing. *Resolution.* Grant the **Credit Manager** role. Note that a member of Odoo's **Accounting / Full Rights** group may additionally maintain a customer's hold directly, but only Credit Managers can override a blocked order. ### The limit is not enforced — it only warns *Cause.* Blocking is opt-in per company. If **Block Over-Limit Orders** is off, you get Odoo's standard warning only. A block also never fires when the customer has no **Credit Limit** set, or when the limit is 0. *Diagnosis.* Check **Settings > Invoicing** for both Odoo's **Credit Limit** feature and this app's **Block Over-Limit Orders** toggle. Then open the customer's Accounting tab and confirm a non-zero **Credit Limit**. Remember the block only evaluates orders still in *Quotation* or *Quotation Sent* — a confirmed order is never retroactively blocked. *Resolution.* Enable **Block Over-Limit Orders** and set a credit limit on the customer. To also stop invoicing held customers, enable **Block Invoicing for Held Customers**. ### Overdue / aging figures look wrong *Cause.* **Overdue Receivables** counts only *posted*, unreconciled receivable journal items whose maturity date is already past, in the company currency. Draft invoices, future-dated instalments, and already-reconciled entries are excluded by design. *Diagnosis.* If a customer looks overdue but the figure is 0, confirm their invoices are **posted** (not draft) and past their **due date**. If a paid invoice still counts, check that the payment is reconciled against it. *Resolution.* Post the invoices, set correct payment terms/due dates, and reconcile payments. The value recomputes against today's date, so it updates on its own once the underlying entries are correct. ### A customer keeps getting auto-held (or won't auto-release) *Cause.* The nightly **Credit: Apply Overdue Holds** cron holds a customer when their overdue amount exceeds the threshold or their oldest invoice is more than the configured days past due, and it auto-releases only holds it applied itself. A manager's **Forced Hold** or **Forced Release** decision always wins and is never overruled by the cron. *Diagnosis.* On the customer, check **Manual Credit Decision**: *Forced Hold* keeps them held, *Forced Release* keeps them off. Confirm the company (or per-customer) **Auto-Hold Overdue Amount** and **Auto-Hold Days Overdue** thresholds — both 0 disables the feature entirely. *Resolution.* Use **Reset to Automatic** to hand control back to the cron, or adjust the thresholds. A manually held customer is only released by a Credit Manager, never by the cron. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_credit_limit -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The fields, views and analysis definitions this module added. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - The security groups this module defined (user assignments to them are dropped). ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.2.4 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.2.4 - A salesperson without invoicing rights can confirm orders again: the credit block reads the customer's receivables and limit with the rights it needs, instead of the salesperson's, which raised an access error on *credit*. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Automatically put customers on credit hold based on their overdue receivables. - And release them again once they pay down. - On a daily schedule, no manual watching required. Also turns Odoo's credit-limit warning into real control: block confirmation of sales orders that would push a customer over their credit limit, put risky customers on hold by hand, and watch your whole AR exposure on one dashboard. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # Customer Portal: Statement of Account & Payments > A statement of account in the customer portal: every open item, how old it is, the total owed, as a PDF - and a way to report what they paid. - **What it is:** A statement of account in the customer portal: every open item, how old it is, the total owed, as a PDF - and a way to report what they paid. - **For:** Accounting - **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:** 119 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_customer_portal - **Not for:** The statement is scoped to the logged-in customer's own commercial partner and the contacts under it, in a company they already have posted documents in. Never a partner or a company named in the request — a page that reports on whoever the URL says is a way to read somebody else's ledger one identifier at a time.; An invoice ticked on the form is checked against that customer's own open items, in the controller and again as a database constraint. A zero or negative amount is refused. A payment dated in the future is refused, because it has not happened. Uploads are PDF, PNG or JPEG, up to 10 MB.; Invoices belonging to a branch of the same customer do appear, and can be ticked — a statement that shows only one contact's invoices never matches what the customer's own accounts department is looking at. - **Manual:** https://docs.bambooforge.dev/bambooforge_customer_portal/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_customer_portal.action_bf_remittance - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_customer_portal - **Manual last updated:** 2026-10-15 --- # Customer Portal: Statement of Account & Payments Odoo Community's portal lists a customer's invoices and lets them pay one. It never tells them what they owe **altogether**, never shows how old any of it is, and gives them nowhere to say *we paid this on Tuesday, here is the reference*. So the question that starts every collection call — what is outstanding, and what have you already sent us — gets answered by exporting a spreadsheet and attaching it to an email. ## The statement Every posted, unpaid invoice and credit note, oldest first: the document, the reference the customer's own accounts department filed it under, the dates, the original amount and what is left on it. Then a total, in one figure, at the top of the page. A credit note reduces what is owed rather than adding to it — which sounds obvious until you have seen a statement where it did the opposite. ## How old it is Five bands: not yet due, 1–30, 31–60, 61–90, and more than 90 days. Measured from the **due date**, not the invoice date. A document on sixty-day terms is not an overdue account in its second week, and an ageing report that says it is will be argued with rather than paid. That one decision is the difference between a statement a customer acts on and a statement a customer replies to. The bands always add up to the total. There is a test that fails if they ever stop doing so. ## As a PDF One button on the portal page. The customer gets the same statement as a document they can file or forward, without anybody in your office producing it. ## Telling you what they paid The other half. The customer ticks which invoices a payment covers, says how much and when, gives the reference their bank will show, and attaches the remittance document. It arrives in your accounts department **beside** the ledger, never in it. Matching one drafts an `account.payment` for somebody to check against the bank before posting. The figures came from outside; a portal that posts receipts unread is a portal that writes into your ledger. The list shows, per advice, what the customer *announced* against what the invoices they ticked actually come to, and says so when the two differ. That gap is nearly always a part payment, a deduction, or an invoice they have forgotten — and all three are worth seeing before the payment is drafted, not after. Nobody can create a remittance advice by hand, in any group. What a customer told you is evidence; somebody who could type one could also type the payment they wish had arrived, and afterwards nothing would tell the two apart. ## What the page will not do The statement is scoped to the logged-in customer's own commercial partner and the contacts under it, in a company they already have posted documents in. Never a partner or a company named in the request — a page that reports on whoever the URL says is a way to read somebody else's ledger one identifier at a time. An invoice ticked on the form is checked against that customer's own open items, in the controller and again as a database constraint. A zero or negative amount is refused. A payment dated in the future is refused, because it has not happened. Uploads are PDF, PNG or JPEG, up to 10 MB. Invoices belonging to a **branch** of the same customer do appear, and can be ticked — a statement that shows only one contact's invoices never matches what the customer's own accounts department is looking at. ## Series Community 16.0, 17.0, 18.0, 19.0 and 20.0. ## How we make sure it works Before every release, **27 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - An advice cannot be matched twice. - A customer never sees another customers invoice. - A rejected advice cannot then be matched. - Another customers invoice cannot be attached. - Another customer never appears. 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: - Post the payment a customer reports: it arrives beside the ledger for your accountant to reconcile, never in it. - Call an overdue account overdue before its terms have run out. - Show a customer anything the portal does not already let them see. ## Install 1. Copy `bambooforge_customer_portal` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Customer Portal: Statement of Account & Payments**. 3. Open **Remittance Advices**. ## 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 Invoicing, Portal apps. No extra Python package. ****Where does it show up?**** Under the **Remittance Advices** menu. ****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. --- # Customer Statement of Account & Aging Report > Statement of account with a running balance and aging (current / 30 / 60 / 90+), printed, previewed or emailed - one at a time or scheduled. - **What it is:** Statement of account with a running balance and aging (current / 30 / 60 / 90+), printed, previewed or emailed - one at a time or scheduled. - **For:** Accounting - **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:** 79 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_customer_statement - **Manual:** https://docs.bambooforge.dev/bambooforge_customer_statement/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_customer_statement.customer_statement_wizard_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_customer_statement - **Manual last updated:** 2026-10-15 --- # Customer Statement of Account & Aging Report Send your customers a clean **statement of account**: open items or the full ledger over a period, with a running balance and an **aging breakdown** (current / 1-30 / 31-60 / 61-90 / 90+). Print it, preview it or email it — one customer or hundreds at once, straight from Odoo. This page is the complete manual. Follow it top to bottom and you can install, print and email statements, and fix the common issues **without contacting support**. ## Overview Chasing overdue receivables starts with showing the customer what they owe. This app builds that statement from your posted invoices and payments: - **Open items or all transactions** — show only what is still outstanding, or the full ledger over a date range. - **Running balance** — every line carries the cumulative balance. - **Aging summary** — outstanding amounts split into current, 1-30, 31-60, 61-90 and 90+ day buckets. - **Print, preview or email** — a professional PDF on your company letterhead, for one customer or a whole batch. - **One-click from the customer** — a Customer Statement button on every customer. ## Requirements - Odoo 20.0. - The **Invoicing/Accounting** app (`account`). Installing this app pulls it in automatically. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_customer_statement` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **Customer Statements** and click *Install*. A **Customer Statements** entry appears under *Accounting / Invoicing > Customers*, a **Customer Statement** button is added to every customer, and a **Customer Statement** option appears in the Print menu of the customer list. ## Quick start 1. Open a customer and click **Customer Statement** (or open *Customers > Customer Statements*). 2. Choose the **Statement Date**, what to **Show** (open items or all transactions) and, for a full ledger, an optional **From** date. 3. Click **Print** to get the PDF, or **Send by Email** to deliver it to the selected customers with the statement attached. ## Open items vs all transactions - **Open items** lists only unreconciled receivable entries — what the customer still owes. This is the usual collection statement. - **All transactions** lists every receivable entry in the period (invoices, credit notes and payments), giving a full ledger with the running balance. The **aging summary** is always based on the open (unreconciled) amounts, bucketed by each entry's due date against the statement date. ## Emailing statements **Send by Email** renders each selected customer's statement as a PDF and emails it using the built-in *Customer Statement* mail template, with the PDF attached. Customers without an email address are skipped. You can edit the email wording under *Settings > Technical > Email > Templates*. ## Dunning: the ladder, the signer, the hold *Configuration -> Dunning Levels* is the ladder the scheduled statements climb: each level names the age of the oldest overdue invoice it applies from, the template it sends, and now: - **Signed by** - the notice goes out as this user, whose name and address fill the template's signature and sender; the credit controller signs the second notice, not the robot; - **Also warn the salesperson** - a to-do on the customer's salesperson; - **Put the customer on credit hold** - with *Credit Limit & Credit Hold* installed, reaching the level blocks new sales orders until the overdue balance is settled, and releases them the day it is. A hold or a release a credit manager set by hand is never touched. The level only ever moves up inside a collection cycle; paying off the overdue balance resets it. **Send Dunning Now** (*Receivables*, or the button on a schedule) lists every customer with an overdue balance, the level they get today, the amount and the address, ticked when there is an address and a level reached. A customer still below the first level shows greyed and unticked; tick them to send the plain statement. Press Send and the notices go out at once, whatever the schedule's calendar says - the chase a controller does on a Friday afternoon. Sending the same level again is a chase, not an escalation. Every notice sent - by the schedule or by hand - is a **line on the customer's chatter**: the level, the address, who signed it. The customer form shows how many notices went out in the current cycle and when the last one did; both reset with the cycle. "Did we chase them?" has an answer. ## Batch statements Select several customers in the Customers list, then use **Print > Customer Statement** (or the wizard's multi-customer field) to produce or email all their statements in one go — each customer on its own page. ## Frequently asked questions ****The statement shows no logo / wrong address.**** The statement uses your company's document layout. Set your logo and details under *Settings > Companies* (or *Settings > General Settings > Document Layout*). ****A paid invoice still shows.**** In *All transactions* every entry shows, including settled ones. Switch to *Open items* to see only what is still outstanding. ****Aging looks off.**** Aging buckets use each entry's due date versus the statement date. Make sure your invoices carry the right due dates (from their payment terms). ****Can I send statements automatically?**** The wizard is manual by design (you choose the date and scope), but the email action can be triggered from a server action or scheduled job if you need automation. ****Does it work with multiple companies?**** Yes. Each statement is computed for the selected company's receivable entries. ## How we make sure it works Before every release, **37 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Below the first level is listed but not ticked. - Sending again is a chase not an escalation. - Second notice puts a human on it. - Archived customer not targeted. - Nobody due nobody mailed. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## Usage The app works in two ways: **on-demand** statements you print or email from the wizard, and **scheduled** statements the module emails automatically. Both build the same PDF from your posted receivable entries. ### On-demand statement for one customer 1. Open a customer (*Accounting / Invoicing > Customers > Customers*) and click the **Customer Statement** stat button. The button appears only on records that are actually customers (`customer_rank > 0`). 2. The **Customer Statement** wizard opens with that customer already filled in the **Customers** field. 3. Set the options: - **Statement Date** — the cut-off date (`date_to`); defaults to today. - **Show** — *Open Items* (only unreconciled receivables still owed) or *All Transactions* (the full ledger with a running balance). - **From** — a start date (`date_from`); shown only when *Show* is *All Transactions*. Leave it empty for the complete history. 4. Click **Print** for the PDF, or **Send by Email** to deliver it. The aging summary (current / 1-30 / 31-60 / 61-90 / 90+) is always computed from the open amounts against the Statement Date. ![](screenshot_wizard.png) ![](screenshot_partner_button.png) ### On-demand statements for many customers - Open *Accounting / Invoicing > Customers > Customer Statements*, then add several names to the **Customers** field, or - select several rows in the Customers list and use **Print > Customer Statement** (the report is bound to the customer list). **Print** produces one PDF with each customer on its own page. **Send by Email** renders each customer's statement separately and emails it with the PDF attached, using the *Customer Statement* mail template. Customers without an email address are skipped; if none of the selected customers has an email, the wizard warns you. ![](screenshot_statement_report.png) ### Automatic scheduled statements Set up a schedule once and let the module email statements for you. 1. Go to *Accounting / Invoicing > Customers > Statement Schedules* (visible to the Accounting Manager) and create a schedule. 2. **Schedule** — pick a **Frequency** (Daily, Weekly, Monthly or Quarterly). For *Weekly*, choose the **Day of Week**; for *Monthly*/*Quarterly*, set the **Day of Month** (values past the end of a short month fall back to its last day). 3. **Statement** — choose **Show** (Open Items or All Transactions), whether to **Attach PDF**, the **Email Template**, and **Skip zero-balance customers** (on by default). 4. **Recipients** — set **Send To**: - *Customers with an outstanding balance* — anyone with unreconciled, non-zero posted receivables. - *Customers in selected tags* — pick the **Customer Tags**. - *All customers*. - *Custom filter* — an Odoo domain on `res.partner`, e.g. `[('country_id.code', '=', 'BE')]`. 5. Leave **active** ticked. Each statement goes to the customer's Invoicing contact (falling back to the customer) in that customer's own language. A daily scheduled action (*Customer Statements: send scheduled statements*) runs every active schedule that is due on the current day. Sending is idempotent per period — a monthly schedule sends at most once per month — so the cron can safely run daily. Use the **Send Now** button on the schedule form to send the current period immediately (it still respects the once-per-period rule recorded in **Last Sent Period**). With no active schedule, the module stays fully manual. ### Worked example You want an open-item statement emailed on the 1st of every month to every customer that still owes you money: 1. *Customers > Statement Schedules > New*. 2. Name: `Monthly open-item statements`; **Frequency** = *Monthly*, **Day of Month** = `1`. 3. **Show** = *Open Items*, **Attach PDF** on, **Skip zero-balance customers** on. 4. **Send To** = *Customers with an outstanding balance*. Save. On the next 1st of the month the cron emails each qualifying customer a PDF of their open items with the aging breakdown, and stamps **Last Sent Period** so no one is emailed twice that month. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_customer_statement -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (1 model, prefixed `customer.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## 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 Invoicing app. No extra Python package. ****Where does it show up?**** Under the **Customer Statements** menu (Statement Schedules). ****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. ## Changelog ### 20.0.1.2.0 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.2.0 - Dunning levels have a form: who signs, whether the salesperson is warned, whether the customer goes on credit hold. - Send Dunning Now: every customer due, at today's level, sent at once. - Every notice is a line on the customer's chatter, with a count and a last date on the form. - The credit-hold bridge, installed by itself with Credit Limit & Credit Hold. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Scheduled, segmented automatic customer statements. - Plus professional on-demand statements of account with a running balance and an aging summary (current / 30 / 60 / 90+). Print, preview or email in one click, single, in batch, or on a schedule. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # Dashboard Builder: Drag-and-drop KPI Dashboards > Drag-and-drop KPI dashboards with charts and targets, threshold alerts that email the people who care, and scheduled PDF snapshots. - **What it is:** Drag-and-drop KPI dashboards with charts and targets, threshold alerts that email the people who care, and scheduled PDF snapshots. - **For:** Extra Tools - **Odoo versions:** 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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_dashboard - **Not for:** The app reads from the Odoo models you point each item at, using the viewing user's own access rights and the active company; it does not send data outside your Odoo instance.; Multi-company is respected: boards and items are scoped to companies via record rules.; Board visibility is enforced by Allowed Groups plus record rules on both boards and items.; In scope today: tiles, the full chart set (bar, horizontal bar, line, area, pie, doughnut, polar area, radial bar, scatter, radar, flower, funnel), KPIs with target / previous-period comparison, list views with target & deviation columns, partner maps, flat and dated targets, drag-and-drop layout, date filtering, email / PDF / image export.; Limits: each chart is capped at 500 group-by buckets for performance, so a very high-cardinality group-by is truncated — group by a coarser field instead. The Map view plots partners that have geolocation coordinates. Targets render on bar/horizontal-bar/ line/area charts and KPIs only. - **Compared with the alternatives:** https://docs.bambooforge.dev/bambooforge_dashboard/compare/ - **Manual:** https://docs.bambooforge.dev/bambooforge_dashboard/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_dashboard.dashboard_board_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_dashboard - **Manual last updated:** 2026-10-15 --- # Dashboard Builder: Drag-and-drop KPI Dashboards Build live operational dashboards inside Odoo 20 without writing code: drop in tiles, charts, KPIs and lists that read straight from any Odoo model, group and measure the data the way you want, drag the tiles into place, then set targets and share the board with the right people. This page is the complete manual. If you follow it top to bottom you can install the app, build your first dashboard, add a chart with the chart builder, set KPIs and targets, share the board, and fix the common issues **without contacting support**. ## Overview BambooForge Dashboard turns any Odoo model into a visual dashboard. Each dashboard is a **board** that holds **items**; every item reads its own model, applies its own filter, and renders as a tile, a chart, a KPI, a list or a map. You arrange the items on a drag-and-drop grid and save the layout. It covers: - **Tiles** — a single number (count, sum, average, or a Python expression) with an icon, colour and optional unit. - **Charts** — bar, horizontal bar, line, area, pie, doughnut, polar area, radial bar, scatter, radar, flower, funnel — each built from a model, a measure and a group-by. - **KPIs** — a single headline number with an optional **target** or a **previous-period comparison**. - **Lists** — a paginated table of records with the fields you choose, optional target and deviation columns. - **Maps** — partners plotted by geolocation. - **Targets** — a flat standard target, or dated target lines plotted against time-grouped charts and KPIs. Every item reads live data through Odoo's own `read_group` / `search` engine, so it always reflects the current database and respects the user's access rights and the active company. ## Requirements - **Odoo**: 20.0, Community or Enterprise. - **Dependencies**: the app depends on **Spreadsheet Dashboard** (`spreadsheet_dashboard`), **Geolocalization** (`base_geolocalize`), **Sales** (`sale_management`) and **CRM** (`crm`); all of them install automatically, so a database without Sales or CRM gets both. The Spreadsheet Dashboard menu becomes the home of the *Dashboard Pro* menu, Geolocalization provides partner latitude/longitude for the Map view, and Sales and CRM carry the ready-made sales and pipeline boards. - **Python**: no extra libraries beyond a standard Odoo 20 install. All charting libraries (amCharts 5, gridstack, pdfmake) ship inside the module. - **Odoo access**: every internal user (group **base.group_user**) can *view* dashboards they are allowed to see. Only a **Dashboard Manager** (`spreadsheet_dashboard.group_dashboard_manager`) can create, edit or delete boards and items. The Odoo administrator (`base.group_system`) is a Dashboard Manager by default. - **Network**: none. Everything runs inside Odoo against your own database. ## Installation 1. Copy `bambooforge_dashboard` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *BambooForge Dashboard*, and press **Activate / Install**. The *Spreadsheet Dashboard* and *Geolocalization* dependencies install automatically. After install you will find a **Dashboards** application in the main menu. The app adds a **Dashboard Pro** entry under it (menu *Dashboards ▸ Dashboard Pro*). Demo data installs a ready-made *Dashboard Demo* board you can open to see every item type at once. ## Getting started — build your first dashboard You build a dashboard in three moves: create the board, add items (tiles, charts, KPIs, lists, maps) to it, then arrange and save the layout. ![A finished BambooForge dashboard with tiles, charts and KPIs](dashboard_demo_2.png) ### Step 1 — Create a dashboard board 1. Open **Dashboards ▸ Dashboard Pro**. You see the boards as kanban cards. 2. Click **New** and fill in the board form: | Field | What to enter | |---|---| | Dashboard Name | The board title, e.g. *Sales Overview*. | | Group of dashboards | Optional grouping shown in the left search panel (Sales, CRM, Finance, Logistics, Services, Marketing, Website, Human Resources ship as defaults). | | Show Under Menu | Optional. Pick one or more existing menus; the app auto-creates a link to this dashboard directly under each chosen menu so users reach it from where they work. | | Menu Name | The label for those link menus (defaults to the Dashboard Name if left blank). | | Company | Multi-company only. The company that owns the board; its items inherit it. | | Allowed Groups | The security groups allowed to open this board. **Leave blank and every internal user can see it**; set one or more groups to restrict it (see *Usage — sharing & access*). | | Default Date Filter | The period the board opens on: *All Time*, *Week/Month/Quarter/Year to Date*, *Last 7/30/90/365 days*, or *Custom Filter* (then pick a start and end date). | 1. Click **Save**, then **View** (header button) to open the live dashboard. ### Step 2 — Add a chart with the chart builder On the open dashboard, as a Dashboard Manager you get an **Add the Charts** button (the `+` in the control panel). Click it to open the **item builder** form. ![The chart builder form — choose type, model, measure and group-by](dashboard_chart_builder.png) Fill it in top to bottom: 1. **Name** — a unique label for the item. 2. **Type** — pick the item type. The live preview on the right updates as you configure the rest. Available types: | Type | What it renders | |---|---| | Tile | A single value with icon, colour and optional unit. | | Bar Chart / Horizontal Bar Chart | Vertical or horizontal bars; supports stacking with a sub-group. | | Line Chart / Area Chart | Trends over a group-by (often a date); supports a cumulative line. | | Pie / Doughnut Chart | Share of a measure per group; doughnut/pie can render as a semi-circle. | | Polar Area / Radial Bar Chart | Circular comparison of a measure across groups. | | Scatter Chart | Points for value distribution. | | Radar View / Flower View | Multi-axis comparison. | | Funnel Chart | Stages sorted high-to-low (sort order is forced to descending). | | List View | A paginated table of records with chosen fields. | | KPI | A single headline number with optional target / comparison. | | Map View | Partners plotted on a world map by geolocation. | 1. **Model** (*Data Source ▸ Model*) — the model the item reads, e.g. *Sales Order*. This is the source of every measure and group-by below. Transient models are excluded. 2. **Domain** — optional filter conditions (the standard Odoo domain editor), e.g. only confirmed orders. 3. **Date Filter Field** — the date/datetime field the board's date filter applies to (defaults to *Created on* when you pick a model). It must be a Date or Datetime field. 4. **Data Type** (*Computation ▸ Data Type*) — how the measure is aggregated: | Data Type | Meaning | |---|---| | Count | Number of records matching the model + domain (no measure field needed). | | Sum | Sum of a numeric **Measure** field. | | Average | Average of a numeric **Measure** field. | | Python Code | A small expression evaluated per group, with `records` (the matching recordset) in scope; set the answer in `result`. Example: `result = len(records)`. | 1. **Measure** — for *Sum* / *Average*, the numeric field to aggregate (only integer, float or monetary fields are offered). 2. **Group By** — the field that becomes the chart's x-axis / categories (any stored field except binary and x2many). If it is a date/datetime field, also pick a **Group By Type** (Day, Week, Month, Quarter, Year). 3. **Sub Group By** — optional second level of grouping; produces stacked bars or multiple series. With a sub-group you can enable **Stacked Bar Chart**. 4. **Sort By Field** / **Sort Order** — optional ordering of the buckets. Switch to the **Display** tab to set colours, icon, layout, **Show Data Value**, **Show Legend**, **Show Records** (click-through to the underlying Odoo list), and **custom unit** (monetary or a short text unit such as `km`). Save the item; it appears on the board. ### Step 3 — Set KPIs and targets **KPI item.** Choose **Type = KPI**, pick a **Model**, **Data Type** and (for sum/average) a **Measure**. In the **Target** group: - Tick **Enable Target** and enter a **Standard Target** value — the KPI shows the headline number against that target. - Or tick **Compare With Previous Period** (in the KPI *Data Source* group) to show the current value next to the same metric for the previous period (Week/Month/Quarter/Year to Date map to last week/month/quarter/year). **Chart targets.** Bar, horizontal bar, line and area charts can show a target too: - A **Standard Target** value plots a flat target series across every bar/point. Show it as an extra bar, or as an overlaid line by enabling **Show Target As Line**. - **Dated target lines** (per-date goals) apply when the chart is grouped by a date field: each goal has a *Date* and a *Value*, and the targets are matched to the matching time buckets of the chart. When dated target lines are in use, the list/pagination limit is managed automatically. ![Charts and KPIs with targets on a dashboard](dashboard_demo_3.png) ## Usage ### Arrange & drag tiles Open a board and, as a Dashboard Manager, choose **Edit Layout** from the gear (cog) menu. The grid becomes draggable (it uses a gridstack grid): 1. Drag any tile to move it; drag its corner to resize it. 2. Click **Save Layout** to persist the arrangement, or **Discard** to revert. The saved arrangement is stored on the board (the *Item Configurations* field), so every viewer sees the same layout. ### Quick-customise, move, duplicate, delete items Hover an item to reveal its toolbar (manager only): the pencil opens a quick-customise popup for that item's type and look, the *i* shows its description, and the bin removes it. You can also **Move** an item to another board or **Duplicate** it across boards from the item actions. ### Date filter, refresh & export The control panel carries the board's date filter; changing it re-reads every item for the new period. From the gear menu a manager can **Send by Email** (uses the bundled *Dashboard Report* mail template), **Print PDF**, or **Capture Dashboard** (saves a board image used on the kanban card). ### Sharing & access rights Who can see a board is controlled by **Allowed Groups** on the board form: - **Blank** — every internal user can open the board and its items. - **One or more groups** — only users in those groups can open it; the app enforces this with record rules on both the board and its items, so a restricted board's charts, measures and data are not even readable by users outside the groups. Who can *build* is fixed by Odoo groups: only **Dashboard Managers** create/edit/delete; everyone else is view-only. Use **Show Under Menu** to surface a board as a link beneath the menus where your users already work. ## Customization - **Look** — per item: background and font colour (with transparency), a built-in icon or an uploaded image, six tile layouts, decimal places for a sum or an average (a count is always a whole number), and a custom unit (monetary symbol or a short text unit, max 5 characters). - **Computation** — beyond count/sum/average, the **Python Code** data type lets you compute a value per group from the matching `records` recordset (e.g. ratios across related models). The expression runs in Odoo's safe-eval sandbox. - **Click-through** — enable **Show Records** so clicking an item opens the underlying Odoo records, or attach an **Item Action** (an existing window action on the same model) to jump to a specific view. - **Grouping** — single or two-level grouping; date group-bys at Day/Week/Month/Quarter/Year; cumulative line on date-grouped charts; fill-temporal to show empty periods. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Chart / KPI shows no data | The model has no records matching the **Domain** and the active **Date Filter**. Widen or clear the domain, switch the board date filter to *All Time*, and confirm the **Date Filter Field** points at a field that actually holds dates for these records. | | Wrong measure or aggregation | **Data Type** and **Measure** disagree. *Count* ignores the measure; *Sum*/*Average* require a numeric **Measure** field (only integer/float/monetary fields are offered). Re-pick the measure after changing the model — changing the model clears it. | | Chart is empty but the model has data | A required **Group By** is missing, or a date group-by has no **Group By Type**. Pick a Group By, and for date fields also pick Day/Week/Month/Quarter/Year. | | Target line not showing | Targets only render on **bar, horizontal bar, line and area** charts and on **KPI** items. Enable **Enable Target** and set a **Standard Target**; dated target lines need the chart grouped by a **date** field so goals can match the buckets. | | "Pagination limit value cannot be Negative or Zero" | The list-view pagination limit must be positive. Set a value above 0, or use dated target lines (which manage the limit automatically). | | User cannot see a dashboard | The board has **Allowed Groups** set and the user is not in them, or the user is not an internal user. Add the user's group to **Allowed Groups**, or clear it to show the board to everyone. | | User cannot create or edit dashboards | Only **Dashboard Managers** can build. Give the user the *Dashboard Manager* group (under the Spreadsheet Dashboard app). Plain users are view-only by design. | | Slow dashboard on a large model | Each chart reads via `read_group` and is capped at 500 buckets. A high-cardinality **Group By** (free-text names, per-day over years) is slow and unreadable. Group by a coarser field or a larger date interval (Month/Quarter/Year) and tighten the domain. | | Map view is empty | Partners have no latitude/longitude. Run Odoo's geolocalization on the contacts (the *Geolocalization* dependency) so they get coordinates to plot. | | "Start date must be less than end date" | The board's custom date filter has the dates reversed. Set Start Date before End Date. | ## Frequently asked questions **Which Odoo versions are supported?** Odoo 20.0, Community or Enterprise. The app depends on the standard *Spreadsheet Dashboard* and *Geolocalization* modules, which ship with Odoo. **Do I need to write code?** No. Tiles, charts, KPIs, lists and maps are built entirely from the form by picking a model, a measure and a group-by. The optional *Python Code* data type is there only if you want a computed value; everything else is point-and-click. **Which models can I chart?** Any non-transient Odoo model your user may read. Measures are numeric fields (integer/float/monetary); group-bys are any stored field except binary and x2many. **Can different teams see different dashboards?** Yes. Set **Allowed Groups** on each board. Leave it blank for everyone, or pick groups to restrict a board to those users — the restriction is enforced down to the item data. **Who is allowed to edit dashboards?** Only users in the *Dashboard Manager* group (the Odoo administrator is one by default). All other internal users are view-only. **How do I move or reuse an item?** Use the item's **Move** action to send it to another board, or **Duplicate** to copy it onto one or more boards. **Can I email or print a dashboard?** Yes — a manager can **Send by Email** (using the bundled report template), **Print PDF**, or **Capture Dashboard** as an image from the gear menu. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - The app reads from the Odoo models you point each item at, using the viewing user's own access rights and the active company; it does not send data outside your Odoo instance. - Multi-company is respected: boards and items are scoped to companies via record rules. - Board visibility is enforced by **Allowed Groups** plus record rules on both boards and items. - **In scope today**: tiles, the full chart set (bar, horizontal bar, line, area, pie, doughnut, polar area, radial bar, scatter, radar, flower, funnel), KPIs with target / previous-period comparison, list views with target & deviation columns, partner maps, flat and dated targets, drag-and-drop layout, date filtering, email / PDF / image export. - **Limits**: each chart is capped at **500 group-by buckets** for performance, so a very high-cardinality group-by is truncated — group by a coarser field instead. The Map view plots partners that have geolocation coordinates. Targets render on bar/horizontal-bar/ line/area charts and KPIs only. ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_dashboard -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (7 models, prefixed `bambooforge.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - The security groups this module defined (user assignments to them are dropped). ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.2.5 - A count is shown as a whole number - "57 won", not "57.00 won" - on the board, in its previews and in the emailed snapshots. - The decimal places of a tile or KPI that shows a sum or an average are set on its Display tab; the field was on no screen before. ### 20.0.1.2.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.6 Current release for Odoo 20.0. This build includes: - Scheduled dashboard subscriptions: emailed PDF + inline HTML snapshots, daily/weekly/monthly, idempotent per period. Drag-and-drop, no-code Odoo dashboards with 15+ chart types, KPI targets, period comparison, drill-down and PDF export. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ### KPI alerts A subscription mails on a schedule; an **alert** mails when a number moves. Create one under *Dashboards ▸ KPI Alerts*: pick a KPI item, choose whether it matters when the value **falls below** or **rises above** your threshold, and set a cooldown so a value sitting on the line does not mail every hour. Recipients can be users, contacts or plain addresses — duplicates collapse. An hourly scheduled action checks every active alert; **Check now** and **Send test mail** are on the form for the impatient. ## How we make sure it works Before every release, **44 automated checks** run against this app on Odoo 17, 18, 19 and 20. Among them: - Alert without recipients does not crash. - Outsider cannot read restricted item. - Idempotent no double send same period. - Not due subscription does not send. - No recipients not due. The complete list is in the source you receive. --- # Financial Dashboard All in One with KPI Tiles > Put any line of a Financial Reports Builder statement on a Dashboard Builder board as a tile or KPI - gross margin, EBITDA, cash - with the board's own date filter, a previous-period comparison, a target, a named owner, and a sentence that says what moved, in the alert and in the weekly snapshot. - **What it is:** Put any line of a Financial Reports Builder statement on a Dashboard Builder board as a tile or KPI - gross margin, EBITDA, cash - with the board's own date filter, a previous-period comparison, a target, a named owner, and a sentence that says what moved, in the alert and in the weekly snapshot. - **For:** Accounting - **Odoo versions:** 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:** 297 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_dashboard_fin_reports - **Not for:** The live tile on the board shows the number, the previous period and the target the way Dashboard Builder draws them; the sentence itself is on the form and in the e-mails, not painted on the tile.; One line per tile. A chart over a statement is the report's own Run form, with its comparison column and drill-down.; Month to date compares with the month before, not with the same month last year; a year-on-year view is a second KPI with a custom range. - **Manual:** https://docs.bambooforge.dev/bambooforge_dashboard_fin_reports/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_dashboard.dashboard_board_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_dashboard_fin_reports - **Manual last updated:** 2026-09-23 --- # Financial KPIs on the Dashboard Dashboard Builder draws numbers from any model. Financial Reports Builder computes the numbers finance actually reports on. This bridge puts a statement line on a board - and tells you, in one sentence, what moved. ## What you get - A new **data type** on a tile or a KPI: *Financial report line*. Pick a report and a line - a prefix line such as revenue, a type line such as cash, or a formula line such as EBITDA - and its amount is the number on the board. - The **board's date filter is the report's period**: month to date, last quarter, a custom range. A balance sheet line is read *as at* the end of the period, the way the report's own Run form does it. - **Previous period** and **target** work exactly as for any other KPI. Month to date compares with the same days of the month before, a quarter with the quarter before, a year with the year before; any other range with the same number of days ending the day before it. - A **KPI owner**: the person who answers for the number. Added to every alert on the item and named in the snapshot. - **What moved**: one sentence on the KPI form, in the threshold alert e-mail and in the scheduled snapshot - "EUR 1.35M, up 12.5% on the previous period (EUR 1.20M); 90% of the EUR 1.50M target." - Multi-currency: a report in another company's currency is converted to the board's currency at the period end. ## Install Needs **Dashboard Builder** and **Financial Reports Builder**, both by BambooForge; the store price of this page includes them. Install the bridge and the new data type appears on every tile and KPI form. ## Putting a statement line on a board 1. Build the statement in *Accounting → Financial Reports* if it is not there yet: a P&L with a *Revenue* line on prefix `70`, a *Result* formula, whatever the owner reads. 2. Open the board, add a **KPI** (or a tile), and under *Computation* set *Data Type* to **Financial report line**. Pick the report and the line. The model and the date field fill in by themselves. 3. Tick **Posted entries only** off if the board should read drafts too. 4. Tick **Compare With Previous Period** and set a **Target** as usual. 5. Set the **KPI owner**. The board's date filter drives the period. A KPI with its own date filter uses that instead; with no filter at all, the number is the fiscal year to date - a P&L since inception is not a figure anybody runs a business on. ## What moved The sentence is built from what is known and nothing else: - the value, formatted in the board's currency; - if the comparison is on: *up 12.5% on the previous period (EUR 1.20M)*, *down*, *unchanged*, or *no previous-period figure to compare with*; - if a target is set: *90% of the EUR 1.50M target*, or *over it by EUR 120K*. It shows on the KPI form under *What moved*, goes into every **threshold alert** e-mail on the item (with the owner named), and becomes a column of the **scheduled snapshot** - inline and in the PDF - so the Monday e-mail says which numbers moved, not only what they are. ## Limits, stated plainly - The live tile on the board shows the number, the previous period and the target the way Dashboard Builder draws them; the sentence itself is on the form and in the e-mails, not painted on the tile. - One line per tile. A chart over a statement is the report's own Run form, with its comparison column and drill-down. - Month to date compares with the month before, not with the same month last year; a year-on-year view is a second KPI with a custom range. ## How we make sure it works Before every release, **17 automated checks** run against this app on Odoo 17, 18, 19 and 20. Among them: - No filter means fiscal year to date. - Draft entries only when asked. - Line must belong to report. - Narrative flat and no previous. - Previous period same length before. The complete list is in the source you receive. --- # Dealer & Distributor Management > Dealer and distributor network management: onboarding, automatic tiers from sales, exclusive territories, credit blocking and sales targets. - **What it is:** Dealer and distributor network management: onboarding, automatic tiers from sales, exclusive territories, credit blocking and sales targets. - **For:** Sales - **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:** 149 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_dealer_management - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Let two active dealers hold an exclusive territory.; Move a tier you locked - Lock Tier pins it and excludes it from recomputation. - **Manual:** https://docs.bambooforge.dev/bambooforge_dealer_management/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_dealer_management.dealer_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_dealer_management - **Manual last updated:** 2026-10-15 --- # Dealer & Distributor Management Run your whole reseller network from inside Odoo 20: onboard dealers and distributors, rank them into automatic tiers from their sales, assign exclusive territories, cap their credit exposure (and block orders that would break the limit), and set sales targets with live achievement tracking. This page is the complete manual. Follow it top to bottom and you can install, onboard your first dealer, configure tiers and territories, manage credit and targets, and fix the common issues **without contacting support**. ## Overview A dealer is a business partner who resells your products. This app turns the standard Odoo contact into a managed dealer record and adds the tooling a distribution business actually needs: - **Onboarding workflow** — every dealer moves through Prospect, Active, Suspended and Terminated, with the status tracked in the chatter. - **Automatic tiers** — Bronze, Silver, Gold and Platinum (fully editable), assigned automatically from each dealer's trailing 12-month sales. - **Territories** — group dealers by area, optionally **exclusive** so only one active dealer can hold a territory. - **Credit control** — set a credit limit per dealer; orders that would push their receivable past the limit are blocked (managers can override). - **Sales targets** — set a target per dealer per period and watch the achieved amount and percentage update from real orders. - **Reporting** — a pivot of dealer sales by tier, territory and dealer. ## Requirements - Odoo 20.0. - The **Sales** app (`sale`). Installing this app pulls it in automatically. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_dealer_management` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **Dealer Management** and click *Install*. A new top-level **Dealers** app appears in the main menu. ## Onboarding your first dealer 1. Open **Dealers > Dealers** and click *New*. 2. Enter the dealer's name. A **Dealer Code** (`DLR/0001`...) is assigned automatically. 3. Optionally set a **Territory**, **Account Manager** and **Credit Limit**. 4. Click **Activate**. The dealer becomes *Active*, an onboarding date is stamped, and — if no tier is set yet — a tier is assigned from their sales. Use **Suspend**, **Terminate** and **Set to Prospect** to move the dealer through its lifecycle. Every change is recorded in the chatter. ## Tiers Go to **Dealers > Configuration > Tiers**. Each tier has: - a **Sales Threshold** — the minimum trailing 12-month sales (company currency) to reach the tier automatically, - an indicative **Dealer Discount (%)** and an optional **Pricelist**, - a colour and a description. The four shipped tiers (Bronze, Silver, Gold, Platinum) are a starting point — rename them, change thresholds, add or remove tiers freely. **Automatic assignment.** A dealer's tier is the highest tier whose threshold is met by their trailing 12-month confirmed sales. Recompute it three ways: - the **Recompute Tier** button on a dealer, - the scheduled action **Dealers: Recompute Tiers** (ships disabled — enable it under *Settings > Technical > Scheduled Actions* to run weekly), - automatically on first activation. Tick **Lock Tier** on a dealer to pin it manually and exclude it from automatic recomputation. ## Territories Go to **Dealers > Configuration > Territories**. A territory groups dealers by area and can list the countries and regions it covers, plus an area manager. Tick **Exclusive** to enforce that only **one active dealer** may hold the territory. Trying to activate a second dealer in an exclusive territory is blocked with a clear message. ## Credit control Set a **Dealer Credit Limit** on a dealer (zero means no limit). The **Credit Available** field shows the limit minus the dealer's current receivable exposure (Odoo's standard `credit` figure). When **Block Orders Over Limit** is on, confirming a sales order that would push the dealer's exposure past the limit is blocked with a message telling you by how much. Users in the **Dealer Manager** group can confirm anyway, so you keep an override path for approved exceptions. ## Sales targets Open a dealer's **Targets** tab, or **Dealers > Targets**, and create a target with a period (start/end) and an amount. The **Achieved** amount and **Achievement (%)** are computed from the dealer's confirmed orders in the period and colour-coded in the list (green at or above target, red well below). Targets refresh when edited; to keep open targets current as new orders arrive, enable the scheduled action **Dealers: Recompute Open Targets** (ships disabled), or hit **Recompute** on a target. ## Reporting **Dealers > Reporting > Dealer Sales** opens a pivot of confirmed dealer orders grouped by tier (add territory or dealer as you like). It answers "which tier and territory drives my revenue" at a glance, and you can switch to a graph or export. ## Security Two groups are provided: - **Dealer User** — can see dealers, tiers, territories, targets and reports, and manage targets. Implies the Salesperson role. - **Dealer Manager** — full configuration of tiers and territories, and the ability to override the credit block on order confirmation. Implies the Sales Manager role. Territories and targets are company-scoped via record rules for multi-company databases. ## Frequently asked questions ****A dealer's tier won't change when I recompute.**** Check whether **Lock Tier** is ticked — locked dealers are skipped. Otherwise confirm the dealer has confirmed sales orders in the trailing 12 months. ****Credit Available shows a number but orders aren't blocked.**** Blocking only applies when the dealer has a non-zero credit limit and **Block Orders Over Limit** is on, and the confirming user is not a Dealer Manager. ****Sales figures look empty.**** Only **confirmed** sales orders (state "Sales Order") in the dealer's company count. Quotations are ignored. ****Can a dealer belong to several territories?**** No — a dealer holds one territory at a time, which keeps exclusivity and reporting unambiguous. ****Does it work with multiple companies?**** Yes. Targets and territories are company-scoped, and sales are read per the dealer's company. ## How we make sure it works Before every release, **24 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Scale refuses without a rate. - Preset discount not overwritten. - A rep without accounting confirms within the limit. - Credit block enforced for non manager. - Pricelist tier no discount stacking. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_dealer_management -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (3 models, prefixed `dealer.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - The security groups this module defined (user assignments to them are dropped). ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Let two active dealers hold an exclusive territory. - Move a tier you locked - Lock Tier pins it and excludes it from recomputation. ## 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 Sales app. No extra Python package. ****Where does it show up?**** Under the **Dealers** menu (Dealers, Targets, Reporting, 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. ## Changelog ### 20.0.1.2.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Manage a dealer/distributor network: onboarding workflow, automatic tiers from sales, exclusive territories, credit limits with order blocking, and sales targets with achievement tracking. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # Clinic All in One for Dental Practices > Dental clinic management with a tooth chart you click, treatment plans priced per tooth with the insurer's share, and accepted work billed. - **What it is:** Dental clinic management with a tooth chart you click, treatment plans priced per tooth with the insurer's share, and accepted work billed. - **For:** Industries - **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:** 268 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_dental - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Prune a clinical record on a timer - nothing is ever pruned; retention is your decision against your regulator.; Carry out work that was never accepted, or without an open visit to record it against. - **Manual:** https://docs.bambooforge.dev/bambooforge_dental/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_dental.action_bf_dental_plan_line_todo - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_dental - **Manual last updated:** 2026-10-15 --- # Clinic All in One for Dental Practices ## What it adds BambooForge Clinic gives a practice its patients, its day sheet and its consultations. Dentistry needs two more things, and this module is those two: a chart, and a plan people pay for. ## The tooth chart - **ISO 3950 / FDI numbering** - the two-digit codes every referral, laboratory ticket and insurance claim in the world already uses. Quadrant first, then the tooth counting outwards from the midline. - Drawn the way it hangs on the wall: upper arch on top, lower arch below, the patient's right on the viewer's left. - **Click a tooth to chart it.** Pick the condition once - caries, filled, crown, root canal, implant, bridge, missing, to extract, treatment planned - then click your way round the mouth. A dentist has gloves on and a mouth open in front of them; that is the whole interaction. - **Permanent and deciduous** dentitions, both complete, so children are not an afterthought. - Surfaces in the usual shorthand (MOD), validated against the real five - M, O, D, B, L. - **Every change keeps what was there before.** The second question an insurer or a lawyer asks is what the mouth looked like in March, and a chart that overwrites itself cannot answer it. **Nothing is ever pruned**: how long a practice keeps a clinical record is its own decision against its own regulator - thirty years for a minor in several EU states - not a default that deletes on a timer. ## Treatment plans - **Priced per tooth**, because nobody accepts four thousand euros of dentistry in one sentence. They accept the crown now and think about the implant. - Each line carries its tooth, its surfaces, a **phase** (this month, next month), the price, the **insurer's expected share** and what the **patient actually pays** - the number people decide on. - **Partial acceptance is the normal case.** Accept some lines, decline others; the plan follows along - presented, partly accepted, accepted, completed - and shows what share of the accepted work is already done. - **Accepted work drops into the visit.** One click puts a line onto the open consultation as a billable item, marks it done and charts the tooth, so the record, the chart and the invoice cannot drift apart. Work that was never accepted cannot be carried out, and work cannot be carried out without an open visit to record it against. - **"Accepted work not done"** is a menu of its own: the list to phone when the diary looks empty. In most practices that list is the largest asset nobody is looking at. ## Requirements Requires BambooForge Clinic. Odoo Community 16.0, 17.0, 18.0, 19.0 or 20.0 - no Enterprise module, no external service. ## How we make sure it works Before every release, **85 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A line cannot be marked done without carrying the work out. - Charting the same condition twice is not a change. - A plan cannot be accepted without being presented. - A finished line cannot be deleted out from under the invoice. - Extending a quote that never expires does not give it a date. 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: - Prune a clinical record on a timer - nothing is ever pruned; retention is your decision against your regulator. - Carry out work that was never accepted, or without an open visit to record it against. ## Install 1. Copy `bambooforge_dental` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Dental Clinic Management**. 3. Open **Accepted work not done** - its menus are Treatment plans, Teeth. ## 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 Web; clinic (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Accepted work not done** menu (Treatment plans, Teeth). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - answered within two business days. This module is licensed OPL-1; one purchase covers one production database plus its staging copies. --- # Direct Print | Print to Network Printers Without IoT Box > Send any report, label or receipt straight from Odoo to a network printer - IPP, CUPS, AirPrint, Zebra and ESC/POS over raw TCP - with no IoT Box, no print-server subscription and no agent on a desktop. Auto-print rules, a print queue with retries, one printer per user, warehouse or report. - **What it is:** Send any report, label or receipt straight from Odoo to a network printer - IPP, CUPS, AirPrint, Zebra and ESC/POS over raw TCP - with no IoT Box, no print-server subscription and no agent on a desktop. Auto-print rules, a print queue with retries, one printer per user, warehouse or report. - **For:** Extra Tools - **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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_direct_print - **Run for real:** Tested for real against CUPS 2.4 (a Generic PostScript queue and a raw Zebra queue), a Zebra-style raw listener on port 9100 and QZ Tray 2.2.6: a delivery slip rendered by Odoo went through IPP and came out of the queue as PostScript, an Odoo ZPL product label printed from the label wizard reached the label printer byte for byte, a validated transfer fired its auto-print rule and printed without anyone pressing Print, a test page and a queued POS receipt went from the browser through QZ Tray's signed websocket to the PC's printers, and the printers' refusals came back as the job's error text - 0 lost jobs. - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Print from a browser to a printer the server cannot reach - a printer with no network port is reached through QZ Tray on its PC, and only while an Odoo tab is open there.; Convert documents: a PDF goes to a document printer, ZPL to a label printer, ESC/POS to a receipt printer, as they are.; Print Point of Sale receipts on its own - that is the POS Receipts add-on. - **Compared with the alternatives:** https://docs.bambooforge.dev/bambooforge_direct_print/compare/ - **Manual:** https://docs.bambooforge.dev/bambooforge_direct_print/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_direct_print.action_bf_print_printer - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_direct_print - **Manual last updated:** 2026-10-15 --- # Direct Print | Print to Network Printers Without IoT Box Send any report, label or receipt **straight from Odoo to a printer**. No IoT Box, no print-server subscription, no agent on a desktop: the Odoo server speaks **IPP** to network printers, CUPS servers and AirPrint devices, and **raw TCP** to Zebra and ESC/POS printers. A printer plugged into a PC by USB is reached through the free **QZ Tray** program on that PC. Every job is on record, with retries and a *Print again* button; auto-print rules print a delivery slip when a transfer is validated or an invoice when it is posted, before anyone reaches a desk. With the *POS Receipts* add-on, the receipt of every paid Point of Sale order prints itself. This page is the complete manual. Follow it top to bottom and you can install the app, add a printer, print a test page, route a report and set up an auto-print rule **without contacting support**. ## Why this exists Odoo Community prints to the browser: a report is a PDF the user downloads and sends to a printer by hand - five clicks and a wrong-printer dialog, forty times a day at a packing bench. Odoo 20 added a Printer module to Community: the user's browser sends ZPL labels to a Zebra printer and an image of a report to an Epson receipt printer - those two kinds of printer only, with no queue and no record of what was printed. For the rest Odoo's answer is the IoT Box, which is Enterprise-only and a device to buy. Most "direct print" apps on the store route your documents through a paid cloud print service or install an agent on a Windows desktop that has to stay on. A network printer already speaks a protocol the Odoo server can speak: **IPP**, the standard behind CUPS and AirPrint. A label printer listens on **port 9100** for raw ZPL. This app uses exactly that - the document goes from the database to the printer, and nowhere else. ## Install 1. Copy `bambooforge_direct_print` into your addons path and update the app list, or install the ZIP from the Odoo Apps store. 2. Install **Direct Print**. Every internal user becomes a *Direct Print / User* (may send documents to the printers, sees their own jobs); the administrator is a *Manager* (sets up printers, rules and report routing). 3. The menu **Direct Print** appears with *Print jobs*, *Printers*, *Auto-print rules* and *Report routing*. Nothing else to install. The Odoo server needs to reach the printer over the network - the printer's port 631 (IPP) or 9100 (raw). ## Add a printer *Direct Print → Printers → New* ****Kind**** Documents (PDF), Labels (ZPL / raw) or Receipts (ESC/POS). A PDF report is only ever sent to a document printer; a ZPL label only to a label printer. ****Transport**** *IPP / CUPS / AirPrint* for network printers and print servers; *Raw TCP* for Zebra, TSC, Godex, Bixolon, Epson receipt printers. ****IPP address**** `ipp://192.168.1.20:631/ipp/print` for a printer, `ipp://cups.local:631/printers/q` for a CUPS queue, `ipps://` for TLS. ****Host / Port**** The printer's IP and 9100 for raw printers. ****Company default for …**** Where a document goes when neither the report nor the user names a printer. One per kind. Press **Test connection**: an IPP printer answers with its state and its model; a raw printer with an open port. Then **Print a test page** - a one-page PDF, a test label or a test receipt, according to the kind. The answer lands on the printer's form (*Last seen*) and as a job under *Print jobs*. > **Tip:** Where is the IPP address? On most printers it is printed on the network > configuration page, or it is `ipp://:631/ipp/print`. On a CUPS > server, `lpstat -v` lists the queues as `ipp://:631/printers/`. > AirPrint printers advertise the same address. ## Print from the Print menu *Direct Print → Report routing* lists every PDF and text report. Open one and set **When printed**: - **Open in the browser** - Odoo's default, unchanged. - **Send to the printer** - the Print menu sends the document and shows *"Delivery Slip - WH/OUT/00012 sent to Office laser"*; nothing downloads. - **Send to the printer and open in the browser** - both. Name a **Printer** on the report, or leave it empty: then the user's own printer is used (*Preferences → My printers*), and failing that the company default of the right kind. **Copies** applies to every send. Reports launched through a wizard - Odoo's product label wizard, the picking operations wizard - carry the wizard's choices with them: a ZPL product label printed for four units goes to the label printer as four labels. A **Print** button of your own can call the same door: ``` report = env.ref("stock.action_report_delivery") report.bf_direct_print(picking.ids) # printer picked as above report.bf_direct_print(picking.ids, printer_id=7, copies=2) ``` ## Auto-print rules *Direct Print → Auto-print rules → New* ****Model**** What to watch: Transfer, Journal Entry, Sales Order, Product ... ****When**** *When a record is created*, or *When a field takes a value* - name the field and the value: `state` becomes `done`. ****Domain**** Only matching records print, e.g. `[('picking_type_code', '=', 'outgoing')]`. ****Report / Printer**** What to print and where: a fixed printer, or the user's own then the company default. The value is watched where Odoo writes it, which includes **stored computed fields**: a transfer's `state` is computed from its moves, and the rule fires the moment the last move is done. A rule fires once per record when the value is reached, not again when the record is saved with the same value. Three rules most warehouses set up on day one: - *Transfer*, `state` becomes `done`, domain outgoing, report *Delivery Slip*, the packing-bench printer. - *Journal Entry*, `state` becomes `posted`, domain `[('move_type', '=', 'out_invoice')]`, report *Invoices*, the office printer. - *Product*, when created, report *Product Label (ZPL)*, the Zebra. Auto-printed jobs are rendered when they are sent, by the queue, so a validation never waits for a PDF. ## The print queue *Direct Print → Print jobs* is every document that went to a printer: queued, sent, retrying, failed or cancelled, with the printer, the user, the copies, the attempt count, how long the printer took, the printer's job id and, when something went wrong, the printer's words. A job the printer refuses is retried after 1, 5, 15 and 60 minutes, then marked failed; **Print again** sends it once more, now. **Cancel** stops a job that has not gone out. The bytes that were sent are attached to the job, so a disputed label can be reprinted exactly. Users see their own jobs; managers see every job of their companies. Sent, failed and cancelled jobs are swept after 30 days (`bambooforge_direct_print.job_retention_days`, 0 keeps them). ## A USB printer on a PC: QZ Tray A printer with no network port - the receipt printer under the till, the label printer on a desk - is reached through [QZ Tray](https://qz.io), a free, open-source program that runs on Windows, macOS and Linux and prints what a web page hands it. The Odoo server never sees that printer; the user's browser does the handing over. 1. Install QZ Tray on the PC the printer is plugged into, and make sure the printer is installed there as a normal printer (it appears in the PC's printer list). 2. *Direct Print → Printers → New*: transport **QZ Tray**, and the **printer name on the PC** exactly as that list shows it. 3. **Printed from the browser of**: the user who works on that PC. Their Odoo tab picks up queued jobs for this printer every fifteen seconds - auto-print rules and *Print again* reach the printer through it. A document printed from the Print menu goes through the browser that printed it, whoever that is. 4. Press **Test connection** on that PC: the browser asks QZ Tray for the printer and records the answer on the form. **Print a test page** works the same way. **Allow or sign.** The first time a site talks to QZ Tray, QZ Tray asks the user whether to allow it; *Remember this decision* keeps it quiet from then on. For an installation with many tills, sign the requests instead: on the printer form, under *QZ Tray trust*, **Generate a signing certificate**, then drop the downloaded `override.crt` into QZ Tray's program folder on each PC (`C:\Program Files\QZ Tray` on Windows, `/opt/qz-tray` on Linux, the app bundle on macOS) and restart QZ Tray. Signed requests print with no prompt at all. The private key stays in Odoo; administrators see it on the printer form and nowhere else. **PDF or raw.** A PDF report is sent in *pixel* mode - QZ Tray renders it and the printer's driver prints it, right for a laser or inkjet. ZPL labels and ESC/POS receipts go as raw bytes. A label or receipt printer whose driver expects raw bytes for everything can take PDFs raw too: tick *Send PDFs as raw bytes*. What the browser must be. Any current Chrome, Edge, Firefox or Safari on the PC; the Odoo tab has to be open for queued jobs to move - a printer behind a browser cannot print while nobody is logged in there. For a printer that must print with nobody around, plug it into the network or into a CUPS server instead. ## Point of Sale receipts Install **Direct Print: POS Receipts** (`bambooforge_direct_print_pos`, installed by itself when Point of Sale is present). Then, per shop, under *Point of Sale → Configuration → Settings → Bills & Receipts*: ****Receipt printed by the server**** Tick it. The moment an order is paid, the server renders the receipt as ESC/POS and sends it to the shop's receipt printer - no print dialog, no IoT Box, no preview screen to click through. ****Receipt printer**** A printer of kind *Receipts*: an Epson, Star, Bixolon or other ESC/POS printer on the network (raw, port 9100), a CUPS queue, or a USB printer on the till's PC through QZ Tray. Empty: the company default for receipts. ****Receipt width**** 58 mm paper (32 columns) or 80 mm paper (42 columns, or 48 in the small font). ****Copies**, **Customer on the receipt**** As they say. The receipt carries the company name and address, the POS *receipt header* and *footer* from the same settings page, the receipt number, date, cashier and customer, every line with quantity, unit price and discount, subtotal, taxes, total, each payment and the change. Accents are folded to what a receipt printer's character set can show. In the back office, a paid order has a **Print receipt** button (reprints exactly) and a *Receipts* counter that opens its jobs. A printer that is down never blocks a sale: the job waits in the queue and retries. Odoo's own receipt printing in the Point of Sale screen is untouched; turn its *Automatic Receipt Printing* off to stop the browser printing a second copy. ## What it prints - **PDF** reports of any model - through IPP, the printer or CUPS converts them. - **ZPL / EPL / raw** labels from `qweb-text` reports (Odoo's own product and lot labels, the BambooForge Label Designer's ZPL) - straight to a label printer over port 9100 or a raw CUPS queue. - **ESC/POS** receipts as raw bytes, for receipt printers on the network or on a PC through QZ Tray; the *POS Receipts* add-on renders them from Point of Sale orders. Sending ZPL to an IPP printer needs the printer, or the CUPS queue, to accept raw data (`application/octet-stream`); on CUPS that is a queue created with `-m raw`. ## Compared with the other ways to print from Odoo | Way | What it means for you | |---|---| | Odoo's IoT Box | Enterprise only, and a device to buy and keep running. Prints from the browser through the box. | | Odoo 20's Printer module | Free, in Community. The user's browser sends ZPL to a Zebra printer and an image of a report to an Epson ePOS receipt printer, for the reports linked to that printer. No office printers, no queue or retries, no record of what was printed. | | Cloud print services | A subscription, and every document leaves your network to come back to a printer a few metres away. Nothing prints when the service is down. | | Desktop-agent apps | A program on a Windows PC that must stay on; the store lists several at roughly 12 to 150 EUR. The PC becomes the print server. | | This app | The Odoo server speaks IPP and raw TCP to the printers itself, so nothing sits in between and nothing leaves; a USB printer is reached through QZ Tray on the PC it is plugged into. Auto-print rules and a queue with retries are included. 129 EUR per Odoo series, source included, no subscription. | The store prices quoted are those shown on the Odoo Apps Store at the time of writing; check the listings for today's figures and for what each app actually installs. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Print from a browser to a printer the server cannot reach - a printer with no network port is reached through QZ Tray on its PC, and only while an Odoo tab is open there. - Convert documents: a PDF goes to a document printer, ZPL to a label printer, ESC/POS to a receipt printer, as they are. - Print Point of Sale receipts on its own - that is the POS Receipts add-on. ## Security notes - The Odoo server talks to network printers; browsers, desktops and third parties are not involved and no document leaves your network. A QZ Tray printer is the exception by design: the document goes from the server to the user's browser to QZ Tray on the same PC, still inside your network. - QZ Tray requests are signed with the company's own key when a certificate is generated; the key is readable by administrators only and never sent to the browser. - IPP credentials, when a printer or CUPS server asks for them, are stored on the printer record and visible to administrators only. - Every job records who sent it and what was sent. - Printers and rules are per company; users see their own jobs. ## Troubleshooting ****Test connection fails with "Connection refused"**** The Odoo server cannot reach the printer's port. Check the address, the port (631 for IPP, 9100 for raw) and that no firewall sits between the server and the printer. ****"client-error-document-format-not-supported"**** The printer does not take the format sent. For a PDF printer, try `application/octet-stream` as *document format* (under debug mode on the printer form); for ZPL on CUPS, make the queue raw. ****"server-error-not-accepting-jobs"**** The queue is paused on the printer or the CUPS server; resume it there. ****Print sent to the printer, nothing came out**** Open the job: *Sent* means the printer accepted it and it is in the printer's own queue - look at the printer's display. *Retrying* shows the printer's answer. ****The Print menu still downloads**** The report's *When printed* is *Open in the browser*, or the browser has an old page open - reload after changing report routing. ****No printer**** "No printer: name one on the report, on your preferences, or as the company default." Do one of the three. ****QZ Tray: "QZ Tray is not running on this PC"**** Start QZ Tray on that PC (its icon sits in the system tray). The browser reaches it on `localhost`; a browser on another PC cannot. ****QZ Tray: "Cannot find printer"**** The *printer name on the PC* differs from what the PC calls it. Open the PC's printer list and copy the name exactly. ****QZ Tray keeps asking to allow the site**** Tick *Remember this decision*, or install the signing certificate as `override.crt` (see *A USB printer on a PC*). ****A queued job for a QZ Tray printer stays "Waiting"**** Nobody has an Odoo tab open on that PC as the printer's user. Log in there, or give the printer to the user who is. ****The POS receipt prints twice**** Odoo's own *Automatic Receipt Printing* is on as well; turn one of the two off. ## Tested against the real thing Automated tests run against a fake printer on every release; the sentence below is the part no fake can give you. Tested for real against CUPS 2.4 (a Generic PostScript queue and a raw Zebra queue), a Zebra-style raw listener on port 9100 and QZ Tray 2.2.6: a delivery slip rendered by Odoo went through IPP and came out of the queue as PostScript, an Odoo ZPL product label printed from the label wizard reached the label printer byte for byte, a validated transfer fired its auto-print rule and printed without anyone pressing Print, a test page and a queued POS receipt went from the browser through QZ Tray's signed websocket to the PC's printers, and the printers' refusals came back as the job's error text - 0 lost jobs. ## Changelog ### 20.0.1.1.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 1.1.0 - QZ Tray transport: a USB or local printer on a PC, printed through the user's browser; test connection and test page from the browser; queued jobs picked up by the printer's user; signed requests with a generated certificate and `override.crt`. - *What it says*: labels and receipts are shown as text on the job. - The **POS Receipts** add-on: the receipt of every paid order rendered as ESC/POS and printed by the server. ### 1.0.0 - Printers over IPP / CUPS / AirPrint and raw TCP; test connection and test page. - Report routing: *Send to the printer* from the Print menu, per report, with a printer per report, per user or per company. - Auto-print rules on creation or on a field value, including stored computed fields. - A print queue with retries, print again, cancel and retention. ## How we make sure it works Before every release, **42 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A pdf cannot go to a label printer. - The host's browser sees the queue and others do not. - No printer is an explanation not a crash. - A user sees their own jobs and no printer settings. - A raw job sends the bytes once per copy. Tested for real against CUPS 2.4 (a Generic PostScript queue and a raw Zebra queue), a Zebra-style raw listener on port 9100 and QZ Tray 2.2.6: a delivery slip rendered by Odoo went through IPP and came out of the queue as PostScript, an Odoo ZPL product label printed from the label wizard reached the label printer byte for byte, a validated transfer fired its auto-print rule and printed without anyone pressing Print, a test page and a queued POS receipt went from the browser through QZ Tray's signed websocket to the PC's printers, and the printers' refusals came back as the job's error text - 0 lost jobs. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - questions, bug reports and feature requests are answered by the developers who wrote the module. --- # Direct Print All in One with POS Receipts > The receipt of every paid Point of Sale order printed on an ESC/POS receipt printer by the Odoo server - over the network or through QZ Tray on the cashier's PC. No IoT Box, no browser print dialog, no cloud print service. A receipt per shop, a Print receipt button on the order, every receipt in the print queue. - **What it is:** The receipt of every paid Point of Sale order printed on an ESC/POS receipt printer by the Odoo server - over the network or through QZ Tray on the cashier's PC. No IoT Box, no browser print dialog, no cloud print service. A receipt per shop, a Print receipt button on the order, every receipt in the print queue. - **For:** Sales/Point of Sale - **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:** 178 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_direct_print_pos - **Run for real:** Tested for real against an ESC/POS raw listener on port 9100 and QZ Tray 2.2.6 on the till's PC: two paid orders printed their receipts by themselves - header, lines with quantity and a 10% discount, total, cash and change, footer, cut - one straight to the network printer and one picked up by the cashier's browser and handed to QZ Tray; a reprint from the order matched the first byte for byte. - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Change the Point of Sale screen or its own receipt printing: it renders the receipt on the server after the order is paid.; Print scripts outside CP437: accents are folded, other symbols print as question marks. - **Manual:** https://docs.bambooforge.dev/bambooforge_direct_print_pos/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-point_of_sale.action_pos_pos_form - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_direct_print_pos - **Manual last updated:** 2026-10-15 --- # Direct Print All in One with POS Receipts The receipt of every paid Point of Sale order, **printed by the Odoo server** on an ESC/POS receipt printer the moment the order is paid. No IoT Box, no browser print dialog, no preview screen to click through, no cloud print service. The printer is on the network, on a CUPS server, or plugged into the till's PC through QZ Tray. Every receipt is a job in Direct Print's queue: retried when the printer is down, reprinted exactly from the order. This add-on needs **Direct Print** (`bambooforge_direct_print`) and installs itself when Point of Sale is present. ## Why this exists Odoo Community prints a POS receipt through the browser: the cashier presses *Print*, the browser's dialog opens, the right printer has to be chosen, and a preview screen sits between every sale and the next one. Only an Epson printer that speaks ePOS is reached without the dialog, from the till's browser by its IP address; for any other printer the IoT Box that removes those steps is Enterprise-only and a device to buy. Receipt printers themselves have spoken the same language for thirty years - ESC/POS - and most of them have a network port that accepts it on port 9100. This add-on renders that language on the server and sends it there. ## Install 1. Install **Direct Print** and add the receipt printer there (*Direct Print → Printers*, kind *Receipts (ESC/POS)*): raw TCP for a printer on the network, IPP for a CUPS queue, QZ Tray for a USB printer on a PC. Press *Print a test page* - a test receipt comes out. 2. Install **Direct Print: POS Receipts**. When Point of Sale is already installed, it installs by itself with Direct Print. ## Set it up per shop *Point of Sale → Configuration → Settings*, pick the shop, section *Bills & Receipts*: ****Receipt printed by the server**** Tick it. From then on, every paid order of this shop prints its receipt on the server side. ****Receipt printer**** The printer from Direct Print. Empty: the company's default receipt printer. ****Receipt width**** *58 mm paper (32 columns)*, *80 mm paper (42 columns)* or *80 mm paper, small font (48 columns)*. ****Receipt copies**** Two for a shop that keeps one. ****Customer on the receipt**** The customer's name under the cashier, when the order has one. The receipt uses the **Header** and **Footer** texts of the same settings page, so what the browser receipt said, this one says too. Turn Odoo's own **Automatic Receipt Printing** off in the same section, or the browser prints a second copy. ## What comes out Company name in double size, address, phone, VAT and website; the header text; receipt number, date, cashier (the employee when POS uses cashiers, else the user) and customer; one line per product with quantity, unit price and discount when they matter; subtotal, taxes and total; every payment and the change; the footer text; the receipt number again; four blank lines and a partial cut. Accents are folded to the printer's character set unless the printer is given one: with the default CP437 `Café crème` prints as `Cafe creme`; with Windows-1252 it prints as written. The bytes are ESC/POS with only the commands every receipt printer honours: initialise, alignment, emphasis, double size, feed, cut. Epson, Star, Bixolon, Citizen, Xprinter and the rest print them. ## In the back office *Point of Sale → Orders → Orders*: a paid order shows **Print receipt** in its header and a **Receipts** counter that opens its print jobs. The job carries the bytes sent and a *What it says* tab with the receipt as text - the receipt can be reprinted exactly, weeks later. ## When the printer is down The sale goes through. The receipt is a job in *Direct Print → Print jobs* in state *Retrying*, with the printer's answer, and is retried after 1, 5, 15 and 60 minutes; *Print again* sends it now. A printer behind QZ Tray prints as soon as the till's Odoo tab is open again. ## Compared with the other ways to print a receipt | Way | What it means for you | |---|---| | Odoo's own receipt printing | The browser prints, through its print dialog; an Epson ePOS printer is reached from the till's browser by its IP address; any other printer through the Enterprise-only IoT Box. | | "POS direct print" apps on the store | Print from the POS screen through the browser or a desktop program; the one with the most purchases at the time of writing is listed at about 208 EUR. | | This add-on | The Odoo server renders ESC/POS from the paid order and sends it to the printer - network, CUPS, or USB through QZ Tray - so the cashier presses nothing and the receipt is in the print queue with retries and exact reprints. 49 EUR on top of Direct Print. | The store price quoted is the one shown on the Odoo Apps Store at the time of writing; check the listing for today's figure. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Change the Point of Sale screen or its own receipt printing: it renders the receipt on the server after the order is paid. - Print scripts outside CP437: accents are folded, other symbols print as question marks. ## Security notes - Receipts are rendered on the server from the order's stored lines and payments; nothing is taken from the browser. - The job records who paid the order and what was sent. - Printers are per company; cashiers see their own jobs. ## Troubleshooting ****Nothing prints when the order is paid**** The shop's *Receipt printed by the server* is off, or no printer is named on the shop or as the company default for receipts. Open the order: a *Receipts* counter with a *Retrying* job means the printer refused - the job's error says why. ****The receipt prints twice**** Odoo's *Automatic Receipt Printing* is on as well. ****Question marks instead of letters**** Pick the printer's **Receipt character set** on the printer in Direct Print (CP437, CP850/858, Windows-1252, 1250, 1251, 1254, 1258 ...); the receipt is encoded in it and the printer switched to it. Letters the chosen page cannot show are folded to plain letters, symbols print as `?`. ****The lines wrap in the wrong place**** Pick the width that matches the paper - 32 columns for 58 mm, 42 for 80 mm. ## Tested against the real thing Automated tests run against a fake printer on every release; the sentence below is the part no fake can give you. Tested for real against an ESC/POS raw listener on port 9100 and QZ Tray 2.2.6 on the till's PC: two paid orders printed their receipts by themselves - header, lines with quantity and a 10% discount, total, cash and change, footer, cut - one straight to the network printer and one picked up by the cashier's browser and handed to QZ Tray; a reprint from the order matched the first byte for byte. ## Changelog ### 20.0.1.0.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 1.0.0 - A receipt printer, width, copies and customer line per shop. - The receipt printed by the server when the order is paid; *Print receipt* and a *Receipts* counter on the order. ## How we make sure it works Before every release, **8 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A printer that is down does not break the sale. - No printer says so only when asked. - The printer's code page keeps the accents. - The receipt says what the order says. - A paid order prints its receipt. Tested for real against an ESC/POS raw listener on port 9100 and QZ Tray 2.2.6 on the till's PC: two paid orders printed their receipts by themselves - header, lines with quantity and a 10% discount, total, cash and change, footer, cut - one straight to the network printer and one picked up by the cashier's browser and handed to QZ Tray; a reprint from the order matched the first byte for byte. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - questions, bug reports and feature requests are answered by the developers who wrote the module. --- # Discount Approval on Sales Orders > Require manager approval when an order's margin falls below a floor or a line is priced below cost - a clean request, approve or refuse trail. - **What it is:** Require manager approval when an order's margin falls below a floor or a line is priced below cost - a clean request, approve or refuse trail. - **For:** Sales - **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_discount_approval - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Let a salesperson approve their own discount, through the button or by creating the order approved.; Change prices: it holds confirmation until a Sales Manager approves a discount above the threshold. - **Manual:** https://docs.bambooforge.dev/bambooforge_discount_approval/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-sale.action_orders - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_discount_approval - **Manual last updated:** 2026-10-15 --- # Discount Approval on Sales Orders Stop margin erosion from over-discounting. Set the maximum discount your salespeople can give on their own — anything above it must be **approved by a sales manager** before the order can be confirmed. This page is the complete manual. Follow it top to bottom and you can install, set your threshold and run the workflow **without contacting support**. ## Overview - You set a **discount threshold** (a percentage) per company. - When a quotation has a line discount **above** that threshold, it cannot be confirmed until a **Sales Manager** approves it. - Salespeople **request approval**; managers **approve** or **refuse**, with the whole decision logged on the order's chatter. - Lower the discount back under the threshold and the order confirms normally — no approval needed. ## Requirements - Odoo 20.0 with **Sales** (`sale_management`). Installing this app pulls it in automatically. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_discount_approval` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **Discount Approval** and click *Install*. ## Setting the threshold Go to **Settings > Sales > Pricing > Discount Approval** and set *Needs approval above* to the maximum self-service discount (for example 15%). The threshold is per company. ## The workflow 1. A salesperson builds a quotation. If any line's discount exceeds the threshold, an **Approval Pending** state applies and confirming is blocked. 2. The salesperson clicks **Request Discount Approval** — a to-do activity is raised for the team leader and the order shows *To Approve*. 3. A **Sales Manager** opens the order and clicks **Approve Discount** (or **Refuse Discount**). Both are logged on the chatter. 4. Once approved, the order confirms normally. If refused, it stays blocked until the discount is reduced or re-approved. If the discount is lowered below the threshold at any point, the block clears automatically. ## Frequently asked questions ****Who can approve?**** Any user in the **Sales / Administrator** (Sales Manager) group. Salespeople cannot approve their own discounts. ****Does it look at the order's overall discount or each line?**** It uses the **highest line discount** on the order, so a single deep-discounted line still triggers approval. ****Can I set different thresholds per company?**** Yes — the threshold is stored per company. ****What if a manager builds the quotation?**** A manager can approve and confirm in one go, since they hold the approval right. ****Does it block other ways of confirming?**** Confirmation is blocked wherever it runs, including the portal and back-office, because the check is on the confirm action itself. ## How we make sure it works Before every release, **20 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Rep cannot self approve via create. - Rep cannot self refuse via create. - Salesperson cannot self approve margin hold. - Non manager cannot approve directly. - Non manager cannot approve. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Let a salesperson approve their own discount, through the button or by creating the order approved. - Change prices: it holds confirmation until a Sales Manager approves a discount above the threshold. ## Troubleshooting ### Order stuck at "To Approve" with no one to approve it ***Cause*** Requesting approval schedules a to-do activity for the sales team leader (`team_id.user_id`) or, failing that, the salesperson — but only a user in the **Sales / Administrator** group (`sales_team.group_sale_manager`) can actually approve. If the team has no leader, or the leader is not a Sales Manager, the activity lands on someone who cannot press **Approve Discount**. ***Diagnosis*** Open the order: the **Approval Pending** ribbon shows and *Discount Approval* reads *To Approve*. Check the activity assignee against the members of the Sales / Administrator group. ***Resolution*** Have any Sales Manager open the order and click **Approve Discount** (or **Refuse Discount**) — the buttons are visible only to that group. Optionally set the team's *Team Leader* to a Sales Manager so future requests route to the right person. ### "Only a Sales Manager can approve or refuse a discount" ***Cause*** The **Approve Discount** / **Refuse Discount** buttons are visible only to the Sales / Administrator group, and both the action and the underlying `write` are guarded — a salesperson (or an RPC call) attempting to set the state to *approved* or *refused* is rejected with this error. ***Diagnosis*** Confirm the acting user's groups under **Settings > Users**. Salespeople hold *Sales / User* only. ***Resolution*** Grant the approver the *Sales / Administrator* group, or route the approval to an existing Sales Manager. This is by design: it prevents self-approval by a salesperson. ### Confirmation blocked: "needs sales-manager approval" ***Cause*** `action_confirm` refuses to confirm while `approval_required` is set and the order is not yet *approved*. This fires on every confirm path (back office and portal), listing the trigger in the *Approval Reason*. ***Resolution*** Either reduce what triggered it (bring the top line discount back under the threshold, or lift the margin), or click **Request Discount Approval** and have a Sales Manager approve. Reducing the discount below the threshold clears the block automatically. ### Discount is over the limit but no approval is requested ***Cause*** Approval keys off the **highest single line discount** versus the per-company *Discount Approval Threshold*. An order-level discount spread across lines, a threshold set too high, or a threshold of 0 (which approves everything above 0%) can all leave `approval_required` unset. ***Diagnosis*** In developer mode inspect `max_line_discount`, `discount_approval_threshold` and `approval_required` on the order. Verify the threshold under **Settings > Sales > Pricing** for the order's company. ***Resolution*** Set the threshold on the correct company (it is per company) and apply discounts on the lines so `max_line_discount` reflects them. ### Margin floor or below-cost check never triggers ***Cause*** Gross margin is computed from each product's **cost (standard price)** in the company currency. When no line carries a cost, the order is treated as full (100%) margin and gives no signal, so neither the *Minimum Gross Margin* floor nor *Block Below-Cost Lines* can fire. A *Minimum Gross Margin* left at 0 also disables the floor check entirely. ***Diagnosis*** Check `bf_margin_percent` and `bf_below_cost` on the order, and confirm the products have a non-zero *Cost*. Confirm *Minimum Gross Margin* and *Block Below-Cost Lines* are set for the company. ***Resolution*** Maintain product costs, set the margin floor above 0, and enable *Block Below-Cost Lines* if below-cost deals should require approval. ### Approval clears itself after the order is edited ***Cause*** Changing the order lines revisits the decision: if the edit removes the trigger, the state falls back to *No Approval Needed*; if an already-approved order still needs approval, it drops back to *To Approve* so it is reviewed again. ***Resolution*** This is intended — margin and discount are re-evaluated on every line change. Re-request and re-approve after material edits. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_discount_approval -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The fields, views and analysis definitions this module added. - The menus, actions, views and reports this module installed. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.2.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.2.2 - A line another app says is already approved - a trade scheme's free units or scheme discount, from *Field Sales Trade Schemes* - no longer counts towards the order's maximum discount, so the scheme's order can be confirmed without a second approval. A discount raised above what the scheme gives still counts. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Require manager approval when an order's gross margin falls below a configurable floor or a line is priced below cost. - On top of your existing discount-percentage threshold. Stop margin erosion, with a clean request-approve-refuse workflow and a guard against self-approval. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # Discuss Pro: Modern Chat UI > A modern, professional redesign of Odoo Discuss: quick reactions, compact density and brand-aware styling, built on Odoo's own theme colours. - **What it is:** A modern, professional redesign of Odoo Discuss: quick reactions, compact density and brand-aware styling, built on Odoo's own theme colours. - **For:** Discuss - **Odoo versions:** 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:** 69 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_discuss_pro - **Manual:** https://docs.bambooforge.dev/bambooforge_discuss_pro/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-mail.action_discuss - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_discuss_pro - **Manual last updated:** 2026-10-15 --- # Discuss Pro: Modern Chat UI A modern, feature-rich redesign of Odoo 20 Discuss. It adds the chat features your team expects — quick reactions, polls, scheduled messages, formatting, forwarding, custom statuses and more — on top of a refined interface that follows your backend theme's colors. This module does not fork the `mail` module. It uses Odoo's official extension points only, so it stays compatible with other apps and uninstalls cleanly. ## Installation 1. Copy `bambooforge_discuss_pro` into your addons path (or install from the Apps store). 2. Go to **Apps**, click *Update Apps List*, search for **Discuss Pro** and install. 3. That's it — open **Discuss** and the new interface is live for every user. No configuration is required. A couple of preferences are available per user (see *Preferences* below). ## Features and how to use them ### Quick reactions Hover any message and a small bar of frequently used emoji appears at the top-right. Click one to react instantly. The full emoji picker and the existing reaction tools remain available — this is just a faster path to the common ones. ### Message formatting The composer has a small formatting toolbar (Bold, Italic, Strikethrough, Inline code). Select some text and click a button, or type the markers yourself: - `**bold**` → **bold** - `*italic*` → *italic* - `~~strike~~` → strikethrough - a backtick pair around text → inline code Formatting is rendered when the message is displayed. ### Polls Odoo 20 ships polls of its own, so on 20 new polls are created with Odoo's **Create Poll** composer action, in channels and group chats. Polls created with earlier versions of Discuss Pro keep showing in the feed. Anyone in the channel can vote by clicking an option. Bars and percentages update in **real time** for everyone. The poll author can **Close poll** at any time to stop further voting. ### Scheduled send (send later) 1. Type your message, then click the **clock** icon in the composer. 2. Pick a time (or use a quick preset) and click **Schedule**. 3. The message is delivered automatically at that time. To review or cancel pending messages, open the **Scheduled messages** panel from the channel header (clock icon). A background job posts due messages every couple of minutes. ### Forward a message Open a message's action menu and choose **Forward**, pick a destination channel, and the message is reposted there with the original author kept as attribution. ### Custom status Set a personal emoji and short status text under **Preferences** (avatar menu → Preferences). Your emoji then appears next to your name in Discuss, with the full text shown on hover. ### Multi-select and bulk actions Hover a message and click the checkbox on its left to select it. Select as many as you like; a floating bar appears with **Star**, **Delete** and **Clear**. Useful for cleaning up or bookmarking several messages at once. ### Quick switcher Click **Jump to…** at the top of the sidebar (or press `Ctrl+K`) to open the command palette. Type `#` to find a channel or `@` to find a person, then press Enter to jump there. ### Density Toggle **Compact** / **Comfortable** spacing from the channel header. Compact fits more messages on screen. Your choice is saved on your own account. ### Pinned bar When a channel has pinned messages, a bar at the top shows the most recent one. Click the bar to jump straight to that message in the conversation. (Use the stock *Pinned Messages* panel in the channel header to pin or unpin.) ### Favorite channels Hover a channel in the sidebar and click the star to mark it a favorite. Favorites are grouped into a **Favorites** section at the top of the sidebar, saved on your account. Click the star again to remove it. ### Code blocks Wrap text in triple backticks to post a multi-line code block: ``` ``` your code here ``` ``` It renders as a dark, monospaced block with a one-click **Copy** button. Use the code-block button in the composer toolbar to insert the markers. ### Search filters In the message search, combine free text with filters: - `from:name` — only messages from a given author. - `has:link` — messages that contain a link. - `has:file` — messages with an attachment. - `has:star` — messages you starred. For example, `from:alice has:file report` finds attachments shared by Alice that mention "report". ### Do Not Disturb Click **Do Not Disturb** at the bottom of the sidebar to silence notification sounds and desktop alerts across Discuss. It stays on (saved to your account) until you turn it off, and can also be set in *Preferences*. ### Profile cards Click a person's name or avatar to open their profile card. It now shows their custom status — the personal emoji and text they set in *Preferences*. ## Preferences Each user can set, under *Preferences*: - **Discuss Density** — Comfortable (default) or Compact. - **Status Emoji** and **Status Text** — your personal status shown in Discuss. - **Do Not Disturb** — silence notification sounds and desktop alerts. Density can also be toggled from the Discuss channel header, Do Not Disturb from the bottom of the sidebar, and favorites via the star on each channel. ## Theme compatibility Every color used by Discuss Pro is derived from Odoo's own theme variables (the primary color, grays and semantic colors) and exposed as `--bf-discuss-*` CSS custom properties. No color is hardcoded. If you install a backend theme that changes the primary color, Discuss Pro recolors to match automatically, with no configuration. The screenshots in the store listing use Odoo's default accent; your installation follows yours. ## Technical notes Discuss Pro is implemented entirely through Odoo's supported extension points: - `mail.message/actions` and `mail.thread/actions` registries (forward action, density toggle, scheduled-messages panel). - Owl component patches and template inheritance for the message, composer and sidebar enhancements. - A small set of backend models for the server-side features: - `bf.discuss.poll` / `bf.discuss.poll.option` / `bf.discuss.poll.vote` - `bf.discuss.scheduled.message` (with a scheduled job to post due messages) - additive fields on `res.partner` / `res.users` (custom status, density, favorite channels, Do Not Disturb) and `mail.message` (poll link) - A dedicated search route (`/bambooforge_discuss_pro/search`) powers the `from:` / `has:` filters, so the stock message routes are not overridden. - The pinned bar and favorites reuse Odoo's own pinned-messages store and sidebar registries; Do Not Disturb patches the out-of-focus notification service. Real-time poll updates use the standard Odoo bus. The `mail` module itself is never modified, so upgrades remain clean. ## Uninstall Uninstalling the module removes its assets, models and the few additive fields, and Discuss returns to stock Odoo. No database residue is left behind. ## How we make sure it works Before every release, **20 automated checks** run against this app on Odoo 17, 18, 19 and 20. Among them: - Cannot schedule message into foreign channel. - Non author cannot close via method. - Non author cannot close via write. - Cannot vote as another partner. - Non author cannot unlink poll. The complete list is in the source you receive. ## Support - Email: [support@bambooforge.dev](mailto:support@bambooforge.dev) - License: OPL-1 (full source included) - Tested on Odoo 20.0. ## Frequently asked questions ### Which features are turned on by default? All of them. Once installed, the redesigned interface and every feature are live with no configuration. The only per-user preferences are Density (Comfortable by default), your status emoji/text and Do Not Disturb. ### Does it work on mobile and small screens? Yes. It runs in the standard Odoo web client, so it works anywhere Discuss does, including mobile browsers, and Compact density helps on narrow screens. Hover-only affordances (quick-react bar, select checkbox) surface on tap on touch devices. ### If a colleague does not have the module, does it affect them? The module is installed per database, not per user, so everyone on that database gets the new interface. Content degrades gracefully: a poll, forward or scheduled message posts as an ordinary channel message, readable by any Odoo user. ### Do polls and scheduled messages persist? Yes. Polls (`bf.discuss.poll`) and pending scheduled messages (`bf.discuss.scheduled.message`) are stored in your database and survive reloads and restarts. A scheduled message leaves the queue once posted; a poll lives with its channel. ### Is any external or third-party service used? No. Everything runs inside your own Odoo instance — no external API calls, no third-party servers, no telemetry. Live poll and reaction updates use Odoo's built-in bus; scheduled sending uses a standard Odoo scheduled action (cron). ### What is the performance impact? Minimal. The features are lightweight client components plus a few small backend models queried only when you open a poll or the scheduled panel. The scheduled-send cron runs every two minutes and processes only messages that are due. ### Does it change or migrate core Discuss data? No. The `mail` module is never forked. It adds its own models and a few additive fields (custom status, density, favorite channels, Do Not Disturb, and a poll link on `mail.message`). Existing data is untouched, so it uninstalls cleanly. ## Questions people ask ****Which Odoo versions does it run on?**** Odoo 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. ****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. ## Troubleshooting ### The new interface or a feature does not appear **Cause.** The browser is serving cached JavaScript/SCSS assets from before the install or update. **Diagnosis.** Confirm the module is *Installed* under **Apps**, then test whether a hard refresh restores the missing element. **Resolution.** Hard-refresh (Ctrl+F5 / Cmd+Shift+R). If it persists, in Developer mode use **Settings → Technical → Regenerate Assets Bundles** and reload. ### Scheduled messages are not being sent **Cause.** The scheduled action is inactive, or the cron scheduler is not running. **Diagnosis.** In Developer mode open **Settings → Technical → Scheduled Actions**, find *Discuss Pro: Post scheduled messages*, and confirm it is active with an advancing next-execution date. Pending items show in the **Scheduled messages** panel. **Resolution.** Ensure a cron worker is enabled (`max_cron_threads` > 0 or a dedicated cron process) and reactivate the action if disabled. Messages post on the next run after their send time, so allow up to the two-minute interval. ### Markdown or code blocks are not rendered **Cause.** Formatting is applied by the client at display time, so a cached client shows raw markers, or the markers are malformed. **Diagnosis.** Check whether the message renders after a hard refresh, and verify balanced markers: `**bold**`, `*italic*`, `~~strike~~`, a backtick pair for inline code, and triple backticks for a code block. **Resolution.** Hard-refresh to load current assets and fix unmatched markers. Formatting is presentational only — the stored body is unchanged, so older messages format as soon as they re-render. ### Poll results or reactions do not update live **Cause.** Live updates rely on Odoo's bus (long-polling / websocket); if it is blocked, clients update only on reload. **Diagnosis.** Check whether reloading shows the correct counts. If it does, the live channel — not the stored data — is the issue. **Resolution.** Ensure the longpolling/websocket endpoint is reachable; behind a reverse proxy the `/websocket` and longpolling routes must be forwarded. Stored votes are always correct; only the live push is affected. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_discuss_pro -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (4 models, prefixed `bf.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.1.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - A modern, professional redesign of Odoo Discuss. - Quick reactions, compact density, brand-aware styling. - Built entirely on Odoo's own theme colors. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # Document Management System (DMS) & Cloud Sync > Document management with folders, per-group access, tags, versions with one-click revert and locking - synced to Nextcloud, ownCloud, S3, Google Drive or OneDrive / SharePoint. - **What it is:** Document management with folders, per-group access, tags, versions with one-click revert and locking - synced to Nextcloud, ownCloud, S3, Google Drive or OneDrive / SharePoint. - **For:** Document Management - **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:** 169 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_dms - **Not for:** Sync is per-folder-tree by binding folders to a storage; there is no per-file cherry-picking.; Remote files that never existed in Odoo are brought in only where Bring in new remote files is ticked, on the storage or the folder; with it off the mirror is Odoo-outward, with remote edits (not remote creations) flowing back.; WebDAV servers that return no ETag get a synthetic one; change detection then relies on push bookkeeping only. - **Compared with the alternatives:** https://docs.bambooforge.dev/bambooforge_dms/compare/ - **Manual:** https://docs.bambooforge.dev/bambooforge_dms/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_dms.action_bf_dms_file - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_dms - **Manual last updated:** 2026-10-15 --- # Document Management System (DMS) & Cloud Sync Attachments scattered over chatter are not a filing system. This app gives Odoo folders with per-group access, files with versions and locks — and a sync engine that mirrors every folder to Nextcloud, ownCloud or an S3-compatible bucket and is honest about being two-way. ## What syncing actually means here Most "cloud sync" modules upload and hope. This one keeps a ledger: - **Local changes are pushed.** Uploading, renaming, moving or editing a file flags it *To Push*; the next sync run (scheduled every 30 minutes, or the **Sync Now** button) uploads it and remembers the server's ETag. - **Remote edits are detected and pulled** — the reconcile pass compares the stored ETag with the server's. A colleague who edited the file straight in Nextcloud does not overwrite anything: the remote copy comes back **as a new version** on top of the history. - **When both sides changed, nothing is guessed.** The file is flagged **Conflict**, an activity is scheduled for its owner, and the form offers two buttons: *Keep Odoo Version* or *Pull Remote Version*. Whichever you pick, the losing content is still there as a version. - **Nothing is overwritten blind.** Before every push the engine re-checks the remote ETag; a remote edit it has not seen yet turns the push into a conflict instead of a lost update. - **Deletions wait their turn.** Deleting a file in Odoo queues the remote deletion in the sync log; the next run executes it. No network calls ever run inside your database transaction. Every byte that moves is a line in *DMS → Configuration → Sync Log*. ## Install 1. Install **BambooForge DMS & Cloud Sync** (depends on Discuss only). 2. Give document librarians **DMS: Manager** — they configure storages and folders and bypass locks. Everyone else gets **DMS: User**. 3. The **DMS** menu appears with Files, Folders, and (for managers) Configuration. ## Folders *DMS → Folders → New* - **Name** and **parent** build the tree; the full path becomes the remote directory when the folder syncs. - **Access groups** — leave empty and every DMS user sees the folder; pick groups and only their members (plus DMS managers) do. Files, versions and searches all obey the folder's groups. - **Storage** decides where the files physically live. Sub-folders suggest their parent's storage; renaming or moving a folder re-locates every file under it on the remote server on the next sync. ## Files *DMS → Files → New*, or just drop a file in. - Every upload is a **version**; the *Versions* tab shows who, when, size and checksum, lets you download any of them and **revert** — which stacks the old content on top as a new version instead of rewriting history. - **Lock** a file while you work on it; other users cannot modify, move or delete it until you unlock (a manager can always break a lock). - **Tags** and the folder panel on the left make the Files view a browser. - The *Related Record* tab links a file to any business document (an invoice, a project task…) and the smart button jumps straight to it. - sha256 **checksums** are computed on every version — the same fingerprint the sync engine uses to detect changes. ## Storages *DMS → Configuration → Storages → New* **Odoo filestore** is the default: files stay local, nothing syncs. **WebDAV (Nextcloud / ownCloud)** - URL is the DAV files endpoint, e.g. `https://cloud.example.com/remote.php/dav/files/odoo`, plus the login of a dedicated sync user. **Test Connection** does a real PROPFIND. - Missing remote directories are created automatically (MKCOL) on the first push. **S3-compatible object storage** - Works with AWS S3, MinIO, Wasabi and anything that speaks the S3 API. Endpoint, region, bucket and a key pair — that is all. - The Signature V4 signer is built into the module. **No boto3, no extra Python packages on your server.** Common options: - **Sync root** — the remote directory / key prefix everything lives under. - **Scheduled sync** — untick to keep a storage manual-only. - **On remote delete** — when a file vanishes on the server, either upload it again (default) or archive it in Odoo and follow the remote's lead. ### Google Drive and OneDrive / SharePoint Two more storage types, spoken to with the company's own OAuth app, so nothing but a refresh token is kept in Odoo: **Google Drive.** In Google Cloud, create an OAuth client of type *web application*, add the redirect address shown on the storage as an authorised redirect URI, enable the Drive API, and paste the client id and secret on the storage. Press **Connect**, sign in as the account that owns the folder, and the storage shows who it is connected as. Files land under *My Drive* / the sync root, or under the folder whose id you put in *Root folder id* - a shared folder or a shared drive. The folder tree is walked by name and created as needed. **OneDrive / SharePoint.** In Entra (Azure AD), register an app, add the redirect address as a *web* platform, grant the delegated permissions `Files.ReadWrite.All` and `offline_access`, create a client secret, paste the id and secret on the storage and press **Connect**. Leave *Drive id* empty for the connected user's OneDrive; put a SharePoint document library's drive id there to sync into the library. Files are addressed by path, so the tree mirrors the folders exactly. Both behave like the other clouds: every file pushed once, pulled when the remote copy changed, a conflict when both sides did, the remote delete policy, the sync ledger. The access token is refreshed by itself when it expires; a refresh token the provider rotates is kept. Neither provider's SDK is needed - the two APIs are spoken over plain HTTP. ## Day to day - The Files list shows each file's sync badge: *To Push*, *In Sync*, *Conflict*, *Error*. Filters for each are one click away. - **Sync Now** on a storage pushes everything pending and reconciles the rest; the same button on a single file does just that file. - The scheduled job *DMS: sync cloud storages* runs every 30 minutes and works through every storage with scheduled sync enabled. One broken server never starves the others. - Sync log rows older than 90 days are cleaned up automatically — except queued deletions, which are work, not history. ## Limits, stated plainly - Sync is per-folder-tree by binding folders to a storage; there is no per-file cherry-picking. - Remote files that never existed in Odoo are brought in only where **Bring in new remote files** is ticked, on the storage or the folder; with it off the mirror is Odoo-outward, with remote *edits* (not remote *creations*) flowing back. - WebDAV servers that return no ETag get a synthetic one; change detection then relies on push bookkeeping only. ## What changed in 1.1 - **Controlled documents (ISO 9001).** Folders can require approval: draft -> in review -> approved -> obsolete, with named approvers, an activity trail, and one honest rule - *editing an approved file sends it back to draft*, even when the new content arrived from the cloud sync. Review due dates remind the approver a week ahead. - **Retention schedules (ISO 15489).** A folder keeps its files n years (from upload or from the effective date); expiry flags the file and asks a person to archive or waive. Nothing is ever deleted automatically. ## What changed in 1.2 - **Document codes.** Give a folder a prefix (QP, SOP, WI) and every file created there receives a sequential code - QP-001, QP-002 - independent of the filename, searchable, quotable in an audit. - **Content search.** Plain-text formats are indexed on upload and the Files search gains a *Content* filter; PDFs are indexed too when pdfminer.six is installed on the server (stated plainly: without it, PDFs are not). ## What changed in 1.3 - **Controlled copies (ISO 9001 7.5.3).** One button stamps an approved PDF on every page - document code, revision, who took it, when, and "uncontrolled once printed" - and writes the issue into a register on the document, so an auditor can see which revision is out there on paper. The register prunes itself after two years. ## How we make sure it works Before every release, **80 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A dead server is one line in the log not one per folder. - A second rule for the same model is refused. - The sync engine does not know which cloud. - The dms own attachments are not documents. - A frozen folder keeps its name. The complete list is in the source you receive. ## 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 **DMS** menu (Files, Folders, Configuration). ****Does anything leave my server?**** It makes calls outside your server only where its manual says so - read the section on that service before you connect it. ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within two business days. This module is licensed OPL-1; one purchase covers one production database plus its staging copies. ## A folder per record, both ways *Configuration > Folder per record.* Name a model, say which folder its records live under, and every record that matches gets a folder of its own: - **Customers** > *Northwind Trading*, **Projects** > *Customer Portal 2.0*, **Employees** > *Ada Boateng*. The folder is made the first time a document arrives, or all at once with **File what is already there** when you write the rule for a model that has eight years of attachments behind it. - The name comes from a pattern - `{display_name}` by default, or `{ref} - {name}`, or any field of the record in braces. A record renamed renames its folder overnight, and the files move with it on the next sync. Two customers really called *Delta Ltd* get two folders, because a folder name is unique among its siblings. - The folder is a DMS folder like any other: access groups, retention, document control, and the storage of its parent - so a folder per customer on a Google Drive or SharePoint storage *is* a folder per customer on Google Drive or SharePoint. With **Mirror attachments** on, the documents travel both ways: - an attachment on the record becomes a file in the record's folder - versioned, tagged, retained and synced, instead of living in a chatter nobody backs up; - a file put in the record's folder becomes an attachment on the record, where the salesperson works; - a file **dropped into that folder on Drive, SharePoint, OneDrive or Nextcloud** by somebody who has never seen Odoo is brought in by the next sync run and lands on the record like the others. That last one is what **Bring in new remote files** does, on the storage and on each folder. Each sync run lists what is in the remote folder, and anything Odoo has never seen is created here - already marked as synced against the remote copy, so it is not pushed straight back over the file somebody has just put there. Untick it for a bucket that other systems write and nobody here reads. Deleting the document in the DMS takes the copy off the record. Deleting the attachment on the record leaves the document in the DMS: the filing system is the archive, and an archive that empties itself when somebody tidies a chatter is not one. ## What changed in 1.4 - **Google Drive** and **OneDrive / SharePoint** as storage back-ends: the company's own OAuth app, Connect once, the same two-way sync, conflict rule and ledger as WebDAV and S3. ## What changed in 1.5 - **A folder per record**: a rule per model - Customers, Projects, Employees - with the folder named from the record and renamed with it. - **Documents both ways**: an attachment on the record is a file in its folder, a file in the folder is an attachment on the record, and a file dropped into that folder on Drive, SharePoint, OneDrive or Nextcloud is brought in by the next sync and lands on the record. --- # EDIFACT EDI: ORDERS, INVOIC and DESADV > EDIFACT EDI: turn the ORDERS files your retail customers send into sales orders, and send back INVOIC and DESADV, with the original kept. - **What it is:** EDIFACT EDI: turn the ORDERS files your retail customers send into sales orders, and send back INVOIC and DESADV, with the original kept. - **For:** Accounting - **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:** 249 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_edifact - **Not for:** This module reads and writes the files. It does not talk to a VAN or an AS2 gateway — those need credentials and a contract, and are usually a separate piece of infrastructure that drops files where Odoo can pick them up.; ORDERS, INVOIC and DESADV only. ORDRSP, RECADV, IFTMIN and friends are not implemented.; Syntax level A with the D96A directory, which is what the grocery trade runs on. A partner on a different directory version may need adjustments. - **Manual:** https://docs.bambooforge.dev/bambooforge_edifact/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_edifact.action_bf_edifact_message - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_edifact - **Manual last updated:** 2026-09-23 --- # EDIFACT EDI: ORDERS, INVOIC and DESADV ## What this is for Retail chains do not email purchase orders. They send EDIFACT, and a supplier who cannot read it does not get listed. Odoo has no EDIFACT support at all. ## Setting up a trading partner *Sales ▸ EDI ▸ Trading Partners* Every chain runs the same standard slightly differently, so their habits live on a profile rather than in the code: - **Their interchange ID and qualifier** — usually a GLN with qualifier 14. - **Ours** — what we are called in the UNB segment. - **Test interchanges** — partners run a test phase and reject live files during it. Leave this on until they say otherwise. - **Product code field** — which of our fields carries the code they send in LIN. Retail chains almost always send an EAN. - **Check prices on orders** — compare the price on the incoming line with our own price list. - **Confirm orders automatically** — leave off until their files have been trusted for a while. An order that confirms itself also reserves stock and can start a delivery. ## Receiving orders An ORDERS interchange becomes a sales order: the buyer is resolved from `NAD+BY` by interchange identifier, products from `LIN`, quantities from `QTY+21` and prices from `PRI+AAA`. Three behaviours are worth knowing: - **A price difference does not stop the order.** The order is created and the difference is written on it, listing what they say against what our price list says. Price disputes are the expensive part of EDI trading and finding them at invoice time is too late. - **An unknown buyer or product code fails loudly.** The message goes to *Error* with the identifier that could not be matched, and the payload is kept. Fix the data and press **Process Again**. - **A resent interchange does not become a second order.** Partners resend; a gateway that takes the same order twice ships it twice. ## Sending invoices and despatch advices `build_invoic` on a posted customer invoice and `build_desadv` on a validated outgoing shipment produce the interchange. Neither will build from a draft invoice or an unvalidated picking — a despatch advice for goods that have not left is worse than none at all. ## The payload Stored exactly as sent or received, and never rewritten. When a partner disputes an order six months later, the file they actually sent is the only thing that settles it. ## Known limits - This module reads and writes the files. It does not talk to a VAN or an AS2 gateway — those need credentials and a contract, and are usually a separate piece of infrastructure that drops files where Odoo can pick them up. - ORDERS, INVOIC and DESADV only. ORDRSP, RECADV, IFTMIN and friends are not implemented. - Syntax level A with the D96A directory, which is what the grocery trade runs on. A partner on a different directory version may need adjustments. ## How we make sure it works Before every release, **29 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The same order is not taken twice. - Something that is not edifact is refused. - Orders in with a price difference is processed not error. - An employee without the checkbox reads no message. - A partner without a profile is refused. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_edifact` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **EDIFACT EDI: ORDERS, INVOIC and DESADV**. 3. Open **Messages** - its menus are Trading Partners, EDI. ## 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 Inventory, Invoicing, Sales apps. No extra Python package. ****Where does it show up?**** Under the **Messages** menu (Trading Partners, EDI). ****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. --- # E-Invoice Pre-Flight Checks > Run the EN 16931 rules against invoices before they leave, so a rejection arrives as a list of fields to fill in, not a code three days later. - **What it is:** Run the EN 16931 rules against invoices before they leave, so a rejection arrives as a list of fields to fill in, not a code three days later. - **For:** Accounting - **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:** 249 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_einvoice - **Not for:** These are the semantic rules of EN 16931, not a schema validation. A national profile can add its own — Leitweg-ID for German public bodies, Chorus Pro service codes in France.; The unit map covers the units that actually turn up on invoices. The point is to notice an unmapped one, not to be exhaustive.; Passing every check does not guarantee acceptance. It removes the reasons that are yours to remove. - **Manual:** https://docs.bambooforge.dev/bambooforge_einvoice/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_einvoice.action_bf_einvoice_rule - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_einvoice - **Manual last updated:** 2026-09-23 --- # E-Invoice Pre-Flight Checks ## What this is for Odoo builds a technically valid UBL or CII file from whatever is on the invoice. Whether the receiving system accepts it is a different question, and the answer arrives days later as a rejection carrying a rule code and no context — usually from a customer's portal, sometimes from a tax authority. This runs the same rules before the invoice leaves. ## The checks *Accounting ▸ Configuration ▸ E-Invoice Checks* They install as records, so a company can switch off what its customers do not enforce and promote a warning to blocking once one of them has cost an invoice. **Blocking** — these are refusals, not preferences: - seller and buyer name, and a real postal address for both (public-sector buyers reject on the buyer address more than on anything else); - a VAT number or legal registration for the seller; - a due date or payment terms when there is something to pay; - at least one line, a description on every line, and a VAT category on every line. **Warnings** — accepted by most, enforced by some: - a unit of measure with no UN/ECE code. Units travel as UN/ECE Rec. 20 codes, and an unmapped one is usually read as "piece" — wrong on anything sold by weight or length, and invisible until a customer queries the quantity; - no buyer reference or order number, which every customer matching invoices to orders automatically will want. ## Using it **Check E-Invoice** on the invoice form, or the *Check for e-invoicing* action on a selection in the list. Findings appear on the invoice with what to fix, and *Accounting ▸ Reporting ▸ E-Invoice Pre-Flight* lists everything that would be rejected across the company. A nightly pass checks posted invoices nobody has checked, so the problem surfaces the morning after posting rather than at send time. For a send flow that should refuse to hand over a bad invoice, call `move.bf_einvoice_assert_ready()`; it checks first if nobody has. ## Known limits - These are the semantic rules of EN 16931, not a schema validation. A national profile can add its own — Leitweg-ID for German public bodies, Chorus Pro service codes in France. - The unit map covers the units that actually turn up on invoices. The point is to notice an unmapped one, not to be exhaustive. - Passing every check does not guarantee acceptance. It removes the reasons that are yours to remove. ## How we make sure it works Before every release, **21 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - An unmapped unit is a warning not a block. - Issue of another company is not readable. - Issue of another company is not searchable. - Assert ready checks when nobody has. - A line with no product and no description is blocking. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_einvoice` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **E-Invoice Pre-Flight Checks**. 3. Open **E-Invoice Checks** - its menus are E-Invoice Pre-Flight. ## 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 Invoicing app. No extra Python package. ****Where does it show up?**** Under the **E-Invoice Checks** menu (E-Invoice Pre-Flight). ****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. --- # EPCIS Traceability for Supply Chain > Record what happened to which lot, where and why, in the GS1 EPCIS vocabulary your trading partners already read - and trace it end to end. - **What it is:** Record what happened to which lot, where and why, in the GS1 EPCIS vocabulary your trading partners already read - and trace it end to end. - **For:** Warehouse - **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:** 249 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_epcis - **Not for:** Events are captured from transfers. Manufacturing (transformation events) and aggregation onto pallets are modelled but not captured automatically — the fields are there for a customisation or a manual record.; Identifiers are built from the product barcode or internal reference. A company with real GS1 company prefixes should map them properly before sending anything to a partner.; This produces and reads the events. It is not an EPCIS repository and does not answer EPCIS queries over HTTP. - **Manual:** https://docs.bambooforge.dev/bambooforge_epcis/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_epcis.action_bf_epcis_event - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_epcis - **Manual last updated:** 2026-09-21 --- # EPCIS Traceability for Supply Chain ## What this is for Odoo records that stock moved. EPCIS is what a trading partner, a regulator or a recall needs instead: the same movement expressed as **what** happened, to **which** serialised items, **where**, and for what business **reason** — in a vocabulary everybody else in the chain reads the same way. ## Capturing A validated receipt or delivery becomes one event per lot moved, with the GS1 business step and disposition that match the transfer: receiving into stock, shipping into transit. A transfer without lots still produces one event for the transfer as a whole. Quantity-level traceability is worth less than serialised traceability, but refusing to record it is how a traceability project dies in month two. Capture runs **hourly on a schedule**, not on validation. Traceability must not sit on the hottest write path in the warehouse — a slow put-away costs more every day than a one-hour delay in a record nobody reads until there is a recall. The button on the transfer captures immediately when somebody needs it now. ## Tracing The **Trace** button on a lot lists every event that ever touched it, oldest first. That is the query a recall runs, and the reason the events exist at all. ## Exporting Select events and run *Export EPCIS 2.0*. The file is an EPCIS 2.0 JSON document — the shape a partner's system imports — with the CBV vocabulary spelled out in full (`urn:epcglobal:cbv:bizstep:shipping`) rather than in Odoo's own words. Serialised events carry an `epcList`; unserialised ones carry a `quantityList` instead, which is what the standard expects. ## Known limits - Events are captured from transfers. Manufacturing (transformation events) and aggregation onto pallets are modelled but not captured automatically — the fields are there for a customisation or a manual record. - Identifiers are built from the product barcode or internal reference. A company with real GS1 company prefixes should map them properly before sending anything to a partner. - This produces and reads the events. It is not an EPCIS repository and does not answer EPCIS queries over HTTP. ## How we make sure it works Before every release, **11 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Capturing twice does not duplicate. - Event of another company is not readable. - Event of another company is not searchable. - An event without lots still reports a quantity. - An unvalidated transfer is refused. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_epcis` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **EPCIS Traceability for Supply Chain**. 3. Open **Traceability Events**. ## 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 Inventory app. No extra Python package. ****Where does it show up?**** Under the **Traceability Events** menu. ****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. --- # EspoCRM Connector > Sync EspoCRM accounts, contacts, leads and opportunities into Odoo over the REST API, with a sync control tower and field-mapping studio. - **What it is:** Sync EspoCRM accounts, contacts, leads and opportunities into Odoo over the REST API, with a sync control tower and field-mapping studio. - **For:** Sales - **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_espocrm_connector - **Run for real:** Tested for real against EspoCRM 9.3: 3 accounts, 5 contacts, 4 leads and 3 opportunities came into Odoo with 0 errors and every binding accounted for. - **Not for:** The connector reads Accounts, Contacts, Leads and Opportunities from your EspoCRM server and writes the corresponding Odoo res.partner and crm.lead records. It never writes, updates or deletes anything in EspoCRM.; Credentials (API key, webhook secret) are stored in administrator-only fields.; In scope today: read-only import of the four entities, per-field mapping with transforms, schema introspection, dry-run + validation + rollback, a resilient queue, and scheduled import/reconciliation.; Out of scope / roadmap: write-back to EspoCRM, inbound webhooks (the route is a no-op), and incremental modifiedAt filtering (v1 lists by maxSize/offset). - **Manual:** https://docs.bambooforge.dev/bambooforge_espocrm_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_espocrm_connector.action_espocrm_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_espocrm_connector - **Manual last updated:** 2026-10-15 --- # EspoCRM Connector A read-only, import-first bridge from EspoCRM into Odoo 20: pull your Accounts, Contacts, Leads and Opportunities into native Odoo contacts and CRM records over the EspoCRM REST API — with a resilient job queue, dry-run safety, pre-flight validation and rollback so you stay in control. The connector never writes back to EspoCRM. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first import, automate it, and fix the common issues **without contacting support**. ## Overview The connector reads from EspoCRM over its REST API and creates/updates the matching Odoo records. It is **one-way only — Odoo ← EspoCRM** (pull). Nothing is ever written back to EspoCRM. It covers four EspoCRM source entities mapped onto two Odoo models: | EspoCRM entity | Lands in Odoo as | Notes | |---|---|---| | Account | `res.partner` (company) | Imported as a company contact (`is_company = True`). | | Contact | `res.partner` (individual) | Imported as an individual, linked to its account where available. | | Lead | `crm.lead` (type *Lead*) | The raw EspoCRM status is kept on the record. | | Opportunity | `crm.lead` (type *Opportunity*) | Amount, close date, probability and the raw EspoCRM stage are kept. | Because four EspoCRM entities map onto only two Odoo models, the connector is keyed by the **EspoCRM entity** end to end. Accounts and Contacts never collide on `res.partner`, and Leads never collide with Opportunities on `crm.lead`. Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ## Tested against the real thing Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records. Tested for real against EspoCRM 9.3: 3 accounts, 5 contacts, 4 leads and 3 opportunities came into Odoo with 0 errors and every binding accounted for. ## Requirements - **Odoo**: 20.0, Community or Enterprise. The connector depends on *Contacts* (`contacts`) and *CRM* (`crm`), which install automatically. - **EspoCRM**: any EspoCRM server exposing the standard REST API at `/api/v1`. The connector uses only generic list and single-record endpoints, so it is version-tolerant. EspoCRM must be reachable from the Odoo server. - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can open the connector screens. The EspoCRM API key and webhook secret are stored in system-only fields, so only a **Settings / Administration** user can read or change them. - **Network**: outbound HTTPS from Odoo to your EspoCRM server. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ## Installation 1. Copy `bambooforge_espocrm_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *EspoCRM*, and press **Activate / Install**. Dependencies (*Contacts*, *CRM*) install automatically. No EspoCRM server is required to evaluate the connector: a mock EspoCRM REST API ships inside the module, so you can install, explore and run the full import flow against sample data before pointing it at a real server. To use it, set the instance **Base URL** to `/espocrm/mock_api` with **API Path** `api/v1` and any non-empty API key. ## Step 1 — Create an API key in EspoCRM The connector authenticates with a single **API key**, sent as the `X-Api-Key` header on every request. There is no OAuth and no token exchange. Create a read-capable API user in EspoCRM: 1. Sign in to EspoCRM as an administrator. 2. Go to **Administration ▸ API Users**. 3. Click **Create API User**. 4. Set **Authentication Method** to **API Key**. EspoCRM generates the key for you. 5. Give the API user **read access** to *Accounts*, *Contacts*, *Leads* and *Opportunities* via its **Roles** (read-only is enough — the connector never writes to EspoCRM). 6. Save, then copy the generated **API Key**. You will paste it into Odoo in Step 2. Keep the key somewhere safe; EspoCRM shows it on the API user record, but treat it like a password. ## Step 2 — Create the connection in Odoo Open **EspoCRM Connector ▸ Configuration ▸ Instances** and create a record. The fields that matter: | Field | What to enter | |---|---| | Name | A label for this server, e.g. *Production EspoCRM*. Must be unique per company. | | Company | The Odoo company that owns the imported records. | | Base URL | Your EspoCRM server root, e.g. `https://crm.example.com`. For the bundled demo, use `/espocrm/mock_api`. | | Authentication | **API Key** — the only supported method. The key is sent as the `X-Api-Key` header. | | API Key | The key from Step 1 (visible to administrators only). | | API Path | The REST path. Leave the default `api/v1` unless your server differs. | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach an EspoCRM on localhost or a private network (this lowers the SSRF guard — see *Safety features*). | | Timeout (s) | HTTP timeout per request. Default `30`. | Then click **Test Connection**. The connector calls a lightweight EspoCRM endpoint (`App/user`, falling back to a one-row `Account` list) and, on success, sets the state to **Connected** and seeds default Lead/Opportunity status mappings. If it fails, the exact error is stored on the form (**last connection error**) and recorded in **Logs** (see *Troubleshooting*). Tip: use **Quick Setup** (button on the instance) to pick a use case and apply a recommended set of auto-import toggles and a safety profile in one step. ## Step 3 — First import (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead of creating or changing Odoo records they produce **Validation Results** you can review under **EspoCRM Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, open the **Import** tab and click **Queue Account Import** (and/or **Queue Contact Import**, **Queue Lead Import**, **Queue Opportunity Import**). This enqueues jobs; it does not block the UI. 2. Jobs are processed by the **Espocrm Queue Processor** scheduled action (every minute), or immediately if you trigger it from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land in the standard Odoo apps: - Accounts and Contacts → **Contacts** (companies and individuals). - Leads and Opportunities → **CRM** (the pipeline; Leads as type *Lead*, Opportunities as type *Opportunity*). Each imported record is tracked by a **binder** keyed on the EspoCRM entity and remote id, so re-imports update the same Odoo record instead of duplicating it. ## Field mapping & customization - **Field Mappings** (**Configuration ▸ Field Mappings**) map EspoCRM source fields to Odoo target fields, per EspoCRM entity, in the **EspoCRM → Odoo** direction. Click **Suggest Mappings** on the instance to seed the business-critical defaults, then adjust. Out of the box the suggested mappings include, for example: | Entity | EspoCRM source path | Odoo target field | |---|---|---| | Account | `name` / `website` / `emailAddress` / `phoneNumber` | `name` / `website` / `email` / `phone` | | Contact | `name` / `emailAddress` / `phoneNumber` | `name` / `email` / `phone` | | Lead | `name` / `emailAddress` / `accountName` | `name` / `email_from` / `partner_name` | | Opportunity | `name` / `amount` / `probability` | `name` / `expected_revenue` / `probability` | - **Per-field transforms**: each mapping can coerce values (String, Integer, Float, Boolean, Uppercase/Lowercase, JSON String, CSV Join, First Item) or run a safe Python **Expression** (variables `value` and `default_value`). A **Preview** shows the result on a sample input before you save. - **Schema Fields** (**Configuration ▸ Schema Fields**) lists the discovered EspoCRM fields per entity alongside the Odoo target fields. Click **Schema Introspection** on the instance to refresh it; the connector samples a live record (or the bundled mock when the API is unreachable) and flattens its shape. - **Suggest Mappings** vs **Schema Introspection**: run introspection first if you want the *intelligent* suggestions (name/type similarity scored) rather than only the core blueprint set. ## Automation (scheduled actions) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | Espocrm Queue Processor | every 1 min | Processes queued import jobs. | | Espocrm Reconciliation | every 15 min | Pulls recent remote records for entities with auto-reconcile enabled and re-queues any that are missing or previously failed. | | Espocrm Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs (per the retention settings). | | Espocrm Flow Scheduler | every 5 min | Runs scheduled import flows. | | Espocrm Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | Espocrm Auto Recover | every 15 min | Reopens a tripped circuit breaker once the cooldown passes and no blocking validation issues remain. | Turn on **Auto-import** / **Auto-reconcile** per entity on the instance (Accounts, Contacts, Leads, Opportunities) to let the scheduled actions keep things in sync hands-free. **Webhooks (not used in this version).** EspoCRM can emit outbound webhooks, but wiring them into the import queue is a roadmap item. A route at `/espocrm/webhook` exists only so that existing subscriptions or health checks do not return 404 — it acknowledges with HTTP 200 and **imports nothing**. Do not rely on it to bring in data; use the scheduled import and reconciliation actions instead. ## Safety features - **Dry-run mode** (`dry_run_mode`, ON by default) — import jobs simulate writes and produce Validation Results to review before you go live. - **Pre-flight validation** (`validation_enabled`) with **Minimal / Standard / Strict** business-validation profiles — gates risky writes; *Standard* is the default. Use **Run Validation Sweep** to validate pending/failed jobs on demand, and **Auto-fix Open Issues** to clear the ones the connector can resolve. - **Resilient queue** — every import is a job with retry and configurable back-off (fixed or exponential), plus a dead-letter state for jobs that exhaust their retries. - **Circuit breaker** (`circuit_state`: *Open / Protected / Tripped*) — after repeated failures (default threshold **5**), or immediately on an auth/configuration error, the instance auto-pauses (*Tripped*). The **Espocrm Auto Recover** action reopens it after a 30-minute cooldown, or click **Resume Sync**. - **Rollback snapshots** (`rollback_enabled`) — imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots**. - **SSRF guard** — the connector refuses internal/loopback/private/reserved hosts (including cloud metadata addresses) for the Base URL unless **Allow internal host** is explicitly enabled. - **Safety profiles** — *Conservative / Balanced / Aggressive* presets set dry-run, validation, batch size and delete strategy together; click **Apply Safety Profile**. - **Credential isolation** — the API key and webhook secret live in system-only fields, readable and editable only by administrators. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with 401/403 | Wrong or missing API key, or the API user lacks read access. Re-check Step 1 and the **API Key** field; confirm the API user's role grants read on Accounts/Contacts/ Leads/Opportunities. | | "Base URL is not allowed … non-public address" | The Base URL points at localhost or a private/reserved IP. For a self-hosted server, enable **Allow internal host** on the instance (test/self-hosted only). | | SSL errors on Test Connection | Self-signed or invalid certificate. Use a valid cert, or turn off **Verify SSL** for testing only. | | "EspoCRM resource not found" / 404 | Wrong **API Path** or Base URL. The default path is `api/v1`; the connector calls `/api/v1/`. Confirm the server root has no trailing path. | | "EspoCRM rate limit reached" (429) | The server is throttling. The client retries with back-off automatically; lower the per-entity **import limit** or raise **API retry** settings if it persists. | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Confirm the **Espocrm Queue Processor** scheduled action is active and the instance is not **Paused**. | | Instance shows *Tripped* | The circuit breaker tripped after repeated failures (or an auth error). Fix the server/credentials; **Auto Recover** reopens it after cooldown, or click **Resume Sync**. | | Nothing is written after Import | You are in **Dry-run**. Review **Validation Results**, then turn dry-run off and re-run the import. | | Records imported twice | Imports are keyed by EspoCRM entity + remote id via the binder, so this should not happen. If it does, check that two instances do not point at the same EspoCRM server. | | Jobs end up *Dead* | They exhausted their retries. Open **Operations ▸ Queue Jobs** (Dead Letter), read the error on the job and in **Logs**, fix the cause, then re-queue. | For anything else, **Operations ▸ Logs** records every API call, payload and error with a timestamp. ## Frequently asked questions **Does this connector write anything back to EspoCRM?** No. It is read-only and import-first: it lists and fetches records from EspoCRM and creates or updates the matching Odoo records. Write-back to EspoCRM is a roadmap item. **Which versions are supported?** Odoo 20.0 on the Odoo side. On the EspoCRM side the connector uses only the generic `/api/v1` list and single-record endpoints, so it is version-tolerant. Validate your exact build with the bundled mock first. **Do I need an EspoCRM server to evaluate it?** No. A mock EspoCRM REST API ships inside the module. Point the Base URL at `/espocrm/mock_api` (API Path `api/v1`, any API key) to run the full import flow against sample data. **How does authentication work?** A single API key sent as the `X-Api-Key` header. Create an API user with API-Key authentication under **Administration ▸ API Users** in EspoCRM. No OAuth, no tokens. **Where do Leads and Opportunities end up?** Both land in the **CRM** pipeline as `crm.lead` records — Leads as type *Lead*, Opportunities as type *Opportunity* (with amount, close date, probability and the raw EspoCRM stage). **Is it safe to run against production data?** Yes. Dry-run is ON by default, validation gates risky writes, and rollback snapshots let you undo a batch. You decide when to go live. **Does it support real-time webhooks?** Not in this version. Real-time inbound webhooks are a roadmap item; the `/espocrm/webhook` route exists only to avoid 404s and imports nothing. Use the scheduled import and reconciliation actions instead. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - The connector **reads** Accounts, Contacts, Leads and Opportunities from your EspoCRM server and **writes** the corresponding Odoo `res.partner` and `crm.lead` records. It never writes, updates or deletes anything in EspoCRM. - Credentials (API key, webhook secret) are stored in administrator-only fields. - **In scope today**: read-only import of the four entities, per-field mapping with transforms, schema introspection, dry-run + validation + rollback, a resilient queue, and scheduled import/reconciliation. - **Out of scope / roadmap**: write-back to EspoCRM, inbound webhooks (the route is a no-op), and incremental `modifiedAt` filtering (v1 lists by `maxSize`/`offset`). ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / EspoCRM REST `api/v1` line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_espocrm_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (20 models, prefixed `espocrm.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ### What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.0.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Sync EspoCRM accounts, contacts, leads & opportunities into Odoo 20 over the REST API. - Now with a live Sync Control Tower, bulk dead-letter & conflict consoles and a field-mapping studio. Read-only against EspoCRM, resilient queue, dry-run safety, auditable source. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **61 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Account and contact bindings do not collide. - Lock claims pending then refuses second claim. - Action domain selects only conflicts. - Action domain selects only dead. - Custom admin path is honoured. Tested for real against EspoCRM 9.3: 3 accounts, 5 contacts, 4 leads and 3 opportunities came into Odoo with 0 errors and every binding accounted for. The complete list is in the source you receive. --- # Expiry Reminders for Contracts, Licenses & Documents > Track contracts, licences, certificates and insurance by expiry date, with automatic reminders, a colour-coded board and a renewals calendar. - **What it is:** Track contracts, licences, certificates and insurance by expiry date, with automatic reminders, a colour-coded board and a renewals calendar. - **For:** Extra Tools - **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:** 79 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_expiry_reminder - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Show documents to a plain employee - they see none unless named on one.; Delete the paper trail: reminders sent stay on record. - **Manual:** https://docs.bambooforge.dev/bambooforge_expiry_reminder/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_expiry_reminder.expiry_document_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_expiry_reminder - **Manual last updated:** 2026-10-15 --- # Expiry Reminders for Contracts, Licenses & Documents Never miss a renewal again. Register any document with an expiry date — contracts, licenses, certificates, insurance, warranties — and the app reminds the responsible person automatically before it lapses, with a colour-coded board, a renewals calendar and one-click renewal. This page is the complete manual. Follow it top to bottom and you can install, register documents, read the board and fix the common issues **without contacting support**. ## Overview Compliance and renewals slip through the cracks because nobody is watching the dates. This app watches them for you: - **Register any expiring document** with a type, a related party, a responsible person, an expiry date and a lead time. - **Automatic status** — Valid, Expiring Soon or Expired, computed from the expiry date and the lead time. - **Reminders before expiry** — an activity for the responsible person and an email, fired automatically once the document enters its lead-time window. - **A board and a calendar** — a kanban grouped by status and a month calendar of upcoming expiries. - **One-click renewal** — roll a document forward a year, keeping the history. ## Requirements - Odoo 20.0. The app only depends on **Discuss** (`mail`), which is part of every Odoo install. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_expiry_reminder` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **Expiry Reminders** and click *Install*. A **Renewals** app appears with a starter set of document types (Contract, License, Certificate, Insurance, Warranty, Other). ## Quick start 1. Open **Renewals > Documents** and click *New*. 2. Enter a name, choose a **Type**, set the **Expiry Date** and, if needed, adjust the **Lead Time (days)** (it defaults from the type). 3. Set the **Responsible** person and optionally a **Related Party**. 4. Save. The document gets a status automatically and appears on the board and the calendar. When it enters its lead-time window, the responsible person is reminded. ## The board and calendar **Renewals > Documents** opens a kanban grouped by **Valid / Expiring Soon / Expired**, each card showing the type, expiry date and days remaining. Switch to the **list** for a sortable table (overdue rows in red, expiring in orange) or the **calendar** to see expiries laid out by month. **Renewals > Expiring & Expired** is a focused worklist of everything that needs attention now. ## How reminders work Each document has a **Lead Time** — the number of days before expiry to start reminding. A daily scheduled action recomputes every document's status and, the first time a document is **Expiring Soon** or **Expired**, it: - schedules a *Renew* activity for the responsible person, dated to the expiry, - sends them the reminder email, and marks the reminder as sent so it does not repeat. Everything is logged on the document's chatter. The scheduled action **Expiry Reminders: Check & Notify** ships **disabled** — enable it under *Settings > Technical > Scheduled Actions* to run it daily. ## Renewing a document When a document is renewed, open it and click **Renew**. The app creates a new document starting at the old expiry date and running one year (adjust as needed), links the two together, and archives the old one — so your history stays intact and the new period is tracked from day one. ## Document types Under **Renewals > Configuration > Document Types** you can rename the types, set each one's **Default Lead Time** and colour, or add your own (e.g. *Safety Inspection*, *Domain Name*, *Visa*). ## Frequently asked questions ****Statuses look stale.**** Status is recomputed by the daily scheduled action and whenever a document is edited. Enable **Expiry Reminders: Check & Notify** so it refreshes every day. ****The reminder went to the wrong person.**** Reminders go to the document's **Responsible** user. Set it on each document. ****I was reminded once but want another nudge.**** Reminders fire once per document to avoid noise. Re-trigger by clearing the reminder (it resets automatically on renewal), or rely on the scheduled *Renew* activity. ****Does it work with multiple companies?**** Yes. Documents are company-scoped via a record rule. ## How we make sure it works Before every release, **25 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The user cannot delete the paper trail. - The user reads types and touches nothing in them. - A plain employee sees no documents at all. - An empty contact falls back to odoo 20's company timezone. - The email less user registers and renews a document. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## Usage Everything happens under the **Renewals** app, which appears in the main menu after installation with a starter set of document types (Contract, License / Permit, Certificate, Insurance, Warranty, Other). ### Register a document 1. Go to **Renewals > Documents** and click *New*. 2. Enter a **Name** and pick a **Type**. Choosing a type fills the **Lead Time (days)** from that type's *Default Lead Time* — for example *Contract* sets 45 days and *Warranty* sets 15. Override it per document if needed. 3. Set the **Expiry Date** (required) and, optionally, a **Start Date**, a **Reference**, a **Related Party** and free-text **Notes**. 4. The **Responsible** defaults to you; change it to the person who should be reminded and who owns the renewal. 5. Save. On save the status is computed automatically from the expiry date and lead time: **Valid** while more than the lead time remains, **Expiring Soon** once inside the lead-time window, and **Expired** once the date has passed. **Days to Expiry** counts down and turns negative after expiry. ### Read the board and calendar **Renewals > Documents** opens as a kanban grouped by status, colour-coded by document type (see `screenshot_kanban.png`). Switch views from the top-right: a **List** where expired rows show in red and expiring rows in orange (`screenshot_list.png`), or a **Calendar** month view of expiry dates coloured by type (`screenshot_calendar.png`). Use the search filters *Expiring Soon*, *Expired*, *My Documents* and *Renewed (archived)*, or group by Status, Type or Responsible. **Renewals > Expiring & Expired** is a pre-filtered worklist of everything needing attention now. ### How the scheduled action reminds people The cron **Expiry Reminders: Check & Notify** (*Settings > Technical > Scheduled Actions*) runs the automatic notifications. It ships **inactive**, so enable it once and set it to run daily (its default interval is every 1 day). Each run recomputes every active document's status, then finds documents that are **Expiring Soon** or **Expired** and not yet reminded (**Reminder Sent** unchecked). For each, once only, it schedules a *Renew: * to-do activity for the **Responsible** user dated to the expiry date, sends that user the *Document Expiry Reminder* email (if they have an email address), ticks **Reminder Sent**, stamps **Last Reminder** and logs a chatter note. Because the flag is set, a document is never reminded twice. To acknowledge a due document manually and stop the cron from notifying about it, use the **Mark Reminded** button on the form (shown while a document is not yet valid and not yet reminded); it ticks **Reminder Sent** and stamps **Last Reminder**. ### Renew a document When a document is up for renewal, open it and click **Renew** in the header. The app creates a fresh document that starts on the old expiry date and runs one year forward, links the pair (**Renewed From** / **Renewed To**), and archives the original — its history and chatter stay intact, and the new period is tracked with its reminder flag cleared from day one. The **Renew** button disappears once a document has already been renewed. ### Worked example A workshop registers its liability insurance: Type *Insurance*, Expiry Date *31/12/2026*, Lead Time *30* days, Responsible *the office manager*. It stays **Valid** until 1 December 2026, when it becomes **Expiring Soon**. On the next cron run the office manager gets a *Renew: Liability Insurance* activity dated 31 December plus the reminder email, and the chatter records "Expiry reminder sent". After the new policy is agreed, clicking **Renew** produces the 2027 document (valid to 31/12/2027) and archives the 2026 one. ### Configure document types Managers can maintain the catalogue under **Renewals > Configuration > Document Types** (see `screenshot_types.png`): rename types, set each one's **Default Lead Time** and colour, add your own (e.g. *Safety Inspection*, *Visa*, *Domain Name*), or open a type to view its documents via the *document count*. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_expiry_reminder -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (2 models, prefixed `expiry.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - The security groups this module defined (user assignments to them are dropped). ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Show documents to a plain employee - they see none unless named on one. - Delete the paper trail: reminders sent stay on record. ## 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 **Renewals** menu (Documents, Expiring & Expired, 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. ## Changelog ### 20.0.1.2.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Track contracts, licenses, certificates, insurance and any document with an expiry date. Get reminded before they lapse with automatic activities and emails, a colour-coded board and a renewals calendar. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # E-Invoicing All in One for Factur-X and ZUGFeRD > Decide which Factur-X or ZUGFeRD profile each customer's invoice PDF claims to be, and check the invoice really carries what it promises. - **What it is:** Decide which Factur-X or ZUGFeRD profile each customer's invoice PDF claims to be, and check the invoice really carries what it promises. - **For:** Accounting - **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:** 318 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_facturx - **Not for:** The profile is recorded and checked here; producing the embedded file is Odoo's own job.; EXTENDED has more optional content than is checked. What is checked is what causes rejections. - **Manual:** https://docs.bambooforge.dev/bambooforge_facturx/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-account.action_move_out_invoice_type - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_facturx - **Manual last updated:** 2026-10-15 --- # E-Invoicing All in One for Factur-X and ZUGFeRD ## What this is for Odoo silently embeds a Factur-X file inside every invoice PDF. Which **profile** that file claims to be is not a detail — each profile is a different promise about what the document contains, and a receiver expecting one will reject another. ## The five profiles - **MINIMUM** and **BASIC WL** carry no lines. They are accounting summaries. France and Germany do not accept them as an invoice for VAT purposes, which is usually discovered by the customer's accountant months later. - **BASIC**, **EN 16931** and **EXTENDED** promise a quantity, a unit price and a tax category on every line. A file that promises them and does not carry them fails on parse. EN 16931 is the safe default: it is the European semantic standard, and every receiver understands it. ## Where the profile lives Agreed **per customer** (their system, their expectation), defaulted **per company** on the company record, and overridable **per invoice** for the occasional exception. ## What gets checked - no profile agreed with this customer — a warning, because the default may be perfectly fine; - MINIMUM used for a real invoice — a warning, with the VAT consequence spelled out; - a line-level profile chosen while the lines are thin — blocking, naming the line and what it is missing; - a tax line in a different currency from the invoice — blocking. Factur-X carries one tax currency, and when they differ the receiver's parser reads the wrong number. Findings appear in the same pre-flight list as the European rules. ## Known limits - The profile is recorded and checked here; producing the embedded file is Odoo's own job. - EXTENDED has more optional content than is checked. What is checked is what causes rejections. ## How we make sure it works Before every release, **9 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A summary profile does not demand line detail. - A complete invoice on en16931 passes the profile rules. - A line profile demands quantity price and tax. - The customer profile wins over the company one. - The profile can be overridden on one invoice. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_facturx` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Factur-X & ZUGFeRD Profiles**. ## 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 Web; einvoice (BambooForge) must be installed first app. No extra Python package. ****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. --- # Farm & Crop Management for Agriculture > Farm and crop management: cycles per field, a spray record an inspector can read, withholding periods that stop a harvest, cost per kilo. - **What it is:** Farm and crop management: cycles per field, a spray record an inspector can read, withholding periods that stop a harvest, cost per kilo. - **For:** Industries - **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:** 119 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_farm - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Count planned work: a spray planned for next week holds nothing back - only signed-off work counts.; Sign off an operation that names no product, or record work on a harvested or cancelled cycle unless marked as a late entry. - **Manual:** https://docs.bambooforge.dev/bambooforge_farm/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_farm.action_bf_crop_cycle - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_farm - **Manual last updated:** 2026-10-15 --- # Farm & Crop Management for Agriculture Built around the two things a farm gets audited on and the one number it never knows. ## The withholding period - Every input carries a **withholding period** in days and the registration number off its label. Zero for a fertiliser; never zero for a plant protection product unless the label really says so. - Signing off a spray sets an **earliest legal harvest date** on the crop cycle, taken from the longest period among the products applied. The cycle names the spray that is holding it. - Harvesting before that date is **refused**, and the refusal names the product, the day it was applied and the number of days. Cutting inside a withholding period is a rejected load, a failed residue test and, in most places, an offence. - The harvest date itself is **bounded**: not in the future, not before the crop went in the ground. A date in the future clears every withholding period ever recorded, which is the one keystroke that would walk a load straight out of the field. - A spray **planned for next week** holds nothing back - only signed-off work counts. A spray still sitting as *planned* and **dated before the harvest** is a different animal: it is a withholding period nobody counted, and the harvest is refused until somebody says whether it happened. - Work cannot be recorded or signed off on a cycle that has been **harvested, written off or cancelled** unless it is marked as a late entry. When a late entry proves the crop was cut inside a withholding period, a **red flag** is written on the cycle naming the day it was cut and the day it could legally have been. ## The spray record - Product, batch, registration number, quantity, the **rate that actually went on per hectare**, the operator, the wind and the weather. - An operation that says something was applied but **names no product** cannot be signed off. It is the entry an inspector reads first. - A pesticide, herbicide or fungicide cannot be applied by an operator with **no spray licence** on file, and not with one that had **expired on the day**. The rule follows the product, not the box somebody ticked: a herbicide recorded under *Weeding* is still a herbicide and still needs a licensed operator. - The withholding period and the registration are **copied onto the line as it is applied**, not read through a relation, and they are never typed in - anything a client, an import or an RPC call sends for them is dropped and re-read from the label. Correct a line from a fertiliser to a pesticide and it becomes a pesticide, period and all. If the label changes next year, what happened this year stays what happened this year. - A signed entry is **locked**: its date, kind, operator, products, area, hours, rates and wind are what happened. Its state moves only through *Done* and *Cancel*, so it cannot be written back to cancelled and take its withholding period and its cost with it, and it cannot be repointed at another crop. Notes and the weather stay editable, because a note is not the record. - **Who signed it off** is recorded from the session, next to the operator somebody typed in. - A signed entry cannot be **deleted**, and neither can a cycle that carries one or that has been harvested or written off. Corrections are new entries; the field record is not something you tidy up. ## The costing - Inputs, labour and machinery roll up from signed-off operations onto the cycle: **total, per hectare and per unit harvested**. - The yield is measured against the **expected yield for that crop**, so a season ends with a variance rather than a feeling. - A crop **written off** keeps its cost. The money was spent either way, and a failed field that shows zero teaches nobody anything. ## The land - Live cycles on a field cannot add up to **more than its area**, and an operation cannot cover more hectares than were planted. The rule is checked from both ends: a field's area cannot be **corrected downwards** under the crops already standing on it, and the parcel is locked while it is checked, so two cycles entered at the same moment on two screens cannot both fit on ground that holds one of them. - Harvesting, cancelling or writing off a cycle **frees the ground** again. - Every cycle ever grown on a parcel stays on it - what went in, what it cost, what came off. Which is what the rotation, a buyer and an organic inspector all ask for. ## The season - A crop cycle runs *planned -> growing -> harvested*, or is written off, or is cancelled while nothing has been done to it. - The harvest is recorded through a short form that insists on a **weight** that is a weight and a moisture that is a percentage, and warns on screen while a spray - or a date in the future - stands in the way. - Crops **coming due within a week** - and crops still held by a spray - land on the list of **whoever planned the cycle**, or of a manager if they have left the farm. Once each, even after the reminder has been dealt with, and the date on it follows the hold every time another spray is signed off. ## Roles - **Field operator** - records what was done in the field and what was applied. Cannot plan a season, harvest, or configure. - **Farm manager** - plans the season, opens and closes cycles, records the harvest, and configures farms, fields, crops and inputs. ## Installation 1. Copy `bambooforge_farm` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. Community only; it depends on `base`, `base_setup`, `mail`, `product` and `uom`. 2. Install **Farm & Crop Management for Agriculture**. 3. Create the farms, then the fields with their real measured area. 4. Create the crops with their unit and expected yield. 5. Create the inputs with their withholding days and registration numbers. 6. Put spray licence numbers on the operators' user records before the first spray, or the first spray cannot be signed off. ## How we make sure it works Before every release, **128 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A spray cannot be signed off on a written off cycle. - A cycle cannot be planted on another company's field. - A spray that names no product cannot be signed off. - A cycle with work on it cannot be cancelled. - A signed entry cannot be repointed at another cycle. 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: - Count planned work: a spray planned for next week holds nothing back - only signed-off work counts. - Sign off an operation that names no product, or record work on a harvested or cancelled cycle unless marked as a late entry. ## 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, Products, Settings, Units of Measure apps. No extra Python package. ****Where does it show up?**** Under the **Farm** menu (Crop cycles, Field record, Fields, 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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) --- # Field Sales and Distribution All in One with Routes and GPS > Beat plans, GPS check-in inside the shop's fence, shelf count, stamped photos, orders on the phone, check-out - and a route compliance report the supervisor reads on Monday. What did not add up is flagged on the visit: a check-in far from the shop, a different phone, two minutes in the shop, a visit nobody planned. - **What it is:** Beat plans, GPS check-in inside the shop's fence, shelf count, stamped photos, orders on the phone, check-out - and a route compliance report the supervisor reads on Monday. What did not add up is flagged on the visit: a check-in far from the shop, a different phone, two minutes in the shop, a visit nobody planned. - **For:** Sales - **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:** 349 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_field_sales - **Not for:** Without a signal the phone keeps what was done for up to three days; clearing the browser's storage before the signal returns loses it.; The position is what the browser reports. A rogue app that fakes the phone's position cannot be detected from a browser; the fence, the accuracy, the phone and the times are the evidence, and they are for a supervisor to weigh.; Trade promotions, distributor stock and van sales are separate add-ons.; Kilometres are straight lines between check-ins, not a route track.; Nearest first orders by straight-line distance, not by road; Google Maps takes ten stops in one trip, so a longer round opens in parts.; A collection is a record of money received until it is posted; posting needs a journal per method and invoicing rights. The receipt is not a fiscal document.; A return's credit note is left in draft for the accountant. Returned goods are not received into a warehouse by this app (the Van Sales add-on takes them onto the van). - **Manual:** https://docs.bambooforge.dev/bambooforge_field_sales/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_field_sales.bf_fs_day_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_field_sales - **Manual last updated:** 2026-10-15 --- # Field Sales and Distribution All in One with Routes and GPS A rep who calls on forty shops a week runs on habit and memory, and the office finds out what happened at the end of the month. This app gives the rep a day and the supervisor a record: routes, a phone screen for the six steps in the shop, orders that are real sales orders, and a route compliance report with everything that did not add up flagged on the visit. ## What you get - **Outlets**: contacts with a channel, a class, a territory, a pin on the map, a check-in fence, a credit limit, an assortment. - **Routes**: a rep's day - the outlets in the order they are driven, on a weekday, every week, two or four. Tonight's job plans tomorrow. - **My Day** on the phone: the visits in order, targets against the day so far, and the six steps at each shop - the shop's card, check in, shelf count, stamped photo, order, check out. A shop not on the route can be added and is marked as such. - **Flags, not walls**: a check-in outside the fence, poor GPS accuracy, a phone that is not the rep's usual one, a visit outside working hours, under two minutes in the shop, a visit nobody planned - each written on the visit for the supervisor. - **Route compliance**: one line per rep per day - planned, visited, skipped, missed, off-route, compliance, strike rate, orders, revenue, minutes in shops, kilometres between check-ins, flags, targets. - **Odoo's own sales orders**, on the outlet's pricelist, linked to the visit and the route. - **Money and goods at the counter**: collect cash, a cheque or a transfer reference against the shop's open invoices and print an 80 mm receipt; take back damaged, expired, unsold goods and empties as a draft credit note at the price the shop paid. - **Cases and units**: a product sold by the case of 6, 12 or 24 is ordered by the case, by the unit or both. - **The month and the focus products**: revenue against the rep's monthly target, and how many shops bought the products pushed this month. - **Surveys**: a few questions put to every shop of a channel - yes/no, numbers, one of a list, text - analysed in a pivot. - **The route in order**: the shops left, nearest first from where the rep stands, and the whole round as one Google Maps trip. - **Works without a signal**: check-in, shelf count, photo, order, money, returns, answers, check-out and skip are kept on the phone and sent when the signal comes back, with the time they happened; products are searched in a catalogue kept on the phone. ![My Day on the phone: the route, the numbers, the next shop](phone.png) ![The visit: the shop's card, the shelf count, the photo, the order](visit.png) ## Requirements - **Odoo**: 20.0, Community or Enterprise, with **Sales** installed. - A phone with a browser and, for check-ins, location enabled. Odoo must be served over HTTPS for the browser to give its position. - **Access**: a **Rep** sees their own routes, visits and days and takes orders; a **Supervisor** sets up territories, outlets and routes, sees every rep, reopens visits. Reps need the Sales *User: Own Documents Only* right to create orders. ## Install 1. Copy `bambooforge_field_sales` into your addons path and restart Odoo. 2. *Apps ▸ Update Apps List*, search for *Field Sales*, **Activate**. 3. *Field Sales ▸ Configuration*: territories and channels. Then outlets, then routes. Give each rep the Rep group and targets on their user form. ## Setting up **Outlets.** Any contact with *Is an Outlet* ticked, on its *Field Sales* tab: channel, class (A, B, C), territory, **latitude and longitude** (the pin the check-in is measured against; the Geolocalize module fills them from the address), the **check-in fence** in metres, a **credit limit** (0: none), and the **assortment** - the products the shelf count asks about. **Routes.** A name, the rep, the weekday, the rhythm (every week, every two weeks, every four weeks, counted from a date), and the stops in driving order. *Plan Today* creates today's visits at once; the nightly job plans every route for tomorrow and closes yesterday's numbers. **Reps.** On the user form, *Field Sales* tab: targets a day (visits, revenue) and the rep's usual phone, set at their first check-in - *Forget this phone* when they get a new one. **Monthly targets.** *Configuration ▸ Monthly Targets*: per rep and month, a revenue target, the month's **focus products** and how many shops should buy one of them. Any day of the month may be typed; the month is kept. **Cases.** On the product, *Units per Case*: above 1, the phone offers the product by the case and by the unit. The order and the stock count units. **Collections and returns.** *Configuration ▸ Collections and Returns*: the journal for cash, for cheques and for transfers; whether a collection is **posted at once** or waits for the office; whether a return makes a **credit note**. **Surveys.** *Configuration ▸ Surveys*: a name, the dates it runs, the channels it is for (none: every shop), and the questions - yes/no, a number, one of a list (the answers separated by commas), or text; a question may be required. ## The rep's day *Field Sales ▸ My Day* on the phone. The top shows visits made against the target, orders, revenue and route compliance so far; below, the visits in route order, done ones greyed. A shop not on the route: search it at the bottom and it is added, marked *off route*. The top also shows **the month**: revenue against the monthly target, and the focus products with the number of shops that bought each. **Nearest first** reorders the shops still to visit from where the phone is (the last check-in when the phone gives no position); **Route in Maps** opens the next ten as one Google Maps trip. At a shop: 1. **The shop** - channel, phone, open balance, credit limit, last visit, last order. The arrow at the top opens navigation to the pin. 2. **Check in** - the phone's position is measured against the pin; farther than the fence (plus the GPS accuracy) is flagged, not refused. *Cannot visit today* skips the shop with a reason. 3. **On the shelf** - a count for each product of the assortment. 4. **Shelf photo** - stamped with the shop, the time, the rep and the position, so that a picture from another day is not this day's. 5. **Order** - *Same as last time* copies the last confirmed order's lines; or add products one by one - a star marks this month's focus products, and a product sold by the case gets a *cs* and a *un* box. The order is a quotation on the outlet's pricelist, linked to the visit; if the outlet's open balance plus the order passes its credit limit, the screen says so - and the order is still taken, for the office to decide. 6. **Collect** - the shop's open invoices, oldest due first, late ones in red. Tick the ones being paid (the amount fills in), or type any amount; cash, cheque (with its number) or transfer (with the reference). *Money received* records it and offers the **receipt**, an 80 mm slip for a mobile printer or a PDF to show. The office posts it as a payment in the method's journal, against the invoices ticked - or the shop's oldest - one invoice after the other; more than is owed stays on the account as an advance. With *Post Collections at Once* it is posted there and then. 7. **Taken back** - products from the assortment, each with a reason: damaged, expired, empties, unsold, other. *Record the return* writes a draft credit note for the shop, priced at what it last paid (net of its discount), the list price if it never bought the product. 8. **Questions** - the surveys running for the shop's channel. Answering again replaces; the phone says which required questions are still open. 9. **Check out** - with a note for next time. Under two minutes in the shop is flagged. ## Without a signal Basements, industrial estates, the countryside: the phone keeps working. - The day is kept on the phone each time it is loaded; opened without a signal, *My Day* shows it as last seen, with a band saying so. - **Check in, shelf count, photo, order, check out, skip** done without a signal are kept on the phone, in order, and shown as done. The band counts them; they are sent by themselves when the signal returns (or with *Send now*). - Each is sent with **the time it was done on the phone**, so a check-in at 9:14 in a cellar is 9:14, not the time the rep came back up. The visit is marked *Recorded Offline*. A phone time more than ten minutes in the future or more than three days old is not believed: the server's time is used and the visit is flagged *phone clock far from the server's*. - A shop not on the route can still be added without a signal when it was found before the signal dropped. Product search works without a signal too: the catalogue of products for sale is kept on the phone and refreshed once a day (up to 5,000 products; beyond that, a search that finds nothing on the phone asks the server when there is a signal). - Collections, returns and survey answers are kept like the rest; the receipt is printed once they have reached the server. - If an action cannot be applied when it arrives - the visit was reopened meanwhile, for instance - the phone says which one and why; the others are applied. - What waits is kept in the browser's database (IndexedDB), not in its small key store: a morning of shelf photos fits. Photos are scaled to 1600 px and saved as JPEG on the phone before they are sent or kept, a few hundred kilobytes instead of several megabytes. Should the phone's storage be full all the same, the rep is told to keep the screen open until the signal is back. - Every action kept on the phone carries its own key. If the signal drops after the server did the work but before the phone heard back, the phone sends it again and the server answers without doing it twice: no second order, no second shop. ## Add-ons Three add-ons build on this app, each on its own: - **Trade Schemes and Display Programmes** - promotions applied to the rep's order with a budget, and display programmes proved by photos. - **Distributors** - the shop's order becomes the distributor's to fill; the distributor's stock, cover and replenishment. - **Van Sales** - load, sell and deliver from the van, collect cash, close the day. ## What the supervisor sees **Visits**: every visit with check-in, minutes, distance from the outlet, orders and flags; filters for today, flagged, off-route, with an order; group by rep, day, outlet. A supervisor can **reopen** a skipped or checked-out visit. **Route Compliance**: a line per rep per day. Planned is the route; visited, skipped and missed add up to it; off-route visits are counted apart. Compliance is planned visits made over planned; strike rate is visits with an order over visits made. Kilometres are the straight lines between consecutive check-ins - not a GPS track, a plausibility figure. Pivot by week and territory; *Compliance Under 80%* filters the days to talk about. **Collections**: what reps took, *To Post* by default, by rep, method and day; *Post the Payment* (invoicing rights) and *Print Receipt*. **Returns**: each with its credit note; *Analysis ▸ Returned Goods* pivots products by reason. **Survey Answers**: a pivot of answers per question; numbers and yes/no also average. **Flags** on a visit: outside the fence, poor GPS accuracy (over 300 m), a different phone, outside working hours (before 6 or after 22 in the rep's timezone), no position sent, under two minutes, not on the day's route. ## Limits, stated plainly - Without a signal the phone keeps what was done for up to three days; clearing the browser's storage before the signal returns loses it. - The position is what the browser reports. A rogue app that fakes the phone's position cannot be detected from a browser; the fence, the accuracy, the phone and the times are the evidence, and they are for a supervisor to weigh. - Trade promotions, distributor stock and van sales are separate add-ons. - Kilometres are straight lines between check-ins, not a route track. - *Nearest first* orders by straight-line distance, not by road; Google Maps takes ten stops in one trip, so a longer round opens in parts. - A collection is a record of money received until it is posted; posting needs a journal per method and invoicing rights. The receipt is not a fiscal document. - A return's credit note is left in draft for the accountant. Returned goods are not received into a warehouse by this app (the Van Sales add-on takes them onto the van). ## Uninstalling Uninstalling removes territories, channels, routes, visits, shelf counts, photos and rep days, and the outlet fields on contacts. Sales orders stay, without their link to the visit. ## Changelog ### 20.0.1.2.0 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.2.0 - Collections on the route against open invoices, with an 80 mm receipt and posting to a journal per method. - Returns, damaged goods and empties at the visit, as a draft credit note. - Sell by the case; monthly targets and focus products on the phone; surveys; nearest-first route order and a Google Maps trip; the product catalogue kept on the phone for searching without a signal. ### 20.0.1.1.1 - The offline queue moved to IndexedDB (localStorage overflowed after a few photos and the queue was lost); photos scaled on the phone. - An action sent twice is done once. ### 20.0.1.1.0 - Works without a signal: actions kept on the phone and sent with their own time; the day cached on the phone; *Recorded Offline* on the visit; a *phone clock* flag. - Hooks for the Trade Schemes, Distributors and Van Sales add-ons. ### 20.0.1.0.0 First release. ## How we make sure it works Before every release, **24 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - What does not add up is flagged not refused. - Returns without credit note. - Returns at last sold price become a credit note. - A queue sent twice is done once. - Collect refuses nonsense and needs a journal. The complete list is in the source you receive. --- # Field Sales Distributors: Secondary Sales and Distributor Stock > The rep takes the shop's order, the distributor fills it: secondary orders, the distributor's stock from sell-in less sell-out corrected by the rep's count, days of cover per product, and a replenishment order suggested to top the distributor up. - **What it is:** The rep takes the shop's order, the distributor fills it: secondary orders, the distributor's stock from sell-in less sell-out corrected by the rep's count, days of cover per product, and a replenishment order suggested to top the distributor up. - **For:** Sales - **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:** 478 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_field_sales_distributor - **Not for:** The distributor's stock is a ledger kept in this database, not the distributor's own system: its accuracy is the counts' accuracy.; A secondary order is not an invoice: what the distributor bills the shop stays in the distributor's books.; The imported sell-out is taken as the distributor states it; a count at its warehouse is what corrects the ledger. - **Manual:** https://docs.bambooforge.dev/bambooforge_field_sales_distributor/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_field_sales_distributor.bf_fs_secondary_order_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_field_sales_distributor - **Manual last updated:** 2026-09-21 --- # Field Sales Distributors: Secondary Sales and Distributor Stock Most of a brand's goods reach the shops through distributors. The brand sees what it sold to them; what they sold on, and what is left in their warehouse, it guesses. This add-on to **Field Sales and Distribution** closes the loop: the rep takes the shop's order, the distributor fills it, and the distributor's stock is known product by product. ## What you get - **Distributors**: contacts marked as such, and outlets marked *served by* one. - **Secondary orders**: an outlet served by a distributor gets, from the rep's phone, an order for the distributor to fill - new, confirmed, delivered - which counts in the rep's day like any other. - **The distributor's stock**: sell-in (goods delivered to the distributor, or sales orders confirmed to it when Inventory is not installed) less sell-out (secondary orders delivered), corrected by the counts reps make at the distributor's warehouse. - **Days of cover** per product from the last thirty days of sell-out, and a **replenishment order** suggested to top the distributor up to its target. ![Secondary orders to deliver, by distributor](secondary.png) ## Requirements - **Field Sales and Distribution** 1.1 or later on the same Odoo series; Odoo 20.0, Community or Enterprise. - Reps see their own secondary orders; supervisors see all, the stock and the distributors. ## Install 1. Copy `bambooforge_field_sales_distributor` next to Field Sales, restart. 2. *Apps*, search for *Distributors*, **Activate**. ## Setting up On a contact, tick **Is a Distributor**; its *Distributor* tab holds the **target days of cover**, the distributor's margin (for reporting) and the **price list for the shops** - what the distributor charges the outlets it serves (empty: the list price). On an outlet's *Field Sales* tab, pick the distributor under **Served By**. **The distributor's own login.** Give the distributor's staff a portal user (a contact under the distributor company, *Grant portal access*): their portal home shows **Orders to Fill**. A distributor can be an outlet too (tick *Is an Outlet* and put it on a route): a rep visiting its warehouse counts its shelves with the ordinary shelf count. ## Day to day **Taking the order.** At an outlet served by a distributor the phone works as always; the order becomes a *secondary order* numbered `SEC/…` for the distributor, and the phone says so. **Filling it.** *Distributors ▸ Secondary Orders*: **Confirmed by Distributor**, then **Delivered**. Delivery takes the units out of the distributor's stock and stamps the delivery time; cancelling a delivered order puts them back. The distributor can do both itself on the portal: *Orders to Fill* lists its orders by state, each with the shop's address and lines, and the buttons **We will deliver it** and **Delivered**; the order's chatter says who pressed them. **Prices.** A secondary order is priced on the distributor's price list for the shops, or at the list price when it has none. With *Trade Schemes* installed the shop's schemes apply to it as to a direct order (see below). **Sell-in.** When the goods reach the distributor depends on what else is installed: - **With Inventory**, the small *Sell-in at Delivery* module installs by itself and the sell-in is the delivery: each validated delivery to the distributor adds what it carried - a part delivery adds the part - and a return takes it off. A confirmed order still waiting in your warehouse is not in the distributor's stock. Orders confirmed before it was installed are recounted from what they actually delivered. - **Without Inventory**, confirming a sales order to a distributor is the only sign the goods left: its units go into the distributor's stock then, and cancelling the order takes them out. Either way the ledger counts in the product's unit: two dozen on the order are 24 units. **Counting.** A shelf count at the distributor's warehouse writes the difference to the ledger as a *count correction* - only the difference. ![The distributor's stock: sell-in, sell-out and counts per product](stock.png) ## Sell-out from the distributor's own system A distributor that runs its own software sends its sales as a file. *Distributors ▸ Import Sell-out*: the distributor, and a CSV (comma, semicolon or tab) or Excel file whose first row names the columns **date**, **product** and **quantity** (*code*, *sku*, *barcode*, *qty*, *units* are understood too). The product is found by internal reference, then barcode, then exact name; dates may be written 2026-09-18, 18/09/2026 or 18.09.2026. Every line read becomes sell-out on the ledger; the lines that could not be read are listed with the reason, and nothing is imported when none could. ## Performance *Distributors ▸ Distributor Performance*: per distributor, the orders waiting, the **fill rate** (secondary orders of the last 30 days delivered, over those not cancelled), the **lead time** (hours from the rep's order to the delivery, averaged), units sold out in 30 days, units in stock, and how many products are under the target cover. ## Schemes and claims With *Trade Schemes* installed as well, the small *Schemes for Distributors* module installs by itself. A secondary order then gets the shop's schemes - percent off on its lines, free units as a line at 100%, an amount off - charged to the same budgets; free units leave the distributor's stock with the rest when it delivers. **Claim the Schemes Given** on the distributor (or *Distributors ▸ Scheme Claims*) collects every use on its delivered secondary orders not claimed yet; **Make the Credit Note** turns the claim into a credit note for the distributor, one line per scheme. A use is claimed once; cancelling a claim frees its uses for the next one. ## Cover and replenishment For each product the daily sell-out is the last thirty days' deliveries divided by thirty; days of cover is the stock divided by that. **Suggest a Replenishment Order** on the distributor creates a draft sales order for every product below its target cover, for the quantity that brings it to the target. Confirm it - and, with Inventory, deliver it - and it is sell-in. ## Limits, stated plainly - The distributor's stock is a ledger kept in this database, not the distributor's own system: its accuracy is the counts' accuracy. - A secondary order is not an invoice: what the distributor bills the shop stays in the distributor's books. - The imported sell-out is taken as the distributor states it; a count at its warehouse is what corrects the ledger. ## Uninstalling Uninstalling removes distributors' flags, secondary orders and the stock ledger. Sales orders stay. ## Changelog ### 20.0.1.1.0 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. The portal card is an Odoo 20 portal entry. ### 20.0.1.1.0 - The distributor's price list for the shops; the distributor's portal (*Orders to Fill*); sell-out imported from CSV or Excel; distributor performance - fill rate, lead time, cover; the delivery time on the secondary order. - With Trade Schemes: schemes on secondary orders and claims settled by credit note (new companion module). ### 20.0.1.0.1 - With Inventory, sell-in is written when the delivery is validated, not when the order is confirmed; returns come off (new companion module *Field Sales Distributors with Inventory*, installed by itself). - Order lines in another unit (dozens, cases) count in the product's unit. ### 20.0.1.0.0 First release. ## How we make sure it works Before every release, **8 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The shops order is the distributors to fill. - Count corrects the ledger. - The distributors price list. The complete list is in the source you receive. --- # Field Sales Trade Schemes and Display Programmes > Trade promotions for field sales: buy-ten-get-one, percent off, amount off - scoped by territory, channel, class or outlet, with a budget that stops the scheme the moment it is spent. Applied to the rep's order on the phone and written down to the cent. Display programmes approved by the supervisor and proved by the visit's photo. - **What it is:** Trade promotions for field sales: buy-ten-get-one, percent off, amount off - scoped by territory, channel, class or outlet, with a budget that stops the scheme the moment it is spent. Applied to the rep's order on the phone and written down to the cent. Display programmes approved by the supervisor and proved by the visit's photo. - **For:** Sales - **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:** 448 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_field_sales_schemes - **Not for:** With the Distributors add-on, the small Schemes for Distributors module installs by itself: schemes apply to the distributor's secondary orders too, and what the distributor gave away is claimed back as a credit note. See the Distributors manual.; Tiers are read on the qualifying lines of one order, not on a shop's purchases over a month.; The free-product reward is the same product line, not a choice of products.; Payouts of display programmes are shown, not paid: pay them the way you pay any customer credit. - **Manual:** https://docs.bambooforge.dev/bambooforge_field_sales_schemes/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_field_sales_schemes.bf_fs_scheme_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_field_sales_schemes - **Manual last updated:** 2026-09-21 --- # Field Sales Trade Schemes and Display Programmes A trade scheme is money the brand spends to make a shop buy the way it wants. Spent without a record, it is a discount nobody can account for. This add-on to **Field Sales and Distribution** gives every scheme a scope, a condition, a reward and a budget, applies it to the rep's order on the phone, and writes every use down with its cost. ## What you get - **Schemes** scoped by territory, channel, outlet class, or a named list of outlets; *once per outlet* if you say so. - **Conditions**: qualifying products, a minimum quantity, a minimum amount. - **Rewards**: percent off the qualifying lines, percent off by tier, free product for every multiple, or an amount off the order. - **Bundles**: a scheme that asks for each of several products, in its own quantity. - **Stacking under control**: a scheme marked *not combinable* stands alone on the order it applies to. - **Free goods at list or at cost**: the budget counts free units the way your finance team does. - **Budgets**: every use is written down with its cost; a scheme that would pass its budget is not applied, and the rep is told why. - On the phone: the schemes that fit the shop above the order, and the ones applied when it is placed. - **Display programmes**: requested by the rep, approved by a supervisor, proved by the display photos of every visit in the period. ![A running scheme with its budget and uses](scheme.png) ## Requirements - **Field Sales and Distribution** (`bambooforge_field_sales`) 1.1 or later on the same Odoo series; Odoo 20.0, Community or Enterprise. - Supervisors create and close schemes and approve display programmes; reps read schemes and request display programmes. ## Install 1. Copy `bambooforge_field_sales_schemes` into your addons path next to Field Sales, restart Odoo. 2. *Apps*, search for *Trade Schemes*, **Activate**. A service product, *Trade scheme discount*, is created for amount-off schemes. ## Setting up a scheme *Field Sales ▸ Schemes ▸ Trade Schemes ▸ New*: - **When**: from and to; the scheme applies to orders dated inside. - **For which shops**: territories, channels and classes (all empty means every shop); or *Only These Outlets*. *Once per Outlet* gives it to an outlet on one order only. - **The order must hold**: either *enough of the qualifying products* - qualifying products (empty means every product on the order) and, optionally, a minimum number of units and a minimum amount of those lines - or *each product of a bundle*: every product listed, each in at least its quantity (six cola and six water). A bundle counts as many times as the order holds it whole. - **It gives**: *percent off* the qualifying lines; *percent off by tier* - tiers of units or amount, each with its percent, the highest tier reached applies (3% from 10 units, 6% from 50, 9% from 100); *free product* - so many units of a product for every multiple of the minimum quantity (ten plus one: minimum 10, one free) or for every whole bundle; or an *amount off* the order. - **Free goods count at**: the list price (default) or the product's cost - what a free unit takes from the budget. - **Not combinable**: when this scheme applies to an order, no other scheme does. Not-combinable schemes are tried first; the first that fits wins. - **Budget**: the most the scheme may cost; 0 for no limit. - **What the rep says**: one sentence shown on the phone. **Start** it. Schemes are tried in their sequence order, not-combinable ones first; several combinable schemes can apply to one order. ## On the order When a rep places an order on the phone, the running schemes that fit the shop are tried in order. A percent off is set on the qualifying lines; a free product is a line at 100% discount; an amount off is a line of the *Trade scheme discount* product. Each use is recorded with its cost - the discount given, the list value of the free units, or the amount - and the rep sees what was applied. An order made in the office gets the same with **Apply Trade Schemes** on the quotation. Applying again replaces what was applied before; a cancelled order gives its cost back to the budget. ![An order with a free-product line and a percent off](order.png) **Scheme Spend** (supervisors) pivots the uses by scheme and month. ## Display programmes *Schemes ▸ Display Programmes*: the programme (cooler, end-cap, window), the outlet, the period and the payout per month. A rep requests it; a supervisor **approves** or **refuses**. Every visit to the outlet in the period with a photo of kind *display* counts as proof; the form shows visits, visits with proof, the proof rate and the photos. Approved programmes end by themselves after their date. ## Limits, stated plainly - With the Distributors add-on, the small *Schemes for Distributors* module installs by itself: schemes apply to the distributor's secondary orders too, and what the distributor gave away is claimed back as a credit note. See the Distributors manual. - Tiers are read on the qualifying lines of one order, not on a shop's purchases over a month. - The free-product reward is the same product line, not a choice of products. - Payouts of display programmes are shown, not paid: pay them the way you pay any customer credit. ## Uninstalling Uninstalling removes schemes, uses and display programmes. Discounts and lines already on orders stay as they are. ## Changelog ### 20.0.1.1.0 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.1.0 - Percent off by tier; bundle conditions; not-combinable schemes; free goods counted at cost or at list price. - The scheme engine also serves distributors' secondary orders (with the new *Schemes for Distributors* module). ### 20.0.1.0.0 First release. ## How we make sure it works Before every release, **10 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Schemes apply to the reps order. - Budget stops the scheme. The complete list is in the source you receive. --- # Field Sales Van Sales: Load, Sell, Deliver and Collect Cash > The van is a stock location: loaded in the morning, every order the rep takes is handed over from it on the spot, cash written against the day, the rest unloaded at night. The day closes with stock and cash counted against what the system says. - **What it is:** The van is a stock location: loaded in the morning, every order the rep takes is handed over from it on the spot, cash written against the day, the rest unloaded at night. The day closes with stock and cash counted against what the system says. - **For:** Sales - **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:** 478 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_field_sales_van - **Not for:** Without a cash journal on the van, cash is recorded, not posted: register payments against the invoices the way your accounting does.; The suggested load reads the shops' last three orders, not a forecast; a shop that never ordered adds nothing.; A shop served by a distributor (with the Distributors add-on) is not sold from the van.; One van per rep, one open day per van. - **Manual:** https://docs.bambooforge.dev/bambooforge_field_sales_van/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_field_sales_van.bf_fs_van_session_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_field_sales_van - **Manual last updated:** 2026-09-21 --- # Field Sales Van Sales: Load, Sell, Deliver and Collect Cash A rep who sells from a van sells what is on the van, hands it over and takes the money. Without a record of the van, stock leaks and cash is a matter of trust. This add-on to **Field Sales and Distribution** gives every van a stock location and every day a closing: stock and cash counted against what the system says. ## What you get - **Vans**, one per rep, each an internal stock location of its own. - **Van days**: loaded in the morning from a load list; closed at night with an evening count, the rest unloaded, the cash counted. - **Sold from the van**: the order the rep takes on the phone is confirmed and delivered from the van at once. What is not on the van cannot be sold from it, and the phone shows what is. - **Cash** written against each order on the phone; the rest is credit given. - **Differences** - units missing, cash short or over - written down for the supervisor. - **A suggested load** from what the day's shops usually take. - **Invoices and cash posted** at the close, into the van's cash journal. - **An 80 mm receipt** for each van order, from the phone. - **Goods taken back** at a shop ride on the van; **van to van** hands stock from one van to another on the road. ![A closed van day: sales, cash expected, counted, the difference](day.png) ## Requirements - **Field Sales and Distribution** 1.2 or later, and Odoo's **Inventory**; Odoo 20.0, Community or Enterprise. - Products sold from a van are storable products. - Reps see their own van days; supervisors see all and set up vans. ## Install 1. Copy `bambooforge_field_sales_van` next to Field Sales, restart. 2. *Apps*, search for *Van Sales*, **Activate**. 3. *Field Sales ▸ Vans ▸ Vans*: a van per rep (the plate as its name), and the warehouse it loads from. Its stock location is created with it. 4. Optionally, on the van: a **cash journal** (closing the day then invoices and posts the cash) and the **load margin** (10% by default). ## The day **Morning.** *Start the Day* on the van (or a new *Van Day*), fill the **Morning Load** and press **Load the Van**: an internal transfer from the warehouse to the van, done at once. **Suggest the Load** fills the load list for you: for each shop on the rep's route that day, what it took on average over its last three orders, added up, plus the van's load margin, less what is still on the van, rounded up to whole cases where the product has a case size. Change what you like before loading. **At the shops.** On the phone the order card shows **what is on the van**, with quantities, to add in one tap. Placing the order confirms it and delivers it from the van on the spot; the phone lists the van orders of the visit with a **cash** field and **Paid**. A line asking for more than the van holds is refused, with the products named - free goods a trade scheme adds count too, so an order is refused as a whole rather than sending the van below zero. The printer icon next to a van order opens its **receipt**, 80 mm wide for a mobile printer. Goods the shop hands back (*Taken back* on the phone) are received into the van and go back to the warehouse with the evening unload. **Van to van.** *Vans ▸ Van to Van*: the van giving, the van receiving, the products; *Hand Over* moves them if the giving van has them and a day open. ![The phone at a shop: what is on the van, the schemes, the order](phone.png) **Evening.** **Evening Count** fills in what the van should hold; correct the counted quantities, enter the **cash counted**, press **Close the Day**. A unit counted missing is written off in the van, everything left goes back to the warehouse by an internal transfer, and a cash difference is noted in the day's chatter and shown in red in the list. With a **cash journal** on the van, closing the day also invoices every van order not invoiced yet, posts the invoices, and registers the cash written against each order as a payment in that journal, against its invoice. What was not paid stays open on the shop's account. The day's *Invoices and Cash* tab lists both. ## Limits, stated plainly - Without a cash journal on the van, cash is recorded, not posted: register payments against the invoices the way your accounting does. - The suggested load reads the shops' last three orders, not a forecast; a shop that never ordered adds nothing. - A shop served by a distributor (with the Distributors add-on) is not sold from the van. - One van per rep, one open day per van. ## Uninstalling Uninstalling removes vans and van days. Stock moves, sales orders and the van locations stay. ## Changelog ### 20.0.1.1.0 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.1.0 - Suggested load from the day's route; invoices and cash posted at the close into the van's cash journal; an 80 mm receipt per van order; goods taken back ride on the van; van-to-van transfers. ### 20.0.1.0.1 - Units another add-on puts on the order - free goods of a trade scheme - are checked against the van too: an order that does not fit is refused as a whole, and the van never goes below zero. ### 20.0.1.0.0 First release. ## How we make sure it works Before every release, **10 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - No van no change. - The van is a location and is loaded. - Goods taken back ride on the van. - Sold from the van on the spot. - Units added after the rep count too. The complete list is in the source you receive. --- # Financial Reports Builder: P&L and Balance Sheet > Build a P&L, balance sheet or any statement your accountant asks for from account prefixes and formulas, and drill down to the entries. - **What it is:** Build a P&L, balance sheet or any statement your accountant asks for from account prefixes and formulas, and drill down to the entries. - **For:** Accounting - **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:** 119 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_fin_reports - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Evaluate anything but numbers: a formula sees floats and nothing else - no records, no environment.; Guess: a formula naming a code no line has, or two lines naming each other, stops the run with an error. - **Manual:** https://docs.bambooforge.dev/bambooforge_fin_reports/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_fin_reports.action_bf_fin_report - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_fin_reports - **Manual last updated:** 2026-10-15 --- # Financial Reports Builder: P&L and Balance Sheet Odoo Community ships a trial balance and a general ledger. The statements a business is actually run on — a profit and loss laid out the way the owner reads it, a balance sheet in the local format, a contribution report per branch — are in Enterprise. This builds them out of two things anybody in finance already thinks in: account prefixes, and formulas over other lines. ## A line is a handful of account codes **Accounting › Reporting › Report Builder › Financial Reports.** Each line names accounts the way people say them out loud: | `60, 61, 62` | three families | |---|---| | `411:419` | a range | | `6, -603` | every 6 account *except* the 603s | An exclusion beats the prefix that let the account in, so the last one means what it looks like it means. Matching happens against the account code, which is the point: an account somebody opens next April lands on the right line by itself, without anybody remembering to come back here. ## …or a handful of account types Every chart of accounts in the world numbers its accounts differently. They all carry the same Odoo account types. A line of kind **Accounts by type** takes `income, income_other` and reads the same on a French chart, a US chart and a Brazilian one. A trailing star takes the family: `expense*` is every expense type this version has — and every expense type a later version adds. 19.0 added `expense_other`; a report written on `expense*` in 16.0 picks it up without being touched. The two reports installed with the module are built this way, so they show real figures on the first run whatever chart you are on. ## Formulas, over lines and nothing else Give a line a code — `GROSS`, `OPEX` — and write arithmetic on the next one: ``` REVENUE - COGS GROSS - TOTOPEX ``` A formula may name a line further down; the run resolves in passes. What it may **not** do is reach anything except those figures. The namespace it evaluates in holds floats and nothing else — no record, no environment, no cursor. That restraint is deliberate and worth stating plainly: Odoo's `safe_eval` restricts bytecode and dunder names, but it does not wrap the objects handed to it. A live recordset in that namespace is read and write access to the whole database for anybody who can edit a report line — and a report definition is exactly the kind of thing people export and email to each other. A formula that names a code no line has, or two lines that name each other in a circle, **stop the run with an error**. They do not print `0.00`. Of the three possible answers that is the worst one, because it looks like a figure. ## Branches count A branch is not another company — it posts under its own `company_id` while sharing its parent's chart of accounts, and Odoo installs no chart for it at all. (Branch accounting arrived with 17.0; on 16.0 an entry like that is refused by Odoo itself.) A statement for the parent counts what its branches posted, and a statement written for a branch finds the chart it actually posts on. Filtering on equality instead would make a group report short by exactly the branch, with nothing on the page to say so. ## It tells you what it left out This is the failure mode of every hand-built statement, and the reason they are quietly distrusted: somebody opens account 6035, it falls in nobody's prefix, and the report still foots — because the totals are formulas over the lines that *do* exist. Every run checks which accounts moved in the period and landed on no line, and says so at the top, with a button straight to the entries. An account on no line is a five-second fix once you know about it and a wrong board pack for a year if you don't. The check stays inside the statement's own subject. A profit and loss is not supposed to have the bank account on it, so it is not told about it — told instead that four hundred accounts are unaccounted for, people stop reading the warning inside a week, and then the day account 6035 really does fall outside every prefix nobody sees it. The scope is taken from the account groups the report's own lines reach. ## A period, or a position A profit and loss covers a **period**: the figures run from the start date to the end date. A balance sheet is a **position**: it is everything the company holds as at the end date, opening balance and all. A report says which it is, and the run obeys it — a position ignores the start date entirely. This is the one mistake in this trade that nobody catches by looking. A balance sheet run as a period shows the *movement* of the year under a title that says balance sheet: the bank line is the year's net cash flow, the capital line is empty in every year but the first, and every figure on it is wrong in a way that reads perfectly. The balance sheet shipped with the module is set to **position**. The comparison column follows the same rule: on a position it means *the position a year ago*, not what moved during last year. ## It can tell you it does not balance The last line of the example balance sheet is **Out of balance (must be zero)**, and the result above it is worked out from the income and expense accounts themselves. That is a deliberate choice against the easy one. A "result for the period" computed as assets less liabilities less equity is not a result — it is whatever makes the two sides meet. A balance sheet built that way *cannot come out wrong*, which means it cannot come out right either: the day somebody lays their sheet out on their own prefixes and forgets `47`, seven thousand in suspense disappears into the plug and the statement still foots. Worked out honestly, the last line is non-zero and somebody goes and looks. ## Compare, drill, print - **Comparison** against the period before or the same period last year — or, on a position, against the position then. A whole number of months is shifted as months, not counted backwards in days: Q2 counted in days starts on 31 December of the year before, and the comparison column then carries the entire year-end close. With the variance in currency and in per cent. Nothing to compare against reads `n/a` rather than `0.0%`: a percentage of nothing is not a percentage, and printing zero puts the biggest movement on the page at the bottom of the sorted column. - **Drill** from any figure to the journal items behind it, already filtered to the accounts, the dates, the journals and the posted state that produced it — the figures, the drill and the list of accounts on no line are all built from one method, so they cannot drift apart into a discrepancy the screen invented. A formula line says so instead, and points at the lines it adds up. - **Posted only, or including drafts** — off is what you want while closing and never what you want when reporting, so it is on by default and printed on the PDF when it is off. - **Per journal** and **per company**. - **Print** a PDF that keeps the indentation and the bold. ## Who can do what - **Financial Report Reader** runs a statement and drills into it. This is the one to hand out. Odoo's own read-only accounting group is in the hidden category and cannot be ticked on the Users form, so without this group the only right an administrator could actually give somebody would be Designer — write access to the formulas the whole company reports on. - **Financial Report Designer** can write the definitions everybody else runs, and implies the reader. A formula is checked before it is saved: it takes `+ - * /` and `abs`, `min`, `max`, `round`, `sum` over other lines' codes. Raising to a power is left out deliberately — `9**9**9**9` is nine characters and occupies a worker until somebody kills it, and writing report lines is one of the lower rights here. Report definitions are fenced per company. A run belongs to the person who ran it — including the numbers it produced, which is not everybody's business. ## Nothing from Enterprise The module depends on `account` and on nothing else. It does not require, use or reimplement `account_reports`. 19.0 ships the `account.report` tables in Community while the engine and the screen stay in Enterprise; this module keeps to its own models and does not touch them. ## How we make sure it works Before every release, **63 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A reader cannot rewrite the statement everybody reports on. - Drilling into a formula says why it cannot. - Formula that is not a number is refused. - A journal of another company is refused. - A formula cannot hang the server. 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: - Evaluate anything but numbers: a formula sees floats and nothing else - no records, no environment. - Guess: a formula naming a code no line has, or two lines naming each other, stops the run with an error. ## Install 1. Copy `bambooforge_fin_reports` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Financial Reports Builder: P&L and Balance Sheet**. 3. Open **Financial Reports** - its menus are Report Builder. ## 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 Invoicing app. No extra Python package. ****Where does it show up?**** Under the **Financial Reports** menu (Report Builder). ****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. --- # Form Builder for Website with Payment > Multi-step website forms with conditional fields, file uploads, an optional payment step, spam protection without CAPTCHAs and CSV export. - **What it is:** Multi-step website forms with conditional fields, file uploads, an optional payment step, spam protection without CAPTCHAs and CSV export. - **For:** Website - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_form_builder - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Use a third-party form service: every form is rendered and stored in your Odoo.; Show a progress bar on a single-step form.; Depend on a page builder: the frontend is plain JavaScript on Bootstrap classes. - **Manual:** https://docs.bambooforge.dev/bambooforge_form_builder/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_form_builder.action_bf_form - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_form_builder - **Manual last updated:** 2026-09-23 --- # Form Builder for Website with Payment The forms your Odoo 20 website actually needs: **multi-step wizards** with a progress bar, **10 field types** including capped file uploads, **conditional fields** ("visible when field X has value Y") enforced in the browser *and* on the server, **server-side validation** of everything, **spam protection without CAPTCHAs** (an invisible honeypot plus a single-use, HMAC-signed render token), an optional **payment step** for deposits, donations and paid applications, and a real **submissions inbox** — searchable, groupable, exportable, with instant e-mail notifications, an optional confirmation copy to the visitor and an optional CRM lead per submission. This page is the complete manual. Follow it top to bottom and you can install, build your first form, publish it and fix the common issues **without contacting support**. ## Overview - **Forms** — a form is a name, an optional description shown above it, a submit button label, and what happens afterwards: an inline **thank-you message** or a **redirect URL**. Each form is published **per website** (multi-website mixin) as its own page at `/form/` with **SEO metadata** (title, description, keywords), rendered inside your theme's layout. - **Steps** — split a long form into steps, each with a title and an optional description. Visitors see a **progress bar** and move with *Next / Back*; every step is checked before they advance. A form with a single step shows no progress bar. Steps are ordered by **drag handle** in the backend. - **Fields** — **10 types**: text, long text, e-mail, phone, number, date, dropdown, radio choices, checkbox and **file upload**. Per field: label, placeholder, required flag, the step it belongs to (empty = first step), an optional **half-width** layout to put two fields side by side, and for dropdown/radio the choices — one per line. Fields are ordered by drag handle too. - **Conditional fields** — any field can be *visible when field X has value Y*. The rule is evaluated **in the browser** (the field appears and disappears as the visitor types) **and again on the server**: a hidden required field never blocks a submission, and a visible one is always enforced — even against a hand-crafted request that bypasses the JavaScript. - **Server-side validation** — every submission is re-validated in Python: required fields, e-mail format, numbers that parse, dropdown and radio answers that match the configured choices, required checkboxes and required files. The browser checks are a courtesy; the server is the law. - **Spam protection, no CAPTCHA** — an invisible **honeypot** field rejects naive bots, and every page render carries an **HMAC-signed token** (signed with your database secret): submissions faster than the form's **minimum seconds** (default 3) are rejected — bots fill forms instantly — and tokens expire after 24 hours. The token is **single-use**: one rendered page can be submitted once, a replay of the same token is refused with *This form was already submitted*, and reloading the page issues a fresh one. No third-party service, no visitor friction. - **File uploads** — up to **3 MB** per file, checked server-side, stored as standard Odoo attachments (`ir.attachment`) on the submission. Oversized uploads are rejected on their **encoded size**, before the file is decoded into memory. Required file fields are enforced. - **Retention (optional)** — set **Delete submissions after (days)** on a form and a **daily job** removes submissions older than that, together with their uploaded files. 0 (the default) keeps them forever. - **Payment step (optional)** — charge a **fixed amount** or take the amount from a **Number field** of the form (donations, deposits, paid applications). The submission is stored as *Awaiting payment* and the visitor is sent to Odoo's standard `/payment/pay` flow with your enabled providers; a **10-minute cron** marks the submission *Paid* once the transaction succeeds. Needs the Payment app. - **Submissions** — stored with all answers and files; the sender is **matched to an existing contact by e-mail, or a new contact is created**. Search, filter, group by form, status or day, and export from the list view (native list export). Your team gets an **instant e-mail notification** at the addresses you configure; the visitor can receive a **confirmation copy** of their answers; each submission can create a **CRM lead** (needs CRM). - **Access rights** — *Form Builder Pro / User* reads forms and manages submissions; *Form Builder Pro / Manager* additionally designs forms, steps and fields. The internal **notification address list** is readable and editable by Managers only. - **Frontend** — framework-free JavaScript on plain Bootstrap classes: no OWL, no jQuery, no page-builder dependency. If your theme renders a website page, it renders these forms. - **Tested** — 23 automated tests, including a real-browser multi-step tour, green on each of Odoo 16, 17, 18, 19 and 20. ## Getting started 1. Install **BambooForge Form Builder Pro** (Community or Enterprise; only the standard `website` app is required). 2. Open **Form Builder ▸ Forms** and create a form: name, description, button label, thank-you message. 3. Add **steps** (optional — skip for a single-page form) and **fields**: pick the type, tick *Required* where needed, drag the handles into order. 4. Set who is told: **Notify on submission** (comma-separated internal e-mails), optionally **Confirmation e-mail** to the visitor and **Create a CRM lead**. 5. **Publish** the form (the *Go to Website* button opens `/form/`; the publish toggle is per website). 6. Submit it once yourself, then open **Form Builder ▸ Submissions** — your answers are there, grouped and exportable. ## Usage ### Building a form **Steps.** Add one line per step — title (shown in the progress bar) and optional description. Order with the drag handle. Fields without a step land on the first step, so a single-page form needs no steps at all. **Fields.** One line per field: label, type, step, required, placeholder. *Half width* renders two consecutive half-width fields side by side on wide screens. For **Dropdown** and **Radio choices**, enter the choices in *Options* — one per line; the server refuses any submitted value that is not in the list. **Conditions.** Set *Visible when* to another field of the same form and *…has value* to the exact answer that reveals the field (for a checkbox use `yes`). The field appears live in the browser as the visitor answers and the same rule is re-checked server-side: hidden required fields never block, visible ones are always enforced. A field cannot depend on itself or on a field of another form — both are refused at save time. ### After the visitor submits - The answers are validated server-side; any error is shown inline and nothing is stored until the submission is clean. - A **submission** is created with the answers, the uploaded files as attachments and the first *E-mail* field as the sender address. The sender is matched to an existing **contact** by e-mail (case-insensitive) or a new contact is created. - The internal **notification e-mails** go out immediately; the visitor gets a **confirmation copy** if enabled; a **CRM lead** is created if enabled and CRM is installed. - The visitor sees your **thank-you message** inline — or is sent to the **redirect URL** if you set one. With a payment step, the flow changes after validation: the submission is stored as **Awaiting payment**, the visitor is redirected to `/payment/pay` with the amount pre-filled, and the notification e-mails are only sent once the cron has confirmed the transaction and moved the submission to **Paid**. ### Working with submissions **Form Builder ▸ Submissions** is the inbox: filter by form or status (*Received / Awaiting payment / Paid*), group by form, status or day, and export the selection with the native list export. Each submission shows the readable answers, the linked contact, the attachments and — for payment forms — the amount, currency and payment reference. From a form, the *Submissions* smart button opens its own submissions directly. ### Taking payments 1. Install the **Payment** app and enable at least one provider (Stripe, PayPal, Adyen, wire transfer…). 2. On the form, tick **Payment step**, then either set a **Fixed amount** or pick **Amount from field** — a Number field of the same form, so the visitor chooses the amount (donations, deposits). One of the two is required; saving refuses a payment form with neither. 3. The visitor pays through Odoo's standard payment page — your provider configuration and branding apply unchanged. The cron **BambooForge Form Builder: confirm paid submissions** runs every 10 minutes and marks the submission **Paid** as soon as the transaction is done, then sends the notifications. An amount of zero (e.g. an empty optional Number field) skips the payment step and stores the submission as *Received* directly. ### Stopping spam Nothing to configure for the honeypot — it ships enabled and invisible. The timing check is per form: **Anti-bot minimum seconds** (default 3) is the minimum time between the page render and the submission; anything faster is rejected with a polite message. The render token is signed with your database secret and expires after 24 hours, so a form left open overnight simply needs a reload. It is also **single-use**: the page a visitor loaded can be submitted once — posting the same token again (a stuck *Submit* button, a replayed request, a bot hammering one captured page) is refused with *This form was already submitted*, and reloading the page hands out a fresh token. If a legitimate audience fills a tiny form very fast, lower the minimum; if a form attracts bots, raise it. ### Keeping submissions only as long as you need **Delete submissions after (days)** sets a retention window per form. Leave it at **0** and submissions are kept forever (the previous behaviour). Set it to, say, 90 and the daily job *BambooForge Form Builder: retention cleanup* deletes every submission of that form older than 90 days **together with its uploaded files**. Deletion is permanent — export what you need first. ## Configuration ### Form options | Option (per form) | Effect | |---|---| | Button label | Text of the submit button (default *Submit*). | | Thank-you message | Shown inline after a successful submission. | | Redirect URL | If set, the visitor is sent here instead of the message. | | Notify on submission | Comma-separated internal e-mails that receive every submission. Visible to **Form Builder Pro / Manager** only. | | Confirmation e-mail | Send the visitor a copy of their answers (uses the first E-mail field). | | Create a CRM lead | One lead per submission, with the answers in the description. Needs CRM. | | Anti-bot minimum seconds | Submissions faster than this after the page rendered are rejected (default 3). The render token is single-use on top of that, so one page can only be submitted once. | | Delete submissions after (days) | Retention window: a daily job deletes submissions of this form older than this, with their uploaded files. **0 (the default) keeps them forever.** | | Company | The company the form (and every submission it collects) belongs to. | | Payment step / Fixed amount / Amount from field | Enable the payment flow; set a fixed amount or point at a Number field. Needs the Payment app. | ### Field types | Type | Validation on submit | |---|---| | Text / Long text | Required if flagged. | | E-mail | Must be a valid e-mail address; the first E-mail field becomes the sender address of the submission. | | Phone | Free text input with a phone keyboard on mobile. | | Number | Must parse as a number (comma or dot decimals accepted); can feed the payment amount. | | Date | Native date picker. | | Dropdown / Radio choices | The answer must be one of the configured options. | | Checkbox | Stored as *yes* when ticked; a required checkbox must be ticked. | | File upload | Up to 3 MB, stored as an attachment; required files are enforced. | ### Access rights - **Form Builder Pro / User** — read forms, manage and export submissions. - **Form Builder Pro / Manager** — additionally create and design forms, steps and fields. Website visitors need no account: the form page and the submit endpoint are public, protected by the honeypot, the signed token and the server-side validation. ## FAQ ****How is this different from Odoo's standard form snippet?**** The standard website form snippet posts one page of fields into a single model and stops there: no steps, no progress bar, no conditional visibility, no payment step, no file-size control and no submission browser — answers end up in whatever model the snippet wrote to. Form Builder Pro is a form engine: multi-step, conditional, payable, with every submission stored with its files in a searchable, exportable inbox. ****Which payment providers can I use?**** Any provider enabled in your Odoo's Payment app — Stripe, PayPal, Adyen, bank wire… The payment step hands the visitor to Odoo's own `/payment/pay` page, so your existing provider configuration, credentials and branding apply unchanged. ****Where are uploaded files stored?**** As standard Odoo attachments (`ir.attachment`) linked to the submission — the same storage and filestore your documents use, covered by your existing backups. Files are capped at 3 MB each, checked server-side. ****What about GDPR?**** Submissions live in **your database only**. Answers, files and e-mail addresses are never routed through BambooForge or any third-party form or CAPTCHA service — the spam protection is entirely local. Deleting a submission deletes its answers; you remain the only controller of the data your forms collect. For data minimisation you can now set an **automatic retention window** per form (*Delete submissions after (days)*): a daily job removes older submissions and their uploaded files for you. ****Does it work with my website theme?**** Yes. The form page renders inside your theme's layout, the markup uses plain Bootstrap classes, and the frontend is framework-free JavaScript — no OWL, no jQuery, no page-builder dependency. ****Why was a fast test submission rejected?**** That is the anti-bot timing check: submissions faster than the form's *Anti-bot minimum seconds* (default 3) after the page rendered are refused. Wait a few seconds and submit again, or lower the minimum on the form. ****Can a visitor skip a required field?**** Only when the field is genuinely hidden by its visibility condition — a hidden required field never blocks. Every visible required field is enforced server-side, even against requests that bypass the browser checks. ****Is it tested?**** Yes: 23 automated tests — field validation of every type, the hidden-required rule, honeypot and timing rejections, file upload, the payment flow, the unpublished-form guard — including a real-browser multi-step tour, on each of Odoo 16, 17, 18, 19 and 20. ## How we make sure it works Before every release, **36 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A replayed nonce is refused by the database. - A plain employee does not see the menu. - The agent builds nothing. - A plain employee never reads the inbox. - The visitor sees only published forms. The complete list is in the source you receive. ## Support - **E-mail** — [support@bambooforge.dev](mailto:support@bambooforge.dev). Every request is answered **within 24 hours**, setup help included. - **What to send** — your Odoo version and edition, the form's configuration (steps, field types, payment options) and for submission issues the exact error message shown on the website or the server log around `bf_form`. - **Money-back guarantee** — report a bug within **2 months** of purchase; if we cannot resolve it within 15 days, you get a full refund. - **Source** — the complete OPL-1 source is included; extend it your way. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Use a third-party form service: every form is rendered and stored in your Odoo. - Show a progress bar on a single-step form. - Depend on a page builder: the frontend is plain JavaScript on Bootstrap classes. ## Install 1. Copy `bambooforge_form_builder` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Form Builder for Website with Payment**. 3. Open **Form Builder** - its menus are Forms, Submissions. ## Changelog ### 20.0.1.3.3 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.1.0 (2026-08-15) - Anti-bot: the HMAC-signed render token is now **single-use**. A rendered page can be submitted once; posting the same token again is refused with *This form was already submitted*, and reloading the page issues a fresh token. - New per-form **Delete submissions after (days)** setting with a daily job that removes expired submissions **and their uploaded files**. 0 (the default) keeps everything, as before. - The internal notification address list (*Notify on submission*) is now visible to **Form Builder Pro / Manager** only. - Uploads are rejected on their encoded size before being decoded — the 3 MB cap is unchanged, it is simply enforced earlier. - New **Company** on the form; submissions carry it too. ### 20.0.1.0.0 (2026-08-14) - Initial release: multi-step forms with progress bar, drag-handle ordering of steps and fields; 10 field types (text, long text, e-mail, phone, number, date, dropdown, radio, checkbox, file upload up to 3 MB stored as attachments); conditional visibility evaluated in the browser and re-enforced server-side; full server-side validation (required, e-mail format, numbers, dropdown/radio choices); CAPTCHA-free spam protection with an invisible honeypot and an HMAC-signed render timestamp (configurable minimum seconds, 24-hour token expiry); optional payment step with fixed or field-driven amount through the standard `/payment/pay` flow and a 10-minute confirmation cron; submissions with answers, files and contact matching by e-mail, searchable, groupable and exportable; instant internal e-mail notifications, optional confirmation copy to the visitor and optional CRM lead per submission; inline thank-you message or redirect URL; per-website publishing with SEO metadata at `/form/`; User and Manager access groups; 23 automated tests including a real-browser multi-step tour on each of Odoo 16, 17, 18, 19 and 20. --- # Form Builder All in One with Web Form to Quote > Turn a website form into a draft quotation: lines picked by the visitor's answers, quantities from their numbers, a price list and a seller. - **What it is:** Turn a website form into a draft quotation: lines picked by the visitor's answers, quantities from their numbers, a price list and a seller. - **For:** Website - **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:** 158 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_form_quote - **Not for:** Options and variants are products - pick the variant you sell; no configurator.; Prices come from the price list or the rule's override; no multi-currency arithmetic beyond what the price list does.; No optional products, no online signature step - the quotation is Odoo's; everything Sales does next still applies. - **Manual:** https://docs.bambooforge.dev/bambooforge_form_quote/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_form_builder.action_bf_form - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_form_quote - **Manual last updated:** 2026-10-15 --- # Form Builder All in One with Web Form to Quote An add-on for **BambooForge Form Builder** with Odoo **Sales** (17.0). A public form becomes a draft quotation - lines picked by the visitor's answers, quantities from their numbers, a price list, a salesperson - and, if you say so, sent on the spot. ## Set it up on the form *Form Builder ▸ Forms ▸ your form ▸ Quotation tab.* - **Quotation**: *No quotation* / *Create a draft quotation* / *Create and send the quotation* (Odoo's standard quotation e-mail, and the order goes to *Quotation Sent*). - **Always add**: a product put on every quotation from this form - the service itself, a setup fee. Optional. - **Price list**, **Salesperson**, **Sales team** - written on the quotation. - **Rules**, read in order; every match adds a line: - *When field* - a field of this form. - *equals* a choice (dropdown / radio / text, case-insensitive), *is ticked* (checkbox), *is filled in* (anything non-empty). - *Add product* - the product (pick the variant you sell). - *Fixed quantity*, or *Quantity from a number field* - the number the visitor typed (a zero or empty number adds nothing). - *Price* - leave 0 for the price list / product price; set to override. - *Note* - appended to the line description. A form with a quotation mode needs at least one rule or an *Always add* product; it is refused otherwise. ## What the visitor's submission becomes When the submission is **complete** - received, or paid when the form takes payment - the bridge builds the quotation for the contact Form Builder found or created by e-mail (no e-mail → *no quotation: no contact* on the submission). Lines from *Always add* and every matched rule; the form's name as *Source document*; the submission as *Customer reference*; every answer in the quotation's **Terms and conditions / note**, so the rep reads the context before calling. The submission shows the **Quotation** and a one-line note (*draft quotation*, *quotation sent*, *no quotation: no rule matched*, …); the quotation shows **From form submission** and **From form**, and the quotations list has a *From a form* filter. A quotation failure never fails the visitor: it is logged and written on the submission. ## Honest about limits - Options and variants are products - pick the variant you sell; no configurator. - Prices come from the price list or the rule's override; no multi-currency arithmetic beyond what the price list does. - No optional products, no online signature step - the quotation is Odoo's; everything Sales does next still applies. ## Technical notes - Uses Form Builder 1.3.0's `_after_submit()` hook; Sales' standard `sale.order` create and `sale.email_template_edi_sale`. - `sale.order.bf_submission_id` (set null on delete) and `bf_form_id` (related, stored) for reporting. - Community only. Needs the Sales app; no Enterprise module. ## How we make sure it works Before every release, **18 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - After submit twice does not duplicate. - No email means a note not a ghost customer. - A broken rule product is noted not raised. - Garbage in the number field adds nothing. - No match and no base means no quotation. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_form_quote` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Form Builder Suite: Web Form to Quotation**. ## 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 Sales; form builder (BambooForge) must be installed first app. No extra Python package. ****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. --- # Gantt View for Projects > A real Gantt chart for Odoo Community: auto-schedule with MS Project constraints, drag to reschedule, links by drag, critical path and float, subtasks, portfolio, workload in hours, baseline, MS Project / Excel / PDF in and out. - **What it is:** A real Gantt chart for Odoo Community: auto-schedule with MS Project constraints, drag to reschedule, links by drag, critical path and float, subtasks, portfolio, workload in hours, baseline, MS Project / Excel / PDF in and out. - **For:** Project - **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:** 249 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_gantt - **Not for:** Bars snap to whole days; this is a planning chart, not a time clock.; The scheduler is a forward pass: it packs early and never levels resources. Two people over capacity are shown in red, not fixed.; The critical path and float weigh each bar by its length in days and do not model resource contention.; Progress is a manual percentage - honest and simple; it does not try to guess from timesheets.; Grouping by assignee shows each task under its first assignee.; A parent task's dates are its own; nothing is rolled up. - **Manual:** https://docs.bambooforge.dev/bambooforge_gantt/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_gantt.action_bf_gantt - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_gantt - **Manual last updated:** 2026-10-15 --- # Gantt View for Projects Community Odoo plans projects in lists and kanbans; the schedule lives in somebody's head. This app draws it — and lets you drag it. ## What you get - A **Gantt timeline** over your existing tasks: one bar per task, grouped by project or by assignee, zoomed by day, week, month or quarter - or **Fit**, which sizes the whole plan to the window - with weekend shading and a dashed *today* line. The chart opens scrolled to today. - **Drag to reschedule** - grab a bar and move it whole days; grab either edge to stretch or shrink it. A plain click opens the task. **Undo** puts the last move back, successors included. - **Automatic successor shifting**: when a moved task would now overlap a task that depends on it, the dependent is pushed to start where its predecessor ends - keeping its duration - and so on down the chain. - **Auto-schedule**: one button packs every open task as early as its dependencies, its constraints and the working calendar allow, the way MS Project's automatic mode does. - **Schedule constraints** on a task - *Start No Earlier Than*, *Must Finish On* and the rest of the MS Project set - drawn as a pin under the bar, red when the bar breaks them. - **Dependency arrows** drawn from Odoo's own task dependencies, typed (FS, SS, FF, SF) with a lag; **draw a new one by dragging** the handle at the end of a bar onto another bar, **click an arrow to remove it**. - **Critical path** and **float**: the chain that decides the end date turns red; every other task's tooltip says how many days it can slip. - **Subtasks** nested under their parent at any depth, folded with a caret; **summary bars** on every project row; a **Portfolio** view with one bar per project. - **Baseline**: freeze the plan, draw it under the live bars, read the slip in days. **Progress line**: a zig-zag against today that shows what is behind and what is ahead at a glance. - **Deadlines** as a flag on the bar, red once missed, with a count in the toolbar; **colour by** project, stage, assignee or deadline. - **Workload in hours**: one row per person, one cell per day, the hours laid on them against the hours the calendar gives them - red when over. - **Files in and out**: export PNG, a printed PDF schedule, Excel, CSV or **MS Project XML**; import MS Project XML, Excel or CSV into a project. - **Milestones** as diamonds on the project row (green once reached); company closing days shaded like weekends; double-bookings flagged. - Built from scratch in OWL and SVG. No bundled Bryntum/dhtmlx builds, no extra JavaScript licence, nothing phoning home. ## Install 1. Install **BambooForge Project Gantt** (depends on Project only). 2. A **Gantt** menu appears in the Project app, and every project form gets a **Gantt** smart button scoped to that project. 3. To see dependency arrows, enable **Task Dependencies** on the project. 4. Set the project's **start date** if you want *Auto-schedule* to pack the plan from it; without one it packs from the earliest bar. ## Where the dates come from The chart draws its own pair of fields — *Gantt Start* and *Gantt End* on the task (visible under *Extra Info → Gantt Schedule*). They are deliberately separate from the deadline: re-planning the schedule does not silently move the commitment you gave the customer. The first time a task appears on the chart with no schedule, it gets a sensible one: - **Start**: the assignment date, else the creation date — at 08:00. - **Length**: the allocated hours at 8 h/day; else up to the deadline; else three days. After that the fields belong to you — the chart, the task form and any automation all write the same two fields. ## Moving tasks - **Drag the bar** to move it, whole days at a time (the time of day is kept, so a task that starts at 08:00 keeps starting at 08:00). - **Drag an edge** to change the duration. - If tasks depend on the one you moved and would now overlap it, they are pushed right, keeping their durations, cascading down the dependency chain. Tasks that already start after their predecessor ends are left alone - a planned gap is your business, not the chart's. A pushed task never lands before its own *Start No Earlier Than*. - **Undo** (the arrow next to *Auto-schedule*) restores the bars of the task you moved and of everything the move pushed. Thirty steps deep. - Dependency cycles (possible via imports) are detected: the shift stops instead of looping forever, and the critical path is simply not shown. ## Constraints and the automatic scheduler A task carries a **schedule constraint** (*Extra Info → Gantt Schedule*): - *As Soon As Possible* - the default; the scheduler may put it anywhere its predecessors allow. - *Start No Earlier Than* / *Finish No Earlier Than* - a fence: the scheduler will not pull it before the date. - *Start No Later Than* / *Finish No Later Than* - a deadline for the scheduler: it cannot force it, but it reports the breach. - *Must Start On* / *Must Finish On* - pinned. The bar goes on the date even if a predecessor ends after it, and the chart shows the clash. A constraint is drawn as a small pin under the bar (blue; red when the bar breaks it), the row label turns red, the toolbar counts the breaches, and the tooltip states the rule. **Auto-schedule** runs a forward pass in dependency order over the open tasks on the chart: a task with no predecessor starts at the project's start date (else at the earliest bar of the plan); every other task starts at the latest of its predecessors' targets - finish-to-start, start-to-start, lag and lead all honoured - and its own fence; the start snaps to a working day; the duration is kept. Closed tasks are not moved: they are history, and they still anchor what depended on them. A banner lists any constraint that could not be honoured. The button is one Undo step. ## Links drawn on the chart Hover a bar: a small circle appears at its end. **Drag it onto another bar** and that task now depends on this one (finish-to-start; open the task to change the type or add a lag). A link that would make a loop is refused with both task names in the message. **Click an arrow** and confirm to remove the dependency - the typed row and Odoo's own link go together. ## Subtasks and the portfolio Sub-tasks sit under their parent, indented, at any depth; the caret in the row label folds them. A parent's bar keeps a darker outline; its own dates stay its own - the chart does not roll children up into the parent, because a parent whose dates are computed cannot be planned. Every project row carries a **summary bar**: earliest start to latest end of its tasks, the done part filled, progress weighted by duration. Switch the grouping to **Portfolio** and only those rows remain - every project on one timeline, a percentage next to each - and a click on a summary opens that project's own chart. ## Critical path and float Tick **Critical path** and the chain of dependent tasks with the largest total duration turns red - bars and arrows. That chain decides the project end date: shorten anything on it and the project ends earlier; shorten anything off it and nothing happens. The computation runs server-side on the tasks currently displayed. Every linked task's tooltip also states its **float**: the days it can slip before the end date moves, from the classic forward and backward pass over durations and lags. Zero float is the critical path. ## Progress line and deadlines Tick **Progress line** and a line runs down from today, bending left to every bar whose progress strip has not reached today (behind) and right to every bar that is ahead. It is the one picture a steering meeting needs. A task's **deadline** is a flag on its bar; it turns red once the bar ends after the deadline or the deadline has passed with the task open, the row label gets a warning mark and the toolbar counts them. Colour the bars **by deadline** to see green (time left), amber (three days or fewer) and red (missed) across the whole plan. ## Workload in hours Tick **Workload** and one row per person appears under the chart, one cell per day. A task's hours (its allocated hours, else eight for every working day it covers) are spread evenly over the working days it spans and over its assignees; the cell compares that with the hours the company calendar gives that day. Light green is half a day or less, amber a full day, red more than the day has - and the row label counts the days a person is over. ## Files in and out **Export** offers five shapes of the tasks on the chart: - **PNG** - the chart as drawn, row labels included. - **PDF** - a printed schedule table: WBS, task, dates, working days, progress, people, predecessors by WBS, slip against the baseline; the critical path marked. - **Excel** and **CSV** - one row per task with every column the chart knows, predecessors written the MS Project way (`12FS+2`). - **MS Project XML** - the mspdi schema that Project, Primavera P6, ProjectLibre and GanttProject open: outline levels, links with type and lag, constraints, deadlines, baselines, resources and assignments. **Import** reads the same three files into a project: rows become tasks with their bars, progress, people (matched by name), predecessors and constraints; outline levels, indented names or a WBS column become sub-tasks. A row whose name matches an open task updates it instead of creating a second one, so a plan can go out to Excel and come back. ## Limits, stated plainly - Bars snap to whole days; this is a planning chart, not a time clock. - The scheduler is a forward pass: it packs early and never levels resources. Two people over capacity are shown in red, not fixed. - The critical path and float weigh each bar by its length in days and do not model resource contention. - Progress is a manual percentage - honest and simple; it does not try to guess from timesheets. - Grouping by assignee shows each task under its first assignee. - A parent task's dates are its own; nothing is rolled up. ## What changed in 2.0 - **Auto-schedule** and **schedule constraints** - the seven-type MS Project set - with breaches drawn, counted and listed. - **Links by drag**, removal by click, loops refused by name. - **Float** in every tooltip; **subtasks** at any depth with folding; **summary bars** and a **Portfolio** grouping. - **Progress line**, **deadline flags** with an overdue count, **colour by** stage, assignee or deadline; **Quarter** zoom and **Fit**; full-screen; a search box; grid lines that switch off. - **Workload in hours** against the calendar's capacity, replacing the task count. - **Export** to PDF, Excel, CSV and MS Project XML; **import** from all three. **Undo**, thirty steps. **Clear baseline**. ## What changed in 1.1 - **People on the chart.** The project manager sits on the project row (avatar and name); every bar carries its assignee's avatar. - **Double-bookings surface themselves.** One person, two open tasks, overlapping days: the bars get a warning stroke, the overlap window is shaded on the assignee lane, the toolbar counts the conflicts and can filter to them - and the tooltip names the colliding task. - **Working calendars.** Durations count working days on the company calendar: bars start on a workday, hour-based lengths skip weekends and closing days, moved or pushed tasks never restart on a holiday, and company closing days are shaded like weekends. ## What changed in 1.2 - **Baseline.** One button freezes today's schedule; a toggle draws the frozen bars under the live ones and the tooltip states the slip in days. Re-planning stops erasing the promise it is measured against. - **Workload heatmap.** A toggle adds one row per person under the chart, one cell per day: green one task, amber two, red three or more - who can take more work is a glance. ## What changed in 1.3 - **Typed dependencies.** A dependency can now say *how* two tasks are tied - finish to start (the default), start to start, finish to finish or start to finish - and carry a lag in working days, negative for a lead. Rescheduling honours the relationship, the arrows leave and land at the right ends, and anything other than a plain finish-to-start is labelled on the link (`SS+2`, `FF`, `FS-3`). - **Export to PNG.** One button rasterises the chart, row labels included, for the status report or the site notice board. ## How we make sure it works Before every release, **78 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Dependency cannot be self. - The schedule is not behind developer mode. - Import msproject from another tool. - Auto schedule must start on wins and is flagged. - Workload without hours assumes a day of eight. The complete list is in the source you receive. ## 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 Project app. No extra Python package. ****Where does it show up?**** Under the **Gantt** menu. ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within two business days. This module is licensed OPL-1; one purchase covers one production database plus its staging copies. --- # Garage & Auto Repair Management > Garage and auto repair job cards with an authorised figure that cannot be exceeded quietly, booked versus actual time, and service history. - **What it is:** Garage and auto repair job cards with an authorised figure that cannot be exceeded quietly, booked versus actual time, and service history. - **For:** Industries - **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:** 149 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_garage - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Finish a job card that has gone past the authorised figure until somebody rings the customer and authorises the difference.; Change a finished, invoiced or cancelled card - only the workshop manager can put it back on the ramp, on record.; Move an authorisation down; it only ever moves up. - **Manual:** https://docs.bambooforge.dev/bambooforge_garage/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_garage.action_bf_garage_job - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_garage - **Manual last updated:** 2026-10-15 --- # Garage & Auto Repair Management Built around the one argument every garage has: the customer authorised a figure and the bill says another. ## The authorisation - The **authorised amount** is a number on the job card, with who gave it and when. It is the figure the customer heard on the phone, so it is **tax included**, and the card is measured against the same. - **Nothing starts without one.** Putting a car on the ramp with nothing agreed is refused, not warned about. - A card that has gone past the figure **cannot be finished**. The keys do not go back until somebody rings the customer and authorises the difference - afterwards it is an argument you lose. - **The gate is on the bill as well as on the ramp.** A part that appears on a card after it was finished is refused outright, and if one ever gets there the invoice will not go out until it is authorised. Once a card is finished, invoiced or cancelled, nothing is added to it, changed on it or taken off it: only the workshop manager can put it **back on the ramp**, and the card says who did. - An authorisation only ever moves **up**. A smaller figure means work coming off the card, not the authorisation shrinking. ## The vehicle - The history belongs to the **registration plate**, not to the owner. Cars change hands; the record does not start again. - The **odometer never goes backwards.** A reading lower than the last one is a typo or a crime, and either way it is refused - on the vehicle, on the job card, through an import and over the API, because they all go through the same door. Every job card writes its reading onto the vehicle. - A reading that went in wrong is **corrected by the workshop manager, with a reason**, and the reason is on the vehicle where the next person can read it. Cancelling a card gives the vehicle back the reading it had before that card was opened. - **Service due by date or by distance**, whichever comes first, and a job that includes a service operation resets both. Cancelling a finished card winds the clock back with it. - Roadworthiness and insurance dates sit beside them, and a scheduled job puts a **call on a named person's list** thirty days out - the reminder owner set on the company, or the advisor who booked the car in last. Never a robot's list, and never the same car twice in a month, whether or not the call was ticked off. ## The workshop - **Book time, charged time and actual time** on every labour line. The ratio between the first and the last is what a workshop is really run on, and it is a field rather than a spreadsheet. - **Parts hold the car.** A part on order puts the job in *waiting*, and a card cannot be finished while a part has not arrived. Each part carries its number, its factor, its cost and therefore its margin. - **One car per ramp**; a double-booked ramp is a promise broken. ## The money - **The driver is not always the payer.** A fleet, a leasing company or an insurer set on the vehicle is carried onto every job card, and the invoice quotes the **plate and the odometer** - what a fleet manager reads when forty of these arrive in a month. - Labour and parts both reach the invoice, taxed through the customer's fiscal position - and the **card shows the same tax the bill will**, including on lines created by an import or over the API. - A card is invoiced **once**, and the card row is held while the invoice is raised, so two people pressing the button at the same moment cannot raise two bills. - An invoiced card cannot be edited - header, labour or parts. Cancel the invoice and the card puts itself back to finished; credit it and the card is free again, because a credit note is not a second bill and the invoice it reversed has stopped being one. ## Roles A **technician** works the card - diagnosis, labour, times, parts - and sees no price and authorises nothing. That is enforced on the fields themselves, not only on the screen: a price is not readable over the API, in an export or through an import either, and the authorisation and the state of the card cannot be written by anybody but the advisor. A **service advisor** books cars in, rings the customer, moves the card through the workshop, authorises and invoices - and comes with the invoicing rights that requires. The **workshop manager** configures, sees the cost and the margin on parts, corrects an odometer and puts a finished card back on the ramp. ## Community only Depends on `product`, `account` and `base_setup` - all Community. Tested on 16.0, 17.0, 18.0, 19.0 and 20.0. ## How we make sure it works Before every release, **84 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A part cannot be added to an invoiced or cancelled card. - A credit note is never mistaken for a second bill. - A credit note stays on the card and frees it. - An advisor cannot put a card back on the ramp. - The authorisation note keeps the money out of the chatter. 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: - Finish a job card that has gone past the authorised figure until somebody rings the customer and authorises the difference. - Change a finished, invoiced or cancelled card - only the workshop manager can put it back on the ramp, on record. - Move an authorisation down; it only ever moves up. ## Install 1. Copy `bambooforge_garage` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Garage & Auto Repair Management**. 3. Open **Garage** - its menus are Job cards, Vehicles, 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, Invoicing, Products, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Garage** menu (Job cards, Vehicles, 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. --- # GDPR Subject Requests: Data Access & Erasure > A register for GDPR access, erasure and portability requests: statutory deadline chased, identity verified, and a response package that opens. - **What it is:** A register for GDPR access, erasure and portability requests: statutory deadline chased, identity verified, and a response package that opens. - **For:** Extra Tools - **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:** 199 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_gdpr - **Not for:** The scan covers models with an email field. Data tied to a person only through a link — a delivery address on an order, a name in a note — is not found automatically.; Erasure and anonymisation are recorded here; performing them is done on the records themselves, with Odoo's privacy lookup or by hand.; One controller per company. Joint controllership arrangements are not modelled. - **Manual:** https://docs.bambooforge.dev/bambooforge_gdpr/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_gdpr.action_bf_gdpr_request - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_gdpr - **Manual last updated:** 2026-09-21 --- # GDPR Subject Requests: Data Access & Erasure ## What this is for Odoo's privacy lookup finds records. It does not record that somebody asked, what they asked for, when the answer is legally due, whether their identity was checked, or what was actually done — which is the whole content of the file a supervisory authority asks to see. ## The clock One month from **receipt** under Art. 12(3) — not from the day somebody notices the email. The deadline is computed and shown as days remaining, and a request past it is flagged in red. An extension adds two further months and needs a written reason, because the person has to be told why within the first month. A second extension is refused: the article allows two extra months, not four. A daily job does two things with the clock. A week before the deadline it puts one to-do on the handler's list — one, not one per morning; the reminder is not repeated while the first is still open. And it flags any request that went past its deadline overnight, so the red mark does not wait for somebody to open the record. ## Identity first A request cannot be answered until identity is confirmed **and how it was checked is written down**. Handing somebody else's data to an impostor is itself a breach, and "we assumed it was them" is not a defence. ## Finding the data **Find Their Data** searches every model in this database that carries an email field, skipping transient models, the mail queue and log tables — they either disappear on their own or hold copies of what is found elsewhere. The search is case-insensitive; a blank email finds nothing rather than everything. Each hit becomes a line that must be given a decision: exported, anonymised, erased, or kept. The request will not be marked answered while any line is still undecided. Keeping a record against an erasure request needs the obligation stated on the line. Erasure is not absolute — but the reason has to be written down. ## The response **Build Response Package** produces a zip: an index of every record found and what happened to it, one readable JSON per exported record, and a cover note. Art. 15 says provide a copy; a letter saying "we hold some data about you" is not one. Binary fields are left out on purpose. A data export is meant to be read by the person who asked, and base64 blobs are neither readable nor what they asked for. ## Refusing A refusal needs the exemption stated. A refusal with no ground is the single thing supervisory authorities pick up every time. ## Known limits - The scan covers models with an email field. Data tied to a person only through a link — a delivery address on an order, a name in a note — is not found automatically. - Erasure and anonymisation are recorded here; performing them is done on the records themselves, with Odoo's privacy lookup or by hand. - One controller per company. Joint controllership arrangements are not modelled. ## How we make sure it works Before every release, **28 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Identity cannot be confirmed without saying how. - A package cannot be built before scanning. - Scanning twice does not duplicate. - A request with no email finds nothing rather than everything. - A plain employee does not see the menu. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_gdpr` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **GDPR Subject Requests: Data Access & Erasure**. 3. Open **Data Protection** - its menus are Subject Requests. ## 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, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Data Protection** menu (Subject Requests). ****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. --- # Gym & Fitness Membership Management > Gym memberships that expire, freeze and renew on their own, a front desk that says why entry is refused, class bookings with a waiting list. - **What it is:** Gym memberships that expire, freeze and renew on their own, a front desk that says why entry is refused, class bookings with a waiting list. - **For:** Industries - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_gym - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Open the door on an unpaid membership when overdue money is set to block - overdue, not merely unpaid.; Show the front desk a crash dialog: a refusal or a server error is a line on the panel. - **Manual:** https://docs.bambooforge.dev/bambooforge_gym/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_gym.action_bf_gym_front_desk - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_gym - **Manual last updated:** 2026-10-15 --- # Gym & Fitness Membership Management One moment decides whether gym software is any good: somebody is standing at the desk and the screen has to say yes or no, and why. ## The front desk Type or scan a name or a member number and the panel answers in one line: **come in**, or **not today** with the actual reason - not started, on hold, ran out on the 3rd, no sessions left, cancelled, not a member here. A refusal is never a red dialog somebody has to dismiss with a queue waiting. If the gym has switched it on, money **overdue** also stops the door - overdue, not merely unpaid, so an invoice raised this morning on fourteen-day terms is not an argument in front of other members. It is **off by default** for the same reason. A server error never reaches the desk as a crash dialog either: the panel says so and nothing is recorded. A scan that arrives while the previous one is still going is not swallowed in silence. ## Plans Monthly, quarterly, yearly, or a **session pass**. A plan carries: - its price, and the product the invoice line is written against; - how many sessions it includes (0 = unlimited) and how long a pass stays valid; - how many **freeze days** a member may take per period; - the **notice period** that still has to be paid; - whether it renews itself; - which classes it opens - empty means all of them. ## Memberships A membership is a contract, not a flag on a contact. - The **price is copied at signature**, so a price rise never rewrites what somebody already signed. - **Two live memberships for one person are refused.** That is how a member ends up billed twice and the door ends up guessing. - A pass may sit alongside a monthly, and at the door the **unlimited membership wins** - a member who paid for unlimited entry would be furious to watch a paid session disappear. ## Holds Injury, travel, a broken leg. - A hold pushes the end date out **when it finishes**, not when it starts - otherwise a member freezes, trains through the freeze and keeps the days. - It gives back **the days actually taken**, not the days asked for. A fortnight abandoned after two days is worth two days. - A hold booked for next week **starts itself** on the day, so the door refuses while it runs. One booked entirely in the future gives nothing until it has begun. - A **finished hold cannot be edited or deleted**. The allowance counts the days that were credited, so the same fortnight cannot be handed out twice by typing over an old row. - A hold that is scrapped gives back no days. ## Leaving Giving notice moves the end date to the end of the **notice period** and the membership stays live and billable until then. The end date only ever moves **outwards**: a year that has been paid for is never cut back to thirty days. The days that run past what was already invoiced are **charged**, or the member trains through the notice for nothing. Notice can only be given on a live membership, and only once. A plan with no notice period ends on the spot. ## Classes - Capacity can never exceed the **studio's fire limit**, and a trainer cannot teach two rooms at once. - A full class puts the next member on a **waiting list**; the moment a seat frees up it goes to whoever waited longest - **skipping anyone whose membership has lapsed in the meantime** and carrying on down the list, with a note saying so. A lapsed member at the head of the queue never swallows the seat. - **Cancelling late costs a session**, inside a window the manager sets - and a **no-show costs the same**, because the seat was held either way. Charging the phone call and not the empty chair rewards the wrong behaviour. - Ticking somebody in from the **waiting list** cannot push a class over its capacity, and cannot be done for a membership that has lapsed. - A member who cancels **can book the class again**; only live bookings are unique. - If the *gym* cancels the class, every seat is released and nobody pays for it. - Closing the register marks whoever was booked and did not turn up as a **no show**, and a scheduled job closes classes that are over so the list is true in the morning without anybody ticking it. ## One visit, one session A session credit is spent in exactly **one place** - the door - and **one visit a day costs one session**, enforced by a unique index rather than by hope. Scanning in for a class you booked marks that register, so somebody who turned up is never closed off as a no-show an hour later. Ticking the register goes through the same code, so a member who scans in and is ticked off by the trainer is charged once. Deleting a visit gives a paid session back, and deleting a booking that carries a late-cancellation or a no-show would too, so both are a **manager's** job. Credits never go negative. ## Billing The invoice names the period it covers, and the same period is never invoiced twice. A scheduled job raises the invoice for a period that has started - **off by default**, because letting software bill people should be a decision - and it never bills a membership that is on hold. ## Roles **Trainer** runs the timetable and the register and reads the medical note - and can actually do all of it, which is worth saying because a role that cannot open its own screens is not a role. Every price, on the plan and on the membership, is hidden from them at **field level**. **Reception** works the door, sells and renews memberships and invoices them, and needs **no accounting role** to do it: the figures the desk sees are computed with sudo rather than by handing a receptionist the whole ledger. **Gym manager** configures plans, classes and studios, and is the only role that can delete a paid visit or a charged booking. Two sites in one database stay two sites: the door only ever looks at memberships of the gym it is standing in, and each site's invoices are raised in its own company. The medical note is protected at field level, not by hiding a page, and it is never tracked into a chatter everybody follows. ## Community only Depends on `product`, `account` and `base_setup` - all Community. Tested on 16.0, 17.0, 18.0, 19.0 and 20.0. ## How we make sure it works Before every release, **148 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A hold that already gave its days cannot give them twice. - One trainer cannot run two sites at the same hour. - A trainer cannot teach two rooms at once. - Notice cannot be given twice. - No form asks the client about a field it cannot see. 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: - Open the door on an unpaid membership when overdue money is set to block - overdue, not merely unpaid. - Show the front desk a crash dialog: a refusal or a server error is a line on the panel. ## Install 1. Copy `bambooforge_gym` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Gym & Fitness Membership Management**. 3. Open **Gym** - its menus are Front desk, Timetable, Members, Memberships. ## 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, Invoicing, Products, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Gym** menu (Front desk, Timetable, Members, Memberships). ****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. --- # Helpdesk & Ticket Management for Community > A helpdesk for Odoo Community: tickets by mail and portal, targets counted in working hours, escalation with a name on it, and safe merges. - **What it is:** A helpdesk for Odoo Community: tickets by mail and portal, targets counted in working hours, escalation with a name on it, and safe merges. - **For:** Extra Tools - **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:** 149 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_helpdesk - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Count a paused ticket as late: the clock stops while you wait on the customer.; Treat moving a ticket to another column as an answer.; Deflect tickets with a knowledge base: that is a separate app, coming.; Send a rating request twice for one close, or to a cancelled ticket. - **Manual:** https://docs.bambooforge.dev/bambooforge_helpdesk/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_helpdesk.action_bf_helpdesk_team - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_helpdesk - **Manual last updated:** 2026-09-21 --- # Helpdesk & Ticket Management for Community Odoo Community has no helpdesk. This is one. It is built around the three things that decide whether a support desk is trusted rather than merely installed: the clock, the mailbox, and who can read what. ## The clock ### Working hours, not wall-clock hours Every service target is a number of **working hours on the team's own calendar**. Four hours promised at 16:00 on a Friday is 11:00 on Monday - one hour left on the Friday, three on the Monday morning - and public holidays on that calendar are skipped like any other closed day. A desk that measures in wall-clock hours reports itself late every weekend and stops being believed within a month. The team falls back to the company calendar when it has none of its own, so a fresh install already counts correctly. ### The clock stops while you are waiting on the customer Mark a stage **Waiting on Customer** and every running target on a ticket in that stage is paused. When the customer answers, the time spent waiting is measured in working hours and **given back**: each deadline moves forward by exactly that much. How long a ticket was paused is on the ticket, in hours, so the number can be argued with rather than believed. The escalation cron knows about the pause too: a paused ticket past its deadline is not reported as late, because you are not late for somebody else's silence. ### What counts as an answer The first-reply target is met by **a message the customer can read**, written by somebody on our side. An internal note is not an answer. Moving the ticket to another column is not an answer. The customer writing again is not an answer. This is the single most commonly faked number in helpdesk software, and the one a customer can check against their own inbox. ### Missing a target has a name on it When a deadline passes, the target is marked missed **once**, with how many working hours it went over, and an activity is created for the escalation user of that policy - or the team leader if the policy names nobody. Not a red colour on a list nobody opens. Running the cron again does not pile up a second activity. ### Reopening starts a new clock, and keeps the old one A reopened ticket gets a **new resolution target**, counted from the reopening, recorded as attempt 2. The rows from attempt 1 are left exactly as they were: last month's report does not change because somebody reopened a ticket in March. The first-reply target is not re-issued. It was promised once and met once; re-issuing it would let a reopen quietly rewrite the number. ### Reclassification Raise a ticket to Urgent and the urgent policy attaches **counted from now**, not backdated to a creation time when nobody had promised anything. A note goes in the chatter saying so. Targets already running are never withdrawn by editing a tag: a promise made is a promise kept. ## The mailbox Each team has an alias. Mail to it opens a ticket in that team; replies land on the ticket they belong to, because the desk answers through the chatter and the mail headers do the rest. An **unknown sender never silently creates a contact**. The name and the address are kept on the ticket, and a human decides whether that is a customer. A helpdesk that creates a partner per inbound mail turns a contact database into a spam trap within a quarter. A known address is matched to its contact, and the customer is added as a follower so the answer reaches them. ### A receipt, once A ticket opened by email is answered at once with its number and a link to follow it, so the customer knows the mail arrived and has something to quote. Per team, on by default, with a template you can replace. It is sent **once per ticket** and **never to a machine**: the raw headers are read before the ticket exists, and anything marked `Auto-Submitted`, `Precedence: bulk`, a bounce, or a no-reply address gets no receipt. The receipt marks itself the same way. Without both halves, a desk that acknowledges mail acknowledges the out-of-office reply to its own acknowledgement, and two mail servers spend the weekend talking to each other. Addresses the customer copied are kept on the ticket as *Also on the thread* - kept, not turned into contacts. ## Answering faster ### Canned responses *Helpdesk -> Configuration -> Canned Responses* The answer the desk gives forty times a week, written once. A canned response is HTML with placeholders - `{{ object.partner_name }}`, `{{ object.number }}`, `{{ object.name }}`, `{{ object.user_id.name }}`, `{{ object.team_id.name }}`, `{{ user.name }}` - a shortcut agents type to find it (`/fixed`), the teams it belongs to (none means every team), and what happens after it is sent: move the ticket to a stage, add tags, or close it with a reason. On a ticket, **Canned reply** opens the picker. The text is rendered for *this* ticket before anything is sent, and the agent can edit it there. A response marked *Send to customer* is a reply the customer reads and counts as a first reply; one that is not is posted as an internal note and counts as nothing. Managers write responses; agents use them. ### A rating after every solved ticket When a ticket closes with a reason that counts as solved (or no reason at all), the customer gets **one mail with three links**: Great, Okay, Bad. One click records the answer; the page it lands on takes an optional comment. The score, the comment and the time land on the ticket, and a note in the chatter says what came back. Rules, because a satisfaction number is easy to inflate: - asked **once per close** - reopening resets it, so the next close asks again; - **not asked** for a cancelled ticket, a ticket with no address, or a team that turned it off (*Ask for a Rating When Closed*, per team); - a change of mind counts for **a day**; after that the first answer stands; - the link carries the ticket's own access token, so nobody rates somebody else's ticket. The team card shows **CSAT** - the share of ratings that were Great - and how many ratings it rests on. Tickets filter and group by rating. ### Suggest a reply *Suggest a reply* on a ticket lists the **solved tickets of the same team whose subject shares words with this one** - the desk's own answer bank, ranked by how many words match and how recent they are. Pick one and the last thing the desk wrote to that customer becomes the draft; edit it and press Send, or send it as a note. Nothing is posted until then. With **AI Assistant** installed and configured (a key, or a local server such as Ollama, in which case nothing leaves the building), the same wizard gains **Draft with AI**: the model reads the ticket, the last messages and the similar tickets' answers and writes a draft. The ticket gains **Summarise**, which posts five lines for whoever picks it up next as an internal note. Both are installed by the *Helpdesk: AI reply drafts and summaries* bridge, by itself. ### WhatsApp and SMS as a channel With the *Marketing Automation: SMS & WhatsApp* add-on installed, a team can be pointed at one of its providers (Twilio or Meta). The number becomes a channel: - an inbound message from a number with an **open** ticket on the team is added to that ticket as the customer's message, and takes it off hold like any other answer; - any other message **opens a ticket** on the team, first line as the subject, with the number on the ticket; a contact whose mobile or phone matches is attached, and nobody is invented otherwise; - every reply an agent sends to the customer on such a ticket is **sent to the number**, and logged on the ticket's *Messages sent* tab with the provider's answer; a failure is a note on the ticket, not silence; - STOP words are honoured before anything else, the way the add-on does. The ticket's **Channel** says where it came from - email, portal, WhatsApp, SMS, or an agent - and the list groups by it. Installed by the *Helpdesk: WhatsApp and SMS channel* bridge, by itself. ## Who can read what ### Agents and managers An **Agent** works tickets: reads, answers, moves and closes them. An agent cannot delete a ticket and cannot change teams, stages or targets. A **Manager** does all that, plus configuration, plus deleting - which is refused on a closed ticket unless it is a manager doing it, because a closed ticket is history. ### Teams that are nobody else's business A team is readable by **every agent** by default, which is how most desks actually run. Set it to **its own agents** and a team handling payroll, legal or staff complaints becomes invisible to the rest of the desk - a record rule, not a hidden menu. ### The customer portal A customer sees their tickets at `/my/tickets`: the conversation, not the back office. Internal notes are filtered **on the server**, by message subtype, before anything is rendered - not hidden by a widget that a future version might render differently. A customer can add a message, close their own ticket, and open a new one. A customer **cannot set the priority**: letting them would let anybody buy themselves a one-hour target. Writing back on a ticket that was waiting on the customer moves it out of that stage automatically, so the desk sees it again and the clock restarts. By default a portal user sees the tickets of their whole company, which is what a customer's operations team expects. Tick **Private to the contact** and a ticket stays with the person named on it - a complaint about a colleague is not for the whole company's portal. There is a plain rate limit on new tickets from the portal: ten in an hour from one contact, after which the form says so. ## Merging Two reports of the same fault are merged from either ticket. The messages, the files and the followers **move** onto the ticket you keep; the others are closed with the reason *Merged into another ticket* and carry a link to the winner. Nothing is deleted. Tickets from two companies are refused: their documents and their books are separate, and a merge would move one company's messages into the other's. Different customers are allowed but warned about in the wizard, in as many words, because sometimes they really are the same fault. ## Housekeeping Two scheduled jobs, both opt-in, both isolating failures per record so one bad ticket cannot stop the run: - **Close tickets nobody answered.** Set a number of days on the team. Tickets sitting in a *Waiting on Customer* stage with no word back for that long are closed with the reason *No answer from the customer*. Tickets the desk still owes an answer are never touched. - **Delete closed tickets past retention.** Set a number of months in Settings, per company. Closed tickets older than that are removed with their messages and files. Zero - the default - keeps everything for ever, which is the right default for anybody who has not thought about it yet. ## What it needs `base`, `mail`, `portal`, `resource` and `base_setup`. All Community. No Enterprise module is required, and none is used. ## How we make sure it works Before every release, **93 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A team cannot be left without a way out. - The portal list hides notes and keeps the order. - A reply nobody receives is not an answer. - A team with history cannot be deleted. - A used target policy cannot be deleted. 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: - Count a paused ticket as late: the clock stops while you wait on the customer. - Treat moving a ticket to another column as an answer. - Deflect tickets with a knowledge base: that is a separate app, coming. - Send a rating request twice for one close, or to a cancelled ticket. ## Install 1. Copy `bambooforge_helpdesk` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Helpdesk & Ticket Management for Community**. 3. Open **Helpdesk** - its menus are Overview, Tickets, Reporting, 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, Portal, Resource, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Helpdesk** menu (Overview, Tickets, Reporting, 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. ## What changed in 1.2 - **Canned responses** with placeholders and a macro (move, tag, close), rendered on the ticket before they go, scoped per team. - **A rating after every solved ticket**: one mail, three links, once per close; CSAT on the team card. - **Suggest a reply** from the team's own solved tickets; **Draft with AI** and **Summarise** with the AI Assistant bridge. - **WhatsApp and SMS as a channel** with the messaging bridge: inbound opens or continues a ticket, replies go back to the number. - The **channel** a ticket came in on is on the ticket. --- # Hotel Management & Room Booking (PMS) > Hotel management and PMS: reservations that cannot double-sell a room, seasonal rates, a folio billed per night, housekeeping and night audit. - **What it is:** Hotel management and PMS: reservations that cannot double-sell a room, seasonal rates, a folio billed per night, housekeeping and night audit. - **For:** Industries - **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:** 149 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_hotel - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Sell the same room twice: the database itself refuses overlapping sales, and a room type cannot sell more rooms than it has.; Overbook by accident - only by the number a manager types on the room type.; Sleep more people in a room than it holds. - **Manual:** https://docs.bambooforge.dev/bambooforge_hotel/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_hotel.action_bf_hotel_arrivals - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_hotel - **Manual last updated:** 2026-10-15 --- # Hotel Management & Room Booking (PMS) Two rules decide whether a hotel can use the software at all: a room cannot be sold twice for the same night, and nobody checks out owing money that was never written down. Everything here supports those two. ## Reservations - Guest, dates, room type, and an **optional room number** - a booking can be held on the type and get its number at check-in, the way a hotel really works. - **Overlapping sales of the same room are refused** by the database itself, with an exclusion constraint over the room and the dates: two clerks assigning the same room in the same second cannot both be told it was free. A cancelled or no-show booking frees the room immediately. - **Bookings held on a room type are counted too.** Confirming a booking checks every night of it against the number of rooms of that type, so a hotel cannot quietly sell fourteen doubles it does not have. Overbooking is allowed by a number the manager types on the room type, never by accident. - A booking that sleeps more people than the room holds is refused: a fire certificate is not a preference. - **Assign a free room** picks a room of the right type that is genuinely free for the whole stay, preferring one that is clean. ## Seasonal rates The same room is not the same price in August. Rates apply per room type over a date range with a **minimum stay**, and where two rates cover a night the lower sequence wins - that is how a promotion beats the season. A stay that crosses a season is priced **night by night**. ## The folio - One line per night at *that night's* rate, plus extras and city tax. - Invoiced in one click; a line that has been invoiced can no longer be edited or deleted - corrections belong on the invoice. - Nights are never posted twice, however often the button is pressed. - Everything on a folio is a **charge**. Deposits and payments belong in Accounting, against the invoice the folio raises, where they are reconciled and can be seen. - Check-out posts the bill, so what the guest is handed has a number on it, and every invoice a stay raised is one click from the booking. Crediting an invoice makes exactly its charges billable again. ## Housekeeping Housekeeping is a **separate state from occupancy**, because a room can be occupied and dirty, or free and not yet cleaned. Checking a guest into a room housekeeping has not released is refused. Checking out puts the room back into "dirty" automatically, and a room with a guest in it cannot be taken out of order. ## Night audit A scheduled job that does what the night manager used to do at 2am: it posts the night that has just been slept and turns yesterday's unarrived bookings into no-shows, so the morning report is true without anybody staying up. Tonight is not posted, because it has not happened. The business date is read through the **company's timezone**, so a hotel at UTC+7 closes its own day and not London's. A **guaranteed** booking - a card guarantee, a prepayment, a corporate contract - is never released by the job: a guest on a delayed connection still has a room at 23:00. A release is stamped and can be undone with **Reinstate**, which checks whether the room is still there rather than pretending it is. ## Roles **Housekeeping** sees the room board and changes room status, and never sees a rate or a folio. **Reception** books, checks in and out, posts charges and invoices. The **manager** configures room types and rates. ## Requirements Odoo Community 16.0, 17.0, 18.0, 19.0 or 20.0 with account and product. No Enterprise module, no external service. ## How we make sure it works Before every release, **77 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A booking the clerk cannot see still blocks the room. - A room cannot be sold twice. - A room cannot be sold by a hotel that does not own it. - A room sold for tonight cannot be taken out of order. - A room type cannot be sold more often than it has rooms. 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: - Sell the same room twice: the database itself refuses overlapping sales, and a room type cannot sell more rooms than it has. - Overbook by accident - only by the number a manager types on the room type. - Sleep more people in a room than it holds. ## Install 1. Copy `bambooforge_hotel` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Hotel Management & Room Booking (PMS)**. 3. Open **Hotel** - its menus are Today's desk, Reservations, Room board, Housekeeping. ## 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, Invoicing, Products, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Hotel** menu (Today's desk, Reservations, Room board, Housekeeping). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - answered within two business days. OPL-1; one purchase covers one production database plus its staging copies. --- # Intrastat & EC Sales List | EU Trade Declarations > Monthly Intrastat arrivals and dispatches plus the EC Sales List, built from posted invoices, with commodity codes, net mass and a file to file. - **What it is:** Monthly Intrastat arrivals and dispatches plus the EC Sales List, built from posted invoices, with commodity codes, net mass and a file to file. - **For:** Accounting - **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:** 199 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_intrastat - **Not for:** Region lists are per country and are not shipped: each member state publishes its own, and most do not ask for one at all.; The CN8 catalogue is not shipped either. It changes every year and runs to thousands of lines; the codes a given company uses are usually a few dozen.; Net mass comes from the product weight. A company that does not maintain weights will file zeros, which most offices reject — the declaration shows the total so this is visible before you send it. - **Manual:** https://docs.bambooforge.dev/bambooforge_intrastat/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_intrastat.action_bf_intrastat_declaration - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_intrastat - **Manual last updated:** 2026-09-21 --- # Intrastat & EC Sales List | EU Trade Declarations ## What this is for Two filings that Odoo Community does not ship and that are not optional for a company trading inside the EU: - **Intrastat** — a monthly count of goods physically crossing a border between two member states, owed to the national statistical office by every company above the national threshold. - **EC Sales List** — a recapitulative statement of turnover billed to VAT numbers in other member states, including services, which Intrastat never sees. Both are built from invoices that are already posted. Nothing has to be typed twice. ## Setting up 1. *Accounting ▸ Configuration ▸ Intrastat ▸ Commodity Codes* — add the CN8 codes you actually trade under. A code that needs a supplementary unit (pairs, items, litres) should say so here; a declaration filed without it is rejected. 2. *Accounting ▸ Configuration ▸ Settings ▸ Intrastat & EC Sales List* — set the transaction nature, transport mode and, if your office asks for one, the region. These become the default on new invoices. Set the two national thresholds in the same place. 3. On each product, set the commodity code and the country of origin. The country of origin is where the goods were **made**, not where you bought them — it has been required on dispatches since the 2022 rules. ## Filing a period *Accounting ▸ Reporting ▸ EU Declarations ▸ Intrastat* Create a declaration, pick the month and the direction, and press **Generate**. The lines are grouped the way the office wants them: one line per commodity code, partner country, country of origin, transaction, transport and region, with the value in your company currency and the net mass in kilograms. Before exporting, the declaration tells you two things it will not let you ignore: - how many products in the period still have no commodity code, and which ones; - whether the period is below the national threshold, in which case there is nothing to file. **Export File** produces either a CSV or a harmonised INSTAT XML. Where an office wants its own envelope, add a `_bf_render_` method and a selection value — the exporter is a plug-in point, not a fixed format. ## Returns and corrections A credit note for goods that physically come back is declared on the **opposite** flow as a positive line, not as a negative on the original one. That is what the offices ask for and what keeps the two flows agreeing with the VAT return. A customer credit note therefore appears in the *arrivals* declaration. A credit note that moves no goods — a price correction, a rebate, a discount — must not create an arrival. Tick **Exclude from Intrastat** on that document. It still counts on the EC Sales List, which measures turnover rather than movement. ## Who belongs on the EC Sales List Only customers who gave you a VAT number. That is what the statement is for: an intra-Community supply is zero-rated because the customer accounts for the tax under their own number, and that number is what the tax office matches your line against. A customer with no VAT number is a private buyer. Their purchase is a distance sale, taxed at the rate of *their* country, and it is declared on the One Stop Shop return — not here. The statement leaves them off on purpose, and says so: the header shows how many customers were left off and what turnover they represent, so the figure can be reconciled against the ledger without going looking for the difference. If a customer who should have a number does not have one, that is a gap in your master data rather than a filing question. Fix it on the customer and generate again; the statement will pick them up. Filing is blocked in exactly one case: a VAT number that was there when the statement was generated and has since been cleared. The file would then go out with a blank number, which is the one thing certain to be rejected. ## The monthly scheduler Filing deadlines fall within days of a period closing, so the work has to be waiting for whoever files it. A daily scheduled action opens and fills last month's arrivals and dispatches on the first of the month, for every company that has Intrastat configured. Running it twice does not duplicate a period. ## Known limits - Region lists are per country and are not shipped: each member state publishes its own, and most do not ask for one at all. - The CN8 catalogue is not shipped either. It changes every year and runs to thousands of lines; the codes a given company uses are usually a few dozen. - Net mass comes from the product weight. A company that does not maintain weights will file zeros, which most offices reject — the declaration shows the total so this is visible before you send it. ## How we make sure it works Before every release, **39 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Customer without vat is not on the statement. - Filed declaration cannot be regenerated. - Filed statement cannot be regenerated. - Left off turnover is shown not hidden. - Missing commodity code is reported not silent. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_intrastat` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Intrastat & EC Sales List**. 3. Open **Intrastat** - its menus are Commodity Codes, EC Sales List, Transaction Natures. ## 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 Invoicing app. No extra Python package. ****Where does it show up?**** Under the **Intrastat** menu (Commodity Codes, EC Sales List, Transaction Natures). ****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. --- # Inventory Insights: Stock KPIs & Analysis > One weekly stock snapshot that says what to do: ABC-XYZ classes, FIFO age, days of cover, dead and excess value, and a ranked action list. - **What it is:** One weekly stock snapshot that says what to do: ABC-XYZ classes, FIFO age, days of cover, dead and excess value, and a ranked action list. - **For:** Warehouse - **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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_inventory_insights - **Manual:** https://docs.bambooforge.dev/bambooforge_inventory_insights/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_inventory_insights.action_bf_inv_snapshot - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_inventory_insights - **Manual last updated:** 2026-10-15 --- # Inventory Insights: Stock KPIs & Analysis One review of your stock that ends with a **ranked list of decisions**, not another dashboard. Every stocked product is classified on four axes at once — value (ABC), demand stability (XYZ), FIFO age and days of cover — and the combination becomes a single recommendation per product with the money attached. This page is the complete manual: install, take a review, read every number, and act on it **without contacting support**. ## What a review answers Most inventory reports answer *what is*: how much stock, how old, how it is classified. The question a stock manager actually has on Monday morning is *what should I do this week, and in which order*. A review answers that: | Recommendation | When it appears | |---|---| | **Reorder** | Cover has fallen below a third of your target and nothing is inbound. Whatever else is true of the product, a stockout costs a sale, so this outranks everything. | | **Liquidate** | Nothing has shipped for longer than your dead-stock delay, and there is still stock on hand. The value shown is the money you get back by clearing it. | | **Stop buying** | Cover is above the target — the excess quantity, valued at cost, is what you are financing for nothing. | | **Watch** | High value with erratic demand (A + Z). No rule will plan this well; a human should look at it. | | **Nothing to do** | Cover inside the target, moving, not old. Most of the catalogue should land here. | Every line carries the reason in words, and the **value at stake** — so the list sorts itself by money and you stop when the time runs out. ## Install 1. Copy `bambooforge_inventory_insights` into your addons path (or install the ZIP), update the app list and install **BambooForge Inventory Insights**. 2. Give yourself **Inventory Insights: Analyst** (*Settings → Users → Other*). Everyone with inventory access can read a review; analysts can take one. 3. The **Inventory Insights** menu appears with *Inventory Reviews* and *Products*. The app needs only `stock`. It reads stock moves, quants and pickings you already have — there is nothing to configure on products first, and nothing is written back to them. ## Take a review *Inventory Insights → Inventory Reviews → New* | Field | What it changes | |---|---| | **As of** | The date the review looks at. Yesterday's review stays as it was — reviews are snapshots, not live reports. | | **Period (days)** | How far back consumption is measured, for demand, cover and variability. 365 for a yearly view, 90 for a fast-moving catalogue. | | **Warehouse** | Empty means every warehouse of the company. | | **Product categories** | Empty means the whole catalogue. Child categories are included, so one review per family is two clicks. | | **A / B cut-off (%)** | The classic 80/15 split of value. Change it and the classes move with it. | | **Cover target (days)** | How much stock you *want* to hold. Above it is excess; below a third of it, with nothing inbound, is a shortage. | | **Dead after (days)** | No issue for this long, with stock on hand, is dead stock. | | **Variability buckets** | The period is cut into this many equal slices; the variation of consumption across them decides the XYZ class. Twelve monthly slices over a year is a good default. | Press **Compute**. A few seconds later the review carries its totals — stock value, dead value, excess value, shortages, OTIF — and **What to do** opens the ranked list. Recomputing replaces the lines; nothing accumulates. **Reset** empties the review and puts it back to draft. ## How each number is worked out ****On hand and stock value**** From the quants in scope, with the quant's own valuation when your Odoo keeps one, and quantity × cost otherwise. ****Demand, per day, days of cover**** Every *done* move out of an internal location to a customer, production or inventory-loss location inside the period. Cover is the quantity on hand divided by the average daily consumption; a product that never moved has no cover, not an infinite one. ****ABC**** Products are ranked by the value of what moved (falling back to stock value when nothing moved) and cut at the cumulative shares you set. The product that crosses the 80% line is still an A item — that is what the cut-off means. ****XYZ**** The period is split into buckets and the coefficient of variation of consumption across them is measured: below `X up to CV` demand is steady, below `Y up to CV` it is variable, above that it is erratic. A product with no demand at all is Z. ****FIFO age**** Receipts are walked from the newest backwards until they cover the quantity on hand; the receipt that closes the gap dates the oldest unit still in stock. A product received two years ago and again last week is a week old if only last week's units are left — the age of the *stock*, not of the product. ****Excess**** Everything above `cover target × daily consumption`, valued at cost. A product with stock and no consumption at all is excess in full. ****Shortage**** Cover below a third of the target with nothing inbound. Inbound means any incoming move that is neither done, cancelled nor draft. ****OTIF**** Each outgoing move with a picking counts once: on time when the picking was done no later than its scheduled date, in full when it left no backorder behind. The review's headline OTIF weighs products by how many deliveries they had. ## Reading the list *Inventory Insights → Products* opens every line of every review; a review's **What to do** button opens just the ones that need a decision. - Group by **Recommendation** for the weekly meeting, by **ABC class** for a purchasing review, by **Age** for a write-off discussion. - The pivot (ABC × age bucket, measured in stock value) is the one slide that usually ends the "is our stock healthy" argument. - Filters ship for shortages, dead stock, excess, class A and erratic demand. Every column is exportable, and the lines are ordinary records: add them to a spreadsheet, a dashboard, or a scheduled action of your own. ## Automating it A cron **Inventory Insights: weekly snapshot** ships disabled. Enable it in *Settings → Technical → Scheduled Actions* and every company gets a review each week — the reviews then form a history you can compare: dead value falling, cover converging on the target, OTIF moving. ## Troubleshooting ****A product I expect is missing**** Reviews only look at storable products with stock or movement in the period, inside the chosen warehouse and categories. ****Everything is class A**** There is only one product in scope, or all products carry the same value. Widen the categories, or check that costs are set: with a zero cost every product weighs the same. ****Cover is empty for a product that clearly sells**** Its sales left the warehouse through a location type the review does not count as consumption (an internal transfer, for instance). Check the picking's destination location type. ****OTIF is 100% and I know it is not**** OTIF only counts pickings that have both a scheduled date and a done date. Deliveries validated without a scheduled date are not counted at all rather than counted as on time. ****Dead stock includes items we just received**** Dead means *nothing shipped out* for the delay, whatever came in. That is deliberate: stock received into a product nobody buys is the most expensive kind. ****The review takes a long time**** It walks every move of the period. Narrow it to one warehouse or one category, or shorten the period; a 90-day review over a family of products is a few seconds. ## What changed in 1.1 - **The recommendation became a button.** Every line now computes a textbook reorder point (average demand over the vendor's lead time plus safety stock at the class's service level) - and *Write Reordering Rules* pushes them into real `stock.warehouse.orderpoint` records the scheduler acts on. Service levels per ABC class are configurable. - **An email digest** sends the analysts the top actions by money at stake. - **Built for volume**: quantities, demand, variability and OTIF now come from grouped SQL queries instead of loading every move into Python - a catalogue of tens of thousands of products stays a report, not an outage. Stock value reads the valuation layers when they exist. - Snapshots are isolated per company. ## What changed in 1.2 - **A forecast that scores itself.** Each line carries a 30-day demand forecast (exponential smoothing over the review's own buckets) and its back-tested **MAPE** - above ~50% the demand is too erratic to trust a point estimate, and the review says so instead of pretending. - **Trend against the last review**: stock value, dead stock and excess deltas, plus the number of products whose ABC class moved. ## How we make sure it works Before every release, **58 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A stock clerk reads the review and touches nothing. - A thin cover with nothing inbound is a shortage. - A product shipped last week is not dead. - Stock within the target is not excess. - An inventory adjustment is not demand. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — questions, bug reports and feature requests are answered by the developers who wrote the module. --- # Jewellery Management | Jewelry Store ERP > Jewellery retail: pieces weighed to their metal, priced off today's rate with an expiring quotation, old gold tested, memo stock, repairs. - **What it is:** Jewellery retail: pieces weighed to their metal, priced off today's rate with an expiring quotation, old gold tested, memo stock, repairs. - **For:** Industries - **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:** 139 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_jewellery - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Keep a price list: prices come from the metal rate of the day and the making charge.; Edit or delete a rate that has been entered or has priced a bill.; Price a metal with no rate. - **Manual:** https://docs.bambooforge.dev/bambooforge_jewellery/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_jewellery.action_bf_jewel_piece - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_jewellery - **Manual last updated:** 2026-09-21 --- # Jewellery Management | Jewelry Store ERP Built around the four places an independent jeweller quietly loses money: the rate, the scale, the memo and the bench. ## The rate - There is no price list. The **rate of the day** per metal is a dated record, with where it came from and who entered it. - A rate that has been entered **cannot be edited**. This morning's quotations were priced on it; a correction this afternoon rewrites what they were based on. Enter the new rate as a new line. - The rate in force is the last one quoted before now. A metal with no rate cannot be priced at all. - A **quotation expires** after the shop's window. Confirming a stale one is refused; it is repriced instead, and the note says what that did to the figure. - A sale is confirmed **at the rate that is the rate**. If the rate has moved since the line was priced - even inside the window - the sale is repriced first, because the gap between the two is the shop's margin in whichever direction it falls. - The clock on a quotation is moved by repricing it and by nothing else. Typing a later date onto a stale quotation would make this morning's price look like this afternoon's, so the counter cannot, and the owner's correction is tracked. - A rate that priced a bill **cannot be deleted** either, and deleting a metal does not take its rate history with it. ## The scale - Every piece carries **gross, stones and net** weight. Only the net weight is metal, and it is what every valuation is worked out from. Fine weight - net at the metal's fineness - is what it is worth if it goes back in the pot. - Stones are recorded in **carats per stone** and converted to grams (one carat is a fifth of a gram, times the number of stones on the line) for the weight, because the scale does not know about carats. A pavé line of six five-pointers weighs all six. - The price is **metal at the rate, plus making, plus stones**. Making is a percentage or a fixed figure; wastage is separate and explicit. Both are the **owner's figures**: the counter sells at them without being shown them. - A **discount cannot exceed the making charge**. Below that the shop is selling metal at a loss. It comes off the bill as it is typed - and so does a piece swapped for another one, which is repriced at once rather than going out at the first piece's price. - A **sold piece is locked**: its weight, metal, stones and hallmark are what the customer has. ## The paperwork - A piece at or over the threshold weight **cannot be sold without a hallmark recorded**. Zero switches the check off for markets that do not require it. - A diamond at or over the threshold carat **needs a certificate number** before the piece goes in the window - including one written in small and grown afterwards. ## The bill, once it is agreed - A confirmed sale is **the bill the customer agreed to**. Its pieces, its old gold, its deposit, its customer and its date do not change afterwards: a piece added later would never be checked or marked sold, and a piece taken off would leave stock sold to nobody. Cancelling puts the pieces back in the window; deleting is refused. - Two tills cannot sell the same piece. The pieces on a sale are **locked while it is confirmed**, so the second till waits and then sees the piece sold rather than selling it again. - Stages are what the buttons did. Confirm, hand over, cancel, send, close: none of them can be written straight onto the record. - The invoice is a plain document - one line per piece and one credit line per lot of old gold, each carrying the weight and the rate it was worked out at, without product or catalogue lines, because a jeweller's stock is unique pieces. A **deposit already taken is carried onto it as a note**: it is money received, and it is registered as a payment like any other. ## The old gold - Metal taken across the counter is **weighed and tested** - the tested fineness, who tested it, and when. A sale with an untested exchange line cannot be confirmed. - The allowance is arithmetic: weight, tested fineness, the refiner's deduction, at the rate in force - converted to the rate's own fineness basis. - The rate it was allowed at is **kept on the line**. What was handed over the counter does not quietly revalue itself because the market moved between the quotation and the till. - Change the weight, the fineness, the metal or the deduction and the **test stamp comes off** - and so does the rate. A figure tested on the old numbers is not a test of the new ones, so it is tested and repriced again before the sale can be confirmed. - It comes off the bill, and it is a **credit line on the invoice** so the customer's copy shows what was allowed and why. ## The memo - Stock out on approval is **still yours**. It cannot be sold from the shop, cannot go on two memos, and a memo cannot close while anything on it is still out or never went out at all. - What may not be sold from the window may not be **sent out** to be sold from somebody else's: an unmarked piece over the threshold does not go on a memo, and is not recorded sold off one either. - The docket is written **before it goes**. Pieces are not added to or taken off a memo that has already gone out, and a memo that is out cannot be deleted. - A piece comes **back** - and is sellable again - or the retailer **keeps it**, which makes it a sale. - A memo overdue back lands on somebody's list, once - on the person who wrote it, or failing that the shop owner. Never on the robot: a job assigned to nobody is stock that is still out in March. A note about something else on the same memo does not count as having asked. ## The bench - A customer's repair is **weighed in and weighed out**. The weight in is written once, in front of the customer; a genuine mis-key is the owner's to correct and it is tracked, because a weight in that can be typed over on the way out is not a weight in at all. - Outside the shop's tolerance it does not go back over the counter until somebody has accepted the difference with a reason and their name against it. That somebody is the **shop owner**, there has to be a weighing-out to compare, and there has to be an actual difference: accepting one that was never measured is how a light repair goes back with a signature on it. - The stones are counted at the counter, in front of the customer. - A repair charged at far above its quote is stopped before the counter is; a repair promised for today is asked about today, on the bench's list or the counter's, and never on the robot's. ## Roles - **Counter** - sells, quotes, takes repairs in and weighs old gold. Cannot enter the rate, see or change a piece's making charge, move the clock on a quotation, correct a weight in, accept a weight difference, or send stock out on memo. - **Shop owner** - enters the rate, sets making charges and the shop's thresholds, sends and closes memos, invoices, accepts weight differences, and melts stock. - Everything is per shop: a piece, a metal, a memo or a repair from one company does not go on another one's paperwork, and the invoice is raised in the company that sold the piece. ## Installation 1. Copy `bambooforge_jewellery` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. Community only; it depends on `base`, `base_setup`, `mail` and `account`. 2. Install **Jewellery Management**. 3. Set up the metals with their fineness and enter the first rate for each; nothing can be priced until the morning's rate is in. 4. Set the six thresholds under Settings. 5. Tag the pieces in the window with their gross weight and stones. ## How we make sure it works Before every release, **144 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A memo does not close over a piece that never went. - A piece is not sold by two tills at once. - A piece on memo cannot be sold from the shop. - A metal with no rate cannot be priced. - A discount cannot exceed the making charge. 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: - Keep a price list: prices come from the metal rate of the day and the making charge. - Edit or delete a rate that has been entered or has priced a bill. - Price a metal with no rate. ## 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, Invoicing, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Jewellery** menu (Pieces, Sales, Memos, Repairs). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) --- # Construction Job Costing & Cost Control > Construction job costing: budget by phase and cost type, commitments from purchase orders, actuals from bills and timesheets, cost to complete. - **What it is:** Construction job costing: budget by phase and cost type, commitments from purchase orders, actuals from bills and timesheets, cost to complete. - **For:** Project - **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:** 179 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_job_costing - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Post accounting entries of its own: it reads posted bills, timesheets and confirmed purchase orders.; Report a cost to complete below what is already committed. - **Manual:** https://docs.bambooforge.dev/bambooforge_job_costing/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_job_costing.action_bf_job - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_job_costing - **Manual last updated:** 2026-10-15 --- # Construction Job Costing & Cost Control A contractor's question is never *what did we spend* — it is *will this job still make money*. That takes four numbers per phase, and this app keeps all four current from the documents you already raise. ## The four numbers **Budget** — what the estimate said, by phase and cost type, plus whatever approved change orders have moved. **Committed** — purchase orders confirmed and not yet billed. The money is spoken for even though no bill has arrived. **Actual** — posted vendor bills, timesheet hours at the employee's cost, and anything you enter by hand. **Cost to complete** — budget minus actual by default, never less than what is already committed; or your own figure when the site says the remaining work costs more than the paper does. From those come **cost at completion**, **variance**, **percent complete** and a **projected margin** that moves the day a change order is signed. ## Install 1. Install **BambooForge Construction Job Costing** (depends on Accounting, Purchase and Project). 2. Give the estimator and the PM **Job Costing: Manager**; site staff can get **Job Costing: User**, which reads everything and can add cost entries. 3. The **Job Costing** menu appears with Jobs, Cost to Complete, Cost Entries, Change Orders and Phases. ## Set a job up *Job Costing → Jobs → New* 1. **Job number** and **name** — the number is unique per company and is what everybody will type. 2. **Customer** and **original contract** — what was signed. 3. **Phases** — the way you cut the job up, with your own cost codes (03300 Slab, 07500 Roofing…). 4. **Budget** — one line per phase and cost type (labour, material, subcontract, equipment, other) with the estimate in *Original budget*. Press **Start job**. A job cannot start without a budget: there would be nothing to measure against. ## Let the documents do the work **Purchase orders.** Every order line has *Job*, *Phase* and *Cost type*. Confirm the order and the line becomes a **commitment** on that budget line. Cancel it and the commitment goes. **Vendor bills.** Post a bill and it becomes an **actual**. A bill made from a purchase order inherits the order line's job, phase and cost type — leave those fields empty on the bill and the commitment is released by exactly the amount billed. Reset the bill to draft and both move back. **Credit notes** reduce the actual. **Timesheets.** Link the job to a project, and every hour booked on that project carries *Job*, *Phase* and *Cost type* on the timesheet line. The cost is the employee's hourly cost times the hours, so own crews sit next to subcontractors in the same report. Correct the hours and the cost follows; delete the line and it goes. An hour with no phase is not costed — there is no budget line to put it on — and an employee with no hourly cost set is booked at zero rather than guessed. **Anything else** — plant you own, a cost you are still arguing about — can be entered by hand under *Cost Entries*, and cancelled without deleting the history. A budget line is created automatically when a purchase or a bill lands on a phase and cost type you had not budgeted. It shows with a zero budget and the full amount as variance, which is exactly what an unbudgeted commitment is. ## Change orders *Job Costing → Change Orders* A change order carries two things: the **contract change** (what the customer agreed to pay) and the **budget lines** it moves (what it will cost, by phase and cost type). The difference is the change order's own margin, shown on the form before anybody signs. Nothing moves while the change order is draft or submitted. **Approve** it and the contract value and the budgets both move — and an approved change order cannot be reopened: raise a reversing one instead, so the baseline stays honest. ## Reading the job The job form shows the totals; the **Budget** tab is the classic cost-to- complete report, red where the variance has gone negative. *Cost to Complete* opens the same lines across every job, with a pivot of phase against cost type. It is the report to bring to a Monday meeting. Every number drills down: the magnifier on a budget line opens the entries behind it, and each entry opens the purchase order or bill it came from. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Post accounting entries of its own: it reads posted bills, timesheets and confirmed purchase orders. - Report a cost to complete below what is already committed. ## Troubleshooting ****A confirmed order shows no commitment**** Its line has no job or no phase. Fill them in and confirm again, or add a manual entry. ****The commitment did not go down when I billed**** The bill line must be linked to the purchase line (bill from the order, or set *Purchase Order Line*). A bill typed from scratch is an actual on its own and cannot know which commitment it replaces. ****Cost to complete looks too high**** It never falls below what is committed: if you have ordered more than the budget, the forecast says so. Use *Revised cost to complete* to state what you actually expect. ****A timesheet hour did not reach the job**** The hour must be on a project that a job points at, and the line needs a phase. Check *Job Costing → Jobs → the job → Project*, then the *Job* and *Phase* columns on the timesheet line. ****Percent complete does not match the site**** It is cost-based (actual ÷ cost at completion). A job that has bought all its material on day one reads high; that is the nature of a cost-based measure, not an error. ****An approved change order was wrong**** Raise a second change order with the opposite amounts. The history of what was approved, and when, is worth more than a tidy list. ## What changed in 1.1 - **Progress billing (AIA G702/703 style).** Applications for payment with a schedule of values per phase (defaulted pro-rata from the budget), cumulative percent complete, previous billed, current due - and one click turns the application into the customer invoice. - **Retainage.** A percentage withheld on every application, tracked on the job, released with its own application when the contract says so. - **Earned value, the light version.** EV and CPI from the numbers the job already keeps; PV and SPI from a straight-line plan between start and planned completion - labelled as such, no false precision. ## What changed in 1.2 - **WIP schedule.** *Job Costing -> WIP Schedule*: earned revenue against billings per job (percentage-of-completion), with over/under-billing columns and totals - the sheet the accountant builds by hand at every close. - **CSI starter phases.** One button seeds a job with the MasterFormat divisions; codes you already use are left untouched. ## How we make sure it works Before every release, **55 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A job cannot start without a budget. - Read only role cannot bill. - An hour without a phase is not costed. - An approved change order cannot be edited. - An approved change order cannot be reopened. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — questions, bug reports and feature requests are answered by the developers who wrote the module. --- # Product Label Designer & Barcode Printing > Design and print product labels and barcodes without XML: drag-and-drop designer, 31 ready stocks (Avery, Dymo, Zebra), QR codes, start-at-label. - **What it is:** Design and print product labels and barcodes without XML: drag-and-drop designer, 31 ready stocks (Avery, Dymo, Zebra), QR codes, start-at-label. - **For:** Warehouse - **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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_label_designer - **Manual:** https://docs.bambooforge.dev/bambooforge_label_designer/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_label_designer.action_bf_label_template - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_label_designer - **Manual last updated:** 2026-09-23 --- # Product Label Designer & Barcode Printing Design labels for **any** Odoo record — products, lots, pickings, assets, members — and print them on the stock you already buy. Blocks are placed in millimetres, barcodes are drawn by Odoo itself, and the print dialog can start half way down a sheet you already used. This page is the whole manual: install, design, print, and fix the usual problems **without contacting support**. ## What it is Three records and nothing else to learn: ****Label stock** (*Labels → Label Stocks*)** The physical sheet or roll: page size, margins, how many labels across and down, the label size and the gaps between them. Thirty-one common stocks ship with the app — Avery A4 and US Letter, Dymo, Zebra and thermal rolls — and you can measure your own in a minute. ****Label template** (*Labels → Label Templates*)** A design for one model on one stock. You place blocks — a field, free text, a price, a barcode, an image, a box, a rule — each with its own position and size in millimetres. Publishing turns it into a normal Odoo PDF report. ****Print dialog**** How many copies, and which label on the sheet to start at. ## Install 1. Copy `bambooforge_label_designer` into your addons path (or install the ZIP), update the app list and install **BambooForge Label Designer**. 2. Give yourself **Label Designer: Designer** (*Settings → Users → Other → Label Designer*). Everyone else can print; only designers can change a template. 3. The **Labels** menu appears with *Label Templates* and *Label Stocks*. The app depends only on `base`, `web` and `product`. It works on Community and Enterprise alike, and prints with the wkhtmltopdf your Odoo already uses — no print server, no font pack, no external service. ## Design a label *Labels → Label Templates → New* 1. **Model** — what you print. `product.template` for shelf labels, `stock.lot` for lot labels, `stock.picking` for parcel labels, or your own model. 2. **Label stock** — pick the sheet you buy. The canvas immediately takes the size of one label on it. 3. **Copies** — how many labels one record gets by default. 4. **Copies from field** *(optional)* — a field holding a quantity, e.g. `qty_available`. It multiplies the number of copies asked for, which is how "one label per unit in stock" becomes a setting instead of a script. 5. **Print cut guides** — a dashed outline around each label. Leave it on while you align a new stock on plain paper; turn it off for the real run. Then place the blocks on the canvas: drag to move, pull the bottom-right corner to resize, and use the panel on the right for the details. Everything snaps to half a millimetre. | Block | What it prints | |---|---| | **Field** | Any field of the record, including a dotted path such as `categ_id.name`. Optional prefix and suffix, size, alignment, bold. | | **Text** | Fixed text: a warning, a unit, your own name. | | **Price** | A numeric field printed as money, with the record's own currency. | | **Barcode** | Code128, EAN13 or QR built from a field. The image is drawn inside the PDF — nothing is fetched over the network at print time. Tick *Print the value under the bars* for a human-readable line. | | **Image** | Any binary field: the product photo, a logo, a signature. | | **Box / Line** | A frame or a rule, in the template's accent colour. | Press **Publish**. That is the moment the design is checked: a block pointing at a field the model does not have, or an unsupported barcode symbology, refuses to publish and tells you which block is wrong. A published template becomes a real `ir.actions.report`, so it appears in the record's **Print** menu and can be called from a server action, a button or a cron. **Preview PDF** prints the first records of the model straight away, which is the fastest way to check a design against the actual stock. ## Print From a list or a form of the model: **Print → Print labels**. - **Copies** — labels per record (multiplied by *Copies from field* when the template uses one). - **Start at label** — the first *free* label on the sheet. Put the part-finished sheet back in the tray, say you are starting at label 14, and the first thirteen positions are left blank. The dialog shows how many labels and how many sheets the job will take before you commit any stock. The **Print** menu entry created by publishing prints with the template's own defaults and no dialog — handy for a one-click label from a form. ## Measuring your own stock *Labels → Label Stocks → New*, then measure the sheet with a ruler: - **Page** — the paper itself (210 × 297 mm for A4, or the label size for a roll). - **Top / left margin** — from the paper edge to the first label. - **Columns / rows**, **label width / height**, and the **gaps** between labels. The form shows how much of the page the layout uses; the record refuses to save if the labels cannot physically fit. Print one page with cut guides on plain paper, hold it against the real sheet, and adjust: printers differ by a millimetre or two, and it is always the printer, never the design. A roll printer is a stock whose page *is* one label: page 50 × 30 mm, one column, one row, no margins. Ten of those ship with the app. ## How it prints Worth knowing when something looks off by a millimetre: - Each stock gets its own paper format, built once and reused. It carries the page size, the stock's margins, and **shrink-to-fit turned off** — the PDF engine otherwise scales the page by about a fifth, which moves every label off its die-cut. - The margins live in that paper format rather than in the page, so page two of a long run lands exactly like page one. - Labels are laid out as inline blocks of a fixed size, the layout every label sheet in the world uses, and no label is ever split across a page. ## Troubleshooting ****Everything is shifted by a few millimetres**** Print one page with cut guides and compare against the sheet. If the whole grid is offset, adjust the stock's top/left margin; if the *spacing* is wrong, adjust the gaps. Some printers also apply their own scaling: check that the print dialog is at 100% and not "fit to page". ****"'x' is not a field of model.y"**** A block points at a field the model does not have — usually after changing the template's model. Open the block and pick the field again. ****"Unsupported barcode symbology"**** Only Code128, EAN13 and QR are supported. EAN13 needs exactly 13 digits (12 plus the check digit); Odoo prints nothing for an invalid value, so check the field's content first. ****The barcode is blank**** The field is empty on that record. Barcodes print only when there is something to encode. ****The price is cut off**** The block is too narrow for the amount at that font size. Widen the block or drop a point of size — the label crops rather than reflow, on purpose, so nothing ever pushes another block off the label. ****Nothing appears in the Print menu**** The template is still in draft, or *Add to print menu* is off. Publish it. ****Two templates for the same model**** Both appear in the Print menu, each under its own name. The print dialog lets you pick; a list with no template for its model shows nothing. ## What changed in 1.1 - **ZPL II output.** The print wizard's *Download ZPL* renders the same block design as ^XA jobs for Zebra-class thermal printers - text, fields, prices, Code128 (with native GS1 mode), EAN-13, QR, boxes and lines, one job per label with the right ^PQ quantity. Image blocks stay PDF-only, stated on the file itself. - Print runs above 5,000 labels are refused with a clear message instead of hanging the PDF engine. - Templates carry a chatter tracking design changes, and are isolated per company. ## How we make sure it works Before every release, **65 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A colour cannot smuggle a style declaration. - A draft template is not published by an edit. - A plain employee does not see the menu. - An unknown block type is skipped not crashed. - No print menu entry when not asked for. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — questions, bug reports and feature requests are answered by the developers who wrote the module. --- # Landed Cost by Value > Split a freight or duty bill across receipts by purchase value, get the right receipts suggested, and read the landed cost on each line. - **What it is:** Split a freight or duty bill across receipts by purchase value, get the right receipts suggested, and read the landed cost on each line. - **For:** Purchases - **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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_landed_cost - **Not for:** It does not change how landed costs are applied. The apply is Odoo's own — the journal entry, the valuation layers and the traceability are core's, untouched, so nothing about your period close changes.; It does not touch the five built-in split methods.; It splits by the purchase price on the order line; a receipt with no purchase line behind it (a manual inventory move) weighs nothing and is skipped, rather than stopping the computation. - **Manual:** https://docs.bambooforge.dev/bambooforge_landed_cost/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_landed_cost.action_bf_landed_by_lot - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_landed_cost - **Manual last updated:** 2026-10-15 --- # Landed Cost by Value Odoo's landed costs split a bill five ways — equally, by quantity, by weight, by volume, and by the stock value already on the goods. The one an importer actually asks for is missing, and the one manual step that eats the afternoon is finding which receipts a freight bill even belongs to. ## Split by purchase value A €2,000 freight bill on a container holding one cheap pallet and one expensive one should fall mostly on the expensive pallet — by what was **paid** for the goods, not by a stock valuation that on a standard-costed product may be a round number set months ago. This adds a sixth split method, **By Purchase Value**, next to the five built in. The weight of each receipt line is its purchase value — the purchase-order unit price times the quantity received, converted to the company currency — so €100 of goods and €900 of goods carry the freight in a 1:9 ratio, not 1:1. The other five methods are untouched: the new split is computed *after* Odoo's own, and only for the lines that use it. A landed cost that splits by weight splits by weight, to the cent, exactly as before. ## Suggest the receipts A forwarder's bill names no purchase order, so the receipts it belongs to have to be found — and scrolling the transfer list for the three that match is the step nobody enjoys. **Suggest receipts from the bill** proposes them: the done incoming transfers from that vendor, within a month and a half of the bill's date, that no other landed cost has already claimed. You untick the wrong ones; nothing is applied until you compute and validate as usual. ## Read it back per receipt line **Inventory › Operations › Landed Costs › Landed Cost by Move** is every landed cost that has been applied, line by line: the move, the product, the quantity, what it was worth before, the cost added, and the final cost. Group by product to answer "what did this pallet actually cost us to land", filter to one landed cost to see where a single bill went. ## What it does not do - It does not change how landed costs are applied. The apply is Odoo's own — the journal entry, the valuation layers and the traceability are core's, untouched, so nothing about your period close changes. - It does not touch the five built-in split methods. - It splits by the purchase price on the order line; a receipt with no purchase line behind it (a manual inventory move) weighs nothing and is skipped, rather than stopping the computation. ## How we make sure it works Before every release, **13 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A move with no purchase line does not crash. - Suggest is refused once the cost is settled. - Quantity counts not just unit price. - Freight and duty can land on the same receipt. - Suggest adds new receipts and keeps the held one. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_landed_cost` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Landed Cost by Value**. 3. Open **Landed Cost by Move**. ## 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 purchase_stock, stock_landed_costs apps. No extra Python package. ****Where does it show up?**** Under the **Landed Cost by Move** menu. ****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. --- # Backend Launcher: Quick App Switcher > Jump back to the record you had open this morning, from any machine: recents and favourites stored on the server, in the command palette. - **What it is:** Jump back to the record you had open this morning, from any machine: recents and favourites stored on the server, in the command palette. - **For:** Extra Tools - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_launcher - **Not for:** It does not remember list views, dashboards or reports — only records you opened. A recents list of "the sales order list, forty times" is not a recents list.; It does not remember a record you were creating and never saved. There is nothing to go back to.; It is an internal tool: portal and public users are refused at the server, not merely hidden from the menu. - **Manual:** https://docs.bambooforge.dev/bambooforge_launcher/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_launcher.action_bf_launcher_recent - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_launcher - **Manual last updated:** 2026-09-24 --- # Backend Launcher: Quick App Switcher Odoo's command palette (**Ctrl+K**) searches menus and commands. It cannot take you back to the quotation you were reading before lunch, and nothing in it survives a different browser. ## Recents, on the server Every record you open is remembered: the model, the record, its name, when you last saw it and how often. Press **Ctrl+K** and type `>`, and the records you have been working in are right there, newest first. The important word is *server*. A recents list kept in the browser's local storage is gone when you pick up the laptop, gone again when IT clears the cache, and gone a third time when you switch to the other browser you keep for the customer portal. This one follows the person: the tab you had open on the office desktop is one keystroke away at home. What it will not do is remember something you were not allowed to see. The record is read with *your* rights before it is written down, so a recents list can never become a back door into a record a rule keeps from you. The list is capped — a list nobody scrolls past twenty should not carry a thousand rows per person forever — and **Launcher › Recently Opened** shows the same thing as a list for people who would rather click, with a *Forget everything* to wipe it. ## Favourites, with a key Pin any record or any menu. Give the ones you live in a number, and **Alt+1** … **Alt+9** go straight there from anywhere in the backend — not just from the screen where a particular view happens to be mounted. Type `*` in the palette for the whole list. A favourite whose target has gone — a record somebody deleted, a menu you no longer have access to — is silently skipped rather than offered. A shortcut into an error page is worse than no shortcut. Two people can each use Alt+3 for their own thing; hotkeys are unique per person, not per database. ## Everything is yours alone Both lists are per user, and a global record rule sees to it: your recents are yours, your favourites are yours, and no report, no export and no clever domain reaches anybody else's. Global rather than attached to a group, because a rule attached to a group is OR-ed with every other group's rule — and "mine is mine" has to hold without exception. ## How it fits every series One small file of JavaScript per series, and on 17.0 through 19.0 it is literally the same file, because everything it hangs off is identical there: the `command_provider` and `command_categories` registries, the hotkey service, and the form controller's model and record id. The namespaces are `>` and `*` because the obvious ones are taken: `web` owns `/` for menus, and `mail` owns `@` for user mentions and `#` for channels wherever Discuss is installed. A recents list served under a palette whose placeholder reads *Search for a user…*, with real users listed beneath it, is not a feature — so a test pins both namespaces and fails if either of the taken ones ever creeps back in. ## What it deliberately does not do - It does not remember list views, dashboards or reports — only records you opened. A recents list of "the sales order list, forty times" is not a recents list. - It does not remember a record you were creating and never saved. There is nothing to go back to. - It is an internal tool: portal and public users are refused at the server, not merely hidden from the menu. ## How we make sure it works Before every release, **35 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A record they cannot read is never remembered. - Two favourites cannot share one key. - The hotkeys do not swallow keys they do not own. - The kind column follows the reader not the writer. - The palette namespaces are not claimed by core. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_launcher` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Backend Launcher: Quick App Switcher**. 3. Open **Launcher** - its menus are Recently Opened, Favourites. ## 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 Web app. No extra Python package. ****Where does it show up?**** Under the **Launcher** menu (Recently Opened, Favourites). ****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. --- # Laundry & Dry Cleaning Management > Laundry and dry cleaning: a tag on every garment, a care label the machine respects, a bag that cannot go home short, and a clear compensation rule. - **What it is:** Laundry and dry cleaning: a tag on every garment, a care label the machine respects, a bag that cannot go home short, and a clear compensation rule. - **For:** Industries - **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:** 129 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_laundry - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Put a garment into the shop uninspected, or into a machine that does the wrong process.; Book a garment for a service its care label does not allow. - **Manual:** https://docs.bambooforge.dev/bambooforge_laundry/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_laundry.action_bf_laundry_ticket - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_laundry - **Manual last updated:** 2026-09-21 --- # Laundry & Dry Cleaning Management Built around the four arguments every shop has at the counter. ## "That mark was already there" - Every garment is **inspected at intake** with the customer standing there, and what was found is written on the garment record. - A ticket **cannot go into the shop** while any garment on it is uninspected, and an uninspected garment cannot go into a machine. - The customer's **declared value** is asked for at the counter, not after the garment is ruined. It is the ceiling on any compensation. ## "You washed a dry-clean-only coat" - The **care label is a rule**. A garment cannot be booked for a service its label does not allow - dry-clean-only takes dry cleaning, pressing or a repair, and nothing else. - A garment cannot go into a **machine that does the wrong process**. A washer takes wash work; the dry-cleaning machine takes dry-cleaning work. - Garments go into a load through **Add garments** on the load, which offers only what may legally go in that machine - and the same refusals hold on every other route in, including writing the load onto the garment by hand or swapping the machine under a full drum. - Once a garment is in a load its **service, care label, colour and inspection stop moving**, so the record and the drum cannot end up saying different things. - A **whites load refuses a dark garment** and a darks load refuses a white one, before the drum turns rather than after a shirt comes out pink. - An **overloaded machine** is flagged: a drum past its capacity does not clean, and the garments come back grey. ## "I gave you five shirts" - Every garment is a **record with a tag**, unique in the shop. - A bag **cannot be handed over** while anything on the ticket is unfinished - counted again at the door, off the garments themselves. The count in is the count out. - Garments are **added and removed at the counter**, while the bag is still being booked in. Once it is in the shop nothing is added to the ticket and nothing is deleted from it. - A missing garment has to be **written off explicitly** as lost, which takes it off the bill and puts a claim behind it. Nothing disappears quietly. - A garment moves through **its buttons** - inspected, ready, written off - so what happened to it is a record rather than an edit, and a garment that has gone home is not editable at all. A correction is a note. ## "You said you'd look after me" - The compensation policy is **arithmetic**: a multiple of the cleaning charge, capped, and never above the value the customer declared. - The figures it is worked out from are **frozen when the bag goes into the shop**: the price, the declared value and the marks stop being editable, and the claim keeps its own copy of them. A compensation base that can be raised after the garment is lost is a compensation base nobody can defend. - An offer **above the policy needs a manager** - to offer it, to write it, and to settle it - and is recorded as above the policy either way. - **One garment, one claim.** A second live claim on the same shirt is refused, and a claim for a lost garment needs the garment written off first. - A claim carries the **machine load** the garment was in, which is usually the answer to how it happened. ## Money and collection - **Nothing leaves unpaid.** The balance is read again at the door and settled at the counter before the bag goes out of it. - What the counter **actually took** is recorded as a figure, not as a tick. Storage that runs up after the money was taken is still owed, and the balance says so. - Unless the customer is **on account** - a hotel, a restaurant, a gym - in which case the bag leaves on a signature and the work is invoiced. Who is on account is the **manager's decision**, and the ticket keeps the answer it was booked under. - **Who took the bag** is written down, with how many garments went with them. It is not always the customer, and it is the one thing nobody records. - A deposit taken at drop-off comes off the balance. A deposit **bigger than the bill**, or a bag cancelled after money was taken, is flagged as a refund rather than left as a negative. - The invoice is raised **once the bag has gone home**, so the storage that ran up while it sat there is on it. ## Uncollected bags - A daily job picks up bags finished more than the free storage period ago: the customer is **written to once**, the date is recorded, and **storage is charged from that date** - not from a date the shop never wrote to anybody about. - Every company is swept with **its own free period**, and the call lands on the person who booked the bag in rather than on the job scheduler. - Storage is **worked out when it is asked for**, so a bag that has been on the rail for six weeks shows six weeks of storage without anybody having to touch the ticket first. - That date is what a disposal has to be able to show later. A shop that cannot produce it is disposing of somebody else's property. ## Roles - **Counter and shop floor** - books bags in, inspects garments, runs machines, hands bags over, opens claims. Cannot change the price list, delete a ticket, put a customer on account, or offer or settle a claim above the policy. - **Shop manager** - sets services, machines and the compensation policy, decides who is an account customer, invoices them, and settles what is above the policy. ## Installation 1. Copy `bambooforge_laundry` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. Community only; it depends on `base`, `base_setup`, `mail`, `product` and `account`. 2. Install **Laundry & Dry Cleaning Management**. 3. Set up the services first: process, turnaround, price. 4. Set up the machines with their real capacity. 5. Set the four numbers under Settings: free storage days, storage per day, the compensation multiple and the cap. 6. Mark your trade customers as account customers on their contact record before their first bag. ## How we make sure it works Before every release, **137 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A dry clean only garment cannot be booked for a wash. - A garment is not deleted once the bag is in the shop. - The price is not raised once the bag is in the shop. - A bag that is not ready cannot be handed over. - A hand wash garment cannot go through a machine wash. 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: - Put a garment into the shop uninspected, or into a machine that does the wrong process. - Book a garment for a service its care label does not allow. ## 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, Invoicing, Products, Settings apps. No extra Python package. ****Where does it show up?**** Under the **Laundry** menu (Tickets, Garments, Machine loads, Claims). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) --- # Document Layout Pack: Invoice & Quote Templates > Six more invoice and quotation layouts in the standard picker, plus per-report overrides, DRAFT/PAID/COPY watermarks and letterhead margins. - **What it is:** Six more invoice and quotation layouts in the standard picker, plus per-report overrides, DRAFT/PAID/COPY watermarks and letterhead margins. - **For:** Accounting - **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:** 119 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_layout_pack - **Manual:** https://docs.bambooforge.dev/bambooforge_layout_pack/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-web.action_base_document_layout_configurator - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_layout_pack - **Manual last updated:** 2026-09-21 --- # Document Layout Pack: Invoice & Quote Templates Six more document layouts in the standard *Configure Document Layout* dialog, a per-report layout override, a watermark, and pre-printed letterhead support. Everything else about your reports stays exactly as it is. ## Install 1. Install **BambooForge Document Layout Pack**. Nothing changes until you pick something: the company keeps the layout it had, and every existing report prints as before. 2. Open *Settings → General Settings → Configure Document Layout*: the new layouts appear beside Odoo's four, with their own preview. ## The six layouts *Settings → General Settings → Configure Document Layout* — the new layouts appear beside Odoo's four, with their own preview. ****Ribbon**** A coloured band across the top in the company's primary colour, logo knocked out on white, footer rule in the same colour. Good for a brand with one strong colour. ****Corporate**** Logo left, company details right, hairline rules top and bottom. The conservative choice for invoices and statements. ****Compact**** A single-line header with a small logo. Made for picking lists, delivery notes and anything internal where the paper should be about the content. ****Editorial**** Serif headings, a double rule and a wider left margin. For quotations and proposals that are read rather than filed. ****Slate**** A dark band top and bottom with white type. Modern, and heavy on toner — check a printed page before rolling it out. ****Letterhead**** For companies printing on pre-printed paper: leaves a blank band at the top of every page, or prints your own header and footer image full width. ## A layout per report *Settings → Technical → Reports* — each report has a **Document layout** field. Set it and that report uses that layout, whatever the company default is; leave it empty and nothing changes. Quotations in Editorial, invoices in Corporate, delivery notes in Compact is three fields, no code. ## Watermark *Settings → Companies → your company → Document Layout Pack* - **Watermark**: off, only while the document is a draft, or on everything. - **Watermark text**: DRAFT, COPY, PAID, "SPECIMEN"… Leave it empty in draft mode and the document's own state is printed instead. - **Opacity**: 8% is visible on screen and barely there on paper. Above 25% it starts fighting the text. - **Colour**: grey by default. The watermark is printed inside the page header, which the PDF engine repeats on every page — so it appears on page 7 as well as page 1. It works with the six layouts here *and* with the four Odoo ships. "Draft" means the document's own `state` is `draft`, `sent` or `quotation`. A document without a state — a partner, a product label — never carries a draft watermark. ## Letterhead Same page of settings: - **Blank top margin (mm)** — how much space the Letterhead layout leaves free at the top for pre-printed paper. - **Letterhead header / footer image** — printed full width instead of the blank band, for companies that would rather print their own. Only the Letterhead layout reads these; the other five ignore them. ## Troubleshooting ****The new layouts are not in the dialog**** They are, below Odoo's four — the dialog lists them by sequence. If the dialog is empty, the module was installed but the assets are cached: reload with Ctrl-F5. ****A report ignores its own layout**** The override applies to reports rendered through Odoo's document layout (`web.external_layout`). A report with a hand-written header of its own never consulted the company layout either, and this changes nothing for it. ****The watermark does not print**** Check the mode: *draft* only prints on documents that have a draft state. Set it to *always* and print again to confirm the settings are right. ****The watermark sits over the text**** That is what a watermark does; lower the opacity. It is behind the text in stacking order, so at 8–12% it reads as paper, not ink. ****The letterhead image is stretched**** It is printed at full page width. Export it at the page's aspect ratio — roughly 210 mm wide by whatever height you need. ****Everything is suddenly Slate**** Somebody changed the company layout, not this module: the pack never changes the current layout on install. ## What changed in 1.1 - **Per-company report layouts.** The per-report override now stores one choice per company - a two-brand group keeps two looks on the same report. - **Watermarks per report, states configurable.** DRAFT on quotations and PAID on invoices finally coexist: each report may override the mode, the text and the list of states that count as draft (add `to_approve` for purchase orders). Watermark settings are tracked in the company chatter. ## How we make sure it works Before every release, **32 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A draft watermark stays off a document without a state. - The pack does not change the default layout. - An always watermark ignores the state. - Follow company mode changes nothing. - Without an override the company layout is used. The complete list is in the source you receive. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) — questions, bug reports and feature requests are answered by the developers who wrote the module. --- # Lead Scoring for CRM > Score and grade CRM leads with a transparent rule engine: points per condition, A/B/C/D grades, automatic recompute and a per-lead breakdown. - **What it is:** Score and grade CRM leads with a transparent rule engine: points per condition, A/B/C/D grades, automatic recompute and a per-lead breakdown. - **For:** Sales - **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_lead_scoring - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Grade a score below every threshold - it gets no grade.; Mix Odoo series: a build is for the series it was bought for. - **Manual:** https://docs.bambooforge.dev/bambooforge_lead_scoring/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_lead_scoring.lead_score_rule_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_lead_scoring - **Manual last updated:** 2026-10-15 --- # Lead Scoring for CRM Stop chasing every lead the same way. This app scores your CRM leads with a transparent, configurable rule engine — points per condition — rolls the scores into **A/B/C/D grades**, recomputes automatically as leads change, and shows on every lead exactly **why** it scored what it did. The Community answer to predictive lead scoring. This page is the complete manual. Follow it top to bottom and you can install, build your scoring model, read the grades and fix the common issues **without contacting support**. ## Overview - **Scoring rules** — each rule grants (or subtracts) points to leads that match an Odoo domain you define with a visual, no-code condition builder. - **Grades** — score thresholds turn raw points into A/B/C/D grades (fully editable) so your team can sort at a glance. - **Automatic scoring** — scores refresh as the lead's fields change, on a schedule, or on demand. No manual upkeep. - **Total transparency** — every lead shows its score, its grade and a breakdown of which rules contributed how many points. - **Built into CRM** — score and grade appear on the lead form, in the pipeline and lead lists, and as a filter and group-by. ## Requirements - Odoo 20.0. - The **CRM** app (`crm`). Installing this app pulls it in automatically. No Enterprise modules and no external Python libraries are required. ## Installation 1. Copy `bambooforge_lead_scoring` into your addons path. 2. Update the apps list (Apps > Update Apps List). 3. Search for **Lead Scoring** and click *Install*. A starter set of rules and grades is created, and existing leads are scored on install. Find the configuration under *CRM > Configuration > Lead Scoring*. ## Building your scoring model **Rules.** Go to *CRM > Configuration > Lead Scoring > Scoring Rules*. Each rule has: - a **Category** (demographic, behavioral or negative) for organisation, - an **Applies When** domain — built with the visual condition editor on the lead's own fields (expected revenue, email, source, tags, job position…), - **Points** — positive to reward, negative to penalise. A lead's score is the sum of the points of every rule it matches. The shipped rules (high revenue, has email/phone, qualified to opportunity, high priority, low probability, lost) are a starting point — edit, add or remove freely. **Grades.** Go to *Grades*. Each grade has a **Minimum Score**; a lead takes the highest grade whose minimum it reaches. The defaults are A (≥ 60), B (≥ 35), C (≥ 15) and D (≥ 0) — adjust the thresholds to your funnel. ## How scoring stays current The score is a stored field that recomputes automatically whenever a lead's common fields change (revenue, email, phone, stage, source, priority, tags and more). In addition: - the **Recompute Score** button on a lead refreshes it on demand, - the **Recompute All Leads** button on a rule re-scores the whole base after you change the rules, - an optional scheduled action **Lead Scoring: Recompute Scores** (disabled by default) re-scores every lead on a schedule. Re-score the base whenever you change rules or thresholds, since existing leads keep their previous score until recomputed. ## Reading scores in CRM - On the **lead/opportunity form**, the grade badge, the score and a per-rule **breakdown** appear next to the expected revenue. - In the **pipeline** and **lead lists**, Score and Grade are available columns. - In the search panel, filter to **A - Hot** or **Positive Score**, and group by **Grade** to see your funnel split by quality. ## Transparency Unlike a black-box predictive score, every lead carries a plain breakdown such as: ``` +30 High expected revenue (≥ 50k) +10 Has an email address +15 Qualified to opportunity ``` so salespeople trust the number and managers can see exactly what the model rewards. ## Frequently asked questions ****A rule's domain won't save.**** The domain must be a valid Odoo domain on the lead. Use the visual editor to avoid syntax mistakes; invalid domains are rejected on save. ****Scores didn't change after I edited the rules.**** Existing leads keep their score until recomputed. Use **Recompute All Leads** on a rule, or enable the scheduled action. ****Can a rule use related fields?**** Yes. Domains can traverse relations, e.g. the partner's country or the source and medium, just like any Odoo domain. ****Does scoring run in real time?**** Yes, for the common lead fields the score recomputes immediately on change. A rule using an unusual field is picked up by the recompute button or the cron. ****Does it work with multiple companies?**** Scoring rules and grades are global; scores are stored per lead, which already carry their own company. ## How we make sure it works Before every release, **11 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A score below every threshold gets no grade. - The breakdown names the rules that paid. - An archived rule stops counting. - Recompute all after rule change. - The highest qualifying grade wins. The complete list is in the source you receive. ## Support Questions or ideas: **support@bambooforge.dev**. ## Usage This section walks through a full working session: configure the model, let leads score themselves, and read the results inside CRM. All configuration lives under **CRM > Configuration > Lead Scoring** (visible to users in the *Sales / Administrator* group). ### Configure scoring rules Open **CRM > Configuration > Lead Scoring > Scoring Rules**. The list opens already populated with the starter rules, colour-coded green for rules that award points and red for rules that subtract them. Each rule has: - **Name** — shown verbatim in the per-lead breakdown, so keep it descriptive. - **Category** — *Demographic*, *Behavioral* or *Negative*; for organisation. - **Points** — added to a matching lead's score; enter a negative number to penalise (the shipped *Lost / archived* rule is `-50`). - **Applies When** — an Odoo domain on `crm.lead`, built with the visual condition editor; a matching lead receives the points. Invalid domains are rejected on save. The shipped rules are *High expected revenue (≥ 50k)* (+30), *Medium expected revenue (10k–50k)* (+15), *Has an email address* (+10), *Has a phone or mobile* (+10), *Qualified to opportunity* (+15), *Marked high priority* (+10), *Very low probability (< 10%)* (-10) and *Lost / archived* (-50). Edit, reorder (drag the handle) or delete them freely. ### Configure grades Open **CRM > Configuration > Lead Scoring > Grades**. A grade has a **Minimum Score**; a lead is assigned the highest grade whose minimum it reaches. The defaults are *A - Hot* (≥ 60), *B - Warm* (≥ 35), *C - Cool* (≥ 15) and *D - Cold* (≥ 0). Adjust the thresholds to match your funnel. Each grade shows its live lead count, and the *View Leads* action opens the leads currently holding that grade. ### How the score is computed and kept current A lead's **Lead Score** is the sum of the points of every active rule whose domain it matches; its **Grade** follows from that total. Both are stored fields that recompute automatically whenever a common lead field changes — expected revenue, email, phone, stage, source, priority, tags, probability and more. To refresh manually: - press **Recompute Score** on an individual lead form, - press **Recompute All Leads** on any rule's form to re-score the whole database after changing rules or thresholds, or - enable the scheduled action **Lead Scoring: Recompute Scores** (under *Settings > Technical > Scheduled Actions*, disabled by default; runs daily). Existing leads keep their previous score until recomputed, so re-score the base after any rule or threshold change. ### Reading scores in CRM - On the **lead/opportunity form**, the grade badge, the numeric score and the per-rule **Score Breakdown** appear just after Expected Revenue. The badge is green for a score ≥ 60 and blue below 35. - In the **pipeline** and **lead lists**, *Lead Score* and *Grade* are optional columns (shown by default). - In the search view, use the **A - Hot** and **Positive Score** filters and the **Grade** group-by to split your funnel by quality. ### Worked example An opportunity with expected revenue 60,000, an email address and high priority matches *High expected revenue*, *Has an email address*, *Qualified to opportunity* and *Marked high priority*, scoring `30 + 10 + 15 + 10 = 65`. As 65 ≥ 60 it is graded **A - Hot**, and its Score Breakdown reads: ``` +30 High expected revenue (≥ 50k) +10 Has an email address +15 Qualified to opportunity +10 Marked high priority ``` If the deal is later lost and archived, the *Lost / archived* rule (-50) applies and the score drops to 15, moving the lead down to grade **C - Cool**. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_lead_scoring -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The fields, views and analysis definitions this module added. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. ### What is preserved - Your standard Odoo data (partners, products, sales, invoices, inventory moves) is **not** deleted — this module only reads and annotates it; the underlying records stay. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## What it does not do Said before you buy, because it is the half that decides whether the app fits: - Grade a score below every threshold - it gets no grade. - Mix Odoo series: a build is for the series it was bought for. ## 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 CRM app. No extra Python package. ****Where does it show up?**** Under the **Scoring Rules** menu (Grades, Lead Scoring). ****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. ## Changelog ### 20.0.1.1.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Score and grade CRM leads with a transparent, configurable rule engine. - Points per condition, A/B/C/D grades, automatic recompute and a per-lead breakdown. The Community answer to predictive lead scoring. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). --- # SEO All in One with Internal Link Suggester > Turn a phrase into an internal link wherever it appears - once per page, never in a heading - with a ledger, one-click undo and orphan report. - **What it is:** Turn a phrase into an internal link wherever it appears - once per page, never in a heading - with a ledger, one-click undo and orphan report. - **For:** Website - **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:** 198 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_link_suggester - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Link inside a link, a heading, a button, code, a script or a text area, or on the page a rule points at.; Match part of a word: door never matches doorway.; Touch a link somebody wrote by hand - it only ever edits anchors it inserted. - **Manual:** https://docs.bambooforge.dev/bambooforge_link_suggester/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_link_suggester.action_bf_link_rule - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_link_suggester - **Manual last updated:** 2026-10-15 --- # SEO All in One with Internal Link Suggester An add-on for **BambooForge SEO Pro**. SEO Pro says which pages are worth linking from a page. This puts the links in - carefully, reversibly, and with a written record of every change. ## Install 1. Install **BambooForge Internal Link Suggester** (it needs SEO Pro). Nothing is edited on install: rules are empty and nothing is applied until somebody presses a button. 2. Open *SEO Pro → Orphan Pages*, and under Configuration, *Link Rules* and *Inserted Links*. ## Rules *SEO Pro → Configuration → Link Rules*. A rule is a **phrase** and the **page it should point at**: | Field | What it does | |---|---| | Phrase | The words as they appear in the copy. Whole words only: *door* never matches *doorway*. Three characters minimum, six words maximum. | | Target page / Target URL | Pick an indexed page and the URL follows it, or type a path. | | Links per page | One to five. One is almost always right. | | Case sensitive | Off by default, so *Oak Door* at the start of a sentence is the same phrase. | | Website | Leave empty to apply everywhere. | ## Applying them On an SEO page: **What would the rules link?** shows what would happen and changes nothing. **Apply link rules** does it. Select several pages in the list and apply to all of them at once. A phrase is **never** linked: - inside a link that is already there; - inside a heading, a button, code, a script or a text area; - on the page the rule points at; - more times than the rule allows, counting links that already exist. The HTML is parsed rather than pattern-matched, so the markup around the phrase survives exactly as the author wrote it - and so does their capitalisation. ### What can be edited Anything SEO Pro indexes whose copy lives in an HTML field: website pages (through the view attached to them), blog posts, product descriptions. A record whose copy is not in a field the module can find is skipped rather than half-edited. ## The ledger, and undo *SEO Pro → Configuration → Inserted Links*. Every link this module made: which rule, which page, which words, who pressed the button and when. **Remove the link** takes the anchor out and leaves the words. It only touches anchors this module inserted - they carry a marker - so a link somebody wrote by hand to the same page is never disturbed. If an editor has rewritten the paragraph and the link went with it, the row says **gone from the page**. That is information, not an error: the rule can put it back, or you can decide the editor was right. ## Orphan pages *SEO Pro → Orphan Pages* lists the pages no other page links to. A crawler can only reach them through the sitemap, which is where it goes last and ranks lowest - and they are usually the pages somebody published in a hurry. Pages marked *noindex* are not reported: a page you asked Google to ignore does not need internal links. Inbound counts are refreshed nightly, and on demand from SEO Pro's own analysis. ## Anchor text The anchor report answers "what does this site call its own pages?". Twenty links to the same page all reading *click here* is one finding. Twenty reading the identical keyphrase is a different and more expensive one - that pattern is what an over-optimisation penalty looks like from the outside. ## Multi-website and multi-company Rules belong to a website or to all of them; a rule written for one site never fires on another. The ledger, the orphan list and the anchor report are all filtered by the websites a user can see and by company through the website's company. ## How we make sure it works Before every release, **40 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The same phrase and target cannot be written twice. - A read only seo user cannot edit the copy. - Applying twice does not link the same page again. - A read only seo user cannot undo either. - One page that cannot be edited does not sink the batch. 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: - Link inside a link, a heading, a button, code, a script or a text area, or on the page a rule points at. - Match part of a word: door never matches doorway. - Touch a link somebody wrote by hand - it only ever edits anchors it inserted. ## 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 Web; seo pro (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Link Rules** menu (Inserted Links, Orphan Pages). ****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. ## Support [support@bambooforge.dev](mailto:support@bambooforge.dev) - the people who wrote it. --- # List View Pro: Better Odoo List Views > Better list views: column presets per person or role, frozen columns, totals on every numeric column, and export of exactly what is on screen. - **What it is:** Better list views: column presets per person or role, frozen columns, totals on every numeric column, and export of exactly what is on screen. - **For:** Extra Tools - **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:** 99 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_list_view_pro - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Apply a preset inside x2many tabs or select dialogs - a preset applies to the list view where the menu is.; Change what a user may see: frozen columns, totals and export work on the rows Odoo already shows them.; Share a preset by default - presets are per person, per role. - **Manual:** https://docs.bambooforge.dev/bambooforge_list_view_pro/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_list_view_pro.action_bf_list_preset - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_list_view_pro - **Manual last updated:** 2026-10-15 --- # List View Pro: Better Odoo List Views The optional-columns menu is where every team's morning starts and where it is lost again by lunch. This remembers it for them — per person, per role — and adds the three things that menu never had: frozen columns, totals on every numeric column, and an export of exactly what is on screen. ## Presets Open any list, press the cog (Favourites on Odoo 20), and choose **Manage columns**. The dialog lists every column the view knows: tick what shows, move it up or down, choose how many stay frozen on the left, tick *Totals*. Give it a name and save. A preset is **mine** by default — nobody else sees it. A **manager** ticks *Share with everybody*, and the preset then reaches every user, or only the roles it names in the back office (Settings › Technical › Column Presets): "the warehouse sees these ten columns; accounts sees those seven" is one shared preset per role, each marked **Applied by default**. Which preset applies when somebody opens a list: 1. the one they last picked themselves on this browser; 2. otherwise their own default; 3. otherwise the shared default that reaches them; 4. otherwise the plain view. Picking **Plain view** is remembered too — it is a choice, not an absence of one. Two *indistinguishable* defaults — same owner, same roles, same list — are refused at save time, because the one that loads would be whichever the database returned first. Two shared defaults aimed at different roles are fine; that is the whole point. When two of them reach the same person, the lower sequence wins, and the field says so. **Only this view.** A preset is per model by default, which is also every x2many tab and every select dialog of that model — so the preset never applies there: it applies to the list of a *list view* only, where the menu is, where it can be undone. Tick *Only this view* in the dialog and it applies to that one list view — Vendor Bills and not Customer Invoices — rather than to every list of the model. A default that is per model loads by itself only on a list that has every column it names: a list built for one job — the comparison of a tender's quotations, say — keeps its own columns, and the preset is still in the menu to pick by hand. Touching the optional-columns dropdown while a preset is active means "I am leaving the preset": the toggle then works from the plain view, and what the browser remembers is the person's own choice, never the preset's. ## What a preset decides, and what it does not A preset decides **which field columns show and in what order**. Buttons and handles keep their place. A column the preset names that the view does not have is simply skipped — the same preset on a list that gained or lost a field still works. A preset never changes **what the data is**: the domain, the grouping, the sort, the record rules. It is a way of looking, and it lives in the list renderer, not in the view definition — so it hooks **any** list, stock or custom, without an inherited view per model. ## Frozen columns The first *N* columns stay put while the rest scroll sideways — header and body together. The sticky offsets are measured from the real column widths after the table is in the DOM, because guessing them from the arch is how frozen columns overlap on the second screen width you try. ## Totals without editing the view Tick **Totals on numeric columns** and every integer, float and monetary column carries a sum — in the footer, and per group when the list is grouped — exactly the way a `sum=` attribute on the field would do it, without anyone editing a view. Monetary columns keep the currency rules the standard footer has: mixed currencies show a dash, not a lie. ## Export what you see **Export visible columns** downloads an xlsx of the visible columns in their order, for the records that match the current search, with no dialog and no field picker. It is its own endpoint rather than a replay of the standard export payload — that payload's shape moves between series, and this one has a single job. Access is **never** sudo: the export reads through `export_data`, so a person gets exactly the rows and fields their rights allow and nothing else. A column they are not allowed to read fails the export rather than arriving blank. ## Security Every internal user is a *List View Pro user*: saves personal presets, applies shared ones. A *manager* writes the presets everybody sees. Ownership of what the dialog saves is decided on the server — a non-manager who sends `shared: true` gets a personal preset, whatever the payload said. Record rules scope reads to *mine, or shared with a role I hold*, and writes to *mine*; a company on a preset scopes it to that company. ## One file per series The list renderer keeps `getActiveColumns()` from Odoo 16 to 20, so that is the one deep patch point. Everything else is feature-detected at runtime — the cog menu exists from 17 and 16 gets the same item in Favourites; the owl state shape of the renderer changed in 19 and the refresh path probes for it rather than assuming. ## What it needs `web` only. Community. No Enterprise module is required, and none is used. ## How we make sure it works Before every release, **32 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A user cannot publish to everybody through the dialog. - A user cannot rewrite a shared preset. - A view of another model is refused. - Garbage is refused not 500. - A manager editing a personal preset does not steal it. 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: - Apply a preset inside x2many tabs or select dialogs - a preset applies to the list view where the menu is. - Change what a user may see: frozen columns, totals and export work on the rows Odoo already shows them. - Share a preset by default - presets are per person, per role. ## Install 1. Copy `bambooforge_list_view_pro` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **List View Pro: Better Odoo List Views**. 3. Open **Column Presets**. ## 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 Settings app. No extra Python package. ****Where does it show up?**** Under the **Column Presets** menu. ****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. --- # Listmonk Connector > Two-way subscription and opt-out sync between Odoo and Listmonk, so an unsubscribe on either side is honoured on both. Import lists too. - **What it is:** Two-way subscription and opt-out sync between Odoo and Listmonk, so an unsubscribe on either side is honoured on both. Import lists too. - **For:** Marketing - **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_listmonk_connector - **Run for real:** Tested for real against two listmonk releases (2 and 6): 5 lists and 8 subscribers, then 4 lists and 14 contacts, came into Odoo with 0 errors; the v6 run showed it wants a dedicated API user, which the manual now covers. - **Not for:** The connector reads lists and subscribers from your Listmonk server and writes the corresponding Odoo mailing lists and mailing contacts. It never writes to Listmonk — there is no export and no two-way sync.; The API token/password is stored in a system-only (admin-only) field.; In scope today: list import (→ mailing.list), subscriber import (→ mailing.contact) with resolved list membership and preserved Listmonk metadata, scheduled import + reconciliation, dry-run, validation and rollback.; Out of scope / roadmap: write-back / export to Listmonk; inbound real-time webhooks; incremental list filtering by modified date (v1 lists by page size / offset). - **Manual:** https://docs.bambooforge.dev/bambooforge_listmonk_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_listmonk_connector.action_listmonk_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_listmonk_connector - **Manual last updated:** 2026-10-15 --- # Listmonk Connector A read-only bridge from Listmonk into Odoo 20's Email Marketing: import your Listmonk **lists** as Odoo **mailing lists** and your Listmonk **subscribers** as Odoo **mailing contacts** — over the official Listmonk REST API, with a resilient job queue, dry-run safety, validation and rollback so you stay in control. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first import, automate it, and fix the common issues **without contacting support**. ## Overview The connector pulls newsletter data from Listmonk into Odoo over the Listmonk JSON REST API. It is **import-only**: data flows **Odoo ← Listmonk** and the connector never writes back to Listmonk. It covers two entities: - **Lists** → `mailing.list` — each Listmonk list becomes an Odoo mailing list. The list name is imported, and the raw Listmonk metadata is kept on the record so the source of truth is auditable: external id, uuid, list type (public/private), opt-in mode (single/double) and the last observed subscriber count. - **Subscribers** → `mailing.contact` — each Listmonk subscriber becomes an Odoo mailing contact (name + email). The raw Listmonk status (enabled / disabled / blocklisted) is kept, and the contact's `list_ids` are resolved from the subscriber's Listmonk lists against the lists you already imported. Because list links are resolved by lookup, **lists are always imported before subscribers**, so each subscriber's list membership lands correctly. Direction of sync: **Odoo ← Listmonk** (pull-only). Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ## Tested against the real thing Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records. Tested for real against two listmonk releases (2 and 6): 5 lists and 8 subscribers, then 4 lists and 14 contacts, came into Odoo with 0 errors; the v6 run showed it wants a dedicated API user, which the manual now covers. ## Requirements - **Odoo**: 20.0, Community or Enterprise. The connector depends on *Email Marketing* (`mass_mailing`), which installs automatically. - **Listmonk**: a reachable Listmonk server. Works with both Listmonk v2 (config admin username + password) and v3+ (API user name + token) — both are Basic-style credentials (see *Step 1*). - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can use the connector screens. The API token/password is stored in a system-only field, so only the **Settings / Administration** user can read or change it. - **Network**: outbound HTTPS from Odoo to your Listmonk server. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ## Installation 1. Copy `bambooforge_listmonk_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *Listmonk*, and press **Activate / Install**. The dependency *Email Marketing* installs automatically. No Listmonk server is required to evaluate the connector: a mock Listmonk REST API ships inside the module, so you can install, explore and run the full import flow against sample data before pointing it at a real server. To use it, set the instance **Base URL** to `/listmonk/mock_api` with **API Path** `api` and any Basic-auth user/token. ## Step 1 — Set up API access on Listmonk The connector authenticates with **HTTP Basic** auth, sent on every request as the `Authorization: Basic base64(user:token)` header. There is no OAuth and no API-key query parameter — just a username and a secret. **Listmonk v3 and newer (recommended):** 1. Sign in to your Listmonk admin UI as a super-admin. 2. Go to **Settings ▸ Users ▸ API users** and create an **API user**. 3. Give it a role that can **read** lists and subscribers (read access is all the connector needs — it never writes to Listmonk). 4. Copy the **API user name** and the generated **token**. The token is shown once. 5. In Odoo, enter the API user name as **API User** and the token as **API Token / Password**. **Listmonk v2:** 1. Use your Listmonk **config admin username** and **admin password**. 2. In Odoo, enter the admin username as **API User** and the admin password as **API Token / Password**. In both cases the connector sends the pair as HTTP Basic over the wire, so use HTTPS in production to keep the credential confidential. ## Step 2 — Create the connection in Odoo Open **Listmonk Connector ▸ Configuration ▸ Instances** and create a record. ![Listmonk instance / connection cockpit](01-connection-cockpit.png) Key fields: | Field | What to enter | |---|---| | Name | A label for this server, e.g. *Production Listmonk*. | | Base URL | Your Listmonk server root, e.g. `https://lists.example.com` (or `http://localhost:9000` for a local server, with *Allow internal host* on). | | Authentication | *HTTP Basic (user + token)* — the only mode. Sent as the `Authorization: Basic` header on every request. | | API User | The Listmonk Basic-auth username — the API user name (v3+) or the config admin username (v2). | | API Token / Password | The Listmonk Basic-auth secret — the API user token (v3+) or the admin password (v2). Visible to administrators only. | | API Path | The Listmonk REST API path. Leave the default `api` unless your server differs. | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach a Listmonk server on localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. The connector calls `GET /api/lists?per_page=1` with your credentials. A green *Connected* state means the URL and Basic credentials are correct; the connector also seeds the default subscriber-status mappings on first connect. If it fails, the exact error is recorded on the form and in **Logs** (see *Troubleshooting*). ## Step 3 — First import (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead of creating Odoo records they produce **Validation Results** you can review under **Listmonk Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, click **Import Lists** first, then **Import Subscribers**. Importing lists first lets each subscriber's list membership resolve. These buttons enqueue jobs; they do not block the UI. 2. Jobs are processed by the **Listmonk Queue Processor** scheduled action (every minute), or immediately if you run the processor manually from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land in the standard **Email Marketing** app: lists under **Email Marketing ▸ Mailing Lists**, contacts under **Email Marketing ▸ Mailing List Contacts**. Each carries its Listmonk external id, so re-imports update the same record instead of duplicating it. ## Field mapping & customization - **Field Mappings** (Configuration) map Listmonk fields to Odoo fields per entity. Use **Generate suggested mappings** on the instance to seed the business-critical ones, then adjust. - **Schema Fields** lists the discovered fields per entity. Run **Schema Introspection** on the instance to refresh it: the connector flattens a live Listmonk sample record (falling back to the bundled mock when the server is unreachable) and lists the Odoo target fields beside it. - **Subscriber Status Mappings** (seeded on first connect) record how each raw Listmonk subscriber status (enabled / disabled / blocklisted) is interpreted. The raw status is always kept verbatim on the contact in `listmonk_status`. - **Listmonk metadata** is preserved verbatim on each imported record (external id, uuid, list type, opt-in mode, subscriber count on lists; uuid and status on contacts) so you can audit exactly what came from Listmonk. ## Automation (scheduled actions) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | Listmonk Queue Processor | every 1 min | Processes queued import jobs. | | Listmonk Reconciliation | every 15 min | Pulls recent remote lists/subscribers for enabled entities and re-queues anything missing or previously failed. | | Listmonk Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs. | | Listmonk Flow Scheduler | every 5 min | Runs scheduled import flows. | | Listmonk Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | Listmonk Auto Recover | every 15 min | Reopens a tripped circuit breaker once the cooldown passes and no blocking validation issues remain. | Turn on **Auto import** / **Auto reconcile** for lists and/or subscribers on the instance to let the scheduled actions keep things in sync hands-free. **Webhooks**: Listmonk does not push entity webhooks for list/subscriber imports, so this connector is **pull-only** — it imports on the schedule above. A `/listmonk/webhook` route exists but is a neutralized placeholder: it acknowledges with HTTP 200 and imports nothing, so existing subscriptions or health checks do not 404. Do not rely on it to import data; use the scheduled import / reconcile flows instead. Inbound webhooks are a roadmap item. ## Safety features - **Dry-run mode** (`Dry-run`, ON by default) — simulate writes and review Validation Results before going live. - **Business validation profiles** (`Business validation profile`: *Minimal / Standard / Strict*) — gate risky writes; *Standard* is the default. - **Resilient queue** — every remote action is a job. API calls retry with exponential back-off (`API retry limit` / backoff / cap), and failed jobs retry up to `Retry limit` before moving to the dead-letter state. - **Circuit breaker** (`Circuit state`) — after repeated failures, or any auth/configuration failure, the instance auto-pauses (*Tripped*). The Auto Recover action reopens it after a cooldown once no blocking issues remain; or click **Resume**. - **Rollback snapshots** (`Rollback enabled`, on by default) — imports can capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots**. - **SSRF guard** — the connector refuses internal/loopback/private/link-local/ reserved hosts unless **Allow internal host** is explicitly enabled. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with 401/403 | Wrong Basic credentials, or the Listmonk user lacks read access. Re-check Step 1: **API User** must be the API user name (v3+) or admin username (v2), and **API Token / Password** the matching token/password. An auth failure also trips the circuit breaker, so click **Resume** after fixing. | | "Base URL ... is not allowed because it resolves to the non-public address" | Base URL points at localhost or a private/loopback IP. Enable **Allow internal host** on the instance (self-hosted/test Listmonk only). | | "the URL must start with http:// or https://" | The Base URL is missing its scheme. Enter the full `https://lists.example.com`. | | SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off **Verify SSL** for testing only. | | Jobs stay in *Pending* | The Queue Processor is off, the instance is paused, or the circuit breaker is tripped. Check **Scheduled Actions** is active, that the instance is not *Paused*, and **Resume** if needed. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures (or an auth/config error). Fix the server/credentials; Auto Recover reopens it after the cooldown, or click **Resume**. | | Subscribers import but have no lists | Subscriber list links resolve against lists already in Odoo. Run **Import Lists** first, then **Import Subscribers** again. | | Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | | Records imported twice | Imports are keyed by the Listmonk external id, so this should not happen. If it does, check that two instances do not point at the same Listmonk server. | | "Listmonk rate limit reached" / server errors | Listmonk returned 429 or a 5xx. The client already retries with back-off; if it persists, lower the import limits or batch size and retry later. | For anything else, **Operations ▸ Logs** records every API call, payload and error with a timestamp. ## Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the Listmonk side, both v2 (config admin username + password) and v3+ (API user name + token) work, because the connector uses HTTP Basic for both. Validate your exact build with the bundled mock first. **Do I need a Listmonk server to evaluate it?** No. A mock Listmonk REST API ships inside the module. Point the instance Base URL at `/listmonk/mock_api` (API Path `api`, any Basic credentials) to run the full import flow against sample data. **How does authentication work?** HTTP Basic only. The connector sends `Authorization: Basic base64(user:token)` on every request. Use HTTPS so the credential is not exposed in transit. **Does the connector write anything back to Listmonk?** No. It is strictly read-only / import-only. It only reads lists and subscribers from Listmonk and creates/updates the matching Odoo mailing lists and contacts. **Is there real-time sync via webhooks?** Not in v1. Listmonk does not push entity webhooks for list/subscriber imports, so the connector pulls on a schedule (every minute for the queue, every 15 minutes for reconciliation). The `/listmonk/webhook` route is a placeholder only. Inbound webhooks are on the roadmap. **Is it safe to run against production data?** Yes — Dry-run is ON by default, validation gates risky writes, and rollback snapshots let you undo a batch. You decide when to go live. And because the connector never writes to Listmonk, your Listmonk data is never at risk. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - The connector **reads** lists and subscribers from your Listmonk server and **writes** the corresponding Odoo mailing lists and mailing contacts. It **never writes to Listmonk** — there is no export and no two-way sync. - The API token/password is stored in a system-only (admin-only) field. - **In scope today**: list import (→ `mailing.list`), subscriber import (→ `mailing.contact`) with resolved list membership and preserved Listmonk metadata, scheduled import + reconciliation, dry-run, validation and rollback. - **Out of scope / roadmap**: write-back / export to Listmonk; inbound real-time webhooks; incremental list filtering by modified date (v1 lists by page size / offset). ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / Listmonk REST API line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_listmonk_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (22 models, prefixed `listmonk.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ### What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.0.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Two-way subscription & opt-out sync between Odoo 20 and Listmonk: unsubscribe/blocklist <-> mail.blacklist + mailing.contact opt-out (compliance-safe), plus Lists & Subscribers import. - Resilient queue, dry-run safety, auditable source. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **71 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - List and subscriber bindings do not collide. - Export dry run does not push. - Import dry run writes nothing. - Import enabled subscriber not suppressed. - Lock claims pending then refuses second claim. Tested for real against two listmonk releases (2 and 6): 5 lists and 8 subscribers, then 4 lists and 14 contacts, came into Odoo with 0 errors; the v6 run showed it wants a dedicated API user, which the manual now covers. The complete list is in the source you receive. --- # Marketing Automation All in One with Landing Pages > Marketing automation with landing pages: build one from a form in a click, enrol submissions straight into a journey, and keep the UTM. - **What it is:** Marketing automation with landing pages: build one from a form in a click, enrol submissions straight into a journey, and keep the UTM. - **For:** Marketing - **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:** 297 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_ma_landing - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Fail a visitor's submission when enrolment fails: the failure is logged and written into the journey note.; Edit the embedded form in place on the page - add a field to the form and the page shows it. - **Manual:** https://docs.bambooforge.dev/bambooforge_ma_landing/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_form_builder.action_bf_form - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_ma_landing - **Manual last updated:** 2026-10-15 --- # Marketing Automation All in One with Landing Pages An add-on for **BambooForge Form Builder** and **BambooForge Marketing Automation** (17.0). A form that enrols its submitters into a journey, a landing page built from the form in one click, and UTM on every submission. ## Enrol submitters in a journey On the form (*After submission*), pick **Enrol in journey**. The journey's audience decides what is enrolled: - *Contacts* - the contact the submission is linked to (Form Builder finds or creates it by e-mail). - *Leads/Opportunities* - the lead the form creates. The form must have *Create a CRM lead* ticked; the rule refuses the pairing otherwise. A lead journey starts the moment the lead exists, payment or not - the lead is the thing being nurtured. - *Mailing contacts* - a mailing contact found by e-mail or created with the submitter's name and e-mail. Contact and mailing-contact journeys start when the submission is **complete**: received, or paid when the form takes payment. The submission shows the **Journey participant** it became and a short **journey note** - *enrolled*, *already in the journey*, *journey not running*, *nothing to enrol (no e-mail)*. The journey honours its own rules: a contact who already finished is not re-enrolled unless the journey allows rejoin; the unique field is respected. The form's **Enrolled** button lists the participants it produced; the journey's **Forms** and **From forms** buttons show the forms feeding it and the participants they brought. Participant lists gain *From form* and *UTM campaign* columns and a *From a form* filter. ## Create a landing page *Create landing page* on the form builds a website page named after the form: a hero block, three benefit blocks, the form itself, a closing block. The blocks are ordinary website building blocks - edit them in the website editor, drag more in. The form in the middle is a **live embed**: add a field to the form and the page shows it; it is not editable in place, on purpose, so the editor cannot freeze a copy of it. The page is created **unpublished**. The form's *Landing page* button opens it; publish it from the website's publish toggle as usual. ## UTM on every submission When the visitor arrived through a link carrying `utm_source`, `utm_medium` or `utm_campaign`, Odoo's own UTM tracking sets cookies; the submission copies them into **UTM source / medium / campaign** (falling back to the referring page's query string), and records whether it came from the generated landing page. The participant carries the UTM campaign too, so the participant report can be grouped by it. ## Technical notes - Uses Form Builder 1.3.0's `_after_submit()` hook and `form_body` template, and Marketing Automation 1.9.0's `_enroll_records()` / `_participant_vals()` hooks. Nothing is copied from either. - Enrolment failure never fails the visitor's submission: it is logged and written into the journey note. - Community only. No Enterprise module, no external service. ## How we make sure it works Before every release, **24 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - The same contact is not enrolled twice. - A form without a journey changes nothing. - An unpublished form shows a notice not a crash. - No email means a note not a ghost contact. - An enrolment error is noted not raised. 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: - Fail a visitor's submission when enrolment fails: the failure is logged and written into the journey note. - Edit the embedded form in place on the page - add a field to the form and the page shows it. ## Install 1. Copy `bambooforge_ma_landing` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Marketing Automation Suite: Landing Pages & Forms**. ## 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 Web; form builder, marketing automation (BambooForge) must be installed first app. No extra Python package. ****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. --- # Marketing Automation All in One with SMS and WhatsApp > Marketing automation over SMS and WhatsApp: journey steps sent through your own Twilio or Meta account, delivery status back, STOP honoured. - **What it is:** Marketing automation over SMS and WhatsApp: journey steps sent through your own Twilio or Meta account, delivery status back, STOP honoured. - **For:** Marketing - **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:** 208 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_ma_messaging - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Start a WhatsApp conversation without an approved template - only a template can.; Move a status backwards - a late sent after delivered is ignored. - **Manual:** https://docs.bambooforge.dev/bambooforge_ma_messaging/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_ma_messaging.action_bf_messaging_provider - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_ma_messaging - **Manual last updated:** 2026-10-15 --- # Marketing Automation All in One with SMS and WhatsApp An add-on for **BambooForge Marketing Automation** (17.0). Two journey step types that send SMS and WhatsApp through **Twilio** or **Meta's WhatsApp Cloud API** with your own account, plus a **Sandbox** to rehearse on; a message log with delivery status coming back; STOP honoured. ## Gateways *Marketing Automation ▸ Gateways*. One record per sender: - **Sandbox** - writes every send to the log with a fake reference; nothing leaves the server. Use it to rehearse a journey. - **Twilio** - Account SID, auth token, and *From*: a Twilio number in E.164, a Messaging Service SID (`MG…`), or your approved WhatsApp number. Channel *SMS* or *WhatsApp* (a Twilio account doing both is two gateway records). - **Meta WhatsApp Cloud API** - Phone number ID and a permanent access token. WhatsApp only. **Send test** posts one message to the *Test number* and writes it to the log: the way to prove a gateway before it meets a journey. ### Callbacks Each gateway shows two URLs (with a secret token in the path): - *Status callback* - Twilio: attached to every message automatically. Meta: statuses arrive on the webhook below. - *Inbound / webhook* - Twilio: paste as the number's "A message comes in" webhook. Meta: paste as the app's webhook URL; it answers Meta's verification GET with the *Webhook verify token* shown on the record; subscribe to *messages*. ## Steps Two new step types on a journey: - **Send SMS (gateway)** - gateway + *Message* (inline placeholders: `Hi {{ object.name }}`). - **Send WhatsApp** - gateway + either *Message* (a session message, only deliverable inside the 24-hour customer window) or a **WhatsApp template** name + language + *Template variables* (comma-separated inline expressions in `{{1}}, {{2}}` order). A template is the only way to *start* a conversation on WhatsApp; Meta approves them in your Business Manager, Twilio maps them to Content SIDs - name them as your provider expects. What happens on a participant: the number is taken from *mobile* then *phone* (19.0 dropped `res.partner.mobile`, so there it is simply *phone*); no number → skipped with a note; a blacklisted number → the participant **exits** the journey (STOP is regulatory); otherwise the message is rendered and sent, the send is logged, a trace is written, the journey advances. The **sending window** holds these steps like e-mails. *Run Test* writes "would send SMS via …" and sends nothing. ## The log *Marketing Automation ▸ Messages*: gateway, number, journey, step, body, state (*queued → sent → delivered → read*, or *failed* with the provider's error), reference, last status time. Statuses never go backwards (a late "sent" after "delivered" is ignored). Tests sent with the button are flagged and hidden by default. ## Retention The message log is swept by Odoo's daily vacuum: rows older than `bambooforge_ma_messaging.log_retention_days` (a system parameter, 365 by default) are deleted. Set it to **0** to keep everything - some regulators ask for that, and then it is a decision rather than an oversight. ## STOP An inbound `STOP` (also `STOPALL`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT`) on either platform adds the number to Odoo's phone blacklist (`phone_validation` installed) and is logged; the next gateway step for that number exits the participant. ## Honest about proof Twilio and Meta were implemented against their published REST contracts and are exercised by this module's tests against recorded stubs. The screenshots and the video run on the Sandbox gateway. Live delivery is proven on **your** account with *Send test*; no third-party credentials were used to build this module, and none are shipped. ## Technical notes - Uses Marketing Automation 1.10.0's hooks: `_messaging_step_types` (sending window), `_run_step_` dispatch, `_dry_run_label`. - Python dependency: `requests`. - Community only. The external services are the ones you already pay for. ## How we make sure it works Before every release, **33 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A gateway in use cannot be deleted. - Dry run names the gateway and sends nothing. - A normal reply is not a stop. - No number is refused before any call. - No number is skipped with a note. 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: - Start a WhatsApp conversation without an approved template - only a template can. - Move a status backwards - a late sent after delivered is ignored. ## Install 1. Copy `bambooforge_ma_messaging` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Marketing Automation Suite: SMS & WhatsApp**. 3. Open **Gateways** - its menus are Messages. ## 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 Web; marketing automation (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Gateways** menu (Messages). ****Does anything leave my server?**** It makes calls outside your server only where its manual says so - read the section on that service before you connect it. ****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. --- # Marketing Automation All in One with Lead Scoring > Marketing automation driven by lead score: crossing a threshold enrols a lead, removes it, or moves it to another branch, with a reason logged. - **What it is:** Marketing automation driven by lead score: crossing a threshold enrols a lead, removes it, or moves it to another branch, with a reason logged. - **For:** Marketing - **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:** 287 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_ma_scoring - **Not for:** Said before you buy, because it is the half that decides whether the app fits:; Re-enter a lead sitting on a line every run: triggers fire on the crossing between last seen and now.; Let a brand-new lead drop below a line on its first day. - **Manual:** https://docs.bambooforge.dev/bambooforge_ma_scoring/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_ma_scoring.action_bf_journey_score_event - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_ma_scoring - **Manual last updated:** 2026-10-15 --- # Marketing Automation All in One with Lead Scoring An add-on for **BambooForge Lead Scoring** and **BambooForge Marketing Automation** (17.0). A lead score that crosses a line moves the lead: into a journey, out of one, or onto another branch - with a log that says why. ## Triggers On a journey whose audience is *Leads/Opportunities*, a **Score triggers** tab. Each trigger is one line and one action: - **Score reaches N** - fires when the score goes from below N to N or above. - **Score drops below N** - fires when the score goes from N or above to below N. - **Grade becomes X** - fires when the grade changes to X. and does one of: - **Enrol in this journey** - the lead joins at the first step. - **Remove from this journey** - the running participant is exited. - **Move to step** - the running participant is put on the chosen step (due after that step's delay). A lead not yet in the journey is enrolled first. This is how a lead that goes Hot jumps to the fast-track branch of the same journey. Two switches per trigger: - **Once per lead** (default on) - fire once per lead, ever. Off: every time the line is crossed. - **Enrol again** - enrol a lead who already finished or left this journey. Off: only leads never in the journey are enrolled (unless the journey itself allows rejoin and the lead is still running). ## Crossings, not levels Every lead remembers the score and grade the triggers last saw. A trigger fires on the **crossing** between last seen and now - a lead sitting at 80 does not re-enter *reaches 50* every ten minutes. A lead seen for the first time can *reach* a line and can *become* a grade, but can never *drop below* one: a brand-new lead at 0 does not "drop below 10" on its first day. ## When it runs - A cron every 10 minutes, on the leads whose score or grade moved since last seen - found with one SQL pass, not a scan of the table. - **Evaluate now** on the journey's triggers tab. - **Evaluate score triggers** on a lead (for the rep who does not want to wait). ## The log *Marketing Automation ▸ Score events*: one row per firing - lead, journey, trigger, score before and after, grade before and after, the action, the participant, and a note (*enrolled*, *moved to 'Fast track'*, *not enrolled: already finished or left*, …). The lead shows **Journeys** and **Score events** buttons; the journey shows **Score events**; each trigger shows how often it fired. A failing trigger never stops the others: the error is logged, the event is not written, and the lead stays a candidate for the next run. ## Technical notes - Uses Marketing Automation 1.9.0's `_enroll_records(records, rejoin=)` and the participant's `_schedule(step)`; Lead Scoring's stored `lead_score` / `lead_grade_id`. Nothing is copied. - Three fields on `crm.lead`: score seen, grade seen, evaluated. - Community only. No Enterprise module, no external service. ## How we make sure it works Before every release, **20 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Without rejoin a finished lead is not enrolled again. - Enrol on a running lead is a no op with a note. - Jump on a lead not in the journey enrols first. - Threshold equal to the score is reached not dropped. - A stopped journey fires nothing but marks seen. 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: - Re-enter a lead sitting on a line every run: triggers fire on the crossing between last seen and now. - Let a brand-new lead drop below a line on its first day. ## Install 1. Copy `bambooforge_ma_scoring` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Marketing Automation Suite: Lead Score Journeys**. 3. Open **Score events**. ## 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 Web; lead scoring, marketing automation (BambooForge) must be installed first app. No extra Python package. ****Where does it show up?**** Under the **Score events** menu. ****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. --- # Marketing Automation: Email Journeys & Campaigns > Drip campaigns and customer journeys on Odoo Community: audience filters, multi-step email sequences with delays and conditions, goal exits. - **What it is:** Drip campaigns and customer journeys on Odoo Community: audience filters, multi-step email sequences with delays and conditions, goal exits. - **For:** Marketing - **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:** 149 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_marketing_automation - **Manual:** https://docs.bambooforge.dev/bambooforge_marketing_automation/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_marketing_automation.action_bf_journey - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_marketing_automation - **Manual last updated:** 2026-09-21 --- # Marketing Automation: Email Journeys & Campaigns Drip campaigns and customer journeys for Odoo 20, running entirely on Community: an audience filter enrolls contacts, mailing contacts or CRM leads automatically (including records created after launch), ordered steps send mail templates or SMS after configurable delays and optional conditions, opens and clicks are tracked per step, branch steps react to that tracking (*if opened*, *if not clicked*, …) or split the audience A/B, a goal filter exits participants who converted, the e-mail blacklist is respected, and every action is logged. The journey is drawn on a visual **Flow** canvas, the form shows open / click / goal rate KPIs, and a **Reporting** menu analyses enrollments and step activity as graphs and pivots. This page is the complete manual. Follow it top to bottom and you can install, launch your first journey, read the statistics and fix the common issues **without contacting support**. ## Overview - **Journeys** — a journey targets one audience model (contacts `res.partner`, mailing contacts `mailing.contact` or CRM leads `crm.lead`) with a domain-widget **Audience filter**. Lifecycle: Draft → Running → Stopped; you can start and stop at any time. - **Automatic enrollment** — pressing *Start* enrolls every record matching the filter. With *Enroll new records automatically* on (the default), a **scan every 10 minutes** also enrolls matching records created after launch. *Allow rejoin* optionally lets a record that already finished the journey be enrolled again by a later scan. - **Steps** — an ordered list (drag-and-drop sequence): each step sends a **mail template**, queues an **SMS** or runs a **server action**, after a wait delay (**hours / days / weeks**, counted from enrollment for the first step, from the previous step otherwise) and an optional *Only send when* condition — a participant that no longer matches skips the step (logged) and continues. Templates must be defined on the journey's audience model; a constraint enforces it. A *Run server action* step is executed with the rights of the user who **created the journey** — never with superuser rights. - **Tracking** — with *Open/click tracking* on (the default), e-mail steps send through an **auto-managed mass mailing** per step (Community's own tracking pixel and link tracker, no external service), giving every step live **Opened** and **Clicked** counters. Off, the template is sent directly and carries no tracking pixel — but the step's mailing still backs an **unsubscribe link** appended to the message, so both paths stay opt-out compliant. - **Run Test (dry run)** — the *Run Test* button walks the whole journey for the first record matching the audience filter and logs what each step **would** do. Nothing leaves the server: no e-mail is sent, no SMS is queued and no server action touches live data. The test participant is flagged as a test and excluded from the counters and the Reporting analyses. - **Company** — an optional *Company* on the journey: when it is set, the audience search only picks up records belonging to that company (or to no company at all). - **Branching** — a step can run *after* a parent step with a trigger: *if opened*, *if not opened*, *if clicked*, *if not clicked* — or an **A/B split** that takes a deterministic percentage of participants (the same record always lands in the same variant). Siblings are evaluated in sequence order like if / elif / else: the first trigger that matches runs, in the same processing tick. - **Goal** — an optional journey-level goal filter, checked **before every step fires**. A matching participant exits as *Goal reached* without receiving the step (e.g. the lead was won). - **Blacklist** — before sending, the participant's e-mail is checked against Odoo's standard blacklist (`mail.blacklist`). Blacklisted addresses are skipped with a logged trace; records without any e-mail address are logged as failed and the journey continues. - **Participants** — one record per enrolled record, with a state machine: *Running*, *Completed*, *Goal reached*, *Exited*, *Failed* — plus a manual *Exit journey* button. A participant whose source record was deleted exits automatically. - **Sending log** — every step execution creates a trace: *Sent* (linked to the mailing trace, or to the actual `mail.mail` when tracking is off), *Action done*, *Skipped* (condition, blacklist or unmet branch trigger, with the reason), *Expired* or *Failed* (with the error text). - **Flow canvas** — the journey form opens on a visual **Flow** tab: the step graph rendered as a flowchart, with trigger badges (*opened / not opened / clicked / A/B split*) on branch cards, wait delays and live per-step sent / opened / clicked counters on each card. Clicking a card edits the step in a dialog; “+” buttons add root steps or branches directly on the canvas, and **drag & drop** a card onto another step to re-wire it as a branch (drop it on the *Audience enters* pill to move it back to the main sequence — its trigger resets to *Always*). The classic **Steps** list sits on the next tab — both edit the same records. - **Statistics** — the kanban card shows participants, e-mails sent and goals per journey; the form adds running / completed counters, stat buttons and **open rate / click rate / goal rate** KPIs, and each step shows its own **Sent / Opened / Clicked counts**. - **Reporting** — an **Automation ▸ Reporting** menu with two analyses, each as graph and pivot: **Participant Analysis** (enrollments by week, stacked by status, test participants excluded by default) and **Step Activity Analysis** (sent / skipped / expired / failed per step). - **Access rights** — *Marketing Automation / User* (read journeys and the log, manage participants) and *Marketing Automation / Manager* (full configuration). ## Requirements | Item | Detail | |---|---| | Odoo | 20.0, Community or Enterprise. Only the standard CE `mass_mailing` app is required — the Enterprise *Marketing Automation* app is **not** needed and is not touched if present. | | CRM leads audience | Selecting *Leads/Opportunities* as audience model requires the (free, CE) **CRM** app to be installed; a validation error tells you if it is missing. | | SMS steps | Require the standard **SMS** app (messages are queued through it and sent with IAP credits); a validation error tells you if it is missing. | | Python / JS | No extra libraries and no external services. Journeys run in your database only. | | Outgoing e-mail | Steps send standard Odoo mail templates — configure an outgoing mail server as usual. | ## Getting started 1. Install **BambooForge Marketing Automation**. 2. Prepare one **mail template per step** (Settings ▸ Technical ▸ Email Templates), defined on your audience model — e.g. on *Contact* with *To* set to `{{ object.email }}`. 3. Open **Automation ▸ Journeys**, create a journey: pick the **Audience model**, build the **Audience filter** with the domain editor (e.g. contacts with a given tag). 4. Build your sequence on the **Flow** tab (the “+” buttons add steps, clicking a card edits it) or on the **Steps** list — for example: *Welcome* (wait 0 hours), *Follow-up* (wait 2 days), *Last call* (wait 1 week) — each with its template and, optionally, an *Only send when* condition. 5. Optionally set a **Goal filter** (e.g. lead is won) and review *Enroll new records automatically* / *Allow rejoin*. 6. Press **Start**. Matching records are enrolled immediately; the cron then sends due steps and keeps enrolling newcomers every 10 minutes. Watch the counters on the form, the **Participants** button and **Automation ▸ Sending Log**. ## Usage ### How a journey processes a participant Every 10 minutes the scheduled action *Marketing Automation: process journeys* handles each running journey: it first enrolls new matching records, then processes every participant whose wait has elapsed (up to 500 per journey per run). For each due participant, in order: 1. **Goal check** — if the record matches the goal filter, the participant exits as *Goal reached*; the step is not sent. 2. **Branch trigger** — on a branch step with an open/click or A/B trigger, the parent step's tracking data for this participant decides. A non-matching trigger is logged as *Skipped* and the next sibling is evaluated **in the same tick** (if / elif / else); when no sibling matches, that path ends. 3. **Step condition** — if the step has an *Only send when* filter and the record no longer matches, the step is logged as *Skipped* and the participant advances to the next step. 4. **Blacklist / e-mail check** — a blacklisted address is logged as *Skipped* (reason `blacklisted`); a record without e-mail is logged as *Failed* (reason `no e-mail`). Both advance. 5. **Send** — with *Open/click tracking* on, the e-mail goes out through the step's auto-managed mass mailing (feeding the Opened / Clicked counters); off, the template is sent as a plain mail. SMS steps queue a text message through the SMS app; action steps run their server action. The trace stores the result, and any error is logged as *Failed* — the journey continues rather than blocking. 6. **Advance** — the participant moves to the step's branches first (if any), otherwise to the next root step, and waits its delay — or becomes *Completed* after the last step. Each journey is processed independently — an error in one journey is logged and rolled back without blocking the others. Editing the flow under running participants is safe too: a participant whose current step was **deleted** completes instead of hanging, a step that was **re-parented** mid-flight simply ends that path, and a goal or *Only send when* filter pointing at a field that no longer exists fails **that participant only** (logged as *Failed*) rather than aborting the whole run. ### Run Test (dry run) *Run Test* on the journey form takes the **first record matching the audience filter**, creates a participant flagged as a test and walks it through the entire flow immediately. It is a **dry run**: every step is written to the sending log as *would send e-mail '…'*, *would send SMS* or *would run server action* — no message is sent, no SMS is queued and no server action executes, so rehearsing a journey can never mail a real customer or modify live records. Test participants are excluded from the journey counters and from the Reporting analyses. ### The Flow tab The journey form opens on the **Flow** tab: the whole step graph as a flowchart. Root steps hang under the *Start* node; branch steps hang under their parent with a **trigger badge** — *opened*, *not opened*, *clicked*, *not clicked* or *A/B split* — and every card shows its **wait delay** and live **sent / opened / clicked** counters, so you see at a glance where participants flow and where they stop. - **Click a card** to edit that step in a dialog (same fields as the Steps list). - **“+” under Start** adds a new root step; **“+” under a card** adds a branch of that step (the trigger defaults to *if opened* for branches). The **Steps** tab keeps the classic editable list — the canvas and the list edit the same step records, use whichever you prefer. ### Reporting **Automation ▸ Reporting** holds two analyses, each opening as a graph with a pivot one click away: - **Participants** (*Participant Analysis*) — enrollments **by week**, stacked **by status** (running, completed, goal reached, exited, failed). Test-mode participants are excluded by default (remove the *Exclude tests* filter to see them). Group by journey to compare campaigns. - **Step Activity** (*Step Activity Analysis*) — every logged trace counted per step and result: **sent / skipped / expired / failed** — the weak step of a sequence stands out immediately. Filter by journey or group by day to watch a launch. On the journey form itself, three KPIs summarise the funnel: **open rate** and **click rate** (opens / clicks over tracked e-mails sent) and **goal rate** (participants that exited as *Goal reached* over all participants). ### Branching, A/B splits and SMS To build a branch, add a step and set its **After step** to the parent (an e-mail step) and its **Trigger** to *if opened*, *if not opened*, *if clicked* or *if not clicked* — the trigger is evaluated when the branch becomes due, against the parent's mailing trace for that participant. Give a parent several children with increasing sequences to model if / elif / else: the first child whose trigger matches runs, the others are skipped (logged), and a child with trigger *Always* at the end acts as the *else*. After a branch chain finishes, that path ends; participants coming from a root step continue with the next root. An **A/B split** is a branch with trigger *A/B split (runs for a % of participants)* and a **Variant share (%)**: assignment is deterministic per record (a hash of journey, parent step and record), so the same record always lands in the same variant and re-processing never flips it. The rest falls through to the next sibling — typically the B variant with trigger *Always*. An **SMS step** writes its text directly on the step (inline placeholders supported, e.g. `Hi {{ object.name }}`) and queues the message through the standard SMS app to the record's mobile or phone number. Participants without any phone number are skipped with a logged trace and the journey continues. Open/click triggers react to e-mail tracking only, so branch on a parent *e-mail* step. ### Enrollment *Start* enrolls everything matching the filter at that moment. After that, the scan only examines records **created since the previous scan** (fast, even on big databases) and enrolls the matching ones — switch *Enroll new records automatically* off for a one-shot, fixed-audience campaign. A record already in the journey is never enrolled twice while it is running; with *Allow rejoin* on, a record that finished the journey may be enrolled again by a later enrollment, otherwise it is excluded for good. ### Managing participants **Automation ▸ Participants** (or the *Participants* button on the journey) lists every enrolled record with its journey, current step, next action time and state. The *Exit journey* button removes a running participant from the flow — nothing more is sent to it. Rows are colour-coded: green for *Goal reached*, muted for exited/failed. ### Reading the log **Automation ▸ Sending Log** shows every step execution: timestamp, journey, participant, step, state (*Sent* / *Skipped* / *Failed*) and the reason note. Sent traces link to the actual e-mail record, where you can open the rendered message and its delivery state (Settings ▸ Technical ▸ Emails). ### Editing a running journey The audience model is locked once the journey leaves Draft. For other changes, the safe procedure is **Stop → edit → Start**: stopping pauses enrollment and sending without losing any participant, and restarting resumes where things stood (plus a fresh enrollment pass over the audience). ### Revenue attribution A journey reports what it earned, not only what it sent. Every participant is stamped when the journey first mails it, and matched against that customer's confirmed sales orders inside **Revenue window (days)** (30 by default; set 0 to switch attribution off). The journey form shows the total earned, the number of orders and the value per participant, refreshed by its own scheduled action every six hours. Test participants and orders placed outside the window are excluded on purpose. ### Sending window Marketing mail that lands at three in the morning reads as spam. Set **Send from** / **Send until** to keep the journey inside working hours (a window may cross midnight, e.g. 20:00 → 06:00), untick **Send at weekends** to hold Saturday and Sunday until Monday, and use **Max sends per run** to cap how many participants one cron pass may action. A step that falls due outside the window is rescheduled to the next opening — never sent, never dropped. ## Configuration Journey settings: | Setting | Effect | |---|---| | Audience model | Contacts, mailing contacts or CRM leads. Locked after Draft. | | Audience filter | Domain editor; matching records enter the journey. | | Enroll new records automatically | On: the 10-minute scan enrolls matching records created after launch. Off: only the records enrolled at Start participate. | | Allow rejoin | On: a record that already finished the journey may be enrolled again by a later enrollment pass. | | Goal filter | Optional; checked before each step — matching participants exit as *Goal reached*. | | Open/click tracking | On (default): e-mail steps send through an auto-managed mass mailing per step, feeding the Opened / Clicked counters and the branch triggers. Off: the template is sent directly, with no tracking pixel and no tracking data — an unsubscribe link taken from the step's mailing is still appended. | | Company | Optional. When set, the audience search only considers records of that company (or records without a company). | Step settings (Steps tab, one row per step, drag to reorder): | Setting (per step) | Effect | |---|---| | Wait / unit | Delay in hours, days or weeks — after enrollment for the first step, after the previous step otherwise. 0 = as soon as the cron runs. | | Step type | *Send e-mail* (mail template), *Send SMS* (text with inline placeholders, queued through the SMS app) or *Run server action* (executed per participant record, with the rights of the user who created the journey). | | Mail template | Rendered per participant and sent to the record's e-mail. Must be defined on the journey's audience model. | | After step / Trigger | Turns the step into a branch: it runs after the parent step, when the trigger matches — *Always*, *if opened / not opened / clicked / not clicked* (against the parent e-mail's tracking) or *A/B split*. Siblings are tried in sequence order, if / elif / else. | | Variant share (%) | A/B split only: percentage of participants taking this branch, assigned deterministically per record; the rest falls through to the next sibling. | | Only send when | Optional condition; a participant that no longer matches skips the step (logged) and continues. | | Sent / Opened / Clicked (read-only) | Live counters per step; Opened and Clicked come from the step's auto-managed mass mailing when tracking is on. | Scheduling: the cron **Marketing Automation: process journeys** runs every 10 minutes and processes up to 500 due participants per journey per run. Adjust the interval under **Settings ▸ Technical ▸ Scheduled Actions** if you want faster reaction or lighter load. Access rights: **Marketing Automation / User** reads journeys, steps and the log and can manage participants (e.g. exit them); **Marketing Automation / Manager** additionally creates and configures journeys and steps and can clean the log. ## Install 1. Copy `bambooforge_marketing_automation` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **Marketing Automation: Email Journeys & Campaigns**. 3. Open **Automation** - its menus are Journeys, Participants, Sending Log, Reporting. ## Troubleshooting ****Nobody was enrolled.**** The journey must be **Running** (a Draft journey does nothing) and the audience filter must match — open the same filter on the audience model's list view to verify. Note the automatic scan only looks at records **created after** the journey started; existing records are enrolled by pressing *Start* (stop and start again to re-run a full enrollment pass). ****Participants are enrolled but nothing is sent.**** Sending happens when the step's wait has elapsed **and** the cron runs — with a 10-minute interval a "0 hours" step can take up to 10 minutes. Check the cron is active (Settings ▸ Technical ▸ Scheduled Actions), an outgoing mail server is configured, and the template's *To* field resolves (e.g. `{{ object.email }}`). ****A participant was skipped.**** That is the module doing its job — open **Sending Log** and read the trace: `blacklisted` (the address unsubscribed), `no e-mail` (the record has no address) or a *Skipped* trace on a step whose *Only send when* condition no longer matched. ****The goal never triggers.**** The goal is evaluated **when a step becomes due**, not continuously — a participant that converts after the last step simply completes. Verify the goal filter matches by testing it on the audience model's list view. ****I cannot pick my template on a step.**** Step templates are filtered to the journey's audience model. Recreate the template on the right model (e.g. *Lead/Opportunity* instead of *Contact*). ****Leads/Opportunities is rejected as audience.**** Install the free **CRM** app first; the journey validates that the audience model is available on your database. ****Opened / Clicked counters stay at 0.**** *Open/click tracking* must be on for the journey — steps sent as plain template mails carry no tracking. The counters come from the standard mass-mailing infrastructure: an open registers when the recipient's mail client loads the tracking pixel (images enabled), a click when a tracked link is followed. Branch triggers read the same data, so *if opened* branches only fire for participants whose trace recorded an open. ****Sending failed for one participant.**** The error is recorded on the *Failed* trace (note column) and the journey continues for everyone else. Fix the cause (usually the template or the mail server) — subsequent steps still go out. ## FAQ ****Does it need Odoo Enterprise or the Marketing Automation app?**** No. It runs on Community with only `mass_mailing` — that is the reason it exists. It installs unchanged on Enterprise too, and does not conflict with Enterprise apps. ****How is it different from Enterprise marketing_automation?**** It implements the working core of drip marketing: automatic enrollment on a domain filter, delayed steps, per-step conditions, goal exits, blacklist handling, statistics, a full log — and, across 20.0.1.1.0–1.4.0, open/click tracking, branching on opens and clicks, A/B splits, SMS steps, a visual **Flow** canvas and a **Reporting** menu (participant and step-activity analyses, graph and pivot) with open / click / goal rate KPIs. It does not clone every Enterprise screen pixel for pixel, but the classic reasons to upgrade are covered — as a one-off purchase instead of a subscription. ****Why not a Mailchimp or Brevo connector?**** Connectors in that range cost $283–345 and only bridge the data — the automation still runs on the SaaS, with its own per-contact pricing, its own copy of your contacts and a synchronisation to maintain. Here journeys run inside Odoo on live data; nothing leaves your server. ****Does it track opens and clicks?**** Yes. With *Open/click tracking* on (the default), every e-mail step sends through an auto-managed mass mailing — Odoo's own tracking pixel and link tracker, no external service — and shows live *Opened* and *Clicked* counters. Branch steps react to that data (*if opened*, *if not clicked*, …). The log still tracks *sent / skipped / failed* per participant, each sent trace linked to the mailing trace or e-mail record. ****Is unsubscribing / GDPR respected?**** Yes. Every send is checked against Odoo's standard e-mail blacklist — the same list the mass-mailing unsubscribe flow feeds — and skips are logged, so honouring an opt-out is provable. Every e-mail also carries an **unsubscribe link**: through the mass mailing when tracking is on, appended from the step's mailing when it is off. No external service ever receives your data. ****Is it tested?**** Yes: a 19-test unit suite covering enrollment, delays, conditions, goals, blacklist, rejoin rules, tracking-based sending, open/click branching, A/B split bounds, SMS steps and counters, plus a real-browser backend tour. ****Can several journeys run at once?**** Yes, without limit; each is processed independently and a failure in one never blocks the others. The same record may participate in several journeys at the same time. ## Upgrade 1. Back up the database. 2. Replace the module folder, restart, then: ``` odoo-bin -d -u bambooforge_marketing_automation --stop-after-init ``` 3. Review the Changelog below for new settings worth enabling. ## Uninstall 1. Export **Automation ▸ Sending Log** (and Participants) first if you need the history — journeys, steps, participants and traces are removed with the module (standard Odoo behaviour). 2. **Apps ▸ BambooForge Marketing Automation ▸ Uninstall.** 3. Contacts, leads, mail templates and all e-mails already sent remain untouched in your database. ## Changelog ### 20.0.1.10.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.6.0 (2026-08-15) - *Run Test* is now a **dry run**: it walks the whole journey for the first matching record and logs what each step would do, without sending an e-mail, queueing an SMS or running a server action against live data. - E-mail steps sent with *Open/click tracking* **off** now always carry an **unsubscribe link**, taken from the step's mailing — so every commercial message is opt-out compliant on both paths. - Server actions in an *Action* step now run with the rights of the user who created the journey instead of superuser rights. - New optional **Company** on the journey: the audience search stays inside that company. - Robustness: deleting or re-parenting a step no longer wedges the participants sitting on it, and a goal or condition filter pointing at a removed field fails only that participant instead of the whole run. ### 20.0.1.5.0 (2026-08-14) - Flow canvas: steps can now be **dragged and dropped** — drop a card on another step to make it a branch, or on the *Audience enters* pill to return it to the main sequence (the trigger resets to *Always*; circular moves are refused). ### 20.0.1.4.0 (2026-08-14) - Flow canvas: the journey form now opens on a visual **Flow** tab — the step graph rendered as a flowchart with trigger badges (*opened / not opened / clicked / A/B split*) on branch cards, wait delays and live sent / opened / clicked counters on every card. Clicking a card edits the step in a dialog; “+” buttons add root steps or branches directly on the canvas. - Reporting menu with two graph + pivot analyses: **Participant Analysis** (enrollments by week, stacked by status, test participants excluded by default) and **Step Activity Analysis** (sent / skipped / expired / failed per step). - Journey KPIs on the form: open rate, click rate and goal rate. ### 20.0.1.3.0 (2026-08-14) - A/B split branches: a child step can take a deterministic percentage of participants (same record always lands in the same variant), the rest falls through to the next sibling. - SMS steps: queue text messages through the standard SMS app, with inline placeholders and phone-less participants skipped safely. ### 20.0.1.2.0 (2026-08-14) - Branching: a step can now run *after* another step with an open/click trigger — *if opened*, *if not opened*, *if clicked*, *if not clicked*. Siblings are evaluated in order (if / elif / else) in the same tick, against the parent step's tracking data. ### 20.0.1.1.0 (2026-08-14) - Open/click tracking: e-mail steps now send through an auto-managed mass mailing (Community infrastructure), giving every step Opened and Clicked counters with no external service. Toggle per journey; the plain template path remains available. ### 20.0.1.0.0 (2026-08-14, initial) - Server-action steps (tag a record, create an activity, update a field) alongside e-mail steps. - Test mode: run the whole flow instantly on one record, excluded from statistics. - Deduplicate enrollment on any field (e.g. e-mail across duplicates). - Step expiry window and journey duplication that clones the steps. - Initial release: journeys on contacts (`res.partner`), mailing contacts (`mailing.contact`) and CRM leads (`crm.lead`) with a domain-based audience filter; automatic enrollment at start plus a 10-minute scan that enrolls records created after launch, with opt-out and *Allow rejoin* options; ordered steps combining a mail template, a wait delay (hours / days / weeks) and an optional *Only send when* condition with silent, logged skip; journey-level goal filter evaluated before each step with *Goal reached* exit; e-mail blacklist (`mail.blacklist`) respected with logged skips and logged no-e-mail failures; per-participant state machine (running / completed / goal reached / exited / failed) with manual *Exit journey* button and automatic exit of deleted records; sending log with sent / skipped / failed traces linked to the actual `mail.mail`; per-step sent counters and journey KPIs (participants, running, completed, goals, e-mails sent) on kanban and form; start / stop lifecycle with template-model validation; per-journey fault isolation in the processing cron (up to 500 due participants per journey per run); user and manager access groups; 9-test unit suite and backend browser tour. ## How we make sure it works Before every release, **50 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A plain employee does not see the menu. - Branch not opened falls to else. - Deleted step does not wedge participant. - The analyst changes nothing. - A plain employee never reads the journeys. The complete list is in the source you receive. --- # Matomo Analytics Connector > Attribute revenue to Matomo campaigns inside Odoo: import goal and ecommerce conversions with UTM and match them to leads and sales orders. - **What it is:** Attribute revenue to Matomo campaigns inside Odoo: import goal and ecommerce conversions with UTM and match them to leads and sales orders. - **For:** Website - **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_matomo_connector - **Run for real:** Tested for real against Matomo 5.11 and 5.12: 3 sites and their visit summaries came into Odoo with 0 errors. - **Not for:** The connector reads sites and visit summaries from your Matomo server and writes the corresponding Odoo records. The auth token is stored in an admin-only field.; In scope today: read-only import of Matomo sites and per-site visit summaries, field mapping, scheduled and on-demand import, dry-run, validation and rollback.; Out of scope: writing back to Matomo (the connector is read-only); webhook push (Matomo does not offer one); historical multi-period reporting (the visit summary is period=day, date=today per site in v1). - **Manual:** https://docs.bambooforge.dev/bambooforge_matomo_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_matomo_connector.action_matomo_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_matomo_connector - **Manual last updated:** 2026-10-15 --- # Matomo Analytics Connector A read-only bridge from Matomo into Odoo 20: import your Matomo **sites** and a per-site **visit summary** of today's traffic so you can see — without leaving Odoo — which sites are tracked, whether ecommerce tracking is on, and how each site is performing today. It ships with a resilient job queue, dry-run safety, pre-flight validation and rollback so you stay in control. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first import, automate it, and fix the common issues **without contacting support**. ## Overview The connector reads from the Matomo Reporting API over `token_auth` and writes the matching records into Odoo. Every call is a single `POST /index.php?module=API&method=...` with the token sent as a form parameter. It covers: - **Sites** — import your Matomo sites (via `SitesManager.getAllSites`) into the **matomo.site** model: the numeric idsite, name, main URL, timezone, currency, the ecommerce flag and the creation timestamp. - **Visit summaries** — import a per-site summary of today's traffic (via `VisitsSummary.get` with `period=day`, `date=today`) into the **matomo.visit.summary** model: visits, actions, unique visitors, bounce rate, actions per visit and average time on site. Direction of sync: **Odoo ← Matomo** (read-only). Matomo is a pull-only analytics source, so the connector never writes back to Matomo. Sites are imported before visit summaries so each summary links back to its site. Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ## Tested against the real thing Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records. Tested for real against Matomo 5.11 and 5.12: 3 sites and their visit summaries came into Odoo with 0 errors. ## Requirements - **Odoo**: 20.0, Community or Enterprise. - **Matomo**: a reachable Matomo server exposing the Reporting API at `index.php` (the standard entry path). Matomo must be reachable from the Odoo server. - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can use the connector screens. The Matomo auth token is stored in a system-only field, so only the **Settings / Administration** user can read or change the **Auth Token**. - **Network**: outbound HTTP/HTTPS from Odoo to your Matomo server. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ## Installation 1. Copy `bambooforge_matomo_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *Matomo*, and press **Activate / Install**. Dependencies (*base*, *mail*) install automatically. No Matomo server is required to evaluate the connector: a mock Matomo API ships inside the module, so you can install, explore and run the full import flow against sample data before pointing it at a real server. ## Step 1 — Create an auth token in Matomo The Matomo Reporting API authenticates with a single auth token (`token_auth`). To create one: 1. Log in to your Matomo server. 2. Go to **Administration ▸ Personal ▸ Security**. 3. Under **Auth tokens**, click **Create new token**. 4. Confirm your password if prompted, give the token a description (e.g. `Odoo`), and create it. 5. Copy the token string shown — you will paste it into Odoo in Step 2. The token is sent only as the `token_auth` POST form parameter on every request, never on the query string. ## Step 2 — Create the connection in Odoo Open **Matomo Connector ▸ Configuration ▸ Instances** and create a record. ![Matomo instance / connection cockpit](01-connection-cockpit.png) Key fields: | Field | What to enter | |---|---| | Name | A label for this server, e.g. *My Analytics*. | | Base URL | Your Matomo server root, e.g. `https://analytics.example.com`. | | Authentication | *Auth Token (token_auth)* — the only supported method. | | Auth Token | The token string from Step 1 (visible to administrators only). | | API Path | Leave the default `index.php` unless your Matomo entry path differs. | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach a Matomo on localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. The connector lists your sites (`SitesManager.getAllSites`) as a lightweight connectivity and auth check. A green *Connected* state means the URL and token are correct. If it fails, the exact error is shown on the form and recorded in **Logs** (see *Troubleshooting*). Tip: use **Quick Setup** (button on the instance) to seed default field mappings and starter flows in one step. ## Step 3 — First import (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead of creating records they produce **Validation Results** you can review under **Matomo Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, click **Queue Site Import** (and then **Queue Visit Summary Import**). This enqueues jobs; it does not block the UI. Import sites first so each summary can link back to its site. 2. Jobs are processed by the **Matomo Queue Processor** scheduled action (every minute), or immediately if you run it from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land under **Matomo Connector ▸ Imported Data ▸ Sites** and **▸ Visit Summaries**. Each carries its Matomo reference (the idsite for sites, a synthetic `site_day_today` id for summaries) so re-imports update the same record instead of duplicating it. ![An imported Matomo site](02-synced-site.png) ![Imported per-site visit summaries](03-visit-summaries.png) ## Field mapping & customization - **Field Mappings** (Configuration) map Matomo fields to Odoo fields per entity. Each mapping has a source path, a target path and an optional transform (string, integer, float, boolean, uppercase, lowercase, JSON string, CSV join, first item, or a safe Python expression). The direction is *Matomo → Odoo* for this read-only connector. - **Schema Fields** lists the discovered Matomo fields per resource. Run schema introspection on the instance to refresh it. - The two import entities are **Site** and **Visit Summary**. The per-run volume is capped by *Import sites limit* (default 100) and *Import visit summaries limit* (default 200) on the instance. ## Automation (scheduled actions) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | Matomo Queue Processor | every 1 min | Processes queued import jobs. | | Matomo Reconciliation | every 15 min | Pulls recent remote changes for enabled entities. | | Matomo Maintenance | every 1 hr | Recovers stale/locked jobs and trims old logs. | | Matomo Flow Scheduler | every 5 min | Runs scheduled sync flows. | | Matomo Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | Matomo Auto Recover | every 15 min | Reopens a tripped circuit breaker once Matomo is healthy. | Turn on **Auto import** / **Auto reconcile** per entity on the instance to let the scheduled actions keep things in sync hands-free. There are **no webhooks**. Matomo is a pull-only analytics source: the Reporting API has no outbound push, so the connector keeps data current by scheduled or on-demand import only. ## Safety features - **Dry-run mode** — simulate writes and review Validation Results before going live (field *Dry-run*, ON by default). - **Business validation profiles** — *Minimal / Standard / Strict* gate risky writes (field *Business validation profile*, default *Standard*). - **Resilient queue** — every remote action is a job with retry and exponential back-off; jobs that exhaust retries move to a *Dead* state. - **Circuit breaker** — after repeated failures an instance auto-pauses (*Tripped*); the Auto Recover action reopens it once Matomo responds again. - **Rollback snapshots** — when enabled, imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots** (field *Rollback enabled*). - **SSRF guard** — the connector refuses internal/loopback/private hosts unless *Allow internal host* is explicitly enabled. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with 401/403 | Wrong or expired auth token, or the token lacks permission. Re-check Step 1 and re-paste the token under **Auth Token**. | | "Matomo auth token (token_auth) is required" | The **Auth Token** field is empty. Paste the token from Step 1 (admin-only field). | | "is not allowed because it resolves to a non-public address" | Base URL points at localhost/private IP. Enable *Allow internal host* on the instance (test/self-hosted Matomo only). | | SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off *Verify SSL* for testing only. | | Matomo API error in the body (HTTP 200) | Matomo answers 200 OK and signals errors in the JSON body. The connector surfaces the Matomo message verbatim; read it on the form and in **Logs**. | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check **Scheduled Actions** is active and the instance is not paused. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures. Fix the server/token; Auto Recover reopens it once Matomo responds. | | Jobs in *Dead* state | The job exhausted its retries. Fix the underlying cause, then re-run from **Operations ▸ Queue Jobs**. | | Records imported twice | Imports are keyed by the Matomo reference, so this should not happen. If it does, check that two instances do not point at the same server. | | Nothing happens after import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | | Rate limit reached | Matomo returned HTTP 429. The connector retries with back-off; if it persists, reduce the import limits or import cadence. | For anything else, **Operations ▸ Logs** records every API call, payload and error with a timestamp. ## Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the Matomo side the connector targets the standard Reporting API at `index.php` with `token_auth`. Validate your exact build with the bundled mock first. **Do I need Matomo installed to evaluate it?** No. A mock Matomo API ships inside, so you can install, explore and demo the full import flow before connecting a real server. **How does authentication work?** Create a Matomo auth token under **Administration ▸ Personal ▸ Security ▸ Auth tokens**, then paste it into the instance **Auth Token** field. It is sent only as the `token_auth` POST form parameter on every request. **Does it write anything back to Matomo?** No. The connector is read-only: it imports sites and visit summaries from Matomo and never sends changes back. **Is there a webhook for real-time updates?** No. Matomo has no outbound webhook push, so the connector keeps data current by scheduled or on-demand import only. **Is it safe to run against production data?** Dry-run is ON by default, validation blocks risky writes, and rollback snapshots let you undo. You decide when to go live. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - The connector **reads** sites and visit summaries from your Matomo server and **writes** the corresponding Odoo records. The auth token is stored in an admin-only field. - **In scope today**: read-only import of Matomo sites and per-site visit summaries, field mapping, scheduled and on-demand import, dry-run, validation and rollback. - **Out of scope**: writing back to Matomo (the connector is read-only); webhook push (Matomo does not offer one); historical multi-period reporting (the visit summary is `period=day`, `date=today` per site in v1). ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / Matomo Reporting API line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_matomo_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (22 models, prefixed `matomo.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ### What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.0.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Attribute revenue to Matomo campaigns inside Odoo 20: import Matomo goal & ecommerce conversions with UTM and match them to crm.lead / sale.order (enrich-only), with a Revenue-by-Campaign report. - Plus sites & visit-summary import. Resilient queue, dry-run, auditable source. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **68 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Enrich only does not overwrite existing utm source. - Site and visit summary bindings do not collide. - Dry run writes nothing. - Attribution disabled captures conversion but no write. - Lock claims pending then refuses second claim. Tested for real against Matomo 5.11 and 5.12: 3 sites and their visit summaries came into Odoo with 0 errors. The complete list is in the source you receive. --- # Medusa Connector > Two-way Medusa.js integration (v1 and v2): products, variants, customers, orders, refunds and stock, with a webhook companion and sync queue. - **What it is:** Two-way Medusa.js integration (v1 and v2): products, variants, customers, orders, refunds and stock, with a webhook companion and sync queue. - **For:** eCommerce - **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:** 199 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_medusa_connector - **Run for real:** Tested for real against Medusa v2: 4 products, 3 customers and 3 orders placed through the Store API came into Odoo in 10 sync jobs with 0 errors, and every Odoo total equals the store total. - **Not for:** The connector reads products, customers, orders and refunds (order_slip) from your Medusa store and writes the corresponding Odoo records. Credentials are stored in admin-only fields.; In scope today: product/variant/customer/order import, product export, order-status mapping, refund discovery, scheduled sync, signed webhooks.; Out of scope / best-effort: tax auto-matching (off by default, name-based, never auto-creates taxes); multi-warehouse stock routing; subscription order types. - **Manual:** https://docs.bambooforge.dev/bambooforge_medusa_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_medusa_connector.action_medusa_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_medusa_connector - **Manual last updated:** 2026-10-15 --- # Medusa Connector A two-way bridge between Odoo 20 and your Medusa.js store (v1 and v2): import products with variants, customers and orders, push product changes back, and reconcile refunds — with a resilient job queue, dry-run safety, validation and rollback so you stay in control. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first sync, automate it, and fix the common issues **without contacting support**. ## Matching the storefront total A storefront charges shipping and grants discounts outside the product lines, so an order rebuilt from those lines alone is worth less than the order the customer paid for: the sale looks smaller than it was and never reconciles against the platform payout. With **Match Storefront Totals** on the instance (on by default): - the shipping charge is imported as its own order line, on a clearly named service product; - whatever still separates Odoo from the platform total — platform discounts, fees, rounding — is posted as a single adjustment line rather than dropped; - the platform total and the remaining difference are stored on the order and shown on its connector tab; - an order that still does not match within **Total Tolerance** raises a validation warning, so the gap is visible instead of silent. Imported lines deliberately carry no Odoo tax: the storefront has already computed tax and it is kept on the order's own tax amount field. Switch the option off if you would rather Odoo recompute everything from your own price lists and tax rules. ### Overview The connector keeps Odoo and Medusa in sync over the Medusa **Admin API**, using a JSON REST client that speaks both the **Medusa v2** and **Medusa v1** lines. A capability layer absorbs the route and money-unit differences between them (v2 uses major currency units, v1 uses minor units / cents). It covers: - **Products** — import Medusa products (with variants, options and images) into Odoo product templates, optionally with stock and tax, and export Odoo product changes back to the store. - **Customers** — import Medusa customers and their addresses into Odoo contacts. - **Orders** — import Medusa orders into Odoo sale orders, mapping each order's Medusa status to a sale-order action (keep draft, confirm, cancel, or ignore). - **Refunds** — optionally discover Medusa `order_slip` credit notes attached to an order and create matching draft customer credit notes in Odoo. Direction of sync: **Odoo ↔ Medusa** (import is the primary flow; product export and webhook-driven updates are supported). Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ### Tested against the real thing Automated tests run against a bundled mock store on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records and real order totals. Tested for real against Medusa v2: 4 products, 3 customers and 3 orders placed through the Store API came into Odoo in 10 sync jobs with 0 errors, and every Odoo total equals the store total. ### Requirements - **Odoo**: 20.0, Community or Enterprise. - **Medusa**: both **Medusa v2** and **Medusa v1** are supported; pick the matching line in the **API version** field. The Medusa server must be reachable from the Odoo server. - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can use the connector screens. Medusa credentials are stored in system-only fields, so only the **Settings / Administration** user can read or change the API key, password, publishable key and webhook secret. - **Network**: outbound HTTP/HTTPS from Odoo to your Medusa server. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ### Installation 1. Copy `bambooforge_medusa_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *Medusa*, and press **Activate / Install**. Dependencies (*Sales*, *Contacts*, *Invoicing*, *Product*) install automatically. No Medusa store is required to evaluate the connector: a mock Medusa Admin API ships inside the module, so you can install, explore and run the full import flow against sample data before pointing it at a real store. Point the instance **Base URL** at `/medusa/mock_api` with auth type *Email + Password* (any email/password is accepted) to try it. ## Webhook subscriber companion (optional, for real-time sync) To push changes from Medusa to Odoo in real time, install the small webhook subscriber that ships with this module at `phase2/medusa_subscriber/bambooforge-webhook.ts`: 1. Copy `bambooforge-webhook.ts` into your Medusa project's `src/subscribers/` directory. 2. Set three environment variables in your Medusa `.env`: - `BAMBOOFORGE_WEBHOOK_URL` — e.g. `https://odoo.example.com/medusa/webhook` - `BAMBOOFORGE_WEBHOOK_SECRET` — must match the instance **Webhook Secret** in Odoo - `BAMBOOFORGE_INSTANCE_ID` — the `medusa.instance` id in Odoo (optional but recommended; appended as `?instance_id=`) 3. Restart Medusa. The companion is written for the Medusa v2 subscriber API; for Medusa v1 see the README in that folder. Without the companion, the scheduled reconciliation still keeps data current (see *Automation*). ### Step 1 — Get Medusa Admin API access The connector authenticates to the Medusa **Admin API**. Choose one of three auth types on the Odoo instance; how you obtain the credential depends on your Medusa line: **Medusa v2 — Admin Secret API Key (recommended)** 1. Sign in to the Medusa Admin dashboard as an admin user. 2. Go to **Settings ▸ Secret API Keys** (Developer / API key management). 3. Create a **Secret API Key**, give it a name like `Odoo`, and copy the key once it is shown. 4. In Odoo, set **Authentication** to *Admin Secret API Key (v2)* and paste the key into **API Key**. The connector sends it as HTTP Basic (key as username, empty password). **Medusa v2 — Email + Password (JWT)** If you prefer not to manage a secret key, set **Authentication** to *Email + Password (JWT)* and enter an admin **Email** and **Password**. The connector exchanges them for a JWT at `POST /auth/user/emailpass` and caches the token. **Medusa v1 — Email + Password or pre-issued token** For a Medusa v1 server, set **API version** to *Medusa v1*. Either use *Email + Password (JWT)* (the connector logs in at `POST /admin/auth/token`), or, if you already hold a token, set **Authentication** to *Bearer Token* and paste it into **API Key**. Optional: if your Medusa requires a publishable key for store-scoped calls, paste it into **Publishable API Key**; it is sent as the `x-publishable-api-key` header. ### Step 2 — Create the connection in Odoo Open **Medusa Connector ▸ Configuration ▸ Instances** and create a record. ![Medusa instance / connection cockpit](01-connection-cockpit.png) Key fields: | Field | What to enter | |---|---| | Name | A label for this store, e.g. *My Live Store*. | | Base URL | Your Medusa server root, e.g. `https://store.example.com` or `http://localhost:9000`. | | API version | *Medusa v2* (default) or *Medusa v1*. Drives money units and the auth login route. | | Authentication | *Admin Secret API Key (v2)*, *Bearer Token*, or *Email + Password (JWT)* — see Step 1. | | API Key | The secret API key (secret-key auth) or pre-issued token (bearer auth). Leave empty for the email/password flow. Visible to administrators only. | | Email / Password | Admin email and password for the email/password JWT flow (password is admin-only). | | Publishable API Key | Optional store-API publishable key, sent as `x-publishable-api-key` (admin-only). | | Admin API path | Leave the default `/admin` unless your server differs. | | Default currency code | Currency (e.g. `usd`, `eur`) used to pick a variant price on import. Default `usd`. | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach a Medusa server on localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. A green *Connected* state means the credentials and URL are correct. On the first successful connection the default order-status mapping is seeded automatically. If it fails, the exact error is shown on the form and recorded in **Logs** (see *Troubleshooting*). Tip: use **Quick Setup** (button on the instance) to pick a solution pack (catalog only, B2C full sync, B2B order-first, retail starter), seed default field mappings and flows, and apply a recommended safety profile in one step. ### Step 3 — First sync (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import and delete jobs *simulate* writes: instead of changing data they produce **Validation Results** you can review under **Medusa Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, click **Import Products** (and/or **Import Customers**, **Import Orders**). This enqueues jobs; it does not block the UI. 2. Jobs are processed by the **Medusa Queue Processor** scheduled action (every minute), or immediately if you run it manually from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land in the standard Odoo apps: **Sales ▸ Products**, **Contacts**, and **Sales ▸ Orders**. Each carries its Medusa reference (a binder record) so re-imports update the same record instead of duplicating it. ### Order status mapping Medusa exposes each order's `status` (e.g. `pending`, `completed`, `archived`, `canceled`, `requires_action`) and a `fulfillment_status` (e.g. `not_fulfilled`, `fulfilled`, `shipped`, `returned`) used as a fallback. Under **Configuration ▸ Order State Mappings** each instance gets a default table (seeded on first successful connection) that decides what happens to the Odoo sale order when an order arrives in a given status: | Medusa status | Type | Default Odoo action | |---|---|---| | pending | order status | Keep draft | | requires_action | order status | Keep draft | | completed | order status | Confirm sale order | | archived | order status | Confirm sale order | | canceled | order status | Cancel sale order | | not_fulfilled | fulfillment status | Keep draft | | fulfilled | fulfillment status | Confirm sale order | | partially_fulfilled | fulfillment status | Confirm sale order | | shipped | fulfillment status | Confirm sale order | | partially_shipped | fulfillment status | Confirm sale order | | returned | fulfillment status | Cancel sale order | Change any row to **Ignore (do nothing)**, **Keep draft**, **Confirm sale order** or **Cancel sale order**. Unknown/custom statuses default to *Ignore*, so a status you have not mapped never triggers a destructive transition. Add a row for any custom status your store introduces (use the exact Medusa status string). ### Field mapping & customization - **Field Mappings** (Configuration) map Medusa fields to Odoo fields per model. Use **Generate suggested mappings** on the instance to seed the business-critical ones, then adjust. The mapping board flags fields that need attention. - **Schema Fields** lists the discovered Medusa fields per resource. Run **Schema Introspection** on the instance to refresh it from your live store (it falls back to the bundled mock sample if the store is unreachable). - **Stock / images / tax / refunds** are opt-in toggles on the instance: *Sync stock*, *Sync images* (and *max images*), *Upload images on export*, *Sync taxes* / *Auto-match taxes on import*, *Sync refunds* / *Auto-post refund*. They are off by default; turn on only what you need. Tax auto-matching is name-based, company-scoped, and never auto-creates taxes. ### Automation (scheduled actions & webhooks) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | Medusa Queue Processor | every 1 min | Processes queued import/export/delete jobs. | | Medusa Reconciliation | every 15 min | Pulls recent remote changes for enabled models. | | Medusa Maintenance | every 1 hr | Recovers stale/locked jobs and trims old logs. | | Medusa Flow Scheduler | every 5 min | Runs scheduled sync flows. | | Medusa Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | Medusa Auto Recover | every 15 min | Reopens a tripped circuit breaker once the store is healthy. | Turn on **Auto import** / **Auto reconcile** per model on the instance to let the scheduled actions keep things in sync hands-free. **Real-time webhooks** (optional): install the subscriber companion (see *Installation*). It POSTs HMAC-SHA256-signed events to: - **Delivery URL**: `https:///medusa/webhook` (optionally `?instance_id=` to target a specific instance). - **Secret**: the instance **Webhook Secret** (admin-only field on the instance). Each delivery carries an `X-Medusa-Signature` header. Odoo verifies the HMAC-SHA256 signature over the raw body, rejects stale or replayed deliveries, and enqueues a safe import. Without webhooks the scheduled reconciliation still keeps data current. ### Safety features - **Dry-run mode** — simulate writes and review Validation Results before going live (field *Dry-run mode*, ON by default). - **Business validation profiles** — *Minimal / Standard / Strict* gate risky writes (field *Business validation profile*). - **Safety profile** — *Conservative / Balanced / Aggressive* presets set the dry-run, validation, batch-size and webhook dials in one click. - **Resilient queue** — every remote action is a job with retry, exponential back-off and a dead-letter state. - **Circuit breaker** — after repeated failures an instance auto-pauses (*Tripped*); the Auto Recover action reopens it once the store responds again (auth/configuration failures trip it immediately and require manual resume). - **Rollback snapshots** — when *Rollback enabled* is on, imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots**. - **SSRF guard** — the connector refuses internal/loopback/private hosts unless *Allow internal host* is explicitly enabled. - **Signed webhooks** — the public webhook route fails closed: a missing secret or signature is rejected, and stale/replayed deliveries are dropped. ### Where Medusa shows up in Odoo The connector stays out of the way on records that have nothing to do with Medusa: - **Sales orders** - the *Medusa* tab, *Queue Medusa Export* and *Medusa Binding* appear only on orders that came from Medusa, are bound to Medusa, or belong to a customer bound to Medusa. - **Products and contacts** - the *Medusa* tab appears once an active Medusa connection exists; the *Medusa Binding* button inside it appears once the record is bound. - List views, the sync queue and the control tower are unchanged. ### Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with 401 / unauthorized | Wrong or revoked API key, token, or email/password; or the wrong **API version** for your server. Re-check Step 1 and that **Authentication** matches your Medusa line. | | "An API key / token is required" error | The selected auth type needs **API Key** filled (secret-key or bearer). Either fill it or switch **Authentication** to *Email + Password (JWT)*. | | "Email and password are required" | Auth type is *Email + Password* but one of the fields is blank. Fill both, or switch to a key/token auth type. | | "Base URL ... is not allowed" / refused internal host | Base URL points at localhost/private IP. Enable *Allow internal host* on the instance (trusted self-hosted Medusa only). | | SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off *Verify SSL* for testing only. | | Orders import but never confirm | The order's Medusa status is mapped to *Keep draft* or *Ignore*. Adjust **Order State Mappings**. | | Product prices are wrong / zero | The variant has no price in **Default currency code**. Set the matching currency (e.g. `usd`, `eur`) on the instance, or check the v1/v2 **API version** (v1 uses minor units / cents). | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check the **Medusa Queue Processor** scheduled action is active and the instance is not *Paused*. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures. Fix the store/credentials; Auto Recover reopens it after cooldown, or click **Resume**. | | Webhook returns 401 (Invalid signature) | The Medusa `BAMBOOFORGE_WEBHOOK_SECRET` does not match the instance **Webhook Secret**, or the signature header is missing. Align both. | | Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | For anything else, **Operations ▸ Logs** records every API call and error with a timestamp. ### Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the store side both **Medusa v2** and **Medusa v1** are supported — pick the line in the **API version** field. Validate your exact build with the bundled mock first. **Do I need a Medusa store to evaluate it?** No. A mock Medusa Admin API ships inside. Point the Base URL at `/medusa/mock_api` (auth *Email + Password*, any credentials) to run the full import flow before connecting a real store. **How does authentication work?** Three ways: a Medusa v2 **Admin Secret API Key** (sent as HTTP Basic), a pre-issued **Bearer Token**, or **Email + Password** exchanged for a JWT (`/auth/user/emailpass` on v2, `/admin/auth/token` on v1). All three are built in. **How does real-time sync work?** Install the bundled webhook subscriber companion in your Medusa project. It POSTs HMAC-SHA256-signed events to `/medusa/webhook`; Odoo verifies the signature and enqueues a safe import. Without it, scheduled reconciliation keeps things in sync. **Is it safe to run against production data?** Dry-run is ON by default, validation blocks risky writes, and rollback snapshots let you undo. You decide when to go live. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ### Data, privacy & limits - The connector reads products, customers, orders and refunds (`order_slip`) from your Medusa store and writes the corresponding Odoo records. Credentials are stored in admin-only fields. - **In scope today**: product/variant/customer/order import, product export, order-status mapping, refund discovery, scheduled sync, signed webhooks. - **Out of scope / best-effort**: tax auto-matching (off by default, name-based, never auto-creates taxes); multi-warehouse stock routing; subscription order types. ### Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / Medusa v1 & v2 Admin API line. ### Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ## Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_medusa_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ## Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ### Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ## What is removed - The module's own tables and every record in them (20 models, prefixed `medusa.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ## What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ### Changelog ## 20.0.1.2.5 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ## 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Two-way Odoo 20 <-> Medusa.js (v1 & v2) connector: products, variants, customers, orders and refunds. - JSON REST client, webhook subscriber companion, two-way stock sync, live Sync Control Tower, resilient queue and dry-run safety. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ### How we make sure it works Before every release, **105 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A domain that does not parse is refused. - Nothing at all is an empty row not a crash. - A product shows its binding button only once bound. - A record outside the filter is not queued. - Map refund to credit note. Tested for real against Medusa v2: 4 products, 3 customers and 3 orders placed through the Store API came into Odoo in 10 sync jobs with 0 errors, and every Odoo total equals the store total. The complete list is in the source you receive. --- # Metabase BI Connector > Embed live Metabase dashboards and questions inside Odoo with short-lived signed JWTs - the secret never leaves your server. - **What it is:** Embed live Metabase dashboards and questions inside Odoo with short-lived signed JWTs - the secret never leaves your server. - **For:** Extra Tools - **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_metabase_connector - **Run for real:** Tested for real against a current Metabase release: 1 database, 42 questions and 3 dashboards came into Odoo with 0 errors. - **Not for:** The connector reads metadata only — database names and engines, question names/chart types and their database link, and dashboard names/collections — and writes the corresponding Odoo catalog records. It does not read or copy the rows inside your databases, nor the result data of any question. The Metabase API key and webhook secret are stored in admin-only fields.; In scope today: read-only import of Metabase databases, questions (cards) and dashboards into Odoo; scheduled reconciliation; dry-run, validation and rollback safety; schema introspection; field-mapping suggestions; a bundled mock for evaluation.; Out of scope / roadmap: any write-back to Metabase; webhook/push intake (the route is a neutralized stub); incremental list filtering by timestamp (v1 lists by cursor pagination); importing question result data or query definitions. - **Manual:** https://docs.bambooforge.dev/bambooforge_metabase_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_metabase_connector.action_metabase_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_metabase_connector - **Manual last updated:** 2026-10-15 --- # Metabase BI Connector A read-only bridge that catalogs your Metabase business-intelligence assets inside Odoo 20: import your Metabase databases, saved questions (cards) and dashboards into a local, searchable catalog — over the official Metabase REST API, with a resilient job queue, dry-run safety and full logging so you stay in control. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first import, automate it, and fix the common issues **without contacting support**. ## Overview The connector reads three Metabase asset types over the Metabase REST API and keeps a local mirror of them inside Odoo. It is **import-only**: nothing is ever written back to Metabase. It covers: - **Databases** — every data source registered in Metabase, with its name and engine. Imported from `GET /api/database` into **metabase.database**. - **Questions (cards)** — saved questions with their chart type (the Metabase `display` value) and a link to the parent Metabase database. Imported from `GET /api/card` into **metabase.question**. - **Dashboards** — dashboards with their collection. Imported from `GET /api/dashboard` into **metabase.dashboard**. Direction of sync: **Odoo ← Metabase** (read-only catalog). The connector never creates, updates or deletes anything in Metabase; it only catalogs metadata about your BI assets inside Odoo. Databases are imported before questions, so each question can resolve its parent database. Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ## Tested against the real thing Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records. Tested for real against a current Metabase release: 1 database, 42 questions and 3 dashboards came into Odoo with 0 errors. ## Requirements - **Odoo**: 20.0, Community or Enterprise. Depends only on **base** and **mail**, which are always present. - **Metabase**: a reachable Metabase server that supports API-key authentication (the `x-api-key` header) on its REST API. The connector reads `/api/database`, `/api/card` and `/api/dashboard`. - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can use the connector screens. The Metabase API key and webhook secret are stored in system-only fields, so only the **Settings / Administration** user can read or change them. - **Network**: outbound HTTPS from the Odoo server to your Metabase server. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ## Installation 1. Copy `bambooforge_metabase_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *Metabase*, and press **Activate / Install**. No Metabase server is required to evaluate the connector: a mock Metabase REST API ships inside the module, so you can install, explore and run the full import flow against sample data before pointing it at a real server. ## Step 1 — Create an API key in Metabase The connector authenticates with a single Metabase API key, sent on every request as the `x-api-key` HTTP header. Create one in Metabase: 1. Sign in to Metabase as an administrator. 2. Open the gear menu and go to **Admin settings ▸ Authentication ▸ API keys** (on some builds: **Admin settings ▸ Settings ▸ API keys**). 3. Click **Create API key**. 4. Give it a name such as `Odoo` and assign it to a group that can read your databases, questions and dashboards. Read access is enough — the connector never writes. 5. Click **Create** and copy the generated key. It is shown once. You will paste this key into Odoo in Step 2. ## Step 2 — Create the connection in Odoo Open **Metabase Connector ▸ Configuration ▸ Instances** and create a record. Key fields: | Field | What to enter | |---|---| | Name | A label for this server, e.g. *Company Metabase*. Must be unique per company. | | Base URL | Your Metabase server root, e.g. `https://metabase.example.com`. | | Authentication | *API Key (x-api-key)* — the only supported method. | | API Key | The key from Step 1. Sent as the `x-api-key` header on every request (visible to administrators only). | | API Path | The API path prefix. Leave the default `api` unless your server differs; the client builds `/api/database` and so on. | | Timeout (seconds) | HTTP timeout per request. Default `30`. | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach a Metabase server on localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. The connector lists your Metabase databases as a lightweight auth + connectivity check; a green *Connected* state means the API key and URL are correct. If it fails, the exact error is recorded on the form (**Last Connection Error**) and in **Logs** (see *Troubleshooting*). Tip: use **Quick Setup** (button on the instance) to seed default flows, field mappings and a recommended schedule in one step. ## Step 3 — First import (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead of changing data they produce **Validation Results** you can review under **Metabase Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, open the **Import** tab and click **Queue Database Import** (and/or **Queue Question Import**, **Queue Dashboard Import**). This enqueues jobs; it does not block the UI. Queueing questions imports databases first so each question resolves its parent database. 2. Jobs are processed by the **Metabase Queue Processor** scheduled action (every minute), or immediately if you run it manually from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the catalog records for real. Imported records land in **Metabase Connector ▸ Catalog**: **Databases**, **Questions** and **Dashboards**. Each carries its Metabase external ID, so re-imports update the same record instead of duplicating it. A question shows its chart type and is linked to its database. ## Field mapping & customization - **Field Mappings** (Configuration) map Metabase fields to the Odoo catalog fields per entity. Use **Suggest Mappings** on the instance to seed the core ones, then adjust. - **Schema Fields** lists the discovered Metabase fields per entity. Run **Schema Introspection** on the instance to refresh it: it samples one live Metabase record per entity (falling back to the bundled mock when the API is unreachable) and flattens its shape next to the Odoo target fields. - **Import limits** are per-entity dials on the instance — *Databases import limit* (default `100`), *Questions import limit* (default `200`), *Dashboards import limit* (default `200`) — capping how many remote IDs each import enqueues. ## Automation (scheduled actions & webhooks) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | Metabase Queue Processor | every 1 min | Processes queued import jobs. | | Metabase Reconciliation | every 15 min | Re-lists enabled entities and queues imports for anything missing or previously failed. | | Metabase Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs. | | Metabase Flow Scheduler | every 5 min | Runs scheduled import flows. | | Metabase Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | Metabase Auto Recover | every 15 min | Reopens a tripped circuit breaker once the server is healthy again. | Turn on **Auto import** / **Auto reconcile** per entity on the instance (Import tab) to let the scheduled actions keep the catalog current hands-free. **Webhooks**: this connector is a pull-only importer, so there is **no webhook intake**. A route exists at `/metabase/webhook` only so anything still pointed at it gets a clear *not supported* reply (HTTP 501) instead of a 404; a genuine push path is a roadmap item, not part of this read-only version. Keep the catalog current with the scheduled actions above. ## Safety features - **Dry-run mode** — simulate writes and review Validation Results before going live. Controlled by *Dry-run* on the instance (ON by default). - **Pre-flight validation** — *Minimal / Standard / Strict* business-validation profiles gate risky writes. Controlled by *Validation* and *Business validation profile*. - **Resilient queue** — every import action is a job with retry and exponential back-off (*Retry limit*, *Backoff policy*, *Backoff base/cap minutes*). HTTP calls additionally retry transient failures (*API retry limit*, default 2). - **Circuit breaker** — after repeated failures (*Auto pause threshold*, default 5) an instance auto-pauses (*Tripped*); the Auto Recover action reopens it once the server responds again. Auth/configuration errors trip it immediately. - **Rollback snapshots** — when *Rollback* is enabled, imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots**. - **SSRF guard** — the connector refuses internal/loopback/private hosts (and cloud-metadata addresses such as `169.254.169.254`) unless *Allow internal host* is explicitly enabled. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with 401 / 403 | Wrong or expired **API key**, or the key's group lacks read access. Re-create the key in **Admin settings ▸ Authentication ▸ API keys** and paste it again. The connector sends it as the `x-api-key` header. | | "API Key (x-api-key) is required" | The **API Key** field is empty. Paste the key from Step 1 (it is an admin-only field). | | "...is not allowed because it resolves to a non-public address" | Base URL points at localhost/private IP (SSRF guard). Enable *Allow internal host* on the instance (self-hosted/test only). | | SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off *Verify SSL* for testing only. | | Connection times out | Metabase is unreachable from the Odoo server or slow to answer. Check the Base URL, firewall/outbound rules, and raise *Timeout (seconds)* if needed. | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check the **Metabase Queue Processor** scheduled action is active and the instance is not *Paused*. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures (or an auth error). Fix the server/credentials; Auto Recover reopens it, or click **Resume Sync**. | | Questions imported without a database link | Databases were not imported first. Use **Queue Question Import** (it imports databases first) or run **Queue Database Import** before questions. | | Records imported twice | Imports are keyed by the Metabase external ID, so this should not happen. If it does, check that two instances do not point at the same server. | | Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | | POST to `/metabase/webhook` returns 501 | Expected. This connector has no webhook intake; use the scheduled actions to keep the catalog current. | For anything else, **Operations ▸ Logs** records every API call and error with a timestamp. ## Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the Metabase side, any server whose REST API supports API-key authentication via the `x-api-key` header and the `/api/database`, `/api/card` and `/api/dashboard` endpoints. Validate your exact build with the bundled mock first. **Do I need a Metabase server to evaluate it?** No. A mock Metabase REST API ships inside, so you can install, explore and demo the full import flow before connecting a real server. **How does authentication work?** Create a Metabase API key under **Admin settings ▸ Authentication ▸ API keys** and paste it into the instance. The connector sends it as the `x-api-key` header on every request. There is no username/password and no token exchange. **Does this write anything back to Metabase?** No. The connector is read-only by design: it only lists and reads your databases, questions and dashboards. It never creates, updates or deletes anything in Metabase. **Is there real-time / webhook sync?** No. This is a pull-only importer; the `/metabase/webhook` route is a stub that answers *not supported*. Use the scheduled **Reconciliation** and **Auto import** to keep the catalog current. A push path is a roadmap item. **Is it safe to run against production data?** Yes. The connector never touches your Metabase data. On the Odoo side, dry-run is ON by default, validation gates risky writes, and rollback snapshots let you undo. You decide when to go live. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - The connector **reads metadata only** — database names and engines, question names/chart types and their database link, and dashboard names/collections — and writes the corresponding Odoo catalog records. It does **not** read or copy the rows inside your databases, nor the result data of any question. The Metabase API key and webhook secret are stored in admin-only fields. - **In scope today**: read-only import of Metabase databases, questions (cards) and dashboards into Odoo; scheduled reconciliation; dry-run, validation and rollback safety; schema introspection; field-mapping suggestions; a bundled mock for evaluation. - **Out of scope / roadmap**: any write-back to Metabase; webhook/push intake (the route is a neutralized stub); incremental list filtering by timestamp (v1 lists by cursor pagination); importing question result data or query definitions. ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / Metabase REST API line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_metabase_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (23 models, prefixed `metabase.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ### What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.0.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Embed live Metabase dashboards & questions inside Odoo 20 via short-lived signed JWTs (HS256, secret never leaves the server). - Plus a searchable catalog of databases, questions & dashboards. Resilient queue, dry-run, auditable source. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **85 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Entity bindings do not collide. - Resolve action always ignore. - Disabled by default no url no crash. - Lock claims pending then refuses second claim. - Action domain selects only conflicts. Tested for real against a current Metabase release: 1 database, 42 questions and 3 dashboards came into Odoo with 0 errors. The complete list is in the source you receive. --- # Shop Floor Scheduler Pro: Finite Capacity Gantt and Tool Life > Plan every work order of the shop in one go, hour by hour, within each work centre's calendar, never more at once than it runs, never two on one mould. Drag a work order and the rest is recalculated around it; replan from any point after a breakdown. Moulds and dies count their life and block when due. - **What it is:** Plan every work order of the shop in one go, hour by hour, within each work centre's calendar, never more at once than it runs, never two on one mould. Drag a work order and the rest is recalculated around it; replan from any point after a breakdown. Moulds and dies count their life and block when due. - **For:** Manufacturing - **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:** 249 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_mrp_scheduler - **Not for:** Durations are Odoo's own expected durations; the scheduler does not estimate them.; Just in time plans backward from the order's deadline; it does not level the load across days.; The components check reads Odoo's forecast; what the forecast does not know - a late supplier - the board does not know either.; A crew is a headcount, not a roster of named people and their skills.; One tool per operation; a work order needing several tools is not modelled.; Machine signals are not read; a breakdown is entered by hand (block the work centre, or replan from the stopped work order). - **Manual:** https://docs.bambooforge.dev/bambooforge_mrp_scheduler/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_mrp_scheduler.bf_mrp_scheduler_action - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_mrp_scheduler - **Manual last updated:** 2026-10-15 --- # Shop Floor Scheduler Pro: Finite Capacity Gantt and Tool Life Odoo plans a work order into the first free slot of its work centre and stops there. When a machine breaks, a rush order lands or a mould wears out, the plan is what the planner keeps in their head. This app plans the whole shop, hour by hour, and keeps the plan honest. ## What you get - **Schedule**: every waiting work order placed in its work centre's working calendar, after the operations it waits for, never more at once than the work centre runs, never two on one tool; manufacturing orders taken by priority, then deadline, then first come first served. - **A Gantt by the hour**: rows per work centre, the calendar shaded, the current moment marked; day, three-day, week and fortnight views. - **Drag to move**: a work order dragged keeps its duration, snaps to the next working time, is **pinned** where it landed, and the rest of its work centre is replanned around it. - **Replan from here**: leave everything planned before a work order alone and replan from it on - after a breakdown, from the work order that stopped. - **Tools, moulds and dies** with a life in units: put one on an operation and no two work orders hold it at once; every finished work order counts its pieces against the life; when it is due the board says so, and when it is exhausted no work order starts on it until it is serviced. - **Components first**: an order's first operation waits for its components to arrive; an order nothing is coming for is named. - **Changeovers**: setting a mould up is planned in front of the work order that needs it, and orders due the same day that use the same mould run one after another. - **Just in time**: backward from the deadline - the last operation ending at it - forward again when there is no room. - **Maintenance blocks**: time booked off a machine, drawn on the board and planned around. - **Operator crews**: machines that share people never run more work orders at once than there are people on shift. - **Conflicts named**: overlaps beyond capacity, a tool double-booked, a tool past its life, components that will not be there, a crew over its headcount, a work order ending after its order's deadline. ![The shop floor board: work centres by the hour, a pinned order, a mould due](board.png) ## Requirements - **Odoo**: 20.0, Community or Enterprise, with **Manufacturing** installed. - Work centres with a **working calendar** (every work centre has one). - **Access**: Manufacturing users see the board and the tools; Manufacturing administrators manage tools. ## Install 1. Copy `bambooforge_mrp_scheduler` into your addons path and restart Odoo. 2. *Apps ▸ Update Apps List*, search for *Shop Floor Scheduler*, **Activate**. 3. *Manufacturing ▸ Planning ▸ Shop Floor Schedule*. ## Setting up **Work centres.** *Manufacturing ▸ Configuration ▸ Work Centers*: each one has a working calendar; set **Work Orders at Once** to how many it really runs side by side (a bench with three people: 3; a press: 1). **Crews.** *Manufacturing ▸ Configuration ▸ Operator Crews*: a crew and how many people are on shift. On each work centre that draws on it, pick the **Crew** and the **Operators per Work Order**. Three presses and two operators: the scheduler never runs a third press at the same time. **Tools and moulds.** *Manufacturing ▸ Configuration ▸ Tools and Moulds*: a name, the work centres it fits, its **life in units** (0 for no limit), at how many units left to **warn**, and its **changeover** in minutes - the time to set it up on a machine that last ran with another tool. Then open the operation in the bill of materials and pick the tool under **Tool / Mould**: every work order of that operation holds it. ![A mould near the end of its life](tool.png) ## Planning **Schedule** plans every waiting work order of the work centres on the board (or of all of them). A running work order and a pinned one are left where they are and planned around. Three choices sit next to the button: - **As soon as possible** or **Just in time for the deadline**. Just in time plans each order backward: its last operation ends at the order's deadline, each operation before it ends where the next begins. An order that would then have to start in the past - or before its components arrive - is planned as soon as possible instead, and shows late if it is. An order without a deadline is always planned as soon as possible. - **Components**: the first operation of an order does not start before its components are expected - the forecast Odoo keeps from receipts and other orders. An order short of a component nothing is coming for is planned all the same, and named in the result and among the conflicts. - **Group by tool**: among orders of the same priority due the same day, those whose first operation uses the same tool are planned one after another. A tool's changeover is planned only when the machine last ran with another tool (or when nothing is known of what ran on it); it is drawn as a hatched stretch in front of the bar, and the work order starts once it is over. Work runs over the lunch break and never outside the calendar; a work order longer than the day continues the next morning. The button says how many were planned and names any that could not be placed within ninety days. **Drag** a bar left or right: it keeps its duration, lands at the next working time (a Saturday drop lands Monday morning), is pinned, and the rest of that work centre is replanned from that moment. **Unpin** hands it back to the scheduler. **Replan from here** (on the selected work order) leaves everything planned before it untouched and replans from it on - the honest way to absorb a breakdown or a rush order without shuffling the morning. **Schedule Work Centres** on a manufacturing order does the same for the work centres that order uses and opens the board. **Block time** (beside the board): a work centre, a start, an end and a reason - a service, a trial run. The block is drawn on the row, hatched, and the next Schedule plans around it; the cross on it removes it. Blocks are Odoo's own time off on the work centre's resource, so a block entered from Odoo's resource screens counts the same. The board shows a work order's manufacturing order, product, quantity, state, duration, tool, planned span and, when it ends after the order's deadline, that it is late. Click a bar for the details; **Open** goes to the work order. ## Conflicts Listed beside the board, each pointing at its work order: - **capacity** - more work orders overlap on a work centre than it runs at once (a hand-placed order, or a work order planned by something else); - **tool** - two work orders hold the same tool at the same time; - **tool life** - the tool on a work order is due or exhausted; - **material** - an order's first operation starts before its components are expected, or nothing is coming for one of them; - **crew** - more operators needed at one moment than the crew has; - **late** is on the bar itself, red. Schedule clears the first two; the third needs the tool serviced. ## Tool life When a work order is finished, its produced quantity is added to the tool's **units since service** and written in the tool's usage log. Below the warn threshold the tool is **due**; at zero life left it is **exhausted** and *Start* on any work order that holds it is refused with the units left and the units needed. **Serviced** on the tool resets the counter and dates the service. ## Limits, stated plainly - Durations are Odoo's own expected durations; the scheduler does not estimate them. - Just in time plans backward from the order's deadline; it does not level the load across days. - The components check reads Odoo's forecast; what the forecast does not know - a late supplier - the board does not know either. - A crew is a headcount, not a roster of named people and their skills. - One tool per operation; a work order needing several tools is not modelled. - Machine signals are not read; a breakdown is entered by hand (block the work centre, or replan from the stopped work order). ## Uninstalling Uninstalling removes the tools, their logs and the pin flags. Work orders keep the dates they had. ## Changelog ### 20.0.1.1.0 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.1.0 - Components: the first operation waits for them; missing supply named. - Tool changeover planned in front of the work order; same-tool orders grouped. - Just in time: backward scheduling from the deadline. - Maintenance blocks on the board; operator crews shared by work centres. - An order's start copied back by Odoo from its own work orders no longer holds it where the last run left it. ### 20.0.1.0.0 First release. ## How we make sure it works Before every release, **16 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - A tool is never held twice. - Nothing coming is said. - Work runs over the break and never at night. - A block on a machine without a calendar. - A centre counts what runs at once. The complete list is in the source you receive. --- # UK VAT Making Tax Digital (MTD) Returns > UK VAT under Making Tax Digital: build the nine boxes from your own taxes with an explicit mapping, keep HMRC obligations, assemble the payload. - **What it is:** UK VAT under Making Tax Digital: build the nine boxes from your own taxes with an explicit mapping, keep HMRC obligations, assemble the payload. - **For:** Accounting - **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:** 249 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_mtd_vat - **Not for:** Not tested against the live HMRC API. Everything up to the HTTP call is covered by tests; what happens inside that call cannot be verified without an HMRC developer account and a company's own grant.; Obligations are entered or imported, not fetched — fetching them needs the same credentials.; One VAT registration per company. - **Manual:** https://docs.bambooforge.dev/bambooforge_mtd_vat/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_mtd_vat.action_bf_mtd_obligation - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_mtd_vat - **Manual last updated:** 2026-09-21 --- # UK VAT Making Tax Digital (MTD) Returns ## What this is for Odoo Community has no UK VAT return — `l10n_uk_reports` is Enterprise. This builds the nine boxes from the ledger and prepares the submission. ## The box mapping *Accounting ▸ Configuration ▸ VAT Box Mapping* Every chart of accounts names its taxes differently, and a return built on guessed names is wrong in a way nobody notices until HMRC does. Each box says explicitly which taxes feed it, and whether it reports **the VAT** (boxes 1, 2, 4) or **the net amount it was charged on** (boxes 6 to 9). Boxes 3 and 5 are never mapped: they are arithmetic on the others. A box with nothing mapped reports zero **and says so** on the return, and the submission is refused until it is dealt with. Quietly filing a zero is how a missing mapping is discovered through an assessment. Dealing with it can mean either thing. Most companies never make an EU acquisition, so boxes 2, 8 and 9 are nil for them for ever — tick **Nil on purpose** on those rows and the return stops treating them as unfinished work. A box cannot be both mapped and nil on purpose; that would be a contradiction about what the figure means. ## The company has to keep its books in sterling The nine boxes are read off the ledger in the company currency and sent to HMRC as bare numbers. The API carries no currency field, so a company keeping its books in euro would file euro totals under a pound heading and hear about it at the assessment. Submission is refused outside GBP, and the return says so on screen before anyone reaches the declaration. A group that trades in both places handles this the way it already does: the British entity is its own company with its own GBP ledger, and the return is prepared there. ## Obligations HMRC issues the periods, and they are not always calendar quarters. Record each one with the **period key** HMRC gave it — a submission has to quote it back, and it is what ties a return to the obligation it satisfies. ## Preparing a return Pick the obligation, press **Compute**, and the boxes are filled from posted entries in the period. Amounts are reported positive on both sides, as the form expects. Before it can be submitted: - the declaration must be ticked — HMRC requires the submitter to confirm the figures are correct and complete, and the API rejects a submission without it; - every box must be mapped, or explicitly confirmed as empty. ## Submitting This module **does not carry HMRC credentials for you**. Production access needs a registered application and an OAuth grant belonging to the company that files, not to a software vendor — so the honest shape is: - the payload and the fraud-prevention headers are assembled and validated here, and nothing in the headers is invented (HMRC rejects fabricated values, and several can only come from the browser a human is sitting at — those are passed in by the caller); - `_bf_send` is one small method to override with your own client; - a submission accepted through another channel can be recorded against the obligation with its receipt. ## Known limits - Not tested against the live HMRC API. Everything up to the HTTP call is covered by tests; what happens inside that call cannot be verified without an HMRC developer account and a company's own grant. - Obligations are entered or imported, not fetched — fetching them needs the same credentials. - One VAT registration per company. ## How we make sure it works Before every release, **25 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Books kept outside sterling cannot be filed. - Fraud headers do not invent what they do not know. - Box mapping of another company is not searchable. - Obligation of another company is not searchable. - Return of another company is not readable. The complete list is in the source you receive. ## Install 1. Copy `bambooforge_mtd_vat` into your addons path and update the app list, or install the ZIP from the Odoo Apps Store. 2. Install **UK VAT Making Tax Digital (MTD) Returns**. 3. Open **VAT Obligations** - its menus are VAT Returns (MTD), VAT Box Mapping. ## 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 Invoicing app. No extra Python package. ****Where does it show up?**** Under the **VAT Obligations** menu (VAT Returns (MTD), VAT Box Mapping). ****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. --- # n8n Connector for Workflow Automation > A two-way bridge between Odoo and n8n: record events fire HMAC-signed webhooks, and a guarded inbound endpoint lets workflows write back. - **What it is:** A two-way bridge between Odoo and n8n: record events fire HMAC-signed webhooks, and a guarded inbound endpoint lets workflows write back. - **For:** Extra Tools - **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_n8n_connector - **Run for real:** Tested for real against a current n8n release: 4 workflows read through the Public API with 0 errors, and a workflow created through the API arrived in Odoo on the next sync. - **Not for:** The connector reads workflows and executions from your n8n server and writes the corresponding Odoo n8n.workflow / n8n.execution records. It never writes to n8n. The API key and webhook secret are stored in admin-only fields.; In scope today: read-only import of workflows and executions, scheduled reconciliation, field mapping, schema introspection, dry-run, validation, rollback snapshots and a multi-step flow engine.; Out of scope: any write-back to n8n (creating/activating/running workflows), inbound change webhooks, and timestamp-based incremental list filtering on the n8n API (v1 lists by cursor pagination; reconciliation re-lists and queues the delta). - **Manual:** https://docs.bambooforge.dev/bambooforge_n8n_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_n8n_connector.action_n8n_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_n8n_connector - **Manual last updated:** 2026-10-15 --- # n8n Connector for Workflow Automation A read-only bridge from your n8n automation server into Odoo 20: it imports your n8n **workflows** and their **executions** so you can monitor your automations — which workflows are active, how many nodes and triggers they have, and whether their recent runs succeeded, failed, are running or waiting — without leaving Odoo. The connector never writes back to n8n. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first import, automate it, and fix the common issues **without contacting support**. ## Overview The connector pulls data from the n8n public API (`X-N8N-API-KEY` auth, cursor pagination) into dedicated Odoo models. It is **import-first and pull-based**: every remote call is a read, and nothing is ever pushed back to n8n. Direction of sync: **Odoo ← n8n** (read-only monitoring; one-way import). It covers two entities: - **Workflows** — imported into **n8n Connector ▸ Imported Data ▸ Workflows** (model `n8n.workflow`). Each record mirrors the n8n workflow shape: name, the *Active in n8n* flag, node count, trigger count, comma-joined tag names, and the created/updated timestamps from n8n. - **Executions** — imported into **n8n Connector ▸ Imported Data ▸ Executions** (model `n8n.execution`). Each record mirrors an n8n run: status (Success, Error, Running, Waiting, Unknown), mode (manual / trigger / webhook / …), the *Finished* flag, started/stopped timestamps, and a best-effort link to the parent workflow once that workflow has also been imported. Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ## Tested against the real thing Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records. Tested for real against a current n8n release: 4 workflows read through the Public API with 0 errors, and a workflow created through the API arrived in Odoo on the next sync. ## Requirements - **Odoo**: 20.0, Community or Enterprise. - **n8n**: any n8n server that exposes the **public REST API** at `/api/v1` with `workflows` and `executions` endpoints (self-hosted or n8n Cloud). The server must be reachable from the Odoo server. - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can use the connector screens. The n8n API key and webhook secret are stored in system-only fields, so only the **Settings / Administration** user can read or change them. - **Network**: outbound HTTPS from Odoo to your n8n server. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ## Installation 1. Copy `bambooforge_n8n_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *n8n*, and press **Activate / Install**. Dependencies (*base*, *mail*) install automatically. No n8n server is required to evaluate the connector: a mock n8n API ships inside the module, so you can install, explore and run the full import flow against sample workflows and executions before pointing it at a real n8n instance. ## Step 1 — Create an n8n API key The connector authenticates with a single n8n API key, sent on every request as the `X-N8N-API-KEY` header. There is no OAuth and no token exchange. In your n8n instance: 1. Open **Settings ▸ n8n API**. 2. Click **Create an API key**. 3. Give the key a label (e.g. `Odoo`) and create it. 4. Copy the key shown — it is displayed once. This is the value you paste into Odoo in Step 2. A read scope is sufficient: the connector only ever issues `GET` requests to `/api/v1/workflows` and `/api/v1/executions`. It never creates, updates or deletes anything in n8n. ## Step 2 — Create the connection in Odoo Open **n8n Connector ▸ Configuration ▸ Instances** and create a record. Key fields: | Field | What to enter | |---|---| | Name | A label for this server, e.g. *Production n8n*. Must be unique per company. | | Base URL | Your n8n server root, e.g. `https://n8n.example.com` (or `http://localhost:5678` for a local server — see *Allow internal host*). | | Authentication | *API Key (X-N8N-API-KEY)* — the only mode n8n's public API supports. | | API Key | The key copied in Step 1 (visible to administrators only). | | API Path | Leave the default `api/v1` unless your n8n server differs. | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach an n8n server on localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. It performs a lightweight `GET /api/v1/workflows?limit=1` to validate the key and reachability. A green *Connected* state means the API key and URL are correct. If it fails, the exact error is recorded on the form (*last connection error*) and in **Logs** (see *Troubleshooting*). Tip: use **Quick Setup** (button on the instance) to seed starter flows, default field mappings and a safety profile in one step. ## Step 3 — First import (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead of creating records they produce **Validation Results** you can review under **n8n Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* be imported before anything is written. To run a first import: 1. On the instance, click **Import Workflows** (and/or **Import Executions**). This enqueues jobs; it does not block the UI. 2. Jobs are processed by the **N8n Queue Processor** scheduled action (every minute), or immediately if you run it manually from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land in **n8n Connector ▸ Imported Data ▸ Workflows** and **▸ Executions**. Each carries its n8n id, so re-imports update the same record instead of duplicating it. Defaults: a workflow import pulls up to **100** workflows per run and an execution import up to **200** (the *Workflows import limit* / *Executions import limit* fields on the instance). The connector follows n8n's cursor pagination across pages automatically. ## Field mapping & customization The two n8n entities have no native Odoo counterpart, so the connector ships dedicated models (`n8n.workflow` / `n8n.execution`) and a built-in mapping that already populates the core fields (name, ids, active flag, node/trigger counts, tags, status, mode, finished, timestamps). You do not need to configure anything for a standard import. To customize: - **Field Mappings** (Configuration) let you map additional n8n payload fields to Odoo fields per entity (*workflow* / *execution*), with optional transforms and defaults. They are applied on top of the built-in mapping. Use **Generate suggested mappings** on the instance to seed the common ones, then adjust. - **Schema Fields** lists the discovered n8n fields per entity alongside the Odoo target fields. Run **Schema Introspection** on the instance to refresh it; it samples one live n8n record (falling back to the bundled mock when the API is unreachable) and flattens its shape so you can see the available paths. - **Execution Status Mappings** (Configuration) hold per-instance rules seeded on first successful connect. n8n execution statuses are normalized to *Success / Error / Running / Waiting / Unknown*; any status the mapping does not recognize is stored as *Unknown* so an unexpected value never breaks an import. ## Automation (scheduled actions) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | N8n Queue Processor | every 1 min | Processes queued import jobs. | | N8n Reconciliation | every 15 min | Pulls recent remote ids for enabled entities and queues any that are missing locally or need a retry. | | N8n Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs. | | N8n Flow Scheduler | every 5 min | Runs scheduled import flows. | | N8n Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | N8n Auto Recover | every 15 min | Reopens a tripped circuit breaker once the cooldown passes and no blocking validation issues remain. | Turn on **Auto import** / **Auto reconcile** per entity on the instance (*Auto import workflows*, *Auto import executions*, *Auto reconcile workflows*, *Auto reconcile executions*) to let the scheduled actions keep the imported data current hands-free. **Webhooks**: n8n does not push workflow/execution change notifications to third parties, so this connector is scheduled-pull only. A route exists at `/n8n/webhook` but it is intentionally inert — it always returns *501 Not Implemented* so nothing breaks on install. Keeping data current relies on the reconciliation schedule above, not on inbound webhooks. ## Safety features - **Dry-run mode** — simulate imports and review Validation Results before going live (field *Dry-run mode*, ON by default). - **Business validation profiles** — *Minimal / Standard / Strict* gate risky writes (field *Business validation profile*, default *Standard*). - **Safety profiles** — *Conservative / Balanced / Aggressive* presets set dry-run, validation, rollback, delete strategy and batch size in one click (field *Safety profile*, default *Balanced*). - **Resilient queue** — every import is a job with retry and backoff (fixed or exponential, base 1 min, cap 120 min by default) and a dead-letter state. - **Circuit breaker** — after repeated failures an instance auto-pauses (*Tripped*, default threshold 5 consecutive failures); the Auto Recover action reopens it after a 30-minute cooldown once no blocking issues remain. - **Rollback snapshots** — when enabled, imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots** (field *Rollback enabled*, ON by default). - **SSRF guard** — the connector refuses internal/loopback/private/link-local/ reserved hosts (including cloud metadata addresses) unless *Allow internal host* is explicitly enabled. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with 401 / 403 | Wrong, expired or revoked n8n API key. Re-create it under **Settings ▸ n8n API** (Step 1) and paste the new key into the **API Key** field. | | "n8n API key (X-N8N-API-KEY) is required" | The **API Key** field is empty. Paste the key from Step 1. The field is admin-only, so a non-admin user cannot see or set it. | | "...is not allowed because it resolves to the non-public address..." | Base URL points at localhost/a private or reserved IP. Enable *Allow internal host* on the instance (self-hosted/local n8n only). | | "the URL must start with http:// or https://" | The Base URL is missing its scheme. Enter the full root, e.g. `https://n8n.example.com`. | | SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off *Verify SSL* for testing only. | | "n8n rate limit reached" (HTTP 429) | n8n is throttling. The client already retries with backoff; lower the import limits or run imports less often if it persists. | | "n8n server error" (HTTP 5xx) | The n8n server returned an error. The client retries transient 5xx with backoff; if it persists, check the n8n server health and logs. | | "n8n resource not found" (HTTP 404) | The API Path is wrong or the public API is not enabled. Confirm the path is `api/v1` and that the n8n public API is turned on. | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check **Scheduled Actions** is active and the instance is not *Paused*. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures. Fix the server/key; Auto Recover reopens it after cooldown, or click **Resume**. | | Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | | Executions have no linked Workflow | The parent workflow has not been imported yet. The link is resolved best-effort; import workflows first, then executions (or re-run the execution import) to populate it. | For anything else, **Operations ▸ Logs** records every API call, payload and error with a timestamp. ## Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the n8n side, any server exposing the public REST API at `/api/v1` with `workflows` and `executions` endpoints (self-hosted or Cloud). Validate your build with the bundled mock first. **Do I need an n8n server to evaluate it?** No. A mock n8n API ships inside, so you can install, explore and demo the full import flow before connecting a real server. **How does authentication work?** Create one n8n API key under **Settings ▸ n8n API** and paste it into the instance. The connector sends it as the `X-N8N-API-KEY` header on every request. No OAuth, no token exchange. **Does it change anything in n8n?** No. The connector is read-only: it only issues `GET` requests to list and fetch workflows and executions. Export to n8n is not supported. **Does it use webhooks for real-time updates?** No. n8n does not push workflow/execution change notifications to third parties, so the connector keeps data current on a schedule (the Reconciliation cron). The `/n8n/webhook` route exists but is inert and returns 501. **Is it safe to run against production data?** It only reads from n8n, and on the Odoo side dry-run is ON by default with validation and rollback snapshots. You decide when to write for real. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - The connector **reads** workflows and executions from your n8n server and **writes** the corresponding Odoo `n8n.workflow` / `n8n.execution` records. It never writes to n8n. The API key and webhook secret are stored in admin-only fields. - **In scope today**: read-only import of workflows and executions, scheduled reconciliation, field mapping, schema introspection, dry-run, validation, rollback snapshots and a multi-step flow engine. - **Out of scope**: any write-back to n8n (creating/activating/running workflows), inbound change webhooks, and timestamp-based incremental list filtering on the n8n API (v1 lists by cursor pagination; reconciliation re-lists and queues the delta). ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / n8n public REST API v1 line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_n8n_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (25 models, prefixed `n8n.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ### What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.0.1 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - A two-way trigger bridge between Odoo 20 and n8n: Odoo record events -> HMAC-signed n8n webhooks (resilient queue), plus a secured inbound endpoint (HMAC + replay-guard + strict allowlist) and prebuilt trigger templates. Workflows/executions import, dry-run, auditable source. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **75 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Workflow and execution bindings do not collide. - Field not in allowlist is dropped. - Inactive trigger does not fire. - Notify job posts signed body. - Lock claims pending then refuses second claim. Tested for real against a current n8n release: 4 workflows read through the Public API with 0 errors, and a workflow created through the API arrived in Odoo on the next sync. The complete list is in the source you receive. --- # NocoDB Connector > Two-way row sync between NocoDB tables and Odoo models, with base and table metadata import, idempotent binders and dry-run safety. - **What it is:** Two-way row sync between NocoDB tables and Odoo models, with base and table metadata import, idempotent binders and dry-run safety. - **For:** Extra Tools - **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_nocodb_connector - **Run for real:** Tested for real against a current NocoDB release: 2 bases and 2 tables (a Deals table with 5 rows) came into Odoo with 0 errors, using an API token created for the purpose. - **Not for:** The connector reads bases, tables and per-table row counts from your NocoDB and writes the corresponding NocoDB Base / NocoDB Table records in Odoo. It never writes back to NocoDB.; The API token is stored in an administrator-only field.; In scope today: base import, table import, live row counts, scheduled re-import/reconciliation, dry-run, validation, rollback snapshots, SSRF guard.; Out of scope / roadmap: importing the actual table records (row data); webhook/push intake from NocoDB; writing any data back to NocoDB. - **Manual:** https://docs.bambooforge.dev/bambooforge_nocodb_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_nocodb_connector.action_nocodb_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_nocodb_connector - **Manual last updated:** 2026-10-15 --- # NocoDB Connector A read-only importer that brings your NocoDB (the open-source no-code database / Airtable alternative) workspace into Odoo 20: it imports your **bases** and the **tables** inside each base, with a live **row count** for every table, so you get an auditable inventory of your NocoDB data without leaving Odoo. Nothing is ever written back to NocoDB. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first import, automate it, and fix the common issues **without contacting support**. ## Overview The connector reads from the NocoDB REST/Meta API (`api/v2`) and mirrors the metadata into dedicated Odoo models. It covers: - **Bases** — every NocoDB base (database/project) from `GET /api/v2/meta/bases`, imported into **NocoDB Base** records (the base id and its title). - **Tables** — the tables inside each base from `GET /api/v2/meta/bases//tables`, imported into **NocoDB Table** records. Each table links back to its base (resolved from the table's `baseId`). - **Row counts** — a live record count per table, read from `GET /api/v2/tables//records?limit=1` -> `pageInfo.totalRows` and stored on the table at import time. Direction of sync: **Odoo <- NocoDB** (import only). This connector never writes to NocoDB — no create, update or delete is ever sent to your workspace. Bases are imported before tables so each table can link back to its base. Importing the actual *table records* (the row data itself) is a roadmap item; v1 imports the base/table inventory and row counts only. Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ## Tested against the real thing Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records. Tested for real against a current NocoDB release: 2 bases and 2 tables (a Deals table with 5 rows) came into Odoo with 0 errors, using an API token created for the purpose. ## Requirements - **Odoo**: 20.0, Community or Enterprise. - **NocoDB**: any NocoDB exposing the v2 Meta/REST API (`/api/v2/meta/bases`, `/api/v2/meta/bases//tables`, `/api/v2/tables//records`) with `xc-token` header auth. NocoDB must be reachable from the Odoo server. - **Python**: no extra libraries beyond a standard Odoo 20 install. - **Odoo access**: any internal user can use the connector screens. The NocoDB API token is stored in a system-only field, so only the **Settings / Administration** user can read or change it. - **Network**: outbound HTTP/HTTPS from Odoo to your NocoDB server. By default the connector refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*). ## Installation 1. Copy `bambooforge_nocodb_connector` into your Odoo addons path. 2. Restart the Odoo service. 3. Open **Apps**, click **Update Apps List**, search for *NocoDB*, and press **Activate / Install**. The only dependencies are *base* and *mail*. No live NocoDB server is required to evaluate the connector: a mock NocoDB API ships inside the module, so you can install, explore and run the full import flow against sample data before pointing it at a real NocoDB. ## Step 1 — Create an API token in NocoDB NocoDB authenticates with a single API token, sent as the `xc-token` header on every request. To create one: 1. In NocoDB, open **Account Settings** (your avatar, top of the sidebar). 2. Go to the **Tokens** section. 3. Click **Create / Add new token**, give it a description such as `Odoo`, and create it. 4. Copy the generated token. You will paste it into Odoo in Step 2. Because the connector is read-only, the token only ever needs read access to your bases, tables and records. ## Step 2 — Create the connection in Odoo Open **NocoDB Connector ▸ Configuration ▸ Instances** and create a record. ![NocoDB instance / connection cockpit](01-connection-cockpit.png) Key fields: | Field | What to enter | |---|---| | Name | A label for this connection, e.g. *My NocoDB*. | | Base URL | Your NocoDB server root, e.g. `https://nocodb.example.com` or `http://localhost:8200`. | | Authentication | *API Token (xc-token)* — the only option NocoDB supports. | | API Token | The token from Step 1 (visible to administrators only). Sent as the `xc-token` header on every request. | | API Path | Leave the default `api/v2` unless your NocoDB differs. The client builds `//meta/bases` and so on. | | Verify SSL | Keep on for production. Turn off only for self-signed test certificates. | | Allow internal host | Off by default. Turn on only to reach a NocoDB on localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. The connector lists your bases as a lightweight auth + connectivity check. A green *Connected* state means the token and URL are correct. If it fails, the exact error is shown on the form and recorded in **Logs** (see *Troubleshooting*). ![An imported NocoDB base with its tables](02-synced-base.png) ## Step 3 — First import (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead of changing data they produce **Validation Results** you can review under **NocoDB Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, click **Queue Base Import** (and/or **Queue Table Import**). This enqueues jobs; it does not block the UI. 2. Jobs are processed by the **Nocodb Queue Processor** scheduled action (every minute), or immediately if you run it manually from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land in **NocoDB Connector ▸ Imported Data ▸ Bases** and **▸ Tables**. Each carries its NocoDB id, so re-imports update the same record instead of duplicating it. Bases are imported before tables so each table links back to its base. ![Imported NocoDB tables with live row counts](03-tables-list.png) ## Field mapping & customization - **Field Mappings** (Configuration) map NocoDB fields to Odoo fields per entity. Use **Generate suggested mappings** on the instance to seed the core ones, then adjust. - **Schema Fields** lists the discovered fields per entity. Run **Schema Introspection** on the instance to refresh it: it records the Odoo target fields and samples one live NocoDB record (falling back to the bundled mock when the API is unreachable) so you can see both sides. - **Import limits** on the instance cap how much is pulled per run: *Bases import limit* (default 100) and *Tables import limit* (default 200), with matching *reconcile* limits. Raise them if your workspace is larger. Because the connector is read-only, there are no stock/image/tax/order toggles — it imports the base and table inventory and row counts, nothing more. ## Automation (scheduled actions & webhooks) The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | Nocodb Queue Processor | every 1 min | Processes queued import jobs. | | Nocodb Reconciliation | every 15 min | Pulls recent remote ids for enabled entities and queues what is missing. | | Nocodb Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs. | | Nocodb Flow Scheduler | every 5 min | Runs scheduled import flows. | | Nocodb Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | Nocodb Auto Recover | every 15 min | Reopens a tripped circuit breaker once NocoDB is healthy again. | Turn on **Auto import** / **Auto reconcile** per entity on the instance (*Auto import bases*, *Auto import tables*, *Auto reconcile bases*, *Auto reconcile tables*) to let the scheduled actions keep the inventory current hands-free. **Webhooks**: this connector is a pull-only importer. The route `/nocodb/webhook` exists only as a neutralized stub that always answers a clear "not supported" response (HTTP 501) — there is **no** webhook intake. A genuine push path (NocoDB does support per-table webhooks) is a roadmap item, not part of this read-only v1. The scheduled reconciliation keeps your inventory current without it. ## Safety features - **Read-only by design** — the connector only ever issues authenticated `GET` calls to NocoDB. There is no write path, so it cannot create, change or delete anything in your workspace. - **Dry-run mode** — simulate Odoo-side writes and review Validation Results before going live (*Dry-run* field, ON by default). - **Business validation profiles** — *Minimal / Standard / Strict* gate risky writes (*Business validation profile* field). - **Resilient queue** — every import is a job with retry and exponential back-off (*Retry limit*, *Backoff policy*, *Backoff base/cap minutes*). - **Circuit breaker** — after repeated failures (or an auth/configuration error) an instance auto-pauses (*Tripped*); the Auto Recover action reopens it once NocoDB responds again (*Auto-pause threshold*, default 5). - **Rollback snapshots** — when enabled, imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots** (*Rollback enabled*). - **SSRF guard** — the connector refuses internal/loopback/private/link-local/ reserved hosts unless *Allow internal host* is explicitly enabled. ## Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails with 401 / 403 | Wrong or missing API token. The `xc-token` is invalid or was revoked. Re-create it in NocoDB (**Account Settings ▸ Tokens**) and paste it into the **API Token** field (Step 1). | | "NocoDB API token (xc-token) is required." | The **API Token** field is empty. Paste the token; only an administrator can see or edit this field. | | "...is not allowed because it resolves to the non-public address..." | Base URL points at localhost/private/loopback/link-local IP. Enable *Allow internal host* on the instance (test/self-hosted only). | | "the URL must start with http:// or https://" | The Base URL has no scheme. Enter the full root, e.g. `https://nocodb.example.com`. | | SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off *Verify SSL* for testing only. | | 404 on bases/tables | The **API Path** is wrong for your NocoDB. The default is `api/v2`; adjust it if your server exposes the API elsewhere. | | Row counts all show 0 | The token cannot read records, or the table is empty. Confirm the token has read access; the count comes from `pageInfo.totalRows` on `/tables//records`. | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check **Scheduled Actions** is active and the instance is not *Paused*. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures (or an auth error). Fix the token/URL; Auto Recover reopens it, or click **Resume**. | | Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | | Tables imported but not linked to a base | The base was not imported yet. Import bases first (the connector does this automatically when both run); re-run the table import. | For anything else, **Operations ▸ Logs** records every API call and error with a timestamp. ## Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the NocoDB side, any server exposing the v2 Meta/REST API with `xc-token` auth. Validate your exact build with the bundled mock first. **Do I need NocoDB installed to evaluate it?** No. A mock NocoDB API ships inside, so you can install, explore and demo the full import flow before connecting a real server. **How does authentication work?** Create an API token in NocoDB under **Account Settings ▸ Tokens** and paste it into the **API Token** field. The connector sends it as the `xc-token` header on every request. **Does the connector write anything to NocoDB?** No. It is read-only: every call is an authenticated `GET`. It cannot create, update or delete bases, tables or records in your workspace. **Does it import the actual rows in my tables?** Not in v1. It imports the base/table inventory and a live row count per table. Importing the row data itself is on the roadmap. **How does it keep the inventory current?** The scheduled actions (Queue Processor + Reconciliation) re-import on a schedule when you enable *Auto import* / *Auto reconcile*. There is no webhook intake. **Is it safe to run against production data?** Yes. It only reads from NocoDB, and on the Odoo side dry-run is ON by default, validation gates risky writes, and rollback snapshots let you undo. You decide when to go live. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ## Data, privacy & limits - The connector **reads** bases, tables and per-table row counts from your NocoDB and writes the corresponding **NocoDB Base** / **NocoDB Table** records in Odoo. It never writes back to NocoDB. - The API token is stored in an administrator-only field. - **In scope today**: base import, table import, live row counts, scheduled re-import/reconciliation, dry-run, validation, rollback snapshots, SSRF guard. - **Out of scope / roadmap**: importing the actual table records (row data); webhook/push intake from NocoDB; writing any data back to NocoDB. ## Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / NocoDB v2 API line. ## Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ### Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_nocodb_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ### Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ## Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ### What is removed - The module's own tables and every record in them (23 models, prefixed `nocodb.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ### What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ## Changelog ### 20.0.1.0.2 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ### 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Odoo 20 connector for NocoDB: two-way row sync (import/export) between selected NocoDB tables and Odoo models, plus base/table metadata import. - Resilient queue, binder-based idempotency, dry-run, SSRF-guarded. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ## How we make sure it works Before every release, **66 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Base and table bindings do not collide. - Dry run export pushes nothing. - Dry run import writes nothing. - Reimport updates not duplicates. - Lock claims pending then refuses second claim. Tested for real against a current NocoDB release: 2 bases and 2 tables (a Deals table with 5 rows) came into Odoo with 0 errors, using an API token created for the purpose. The complete list is in the source you receive. --- # nopCommerce Connector > Read-only nopCommerce import: products, customers and orders read straight from the nopCommerce database on a schedule - no REST API needed. - **What it is:** Read-only nopCommerce import: products, customers and orders read straight from the nopCommerce database on a schedule - no REST API needed. - **For:** eCommerce - **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:** 199 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_nopcommerce_connector - **Run for real:** Tested for real against nopCommerce 4.80 and the current release: 47 products, 6 customers and 5 orders read straight from the store's MySQL database with 0 errors, every order total matching the source exactly. - **Not for:** The connector reads products, customers and orders from the nopCommerce MySQL database and writes the corresponding Odoo records. The MySQL credentials are stored in admin-only fields.; Read-only: the connector never writes to, updates or deletes anything in nopCommerce. It opens the database connection only to run SELECT queries.; In scope today: product/customer/order import, order-status mapping, scheduled sync, dry-run, validation and rollback.; Out of scope / roadmap: refund/credit-note import and inbound webhooks are not yet supported. Export back to nopCommerce is not supported (read-only by design). Tax auto-matching is best-effort, name-based and never auto-creates taxes; multi-warehouse stock routing is not handled. - **Manual:** https://docs.bambooforge.dev/bambooforge_nopcommerce_connector/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_nopcommerce_connector.action_nopcommerce_control_tower - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_nopcommerce_connector - **Manual last updated:** 2026-10-15 --- # nopCommerce Connector A read-only bridge from your nopCommerce store into Odoo 20: import products, customers and orders on a schedule by reading the nopCommerce **MySQL database directly** — with a resilient job queue, dry-run safety, validation and rollback so you stay in control. nopCommerce ships no REST API by default, so the connector never depends on one: it connects to the database with read-only credentials and never writes back. This page is the complete manual. If you follow it top to bottom you can install, connect, run your first sync, automate it, and fix the common issues **without contacting support**. ## Matching the storefront total A storefront charges shipping and grants discounts outside the product lines, so an order rebuilt from those lines alone is worth less than the order the customer paid for: the sale looks smaller than it was and never reconciles against the platform payout. With **Match Storefront Totals** on the instance (on by default): - the shipping charge is imported as its own order line, on a clearly named service product; - whatever still separates Odoo from the platform total — platform discounts, fees, rounding — is posted as a single adjustment line rather than dropped; - the platform total and the remaining difference are stored on the order and shown on its connector tab; - an order that still does not match within **Total Tolerance** raises a validation warning, so the gap is visible instead of silent. Imported lines deliberately carry no Odoo tax: the storefront has already computed tax and it is kept on the order's own tax amount field. Switch the option off if you would rather Odoo recompute everything from your own price lists and tax rules. ### Overview The connector imports data from nopCommerce into Odoo by reading the nopCommerce MySQL database directly (via the `pymysql` library) with the credentials you supply. nopCommerce has no built-in REST API, so there is no API key to generate and no plugin to install on the store side — only read access to the database. It covers: - **Products** — import nopCommerce products into Odoo product templates (name, SKU, price, categories; optionally stock and image URLs). - **Customers** — import nopCommerce customers (name, email, company, phone, role) into Odoo contacts. - **Orders** — import nopCommerce orders into Odoo sale orders (line items, totals, payment method), mapping each order's nopCommerce status to a sale-order action (keep draft, confirm, cancel, or ignore). Direction of sync: **Odoo ← nopCommerce** (import only). The connector is read-only: it never writes to, updates, or deletes anything in nopCommerce, and nopCommerce never pushes to Odoo. Synchronisation runs on a schedule, like a database mirror. Money is read verbatim — nopCommerce stores decimal major units. Every database read goes through a queue: nothing is written to Odoo until a job runs, jobs retry with back-off on transient failures, and a circuit breaker pauses an instance that keeps failing. ### Tested against the real thing Automated tests run against a bundled mock store on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records and real order totals. Tested for real against nopCommerce 4.80 and the current release: 47 products, 6 customers and 5 orders read straight from the store's MySQL database with 0 errors, every order total matching the source exactly. ### Requirements - **Odoo**: 20.0, Community or Enterprise. - **nopCommerce**: live-tested against nopCommerce 4.80 on MySQL/MariaDB. The connector reads the standard nopCommerce schema (PascalCase tables/columns), so the 4.x line on MySQL is supported. The MySQL server must be reachable from the Odoo server. - **Python**: the Odoo server needs the **pymysql** library (`pip install pymysql`). It is the only extra dependency; without it the connector cannot open a database connection and Test Connection fails with a clear message. - **Odoo access**: any internal user can use the connector screens. The MySQL **User** and **Password** are stored in system-only fields, so only the **Settings / Administration** user can read or change them. - **Network**: direct TCP connectivity from the Odoo server to the nopCommerce MySQL port (default `3306`). By default the connector refuses internal/loopback/private storefront hosts as an SSRF safeguard (see *Safety features*). ### Installation 1. Install the `pymysql` Python library on the Odoo server (`pip install pymysql`) and restart Odoo so it is importable. 2. Copy `bambooforge_nopcommerce_connector` into your Odoo addons path. 3. Restart the Odoo service. 4. Open **Apps**, click **Update Apps List**, search for *nopCommerce*, and press **Activate / Install**. Dependencies (*Sales*, *Contacts*, *Invoicing*) install automatically. No live nopCommerce store is required to evaluate the mapping: a bundled set of nopCommerce-row-shaped mock fixtures lets you install the module, run schema introspection and explore the field mappings against sample data. Running a real import, however, needs a reachable MySQL database (see Step 1). ### Step 1 — Grant read-only access to the nopCommerce MySQL database The connector authenticates by connecting to the nopCommerce database, so the first step is on your **database server**, not in the nopCommerce admin. Create a dedicated MySQL user that can only *read* the nopCommerce database, and make sure the Odoo server can reach it over the network. 1. Open a MySQL/MariaDB client (e.g. `mysql` CLI, phpMyAdmin) as an administrator on the server that hosts the nopCommerce database. 2. Create a read-only user and grant it `SELECT` on the nopCommerce database only. Replace the database name, username, password and host to match your setup: ``` CREATE USER 'odoo_ro'@'%' IDENTIFIED BY 'a-strong-password'; GRANT SELECT ON nopcommerce.* TO 'odoo_ro'@'%'; FLUSH PRIVILEGES; ``` Grant **SELECT only** — the connector never needs INSERT, UPDATE or DELETE. A read-only grant is the safest way to guarantee nopCommerce is never modified. If your Odoo server has a fixed IP, replace `'%'` with that IP for a tighter grant (e.g. `'odoo_ro'@'10.0.0.5'`). 3. Allow the connection over the network: - Make sure MySQL listens on an address the Odoo server can reach (check `bind-address` in `my.cnf`; `127.0.0.1` only allows connections from the same host). - Open the MySQL TCP port (default **3306**) in any firewall between the Odoo server and the database server. - If Odoo and nopCommerce share one host, the database is reachable at `127.0.0.1:3306` — but note that a loopback/private host triggers the SSRF guard on the *storefront* URL (see Step 2 and *Safety features*). 4. From the Odoo server, confirm the user can connect and read, for example: ``` mysql -h -P 3306 -u odoo_ro -p nopcommerce -e "SELECT 1;" ``` A clean `1` means the credentials, network path and port are correct. ### Step 2 — Create the connection in Odoo Open **nopCommerce Connector ▸ Configuration ▸ Instances** and create a record. ![nopCommerce instance / connection cockpit](01-connection-cockpit.png) Key fields: | Field | What to enter | |---|---| | Name | A label for this store, e.g. *My Live Store*. | | MySQL Host | Hostname or IP of the nopCommerce MySQL server. Default `127.0.0.1`. | | MySQL Port | TCP port of the MySQL server. Default `3306`. | | MySQL Database | Name of the nopCommerce database/schema, e.g. `nopcommerce`. | | MySQL User | The read-only user from Step 1, e.g. `odoo_ro` (administrators only). | | MySQL Password | The password for that user (administrators only). | | Base URL | Your storefront root, e.g. `https://store.example.com`. Used **only** to build product image links (`/images/thumbs/...`); it is not used for data access. | | Allow internal host | Off by default. Turn on only to point the storefront Base URL at localhost or a private network (lowers the SSRF guard — see *Safety features*). | Then click **Test Connection**. The connector opens a database connection and runs `SELECT 1`. A green *Connected* state means the host, port, database name and credentials are correct. On the first successful connect, the default order-status mapping table is seeded automatically. If it fails, the exact error is shown on the form and recorded in **Logs** (see *Troubleshooting*). Tip: use **Quick Setup** (button on the instance) to pick a use case (catalog only, B2C full sync, …), seed default field mappings and order-status rules, and apply a recommended schedule in one step. ### Step 3 — First sync (dry-run, then live) New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead of changing data they produce **Validation Results** you can review under **nopCommerce Connector ▸ Operations ▸ Validation Results**. This lets you confirm what *would* happen before anything is written. To run a first import: 1. On the instance, click **Import Products** (and/or **Import Customers**, **Import Orders**). This enqueues jobs; it does not block the UI. 2. Jobs are processed by the **nopCommerce Queue Processor** scheduled action (every minute), or immediately if you run it manually from **Operations ▸ Queue Jobs**. 3. Review **Validation Results** while still in dry-run. 4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to write the records for real. Imported records land in the standard Odoo apps: **Sales ▸ Products**, **Contacts**, and **Sales ▸ Orders**. Each carries its nopCommerce reference so re-imports update the same record instead of duplicating it. ### Order status mapping nopCommerce stores each order's status as a numeric `OrderStatusId`, which the connector normalizes to a status slug (`pending`, `processing`, `complete`, `cancelled`). Under **Configuration ▸ Order State Mappings** each instance gets a default table that decides what happens to the Odoo sale order when an order arrives in a given status: ![nopCommerce order state mapping table](04-order-state-mapping.png) | nopCommerce status | Default Odoo action | |---|---| | pending | Keep draft | | processing | Confirm sale order | | complete | Confirm sale order | | cancelled | Cancel sale order | Change any row to **Ignore (do nothing)**, **Keep draft**, **Confirm sale order** or **Cancel sale order**. Unknown/unmapped statuses default to *Ignore*, so a status you have not mapped never triggers a destructive transition. ### Field mapping & customization - **Field Mappings** (Configuration) map nopCommerce columns to Odoo fields per model. Use **Generate suggested mappings** on the instance to seed the business-critical ones, then adjust. The mapping board flags fields that need attention. - **Schema Fields** lists the discovered nopCommerce columns per resource. Run **Schema Introspection** on the instance to refresh it (it samples live rows; if the database is unreachable it falls back to the bundled sample row so you can still review the mapping). - **Stock / images / tax** are opt-in toggles on the instance: *Sync stock* (reads `StockQuantity`), *Sync images* (and *max images*), *Sync taxes* / *Auto-match taxes* (best effort, name-based, never auto-creates taxes). They are off by default; turn on only what you need. ### Automation (scheduled actions) Sync is **schedule-driven only**. nopCommerce has no REST API and emits no events, so there are no real-time webhooks — the connector polls the database on a cron, like a database mirror. The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**): | Scheduled action | Default | Purpose | |---|---|---| | nopCommerce Queue Processor | every 1 min | Processes queued import jobs. | | nopCommerce Reconciliation | every 15 min | Re-reads recent rows for enabled models and re-queues anything missing. | | nopCommerce Maintenance | every 1 hr | Recovers stale/locked jobs and trims old logs. | | nopCommerce Flow Scheduler | every 5 min | Runs scheduled sync flows. | | nopCommerce Flow Metrics | every 1 hr | Aggregates flow-run metrics. | | nopCommerce Auto Recover | every 15 min | Reopens a tripped circuit breaker once the database is healthy. | Turn on **Auto import** / **Auto reconcile** per model on the instance to let the scheduled actions keep things in sync hands-free. ### Safety features - **Read-only by design** — the connector only ever runs `SELECT` queries against nopCommerce; it never writes, updates or deletes there. Use a read-only MySQL grant (Step 1) to enforce this at the database level. - **Dry-run mode** — simulate writes and review Validation Results before going live. - **Business validation profiles** — *Minimal / Standard / Strict* gate risky writes into Odoo. - **Resilient queue** — every read/import action is a job with retry and exponential back-off. - **Circuit breaker** — after repeated failures an instance auto-pauses (*Tripped*); the Auto Recover action reopens it once the database responds again. - **Rollback snapshots** — when enabled, imports capture a snapshot so you can undo a batch from **Operations ▸ Rollback Snapshots**. - **SSRF guard** — the connector refuses an internal/loopback/private storefront Base URL unless *Allow internal host* is explicitly enabled. ### Where nopCommerce shows up in Odoo The connector stays out of the way on records that have nothing to do with nopCommerce: - **Sales orders** - the *nopCommerce* tab, *Queue nopCommerce Export* and *nopCommerce Binding* appear only on orders that came from nopCommerce, are bound to nopCommerce, or belong to a customer bound to nopCommerce. - **Products and contacts** - the *nopCommerce* tab appears once an active nopCommerce connection exists; the *nopCommerce Binding* button inside it appears once the record is bound. - List views, the sync queue and the control tower are unchanged. ### Troubleshooting | Symptom | Cause and fix | |---|---| | Test Connection fails: "pymysql ... is required" | The `pymysql` library is not installed on the Odoo server. Run `pip install pymysql` and restart Odoo. | | Test Connection fails: access denied / authentication error | Wrong MySQL user or password, or the user lacks rights on this database. Re-check the **MySQL User / Password** and the `GRANT SELECT` from Step 1. | | Test Connection fails: can't connect / host not reachable / timeout | The Odoo server cannot reach the MySQL host:port. Check the **MySQL Host** and **Port**, that MySQL's `bind-address` allows remote connections, and that port 3306 is open in the firewall. | | Test Connection fails: "MySQL database name is required" | The **MySQL Database** field is empty. Enter the nopCommerce schema name. | | "Base URL ... is not allowed" / refused internal host | The storefront **Base URL** points at localhost or a private IP. Enable *Allow internal host* on the instance (test/self-hosted only). | | Orders import but never confirm | The order's nopCommerce status is mapped to *Keep draft* or *Ignore*. Adjust **Order State Mappings**. | | Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Check **Scheduled Actions** is active and the instance is not *Paused*. | | Instance shows *Tripped* | Circuit breaker tripped after repeated failures. Fix the database/credentials; Auto Recover reopens it, or click **Resume**. | | Records imported twice | Imports are keyed by the nopCommerce id, so this should not happen. If it does, check that two instances do not point at the same database. | | Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. | For anything else, **Operations ▸ Logs** records every database read and error with a timestamp. ### Frequently asked questions **Which versions are supported?** Odoo 20.0 on the Odoo side. On the store side the connector was live-tested on nopCommerce 4.80 on MySQL/MariaDB and reads the standard 4.x schema. Validate your exact build with the bundled sample fixtures first. **Do I need nopCommerce installed to evaluate it?** Not for the mapping. Bundled nopCommerce-row-shaped sample fixtures let you install the module, run schema introspection and explore the field mappings. A real import needs a reachable MySQL database. **How does authentication work?** There is no API key. nopCommerce has no REST API, so the connector connects to the nopCommerce **MySQL database** with read-only credentials (host, port, database, user, password). Generate those credentials on your database server (see Step 1). **Does this write anything back to nopCommerce?** No. The connector is read-only: it only runs `SELECT` queries and never writes, updates or deletes in nopCommerce. Use a read-only MySQL grant to enforce this at the database level. **How does real-time sync work?** There is no real-time/webhook path — nopCommerce has no REST API and emits no events. Sync is scheduled: the connector polls the database on a cron (every minute for the queue, every 15 minutes for reconciliation). Tighten the cron interval if you need fresher data. **Is it safe to run against production data?** Yes. It only reads nopCommerce. On the Odoo side, dry-run is ON by default, validation blocks risky writes, and rollback snapshots let you undo. You decide when to go live. **What support and refund policy do I get?** Every request is answered within 24 hours, setup help included. If you report a bug within 2 months of purchase and it is not resolved within 15 days, you are entitled to a full money-back refund. ### Data, privacy & limits - The connector reads products, customers and orders from the nopCommerce MySQL database and writes the corresponding Odoo records. The MySQL credentials are stored in admin-only fields. - **Read-only**: the connector never writes to, updates or deletes anything in nopCommerce. It opens the database connection only to run `SELECT` queries. - **In scope today**: product/customer/order import, order-status mapping, scheduled sync, dry-run, validation and rollback. - **Out of scope / roadmap**: refund/credit-note import and inbound webhooks are not yet supported. Export back to nopCommerce is not supported (read-only by design). Tax auto-matching is best-effort, name-based and never auto-creates taxes; multi-warehouse stock routing is not handled. ### Support & updates - Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included. - Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full refund. - Full source is included. Updates track the supported Odoo 20 / nopCommerce 4.x line. ### Upgrading & version compatibility This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its own dedicated build of this module — always install the build that matches your Odoo version. Mixing a build with a different Odoo series is not supported. ## Patch upgrades (same series, e.g. 20.0.1.0.0 → later) 1. **Back up** your database and filestore first. 2. Replace the module folder with the newer build. 3. Restart Odoo with the module updated: ``` ./odoo-bin -c your.conf -u bambooforge_nopcommerce_connector -d your_db ``` 4. Odoo applies any schema/data changes automatically. Your existing records and configuration are preserved. ## Cross-version migration (moving to a newer Odoo) Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade tooling. When you migrate the database to the next Odoo series, install the matching build of this module for that series. Data created by this module carries over with the database migration. After any upgrade the module's scheduled actions resume on their normal cadence — no manual re-activation is required. ### Uninstallation You can remove this module at any time from **Apps → (this module) → Uninstall**, or from the command line. Uninstalling is clean and reversible by reinstalling — but note what is and is not deleted. ## What is removed - The module's own tables and every record in them (20 models, prefixed `nopcommerce.*`) — this is the data this module created. - The menus, actions, views and reports this module installed. - Its scheduled actions (cron jobs) — they stop immediately on uninstall. - Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo. ## What is preserved - **Your remote platform is never touched.** Uninstalling only removes the Odoo-side connector; products, customers and orders on the external store/service are untouched. - Records already imported into standard Odoo models (e.g. contacts, products, sales orders) remain — they are ordinary Odoo records once created. - Attachments and chatter messages on standard records are kept. As always, take a database backup before uninstalling in production. ### Changelog ## 20.0.1.2.5 - Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships. ## 20.0.1.0.0 Current release for Odoo 20.0. This build includes: - Read-only Odoo 20 <- nopCommerce connector: imports products, customers and orders by reading the nopCommerce MySQL database directly (no REST API needed) on a schedule. - With a live Sync Control Tower, resilient queue, dead-letter console, conflict workbench and dry-run safety. Feature additions and fixes ship as new builds on the Odoo Apps store; this page and the module's `version` reflect the current published release. Always keep the build matched to your Odoo series (see *Upgrading & version compatibility*). ### How we make sure it works Before every release, **101 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20. Among them: - Nothing at all is an empty row not a crash. - A product shows its binding button only once bound. - Import to odoo true but dry run never moves stock. - Push stock read only raises even for negative. - Stock export job records clean skip no write. Tested for real against nopCommerce 4.80 and the current release: 47 products, 6 customers and 5 orders read straight from the store's MySQL database with 0 errors, every order total matching the source exactly. The complete list is in the source you receive. --- # Attachment Preview: Office & PDF Viewer > Preview Microsoft Office and LibreOffice files, images and PDFs inline on any record, without downloading the attachment first. - **What it is:** Preview Microsoft Office and LibreOffice files, images and PDFs inline on any record, without downloading the attachment first. - **For:** Document Management - **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:** 69 EUR, one-time, on the Odoo Apps Store - **Technical name:** bambooforge_office_preview - **Not for:** Be precise about where each file goes — it differs by format:; Microsoft Office documents and OpenDocument documents (Word/Excel/PowerPoint, .rtf, .txt, .odt, .ods, .odp) are rendered by Microsoft's Office Online viewer, a third-party service. To render them, Microsoft's service fetches the file content from your Odoo server over the internet (the same as opening the file on Office.com). For these formats the file content leaves your server and is processed by Microsoft.; OpenDocument templates (.ott, .otp, .ots) are rendered by the bundled ViewerJS / WebODF entirely in your browser. The file content does not go to any external service.; PDFs use Odoo's built-in PDF viewer — local, unchanged by this app.; How the Microsoft link is protected:; A preview is only generated after an access check — the requesting user must have read access to the attachment under Odoo's record rules.; Microsoft is handed a short-lived, HMAC-signed token (valid 6 hours) pointing at a file route that verifies the signature and expiry. There is no permanent public file URL; a leaked link stops working when it expires.; The signing secret is generated randomly on first use and stored in the system parameter bambooforge_office_preview.token_secret. - **Manual:** https://docs.bambooforge.dev/bambooforge_office_preview/ - **Live demo:** https://demo20.bambooforge.dev/odoo/action-base.action_attachment - **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_office_preview - **Manual last updated:** 2026-10-15 --- # Attachment Preview: Office & PDF Viewer Open Microsoft Office, OpenDocument and PDF attachments **inline** inside Odoo — in the chatter attachment view and in Odoo's web file-viewer modal — instead of downloading every file just to read it. Word, Excel, PowerPoint and OpenDocument documents render through Microsoft's official Office Online viewer; OpenDocument templates render locally in your browser, and PDFs use Odoo's built-in viewer. This page is the complete manual. If you follow it top to bottom you can install the app, understand exactly how each file type is rendered (and where its content goes), use it, and fix the common issues **without contacting support**. ## Overview The app extends Odoo's attachment handling so that office documents preview in place, the same way Odoo already previews images and PDFs. There is nothing to configure and no new menu — once installed, eligible attachments simply become viewable. It recognises an attachment by its MIME type, and falls back to the file extension when the MIME type is generic or missing. Three rendering paths are used, chosen automatically per file: | File type | Examples | Rendered by | |---|---|---| | Microsoft Office documents | .doc, .docx, .docm, .dotx, .dotm, .xls, .xlsx, .xlsm, .xltx, .xltm, .ppt, .pptx, .pptm; plus .rtf and .txt | Microsoft Office Online viewer (external service) | | OpenDocument documents | .odt, .ods, .odp | Microsoft Office Online viewer (external service) | | OpenDocument templates | .ott, .otp, .ots | Bundled ViewerJS / WebODF, locally in your browser | | PDF | .pdf | Odoo's built-in PDF viewer (unchanged) | The app touches two places in the Odoo web client: - the **chatter attachment view** (the document panel that opens beside a record's messages), and - Odoo's **web file-viewer modal** (the full-screen overlay you get when you click an attachment). Wherever Odoo would already show an image or PDF preview, this app adds the office and OpenDocument formats alongside it. SVG files are deliberately left non-previewable for safety. ## Requirements - **Odoo**: 20.0, Community or Enterprise. The app depends only on the **Mail** module (`mail`), which is present on every Odoo install. - **Python**: no extra libraries. Nothing is added or compiled on the server. - **Browser**: any current desktop browser (Chrome, Firefox, Edge, Safari). Previews render in an `