Friday, September 11, 2026
14 changes · saas-19.1
Resolved issues and error corrections
Fixes an issue where saving an employee attendance could fail when overlapping Planning slots had different allocation percentages. This makes attendance and overtime recalculations more reliable while still allowing Planning conflicts to be handled separately.
Original PR description
### Analysis Planning schedule computation may merge intervals coming from two different sources. Fully allocated Planning slots reuse intervals from the employee working schedule, whose payload is a…
### Analysis
Planning schedule computation may merge intervals coming from two different sources.
Fully allocated Planning slots reuse intervals from the employee working schedule, whose payload is a `resource.calendar.attendance` recordset. Partial allocations, however, create synthetic intervals using a `resource.calendar` recordset.
When such Planning slots overlap, these intervals are merged into the same `Intervals` instance. This can raise a `TypeError` while sorting or merging the interval payloads, since recordsets from different models cannot be combined.
This can surface during attendance creation or deletion when overtime recomputation requests the employee Planning schedule.
### Steps to reproduce:
1. Install the following applications/modules:
- Attendances
- Planning
- Payroll / Work Entries
- `hr_work_entry_planning_attendance`
2. Create an employee with:
- Work Entry Source: Planning
- A flexible working schedule
- An employee version covering the test date
3. Create two published Planning slots for the same employee on the same day
and with overlapping times:
- Slot 1: 08:00–17:00, Allocated Percentage = 100%
- Slot 2: 08:00–17:00, Allocated Percentage = 99%
4. Create a completed attendance for the same employee on that day, for
example:
- Check In: 08:00
- Check Out: 17:00
5. Save the attendance.
Current behavior:
Attendance creation crashes while recomputing overtime/schedules with a
TypeError caused by mixing `resource.calendar` and
`resource.calendar.attendance` recordsets inside an `Intervals` instance.
Depending on the exact interval boundaries, the error can be:
```
TypeError: '<' not supported between instances of
'resource.calendar.attendance' and 'resource.calendar'
```
or:
```
TypeError: inconsistent models in:
resource.calendar() | resource.calendar.attendance(...)
```
Expected behavior:
The attendance should be created successfully. Conflicting Planning slots
may still be reported as a Planning conflict, but attendance schedule
recomputation must not crash with an internal TypeError.
### Fix
This commit creates the partial-allocation intervals with an empty `resource.calendar.attendance` recordset instead. This keeps the interval payload model consistent with the working schedule intervals.
opw-6511903
Forward-Port-Of: odoo/enterprise#130820This fixes a crash when exporting the Peruvian Inventory and Balance General Ledger report. Users can now generate the report successfully while keeping the required SUNAT file format.
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
This fix stops Odoo from automatically saving and closing an editable list row when users open a mobile file picker inside a form. It prevents in-progress edits, such as adding course resources, from being silently lost on mobile devices and tablets.
Original PR description
The form view autosaves on 'visibilitychange' (e.g. when the user switches tab/app) to avoid losing unsaved changes. On mobile, opening the native file picker for a binary field also fires 'visibilitychange', which triggered this autosave. When that binary field was part of an editable x2many list, the autosave forced the row out of edition before the file could be selected, silently discarding the edition in progress. Skip the autosave when a x2many field of the root record currently has a row in edition. Can be reproduced in eLearning > course > content > Additional Resources, on mobile devices (must force "desktop mode" in the browser), and on tablets. opw~6517735 Description of the issue/feature this PR addresses: Current behavior before PR: Desired behavior after PR is merged: --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#287730 Forward-Port-Of: odoo/odoo#287581
This fixes an issue where mobile users could not see the search field options on the Point of Sale Orders screen. Staff can now search orders by details such as date or customer on small screens, improving usability for mobile PoS workflows.
Original PR description
Steps to reproduce: - Open a PoS session on a small screen (mobile app or mobile browser) - Register at least one order, so the order list is not empty - Go to the Orders screen and type a term in…
Steps to reproduce: - Open a PoS session on a small screen (mobile app or mobile browser) - Register at least one order, so the order list is not empty - Go to the Orders screen and type a term in the search bar Issue: On a desktop the search bar drops down the list of fields to search on (Reference, Receipt Number, Invoice Number, Date, Customer). On a small screen that list never shows up, so the search silently falls back to the first field and there is no way to search by Date or by Customer. Cause: The list is rendered, but painted behind the order list. Under the media-breakpoint-down(sm) block of ticket_screen.scss the order list becomes `position: sticky; z-index: 1`, so a sibling rule raised `.search .fields` to `z-index: 2` to keep the dropdown on top. The `z-1` utility class put on that dropdown in 07f743843830 compiles to `z-index: 1 !important` and overrides the rule. Both elements end up at `z-index: 1` in the same stacking context, and the order list wins the paint order because it comes later in the DOM. Fix: Drop the `z-1` utility and declare `z-index: 2` on `.fields` in the search bar's own stylesheet, which makes the small-screen override in ticket_screen.scss redundant. The stacking of the dropdown now lives in a single place, next to the rest of its styling, so a utility class added to that element cannot silently disable it again. A tour clicks its target element directly and so cannot see a purely visual overlap, which is why the existing MobileTestUi runs of TicketScreen.search() never caught this. The added assertion checks that a suggestion is the topmost element at its own center. opw-6540466 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#286983
Empty or interrupted attachment uploads no longer cause the media dialog or chatter to crash. The system now handles missing file checksums safely, so users can continue working even when malformed or zero-byte attachments exist.
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#287444The WhatsApp disconnect option is now only shown where it can be safely used, preventing manually configured accounts from appearing connected while incoming messages have actually stopped. This also avoids access errors for regular internal users opening WhatsApp account settings.
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
German point-of-sale sessions using Fiskaly no longer fail to close when an order customer is an address contact without its own name. The export now uses the displayed customer name or a safe fallback, helping businesses complete session closing and required compliance exports reliably.
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
This fix ensures Kenya OSCU invoice numbering is only rolled back when a new number was actually used. It prevents duplicate invoice numbers from being reused after failed eTIMS submissions, reducing errors and compliance disruption.
Original PR description
Followup of d722a68d97d, we should only decrement the OSCU invoice sequence if a new OSCU invoice number was consumed, otherwise it would possibly lead to sending invoice with a duplicate invoice…
Followup of d722a68d97d, we should only decrement the OSCU invoice sequence if a new OSCU invoice number was consumed, otherwise it would possibly lead to sending invoice with a duplicate invoice number to eTIMS. Use case: 1/ We try sending a newly posted invoice to eTIMS, but eTIMS is unreachable: * we allocated a new OSCU invoice number (for ex. `1800`) * we try sending the invoice to eTIMS but fail with a connection error (error code: `CON`) We keep the consumed invoice number. 2/ We try a second time, this time eTIMS is reachable: * as invoice already has a number (for ex. 1800), we check if it exists on eTIMS (that's not the case, error code: `001`) * So we continue and try sending the invoice to eTIMS, this time we get another error, like missing customer information. Here, in that specific conditions, we will decrease the OSCU invoice sequence (back to `1799`) while we haven't consumed any new number. 3/ We fix the previous error (missing customer information) and then successfully send the invoice to eTIMS. 4/ We then try sending another invoice that have no OSCU invoice number yet, as the sequence has been decreased, we will get the OSCU invoice number `1800` again and trying to send it to eTIMS will return an error: `Invoice number already exists`. opw-6490273
This fix helps the HTML editor recover correctly when it detects outdated content during collaborative editing. Users are less likely to remain stuck with stale text after another save has happened, improving content reliability.
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#287408Time off measured in hours now correctly ignores non-working periods in employee schedules. This prevents leave balances from being overstated when a multi-day absence includes partial non-working days, improving payroll and HR accuracy.
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#285955
French PDP Flow 10 responses are now processed instead of only being saved as files. Rejected reports show their rejection details, move to the correct status, and can be corrected and resent manually 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
Downloaded signed PDFs now correctly align multiline Arabic text entered in right-to-left text areas. This improves document readability and ensures signed forms match the intended layout for Arabic-speaking users.
Original PR description
Issue: ---------------------------------------- Arabic text in a rigth-to-left text area is not aligned in the downloaded PDF documents. Steps to reproduce: ---------------------------------------- -…
Issue: ---------------------------------------- Arabic text in a rigth-to-left text area is not aligned in the downloaded PDF documents. Steps to reproduce: ---------------------------------------- - Create a template with a rigth-to-left textarea., Make it fillable by the user. - Click "Sign Now" - Enter multiline arabic text - Validate and download PDF - The lines aren't aligned in the PDF Cause: ---------------------------------------- We compute the empty space before the line with `stringWidth()` on the line but this method doesn't do text-shaping on arabic text. We then do the text-shaping using `reshape_text()` and draw the line. the text shpaing "merged" some caracters making `stringWidth()` return a higher number than the actual drawned line. Solution: ---------------------------------------- Call `reshape_text()` before `stringWidth()`. Before: <img width="937" height="247" alt="image" src="https://github.com/user-attachments/assets/8a43a677-fd29-4d8e-a1cd-419b358667b9" /> After: <img width="896" height="213" alt="image" src="https://github.com/user-attachments/assets/cf8d0ebe-e363-4e07-9eb5-e4963e5ca137" /> opw-6438643x Forward-Port-Of: odoo/enterprise#130991 Forward-Port-Of: odoo/enterprise#130672
Point of Sale register closing messages now include cash in/out movements made by POS managers who do not have accounting access. This prevents misleading closing differences in session records when the cash counted by the user was actually correct.
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
The Point of Sale product configurator no longer shows extra-price badges when a fixed pricelist means those extras will not actually be charged. This prevents cashiers and customers from seeing misleading price information during checkout.
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