Friday, September 11, 2026
15 changes · saas-19.2
Resolved issues and error corrections
This fixes SEPA Credit Transfer exports so they no longer include an address field that some European banks reject. Businesses using Austrian, German, and other strict SEPA banks should be able to validate and submit batch payment XML files successfully again.
Original PR description
### Issue before this commit: Generating a SEPA Credit Transfer XML for certain European banks (e.g., Austrian and German banks) fails because the generated XML contains an unexpected `<CtrySubDvsn>`…
### Issue before this commit: Generating a SEPA Credit Transfer XML for certain European banks (e.g., Austrian and German banks) fails because the generated XML contains an unexpected `<CtrySubDvsn>` tag inside the `<PstlAdr>` node, leading to the rejection of the batch payment file. ### Steps to reproduce the issue: 1. Download Accounting and l10n_at 2. Go to settings and activate SEPA Credit Transfer / ISO20022 3. Go to Journals > Bank > set an account number 4. Create an austrian contact (ex. FK Austria Wien AG) and set in the invoicing tab a bank (ex. AT526000071856851733) and set it trusted 5. Go to Bills, create a new one with the contact created and confirm it 6. Then click 'PAY' and select SEPA Credit Transfer 7. Go to Vendors > Batch Payments 8. Create a new one with: 1. Bank as bank 2. SEPA Credit Transfer as Payment Method 3. Add the bill just created 9. Validate and download the XML 10. See that a wrong tag <CtrySubDvsn> is added. This makes some banks refuse it ### Cause of the issue: External commit 30b023d394a5e4de64873188aa1d5961fecccb10 introduced the `<CtrySubDvsn>` tag to support US and CA requirements. However, the change was incorrectly applied to the common `account_iso20022` file, making it leak into standard European SEPA exports where the tag is not compliant with certain strict banking validation rules. ### Reason to introduce the fix: Revert the generic addition of the `<CtrySubDvsn>` tag in the common ISO20022 XML generation and restrict it only to the specific localizations (US/CA) that require it. This brings the `<PstlAdr>` node back to compliance, allowing Austrian, German, and other European banks to successfully process the files. opw-6523035 Forward-Port-Of: odoo/enterprise#131156 Forward-Port-Of: odoo/enterprise#131090
Fixes an issue that caused the Peruvian Inventory and Balance General Ledger report export to fail. Users can now generate the report reliably while preserving the file format required by SUNAT.
Original PR description
### Description of the issue/feature this PR addresses: This PR fixes a server crash in the Peruvian localization (l10n_pe_reports_lib) that occurs when generating the "Inventory and Balance" General…
### Description of the issue/feature this PR addresses: This PR fixes a server crash in the Peruvian localization (l10n_pe_reports_lib) that occurs when generating the "Inventory and Balance" General Ledger report. The crash is triggered by strict validation rules within Python's csv module, which rejects the custom line terminator used to fulfill the SUNAT PLE formatting requirements. ### Current behavior before PR: When a user attempts to generate and export the "Inventory and Balance" report, the server crashes with a ValueError: bad delimiter or lineterminator value. This happens because the csv.DictWriter is initialized with lineterminator='|\n' to ensure every row ends with a pipe. Python's underlying csv implementation rejects this, as it expects standard line endings (\r, \n, or \r\n) and throws an error if the delimiter character (|) is included in the terminator string. ### Desired behavior after PR is merged: The "Inventory and Balance" report generates successfully without server errors. The code now uses the standard lineterminator='\n' to satisfy Python's validation rules. To maintain the mandatory trailing pipe (|) at the end of each row required by SUNAT, a dummy empty column (['']) is appended to the field names with restval=''. This prompts the writer to naturally append the final pipe as a column delimiter before the newline, resulting in the exact |\n output format required, safely and reliably. opw-6509674 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/enterprise#129636
Odoo now handles empty or incomplete image attachments without crashing the media picker or chatter. This helps users continue working even when an upload is interrupted or a malformed email creates an invalid attachment.
Original PR description
Problem: Interrupted uploads or malformed email payloads can create 0-byte binary `ir.attachment` records where the `checksum` is `False`. Accessing the computed `image_src` field on these records…
Problem:
Interrupted uploads or malformed email payloads can create 0-byte binary `ir.attachment` records where the `checksum` is `False`. Accessing the computed `image_src` field on these records triggers a `TypeError: 'bool' object is not subscriptable` when attempting to slice `attachment.checksum[:8]`. This crashes the Media Dialog and Chatter.
Purpose:
Add a fallback boolean guard to `attachment.checksum` inside `_compute_image_src` so that empty attachments evaluate safely to a string ('0') instead of raising a traceback, allowing the UI to render gracefully.
Steps to Reproduce on Runbot:
1. Go to Settings > Technical > Database Structure > Attachments.
2. Create a new record: Name: `test.png`, Type: `File` (Binary), File Content: [leave empty], Is public document: Checked.
3. Open any record with a Chatter (e.g., Contact or CRM Lead) and click "Insert Image" to open the Media Dialog.
4. The system attempts to evaluate `image_src` and throws the `TypeError`.
Notes:
A test (`test_compute_image_src_empty_checksum`) was added to `test_ir_attachment.py`
opw-6482674
Forward-Port-Of: odoo/odoo#287639
Forward-Port-Of: odoo/odoo#287444Time off measured in hours is now calculated using only actual working periods from the employee schedule. This prevents non-working periods, such as a Friday afternoon marked unavailable, from being incorrectly counted and helps keep leave balances accurate.
Original PR description
When computing the amount of hours used by a time off, if a time off entry was set in the working schedule it would not be taken into account and the amount of hours used would be wrong. Steps to reproduce: ------------------- * Open any working schedule WS and make sure that the friday has morning set as working time and afternoon as non-working time * Make sure the time off type is configured to use hours and not days * Create a time off of 2 days that includes a friday > Observation: The amount of hours used is 16 hours instead of 12 hours Why the fix: ------------ When now filter out the non working period when computing the work intervals. opw-6469587 Forward-Port-Of: odoo/odoo#287505 Forward-Port-Of: odoo/odoo#285955
The WhatsApp Disconnect button is now only shown to administrators for accounts set up through onboarding. This prevents manual WhatsApp accounts from being accidentally disconnected in Meta while still appearing configured in Odoo, and avoids access errors for regular internal users opening the account form.
Original PR description
`show_disconnect` was true for any account holding a token, so the Disconnect button showed on manually configured accounts as well. Pressing it unsubscribes the webhook on Meta first, then clears the token, and that write is rejected on a manual account since the credentials are required there. Odoo rolls back, Meta does not, so the account keeps looking configured while incoming messages stop. The compute also read `token`, restricted to WhatsApp administrators, while every internal user has read access on `whatsapp.account`, so opening the account form as a regular user raised an AccessError. The button is now shown only for administrators on onboarded accounts, and `button_disconnect` checks write access, since hiding a button does not stop an RPC call and the request to Meta happens before the write that would have been refused. Task-6544628 Forward-Port-Of: odoo/enterprise#130759
Validated time off that is shortened, such as when an employee leaves mid-leave, now keeps the employee calendar correctly updated. This prevents payroll from incorrectly counting approved time off days as worked attendance.
Original PR description
Problem ------- Fixes bug caused by PR odoo#249527. When a validated time off's dates are shortened while it stays validated (e.g. when the employee's departure date falls int he middle of the…
Problem ------- Fixes bug caused by PR odoo#249527. When a validated time off's dates are shortened while it stays validated (e.g. when the employee's departure date falls int he middle of the leave), the linked resource.calendar.leaves record was unconditionally unlinked. To reproduce: 1. Create and validate a time off request covering a whole month. 2. Register the employee's departure with a departure date in the middle of that time off. 3. Generate the employee's last payslip. The leave is correctly cut at the departure date, but since it never leaves the `validate` state, it never goes through `_validate_leave_request()` again, so its resource.calendar.leaves record is never recreated. The days that were covered by the deleted entry are no longer blocked in the employee's resource calendar, so the payslip's worked day lines (computed from resource.calendar.leaves) count them as attendance instead of time off. Cause ----- `hr.leave.write()` removed the resource.calendar.leaves record any time either the state changed away from `validate` or the leave's dates changed, regardless of whether the leave remained validated. Date-only changes on an already-validated leave never re-trigger validation, so the entry was not recreated. Solution -------- Only remove the resource.calendar.leaves record when the leave actually loses its validated state. When a validated leave's dates change but it stays validated, amend the existing resource.calendar.leaves record in place instead, falling back to creating one if none exists. Related PR: odoo#249527 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287624 Forward-Port-Of: odoo/odoo#286465
Fixed an issue that could prevent German point-of-sale sessions from closing when an order used a customer address contact without its own name. The system now uses the displayed contact name, or a safe fallback if needed, so required compliance export data is still produced.
Original PR description
Steps to reproduce: - German PoS with Fiskaly TSS configured - Set a customer that is an address contact (type invoice/delivery/other) on an order; such a contact has no name of its own, it is…
Steps to reproduce: - German PoS with Fiskaly TSS configured - Set a customer that is an address contact (type invoice/delivery/other) on an order; such a contact has no name of its own, it is displayed with its parent's - Close the session Issue: Closing crashes with "TypeError: 'bool' object is not subscriptable" and the session cannot be closed at all. Cause: _get_dsfinvk_cash_point_closing_data builds the DSFinV-K buyer block from partner.name, which is only guarded by "if partner := o.partner_id". res.partner.name is not required: the res_partner_check_name constraint only enforces it for type='contact', so an address contact stores NULL there and partner.name reads as False, which cannot be sliced. Fix: Fall back on complete_name - what the UI displays for those contacts, "Parent Company, Delivery" - and on a literal when even that is empty, since the buyer name is mandatory in the export. Reading name first leaves the exported value untouched for every partner that has one. opw-6518336 Forward-Port-Of: odoo/enterprise#129709
French PDP Flow 10 responses from the public platform are now processed instead of only being stored. Rejected reports show the rejection reason, move to the correct status, and can be corrected and resent with a new transmission reference.
Original PR description
Flow 10 PPF responses were stored as attachments without being processed. Consequently, rejected reports remained marked as sent, the rejection reason was not shown to users, and corrected reports could not be submitted again. Process the PPF response codes, update the flow state, and log the returned details in the chatter. Keep response attachments separate from the outgoing payload and allow rejected reports to be manually resent with their original moves and a new transmission identifier. no task id Forward-Port-Of: odoo/odoo#287338
Fixes an issue where the HTML editor could keep outdated content after detecting a stale document. The editor now uses the latest content saved on the server, reducing the risk of users seeing or continuing to edit obsolete information.
Original PR description
Before this commit, an editor recovering from a stale document could stop on this error and keep the stale content:
```
Error: Concurency detected while recovering from a stale document. The
last history id of the server is different from the history id received
by the html_field_write event.
at CollaborationOdooPlugin.resetFromServerAndResyncWithPeers
```
This happens because the recovery compares the history id of the record with the one the html_field_write event carried. A write keeps only the last step id in the field, so an event handled after a later write names an id the record no longer holds. As a result, the recovery stops there and the document stays stale.
This commit fixes the issue by taking the history id read from the record as the new server reference, so the editor converges on the document the server holds.
https://runbot.odoo.com/odoo/error/944595
Forward-Port-Of: odoo/odoo#287408Files sent through Odoo Live Chat can now be downloaded reliably when the chat widget is embedded on an external website. This prevents customers or visitors from hitting browser download errors caused by cross-site restrictions, improving the support experience.
Original PR description
**Steps to reproduce:** - Install `im_livechat` module - Copy the code from the livechat channel's widget tab - Paste it in an external website `<head>` (e.g., local python webserver on `0.0.0.0`) -…
**Steps to reproduce:**
- Install `im_livechat` module
- Copy the code from the livechat channel's widget tab
- Paste it in an external website `<head>` (e.g., local python webserver on `0.0.0.0`)
- Start a conversation and send a file from odoo
- Try to download it from the external website
- POST request is sent for the download
- Download fails: `CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.`
- Also when the file is a PDF the preview won't open: `404 (File not found)`
**Issue:**
Mix of multiple issues:
- The download is triggered using a POST request instead of a GET request
(see similar issue for the file viewer [1])
- The file route does not provide the required CORS headers, so requests
originating from the external website are blocked by the browser
- PDF files are rooted to the related path of the `pdfjs` fileviewer
(e.g. `http://0.0.0.0:8000/web/static/lib/pdfjs/web/viewer.html?file=...`)
```xml
<!--
Template rendering all the scripts required to execute the Livechat from an external page (which not contain Odoo)
-->
<template id="external_loader" name="Livechat : external_script field of livechat channel">
<!-- the loader -->
<script defer="defer" t-attf-src="{{url}}/im_livechat/loader/{{channel_id}}" type="text/javascript"/>
<!-- js of all the required lib (internal and external) -->
<script defer="defer" t-attf-src="{{url}}/im_livechat/assets_embed.js" type="text/javascript" />
</template>
```
**Fix:**
Make the `downloadFile` helper handle cross-origin URLs by falling back to a native `<a download>` click (like before) when the target route has the same origin as the embedded script. Could use `session.origin` or `new URL(document.currentScript.src).origin` for this. This is done to avoid allowing CORS on the file content route.
The file viewer issue is handled separately by [1].
For the PDF issue we could manually add the origin to the full url everywhere (but we get some `SecurityError` error from the library due to the cross-origin iframe), add the libjs library in the `assets_embed` (not sure it's possible in `im_livechat.assets_embed_external`), or block external PDF preview for now.
[1] https://github.com/odoo/odoo/pull/281330
opw-6444167
Forward-Port-Of: odoo/odoo#287535
Forward-Port-Of: odoo/odoo#286180Changing the delivery address on an outgoing rental transfer no longer redirects the destination away from the rental location. This helps rental operations keep stock movements accurate and prevents confusion or manual corrections when customer delivery details change.
Original PR description
**Issue** Changing the `partner_id` of an outgoing rental transfer could reset its destination location to the partner customer location instead of the rental location. **Steps to reproduce** -…
**Issue** Changing the `partner_id` of an outgoing rental transfer could reset its destination location to the partner customer location instead of the rental location. **Steps to reproduce** - Enable Rental Transfers from Rental Configuration. - Create a Sales Order for a rental product. - Open the related delivery transfer. - Using Studio, make the Destination Location field visible. -> Current destination location is: Partner/Customer/Rental - Change the delivery address -> The destination location become: Partner/Customer **Cause** Changing the delivery address (i.e: the partner_id) triggers `_compute_location_id`: https://github.com/odoo/odoo/blob/85372b625a80ab50fe2d8a6bce49a890b2bf1665/addons/stock/models/stock_picking.py#L949-L950 Since commit https://github.com/odoo/odoo/commit/8c90fc1fd336ee872fd67d8d72473e4f6c55b2e0, not only draft picking are recomputed. As a result, `location_dest_id` is set as `picking.partner_id.property_stock_customer` https://github.com/odoo/odoo/blob/85372b625a80ab50fe2d8a6bce49a890b2bf1665/addons/stock/models/stock_picking.py#L959-L961 https://github.com/odoo/odoo/blob/85372b625a80ab50fe2d8a6bce49a890b2bf1665/addons/stock/models/stock_picking.py#L963 Regardless whether we are in rental setup opw-6237495 Forward-Port-Of: odoo/enterprise#126158 Forward-Port-Of: odoo/enterprise#123564
Fixed an issue where creating or updating a time off request could incorrectly save a one-day request as zero days when an automation rule created an activity. This ensures automated activity notifications no longer interfere with time off duration calculations, improving reliability for HR workflows.
Original PR description
Problem: A time off request is saved with a duration of 0 days instead of 1 day as soon as an automation rule on Time Off has an action that creates an activity. Cause: `_compute_field_value` runs…
Problem: A time off request is saved with a duration of 0 days instead of 1 day as soon as an automation rule on Time Off has an action that creates an activity. Cause: `_compute_field_value` runs the actions while the fields of the computation it wraps are still protected and not written yet. On `hr.leave` the actions run from a computation nested in `_compute_date_from_to`, so `date_from` is empty when the activity notification reads `display_name`, and `_compute_duration` stores 0. Setting `date_from` afterwards does not mark `number_of_days` to compute again since it is protected. Solution: Extract the snapshot `_filter_pre` already does into `_keep_to_compute` and wrap the post filter and the actions with it, so the fields depending on the running computation stay to compute. Same treatment as 488419a5ca49 (odoo/odoo#243611) on the pre filter. Steps to reproduce: - Enable the developer mode. - Go to Settings > Technical > Automation Rules and create a rule on the Time Off model with the trigger On create and edit. - Add an action of type Create Activity, set its Responsible to another user, and save. - Go to Time Off > New, pick an employee and a time off type, and request one working day. - Observe that the request shows a duration of 0 days. Ticket [link](https://www.odoo.com/odoo/project.task/6498783) opw-6498783 Forward-Port-Of: odoo/odoo#287536 Forward-Port-Of: odoo/odoo#286791
Refund and credit note lines without a product now suggest the correct type of account based on whether the document is for a sale or a purchase. This prevents customer credit notes from using expense accounts and vendor credit notes from using income accounts, reducing accounting errors for contacts used as both customers and vendors.
Original PR description
Before this commit, when adding a line without a product to an invoice or credit note, `_get_most_frequent_account_for_partner` picked the partner's most-used account, filtered to an income or…
Before this commit, when adding a line without a product to an invoice or credit note, `_get_most_frequent_account_for_partner` picked the partner's most-used account, filtered to an income or expense account depending on `get_inbound_types` and `get_outbound_types`. Those helpers classify move types by cash-flow direction which is correct for choosing a receivable and payable account but wrong for choosing an income ro expense account: they group `in_refund` with `out_invoice` as "inbound", and `out_refund` with `in_invoice` as "outbound". As a result, a Vendor Credit Note line with no product would be filtered to income accounts instead of expense accounts, and a Customer Credit Note line to expense accounts instead of income accounts. This only surfaced for contacts who are both customer and vendor, since the query needs matching history to return a result; otherwise it silently falls back to the journal's default account, masking the bug for ordinary contacts. This commit uses `get_sale_types` and `get_purchase_types` instead, which classify by document side, sale vs. purchase rather than cash-flow direction, matching the classification already used for product-based lines `is_sale_document` and `is_purchase_document` opw-6373124 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#278794 Forward-Port-Of: odoo/odoo#276846
The Point of Sale product configurator now hides extra-price labels when a fixed pricelist means those extras will not actually be charged. This prevents staff from seeing misleading add-on prices and helps keep checkout pricing clear and consistent for customers.
Original PR description
Steps to reproduce: - Create a pricelist "TAKEAWAY" with a fixed price of 10.00 on a product whose sales price is 20.00 - Create a preset "TAKEOUT" and set its pricelist to "TAKEAWAY" - Add to that…
Steps to reproduce: - Create a pricelist "TAKEAWAY" with a fixed price of 10.00 on a product whose sales price is 20.00 - Create a preset "TAKEOUT" and set its pricelist to "TAKEAWAY" - Add to that product an attribute with variant creation "Never", with a value carrying an extra price of 1.00 - In the POS, switch to the preset "TAKEOUT" and click the product Issue: The configurator advertises the attribute value with a "+ $ 1.00" badge, but that extra is charged nowhere: the title of the popup and the resulting order line both stay at the 10.00 of the pricelist. Cause: A fixed pricelist rule replaces the whole price of the product, the attribute extra prices included: _compute_price on product.pricelist.item returns fixed_price and never reaches _compute_base_price, the only place where _get_attributes_extra_price is taken into account. getPrice is a faithful port of that and overwrites `basePrice + price_extra` with rule.fixed_price. The configurator, however, rendered its badges out of value.price_extra alone, without ever asking what the pricelist of the order does with it. Fix: Only advertise an extra price when the price of the product actually reflects it, the way website_sale already does with the show_extra_price of _get_additionnal_combination_info. Asking getPrice covers more than a fixed rule: a rule based on another pricelist recurses with no extra either, and a full discount leaves nothing of it. Combo items keep their badges, since computeComboItems adds their extras on top of the combo price, like _get_combo_item_display_price does. opw-6528187 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#286475
Point of Sale cash closing messages now include cash in and cash out movements even when the closing user does not have accounting access. This prevents incorrect closing differences from being recorded, helping store managers trust register closing reports.
Original PR description
Steps to reproduce: - Give a user PoS Administrator rights and no Accounting rights. - As that user, open a session with cash control, sell something paid in cash and do a Cash Out. - Close the…
Steps to reproduce: - Give a user PoS Administrator rights and no Accounting rights. - As that user, open a session with cash control, sell something paid in cash and do a Cash Out. - Close the register. The closing popup shows the expected cash (opening + cash payments - cash out); enter exactly that amount, the popup shows no difference. - Open the closed session: its chatter says "Closing difference: -X", X being the cash out amount, and "Closing expected" is the amount before the cash out. Issue: The closing control data agrees with the user, but the closing message records a cash difference equal to the cash out. Cause: `_compute_cash_balance` sums `statement_line_ids` with the rights of the current user. Cash moves are `account.bank.statement.line` records, which `_inherits` `account.move`, so the account.move record rules apply to them too. For a PoS user the only such rule is `rule_invoice_pos_user` (`pos_order_ids != False`), and a cash move has no PoS order: unless the user is in `account.group_account_invoice`, their own cash moves are filtered out and `cash_register_balance_end` ignores them, while `get_closing_control_data` sums the lines in sudo. Since da8be203ec32 PoS managers can create cash moves without any accounting group, which made this visible: in 18.0 cash in/out required `account.group_account_invoice`, whose `account_move_see_all` rule makes every move readable. The values stored at closing are right by accident: `_validate_session` reads the lines in sudo just before, and the one2many cache is shared between the sudo and non-sudo environments of the same transaction. The message posted by `update_closing_control_state_session` runs in its own transaction and gets the filtered sum. Fix: Read the cash lines in sudo in `_compute_cash_balance`, as `get_closing_control_data`, `get_cash_in_out_list` and `_validate_session` already do. Users with accounting rights see all lines already, so nothing changes for them. opw-6521591 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#286308