Monday, September 7, 2026
17 changes · saas-18.3
Resolved issues and error corrections
When portal users submit a website form to create a Field Service task, the phone number they provide is now saved on the task. Public submissions still include the phone number in the task description, while safeguards prevent users from changing another contact's details.
Original PR description
# How to reproduce - Install Field Service - Add a form on the Website - Set the form's action to "Create a Task" - Create a new internal/portal user - Login as that user - Submit the form with the required data and a phone number - Inspect the created task as an admin # Issue partner_phone is empty # Cause If the form alters an existing user, we prevent any edition of that user : https://github.com/odoo/odoo/blob/615e54ecd2722433953931e1d51be15b069288c3/addons/website_project/controllers/main.py#L52-L59 # Proposed Solution The PO asked for the following : - if the task is submitted by a portal user -> show the number in partner_phone - if the task is submitted by a public user -> show the number in the description BUT, for security reason, we can't let a user edit any other user. So we limit the assignation to partner_phone only when the current user correspond to the edited partner opw-6374641 Forward-Port-Of: odoo/odoo#285065
Inventory users without Accounting permissions can now view and create E-Waybills without access errors. This prevents disruption during stock and delivery workflows that require E-Waybill documents.
Original PR description
Before this commit Inventory users without Accounting permissions could get an Access Error when viewing or creating an E-Waybill because they could not access the required document types. After this commit Inventory users can now view and create E-Waybills without an Access Error. task-6515093
The Indian GST reporting token refresh process now preserves the existing token instead of replacing it with an empty or unrelated response value. This prevents incorrect token data from being saved and helps keep GST reporting access stable when a token is refreshed.
Original PR description
Previously, `_cron_refresh_gst_token` updated the value of `l10n_in_gstr_gst_token` when refreshing the GST token using `response.get('txn')`.
However, the response received during a token refresh is: `{'status_cd': '1', 'status_desc': 'If previous Auth Token is found'}`
The GST token itself remains unchanged during a refresh; only its validity is extended. Therefore, writing `l10n_in_gstr_gst_token` with `response.get('txn')` is unnecessary and incorrect.
This commit removes that write operation.
Forward-Port-Of: odoo/enterprise#130313French point of sale transactions that encounter e-invoicing data issues will now download the regular invoice instead of an incorrect pro-forma document. This keeps sales flowing while avoiding confusion for customers and staff when external e-invoicing submission cannot proceed.
Original PR description
## Steps to reproduce: - Create a customer with France E-Invoicing (UBL2.1) as eInvoice format in a French company - Go to the PoS, select that partner - Make a sale and ask for an invoice - The…
## Steps to reproduce: - Create a customer with France E-Invoicing (UBL2.1) as eInvoice format in a French company - Go to the PoS, select that partner - Make a sale and ask for an invoice - The downloaded invoice will be a pro-forma invoice ## Why the fix: The pro-forma should not be used here, it is because it is used as a fallback when we get an error while trying to print the invoice. https://github.com/odoo/odoo/blob/4a508586970e44367bbdbbb3cbe88ffb5a1eadb7/addons/account/models/account_move.py#L6187-L6204 As we get an error while trying to send the data with this setup, it goes to the fallback and prints a pro-forma invoice, even though this should not be the case, a regular invoice would do. This happens because when an error is found, we do not populate invoice_pdf_report_id, so it goes to the fallback. We now check if there are any errors in the order, and if there are and the customer requests an ubl_21_fr invoice, we just print the invoice as it is, without going to the pro-forma fallback, as this is not the intended flow. With this fix, we now have the same flow as we do in the sales module, that allows the sale even if the customer has missing data. It will just print the invoice and allow the sale but won't send anything to external entities. opw-6428369 Forward-Port-Of: odoo/odoo#281420
Validated time off entries now keep their calendar blocking when their dates are shortened, such as when an employee leaves mid-leave. This prevents payroll from incorrectly counting affected 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
Early payment discount entries now preserve the invoice line cost allocation for all discount calculation methods. This prevents reporting details from being lost when discounts are applied, while leaving the existing included-tax behavior unchanged.
Original PR description
_get_invoice_counterpart_amls_for_early_payment_discount_per_payment_term_line only split the early payment discount amount per invoice line (and thus preserved each line's own analytic distribution)…
_get_invoice_counterpart_amls_for_early_payment_discount_per_payment_term_line only split the early payment discount amount per invoice line (and thus preserved each line's own analytic distribution) when the payment term's early_pay_discount_computation was "included". For "mixed" and "excluded", it instead built a single counterpart line, discarding the analytic distribution of the invoice lines. Compute the per-invoice-line base amounts (and the corresponding price_unit for "mixed") for all three computations, keeping only the tax calculation restricted to "included". This way "mixed" and "excluded" discount entries now keep the analytic distribution of the invoice line they come from, while behavior for "included" is unchanged. Steps: - Create 3 payment terms with EPD, each one with a different `early_pay_discount_computation` setting - For each payment terms, create one invoice with two lines, only one having an analytic distribution - Confirm and pay the 3 invoices (early payment, discount applied) Issue: For 'mixed' and 'excluded', the discount line is the sum of the discount from the 2 invoice lines, and the analytic distribution is lost. opw-6380278 Forward-Port-Of: odoo/odoo#282541
Bank reconciliation rules now use the company's language when creating journal item labels. This prevents automated reconciliation and users in different languages from creating inconsistent or incorrect labels for the same rule.
Original PR description
### Problem `label` on `account.reconcile.model.line` is a **translatable** field, but its value is written onto the journal item created when the model is applied (`account.move.line.name`). That…
### Problem
`label` on `account.reconcile.model.line` is a **translatable** field, but its value is
written onto the journal item created when the model is applied (`account.move.line.name`).
That means the label is read in the language of whoever applies the model:
- a user working in another language writes the translated value;
- the auto-reconciliation cron writes the **source** value, since it runs as OdooBot.
So the very same reconcile model ends up writing two different labels on the journal items,
depending on who applied it.
### How it shows up
It becomes visible when a reconcile model is created by **duplicating** an existing one and
the label is then edited while working in a non-source language. The translation holds the
new text, while the source value silently keeps the label of the original model — and the
source value is exactly the one the cron writes. The result is a set of journal items where
some carry the intended label and some carry the label of an unrelated model.
### Fix
The journal item belongs to the company, so the label is read in the **company** language
via a small `_get_aml_label()` helper, instead of the language of the current environment.
It falls back to the current behaviour when the company has no language set.
`_prepare_aml_vals()` is the only place in 18.0 that reads `self.label` for the journal item.
### Test
Adds `TestBankRecWidget.test_auto_reconcile_model_label_uses_company_lang`: a reconcile model
whose line label is translated in the company language, applied by the auto-reconciliation
cron running in the source language, and asserts the journal item carries the company-language
label.
```
odoo -d <db> -u account_accountant --test-enable --stop-after-init \
--test-tags /account_accountant:TestBankRecWidget.test_auto_reconcile_model_label_uses_company_lang
```
Without the fix the test fails with `[{'name': 'Frais bancaires'}] != [{'name': 'Bank fees'}]`;
with the fix it passes. The full `account_accountant` suite was also run on a clean 18.0
database: 203 tests, 0 failed, 0 errors.
Forward-Port-Of: odoo/enterprise#128433This fixes a Windows-specific issue that prevented Odoo's automated tests from running because file paths were interpreted differently than on Linux or Mac. The change helps Windows-based developers validate changes locally and reduces avoidable test failures across platforms.
Original PR description
### Description of the issue/feature this PR addresses Since February 2026, changes were made in `/tests/common.py` that were probably never tested on anything else than Linux or Mac machines. Since…
### Description of the issue/feature this PR addresses
Since February 2026, changes were made in `/tests/common.py` that were probably never tested on anything else than Linux or Mac machines. Since that time, Windows users like me (sorry) are not able to run the Odoo tests anymore on their machine. Because the problem remains to this day, I thought I'd submit a PR to fix this permanently.
I have not checked versions older than 18, but it seems the problem does not occur in Odoo 19 anymore, as the code in `/tests/common.py` has been greatly improved.
### Current behavior before PR
Running `odoo-bin` with the `--test-enable` flag never works on Windows, because the simple check on [this line](https://github.com/odoo/odoo/blob/18.0/odoo/tests/common.py#L979) fails due to Windows using backslashes instead of forward slashes in file paths.
It results in the following error (and a whole lot more afterwards as a result of that):
```
2026-08-19 10:28:44,868 48604 INFO odoo-local odoo.tests.common: C:\Apps\Python312\Lib\unittest\mock.py:1581:__enter__ setting Users._crypt_context to <function TransactionCase.setUpClass.<locals>._crypt_context at 0x000002C48DD2C540>
Stack (most recent call last):
File "C:\Program Files\JetBrains\PyCharm 2024.1.4\plugins\python-ce\helpers\pydev\pydevd.py", line 2391, in <module>
main()
File "C:\Program Files\JetBrains\PyCharm 2024.1.4\plugins\python-ce\helpers\pydev\pydevd.py", line 2372, in main
globals = debugger.run(setup['file'], None, None, is_module)
File "C:\Program Files\JetBrains\PyCharm 2024.1.4\plugins\python-ce\helpers\pydev\pydevd.py", line 1640, in run
return self._exec(is_module, entry_point_fn, module_name, file, globals, locals)
File "C:\Program Files\JetBrains\PyCharm 2024.1.4\plugins\python-ce\helpers\pydev\pydevd.py", line 1647, in _exec
pydev_imports.execfile(file, globals, locals) # execute the script
File "C:\Program Files\JetBrains\PyCharm 2024.1.4\plugins\python-ce\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
exec(compile(contents+"\n", file, 'exec'), glob, loc)
File "odoo-bin", line 8, in <module>
odoo.cli.main()
File "[REMOVED]\odoo\odoo\cli\command.py", line 76, in main
o.run(args)
File "[REMOVED]\odoo\odoo\cli\server.py", line 182, in run
main(args)
File "[REMOVED]\odoo\odoo\cli\server.py", line 175, in main
rc = odoo.service.server.start(preload=preload, stop=stop)
File "[REMOVED]\odoo\odoo\service\server.py", line 1510, in start
rc = server.run(preload, stop)
File "[REMOVED]\odoo\odoo\service\server.py", line 644, in run
rc = preload_registries(preload)
File "[REMOVED]\odoo\odoo\service\server.py", line 1414, in preload_registries
registry = Registry.new(dbname, update_module=update_module)
File "[REMOVED]\venv\Lib\site-packages\decorator.py", line 232, in fun
return caller(func, *(extras + args), **kw)
File "[REMOVED]\odoo\odoo\tools\func.py", line 97, in locked
return func(inst, *args, **kwargs)
File "[REMOVED]\odoo\odoo\modules\registry.py", line 118, in new
odoo.modules.load_modules(registry, force_demo, status, update_module)
File "[REMOVED]\odoo\odoo\modules\loading.py", line 485, in load_modules
processed_modules += load_marked_modules(env, graph,
File "[REMOVED]\odoo\odoo\modules\loading.py", line 365, in load_marked_modules
loaded, processed = load_module_graph(
File "[REMOVED]\odoo\odoo\modules\loading.py", line 284, in load_module_graph
test_results = loader.run_suite(suite, global_report=report)
File "[REMOVED]\odoo\odoo\tests\loader.py", line 118, in run_suite
suite(results)
File "C:\Apps\Python312\Lib\unittest\suite.py", line 84, in __call__
return self.run(*args, **kwds)
File "[REMOVED]\odoo\odoo\tests\suite.py", line 43, in run
self._handleClassSetUp(test, result)
File "[REMOVED]\odoo\odoo\tests\suite.py", line 175, in _handleClassSetUp
super()._handleClassSetUp(test, result)
File "[REMOVED]\odoo\odoo\tests\suite.py", line 65, in _handleClassSetUp
currentClass.setUpClass()
File "[REMOVED]\odoo\odoo\tests\common.py", line 1076, in setUpClass
cls.startClassPatcher(cls._crypt_context_patcher)
File "[REMOVED]\odoo\odoo\tests\common.py", line 443, in startClassPatcher
mock = patcher.start()
File "C:\Apps\Python312\Lib\unittest\mock.py", line 1624, in start
result = self.__enter__()
File "C:\Apps\Python312\Lib\unittest\mock.py", line 1581, in __enter__
setattr(self.target, self.attribute, new_attr)
File "[REMOVED]\odoo\odoo\tests\common.py", line 985, in metamodel_setattr
_logger.runbot(
File "[REMOVED]\odoo\odoo\netsvc.py", line 376, in runbot
self.log(logging.RUNBOT, message, *args, **kws)
```
### Desired behavior after PR is merged:
The test suite runs without problems on Windows.
Btw, it would probably be better to use `pathlib` instead as a general improvement for all places in the code where local paths are being handled, but that's out of scope here now.
---
- [x] I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
Forward-Port-Of: odoo/odoo#283210The online shop now blocks combo products from being added to the cart when required choices are missing, even if someone bypasses the on-screen button controls. This prevents customers from checking out with incomplete combo orders and keeps order data consistent.
Original PR description
Steps to reproduce: =================== 1. Publish a combo product whose choices each offer more than one item, so the combo configurator is shown. 2. On the shop, open the product and click "Add to…
Steps to reproduce: =================== 1. Publish a combo product whose choices each offer more than one item, so the combo configurator is shown. 2. On the shop, open the product and click "Add to Cart". 3. Select an item for one combo choice only. The dialog's "Add to cart" button stays disabled. 4. Remove the `disabled` attribute from that button with the browser developer tools and click it. => The combo is added to the cart with one of its choices unanswered, and the order can be paid in that state. Root cause: =========== The incomplete selection is only prevented on the client side, by disabling the button until every choice has been answered. Server side, `/website_sale/combo_configurator/update_cart` only rejects a completely empty selection, never that the selected items cover every choice. Fix: ==== Reject the request unless the selected combo items cover exactly the combo choices of the product. Comparing the set of choices rather than counting the items also rejects a selection answering the same choice twice while leaving another one unanswered. opw-6478837 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#283465
Invoice PDFs for Guatemala and Uruguay now show the correct identification label based on the customer’s selected document type. This prevents confusion when customers use an identification type other than the default VAT number label.
Original PR description
Currently invoice reports use the label related to the company's country for the partner's vat number, however in the case of latin america countries, a new field l10n_latam_identification_type_id is…
Currently invoice reports use the label related to the company's country for the partner's vat number, however in the case of latin america countries, a new field l10n_latam_identification_type_id is added to partners that allows the use of different identification types. This results in invoice reports showing the identification number next to an incorrect label. This commit fixes the issue for Guatemala and Uruguay by overriding the vat label in their respective invoice template to match the selected identification type. Steps to reproduce: - Install a latam localisation (Uruguay or Guatemala) - Change company to that country's Company - Create a partner located in said country, ensure the identification number is set to something else than the default one - Create an invoice for the partner, confirm it, and then Send it - In the pdf generated for the invoice you will see that under the partner's address the "vat number" has the wrong label opw-6087359 related to: https://github.com/odoo/odoo/pull/260159 Forward-Port-Of: odoo/enterprise#122512
Restored or duplicated databases with neutralization enabled are now neutralized before background scheduled tasks can detect and run on them. This reduces the risk of copied databases accidentally sending emails, processing jobs, or triggering other automated actions before they are made safe.
Original PR description
Before this commit: If you restore a backup or duplicate a database via /web/database/manager with "Neutralize" enabled, the cron workers had a small window between the creation of the Registry and the neutralization of the database where they could try to execute crons. Since neutralize_database does not require a full registry to run (it only does raw SQL operations), we can run it before the Registry creation (which has the effect, among other things, of making the cron workers aware of the new database). opw-6517819 Forward-Port-Of: odoo/odoo#286532
On mobile devices, opening a full-screen image preview now hides the editor toolbar so the preview controls are accessible. This improves the editing experience by preventing overlapping controls and also dismisses the mobile keyboard when the preview opens.
Original PR description
When displaying the full screen image preview lightbox on mobile, the toolbar remains displayed. Because of this, the toolbar of the lightbox cannot be accessed. This commit hides the toolbar when a lightbox is displayed. task-6370220 Forward-Port-Of: odoo/odoo#274949
The Send to eTransport action is now available when a Romanian stock transfer is ready as well as when it is completed. This helps users submit required transport information at the appropriate stage without waiting for the transfer to be marked done.
Original PR description
Currently, the Send to eTransport button on `stock.picking` is only visible when picking is done. This PR fixes this behaviour and makes it visible when picking is ready or done both. task-5930984 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#257321
Blog text formatted as bold now appears clearly heavier, even when the surrounding text uses a light font style. This helps editors and readers see emphasized content as intended across different fonts.
Original PR description
Problem: When a parent element applies a `font-weight: 300` to its content, a child `<strong>` tag defaults to `font-weight: bolder`, which resolves to a computed font weight of `400`. For certain font families, weight `400` is visually identical to `300`, leaving no visual distinction for bold text. Cause: `<strong>` tags relied on relative weight boosting (`bolder`), which only increases the parent weight from `300` to `400` instead of applying explicit bold weight. Solution: Explicitly set `font-weight: bold` on `strong` for `.o_wblog_read_text` Steps to reproduce: - Go to /blog - Open any blog - Open editor. - Select some text from the content of the blog. - Apply Bold. - Observe that there is no visual difference. opw-6460699 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr Forward-Port-Of: odoo/odoo#282667
Fixed an issue in Restaurant POS where items already completed by the kitchen could appear again after being split and transferred to another table. This prevents duplicate kitchen work and keeps preparation tickets accurate when staff move items between tables.
Original PR description
After a preparation ticket was marked as completed, transferring one of its items to another table through Split still kept that item in the order's kitchen history. When a new product was later…
After a preparation ticket was marked as completed, transferring one of its items to another table through Split still kept that item in the order's kitchen history. When a new product was later added and sent from the destination table, the preparation display created a new ticket containing both the new item and the already completed one. Steps to reproduce: ------------------- * Open a POS session and the Preparation Display * Select Table 1, add 2 items and send them to the kitchen * Mark the preparation ticket as Completed * On Table 1, open Split, select one item and Transfer it to Table 2 * Open Table 2, add a new item and send it to the kitchen > Observation: The new preparation ticket contains the newly added item and the previously completed transferred item. Why the fix: ------------ Split and table transfer move preparation history to the destination order with a new line uuid, but the preparation display still tracks the original ticket on the source order. Without marking the moved quantity as already sent, the server treated the transferred line as pending and included it again in the next ticket. Set transferredQty when moving preparation history on split/transfer, and read it reliably in _process_preparation_changes so only items with a real quantity increase are sent to the kitchen. opw-6146176 Related : https://github.com/odoo/odoo/pull/277780
Guest checkout customers who enter a valid EU VAT number now have it verified immediately when their address is created. This ensures eligible intra-community purchases receive the correct 0% VAT treatment instead of being charged domestic VAT.
Original PR description
# Description **Steps to Reproduce** 1. Install Belgium Localization, then go to **Settings → Accounting/Invoicing**. 2. Enable **Verify VAT Numbers** (`vat_check_vies`). 3. Go to **Accounting →…
# Description **Steps to Reproduce** 1. Install Belgium Localization, then go to **Settings → Accounting/Invoicing**. 2. Enable **Verify VAT Numbers** (`vat_check_vies`). 3. Go to **Accounting → Configuration → Fiscal Positions** and confirm or create an **Intra-Community** fiscal position with: - Detect Automatically (`auto_apply`): enabled - VAT Required (`vat_required`): enabled - Country Group: EU, no specific country configured 4. Open an incognito/private browser window and make sure the session is unauthenticated. 5. Go to the website's `/shop` page and add any product to the cart. 6. Proceed to checkout until reaching the Address step (`/shop/address`). 7. Enter a delivery address in an EU country different from the company's country (e.g. company in Belgium, delivery address in the Netherlands). 8. In the VAT Number field, enter a real, valid, VIES-registered VAT number corresponding to the delivery country (e.g. a valid NL VAT number for a Netherlands address). 9. Click **Save Address / Continue** and proceed to the Payment step (`/shop/payment`). 10. Check the tax applied to the delivery line and the resulting order total. **Issue** The delivery product and the overall order are taxed at the standard/domestic VAT rate instead of the expected 0% intra-community rate — even though the customer provided a valid, VIES-registered EU VAT number matching the delivery country. **Root Cause** In `base_vat`, `res.partner.create()` unconditionally removes `vies_valid` from the ORM's pending computation queue via `env.remove_to_compute()`, relying on a subsequent `write()` to trigger the actual VIES check. This holds for the standard backend flow, where creation is followed by a `write()` — but the website guest checkout flow differs: - `website_sale` creates the guest partner through `_create_new_address()`. - The partner is created via a single `create()` call, with no follow-up `write()`. - `_compute_vies_valid()` is therefore never triggered. - `vies_valid` remains permanently unset (`NULL`), despite a VAT number being provided. Downstream, `account.fiscal.position._get_vat_required_valid()` reads this unset value as falsy, so the Intra-Community fiscal position's `vat_required` condition fails and is rejected in favor of another applicable position (e.g. EU B2C or Domestic). **Solution** After partner creation, explicitly trigger `_compute_vies_valid()` when the partner has a VAT number and the operation is not part of a file import (`import_file` context) — performing the VIES check immediately instead of relying on a `write()` that guest checkout never issues. **Result** Guest customers providing a valid EU VAT number now get `vies_valid` computed immediately at creation. Fiscal position detection correctly identifies the Intra-Community position, and the expected 0% VAT treatment is applied to the delivery and order. OPW: 6522992 Forward-Port-Of: odoo/odoo#286201
Restaurant POS now correctly handles items moved between tables after kitchen preparation is completed. This prevents already completed items from appearing again on new kitchen tickets, reducing duplicate work and confusion for staff.
Original PR description
After a preparation ticket was marked as completed, transferring one of its items to another table through Split still kept that item in the order's kitchen history. When a new product was later…
After a preparation ticket was marked as completed, transferring one of its items to another table through Split still kept that item in the order's kitchen history. When a new product was later added and sent from the destination table, the preparation display created a new ticket containing both the new item and the already completed one. Steps to reproduce: ------------------- * Open a POS session and the Preparation Display * Select Table 1, add 2 items and send them to the kitchen * Mark the preparation ticket as Completed * On Table 1, open Split, select one item and Transfer it to Table 2 * Open Table 2, add a new item and send it to the kitchen > Observation: The new preparation ticket contains the newly added item and the previously completed transferred item. Why the fix: ------------ Split and table transfer move preparation history to the destination order with a new line uuid, but the preparation display still tracks the original ticket on the source order. Without marking the moved quantity as already sent, the server treated the transferred line as pending and included it again in the next ticket. Set transferredQty when moving preparation history on split/transfer, and read it reliably in _process_preparation_changes so only items with a real quantity increase are sent to the kitchen. opw-6146176 Related : https://github.com/odoo/enterprise/pull/125165