From dad19a769a1123538d9717ff9b9134d97d3ff194 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Tue, 10 Jul 2018 15:33:14 +0200 Subject: [PATCH 1/2] [FIX] sale: Price on SO not working with different uom Steps to reproduce the bug: - configuration on sale: - Unit of measure: Some products may be sold/purchased in different units of measure (Advanced) - Sale Price: Advaced pricing based on formulas (discounts, margins, rounding) - Discount: Allow discounts on sale order lines - kg as the Reference UOM for weight - create an uom 10 kg - Category: Weight - Type: Bigger... - Bigger ratio: 10.0 - create a product P - Sale price: 100 - Unit of measure: kg - Purchase UOM: kg - create a pricelist "My pricelist" - Discount policy: Show public price & discount to the customer - Items : All products - 10% discount - create a SO with "My pricelist" - create a SO line with P, the unit price is automatically 90 with 10 % of discount - change the uom of the line with 10kg Bug: The unit price of the line didn't change and the discount on the line disappeared. In function product_uom_change, the uom of the SO line has been set in the context of the product(like in function product_id_change). So in funciton _get_display_price, the function _get_real_price_currency must be called with the uom of product in the context. In this way, the lst_price will be fetch on product with the right uom without any bad conversion (because uom.id == product_uom with uom from the parameters and product_uom from the context). But with function get_product_price_rule, no need to call this function with the uom in context as it is already set in the product(check "if uom_id: ... else ..." in this function). The same logic has been applied in function _onchange_discount opw:186385 --- addons/sale/models/sale.py | 27 ++-- addons/sale/tests/test_product_id_change.py | 155 ++++++++++++++++++++ 2 files changed, 173 insertions(+), 9 deletions(-) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 190b7535b4f..633a2d0ff41 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -865,11 +865,11 @@ class SaleOrderLine(models.Model): # TO DO: move me in master/saas-16 on sale.order if self.order_id.pricelist_id.discount_policy == 'with_discount': return product.with_context(pricelist=self.order_id.pricelist_id.id).price - final_price, rule_id = self.order_id.pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order) - base_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id) + final_price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + base_price, currency_id = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) if currency_id != self.order_id.pricelist_id.currency_id.id: - base_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(base_price, self.order_id.pricelist_id.currency_id) + base_price = self.env['res.currency'].browse(currency_id).with_context(product_context).compute(base_price, self.order_id.pricelist_id.currency_id) # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) @@ -1014,16 +1014,25 @@ class SaleOrderLine(models.Model): self.env.user.has_group('sale.group_discount_per_so_line')): return - context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order) - pricelist_context = dict(context_partner, uom=self.product_uom.id) + product = self.product_id.with_context( + lang=self.order_id.partner_id.lang, + partner=self.order_id.partner_id.id, + quantity=self.product_uom_qty, + date=self.order_id.date_order, + pricelist=self.order_id.pricelist_id.id, + uom=self.product_uom.id, + fiscal_position=self.env.context.get('fiscal_position') + ) - price, rule_id = self.order_id.pricelist_id.with_context(pricelist_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id) + + price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + new_list_price, currency_id = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) if new_list_price != 0: if self.order_id.pricelist_id.currency_id.id != currency_id: # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency - new_list_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(new_list_price, self.order_id.pricelist_id.currency_id) + new_list_price = self.env['res.currency'].browse(currency_id).with_context(product_context).compute(new_list_price, self.order_id.pricelist_id.currency_id) discount = (new_list_price - price) / new_list_price * 100 if discount > 0: self.discount = discount diff --git a/addons/sale/tests/test_product_id_change.py b/addons/sale/tests/test_product_id_change.py index bf61c8d644f..7d440c4c709 100644 --- a/addons/sale/tests/test_product_id_change.py +++ b/addons/sale/tests/test_product_id_change.py @@ -116,3 +116,158 @@ class TestOnchangeProductId(TransactionCase): so.date_order = '2017-12-30' order_line.product_id_change() self.assertEqual(order_line.price_unit, 50, "Second date pricelist rule not applied") + + def test_pricelist_uom_discount(self): + """ Test prices and discounts are correctly applied based on date and uom""" + computer_case = self.env.ref('product.product_product_16') + computer_case.list_price = 100 + partner = self.res_partner_model.create(dict(name="George")) + categ_unit_id = self.ref('product.product_uom_unit') + goup_discount_id = self.ref('sale.group_discount_per_so_line') + self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]}) + new_uom = self.env['product.uom'].create({ + 'name': '10 units', + 'factor_inv': 10, + 'uom_type': 'bigger', + 'rounding': 1.0, + 'category_id': categ_unit_id + }) + christmas_pricelist = self.env['product.pricelist'].create({ + 'name': 'Christmas pricelist', + 'discount_policy': 'without_discount', + 'item_ids': [(0, 0, { + 'date_start': "2017-12-01", + 'date_end': "2017-12-30", + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 10, + 'applied_on': '3_global', + 'name': 'Christmas discount' + })] + }) + + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'date_order': '2017-12-20', + 'pricelist_id': christmas_pricelist.id, + }) + + order_line = self.env['sale.order.line'].new({ + 'order_id': so.id, + 'product_id': computer_case.id, + }) + + # force compute uom and prices + order_line.product_id_change() + order_line.product_uom_change() + order_line._onchange_discount() + self.assertEqual(order_line.price_subtotal, 90, "Christmas discount pricelist rule not applied") + self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%") + order_line.product_uom = new_uom + order_line.product_uom_change() + order_line._onchange_discount() + self.assertEqual(order_line.price_subtotal, 900, "Christmas discount pricelist rule not applied") + self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%") + + def test_pricelist_based_on_other(self): + """ Test price and discount are correctly applied with a pricelist based on an other one""" + computer_case = self.env.ref('product.product_product_16') + computer_case.list_price = 100 + partner = self.res_partner_model.create(dict(name="George")) + categ_unit_id = self.ref('product.product_uom_unit') + goup_discount_id = self.ref('sale.group_discount_per_so_line') + self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]}) + + first_pricelist = self.env['product.pricelist'].create({ + 'name': 'First pricelist', + 'discount_policy': 'without_discount', + 'item_ids': [(0, 0, { + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 10, + 'applied_on': '3_global', + 'name': 'First discount' + })] + }) + + second_pricelist = self.env['product.pricelist'].create({ + 'name': 'Second pricelist', + 'discount_policy': 'without_discount', + 'item_ids': [(0, 0, { + 'compute_price': 'formula', + 'base': 'pricelist', + 'base_pricelist_id': first_pricelist.id, + 'price_discount': 10, + 'applied_on': '3_global', + 'name': 'Second discount' + })] + }) + + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'date_order': '2018-07-11', + 'pricelist_id': second_pricelist.id, + }) + + order_line = self.env['sale.order.line'].new({ + 'order_id': so.id, + 'product_id': computer_case.id, + }) + + # force compute uom and prices + order_line.product_id_change() + order_line._onchange_discount() + self.assertEqual(order_line.price_subtotal, 81, "Second pricelist rule not applied") + self.assertEqual(order_line.discount, 19, "Second discount not applied") + + def test_pricelist_with_other_currency(self): + """ Test prices are correctly applied with a pricelist with an other currency""" + computer_case = self.env.ref('product.product_product_16') + computer_case.list_price = 100 + partner = self.res_partner_model.create(dict(name="George")) + categ_unit_id = self.ref('product.product_uom_unit') + other_currency = self.env['res.currency'].create({'name': 'other currency', + 'symbol': 'other'}) + self.env['res.currency.rate'].create({'name': '2018-07-11', + 'rate': 2.0, + 'currency_id': other_currency.id, + 'company_id': self.env.user.company_id.id}) + new_uom = self.env['product.uom'].create({ + 'name': '10 units', + 'factor_inv': 10, + 'uom_type': 'bigger', + 'rounding': 1.0, + 'category_id': categ_unit_id + }) + + # This pricelist doesn't show the discount + first_pricelist = self.env['product.pricelist'].create({ + 'name': 'First pricelist', + 'currency_id': other_currency.id, + 'discount_policy': 'with_discount', + 'item_ids': [(0, 0, { + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 10, + 'applied_on': '3_global', + 'name': 'First discount' + })] + }) + + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'date_order': '2018-07-12', + 'pricelist_id': first_pricelist.id, + }) + + order_line = self.env['sale.order.line'].new({ + 'order_id': so.id, + 'product_id': computer_case.id, + }) + + # force compute uom and prices + order_line.product_id_change() + self.assertEqual(order_line.price_unit, 180, "First pricelist rule not applied") + order_line.product_uom = new_uom + order_line.product_uom_change() + self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied") From 755081cbf4d3df629a33565fc4e59bb4d020ad52 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Thu, 12 Jul 2018 11:46:43 +0200 Subject: [PATCH 2/2] [FIX] account: reconciliation widget rounding Set the rounding method to Round Globally Create a tax A with rate = 1.1040 % Create a bank statement with one line having amount = 1153.960 Go on to reconcile that bank statement Reconcile with a new line on which the tax A applies Create the write-off Click on Reconcile Before this commit, an error was thrown by the server (Move unbalanced) This was because the amount of the write-off line sent by JS (which is the tax amount) was 12.7397 The server though computes the taxes by forcing the rounding and gives off a tax amount of 12.74 Hence the error After this commit, the JS will fetch the amount of the taxes by forcing the rounding there also And the reconciliation process goes well This fix is already present in v11.0 at 6fce298d7013379c4383e8368c40ee06d6146423 OPW 1859939 closes #25733 --- addons/account/static/src/js/account_reconciliation_widgets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js index a2b0f66d9b7..5d00984157c 100644 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ b/addons/account/static/src/js/account_reconciliation_widgets.js @@ -1000,7 +1000,7 @@ var abstractReconciliationLine = Widget.extend({ var tax_id = self.tax_id_field.get("value"); if (amount && tax_id) { deferred_tax = self.model_tax - .call("json_friendly_compute_all", [[tax_id], amount, self.get("currency_id")]) + .call("json_friendly_compute_all", [[tax_id], amount, self.get("currency_id")], {context: {round: true}}) // just as the python will do .then(function(data){ line_created_being_edited.length = 1; // remove tax lines line_created_being_edited[0].amount_before_tax = amount;