Skip to content

[WIP] [MIG] purchase_stock_ux: Migration to 20.0 - #397

Open
les-adhoc wants to merge 15 commits into
ingadhoc:20.0from
adhoc-dev:20.0-t-76382-les
Open

les-adhoc wants to merge 15 commits into
ingadhoc:20.0from
adhoc-dev:20.0-t-76382-les

Conversation

@les-adhoc

@les-adhoc les-adhoc commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Tarea: https://www.adhoc.inc/odoo/project.task/76382

Migration of purchase_stock_ux from 19.0 to 20.0, following the functional spec of the task.

Included from 19.0

What changes in 20.0

  • Returns: the 19 return wizard is gone. The module uses the 20 flow (draft return picking and Exchange). to_refund is no longer shown in any view and new returns are always to refund.
  • Exchange receipts of returns made in 19: a return made in 19 with "Update quantities on SO/PO" unchecked keeps to_refund=False, and an exchange made in 20 copies it. The rule of d404c5b is kept so the replacement does not count as received (without it, a purchase of 10 ends with 12 received and 12 to bill).
  • Internal notes: they still go to the receipt, now through stock.move._get_new_picking_values (_prepare_picking is gone in 20).
  • Vendor on replenishment rules: the module no longer fixes it; the rule uses the 20 placeholder that follows the main vendor. stock_orderpoint.py and its test are removed.
  • Price of new lines without vendor price: the fallback to the product cost moves to _prepare_purchase_order_line_from_procurement (the 19 override of stock.rule._prepare_purchase_order_line never ran).
  • Cancel remaining:
    • core 20 creates a receipt when the line quantity drops and nothing is left to receive (MTO); the module skips it, as 19 did.
    • a purchase manager without manufacturing access can cancel the remaining quantity of a subcontracted line.
    • moves that core adds to an exchange picking after the exchange are cancelled; only the replacement is kept.
  • Returns and billing: the overrides of _compute_qty_invoiced and _compute_invoice_status that subtracted qty_returned are removed. qty_returned stays on the line and still feeds the receipt status and cancel remaining.
  • Line label: in 20 the name of a purchase line holds only the extra description; the origin description of the receipt moves and the cancel remaining message now read label, which holds what name held in 19.
  • Settings: get_bool / set_bool (the 20 API); _make_po_get_domain reads its parameter directly.
  • upgrade_code 19.4-00-ir-access, Material Symbols icons, uom_id renames, uom.uom precision API.

Tests

  • Tests adapted to 20 (return helpers without the wizard, routes product_selectable). They run as superuser, as in 19.
  • New tests for: internal notes on the receipt, settings saved, vendor-less price, cancel remaining on a locked order and with more billed than received, exchange receipts (19 returns and 20 returns), cancel remaining without manufacturing access, MTO receipt cancelled by hand, quantity added to an exchange picking.
  • Local run on a 20.0 database: 0 failed, 0 error(s) of 34 tests.

Product decisions

  • Billing of returns: the module no longer deducts the returned quantity from the quantity to bill on products billed on ordered quantities; it bills as Odoo does. With an exchange, the bill keeps the ordered quantity. A return without replacement is settled with a vendor credit note. Nothing changes on received quantities.
  • Replenishment rules created in 19: the rules created automatically (OdooBot, manual trigger) lose their fixed vendor and follow the main one. That is a data script in ingadhoc/odoo-upgrade, in its own PR.

jcadhoc and others added 13 commits October 29, 2026 12:03
…ndor receipt nor a counter-delivery

When cancelling the remaining qty, lowering product_qty does not always cancel the
pending receipt: with a refund return (ticket 124773) a live pending IN remains and
inflates the forecast of units to receive, and when the negative move does not net
against the pending one (different price/final location) a counter-delivery to the
vendor is generated (ticket 124957).

We now explicitly cancel the line's open remaining move. In 1, 2 or 3 step receptions
the pending qty to receive always lives in the first-step move (vendor->input), which
hangs off move_ids and has no downstream; core (_action_cancel) propagates to later
moves if needed, so there is no need to walk the chain. The cancel is scoped to genuine
forward vendor receipts: exchange-return replacements (a legitimate expected receipt)
and open, not-yet-validated vendor returns (origin_returned_move_id, which also hang off
move_ids and are not part of the remainder) are excluded. product_qty is not touched, so
invoicing (qty_to_invoice) stays intact.

Forward port note: in 19.0 purchase exchanges are not flagged with the sale_stock_ux
is_exchange_move field (that wizard override does not exist here); they are detected from
the return picking, as _should_count_for_quantity_received already did. We extract that
criterion into _is_exchange_receipt() and use it for the guard, so the exclusion is real
in this version instead of dead code.

Tests (test_cancel_remaining.py, over 1/2/3 steps): partial receipt, refund return
(124773), preservation of goods in transit, and an open vendor return that must survive
cancel remaining (RED without the origin_returned_move_id guard).

closes ingadhoc#365

X-original-commit: 44a1146
Signed-off-by: Luciano Esperlazza <les@adhoc.inc>
Signed-off-by: ced-adhoc <ced@adhoc.com.ar>
(cherry picked from commit 885a7a6)

In 20.0, _should_count_for_quantity_received is not overridden: the
exchange receipt rule of d404c5b is not brought, so only
_is_exchange_receipt() comes with this commit.
Cancel remaining on a subcontracted line raised "Nothing to check the
availability for." and left the order unusable, with nothing or part of
it received. Lowering product_qty makes core create a negative move that
nets against the pending receipt and empties it. mrp_subcontracting then
calls action_assign() on that receipt in _action_confirm(), and the
receipt has no open move left.

For subcontracted lines, cancel the open receipt moves before lowering
the quantity, so no negative move is created. Cancelling the receipt
cancels the subcontracting MO and, through its procurement group, the
resupply transfer. Ordering the quantity again creates a new MO with
the current BoM. propagate_cancel is turned off first, as core does
when it nets the negative move, so a customer delivery fed by the
receipt is not cancelled.

If the subcontractor already recorded production on the MO, stop with
a message instead of cancelling it and losing what was recorded: the
user receives what was produced or cancels the MO first.

With a refunded return the line keeps the previous flow: there the
negative move does not empty the receipt, and cancelling first would
make core create a new receipt and MO for the returned quantity.

Other lines keep the current flow. The filter of open moves to cancel
moves to _get_remaining_moves_to_cancel(), used in both places.

Change note:
Al usar "Cancelar remanente" en una línea de compra de un producto
subcontratado, con nada o parte recibido, Odoo mostraba el error "No
hay nada para lo que comprobar disponibilidad" y la compra quedaba sin
poder modificarse. Ahora se cancelan la recepción pendiente, la orden
de subcontratación y el reabastecimiento al subcontratista. Si después
se vuelve a cargar la cantidad, se genera todo de nuevo con la lista
de materiales vigente. La entrega a un cliente que dependa de esa
compra no se cancela. Si el subcontratista ya registró producción, se
avisa para recibir lo producido o cancelar esa orden de producción
antes de cancelar el remanente.

X-original-commit: 395ee69
(cherry picked from commit 2685dbb)
The cancel remaining guard relied on a helper and a flag that do not
exist in 19.0 (subcontracting_has_been_recorded is never written), so
it never stopped and the subcontracting MO was cancelled together with
what the subcontractor recorded. Use the state of the MO instead:
progress or to_close.

(cherry picked from commit 2584a69)
Odoo 20 does not load Font Awesome anymore. Applied port_icons.py
(Odoo's fa_to_ms table): fa-ban -> block, fa-credit-card -> credit_card.
- Internal notes: purchase.order._prepare_picking is gone in 20, the
  receipt is created from its moves. The notes go through
  stock.move._get_new_picking_values.
- Replenishment rules: drop the override that saved the main vendor in
  the rule. Odoo 20 shows it as a placeholder that follows the main
  vendor (decision of the PO in the task).
- Vendor without price: stock.rule._prepare_purchase_order_line does
  not exist in core (not in 19 either), so new lines never took the
  product cost. Move it to _prepare_purchase_order_line_from_procurement.
- Exchange receipt: in 20 to_refund is not editable and is always set,
  so the exchange replacement is detected without it.
- Cancel remaining on subcontracted lines: core 20 creates a new receipt
  for the open destination moves (MTO) when the quantity drops to zero,
  so skip _create_or_update_picking once the remainder was cancelled.
- uom renames: product_uom / product_uom_id -> uom_id, uom rounding ->
  is_zero.
- get_param / set_param -> get_bool / set_bool (core reads
  stock.propagate_uom with get_bool).
- Remove dead code: fields_view_get, and the relock of orders with
  state "done" (that state does not exist in 19 nor 20, locking is the
  "locked" field and does not block the write).
- The return wizard is gone: returns are created with
  stock.picking.action_return and exchanges with action_exchange.
- PurchaseTestCommon runs as a limited test user in 20: these tests set
  up warehouses, routes, BoMs and sale orders, so they run as admin, as
  in 19. Routes are made product selectable before linking them.
- A subcontracting MO is in progress once a component is consumed (not
  with qty_producing), and without work orders it never gets to_close.
- Drop the test of the main vendor saved in the replenishment rule.
- New tests: internal notes on the receipt, settings saved, new line
  with the product cost, cancel remaining on a locked order and with
  more billed than received, and exchange receipts.
The vendor of replenishment rules is Odoo's in 20, and the README now
lists what the module does and was missing: receipt status filters,
mass cancel remaining, bills without empty lines, product cost when the
vendor has no price, grouping by buyer and unit of measure propagation.
Exported with es_po_es419.py (ADR 0059 of actua-20) and translated the
empty terms. Purchase Order and Purchase Order Line take the core es_419
wording. Left empty: the module description (README), the module name,
and a message whose source is already in Spanish.
A return made in 19 with "Update quantities on SO/PO" unchecked keeps
to_refund unchecked in 20, and an exchange made in 20 copies it. The
return does not lower the received quantity, but the replacement raised
it: a purchase of 10 ended with 12 received and 12 to bill. Bring back
the rule of d404c5b: an exchange receipt without "to refund" does not
count as received. Returns made in 20 are always to refund, so they keep
the standard result.

An exchange receipt is also told apart from the moves that core adds to
that picking when the line quantity is raised afterwards, so cancel
remaining cancels those and keeps only the replacement.
…s nor a new receipt

On a subcontracted line with nothing received, cancelling the remaining
quantity cancels the subcontracting MO, and a purchase manager without
manufacturing access got an AccessError. The check and the cancel of the
subcontracted moves run as superuser.

When nothing is left to receive on the line (for example, the receipt
of a line bought on order was cancelled by hand), core 20 created a new
empty receipt when the quantity went down, where 19 skipped the line.
Skip it as in 19, not only for subcontracting.
…parameter

_make_po_get_domain read every setting with res.config.settings
get_values() to know one of them. Any installed module whose get_values
fails broke every purchase procurement. Read the parameter directly.
@roboadhoc

Copy link
Copy Markdown
Contributor

Pull request status dashboard

The module deducted the returned quantity from the quantity to bill on
products billed on ordered quantities. With the exchange of Odoo 20 the
vendor replaces what was returned, and the bill came out short (a
purchase of 10 with 2 exchanged was billed for 8). In 19 this was
avoided by unchecking "Update quantities on SO/PO", which 20 no longer
shows.

Remove the rule and keep Odoo's: on ordered quantities, the ordered
quantity minus what was billed. A return without replacement is settled
with a vendor credit note. On received quantities nothing changes. The
returned quantity is still shown on the line and still counts for the
receipt status and for cancel remaining.
In 20 the name of a purchase order line holds only the extra
description and is empty by default; the product and the description
together are the new label field. The origin description of the
receipt moves and the chatter message of cancel remaining read name,
so they came out empty. Read label, which holds what name held in 19.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants