95 lines
4.8 KiB
Python
95 lines
4.8 KiB
Python
from odoo import models
|
|
from odoo.tools import float_compare
|
|
|
|
|
|
class StockMove(models.Model):
|
|
_inherit = "stock.move"
|
|
|
|
def _action_done(self, cancel_backorder=False):
|
|
"""Fix negative AVCO ``standard_price`` after a move is done.
|
|
|
|
Odoo core (``stock_account/models/stock_move.py``,
|
|
``product_price_update_before_done``) computes the new average cost
|
|
of an AVCO product as a weighted average between the current
|
|
valuation (``quantity_svl`` / ``standard_price``) and the incoming
|
|
move. When the product has non-positive stock (``quantity_svl <= 0``)
|
|
and the incoming quantity only partially compensates it, the
|
|
resulting weighted average can become negative or zero, corrupting
|
|
the product cost for every subsequent outgoing valuation.
|
|
|
|
This is a known, unfixed upstream bug: see
|
|
https://github.com/odoo/odoo/issues/187169
|
|
|
|
Rather than copying/overriding the core algorithm (which would break
|
|
compatibility with any OCA module -- present or future -- also
|
|
inheriting ``product_price_update_before_done``), we post-process the
|
|
final result of the whole ``_action_done`` flow:
|
|
|
|
- *Before* calling ``super()``, we detect the AVCO products that are
|
|
"at risk": incoming moves (``move._is_in()``) of a product with
|
|
``cost_method == 'average'`` whose stock valuation quantity
|
|
(``quantity_svl``) is already ``<= 0`` *before* this move is
|
|
processed. This is exactly the precondition under which the core's
|
|
weighted-average formula can degenerate to a negative or zero
|
|
result. We capture, for each such ``(company, product)``, the
|
|
incoming move's own ``price_unit`` (``move._get_price_unit()``).
|
|
- We let ``super()._action_done()`` run its full logic unmodified
|
|
(AVCO weighted average, FIFO branch, and the ``_run_fifo_vacuum()``
|
|
step that runs at the very end of ``_action_done``).
|
|
- *After* ``super()`` has fully completed, for every "at risk"
|
|
``(company, product)`` captured above, we unconditionally rewrite
|
|
``standard_price`` with the captured incoming ``price_unit`` (not
|
|
the product's configured purchase price).
|
|
|
|
Note this fix is intentionally based on the *precondition*
|
|
(``quantity_svl <= 0`` before the move) rather than on inspecting the
|
|
*final* ``standard_price`` sign. An earlier version of this fix only
|
|
rewrote ``standard_price`` when it was left ``<= 0`` after
|
|
``super()``, but that check could be silently bypassed: some module
|
|
reacting to the ``_run_fifo_vacuum()`` step (e.g. the OCA module
|
|
``product_cost_price_avco_sync``, which hooks into
|
|
``stock.valuation.layer.write()``) can rewrite ``standard_price`` to
|
|
a *different but still positive* value before our check ran,
|
|
preventing the fix from ever triggering even though the scenario
|
|
that causes the original bug did occur. Keying off the precondition
|
|
instead makes the fix robust regardless of what any other module
|
|
does afterwards, without depending on assumptions about whether the
|
|
vacuum runs in production (in practice, ``product_cost_price_avco_sync``
|
|
disables the vacuum outside of tests for performance reasons, since
|
|
it is only meaningful for FIFO -- but the fix should not rely on
|
|
that being permanently true).
|
|
"""
|
|
at_risk_price_unit = {}
|
|
for move in self.filtered(
|
|
lambda m: m._is_in()
|
|
and m.with_company(m.company_id).product_id.cost_method == "average"
|
|
):
|
|
company = move.company_id
|
|
product = move.product_id.with_company(company)
|
|
quantity_svl = product.sudo().quantity_svl
|
|
rounding = product.uom_id.rounding
|
|
if float_compare(quantity_svl, 0.0, precision_rounding=rounding) <= 0:
|
|
at_risk_price_unit[(company.id, product.id)] = move._get_price_unit()
|
|
|
|
res = super()._action_done(cancel_backorder=cancel_backorder)
|
|
|
|
if not at_risk_price_unit:
|
|
return res
|
|
|
|
companies = self.env["res.company"].browse(
|
|
{key[0] for key in at_risk_price_unit}
|
|
)
|
|
products = self.env["product.product"].browse(
|
|
{key[1] for key in at_risk_price_unit}
|
|
)
|
|
company_by_id = {company.id: company for company in companies}
|
|
product_by_id = {product.id: product for product in products}
|
|
|
|
for (company_id, product_id), price_unit in at_risk_price_unit.items():
|
|
company = company_by_id[company_id]
|
|
product = product_by_id[product_id]
|
|
product.with_company(company.id).with_context(
|
|
disable_auto_svl=True
|
|
).sudo().write({"standard_price": price_unit})
|
|
|
|
return res
|