From 6ce55c86d34268c75bb433a45d306bb275cac687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Todorovich?= Date: Thu, 17 Sep 2026 14:57:04 -0300 Subject: [PATCH 1/3] [FIX] sale_report_margin: report the purchase price as a unit cost The purchase price was a sum of the unit costs of the grouped lines, without currency conversion. Report it as the average unit cost converted to the company currency, like the unit price. --- sale_report_margin/README.rst | 11 +++-- sale_report_margin/readme/CONTRIBUTORS.md | 6 ++- sale_report_margin/readme/DESCRIPTION.md | 7 +-- sale_report_margin/reports/sale_report.py | 8 ++- .../static/description/index.html | 14 ++++-- .../tests/test_sale_report_margin.py | 49 +++++++++++++++++++ 6 files changed, 83 insertions(+), 12 deletions(-) diff --git a/sale_report_margin/README.rst b/sale_report_margin/README.rst index 52c56a659..793b0e8d0 100644 --- a/sale_report_margin/README.rst +++ b/sale_report_margin/README.rst @@ -32,9 +32,10 @@ Sale Report Margin |badge1| |badge2| |badge3| |badge4| |badge5| -Adds purchase_price field to sale report. This field comes from the -*Cost* field configured in the products themselves and on the defined -inventory valuation method. +Adds purchase_price field to sale report. This field is the unit cost of +the sold products, in the company currency. It comes from the *Cost* +field configured in the products themselves and on the defined inventory +valuation method. **Table of contents** @@ -81,6 +82,10 @@ Contributors - SodexisTeam +- `Camptocamp `__: + + - Iván Todorovich + Maintainers ----------- diff --git a/sale_report_margin/readme/CONTRIBUTORS.md b/sale_report_margin/readme/CONTRIBUTORS.md index 8736c3949..f8bbd6168 100644 --- a/sale_report_margin/readme/CONTRIBUTORS.md +++ b/sale_report_margin/readme/CONTRIBUTORS.md @@ -4,4 +4,8 @@ > - David Vidal > - Carlos Roca -- SodexisTeam \<\> \ No newline at end of file +- SodexisTeam \<\> + +- [Camptocamp](https://www.camptocamp.com): + + > - Iván Todorovich \<\> diff --git a/sale_report_margin/readme/DESCRIPTION.md b/sale_report_margin/readme/DESCRIPTION.md index d1112d818..ec3140c9e 100644 --- a/sale_report_margin/readme/DESCRIPTION.md +++ b/sale_report_margin/readme/DESCRIPTION.md @@ -1,3 +1,4 @@ -Adds purchase_price field to sale report. This field comes from the -*Cost* field configured in the products themselves and on the defined -inventory valuation method. +Adds purchase_price field to sale report. This field is the unit cost of +the sold products, in the company currency. It comes from the *Cost* +field configured in the products themselves and on the defined inventory +valuation method. diff --git a/sale_report_margin/reports/sale_report.py b/sale_report_margin/reports/sale_report.py index be3f576e6..a994e3db5 100644 --- a/sale_report_margin/reports/sale_report.py +++ b/sale_report_margin/reports/sale_report.py @@ -1,4 +1,5 @@ # Copyright 2018 Tecnativa - Sergio Teruel +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields, models @@ -7,9 +8,12 @@ class SaleReport(models.Model): _inherit = "sale.report" - purchase_price = fields.Float(readonly=True) + purchase_price = fields.Float(readonly=True, aggregator="avg") def _select_additional_fields(self): res = super()._select_additional_fields() - res["purchase_price"] = "SUM(l.purchase_price)" + # Unit cost, converted to the company currency like ``price_unit``. + res["purchase_price"] = f"""AVG(l.purchase_price + / {self._case_value_or_one("s.currency_rate")} + * {self._case_value_or_one("account_currency_table.rate")})""" return res diff --git a/sale_report_margin/static/description/index.html b/sale_report_margin/static/description/index.html index d84f66ba8..f3fab6350 100644 --- a/sale_report_margin/static/description/index.html +++ b/sale_report_margin/static/description/index.html @@ -375,9 +375,10 @@

Sale Report Margin

!! source digest: sha256:3ba588ce7e07b01b5bc3c5c65dcb27d213765f309a80e4222d264c3b85fccef6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/margin-analysis Translate me on Weblate Try me on Runboat

-

Adds purchase_price field to sale report. This field comes from the -Cost field configured in the products themselves and on the defined -inventory valuation method.

+

Adds purchase_price field to sale report. This field is the unit cost of +the sold products, in the company currency. It comes from the Cost +field configured in the products themselves and on the defined inventory +valuation method.

Table of contents

diff --git a/sale_report_margin/tests/test_sale_report_margin.py b/sale_report_margin/tests/test_sale_report_margin.py index ca00ba9a3..8fec91eee 100644 --- a/sale_report_margin/tests/test_sale_report_margin.py +++ b/sale_report_margin/tests/test_sale_report_margin.py @@ -1,6 +1,7 @@ # Copyright 2023 Álvaro Marcos # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields from odoo.orm.commands import Command from odoo.addons.base.tests.common import BaseCommon @@ -40,3 +41,51 @@ def test_sale_report_margin(self): ] ) self.assertEqual(report.purchase_price, 8.0) + + def test_sale_report_margin_other_currency(self): + """The cost of an order in another currency is reported in the company one. + + Scenario: + 1. Set up a currency worth half of the company currency. + 2. Confirm an order in that currency with one line sold at 10 with + a cost of 8. + Expected: + - The report row shows a unit price of 5 and a purchase price of 4, + both in the company currency. + """ + currency = self.quick_ref("base.EUR") + currency.active = True + self.env["res.currency.rate"].create( + { + "currency_id": currency.id, + "company_id": self.env.company.id, + "name": fields.Date.today(), + "rate": 2.0, + } + ) + pricelist = self.env["product.pricelist"].create( + {"name": "Test Pricelist", "currency_id": currency.id} + ) + order = self.env["sale.order"].create( + { + "name": "Test Order", + "partner_id": self.partner.id, + "pricelist_id": pricelist.id, + "order_line": [ + Command.create( + { + "product_id": self.product_product.id, + "price_unit": 10.0, + "product_uom_qty": 1.0, + "purchase_price": 8.0, + }, + ) + ], + } + ) + order.action_confirm() + report = self.env["sale.report"].search( + [("order_reference", "=", f"{order._name},{order.id}")] + ) + self.assertEqual(report.price_unit, 5.0) + self.assertEqual(report.purchase_price, 4.0) From 9d6550b594f752895188ae2d0c8110c98fc1ba60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Todorovich?= Date: Thu, 17 Sep 2026 14:57:36 -0300 Subject: [PATCH 2/3] [IMP] sale_report_margin: add the margin percentage to the sale report Ratio of the margin over the untaxed total of the grouped lines, as an averaged measure. --- sale_report_margin/README.rst | 12 +++-- sale_report_margin/readme/DESCRIPTION.md | 10 ++-- sale_report_margin/readme/USAGE.md | 2 +- sale_report_margin/reports/sale_report.py | 6 +++ .../static/description/index.html | 13 +++-- .../tests/test_sale_report_margin.py | 51 +++++++++++++++++-- 6 files changed, 75 insertions(+), 19 deletions(-) diff --git a/sale_report_margin/README.rst b/sale_report_margin/README.rst index 793b0e8d0..9418147fc 100644 --- a/sale_report_margin/README.rst +++ b/sale_report_margin/README.rst @@ -32,10 +32,12 @@ Sale Report Margin |badge1| |badge2| |badge3| |badge4| |badge5| -Adds purchase_price field to sale report. This field is the unit cost of -the sold products, in the company currency. It comes from the *Cost* -field configured in the products themselves and on the defined inventory -valuation method. +Adds margin information to the sales analysis report: + +- ``Purchase Price``: the unit cost of the sold products, in the company + currency. It comes from the *Cost* field configured in the products + themselves and on the defined inventory valuation method. +- ``Margin (%)``: the margin over the untaxed total. **Table of contents** @@ -46,7 +48,7 @@ Usage ===== Go to *Sales > Reporting > Sales* and you'll find the new *Purchase -Price* measure. +Price* and *Margin (%)* measures. Known issues / Roadmap ====================== diff --git a/sale_report_margin/readme/DESCRIPTION.md b/sale_report_margin/readme/DESCRIPTION.md index ec3140c9e..0eabbb3c0 100644 --- a/sale_report_margin/readme/DESCRIPTION.md +++ b/sale_report_margin/readme/DESCRIPTION.md @@ -1,4 +1,6 @@ -Adds purchase_price field to sale report. This field is the unit cost of -the sold products, in the company currency. It comes from the *Cost* -field configured in the products themselves and on the defined inventory -valuation method. +Adds margin information to the sales analysis report: + +- `Purchase Price`: the unit cost of the sold products, in the company + currency. It comes from the *Cost* field configured in the products + themselves and on the defined inventory valuation method. +- `Margin (%)`: the margin over the untaxed total. diff --git a/sale_report_margin/readme/USAGE.md b/sale_report_margin/readme/USAGE.md index 833ee81d6..ffa0b8c01 100644 --- a/sale_report_margin/readme/USAGE.md +++ b/sale_report_margin/readme/USAGE.md @@ -1,2 +1,2 @@ Go to *Sales \> Reporting \> Sales* and you'll find the new *Purchase -Price* measure. +Price* and *Margin (%)* measures. diff --git a/sale_report_margin/reports/sale_report.py b/sale_report_margin/reports/sale_report.py index a994e3db5..0dab34372 100644 --- a/sale_report_margin/reports/sale_report.py +++ b/sale_report_margin/reports/sale_report.py @@ -9,6 +9,7 @@ class SaleReport(models.Model): _inherit = "sale.report" purchase_price = fields.Float(readonly=True, aggregator="avg") + margin_percent = fields.Float(string="Margin (%)", readonly=True, aggregator="avg") def _select_additional_fields(self): res = super()._select_additional_fields() @@ -16,4 +17,9 @@ def _select_additional_fields(self): res["purchase_price"] = f"""AVG(l.purchase_price / {self._case_value_or_one("s.currency_rate")} * {self._case_value_or_one("account_currency_table.rate")})""" + # Ratio of the grouped lines' margin over their untaxed total. The currency + # conversion applied to both amounts in the report cancels out. + res["margin_percent"] = """ + CASE WHEN SUM(l.price_subtotal) = 0 THEN 0 + ELSE SUM(l.margin) / SUM(l.price_subtotal) END""" return res diff --git a/sale_report_margin/static/description/index.html b/sale_report_margin/static/description/index.html index f3fab6350..8f8b5401d 100644 --- a/sale_report_margin/static/description/index.html +++ b/sale_report_margin/static/description/index.html @@ -375,10 +375,13 @@

Sale Report Margin

!! source digest: sha256:3ba588ce7e07b01b5bc3c5c65dcb27d213765f309a80e4222d264c3b85fccef6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/margin-analysis Translate me on Weblate Try me on Runboat

-

Adds purchase_price field to sale report. This field is the unit cost of -the sold products, in the company currency. It comes from the Cost -field configured in the products themselves and on the defined inventory -valuation method.

+

Adds margin information to the sales analysis report:

+
    +
  • Purchase Price: the unit cost of the sold products, in the company +currency. It comes from the Cost field configured in the products +themselves and on the defined inventory valuation method.
  • +
  • Margin (%): the margin over the untaxed total.
  • +

Table of contents

    @@ -396,7 +399,7 @@

    Sale Report Margin

    Usage

    Go to Sales > Reporting > Sales and you’ll find the new Purchase -Price measure.

    +Price and Margin (%) measures.

    Known issues / Roadmap

    diff --git a/sale_report_margin/tests/test_sale_report_margin.py b/sale_report_margin/tests/test_sale_report_margin.py index 8fec91eee..f8ad709fc 100644 --- a/sale_report_margin/tests/test_sale_report_margin.py +++ b/sale_report_margin/tests/test_sale_report_margin.py @@ -16,7 +16,14 @@ def setUpClass(cls): ) def test_sale_report_margin(self): - """Check purchase_price in sale report""" + """Check the purchase price and margin of a line in the sale report. + + Scenario: + 1. Confirm an order with one line sold at 10 with a cost of 8. + Expected: + - The report row shows a purchase price of 8. + - The report row shows a margin of 2, that is 20% of the untaxed total. + """ order = self.env["sale.order"].create( { "name": "Test Order", @@ -41,17 +48,51 @@ def test_sale_report_margin(self): ] ) self.assertEqual(report.purchase_price, 8.0) + self.assertEqual(report.margin, 2.0) + self.assertEqual(report.margin_percent, 0.2) + + def test_sale_report_margin_percent_no_subtotal(self): + """A free line has no margin percentage instead of a division error. + + Scenario: + 1. Confirm an order with one line sold at 0 with a cost of 8. + Expected: + - The report row shows a margin of -8 and a margin percentage of 0. + """ + order = self.env["sale.order"].create( + { + "name": "Test Order", + "partner_id": self.partner.id, + "order_line": [ + Command.create( + { + "product_id": self.product_product.id, + "price_unit": 0.0, + "product_uom_qty": 1.0, + "purchase_price": 8.0, + }, + ) + ], + } + ) + order.action_confirm() + report = self.env["sale.report"].search( + [("order_reference", "=", f"{order._name},{order.id}")] + ) + self.assertEqual(report.margin, -8.0) + self.assertEqual(report.margin_percent, 0.0) def test_sale_report_margin_other_currency(self): - """The cost of an order in another currency is reported in the company one. + """Amounts of an order in another currency are reported in the company one. Scenario: 1. Set up a currency worth half of the company currency. 2. Confirm an order in that currency with one line sold at 10 with a cost of 8. Expected: - - The report row shows a unit price of 5 and a purchase price of 4, - both in the company currency. + - The report row shows a unit price of 5, a purchase price of 4 and + a margin of 1, all in the company currency. + - The margin percentage is still 20%. """ currency = self.quick_ref("base.EUR") currency.active = True @@ -89,3 +130,5 @@ def test_sale_report_margin_other_currency(self): ) self.assertEqual(report.price_unit, 5.0) self.assertEqual(report.purchase_price, 4.0) + self.assertEqual(report.margin, 1.0) + self.assertEqual(report.margin_percent, 0.2) From 2033e37766879427677d25e4f8fc8adcbc46203d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Todorovich?= Date: Thu, 17 Sep 2026 14:57:58 -0300 Subject: [PATCH 3/3] [IMP] sale_report_margin: show the margin in the list view Add the purchase price next to the unit price, and the margin and margin percentage next to the total. --- sale_report_margin/README.rst | 6 +++++ sale_report_margin/__manifest__.py | 1 + sale_report_margin/readme/DESCRIPTION.md | 3 +++ sale_report_margin/readme/USAGE.md | 3 +++ .../static/description/index.html | 4 +++ .../views/sale_report_views.xml | 25 +++++++++++++++++++ 6 files changed, 42 insertions(+) create mode 100644 sale_report_margin/views/sale_report_views.xml diff --git a/sale_report_margin/README.rst b/sale_report_margin/README.rst index 9418147fc..142d37de9 100644 --- a/sale_report_margin/README.rst +++ b/sale_report_margin/README.rst @@ -39,6 +39,9 @@ Adds margin information to the sales analysis report: themselves and on the defined inventory valuation method. - ``Margin (%)``: the margin over the untaxed total. +The purchase price, margin and margin percentage are also shown as +columns in the list view of the report. + **Table of contents** .. contents:: @@ -50,6 +53,9 @@ Usage Go to *Sales > Reporting > Sales* and you'll find the new *Purchase Price* and *Margin (%)* measures. +In the list view, the *Purchase Price* column is displayed next to the +unit price, and the *Margin* and *Margin (%)* columns next to the total. + Known issues / Roadmap ====================== diff --git a/sale_report_margin/__manifest__.py b/sale_report_margin/__manifest__.py index e4ece00ea..845b1bfa6 100644 --- a/sale_report_margin/__manifest__.py +++ b/sale_report_margin/__manifest__.py @@ -10,5 +10,6 @@ "category": "Sales", "license": "AGPL-3", "depends": ["sale_margin"], + "data": ["views/sale_report_views.xml"], "installable": True, } diff --git a/sale_report_margin/readme/DESCRIPTION.md b/sale_report_margin/readme/DESCRIPTION.md index 0eabbb3c0..2956b587d 100644 --- a/sale_report_margin/readme/DESCRIPTION.md +++ b/sale_report_margin/readme/DESCRIPTION.md @@ -4,3 +4,6 @@ Adds margin information to the sales analysis report: currency. It comes from the *Cost* field configured in the products themselves and on the defined inventory valuation method. - `Margin (%)`: the margin over the untaxed total. + +The purchase price, margin and margin percentage are also shown as columns +in the list view of the report. diff --git a/sale_report_margin/readme/USAGE.md b/sale_report_margin/readme/USAGE.md index ffa0b8c01..99fd291f3 100644 --- a/sale_report_margin/readme/USAGE.md +++ b/sale_report_margin/readme/USAGE.md @@ -1,2 +1,5 @@ Go to *Sales \> Reporting \> Sales* and you'll find the new *Purchase Price* and *Margin (%)* measures. + +In the list view, the *Purchase Price* column is displayed next to the +unit price, and the *Margin* and *Margin (%)* columns next to the total. diff --git a/sale_report_margin/static/description/index.html b/sale_report_margin/static/description/index.html index 8f8b5401d..7a108e136 100644 --- a/sale_report_margin/static/description/index.html +++ b/sale_report_margin/static/description/index.html @@ -382,6 +382,8 @@

    Sale Report Margin

    themselves and on the defined inventory valuation method.
  • Margin (%): the margin over the untaxed total.
+

The purchase price, margin and margin percentage are also shown as +columns in the list view of the report.

Table of contents

    @@ -400,6 +402,8 @@

    Sale Report Margin

    Usage

    Go to Sales > Reporting > Sales and you’ll find the new Purchase Price and Margin (%) measures.

    +

    In the list view, the Purchase Price column is displayed next to the +unit price, and the Margin and Margin (%) columns next to the total.

Known issues / Roadmap

diff --git a/sale_report_margin/views/sale_report_views.xml b/sale_report_margin/views/sale_report_views.xml new file mode 100644 index 000000000..6f8cb5e59 --- /dev/null +++ b/sale_report_margin/views/sale_report_views.xml @@ -0,0 +1,25 @@ + + + + + sale.report + + + + + + + + + + + +