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
21 changes: 17 additions & 4 deletions sale_report_margin/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -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
======================
Expand Down Expand Up @@ -81,6 +90,10 @@ Contributors

- SodexisTeam <dev@sodexis.com>

- `Camptocamp <https://www.camptocamp.com>`__:

- Iván Todorovich <ivan.todorovich@camptocamp.com>

Maintainers
-----------

Expand Down
1 change: 1 addition & 0 deletions sale_report_margin/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"category": "Sales",
"license": "AGPL-3",
"depends": ["sale_margin"],
"data": ["views/sale_report_views.xml"],
"installable": True,
}
6 changes: 5 additions & 1 deletion sale_report_margin/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
> - David Vidal
> - Carlos Roca

- SodexisTeam \<<dev@sodexis.com>\>
- SodexisTeam \<<dev@sodexis.com>\>

- [Camptocamp](https://www.camptocamp.com):

> - Iván Todorovich \<<ivan.todorovich@camptocamp.com>\>
12 changes: 9 additions & 3 deletions sale_report_margin/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion sale_report_margin/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 12 additions & 2 deletions sale_report_margin/reports/sale_report.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
23 changes: 19 additions & 4 deletions sale_report_margin/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,15 @@ <h1>Sale Report Margin</h1>
!! source digest: sha256:3ba588ce7e07b01b5bc3c5c65dcb27d213765f309a80e4222d264c3b85fccef6
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.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/margin-analysis/tree/19.0/sale_report_margin"><img alt="OCA/margin-analysis" src="https://img.shields.io/badge/github-OCA%2Fmargin--analysis-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/margin-analysis-19-0/margin-analysis-19-0-sale_report_margin"><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/margin-analysis&amp;target_branch=19.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Adds purchase_price field to sale report. This field comes from the
<em>Cost</em> field configured in the products themselves and on the defined
inventory valuation method.</p>
<p>Adds margin information to the sales analysis report:</p>
<ul class="simple">
<li><tt class="docutils literal">Purchase Price</tt>: the unit cost of the sold products, in the company
currency. It comes from the <em>Cost</em> field configured in the products
themselves and on the defined inventory valuation method.</li>
<li><tt class="docutils literal">Margin (%)</tt>: the margin over the untaxed total.</li>
</ul>
<p>The purchase price, margin and margin percentage are also shown as
columns in the list view of the report.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
Expand All @@ -395,7 +401,9 @@ <h1>Sale Report Margin</h1>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>Go to <em>Sales &gt; Reporting &gt; Sales</em> and you’ll find the new <em>Purchase
Price</em> measure.</p>
Price</em> and <em>Margin (%)</em> measures.</p>
<p>In the list view, the <em>Purchase Price</em> column is displayed next to the
unit price, and the <em>Margin</em> and <em>Margin (%)</em> columns next to the total.</p>
</div>
<div class="section" id="known-issues-roadmap">
<h2><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h2>
Expand Down Expand Up @@ -433,6 +441,13 @@ <h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
</li>
<li><p class="first">SodexisTeam &lt;<a class="reference external" href="mailto:dev&#64;sodexis.com">dev&#64;sodexis.com</a>&gt;</p>
</li>
<li><p class="first"><a class="reference external" href="https://www.camptocamp.com">Camptocamp</a>:</p>
<blockquote>
<ul class="simple">
<li>Iván Todorovich &lt;<a class="reference external" href="mailto:ivan.todorovich&#64;camptocamp.com">ivan.todorovich&#64;camptocamp.com</a>&gt;</li>
</ul>
</blockquote>
</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
94 changes: 93 additions & 1 deletion sale_report_margin/tests/test_sale_report_margin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2023 Álvaro Marcos <alvaro.marcos@factorlibre.com>
# 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
Expand All @@ -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",
Expand All @@ -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)
25 changes: 25 additions & 0 deletions sale_report_margin/views/sale_report_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2026 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="sale_report_view_tree" model="ir.ui.view">
<field name="model">sale.report</field>
<field name="inherit_id" ref="sale.sale_report_view_tree" />
<field name="arch" type="xml">
<field name="price_unit" position="after">
<field name="purchase_price" optional="show" widget="monetary" />
</field>
<field name="price_total" position="after">
<field
name="margin"
optional="show"
widget="monetary"
sum="Sum of Margin"
/>
<field name="margin_percent" optional="show" widget="percentage" />
</field>
</field>
</record>
</odoo>
Loading