Wednesday, September 9, 2026
15 changes · 18.0
Enhancements to existing features
Point of Sale sessions with many pay-later customer balances now close more reliably by limiting reconciliation work to the relevant open items. This prevents excessive memory use and worker crashes while keeping the resulting payments and balances the same.
Original PR description
At session close, the pay later receivable lines are reconciled with every unreconciled posted line of the same partners in the PoS journal and PoS invoices, searched with no bound. A customer buying…
At session close, the pay later receivable lines are reconciled with every unreconciled posted line of the same partners in the PoS journal and PoS invoices, searched with no bound. A customer buying on credit accumulates one open line per order and none of them is ever matched until a settlement comes in, so this set only grows. On a database with 48k open items for a single partner, the close hands `_reconcile_plan` 55k lines over 28k moves; `_sync_dynamic_lines` then evaluates `m.line_ids.tax_ids` on every move of the container, prefetching the 262k lines of those moves. That is about 1 GiB: the worker is killed and the session cannot be closed. All of this to reconcile nothing most of the time: reconciliation only pairs opposite signs, and a session that merely adds charges brings no credit to allocate. When there is one, the engine consumes the open items oldest first (`date_maturity` or `date`) and stops when the credit is used up, so anything past that point is loaded for nothing. Look the open items up per partner, account and currency, and only call `_reconcile_plan` when the session's lines or the customer's open credits give something to allocate. Take the open debits in the order the engine consumes them - the PoS lines have no `date_maturity`, so the ordering is done in Python on `date_maturity or date` - and hand over only as many as the credits cover. Both lookups are capped at 2000 lines: a settlement larger than the oldest 2000 open items leaves its remainder as an open credit, allocated by the next close. The partials created are the same as before, only the size of the batch given to `_reconcile_plan` changes. opw-6529792
Resolved issues and error corrections
This fixes a checkout issue where valid free reward products from loyalty promotions could block customers from completing their order. Customers can now continue checkout when a zero-priced item is a legitimate loyalty reward, while other zero-price product safeguards remain in place.
Original PR description
Steps to reproduce: - In a fresh 18.0 database (not runbot), install `website_sale_loyalty` - Website > Configuration > enable "Prevent Sale of Zero Priced Product" - Set up a "Buy X Get Y" loyalty…
Steps to reproduce: - In a fresh 18.0 database (not runbot), install `website_sale_loyalty` - Website > Configuration > enable "Prevent Sale of Zero Priced Product" - Set up a "Buy X Get Y" loyalty program to reward a free product (the 3 Large Cabinet one from demo data will work) - Go to website and add one of the product to cart - Go the the shop page and increase the quantity one at a time Issue: The first additional product over the threshold works fine, but adding one more after that will disable the checkout button and give error message "Warning! Some products in your cart are not available for purchase in your country. Please remove them or contact us." Cause: When the reward line is first added, it has the default `price_unit` of the product and 100% discount. On subsequent changes, `price_unit` is reset to `0.00` and gets caught by this check introduced in #284959. The unit_price of the reward order line is changed to 0 in `_reset_loyalty()@sale_loyalty/models/sale_order_line.py` and is needed for other logic to successfully complete. Fix: Another exception is added to `_get_zero_priced_lines()` so order lines from reward products are exempt from the check, using `is_reward_line` opw-6528237 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
This fix prevents the Point of Sale customer list from crashing after a cashier removes a coupon reward from an order. Staff can continue selecting customers normally, while valid loyalty cards and coupons remain available.
Original PR description
Steps to reproduce: - A coupon program with a coupon assigned to a customer - In the PoS, set that customer on the order - Enter the coupon code, its reward line is added - Remove the reward line -…
Steps to reproduce: - A coupon program with a coupon assigned to a customer - In the PoS, set that customer on the order - Enter the coupon code, its reward line is added - Remove the reward line - Click the customer button Issue: The customer list does not open. The PoS crashes with "TypeError: Cannot read properties of undefined (reading 'id')" raised while rendering PartnerLine. Cause: `partnerId2CouponIds` maps a partner to the ids of its `loyalty.card` records. It is filled at boot and on every `loyalty.card` create, but nothing ever removes an id from it: the models only trigger a create event. Removing the reward line of a code activated coupon deletes that card from the local models (`_setValue` in the order summary), so its id stays in the map while the record is gone. `getLoyaltyCards` pushed `models["loyalty.card"].get(id)` unconditionally, hence an `undefined` entry in the list the PartnerLine template iterates over with `t-key="_loyaltyCard.id"`. Fix: Skip the ids whose record no longer exists. The partner keeps its remaining cards, and the path where no card was deleted is unchanged. The stale id is not pruned from the map: it is reached through the reactive store proxy, and mutating it there would notify subscribers in the middle of a render. opw-6517564 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Self-service kiosk orders are now marked as already sent to the kitchen when the kiosk prints the kitchen ticket. This prevents staff from accidentally sending the same order again from the main Point of Sale, reducing duplicate food preparation and kitchen confusion.
Original PR description
Steps to reproduce: - Restaurant PoS with a kitchen printer, self-ordering in kiosk mode - Place an order on the kiosk: the kiosk prints the kitchen ticket - Open the same order in the PoS and press "Order" Issue: The PoS prints every line of the order again as a new one, so the kitchen prepares the order twice. Cause: The kiosk prints its own kitchen tickets (printKioskChanges) but never records that in last_order_preparation_change, the state the PoS diffs against to find the lines that still have to be sent. The order thus reaches the PoS with all its lines unsent. Fix: Update last_order_preparation_change before the kiosk submits the order to the server, as 19.0 already does since 555df96ac87a. Only in kiosk mode: in 18.0 a mobile order is still sent to the kitchen by the cashier from the PoS, so it has to stay unsent. opw-6520435 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
This fixes a Point of Sale issue where selling two physical gift cards of the same value could combine them into one line with a duplicate code. Each gift card, e-wallet, or discount line that must stay separate now remains separate, preventing checkout failures and unsynced orders.
Original PR description
Steps to reproduce: - Create a gift card program (several programs sharing the same gift card product show the same issue) - In the PoS, sell a physical gift card: click the gift card product and set…
Steps to reproduce:
- Create a gift card program (several programs sharing the same gift card product show the same issue)
- In the PoS, sell a physical gift card: click the gift card product and set a code through "Sell physical gift card?"
- Click the gift card product again to sell a second physical card of the same value
- Validate the order
Issue:
The second unit is merged into the already coded orderline (one line, qty 2, one code), so the "Sell physical gift card?" link is no longer displayed and the second code cannot be entered. Validating the order then fails with "The operation cannot be completed: A coupon/loyalty card must have a unique code." and the order stays unsynced: the qty 2 line is split into two point entries both carrying the same gift_code, so two loyalty.card records are created with the same code.
Cause:
_setupGiftCardOptions() (and setupEWalletOptions()) pass merge=false so that a gift card line is never merged, and until 17.0 add_product() honored it ("options.merge !== false"). The 18.0 store refactoring dropped it: addLineToOrder() decides merging from a local variable that is only set to false when a price_unit is given in vals, and never reads opts.merge. The option became dead code, in point_of_sale's addLineToOrder as well as for the pos_discount caller.
Fix:
Honor opts.merge === false in addLineToOrder(). Callers that do not pass the option are unaffected, so the default merging behaviour is byte-for-byte the same; only the callers explicitly forbidding a merge (gift card, ewallet and discount lines) get their pre-18.0 behaviour back.
opw-6466324
---
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-prThis fixes a problem where the HTML editor could keep outdated content after detecting a stale document. The editor now uses the latest saved version from the record, helping users return to the correct server-held content instead of being stuck with stale text.
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/944595This update aligns how PDF files are handled across supported software versions, reducing the risk of document processing issues on standard Odoo deployments. It also improves compatibility with newer PDF libraries, helping keep PDF-related features reliable after system updates.
Original PR description
Align pypdf usage with the PyPDF2 1.26 API used on Ubuntu Jammy, Odoo 17.0's main supported Ubuntu version, and add the missing compatibility mapping for newer pypdf versions. Forward-Port-Of: odoo/enterprise#130971
Right-to-left Arabic text entered in multiline signature fields now aligns correctly in downloaded PDF documents. This improves document readability and presentation for users signing or generating Arabic-language documents.
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
Social posts now better preserve the formatting users intended when composing content. This reduces cases where text or special elements are displayed incorrectly after posting, improving consistency and trust in the social publishing workflow.
Original PR description
This commit fixes an issue for the social post formatter mixin's regexes being too lenient. The rendering of some elements could be incorrect from what the user initially wanted to create as a post. Now the regexes have been narrowed down so that the resulting formatted value is more in line with what the user wanted. task-6026857
This fixes a Mexican electronic invoicing issue where payment documents could show a slightly different exchange rate than the related invoice, even when both used the same official rate date. Payments now keep the stored official rate when the difference is only a rounding tolerance, reducing CFDI mismatches and potential validation issues.
Original PR description
Issue: Currency rate for payment is recomputed up to 6 digit. However, it may differ from the official one stored in db up to 4 digits. Which creates differencies between invoice and payment rate…
Issue: Currency rate for payment is recomputed up to 6 digit. However, it may differ from the official one stored in db up to 4 digits. Which creates differencies between invoice and payment rate made at the same date. Steps to reproduce: - In MX company, - Enable USD, - Set currency rate for today to 1 USD = 17.4455 MXN - Create a PPD invoice (due date > 40 days) in USD - Add a line with - qty: 1, - unit_price: 3.488 - tax: 16% (default tax) - Send it to CFDI - Create payment - On the invoice Form click on "Update Payment" Current behavior: - In the CFDI sheet, Payment and Invoice XML files will have different currency rates Expected behavior: - In the CFDI sheet, Payment and Invoice XML files will have the same currency rate Cause: PACs require having the payment `amount` to be equal to `currency_amount * currency_rate`. For huge amout it may happen that using the 6 digits rounding of currency rate to compute the amount won't fall exactly on the two digit precision for the amount and payment would be refused. Therefore, for all payment, we recompute a 6 digits precision `currency_rate` from `amount` and `currency_amount` then using it to compute the final amount. However, Banxico (Mexican central Bank) publish rates with a 4 digit precision. Recomputing the currency rate up to 6 digits may slightly change it from the 4 digit precision official currency rate. opw-6411530
This change prevents test sessions from being unexpectedly rotated during login checks, reducing random test failures. It helps keep automated validation stable and avoids delays caused by intermittent failures in the development pipeline.
Original PR description
The test harness patches the CryptContext object to spend less time hashing to decrease total test runtime. Because the hash parameters have changed, every first login (per transaction) per user will result in a hash rotation. The session_id is also rotated when the password hash rotates. When multiple requests are sent to the server while a session rotation is underway, the session datastore holds either a valid, or expired, or logged-out user session. This is a source of indeterminism in tests that can be prevented by always returning None value for replacement hash. REF Runbot; https://runbot.odoo.com/odoo/error/242811 REF Runbot; https://runbot.odoo.com/odoo/error/233722 Forward-Port-Of: odoo/odoo#285710
This fix makes the product identifiers sent to Google Analytics match the identifiers used in Google Merchant Center feeds. This helps Google Ads correctly connect Shopping ad clicks with purchases, improving attribution and diagnostic accuracy.
Original PR description
**Issue:** When google analytics (GA) and google merchant center (GMC) are setup, Google Ads diagonistic reports that item IDs cannot be matched to Merchant Center. **Why this happens:** `order_lines_2_google_api` sets `product.barcode or product.id` for `item_id`, while `product.feed._prepare_gmc_items` defaults to `product.default_code or product.id` for the feed's `id` field. Google Ads/Analytics attribution relies on GA4's `item_id` matching GMC's `id` for the same product to connect Shopping ad clicks to purchase events. **References:** https://support.google.com/merchants/answer/6324405?sjid=15224664355638221483-NC https://support.google.com/google-ads/answer/14943675?hl=en Backport of d0bf183b053eb0133e6f7e5e04481ca07bff6bb3 opw-6443326 Forward-Port-Of: odoo/odoo#287152
Large marketing emails with several WebP images could fail to save or silently lose the end of the message. This fix preserves the full email content by handling oversized embedded images safely during save, reducing the risk of broken campaigns and lost work.
Original PR description
Root cause: Webp images are not supported by Outlook, so the editor turns each one into an inline base64 image before the body is saved. A mailing with a few webp images ends up with a body of more…
Root cause: Webp images are not supported by Outlook, so the editor turns each one into an inline base64 image before the body is saved. A mailing with a few webp images ends up with a body of more than 10 million characters, and each background image is copied a second and a third time for the Outlook fallback. lxml stops reading a document that big and gives back what it read so far, without raising anything. The body is cut in the middle of one of those base64 images. base64.b64decode then gets a piece of an image and raises, which is the error in the ticket. When the cut lands between two elements instead, there is no error at all and the end of the body is simply gone. On the customer's mailing the body is 16.5 million characters and 6.9 million of them are dropped on save. Fix: Put the base64 images aside in _convert_inline_images_to_urls before the body is read, and leave a short placeholder in their place. lxml then only reads the tags, which stays small. The image is given back when the attachment is created, and any placeholder still in the body is put back at the end. This method is the right place because it is the one that reads the body while the images are still inside it. Steps to reproduce: 1. Install Email Marketing. 2. Go to Email Marketing > Mailings > New. 3. Set a subject, pick a mailing list, then open the Mail Body tab. 4. Pick the Start From Scratch theme. 5. Drag a Cover block and replace its background image with a webp image of about 1 MB. 6. Drag three Text - Image blocks and replace each image with a webp image of about 1 MB. 7. Type four lines of text under the last image. 8. Click Save. => Odoo Server Error, binascii.Error: Invalid base64-encoded string: number of data characters cannot be 1 more than a multiple of 4 Ticket [link](https://www.odoo.com/odoo/project.task/6450151) opw-6450151
The Helpdesk Auto Assignment group name has been corrected to fix a spelling mistake. This improves clarity for users and administrators without changing any functionality.
Original PR description
This commit fixes the typo in the "Auto Assignment" group. task-6542450 Forward-Port-Of: odoo/enterprise#130841
Saudi e-invoices using ZATCA validation can no longer be confirmed when rounding creates negative untaxed or tax amounts. This prevents invalid invoices from being submitted and rejected by the ZATCA/Fatoora portal.
Original PR description
**Steps to reproduce:** - Install the l10n_sa_edi module, set the company to Saudi Arabia, and onboard the sales journal. - Set the 15% sales tax to Included in Price. - Create a customer invoice for…
**Steps to reproduce:** - Install the l10n_sa_edi module, set the company to Saudi Arabia, and onboard the sales journal. - Set the 15% sales tax to Included in Price. - Create a customer invoice for an individual with three lines: 19.00, 12.00, and -31.00, all using the 15% tax. - The invoice totals are -0.01 untaxed, 0.01 tax, and 0.00 total. - Generate the ZATCA UBL. **Observed behavior:** - ZATCA rejects the submission with BR-S-08, BR-CO-13, and BR-CO-15. **Cause:** - When the tax is Included in Price, rounding can result in a negative untaxed amount (-0.01) while the tax amount remains positive (0.01). Negative invoice amounts are not accepted by the ZATCA/Fatoora portal, resulting in an invalid UBL and rejection. **Fix:** - Add a validation before confirming the invoice to prevent invoices with a negative untaxed or tax amount from being confirmed when ZATCA is enabled. This prevents invalid invoices from being submitted to ZATCA and avoids the resulting UBL validation errors. Reference: [ZATCA Guidance on Negative Values in Unit Price](https://zatca1.discourse.group/t/negative-values-in-unit-price-and-line/5376negative-values-in-unit-price-and-line) opw-6433617