Branch
[FIX] stock_account: let a product without stock change category without valuation access Changing the category of a product to one with another cost method or valuation empties and replenishes its valuation layers. Since 61782d7163be ("[PERF] stock_account: prevent MemoryError in _svl_empty_stock"), `_svl_empty_stock()` reads `stock.valuation.layer` up front for every impacted product to collect the lots of lot-valuated ones, even when no product holds any stock. Users allowed to edit products but not Inventory/Administrator (every other manager group) therefore got "You are not allowed to access 'Stock Valuation Layer'" right after creating a product and changing its category. Return early when no impacted product holds stock, and only collect lots of lot-valuated products holding stock, the only ones whose lots are used. A product with stock still creates layers, and that stays refused for those users, exactly as before.
[FIX] website_sale: a garbage ?category= on a product page must not answer 500 SQL-injection scanners request product pages with payloads in ?category= (e.g. "1' AND 1=1 UNION SELECT NULL-- -"): int(category) raised ValueError in _prepare_product_values and the page answered 500 - 1,031 times in one week on a production 16.0 shop. The earlier guard only covered the shop listing. The category only drives the breadcrumb and the back link here, so an invalid one is ignored, as upstream 19.0 does.
[FIX] project: wait for quick-create to enable before editing The "Second task" step in project_update_tour ran right after the prior quick-create ("New task") was validated, while KanbanRecordQuickCreate was still in its post-validate disabled window (o_disabled, pointer-events: none) held until web_save -> web_read -> onchange -> model.load resolve. Under network/CI latency the tour's click, aimed at that inert input, resolved to the ancestor `.o_kanban_group` instead (pointer-events: none excludes the input from hit-testing), and the following edit step then failed with "target should be editable". This reproduced 3/3 on retry within a single Viindoo runbot build but was intermittent across separate builds, consistent with a latency-dependent race, and reproduced locally on demand by injecting 150ms of CDP network latency. Guard the step's trigger to wait until the quick-create leaves o_disabled before interacting. The earlier "New task" edit step is left unchanged: it runs right after opening a fresh quick-create, not inside any post-validate disabled window. Verified on this fix: one 0ms-latency run and two 150ms-latency runs, two tours succeeded and none failed in each run. Without the guard the same 150ms latency fails the tour at this step. Live users are unaffected: focus stays in the quick-create input during the save. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] tests: park the mouse out of the viewport before browser tests Seven `web` module HOOT/browser tests were deterministically red on the Viindoo runbot (18.0): four in WebSuite.test_hoot, three in WebSuite.test_unit_desktop (autocomplete and sortable). On the Viindoo runbot, the failure signature is byte-identical to a mouse cursor resting inside the viewport at test-page load. The runbot's own OS-level cursor position was never directly observed - only the in-page mechanism is proven: a known cursor position lets Blink dispatch a genuine TRUSTED pointerover/mouseover event into whatever element sits under that point on the next focus/scroll/ layout change, corrupting hoot-dom fixture assertions, AutoComplete's mouseover handlers, and sortable's edge-scroll math. Add ChromeBrowser._park_mouse_outside_viewport(), which sends CDP Input.dispatchMouseEvent {type: 'mouseMoved', x: -100, y: -100} (defensive: logs and never raises on failure). Call it from HttpCase.browser_js right after _wait_ready succeeds and before test code runs. Add addons/web/tests/test_browser_harness.py (BrowserHarnessCursorHygieneTests) as a guard: a permanent sensitivity proof plus the actual regression guard, both authored test-first (RED, confirmed failing before the fix, confirmed green after). Verified by a prior debug run's toggle on Chrome for Testing 150.0.7871.114 (the runbot's exact build): 7/7 failures reproduced then cleared, byte-identical output both directions. Re-confirmed on the finished fix with an integrated run on a fresh ephemeral Odoo 18.0 instance (core-only, web module), same Chrome build: 0 failed, 0 error(s) of 4 tests - both new guard-test methods green, WebSuite.test_hoot 199/199, and WebSuite.test_unit_desktop[@web/core/autocomplete,@web/core/utils/sortable] 33/33, the exact previously-failing set. No existing test, hoot, or hoot-dom file was touched. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] project: wait for quick-create to enable before editing The "Second task" step in project_update_tour ran right after the prior quick-create ("New task") was validated, while KanbanRecordQuickCreate was still in its post-validate disabled window (o_disabled, pointer-events: none) held until web_save -> web_read -> onchange -> model.load resolve. Under network/CI latency the tour's click, aimed at that inert input, resolved to the ancestor `.o_kanban_group` instead (pointer-events: none excludes the input from hit-testing), and the following edit step then failed with "target should be editable". This reproduced 3/3 on retry within a single Viindoo runbot build but was intermittent across separate builds, consistent with a latency-dependent race, and reproduced locally on demand by injecting 150ms of CDP network latency. Guard the step's trigger to wait until the quick-create leaves o_disabled before interacting. The earlier "New task" edit step is left unchanged: it runs right after opening a fresh quick-create, not inside any post-validate disabled window. Verified on this fix: one 0ms-latency run and two 150ms-latency runs, two tours succeeded and none failed in each run. Without the guard the same 150ms latency fails the tour at this step. Live users are unaffected: focus stays in the quick-create input during the save. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] account_edi_ubl_cii: prevent deferred test crash on Community test_invoice_deferred_dates sets invoice.line fields deferred_start_date/deferred_end_date on the new line's create-vals, but only the Enterprise addon account_accountant defines them. Without it installed, the test crashed on every Community build instead of being skipped. Guard it with ensure_installed("account_accountant"), the same idiom already used four times elsewhere in this file, since the fields belong to the Enterprise deferred-revenue feature and are absent here by design. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] pos_restaurant: wait for the order sync instead of racing it The "Check if order has a server ID" step of the test_book_and_release_table tour sampled the order's id exactly once and threw if it was not yet a number. Nothing sequenced that sample after the syncAllOrders RPC that assigns the id: the preceding waitForLoading() only waits for `body:not(:has(.loader))`, and the step's own `trigger: "body"` matches immediately. Whether the step passed was therefore decided by whether the server happened to answer first. On Viindoo's runbot it does not. Subbuild 407980 shows the step running at 02:05:36,110 and the server logging "created pos.order #563" at 02:05:36,152 - the assertion lost the race by 42 ms and the tour failed, even though the order was created successfully moments later. The margin is small and hardware-dependent, not caused by module load. Measured across 24 runs at four addons-path scales (58, 126, 293 and 1330 installed modules - the last exceeding runbot's own 1089), the server won locally by +50..+93 ms every time, and a 23x growth in installed modules moved the mean by only ~24%. Registry size, POS asset-bundle weight and third-party POS addons were each ruled out as the mechanism; what differs is how fast the machine is. Poll for the id with a deadline instead of sampling once, so the step waits for the sync it depends on rather than racing it. The thrown error is unchanged, so existing log triage still matches. The usual idiom for a non-deterministic tour step is a more specific `trigger`, letting the tour engine's own retry loop do the waiting - see 8cb86ec07ad5 "[FIX] lunch: fix non-determistic tour error". It does not transfer here: the condition is JS model state (posmodel's pos.order id), not a DOM fact, so there is no selector to wait on. Verified by constructing the failure, since the race is won on the development machine and cannot be reproduced there naturally. With a 400 ms sleep injected into pos.order.sync_from_ui, the unfixed tour fails with exactly the runbot signature ("FAILED: [7/10] ... Step Check if order has a server ID" / "Order does not have a valid server ID", delta -360 ms). With that same sleep still in place, the fixed tour passes all 10 steps (delta -320 ms - the server is still slow, the step now waits for it). With the sleep removed the fixed tour passes at +86 ms, back inside the natural baseline band, and the rest of the pos_restaurant suite is unaffected (0 failed, 0 errors; the one skip, test_13_crm_team, needs pos_sale and is pre-existing).
[FIX] pos_restaurant: wait for the order sync instead of racing it The "Check if order has a server ID" step of the test_book_and_release_table tour sampled the order's id exactly once and threw if it was not yet a number. Nothing sequenced that sample after the syncAllOrders RPC that assigns the id: the preceding waitForLoading() only waits for `body:not(:has(.loader))`, and the step's own `trigger: "body"` matches immediately. Whether the step passed was therefore decided by whether the server happened to answer first. On Viindoo's runbot it does not. Subbuild 407980 shows the step running at 02:05:36,110 and the server logging "created pos.order #563" at 02:05:36,152 - the assertion lost the race by 42 ms and the tour failed, even though the order was created successfully moments later. The margin is small and hardware-dependent, not caused by module load. Measured across 24 runs at four addons-path scales (58, 126, 293 and 1330 installed modules - the last exceeding runbot's own 1089), the server won locally by +50..+93 ms every time, and a 23x growth in installed modules moved the mean by only ~24%. Registry size, POS asset-bundle weight and third-party POS addons were each ruled out as the mechanism; what differs is how fast the machine is. Poll for the id with a deadline instead of sampling once, so the step waits for the sync it depends on rather than racing it. The thrown error is unchanged, so existing log triage still matches. The usual idiom for a non-deterministic tour step is a more specific `trigger`, letting the tour engine's own retry loop do the waiting - see 8cb86ec07ad5 "[FIX] lunch: fix non-determistic tour error". It does not transfer here: the condition is JS model state (posmodel's pos.order id), not a DOM fact, so there is no selector to wait on. Verified by constructing the failure, since the race is won on the development machine and cannot be reproduced there naturally. With a 400 ms sleep injected into pos.order.sync_from_ui, the unfixed tour fails with exactly the runbot signature ("FAILED: [7/10] ... Step Check if order has a server ID" / "Order does not have a valid server ID", delta -360 ms). With that same sleep still in place, the fixed tour passes all 10 steps (delta -320 ms - the server is still slow, the step now waits for it). With the sleep removed the fixed tour passes at +86 ms, back inside the natural baseline band, and the rest of the pos_restaurant suite is unaffected (0 failed, 0 errors; the one skip, test_13_crm_team, needs pos_sale and is pre-existing).
[FIX] website_sale: a garbage ?category= on a product page must not answer 500 SQL-injection scanners request product pages with payloads in ?category= (e.g. "1' AND 1=1 UNION SELECT NULL-- -"): int(category) raised ValueError in _prepare_product_values and the page answered 500 - 1,031 times in one week on a production 16.0 shop. The earlier guard only covered the shop listing. The category only drives the breadcrumb and the back link here, so an invalid one is ignored, as upstream 19.0 does.
[FIX] website_sale: a garbage ?category= on a product page must not answer 500 SQL-injection scanners request product pages with payloads in ?category= (e.g. "1' AND 1=1 UNION SELECT NULL-- -"): int(category) raised ValueError in _prepare_product_values and the page answered 500 - 1,031 times in one week on a production 16.0 shop. The earlier guard only covered the shop listing. The category only drives the breadcrumb and the back link here, so an invalid one is ignored, as upstream 19.0 does.
[FIX] stock: avoid O(n²) move-line scan when reserving serial products _update_reserved_quantity searches for an updatable move line per reserved quant via `next(l for l in self.move_line_ids if l._reservation_is_updatable(..))`. This scans the (growing) move_line_ids for every quant → O(n²) on a move of n serial units. But stock.move.line._reservation_is_updatable returns False unconditionally for tracking == 'serial', so the scan can never find a candidate and is pure waste.
[FIX] website_sale: a garbage ?category= on a product page must not answer 500 SQL-injection scanners request product pages with payloads in ?category= (e.g. "1' AND 1=1 UNION SELECT NULL-- -"): int(category) raised ValueError in _prepare_product_values and the page answered 500 - 1,031 times in one week on a production 16.0 shop. The earlier guard only covered the shop listing. The category only drives the breadcrumb and the back link here, so an invalid one is ignored, as upstream 19.0 does.
[FIX] test_base_automation: deflake custom reference field tour Switching the trigger from on_stage_set to on_tag_set swaps the relation of trg_field_ref through an onchange round-trip. The tour could open the autocomplete dropdown before that round-trip completed: it then listed the records of the previous model and never refreshed, because the (empty) field value did not change so the AutoComplete never closes it. The guard added by ca1c63e363ce does not help: its descendant :not() selector matches any child element and passes immediately. Retry the second dropdown assertion: close (Escape) and reopen the dropdown until the relation switch has landed, with a 10s cap. Reproduced deterministically by delaying _compute_trg_field_ref by 500ms; the reworked tour passes under the same delay.
[FIX] point_of_sale: write the country id, not the record, in tests common TestPoSCommon.setUpClass writes the company country as a res.country record instead of its id. The ORM tolerates it, but every res.company write override that inspects vals['country_id'] then receives a record where any other caller passes an id or False. An override comparing that value against an id trips the reflected BaseModel.__eq__, which emits an "unsupported operand type(s)" py.warnings for every test class derived from TestPoSCommon, and the comparison silently evaluates to False. Write the id, as done everywhere else in the test suite.
[FIX] website: deflake edit_menus tour when menu items overflow the dialog The final drag section of the edit_menus tour operates on rows the tour itself appends at the bottom of the menu editor list. The drag helper (drag_and_drop_native) computes viewport coordinates from getBoundingClientRect() without scrolling the target into view first, so when installed modules add enough top-level menu items (e.g. to_runbot adds 'Runbot' and 'Runbot Standalone'), the rows to drag overflow the fixed-height dialog body (~490px at 1366x768) and the pointer events silently miss, failing the tour at: Check if 'nested_menu' and 'Modnar !!' is nested under 'new_menu' Scroll the dialog body to the bottom right before the drag section so the dragged rows are always within view. Reproduced and verified on a database where the tour-time list reaches 14-18 rows: failed 6/6 before, passes 3/3 (14 rows), and still passes at 18 and 10 rows after the fix.
[FIX] point_of_sale: adapt tour helper to integer quantity display Commit bf677a9c7e08 backported the Odoo 19 behavior of displaying whole-number quantities without a decimal part, but the tour helper still compared the raw expected quantity string ('1.0'/'1.00') against the rendered '1', breaking every orderline check step across the POS test suites (runbot 223649). Normalize the expected quantity the same way upstream does in 456a7428f8e3 (order_widget_util.js).
[FIX] website: deflake snippet_empty_parent_autoremove tour The customize panel is rebuilt asynchronously (behind the edition mutex) after every selection change or removal, and the stale panel of the previous selection has the same shape as the fresh one. The bare :nth-last-child(3) trigger of 'Remove selected block' can match the stale panel while its editor is being destroyed; the click is then silently swallowed, the column stays in place and the tour times out on 'Check that #wrap is empty' on loaded hosts. Scope the remove trigger to a panel actually showing the Column options (every use of the helper removes a Column block and the stale panel of its parent snippet has no such block), and wait for the second column to leave the iframe DOM before selecting the first one - the column and its options panel are removed in the same synchronous block of removeSnippet, so this guarantees the stale panel is gone. Reproduced on a stock database under host load (2/3 failures with stepDelay=0); consistently green with this change.
[IMP] point_of_sale: hide decimals for integer quantities on receipts Quantities were always formatted with the global 'Product Unit of Measure' decimal precision, so a quantity of 1 was displayed as 1.000 (with a 3-digit precision) on the order screen and the printed receipt, even for countable units such as Units/Piece. Backport the Odoo 19 behavior: whole-number quantities are displayed without a decimal part, while fractional quantities (e.g. 1.5 kg) keep the standard precision-based formatting.
[FIX] pos_loyalty: earn loyalty points on POS orders paid online Steps to reproduce: - Install pos_loyalty + pos_online_payment. - Configure a nominative "loyalty" program (trigger=auto, applies_on=both) that awards points, and an online payment method. - In the POS, add a first-time eligible customer + a product, then pay the order with the online payment method and let the customer pay. Issue: The customer earns no loyalty points (no loyalty.card is even created for a first-time buyer). Cash orders work. The loss is intermittent for customers whose card is already cached. Cause: After a successful online payment the order is rebuilt from the server data, which does not carry the client-side `couponPointChanges`, then re-selected. `set_order` schedules an async `_updateRewards()` recompute but does NOT await it, and `afterPaidOrderSavedOnServer` immediately calls `postPushOrderResolve`, so `confirm_coupon_programs` reads an empty `couponPointChanges`. A first-time buyer additionally needs an async `fetchLoyaltyCard` RPC, so the recompute can never win the race. Fix: - `_updateRewards`: return the mutex promise so it can be awaited. - `afterPaidOrderSavedOnServer`: await the reward recompute before confirming the coupon programs.
[FIX] sale_loyalty: allow invoicing-only users to open sale orders Steps to reproduce: 1. Create a user in the Invoicing group (account.group_account_invoice) only, without any Sales group. 2. As that user, open any sale order form (draft or confirmed with loyalty history). Issue: The form never opens; the client raises "You are not allowed to access 'Loyalty Coupon' (loyalty.card) records". This blocks a user who already has read/write access to sale.order in core, via the access_sale_order_invoicing_payments rule in sale's own security CSV, from doing their job. Cause: Two non-sudo reads in the sale.order extension hit loyalty models that are restricted to the Sales groups: the gift card stat button count queries loyalty.card directly, and the loyalty summary looks up the coupon point name via coupon_point_ids.coupon_id without the sudo already used one line above for loyalty.history. Fix: Gate the gift card count query behind a has_access('read') check, so a user without access simply sees a zero count (the stat button stays hidden), matching the has_access guard sale.order already uses in _create_invoices(). Read the coupon point name through sudo(), consistent with the loyalty.history read beside it, since the loyalty summary is meant to be visible to anyone who can already read the order. Signed-off-by: David Tran <david.tran@tvtmarine.com> (cherry picked from commit 84536036b471ced708a149c40c7f01dbf9a2d2fc)
[FIX] sale_loyalty: allow invoicing-only users to open sale orders Steps to reproduce: 1. Create a user in the Invoicing group (account.group_account_invoice) only, without any Sales group. 2. As that user, open any sale order form (draft or confirmed with loyalty history). Issue: The form never opens; the client raises "You are not allowed to access 'Loyalty Coupon' (loyalty.card) records". This blocks a user who already has read/write access to sale.order in core, via the access_sale_order_invoicing_payments rule in sale's own security CSV, from doing their job. Cause: Two non-sudo reads in the sale.order extension hit loyalty models that are restricted to the Sales groups: the gift card stat button count queries loyalty.card directly, and the loyalty summary looks up the coupon point name via coupon_point_ids.coupon_id without the sudo already used one line above for loyalty.history. Fix: Gate the gift card count query behind a has_access('read') check, so a user without access simply sees a zero count (the stat button stays hidden), matching the has_access guard sale.order already uses in _create_invoices(). Read the coupon point name through sudo(), consistent with the loyalty.history read beside it, since the loyalty summary is meant to be visible to anyone who can already read the order. Signed-off-by: David Tran <david.tran@tvtmarine.com> (cherry picked from commit d5acfaf89b7beffa9ff6c3289432ee0507e55683)
[FIX] account_edi_ubl_cii: prevent deferred test crash on Community test_invoice_deferred_dates sets invoice.line fields deferred_start_date/deferred_end_date on the new line's create-vals, but only the Enterprise addon account_accountant defines them. Without it installed, the test crashed on every Community build instead of being skipped. Guard it with ensure_installed("account_accountant"), the same idiom already used four times elsewhere in this file, since the fields belong to the Enterprise deferred-revenue feature and are absent here by design. Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] project: wait for quick-create to enable before editing The "Second task" step in project_update_tour ran right after the prior quick-create ("New task") was validated, while KanbanRecordQuickCreate was still in its post-validate disabled window (o_disabled, pointer-events: none) held until web_save -> web_read -> onchange -> model.load resolve. Under network/CI latency the tour's click, aimed at that inert input, resolved to the ancestor `.o_kanban_group` instead (pointer-events: none excludes the input from hit-testing), and the following edit step then failed with "target should be editable". This reproduced 3/3 on retry within a single Viindoo runbot build but was intermittent across separate builds, consistent with a latency-dependent race, and reproduced locally on demand by injecting 150ms of CDP network latency. Guard the step's trigger to wait until the quick-create leaves o_disabled before interacting. The earlier "New task" edit step is left unchanged: it runs right after opening a fresh quick-create, not inside any post-validate disabled window. Verified on this fix: one 0ms-latency run and two 150ms-latency runs, two tours succeeded and none failed in each run. Without the guard the same 150ms latency fails the tour at this step. Live users are unaffected: focus stays in the quick-create input during the save. Forward-port to 19.0: applied unchanged. KanbanRecordQuickCreate on 19.0 keeps the same disabled window between the previous record's save and model.load, so the guard addresses the same race. Re-verified on 19.0 (fresh project database, no demo data, same Chrome build): both tours of test_01_project_tour succeeded, 0 failed of 1 test. X-original-commit: ffcd4ae4ee8b3ce5bef087f5f049c96f62f37e87 Signed-off-by: David Tran <david.tran@tvtmarine.com>
[FIX] pos_loyalty: customer-search tours must query the server Four POS tours typed a customer's name into the partner-search box but never pressed Enter, so they only client-side-filtered the preloaded slice of at most 100 partners instead of querying the server, which searches every partner but is only triggered by pressing Enter. This made the tours fail whenever a larger partner set in the target database pushed the fixture partner outside that preloaded slice. Press Enter right after opening the partner-selection screen, using the existing PartnerList.searchCustomerValue(name, true) helper whose true argument triggers the Enter keypress and the server-side search, reusing the pattern already proven elsewhere in this module. Applies to test_not_create_loyalty_card_expired_program, PosOrderClaimReward and PosOrderNoPoints (pos_loyalty_loyalty_program_tour.js) and test_refund_does_not_decrease_points (pos_loyalty_tour.js). Test-only change, no production code touched.