Thursday, September 10, 2026
16 changes · saas-19.3
Resolved issues and error corrections
Point of Sale now avoids charging product variant extra prices twice when items are selected through barcode search or similar flows. This ensures customers are charged the correct amount and helps prevent checkout 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#286460Fixes a website builder issue where applying text animation across multiple text blocks could shift centered content out of alignment. This helps editors animate selected text without disrupting the page layout visitors see.
Original PR description
### Problem: Text animation used a single span around the full selection range. When a selection crossed block elements, this placed headings and paragraphs inside a span, producing invalid HTML and changing their layout. ### Steps to reproduce: 1. Drag & drop a snippet (like the "Cover" block) that contains page centered text 2. Select a range of text spanning multiple block elements within the added snippet 3. Add an animation to the selected text. <img width="800" height="200" alt="image" src="https://github.com/user-attachments/assets/45074927-4d60-4517-b65b-7ba6d667e060" /> ### Solution: This PR splits the selection by block and creates one inline animation wrapper per block instead. The builder applies animation options to the resulting elements as one group, without changing the document's block structure. task-5155887 Forward-Port-Of: odoo/odoo#283176
This update brings the spreadsheet component to the latest 19.3 version and fixes several issues affecting charts, dashboards, search and replace, printing, copy/paste, and XLSX export. Users should see more reliable spreadsheet dashboards, cleaner chart behavior, and improved performance when calculations involve large ranges.
Original PR description
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/e488e8d941 [REL] 19.3.20 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0)…
### Contains the following commits: https://github.com/odoo/o-spreadsheet/commit/e488e8d941 [REL] 19.3.20 [Task: 0](https://www.odoo.com/odoo/2328/tasks/0) https://github.com/odoo/o-spreadsheet/commit/7815f57ec0 [FIX] search and replace: manage invalid range [Task: 6483222](https://www.odoo.com/odoo/2328/tasks/6483222) https://github.com/odoo/o-spreadsheet/commit/c1f9cf5cec [FIX] xlsx: fix geo chart xlsx export [Task: 4632983](https://www.odoo.com/odoo/2328/tasks/4632983) https://github.com/odoo/o-spreadsheet/commit/f671a95d65 [FIX] calendar chart: wrong groupBy choice filtering [Task: 5358625](https://www.odoo.com/odoo/2328/tasks/5358625) https://github.com/odoo/o-spreadsheet/commit/c462d37100 [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/a089874e22 [FIX] dashboard: fix background color [Task: 6497278](https://www.odoo.com/odoo/2328/tasks/6497278) https://github.com/odoo/o-spreadsheet/commit/0990f3a96a [FIX] charts: some charts cannot be aggregated [Task: 6501177](https://www.odoo.com/odoo/2328/tasks/6501177) https://github.com/odoo/o-spreadsheet/commit/59c2807dee [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/7ef89113ba [FIX] ComposerHighlight: Highlight the correct sheet with `#` [Task: 6527596](https://www.odoo.com/odoo/2328/tasks/6527596) https://github.com/odoo/o-spreadsheet/commit/7f49eac974 [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/c3b4cde5f7 [FIX] print: handle hidden headers [Task: 6332587](https://www.odoo.com/odoo/2328/tasks/6332587) https://github.com/odoo/o-spreadsheet/commit/53045ccda0 [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>
The delivery method wizard now recalculates shipping costs when the total package weight is edited. This prevents orders from receiving an incorrect zero shipping fee when using weight-based delivery pricing.
Original PR description
Steps to reproduce ------------------ - Install `website_sale_stock` and `sale_management` module. - Create a product. - Create a rule-based delivery method with: - In Pricing tab click add a line -…
Steps to reproduce ------------------ - Install `website_sale_stock` and `sale_management` module. - Create a product. - Create a rule-based delivery method with: - In Pricing tab click add a line - condition: `Quantity >= 0` - variable factor: `weight` - price per unit: 2 - Create a quotation containing the product. - Open the delivery method wizard by clicking into `Add Shipping` - Select the rule-based delivery method. - Change the total weight from 0 to 10. - Add the delivery method. Issue ----- - Changing the total weight does not recompute the delivery price. - The delivery line is added with a price of 0 instead of 20. Cause ----- The delivery wizard defines `_onchange_carrier_id` for both `carrier_id` and `total_weight` https://github.com/odoo/odoo/blob/9860ba5a593c5e7723494674b62890d16ae1a9a8/addons/delivery/wizard/choose_delivery_carrier.py#L41-L50 The call flow is expected to be: `total_weight` change -> `_onchange_carrier_id` -> `_get_delivery_rate` -> `rate_shipment` `_get_delivery_rate` passes the manually entered weight through the `order_weight` context key: https://github.com/odoo/odoo/blob/9860ba5a593c5e7723494674b62890d16ae1a9a8/addons/delivery/wizard/choose_delivery_carrier.py#L87-L90 The rule-based carrier gives this context value priority over the saved order weight and the weight computed from order lines: https://github.com/odoo/odoo/blob/9860ba5a593c5e7723494674b62890d16ae1a9a8/addons/delivery/models/delivery_carrier.py#L588-L594 For example, with `total_weight = 10` and a price factor of 2, the expected calculation is: `delivery_price = 0 + 2 * 10 = 20` However, the pickup-location implementation added an override in `website_sale_stock` that declared only `carrier_id` as an onchange trigger: https://github.com/odoo/odoo/blob/0da3259034ff3b2b79f417df53969fe017207195/addons/website_sale_stock/wizard/choose_delivery_carrier.py#L13-L16 Since the override uses the same method name, its decorator replaces the base onchange specification. Therefore, changing `total_weight` does not call the method and the initial `delivery_price = 0` is kept. This does not occur in saas-19.2. because `website_sale_stock` does not override the delivery wizard there. The base method remains registered for both `carrier_id` and `total_weight`: The conflicting override was introduced later in this [commit](https://github.com/odoo/odoo/commit/0da3259034ff3b2b79f417df53969fe017207195) from saas-19.3 Fix --- Add `total_weight` to the `website_sale_stock` onchange decorator so the inherited delivery-rate computation runs when the user edits the weight. --- opw-6516301 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
The Mexican point-of-sale self-invoicing test was updated to match the new flow where public users can no longer edit existing customer details. This keeps automated checks aligned with the safer process of creating a new customer record when needed, reducing risk around customer data handling.
Original PR description
Before the related pr commit: - Public users could update customer data during the self-invoicing flow. - The test_qr_code_receipt_mx test relied on this behavior when updating customer data. After the ref commit: - Public users can no longer update customer data during self-invoicing. - Update test_qr_code_receipt_mx to create a new partner with the required customer data when the order is not linked to a customer. Related PR: odoo/odoo#283470 Task-6272660 Forward-Port-Of: odoo/enterprise#130700 Forward-Port-Of: odoo/enterprise#129948
Creating multiple helpdesk teams with website forms no longer creates duplicate Help menu entries on the website. Existing menus are reused across teams and only removed when no team still needs them, keeping the website navigation clean and consistent.
Original PR description
Currently on creating new helpdesk team everytime a new website menu is created. ### **Steps to Reproduce:** 1) Install website_helpdesk 2) Navigate to `Helpdesk>Configuration>Helpdesk Team`. 3)…
Currently on creating new helpdesk team everytime a new website menu is created. ### **Steps to Reproduce:** 1) Install website_helpdesk 2) Navigate to `Helpdesk>Configuration>Helpdesk Team`. 3) Create 2 helpdesk team with `Website Form` option enable. 4) Navigate to Website. ### **Observed Behavior:** Two Help menus are created. ### **Expected Behavior:** Multiple menus should not be created. ### **Root Cause:** The menu creation logic relies on the following [condition](https://github.com/odoo/enterprise/blob/b66097122ba3a758734ac6fb2b26579c35cb72c2/website_helpdesk/models/helpdesk.py#L111-L112) `team_count_by_website` is built from `_read_group(..., ['website_id'], ...)`, which keys its result by the `website_id` *recordset*, not its id. Looking it up with `team_count_by_website.get(website.id, 0)` therefore always misses and falls back to `0`, so `team_count <= 1` is always `True` regardless of how many teams already exist for that website. The only thing left guarding menu creation is `any(team.website_menu_id for team in teams)`, which only looks at the teams in the current create/write call, not every team on that website. So saving a second team in a separate call always creates another menu. ### Fix: Make the website menu a resource shared by every team with the website form enabled on a given website, instead of "owned" by whichever team created it: - Before creating a new menu, look up other teams (active or archived) that already point to a menu, matched through the `website_menu_id` relation between teams rather than a hardcoded `/helpdesk` URL, so a customized menu URL doesn't cause a duplicate to be created. Reuse that menu when found. - Only delete a menu once no team (active or archived) still references it, checked before removing a team's own reference. **opw-6303846** Forward-Port-Of: odoo/enterprise#130207 Forward-Port-Of: odoo/enterprise#121120
The missing e-invoice check now opens a list limited to the invoices that actually need attention. This prevents users from seeing unrelated or overly broad invoice lists, reducing confusion during Indian e-invoicing compliance checks.
Original PR description
The `missing_einvoice` check passed custom `views` but no explicit `domain`, so its action ignored the `moves` recordset and instead opened all matching `account.move` records — an unfiltered list when no invoices were missing, and every eligible invoice (not just the flagged ones) otherwise.
Guard the action to only build when `moves` is non-empty, and pass `domain=[('id', 'in', moves.ids)]` so it's always scoped to the invoices actually found.
task-6544790
Forward-Port-Of: odoo/enterprise#130483The web code editor now correctly allows protected self-closing template tags to be changed or removed when appropriate. This prevents unnecessary editing blocks and makes template maintenance smoother for users working with web views.
Original PR description
Currently, when we get readonly attributes to prevent overwriting or deletion, we only ignore them if the selection that is being modified is contained between `<>` tags. To rectify this behavior, we also include the `<\>` self closing tags so that they may also be overwritten/deleted. opw-6325841 Forward-Port-Of: odoo/odoo#287393
Fixed an issue where invoice forms using document extraction could crash when users clicked into certain code-style fields. The change makes field detection more precise, helping keep invoice editing stable in developer or localized configurations.
Original PR description
Follow-up of odoo/odoo#287085, as suggested by @aab-odoo: on a stable branch the fix belongs here rather than in the ace templates. Fixes odoo/odoo#286871. opw-6543997 ### The problem The `focusin`…
Follow-up of odoo/odoo#287085, as suggested by @aab-odoo: on a stable branch
the fix belongs here rather than in the ace templates.
Fixes odoo/odoo#286871.
opw-6543997
### The problem
The `focusin` listener of the extract mixin walks up from the focused node with
`closest(".o_field_widget,.o_field_cell")` and assumes whatever it finds
identifies a field. Not every `.o_field_widget` node does: a widget template
may render its own `.o_field_widget` inside the one the `Field` wrapper already
provides, and that inner div has no `name`. `web.AceField` (`widget="code"`)
does exactly that:
```html
<div name="my_field" class="o_field_widget o_field_code ...">
<div class="o_field_widget oe_form_field o_ace_view_editor oe_ace_open">
<div class="ace_editor">... <textarea class="ace_text-input">
```
The listener stops on the inner div, `getFullFieldName` finds no name and
builds `"<parent_field>.null"`. Since the name now contains a dot, `getBoxType`
takes the x2many branch:
```js
modelFieldType = this.props.record.data[parentField]?._config.fields[fieldName]?.type;
```
On a code field `record.data[parentField]` is a plain string. It is truthy, so
the optional chaining does not short-circuit, but it has no `_config` →
`undefined.fields` → `TypeError: Cannot read properties of undefined (reading
'fields')`, and the whole form breaks.
`account_invoice_extract` installs the renderer for `account_move_form` with
`force: true`, so this runs on every invoice form.
### Steps to reproduce
1. Install `account_invoice_extract`.
2. Put a `widget="code"` field on the `account.move` form view (with
`l10n_ar_edi` installed there are already two on the ARCA tab).
3. Give the field a value — with an empty value the error does not happen,
`record.data[parentField]` is `false` and the optional chaining cuts.
4. Enable developer mode and click inside the code editor.
Reproduced on a plain 19.0 runbot build.
### The fix
Restrict the selector to `.o_field_widget[name]`, so nodes that cannot identify
a field are skipped. The `focusout` listener a few lines below also matches
`.o_field_widget`, but it only calls `onBlurFieldWidget()` and never resolves a
name, so it is left as is.
The duplicated class in the ace templates looks like the actual root cause and
is handled separately in odoo/odoo#287085, retargeted to `master`.
Forward-Port-Of: odoo/enterprise#130952The Timesheet Assistant no longer crashes when generating sample data for projects linked to partners who do not have an email address. This makes the sample data generation more reliable for users working with incomplete partner records.
Original PR description
Forward-Port-Of: odoo/enterprise#131002
This fixes a problem that allowed a company’s main Projects folder in Documents to be archived even though it should be protected. The change helps prevent accidental disruption to project document organization and access.
Original PR description
The company's project folder is supposed to be [impossible](https://github.com/odoo/enterprise/blob/20cc61e69aa3f6a59de1e962b25ce11fa402bf22/documents_project/models/documents_document.py#L44) to archive, by using the `_unlink_except_company_folders` logic. However, the method that adds the company field to the list of fields to check was missing, so it was still possible to archive a folder set as projects folder. Forward-Port-Of: odoo/enterprise#120605
Turkish e-Ledger exports now fill in line numbers automatically and keep numbering continuous across monthly filings within the same fiscal period. Entries are exported from oldest to newest so numbering matches official expectations and reduces manual correction work.
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
Sold-out event tickets or time slots are now correctly treated as unavailable instead of allowing the normal maximum order quantity. This prevents future booking flows or integrations from mistakenly offering places that are already fully booked.
Original PR description
### Steps to reproduce: event = env['event.event'].create({ 'name': 'Repro Event', 'date_begin': '2026-09-01 08:00:00', 'date_end': '2026-09-01 18:00:00', }) ticket =…
### Steps to reproduce:
event = env['event.event'].create({
'name': 'Repro Event',
'date_begin': '2026-09-01 08:00:00',
'date_end': '2026-09-01 18:00:00',
})
ticket = env['event.event.ticket'].create({
'event_id': event.id,
'name': 'VIP',
'seats_limited': True,
'seats_max': 1,
})
env['event.registration'].create({
'event_id': event.id,
'event_ticket_id': ticket.id,
'name': 'Attendee 1',
'state': 'open',
})
ticket.seats_available -> 0
ticket.is_sold_out -> True
result = ticket._get_current_limit_per_order(event=event) print(result) # {ticket.id: 30} -- expected {ticket.id: 0}
### Issue and Expected
`_get_current_limit_per_order()` used `if not seats_available:` to detect the "no limit" case returned by `_get_seats_availability()`. That check is truthy for both `None` (genuinely no limit) and the integer `0` (fully booked), so a sold-out ticket/slot combination was incorrectly treated as unlimited and returned `limit_max_per_order or EVENT_MAX_TICKETS` (e.g. 30) instead of `0`.
### Fix
`_get_seats_availability()` explicitly documents `None` as the "no limit" sentinel, with `0` meaning "constrained, zero seats left". Use `seats_available is None` to preserve that distinction instead of a falsy check.
No functional regression was found in the current website_event flow: sold-out slots/tickets are filtered or re-derived independently before reaching this value in every existing UI path. This fixes the underlying contract of the method itself, so future or additional callers don't inherit the wrong value.
https://github.com/odoo/odoo/issues/284098
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#284100Accounting now checks for duplicate receipts in the same way it already checks duplicate bills and invoices. This helps prevent duplicate supplier or customer documents from being recorded when users switch between receipt and invoice or bill types.
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
German Intrastat exports now correctly use region code 99 for dispatches of goods whose origin is outside Germany. The change also improves export reliability in German locale by handling decimal formatting, stable dispatch/arrival detection, and legally required weight rounding.
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
Argentine electronic invoices can now be printed even when a foreign customer uses an identification type without an ARCA code. This prevents a crash in QR code generation and keeps export invoicing documents accessible after posting.
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