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