add stock_account_avco_negative_fix
This commit is contained in:
parent
10a288f8d5
commit
b07082f70d
9 changed files with 302 additions and 4 deletions
95
stock_account_avco_negative_fix/models/stock_move.py
Normal file
95
stock_account_avco_negative_fix/models/stock_move.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue