Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions purchase_stock_price_unit_sync/README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

==============================
Purchase stock price unit sync
==============================
Expand All @@ -17,7 +13,7 @@ Purchase stock price unit sync
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github
Expand All @@ -35,13 +31,59 @@ Purchase stock price unit sync
This module allows to sync picking cost prices with purchase order line
price when moves are already done.

It does the same when the price is corrected on the **vendor bill**.
Odoo corrects a price difference with a child valuation layer worth the
difference times the quantity that has not left stock yet, and sends the
rest to the expense account, so the moves that already left keep the
cost that turned out to be wrong. Here the invoiced price is applied to
the whole receipt instead, which leaves the bill on the same footing as
changing the price on the purchase order.

Can be used with product_cost_price_avco_sync.

**Table of contents**

.. contents::
:local:

Configuration
=============

The correction through the vendor bill needs
``product_cost_price_avco_sync`` installed: it is that module which
replays the valuation chain once the layer changes, re-pricing what
already left stock and correcting a valuation asked for a date before
the correction. Without it the bill keeps Odoo's standard behaviour.

It applies to products with the **Average Cost (AVCO)** costing method
and **manual** inventory valuation. Refunds are left to Odoo, which
compensates them against the original bill with a logic of its own, and
so is automated valuation, where the journal entry of the layer is
already posted and restating it would pull stock valuation and
accounting apart.

Usage
=====

There are two moments where the real price of a purchase shows up after
the goods have already been received, and both are synced:

- **The purchase order line.** Changing its price writes the new one on
the stock moves that are already done and on their valuation layers.
- **The vendor bill.** Posting it at a different price applies that
price to the whole receipt layer, instead of only to the part that has
not left stock yet, which is what Odoo does on its own.

Correcting the same price in both places does not count it twice:
whichever runs second finds the layer already worth what it says and
does nothing.

With ``product_cost_price_avco_sync`` installed, either of them replays
the valuation chain, so the outgoing moves valued in between are
re-priced and a stock valuation asked for a date before the correction
comes out right. Without it, only the layers themselves are written and
the vendor bill keeps Odoo's standard behaviour.

Bug Tracker
===========

Expand Down
2 changes: 1 addition & 1 deletion purchase_stock_price_unit_sync/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"name": "Purchase stock price unit sync",
"summary": "Update cost price in stock moves already done",
"version": "18.0.1.0.0",
"version": "18.0.1.1.0",
"category": "Purchase",
"website": "https://github.com/OCA/purchase-workflow",
"author": "Tecnativa, Odoo Community Association (OCA)",
Expand Down
1 change: 1 addition & 0 deletions purchase_stock_price_unit_sync/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import account_move_line
from . import purchase_order
74 changes: 74 additions & 0 deletions purchase_stock_price_unit_sync/models/account_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2026 Tecnativa - Carlos Dauden
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models
from odoo.tools import float_compare


class AccountMoveLine(models.Model):
_inherit = "account.move.line"

def _is_price_unit_sync_restated(self, layer):
"""Whether the price difference of this invoice line has to be applied
by restating the layer instead of by correcting only what is left of it.

It needs `product_cost_price_avco_sync`, which is what replays the chain

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your docstring, should this module have an explicit dependency on product_cost_price_avco_sync in order to work correctly?

once the layer changes: restating it on its own, without that replay,
would leave the layer inconsistent and would also throw away the
correction Odoo does make, which is worse than not doing anything.

Refunds are left to Odoo, they have a compensation logic of their own,
and so is automated valuation, where the journal entry of the layer is
already posted and restating it would pull the two apart.
"""
self.ensure_one()
if not hasattr(self.env["stock.valuation.layer"], "_cost_price_avco_sync"):
return False
product = self.product_id.with_company(self.company_id)
return (
not self.is_refund
and product.cost_method == "average"
and product.valuation != "real_time"
and layer.stock_move_id
and not layer.stock_valuation_layer_id
)

def _prepare_pdiff_vals(
self, layer, aml, layer_price_unit, out_qty_to_invoice, qty_to_correct
):
"""Apply the invoiced price to the whole layer, not only to what is
still on hand.

Odoo corrects a price difference with a child layer worth the
difference times the quantity that has not left stock yet, and sends the
rest to the expense account. The stock ends up valued right, but the
moves that already left keep the cost that turned out to be wrong, so
the margin of what was sold, and any valuation asked for a date before
the invoice, stay wrong too.

Writing the invoiced price on the layer instead makes
`product_cost_price_avco_sync` replay the chain, which corrects both,
and leaves the bill on the same footing as changing the price on the
purchase order, which this module already syncs. Odoo has done the hard
part by the time this runs: which layer this invoice line pays for, and
at what price, comes from its own matching of layers and bills.
"""
if not self._is_price_unit_sync_restated(layer):
return super()._prepare_pdiff_vals(
layer, aml, layer_price_unit, out_qty_to_invoice, qty_to_correct
)
# Same conversion Odoo does to compare the invoiced price with the layer
price_unit = aml._get_gross_unit_price() / aml.currency_rate
price_unit = aml.product_uom_id._compute_price(
price_unit, self.product_id.uom_id
)
precision = max(
aml.currency_id.decimal_places,
layer.currency_id.decimal_places,
self.env["decimal.precision"].precision_get("Product Price"),
)
if float_compare(price_unit, layer_price_unit, precision_digits=precision):
layer.unit_cost = price_unit
# Nothing is left for Odoo to create: the correction is already in the
# layer, and adding its child layer on top would count it twice.
return [], []
11 changes: 11 additions & 0 deletions purchase_stock_price_unit_sync/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
The correction through the vendor bill needs `product_cost_price_avco_sync`
installed: it is that module which replays the valuation chain once the layer
changes, re-pricing what already left stock and correcting a valuation asked
for a date before the correction. Without it the bill keeps Odoo's standard
behaviour.

It applies to products with the **Average Cost (AVCO)** costing method and
**manual** inventory valuation. Refunds are left to Odoo, which compensates them
against the original bill with a logic of its own, and so is automated
valuation, where the journal entry of the layer is already posted and restating
it would pull stock valuation and accounting apart.
8 changes: 8 additions & 0 deletions purchase_stock_price_unit_sync/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
This module allows to sync picking cost prices with purchase order line
price when moves are already done.

It does the same when the price is corrected on the **vendor bill**. Odoo
corrects a price difference with a child valuation layer worth the difference
times the quantity that has not left stock yet, and sends the rest to the
expense account, so the moves that already left keep the cost that turned out
to be wrong. Here the invoiced price is applied to the whole receipt instead,
which leaves the bill on the same footing as changing the price on the purchase
order.

Can be used with product_cost_price_avco_sync.
17 changes: 17 additions & 0 deletions purchase_stock_price_unit_sync/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
There are two moments where the real price of a purchase shows up after the
goods have already been received, and both are synced:

- **The purchase order line.** Changing its price writes the new one on the
stock moves that are already done and on their valuation layers.
- **The vendor bill.** Posting it at a different price applies that price to the
whole receipt layer, instead of only to the part that has not left stock yet,
which is what Odoo does on its own.

Correcting the same price in both places does not count it twice: whichever runs
second finds the layer already worth what it says and does nothing.

With `product_cost_price_avco_sync` installed, either of them replays the
valuation chain, so the outgoing moves valued in between are re-priced and a
stock valuation asked for a date before the correction comes out right. Without
it, only the layers themselves are written and the vendor bill keeps Odoo's
standard behaviour.
77 changes: 57 additions & 20 deletions purchase_stock_price_unit_sync/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<title>Purchase stock price unit sync</title>
<style type="text/css">

/*
Expand Down Expand Up @@ -360,54 +360,92 @@
</style>
</head>
<body>
<div class="document">
<div class="document" id="purchase-stock-price-unit-sync">
<h1 class="title">Purchase stock price unit sync</h1>


<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="purchase-stock-price-unit-sync">
<h1>Purchase stock price unit sync</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:99f6c9d7e64b0eb277a65118df056874abd5b3eefaa741571e4dce695d53aff5
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/purchase-workflow/tree/18.0/purchase_stock_price_unit_sync"><img alt="OCA/purchase-workflow" src="https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/purchase-workflow-18-0/purchase-workflow-18-0-purchase_stock_price_unit_sync"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/purchase-workflow/tree/18.0/purchase_stock_price_unit_sync"><img alt="OCA/purchase-workflow" src="https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/purchase-workflow-18-0/purchase-workflow-18-0-purchase_stock_price_unit_sync"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows to sync picking cost prices with purchase order line
price when moves are already done.</p>
<p>It does the same when the price is corrected on the <strong>vendor bill</strong>.
Odoo corrects a price difference with a child valuation layer worth the
difference times the quantity that has not left stock yet, and sends the
rest to the expense account, so the moves that already left keep the
cost that turned out to be wrong. Here the invoiced price is applied to
the whole receipt instead, which leaves the bill on the same footing as
changing the price on the purchase order.</p>
<p>Can be used with product_cost_price_avco_sync.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>The correction through the vendor bill needs
<tt class="docutils literal">product_cost_price_avco_sync</tt> installed: it is that module which
replays the valuation chain once the layer changes, re-pricing what
already left stock and correcting a valuation asked for a date before
the correction. Without it the bill keeps Odoo’s standard behaviour.</p>
<p>It applies to products with the <strong>Average Cost (AVCO)</strong> costing method
and <strong>manual</strong> inventory valuation. Refunds are left to Odoo, which
compensates them against the original bill with a logic of its own, and
so is automated valuation, where the journal entry of the layer is
already posted and restating it would pull stock valuation and
accounting apart.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>There are two moments where the real price of a purchase shows up after
the goods have already been received, and both are synced:</p>
<ul class="simple">
<li><strong>The purchase order line.</strong> Changing its price writes the new one on
the stock moves that are already done and on their valuation layers.</li>
<li><strong>The vendor bill.</strong> Posting it at a different price applies that
price to the whole receipt layer, instead of only to the part that has
not left stock yet, which is what Odoo does on its own.</li>
</ul>
<p>Correcting the same price in both places does not count it twice:
whichever runs second finds the layer already worth what it says and
does nothing.</p>
<p>With <tt class="docutils literal">product_cost_price_avco_sync</tt> installed, either of them replays
the valuation chain, so the outgoing moves valued in between are
re-priced and a stock valuation asked for a date before the correction
comes out right. Without it, only the layers themselves are written and
the vendor bill keeps Odoo’s standard behaviour.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h2>
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/purchase-workflow/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/purchase-workflow/issues/new?body=module:%20purchase_stock_price_unit_sync%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-2">Credits</a></h2>
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-3">Authors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-4">Contributors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul>
<li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
<blockquote>
Expand All @@ -421,7 +459,7 @@ <h3><a class="toc-backref" href="#toc-entry-4">Contributors</a></h3>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h3>
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
Expand All @@ -434,6 +472,5 @@ <h3><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h3>
</div>
</div>
</div>
</div>
</body>
</html>
3 changes: 1 addition & 2 deletions purchase_stock_price_unit_sync/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import test_bill_price_difference
from . import test_purchase_stock_price_unit_sync
Loading
Loading