add stock_account_avco_negative_fix

This commit is contained in:
Luis 2026-08-03 13:10:09 +02:00
parent 10a288f8d5
commit b07082f70d
9 changed files with 302 additions and 4 deletions

View file

@ -0,0 +1,75 @@
# Stock Account AVCO Negative Fix
Corrige un bug del core de Odoo 16 en la valoración de coste promedio (AVCO)
que puede dejar el `standard_price` de un producto en un valor negativo (o
cero) cuando existe stock negativo (`quantity_svl < 0`) y se recibe una
compra que compensa solo parcialmente ese stock.
## Descripción del problema
El método `product_price_update_before_done()` de
`stock_account/models/stock_move.py` calcula el nuevo coste medio ponderado
de un producto AVCO con la fórmula:
```
new_std_price = (amount_unit * qty_svl + price_unit_in * qty) / (qty_svl + qty)
```
Cuando `(qty_svl + qty)` es un valor positivo muy pequeño (el stock negativo
casi se compensa con la compra recibida, pero no del todo), el numerador
puede resultar negativo y el `standard_price` calculado se vuelve negativo,
corrompiendo la valoración de inventario en los recálculos posteriores.
Bug reportado en upstream sin corrección oficial:
https://github.com/odoo/odoo/issues/187169
## Solución
Este módulo **no** copia ni sobrescribe el algoritmo del core. En su lugar,
hereda `stock.move` y aplica una estrategia de post-procesado con `super()`
sobre `_action_done()`, basada en una **precondición** evaluada antes de que
nada más intervenga:
1. Antes de llamar a `super()`, para cada movimiento entrante AVCO relevante
(`move._is_in()` y `cost_method == 'average'`), comprueba si el producto
tenía `quantity_svl <= 0` **antes** de este movimiento (la precondición
bajo la cual el core puede degenerar a un coste negativo o cero). Si se
cumple, captura el `price_unit` entrante (`move._get_price_unit()`).
2. Llama a `super()._action_done()` para que el core ejecute su lógica
completa sin modificaciones (media ponderada AVCO, rama FIFO y el
`_run_fifo_vacuum()` final).
3. Después de `super()`, para cada producto marcado como "en riesgo" en el
paso 1, reescribe **incondicionalmente** su `standard_price` con el
`price_unit` capturado —sin comprobar el valor que haya quedado escrito—,
replicando el mismo patrón de escritura del core (`disable_auto_svl=True`,
`sudo()`) para no generar SVLs de revaluación espurios.
**¿Por qué precondición incondicional y no comprobar el signo final?** Una
primera versión de este fix comprobaba el signo de `standard_price`
*después* de `super()`. Se detectó que ese criterio es insuficiente: otros
módulos que reaccionan durante `_action_done()` (por ejemplo, el OCA
`product_cost_price_avco_sync`, que sincroniza el coste AVCO al reaccionar a
`stock.valuation.layer.write()` disparado por `_run_fifo_vacuum()`) pueden
reescribir `standard_price` a un valor positivo pero distinto del esperado,
haciendo que la comprobación `<= 0` nunca se cumpla aunque la situación de
riesgo sí se haya dado. Al decidir **antes** de que empiece la cadena y
aplicar la corrección de forma incondicional, el fix es robusto frente a
cualquier módulo que reescriba `standard_price` durante el proceso, ahora o
en el futuro.
Este enfoque evita duplicar el algoritmo del core (resiliente a cambios de
upstream) y es compatible con cualquier otro módulo, presente o futuro, que
también herede `product_price_update_before_done`/`_action_done` sobre
`stock.move`, ya que la cadena de `super()` se respeta íntegramente.
## Alcance
- Solo actúa sobre productos con `cost_method = 'average'`.
- Productos FIFO o de coste estándar no se ven afectados.
- No introduce modelos, vistas ni campos nuevos.
## Créditos
### Autores
- Criptomart