Skip to content

[FIX] product_replenishment_cost_mrp: compute BoM based cost record by record - #936

Open
fw-bot-adhoc wants to merge 1 commit into
ingadhoc:19.0from
adhoc-dev:19.0-18.0-t-72199-les-7851-fw
Open

[FIX] product_replenishment_cost_mrp: compute BoM based cost record by record#936
fw-bot-adhoc wants to merge 1 commit into
ingadhoc:19.0from
adhoc-dev:19.0-18.0-t-72199-les-7851-fw

Conversation

@fw-bot-adhoc

Copy link
Copy Markdown

Un producto con costo de reposición "Basado en LdM" lee el costo de reposición de sus componentes, y un componente puede ser a su vez "Basado en LdM". Los campos no estaban declarados recursive, así que el ORM los computaba de a lotes y protegía el lote entero: leer el campo sobre un registro del lote que todavía no se computó devuelve 0 sin error, y el padre pierde en silencio el costo de ese componente.

Solo se manifiesta cuando no queda ningún componente de la cadena fuera del lote que lo resuelva. Por eso la ficha del producto siempre da bien, la acción contextual falla solo si se selecciona toda la cadena, y la acción planificada —que recorre el catálogo entero— falla siempre. El valor tampoco es estable entre corridas: depende de qué productos caen en el mismo lote y del orden en que se leen, que difiere entre product.product (por default_code) y product.template (por name).

El cambio

Declarar recursive=True sobre replenishment_cost y replenishment_base_cost_on_currency (comparten compute, la protección los agrupa). El ORM pasa a computarlos de a un registro: la protección cubre un solo record y la lectura del componente lo computa en vez de devolver cero.

La declaración va en este módulo y no en product_replenishment_cost porque la recursión la introduce acá: sin costo basado en LdM ningún producto lee el campo de otro, y las bases que no fabrican no pagan el costo.

Alternativas descartadas

  • Ordenar el proceso, componentes primero: la protección aplica al lote completo sin importar el orden, y el padre no lee el costo contable ya grabado del componente sino que le pide recalcular el costo de reposición.
  • Achicar los lotes: reduce la probabilidad pero no la elimina; deja un bug intermitente en vez de uno consistente.
  • Almacenar el campo: el costo depende de la compañía activa y de la cotización del día, así que un valor almacenado no puede depender del contexto. Es otro alcance.
  • Calcular el costo del componente sin leer el campo: duplica la lógica y recalcula el árbol entero por cada padre.

Test plan

Dos tests nuevos en test_replenishment_cost_bom.py, ambos sobre un terminado (30) construido sobre un sub armado (20) más un insumo (10). Hay que iterar el recordset: browse() resetea el prefetch set y computa de a uno, que es justamente el escenario que no falla.

  • test_multi_level_bom_computed_as_batch — lectura del campo con toda la cadena en el mismo lote.
  • test_update_cost_computed_as_batch — la acción que graba el costo contable sobre esa misma selección.

Sin el fix ambos dan 10.0 != 30.0. Con el fix pasan los 5 del módulo, más los 16 de product_replenishment_cost.

Medición

Catálogo sintético de 2000 productos, 1000 de ellos "Basado en LdM" (500 sub armados y 500 terminados construidos sobre esos sub armados). Leyendo los 1000 juntos, que es lo que hace la acción planificada:

Terminados correctos Costo total Mejor de 3 corridas
Sin el fix 0/500 5000 (debería ser 15000) 6,13s
Con el fix 500/500 15000 6,99s

+14% de tiempo. Sobre los 2000 productos mezclados el overhead baja a +10%, porque los productos con costo manual o de proveedor no cambian de comportamiento.

Nota para el reviewer

Forward-Port-Of: #933

…y record

A product whose replenishment cost type is "Based on BoM" reads the replenishment
cost of its components, and a component can be based on a BoM as well. The fields
were not declared recursive, so the ORM computed them in batches and protected the
whole batch: reading the field on a record of the batch that was not computed yet
returns 0 with no error, and the parent silently lost the cost of that component.

It only shows up when no component of the chain is left outside the batch to
resolve it. That is why the product form is always right, the contextual action
fails only when the whole chain is selected, and the scheduled action, which walks
the whole catalog, is always wrong. The resulting value is not even stable between
runs: it depends on which products fall in the same batch and on the order they are
read, which differs between product.product and product.template.

Declaring the fields recursive makes the ORM compute them one record at a time, so
the protection covers a single record and reading a component computes it instead
of returning zero. The declaration lives in this module and not in
product_replenishment_cost because the recursion is introduced here: with no BoM
based cost, no product reads the field of another one, and the bases that do not
manufacture do not pay for it.

Measured on a synthetic catalog of 1000 products based on BoM, 500 of them built
over a sub assembly: without the fix every parent was undervalued, 5000 instead of
15000 in total, and the fix costs 14% more time, 6.13s to 6.99s on the best of
three runs.

X-original-commit: fd3fd04
@roboadhoc

Copy link
Copy Markdown
Contributor

Pull request status dashboard

@fw-bot-adhoc

Copy link
Copy Markdown
Author

@les-adhoc @mav-adhoc this PR targets 19.0 and is the last of the forward-port chain.

To merge the full chain, use

@roboadhoc r+

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

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.

3 participants