diff --git a/sale_report_margin/README.rst b/sale_report_margin/README.rst index 52c56a659..142d37de9 100644 --- a/sale_report_margin/README.rst +++ b/sale_report_margin/README.rst @@ -32,9 +32,15 @@ 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 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. + +The purchase price, margin and margin percentage are also shown as +columns in the list view of the report. **Table of contents** @@ -45,7 +51,10 @@ Usage ===== Go to *Sales > Reporting > Sales* and you'll find the new *Purchase -Price* measure. +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 ====================== @@ -81,6 +90,10 @@ Contributors - SodexisTeam +- `Camptocamp `__: + + - Iván Todorovich + Maintainers ----------- 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/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..2956b587d 100644 --- a/sale_report_margin/readme/DESCRIPTION.md +++ b/sale_report_margin/readme/DESCRIPTION.md @@ -1,3 +1,9 @@ -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 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. + +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 833ee81d6..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* measure. +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/reports/sale_report.py b/sale_report_margin/reports/sale_report.py index be3f576e6..0dab34372 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,18 @@ class SaleReport(models.Model): _inherit = "sale.report" - purchase_price = fields.Float(readonly=True) + 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() - 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")})""" + # 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 d84f66ba8..7a108e136 100644 --- a/sale_report_margin/static/description/index.html +++ b/sale_report_margin/static/description/index.html @@ -375,9 +375,15 @@

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 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.
  • +
+

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

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..f8ad709fc 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 @@ -15,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", @@ -40,3 +48,87 @@ 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): + """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, 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 + 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) + self.assertEqual(report.margin, 1.0) + self.assertEqual(report.margin_percent, 0.2) 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 + + + + + + + + + + + +