From a5ebcccd346148aa17aec1185f2c3495354ea1d2 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 18 Jun 2024 10:16:29 +0200 Subject: [PATCH 1/4] [FIX] sale_margin_delivered: Uses the right field to get price reduce 'price_reduce' is deprecated and removed into the next version. Compute the prirce_reduct from the price_subotal / product_uom_qty. We might be tempted to use the 'price_reduce_taxecl' field from the sale order line but this field is rounded by default to the monetary precision. As an additional benefit this change ensures the compatibility with the 'sale_triple_discount' addon. Indeed, when 'sale_triple_discount' is installed, the discount field is not used as an aggregation of all the applied discount. It's only use to store the first discount applied. Therefore, the field is not properly computed since it doesn't include the second and third discount. --- sale_margin_delivered/models/sale_margin.py | 12 +++++++---- .../tests/test_sale_margin_delivered.py | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/sale_margin_delivered/models/sale_margin.py b/sale_margin_delivered/models/sale_margin.py index 50ceb550..d5fa82a4 100644 --- a/sale_margin_delivered/models/sale_margin.py +++ b/sale_margin_delivered/models/sale_margin.py @@ -72,6 +72,10 @@ def _compute_margin_delivered(self): self.margin_delivered_percent = 0.0 self.purchase_price_delivery = 0.0 for line in self.filtered("qty_delivered"): + # we need to compute the price reduce to avoid rounding issues + # the one stored in the line is rounded to the product price precision + price_reduce_taxexcl = line.price_subtotal / line.product_uom_qty + if line.product_id.type != "product": currency = line.order_id.pricelist_id.currency_id price = line.purchase_price @@ -91,13 +95,13 @@ def _compute_margin_delivered(self): ) # Inverse qty_delivered because outgoing quantities are negative line.margin_delivered = -qty_delivered * ( - line.price_reduce - line.purchase_price_delivery + price_reduce_taxexcl - line.purchase_price_delivery ) # compute percent margin based on delivered quantities or ordered # quantities - if line.price_reduce: + if price_reduce_taxexcl: line.margin_delivered_percent = ( - (line.price_reduce - line.purchase_price_delivery) - / line.price_reduce + (price_reduce_taxexcl - line.purchase_price_delivery) + / price_reduce_taxexcl * 100.0 ) diff --git a/sale_margin_delivered/tests/test_sale_margin_delivered.py b/sale_margin_delivered/tests/test_sale_margin_delivered.py index bd0c5f9c..34ec09ac 100644 --- a/sale_margin_delivered/tests/test_sale_margin_delivered.py +++ b/sale_margin_delivered/tests/test_sale_margin_delivered.py @@ -183,3 +183,23 @@ def test_sale_margin_delivered_return_no_refund_excess(self): self.assertEqual(order_line.margin_delivered, 120.0) self.assertEqual(order_line.margin_delivered_percent, 50.0) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_delivered_precision(self): + self.product.standard_price = 10.30 + self.product.list_price = 20.17 + sale_order = self._new_sale_order() + sale_order.order_line[:1].discount = 17.0 + sale_order.action_confirm() + picking = sale_order.picking_ids + picking.action_assign() + picking.move_line_ids.qty_done = 6.0 + picking._action_done() + order_line = sale_order.order_line[:1] + # price_subtotal is rounded + self.assertEqual(order_line.price_subtotal, 100.45) + # the unit reduce price will be computed as 100.45 / 6 = 16.741666666666667 + # it should not be rounded to 16.74 + # margin_delivered: round(6 * ((100.45 /6) - 10.30)) != round(6 * (16.74 - 10.30)) + self.assertEqual(order_line.margin_delivered, 38.65) + self.assertAlmostEqual(order_line.margin_delivered_percent, 38.47685415629666) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) From 6f36c4f061ebf8d231d3088faae83268e1263667 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 19 Jun 2024 15:55:39 +0200 Subject: [PATCH 2/4] [IMP] sale_margin_delivered: Improve UI Put all the new fields after the orginal margin fields from odoo. (prior to this change, the new fields were displayed among the margin fields from Odoo --- sale_margin_delivered/views/sale_margin_delivered_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sale_margin_delivered/views/sale_margin_delivered_view.xml b/sale_margin_delivered/views/sale_margin_delivered_view.xml index 696a40c3..2ca7cf5f 100644 --- a/sale_margin_delivered/views/sale_margin_delivered_view.xml +++ b/sale_margin_delivered/views/sale_margin_delivered_view.xml @@ -6,7 +6,7 @@ sale.order - + Date: Wed, 19 Jun 2024 16:37:55 +0200 Subject: [PATCH 3/4] [IMP] sale_margin_delivered: Store margin delivered percent as a fration of 1 As it's done into the sale_margin addon from odoo --- sale_margin_delivered/__manifest__.py | 2 +- .../migrations/16.0.2.0.0/post-migration.py | 15 ++++++++++ sale_margin_delivered/models/sale_margin.py | 8 ++--- .../static/description/index.html | 11 ++++--- .../tests/test_sale_margin_delivered.py | 29 ++++++++++++++----- .../views/sale_margin_delivered_view.xml | 2 ++ .../test_sale_margin_delivered_dropship.py | 2 +- 7 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 sale_margin_delivered/migrations/16.0.2.0.0/post-migration.py diff --git a/sale_margin_delivered/__manifest__.py b/sale_margin_delivered/__manifest__.py index e8682ada..d056f316 100644 --- a/sale_margin_delivered/__manifest__.py +++ b/sale_margin_delivered/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Sale Margin Delivered", - "version": "16.0.1.0.5", + "version": "16.0.2.0.0", "author": "Tecnativa, Odoo Community Association (OCA)", "website": "https://github.com/OCA/margin-analysis", "category": "Sales", diff --git a/sale_margin_delivered/migrations/16.0.2.0.0/post-migration.py b/sale_margin_delivered/migrations/16.0.2.0.0/post-migration.py new file mode 100644 index 00000000..64d5bf8a --- /dev/null +++ b/sale_margin_delivered/migrations/16.0.2.0.0/post-migration.py @@ -0,0 +1,15 @@ +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version=None): + """Store margin delivered precentage as a fraction of 1""" + cr.execute( + """ + UPDATE sale_order_line + SET margin_delivered_percent = margin_delivered_percent / 100 + WHERE margin_delivered_percent > 0 + """ + ) + _logger.info("Updated %d records", cr.rowcount) diff --git a/sale_margin_delivered/models/sale_margin.py b/sale_margin_delivered/models/sale_margin.py index d5fa82a4..5eb8ce58 100644 --- a/sale_margin_delivered/models/sale_margin.py +++ b/sale_margin_delivered/models/sale_margin.py @@ -22,7 +22,7 @@ class SaleOrderLine(models.Model): help="Margin percent between the Unit Price with discounts and " "Delivered Unit Cost.\n\n" "Formula: ((Unit Price with Discounts - Average Unit Cost of " - "delivered products) / Unit Price with Discounts) * 100.0", + "delivered products) / Unit Price with Discounts)", ) purchase_price_delivery = fields.Float( compute="_compute_margin_delivered", @@ -101,7 +101,5 @@ def _compute_margin_delivered(self): # quantities if price_reduce_taxexcl: line.margin_delivered_percent = ( - (price_reduce_taxexcl - line.purchase_price_delivery) - / price_reduce_taxexcl - * 100.0 - ) + price_reduce_taxexcl - line.purchase_price_delivery + ) / price_reduce_taxexcl diff --git a/sale_margin_delivered/static/description/index.html b/sale_margin_delivered/static/description/index.html index ed03cc6b..746e3daa 100644 --- a/sale_margin_delivered/static/description/index.html +++ b/sale_margin_delivered/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -452,7 +453,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/sale_margin_delivered/tests/test_sale_margin_delivered.py b/sale_margin_delivered/tests/test_sale_margin_delivered.py index 34ec09ac..bd9ebb59 100644 --- a/sale_margin_delivered/tests/test_sale_margin_delivered.py +++ b/sale_margin_delivered/tests/test_sale_margin_delivered.py @@ -91,7 +91,7 @@ def test_sale_margin_delivered(self): picking._action_done() order_line = sale_order.order_line[:1] self.assertEqual(order_line.margin_delivered, 30.0) - self.assertEqual(order_line.margin_delivered_percent, 50.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) def test_sale_margin_delivered_excess(self): @@ -104,7 +104,7 @@ def test_sale_margin_delivered_excess(self): picking._action_done() order_line = sale_order.order_line[:1] self.assertEqual(order_line.margin_delivered, 120.0) - self.assertEqual(order_line.margin_delivered_percent, 50.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) def test_sale_margin_zero(self): @@ -142,7 +142,7 @@ def test_sale_margin_delivered_return_to_refund(self): picking_return._action_done() order_line = sale_order.order_line[:1] self.assertEqual(order_line.margin_delivered, 30.0) - self.assertEqual(order_line.margin_delivered_percent, 50.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) def test_sale_margin_delivered_return_to_refund_excess(self): @@ -155,7 +155,7 @@ def test_sale_margin_delivered_return_to_refund_excess(self): picking_return._action_done() order_line = sale_order.order_line[:1] self.assertEqual(order_line.margin_delivered, 90.0) - self.assertEqual(order_line.margin_delivered_percent, 50.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) def test_sale_margin_delivered_return_no_refund(self): @@ -168,7 +168,7 @@ def test_sale_margin_delivered_return_no_refund(self): picking_return._action_done() order_line = sale_order.order_line[:1] self.assertEqual(order_line.margin_delivered, 60.0) - self.assertEqual(order_line.margin_delivered_percent, 50.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) def test_sale_margin_delivered_return_no_refund_excess(self): @@ -181,7 +181,7 @@ def test_sale_margin_delivered_return_no_refund_excess(self): picking_return._action_done() order_line = sale_order.order_line[:1] self.assertEqual(order_line.margin_delivered, 120.0) - self.assertEqual(order_line.margin_delivered_percent, 50.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) def test_sale_margin_delivered_precision(self): @@ -201,5 +201,20 @@ def test_sale_margin_delivered_precision(self): # it should not be rounded to 16.74 # margin_delivered: round(6 * ((100.45 /6) - 10.30)) != round(6 * (16.74 - 10.30)) self.assertEqual(order_line.margin_delivered, 38.65) - self.assertAlmostEqual(order_line.margin_delivered_percent, 38.47685415629666) + self.assertAlmostEqual(order_line.margin_delivered_percent, 0.38476854156296) + self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) + + def test_sale_margin_no_cost(self): + self.product.standard_price = 0.0 + self.product.list_price = 20 + sale_order = self._new_sale_order() + sale_order.action_confirm() + picking = sale_order.picking_ids + picking.action_assign() + picking.move_line_ids.qty_done = 6.0 + picking._action_done() + order_line = sale_order.order_line[:1] + # price_subtotal is rounded + self.assertEqual(order_line.margin_delivered, 120) + self.assertAlmostEqual(order_line.margin_delivered_percent, 1) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) diff --git a/sale_margin_delivered/views/sale_margin_delivered_view.xml b/sale_margin_delivered/views/sale_margin_delivered_view.xml index 2ca7cf5f..b07662ac 100644 --- a/sale_margin_delivered/views/sale_margin_delivered_view.xml +++ b/sale_margin_delivered/views/sale_margin_delivered_view.xml @@ -16,6 +16,8 @@ name="margin_delivered_percent" string="Margin dlvd. (%)" optional="hide" + attrs="{'invisible': [('price_subtotal', '=', 0)]}" + widget="percentage" /> diff --git a/sale_margin_delivered_dropshipping/tests/test_sale_margin_delivered_dropship.py b/sale_margin_delivered_dropshipping/tests/test_sale_margin_delivered_dropship.py index 296d1d88..b986bfab 100644 --- a/sale_margin_delivered_dropshipping/tests/test_sale_margin_delivered_dropship.py +++ b/sale_margin_delivered_dropshipping/tests/test_sale_margin_delivered_dropship.py @@ -48,5 +48,5 @@ def test_sale_margin_delivered_dropship(self): picking_return._action_done() order_line = sale_order.order_line[:1] self.assertEqual(order_line.margin_delivered, 30.0) - self.assertEqual(order_line.margin_delivered_percent, 50.0) + self.assertEqual(order_line.margin_delivered_percent, 0.5) self.assertEqual(order_line.purchase_price_delivery, order_line.purchase_price) From 40f3658905c4488712619d2a8ad46f17123fd601 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 20 Jun 2024 10:08:54 +0200 Subject: [PATCH 4/4] [FIX] sale_margin_analysis: Avoid ZeroDivisionError --- sale_margin_delivered/models/sale_margin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sale_margin_delivered/models/sale_margin.py b/sale_margin_delivered/models/sale_margin.py index 5eb8ce58..29682716 100644 --- a/sale_margin_delivered/models/sale_margin.py +++ b/sale_margin_delivered/models/sale_margin.py @@ -74,7 +74,11 @@ def _compute_margin_delivered(self): for line in self.filtered("qty_delivered"): # we need to compute the price reduce to avoid rounding issues # the one stored in the line is rounded to the product price precision - price_reduce_taxexcl = line.price_subtotal / line.product_uom_qty + price_reduce_taxexcl = ( + line.price_subtotal / line.product_uom_qty + if line.product_uom_qty + else 0.0 + ) if line.product_id.type != "product": currency = line.order_id.pricelist_id.currency_id