Skip to content

[FIX] purchase_stock_ux: cancel remaining on subcontracted lines - #384

Open
fw-bot-adhoc wants to merge 2 commits into
ingadhoc:19.0from
adhoc-dev:19.0-18.0-h-129539-ced-9299-fw
Open

fw-bot-adhoc wants to merge 2 commits into
ingadhoc:19.0from
adhoc-dev:19.0-18.0-h-129539-ced-9299-fw

Conversation

@fw-bot-adhoc

Copy link
Copy Markdown

Ticket: https://www.adhoc.inc/odoo/helpdesk.ticket/129539

Qué pasa

En una línea de compra de un producto subcontratado, con nada o parte recibido, "Cancelar remanente" falla con Nothing to check the availability for. y la compra queda sin poder modificarse: la recepción, la orden de subcontratación y el reabastecimiento al subcontratista quedan abiertos con la lista de materiales vieja.

Por qué

Al bajar product_qty, core crea un movimiento negativo que se compensa contra la recepción pendiente y la deja en cero. Después, stock.move._action_confirm() de mrp_subcontracting llama a action_assign() sobre esa recepción, que ya no tiene movimientos abiertos, y lanza el error.

Qué cambia

Solo en líneas subcontratadas, se cancelan los movimientos de recepción pendientes antes de bajar la cantidad, así no se crea el movimiento negativo:

  • se cancela la recepción, y con ella la orden de subcontratación y, por su grupo de abastecimiento, el reabastecimiento;
  • al volver a cargar la cantidad se genera una orden de subcontratación nueva con la lista de materiales vigente;
  • antes de cancelar se apaga propagate_cancel, igual que hace core al compensar el negativo (_clean_merged), para no cancelar una entrega a cliente que dependa de esa recepción (MTO con la regla de Comprar propagando la cancelación);
  • si el subcontratista ya registró producción en la orden (_get_recorded_subcontract_production()), el botón frena con un mensaje: recibir lo producido o cancelar esa orden antes de cancelar el remanente. Así no se pierde lo registrado sin aviso;
  • con una devolución con reembolso la línea sigue el flujo anterior: ahí el negativo no vacía la recepción, y cancelar antes haría que core cree una recepción y una orden nuevas por lo devuelto.

Las líneas que no son subcontratadas siguen igual que hoy; la compensación general del movimiento negativo la resuelve stock_ux (oba_features.net_negative_moves). El filtro de movimientos pendientes a cancelar pasa a _get_remaining_moves_to_cancel(), que se usa en los dos lugares.

Tests

En test_cancel_remaining.py (se saltean si no están instalados mrp_subcontracting / sale_stock):

  • test_subcontracted_cancel_remaining_nothing_received (recepción en 1/2/3 pasos): no hay error; se cancelan la recepción, la orden de subcontratación y el reabastecimiento; al volver a cargar la cantidad se usa la lista de materiales nueva. Con el código anterior falla con el mismo error.
  • test_subcontracted_partial_receipt_cancel_remaining (1/2/3 pasos): se cancela lo pendiente y su orden, y se conserva el tramo interno de lo recibido.
  • test_subcontracted_recorded_production_blocks_cancel_remaining: con producción registrada, el botón frena y la orden sigue viva.
  • test_subcontracted_refunded_return_cancel_remaining: con devolución con reembolso no se crean recepciones ni órdenes de más. Falla sin la condición nueva.
  • test_subcontracted_cancel_remaining_keeps_sale_delivery (1/2/3 pasos): la entrega al cliente no se cancela. Falla si se saca el resguardo de propagate_cancel.

Corrí en local toda la suite de purchase_stock_ux (14 tests) en verde. Además probé, fuera de los tests del PR, compra para una venta con recepción en 1/2/3 pasos y entrega en 1/2/3 pasos, con nada y con parte recibido, y compras con varias líneas: sin error y sin cancelar la entrega al cliente (con el código anterior, 21 de esos escenarios daban el error).

Pendiente de verificar en runbot: que los tests no se salteen por falta de mrp_subcontracting / sale_stock en el build.

Forward-Port-Of: #377

@roboadhoc

Copy link
Copy Markdown
Contributor

Pull request status dashboard

@fw-bot-adhoc

Copy link
Copy Markdown
Author

@ced-adhoc @mav-adhoc cherrypicking of pull request #377 failed.

stdout:

Auto-merging purchase_stock_ux/i18n/es.po
Auto-merging purchase_stock_ux/models/purchase_order_line.py
CONFLICT (content): Merge conflict in purchase_stock_ux/models/purchase_order_line.py
CONFLICT (modify/delete): purchase_stock_ux/tests/test_cancel_remaining.py deleted in 29a5bf9453e6f8355b9804bab6200592cc655957 and modified in 2b8bde15e519c1cfdf96d3833b58986102c1254c.  Version 2b8bde15e519c1cfdf96d3833b58986102c1254c of purchase_stock_ux/tests/test_cancel_remaining.py left in tree.

Either perform the forward-port manually (and push to this branch, proceeding as usual) or close this PR (maybe?).

:shipit: you can use git-fw to re-do the forward-port for you locally.

⚠️ after resolving this conflict, you will need to merge it via @roboadhoc.

More info at https://github.com/odoo/odoo/wiki/Mergebot#forward-port

@fw-bot-adhoc

Copy link
Copy Markdown
Author

@ced-adhoc @mav-adhoc this forward port of #377 is awaiting action (not merged or closed).

@ced-adhoc
ced-adhoc force-pushed the 19.0-18.0-h-129539-ced-9299-fw branch 3 times, most recently from 5ff85cf to c60a116 Compare October 27, 2026 13:54
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
@ced-adhoc
ced-adhoc force-pushed the 19.0-18.0-h-129539-ced-9299-fw branch from c60a116 to 2685dbb Compare October 27, 2026 14:24
@ced-adhoc

ced-adhoc commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Se retira del forward-port test_subcontracted_refunded_return_cancel_remaining: en 19.0 el caso con devolución con reembolso ya falla sin este cambio. La suite de purchase_stock_ux en 19.0 pasa en local (23 tests). Seguimiento en la tarea de I+D 78562.

@mav-adhoc

Copy link
Copy Markdown
Contributor

El resguardo de producción registrada queda inerte en 19.0: el core borró _get_recorded_subcontract_production() y dejó subcontracting_has_been_recorded como campo muerto (en 19.0 su única aparición es la declaración, con # TODO: remove in master), así que recorded_productions siempre viene vacío. El test no lo detecta porque setea el campo a mano.

Importa porque _action_cancel() de mrp_subcontracting cancela toda orden de subcontratación activa sin mirar lo registrado: antes el botón frenaba con el error, ahora el camino se habilita y se pierde lo que registró el subcontratista. Dos salidas: adaptarlo a lo que sí vive en 19.0 (state == 'progress', que el core activa con qty_producing o move_raw_ids.picked, sumando el state not in ('done', 'cancel') que tenía el helper de 18.0), o sacar resguardo y test y dejarlo registrado.

El resto del port lo revisé y está bien.

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.
@mav-adhoc

Copy link
Copy Markdown
Contributor

@roboadhoc r+nobump

@roboadhoc

Copy link
Copy Markdown
Contributor

@ced-adhoc @mav-adhoc because this PR has multiple commits, I need to know how to merge it:

  • merge to merge directly, using the PR as merge commit message
  • rebase-merge to rebase and merge, using the PR as merge commit message
  • rebase-ff to rebase and fast-forward
  • rebase-autosquash to rebase and fast-forward, squashing the fixup! and squash! commits

@les-adhoc

Copy link
Copy Markdown
Contributor

Found while forward-porting this change to 20.0: a purchase manager without manufacturing access gets an AccessError when cancelling the remaining quantity of a subcontracted line with nothing received.

Steps (reproduced on 20.0, same code path in 19.0 by reading, not run on 19.0):

  1. User with purchase.group_purchase_manager and stock.group_stock_user, without mrp.group_mrp_user.
  2. Confirm a purchase of a product with a subcontracting BoM; receive nothing.
  3. As that user, click "Cancel remaining" on the line.

Result: AccessError on production_ids of the transfer. The trace goes through subcontracted_moves._action_cancel() → mrp_subcontracting StockMove._action_cancel() → mrp.production.action_cancel(). With a partial receipt it does not fail.

The tests of this PR run as superuser, so they do not catch it.

In 20.0 we fixed it by running the check and the cancel of the subcontracted moves as superuser (the MO belongs to the subcontractor, not to the buyer):

if subcontracted_moves:
    # The MO is the subcontractor's: a purchase user may not have manufacturing access
    subcontracted_moves = subcontracted_moves.sudo()

with a test that runs button_cancel_remaining() with such a user. The same change would apply here.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants