Thursday, September 10, 2026
18 changes · saas-19.4
New functionality added to Odoo
Belgian companies can now generate SAF-T reports directly in Odoo. This supports local compliance needs by making it easier to produce the required accounting export for audits or reporting requests.
Original PR description
SAF-T reports can now be generated for Belgium. task-5129628 Forward-Port-Of: odoo/enterprise#107270
Enhancements to existing features
Imported electronic invoices now better recognize reverse charge taxes even when they appear as 0% in the XML. This helps accounting teams avoid manual tax corrections and also keeps analytic distribution information from prediction during import.
Original PR description
Reverse charge taxes can be reported as 0% in the XML, meaning that we wouldn't be able to predict them even if e already set it right on a previous invoice for the same partner.Description of the issue/feature this PR addresses:
Resolved issues and error corrections
This fix keeps website icons and selected product options readable when a dark color palette is used. It ensures automatic contrast colors are preserved so storefront pages look consistent and remain easy to use.
Original PR description
Steps to reproduce: - Select a dark website color palette. - Drag and drop a "Features" snippet onto the page. => The icons use dark text over their dark `bg-o-color-3` backgrounds. Before this commit, [1] gave shaped icons a dark fallback color to make their default light backgrounds readable. The shaped icon selector was more specific than `bg-o-color-*`, so it also overrode the contrast color computed for explicit backgrounds. After this commit, the fallback applies only without a `bg-*` class. Explicit backgrounds keep their automatically contrasted color while icons with the default background remain readable. [1]: c5e16ae task-6485048
Installing the Colombian electronic invoicing features is now faster on large databases. The change avoids a time-consuming one-time recalculation during setup while keeping normal invoice updates working as before.
Original PR description
- Pre-create the stored computed columns `l10n_co_edi_type`, `l10n_co_dian_state`, and `l10n_co_edi_cufe_cude_ref` in `_auto_init()`. - This prevents Odoo from computing and writing these fields for all existing `account.move` records when installing `l10n_co_dian`. - This is particularly important for large databases with a high volume of Colombian accounting moves, where the initial computation can take too long and cause the module installation to hit the time limit. - The compute methods are kept unchanged, so the fields continue to be computed normally for subsequent record creation or dependency changes. **opw-6451331** Forward-Port-Of: odoo/enterprise#130671 Forward-Port-Of: odoo/enterprise#129507
Completing a field service shift now correctly recalculates the planned hours assigned to it. This helps keep scheduling, timesheet, and service planning information aligned for better operational reporting.
Original PR description
This reverts commit b642ea4d5d2c0b4bd83c70103f7e8dfa6ef736dd. opw-6542896 Forward-Port-Of: odoo/enterprise#130832
This update prevents users from being blocked by an access error when saving records that include restricted calculated fields. It ensures internal calculations can complete safely while keeping normal field visibility rules in place.
Original PR description
**Description of the issue/feature this PR addresses:** If a field is `computed`, `precomputed`, and has `groups` defined, an `AccessError` is raised when a user who does not have the required group…
**Description of the issue/feature this PR addresses:** If a field is `computed`, `precomputed`, and has `groups` defined, an `AccessError` is raised when a user who does not have the required group creates a record because the field is precomputed. After this commit https://github.com/odoo/odoo/pull/201565/changes/48521a311a6dc857c3808db70ed359c7866aa125, Odoo checks field access in `__get__`, so an `AccessError` is now raised. If the field is not precomputed, everything works fine. Therefore, this commit prevents the error by using `sudo()` to recompute the value. I have attached a module to demonstrate the issue. **Steps to reproduce the issue:** 1. Install the attached module. [sale_margin_security_test.zip](https://github.com/user-attachments/files/31967357/sale_margin_security_test.zip) 2. Create a sales order and add a product. 3. Try to save the sales order. The AccessError is raised. https://github.com/user-attachments/assets/54c7e4e2-005f-4ced-bb62-22d0e00049d9 For more context, this module is a simple example extracted from the OCA `sale_margin_security` module, which inherits from a mixin and adds groups to the fields: https://github.com/OCA/margin-analysis/pull/285 You can see the error in this PR: https://github.com/OCA/margin-analysis/actions/runs/34254749207/job/102157600233?pr=285#step:8:126 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287257
Fixes an issue where Point of Sale could charge variant extra prices twice when products were selected through barcode search or related flows. This helps ensure customers are charged the intended price and reduces cashier pricing errors.
Original PR description
Steps to reproduce: - Create an attribute "Size" with values M and L, variants creation mode "Dynamically", and an extra price of 10 on L - Create a product at 30 using that attribute, and give the L…
Steps to reproduce:
- Create an attribute "Size" with values M and L, variants creation mode "Dynamically", and an extra price of 10 on L
- Create a product at 30 using that attribute, and give the L variant a barcode
- In the PoS, type that barcode in the search bar and click the card
Issue:
The line is added at 50 instead of 40. Scanning the barcode with a barcode reader was fixed by c1ae3261e895, but resolving the variant from the search bar still charges the extra price twice.
Cause:
The lst_price of a variant already contains the extra price of every attribute value that creates a variant ('always' and 'dynamic'); only 'no_variant' extras are missing from it and have to be carried by the order line as price_extra. This is what ProductConfiguratorPopup does, hence the correct price when the configurator opens.
Both openConfigurator(), when the resolved variant leaves a single value per attribute line and no popup is needed, and handleConfigurableProduct(), when configure is false, filter those values with create_variant !== 'always', so a 'dynamic' extra is added on top of a lst_price that already includes it. The same fix was made in 18.0 by d4fabfa08d1c but was lost when pos_store.js was refactored in saas-18.1.
Fix:
Filter on create_variant === 'no_variant' in both places, like the configurator popup. This supersedes the !opts.code condition of c1ae3261e895: product_template_variant_value_ids never holds 'no_variant' values, so the extra is now ignored however the line was added - scan, barcode search, sale order import or optional product.
opw-6531114
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#286460This update refreshes the spreadsheet engine and fixes several user-facing issues around charts, printing, search and replace, copy/paste, and dashboard loading. Users should see more reliable spreadsheet reports, better exports, and fewer display problems in dashboards and printed views.
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/cdb63b4ec9 [REL] 19.4.12 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/cdb63b4ec9 [REL] 19.4.12 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/3aada7e2af [FIX] search and replace: manage invalid range [Task: 6483222](https://www.odoo.com/odoo/2328/tasks/6483222) https://github.com/odoo/o-spreadsheet/commit/1512b72179 [FIX] xlsx: fix geo chart xlsx export [Task: 4632983](https://www.odoo.com/odoo/2328/tasks/4632983) https://github.com/odoo/o-spreadsheet/commit/653c219732 [FIX] calendar chart: wrong groupBy choice filtering [Task: 5358625](https://www.odoo.com/odoo/2328/tasks/5358625) https://github.com/odoo/o-spreadsheet/commit/e2475d3d89 [FIX] charts: show value do not work for combo chart [Task: 6528059](https://www.odoo.com/odoo/2328/tasks/6528059) https://github.com/odoo/o-spreadsheet/commit/d05a4c1d8a [FIX] charts: some charts cannot be aggregated [Task: 6501177](https://www.odoo.com/odoo/2328/tasks/6501177) https://github.com/odoo/o-spreadsheet/commit/9a60018018 [FIX] print: always print in light mode [Task: 6432165](https://www.odoo.com/odoo/2328/tasks/6432165) https://github.com/odoo/o-spreadsheet/commit/a462726737 [FIX] charts: change chart runtime when changing the theme [Task: 6432165](https://www.odoo.com/odoo/2328/tasks/6432165) https://github.com/odoo/o-spreadsheet/commit/0563389302 [FIX] print: handle hidden headers [Task: 6332587](https://www.odoo.com/odoo/2328/tasks/6332587) https://github.com/odoo/o-spreadsheet/commit/09b1f961cc [IMP] package: update owl to alpha 49 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/10529bf90d [FIX] composer speech_bubble: move after rendering [Task: 6484405](https://www.odoo.com/odoo/2328/tasks/6484405) https://github.com/odoo/o-spreadsheet/commit/b5a6ad8264 [FIX] grid: hide AddRowFooter when the mainViewport is too small [Task: 6103620](https://www.odoo.com/odoo/2328/tasks/6103620) https://github.com/odoo/o-spreadsheet/commit/94b92798e5 [FIX] ComposerHighlight: Highlight the correct sheet with `#` [Task: 6527596](https://www.odoo.com/odoo/2328/tasks/6527596) https://github.com/odoo/o-spreadsheet/commit/6b1932c4a2 [FIX] clipboard: typo on copy/paste on a merge [Task: 6515850](https://www.odoo.com/odoo/2328/tasks/6515850) https://github.com/odoo/o-spreadsheet/commit/29c3a79719 [PERF] evaluation: clip range to sheet [Task: 6483083](https://www.odoo.com/odoo/2328/tasks/6483083) Co-authored-by: Florian Damhaut (flda) <flda@odoo.com> Co-authored-by: Anthony Hendrickx (anhe) <anhe@odoo.com> Co-authored-by: Alexis Lacroix (laa) <laa@odoo.com> Co-authored-by: Lucas Lefèvre (lul) <lul@odoo.com> Co-authored-by: Adrien Minne (adrm) <adrm@odoo.com> Co-authored-by: Ronak Mukeshbhai Bharadiya (rmbh) <rmbh@odoo.com> Co-authored-by: Dhrutik Patel (dhrp) <dhrp@odoo.com> Co-authored-by: Rémi Rahir (rar) <rar@odoo.com> Co-authored-by: Pierre Rousseau (pro) <pro@odoo.com> Co-authored-by: Vincent Schippefilt (vsc) <vsc@odoo.com> Co-authored-by: Marceline Thomas (matho) <matho@odoo.com>
Messages now correctly remove people who were accidentally mentioned and then replaced before sending. This prevents unintended recipients from being notified when their name only appears as part of another person’s mention.
Original PR description
When a user mentions the wrong partner and corrects it by continuing to type, e.g. picking "John" by mistake, typing further so the text becomes "@ John Doe" and picking "John Doe" in the suggestion…
When a user mentions the wrong partner and corrects it by continuing to type, e.g. picking "John" by mistake, typing further so the text becomes "@ John Doe" and picking "John Doe" in the suggestion popup, the discarded first pick stays in the composer's mentioned partners. On post, mentions are validated by searching the body for "@<name>", and "@ John" is found inside "@ John Doe", so the partner the user tried to replace is kept in the recipients and gets notified even though no mention of them remains visible in the message. Validate mentions from the longest mention text to the shortest, counting the occurrences of each text and blanking them out before looking for shorter ones. A partner whose mention text only appears inside a longer mention is dropped, while distinct partners sharing the same name each consume one occurrence. Steps to reproduce: - Create contacts "John" and "John Doe" - On any record, open the chatter and type "@John", pick "John" by mistake, then keep typing " Doe" and pick "John Doe" in the suggestion popup to correct it - Send the message => The message is also sent to "John" although only "@John Doe" appears in the body. --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287122 Forward-Port-Of: odoo/odoo#284239
This fixes an issue where some outgoing emails could fail during setup or data loading when translations were involved. It helps ensure emails are sent reliably after database changes are saved, especially in non-English environments such as French.
Original PR description
Currently an exception is generated when the `send_after_commit` tries to send the email as below step: - Create a database without demo data and language `fr_FR` - Install the `appointment` module -…
Currently an exception is generated when the `send_after_commit` tries to send the email as below step: - Create a database without demo data and language `fr_FR` - Install the `appointment` module - Set up outgoing email server - Error appears in the log when loading the demo data Error: `TypeError:'NoneType' object is not subscriptable` This issue occurs after the recent refactoring changes in [1]. The method `send_after_commit` (see[2]) sends the email with a new cursor after committing to the current cursor, and here when `_send()` tries to send the mail, it uses the `_()` method for translation, which accesses `self.env` (self refers to the old closed cursor). However, at this point, the cursor is already closed. As a result, when the code at [3] is reached from `ormcache`, it raises the above error because `model.env.transaction.ormcaches__` is `None` (code ref [4]). This commit fixes the above issue by using `self.env._()` for translation instead of `_()` while sending the mail to ensure the translation accesses the current environment cursor instead of the closed one. [1]: https://github.com/odoo/odoo/commit/13c3adf3a8b5ba6325190d6b9aea45fb8a6a8b2f [2]: https://github.com/odoo/odoo/blob/86d750e777d1adc09f53016a255c7b1158fd309c/addons/mail/models/mail_mail.py#L708-L713 [3]: https://github.com/odoo/odoo/blob/5ef7829895b2e05650da394c1e35dfdc3a23c066/odoo/orm/cache.py#L111 [4]: https://github.com/odoo/odoo/blob/5ef7829895b2e05650da394c1e35dfdc3a23c066/odoo/orm/environments.py#L1012 Sentry-7608119520
Timesheet tracking now handles short work activity after an away-from-keyboard period more accurately. This prevents small work events from being incorrectly counted as AFK time, improving the reliability of recorded working time.
Original PR description
Before this Commit, if an AFK event was followed by small events, those small events would be packed into the AFK event. This caused the recorded time worked to be inaccurate. After this Commit, when an AFK event is followed by small events, they are either packed together to form a bigger non‑AFK event or ignored if they can't be packed. task-[6486145](https://www.odoo.com/odoo/project/4105/tasks/6486145) Forward-Port-Of: odoo/enterprise#128710
Odoo now checks for duplicate purchase receipts and outgoing receipts in the same way it already checked bills and invoices. This helps prevent duplicate accounting documents from being missed when document types are changed, reducing the risk of repeated payments or incorrect records.
Original PR description
Right now a duplicate is detected if it's a bill, but is not when you switch it to a receipt. This fix makes sure that both bills and purchase receipts duplicates are detected and are checked against each other. The same change is done for invoices and outgoing receipts. task-6115836 Forward-Port-Of: odoo/odoo#287261 Forward-Port-Of: odoo/odoo#279455
This fixes checkout for orders that include a free promotional reward item when zero-priced product sales are otherwise blocked. Customers can now complete valid purchases with free gifts instead of being sent back to the cart with a warning.
Original PR description
As of commit b8e790b2, a cart containing a product priced at 0 while the website forbids the sale of zero-priced products is no longer payable: the customer is redirected back to the cart with a warning. Reward lines were caught by that new rule. A promotion offering a free gift whose product has no sale price adds a reward line priced at 0 to the cart, so the whole cart became unpayable even though nothing was wrong with it. This commit excludes reward lines from the zero-priced rule, the same way delivery lines already are. opw-6526396 Forward-Port-Of: odoo/odoo#287232 Forward-Port-Of: odoo/odoo#286464
German Intrastat exports now use the required region code 99 when dispatched goods originate outside Germany. The fix also improves German export reliability by handling local number formatting and aligning weight rounding with official reporting rules.
Original PR description
### Issue: In Germany, the intrastat report requires region code `99` for dispatch moves where the product origin country is not DE Per the official specification (section 5.8 — Region of origin):…
### Issue: In Germany, the intrastat report requires region code `99` for dispatch moves where the product origin country is not DE Per the official specification (section 5.8 — Region of origin): "As for goods with foreign origin, code '99' should be entered" https://erhebungsportal.estatistik.de/Erhebungsportal/api/assets/files?downloadId=0c5422f111104705b021b41616ad1ecc But `99` was not being used in those cases ### Cause: `99` was already the default fallback when no `region_code` is set but `_fill_missing_values` had no condition to override the region code for dispatch moves with a non-DE origin country ### Notes: While fixing this, two additional issues were found when exporting with the German locale (`de_DE`): - `weight` and `supplementary_units` are formatted with a comma as decimal separator by the German locale, causing `float()` to raise a `ValueError` — fixed by normalizing to dot before conversion, as done in other localizations - The dispatch/arrival check was comparing against the translated label (e.g. `'Versand'`) instead of a stable identifier A new `intrastat_type_code` field with static values `arrival` and `dispatch` is added based on the existing `id` values from `default_type`, avoiding locale-dependent comparisons This improvement could be extended to other localizations - Net mass is now rounded to full kilograms per item (see §5.14 of the specification linked above) A weight rounding down to 0 kg is reported as `0` instead of being silently dropped (QWeb `t-out` omits the element when the value is `None`) Totals are computed as the sum of already-rounded per-item values to stay internally consistent ### Steps to reproduce: - Install `sale_management`, `l10n_de_account` and `accountant` - Switch to the DE company - In Settings, configure the Intrastat values: -- Default invoice transaction code: 11 -- Default refund transaction code: 21 -- Intrastat region: 07 - Create 3 products with a commodity code set and country of origin: one DE, one empty, one other country - Create and confirm a Sale Order for all products with a European partner, deliver all, create and send the invoice - Open the Intrastat report (Accounting > Reporting > Taxes & Fiscal > Intrastat) - Set the month to the current month Before the fix, all regions show 07 Expected: non-DE origin products should show 99 opw-6455585 Forward-Port-Of: odoo/enterprise#128747
Fixed an issue where Time Off accrual allocations could fail if an accrual plan was approved before any milestones were added. The system now correctly initializes the accrual timing when the first milestone is added, allowing employees to save time off requests without errors.
Original PR description
## Steps to Reproduce: - Install the Time Off module. - Create an Accrual Plan without any milestones. - Create an Accrual Allocation using the newly created accrual plan. - Approve the allocation. -…
## Steps to Reproduce: - Install the Time Off module. - Create an Accrual Plan without any milestones. - Create an Accrual Allocation using the newly created accrual plan. - Approve the allocation. - Add a milestone to the accrual plan. - Create a new Time Off request after the allocation start date. - Save the record. ## Error: `TypeError - '>' not supported between instances of 'datetime.date' and 'bool'` ## Cause: `lastcall` is initialized by method `_add_lastcalls()`, which is only called at create and write. When an accrual allocation is created with an accrual plan that has no milestones, `_add_lastcalls()` returns early because `level_ids` is empty, leaving `lastcall` set to `False`. - [1] If a milestone is added later, `lastcall` is compared with `first_level_start_date`, resulting in a comparison between boolean and datetime, which raises an error. ## Fix: When `lastcall` is not set, default it to `first_level_start_date`. [1] - https://github.com/odoo/odoo/blob/27036bea232572ba692fbb95387911eb453266bf/addons/hr_holidays/models/hr_leave_allocation.py#L703-L706 sentry-7615375197 Forward-Port-Of: odoo/odoo#282074 Forward-Port-Of: odoo/odoo#280674
The Turkish e-Ledger export now fills line numbers automatically and keeps numbering continuous across monthly filings within the same fiscal period. This helps ensure reports match GIB expectations and avoids manual post-export corrections or numbering resets.
Original PR description
Before: the `LineNumber` column of the e-Ledger CSV was always empty, left to be filled after the export. Since the ledger is filed monthly, whatever filled it restarted at 1 every month, while GİB expects the numbering to start once, at the beginning of the fiscal period, and to run unbroken until its end. Now: the column is written by the export. An export starting after the first day of the fiscal period is offset by the number of rows that period already counts, so February continues where January stopped, and a full-year export numbers the same rows identically. The lines are also read in ascending date order. They were sorted by the default order of `account.move.line`, the most recent first, so the first row of the file was the last entry of the period and could not be numbered 1. This reverses `EntryNumberCounter` as well, which now counts from the oldest entry. task-6424730 Forward-Port-Of: odoo/enterprise#130923 Forward-Port-Of: odoo/enterprise#127518
This fix prevents restaurant tables in the German POS certification flow from being opened before required certification updates are complete. It avoids a timing issue that could block or delay table access, making point-of-sale use more reliable for staff.
Original PR description
In this commit: ------------------ - The API call is triggered when an order is updated in Fiskaly. - Previously, the request could be awaited while opening a table, blocking the table from opening before the product screen was ready. - Now, the request is awaited before allowing the table click, ensuring the table opens only after the request is completed. Task: 6522079 Forward-Port-Of: odoo/enterprise#130896 Forward-Port-Of: odoo/enterprise#130132
Argentine electronic invoices could fail to print when an export customer used an identification type without an ARCA code, such as a foreign VAT number. The fix restores the expected fallback value so the QR code can be generated and invoices can be printed reliably.
Original PR description
## Description Printing any posted electronic invoice whose commercial partner has an identification type **without** ARCA code (typically the generic `VAT` type from `l10n_latam_base`, common on…
## Description
Printing any posted electronic invoice whose commercial partner has an identification type **without** ARCA code (typically the generic `VAT` type from `l10n_latam_base`, common on foreign partners of export invoices) crashes:
```
File ".../l10n_ar_edi/models/account_move.py", line 161, in _compute_l10n_ar_afip_qr_code
data.update({'tipoDocRec': int(rec._get_partner_code_id(commercial_partner_id))})
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
```
## Steps to reproduce
1. Install `l10n_ar_edi`.
2. Create a customer: Country **Spain**, Identification Type **VAT** (no ARCA code), Identification Number `ESA12345674`, ARCA Responsibility Type **Cliente / Proveedor del Exterior**.
3. Create and validate an invoice for this customer on an export electronic journal (document type 19).
4. Print the invoice → traceback above.
## Root cause
Since bb8e6fda72ae6364cf7806acb400b071f4108fc9 ([FIX] l10n_ar_edi: return the correct ARCA code when final consumer, odoo/enterprise#106881), `_get_partner_code_id()` lost its final `return partner_id_code` fallback: when the identification type has no ARCA code and the partner is not a Final Consumer, the method now returns an implicit `None`. The QR code compute casts the result with `int()`, which accepted the previous falsy return (`int(False) == 0`, rendering `tipoDocRec: 0` as in 17.0/18.0) but raises on `None` — making every such posted invoice impossible to print.
## Fix
Restore the fallback return so the method always returns the identification type code (possibly falsy) instead of an implicit `None`. The intent of bb8e6fda72ae is preserved: a Final Consumer with an identification number still gets its real code, and the other callers already handle falsy values (`partner_id_code or 0`).
Forward-Port-Of: odoo/enterprise#126714