From 28a9fba05f189c75b92f1b91e14bcb707f277f44 Mon Sep 17 00:00:00 2001 From: svs-odoo Date: Wed, 27 Sep 2023 15:43:25 +0200 Subject: [PATCH] [IMP] product,purchase: compute price with discount Add two computed fields, `price_discounted` on `product.supplierinfo` and `price_unit_discounted` on `purchase.order.line`, to get more precise price. For the vendor pricelist, it's useful to get the actual lower price (a vendor price can be lower than another one but the result can be different if the discount is take in account). For the purchase order line, it's useful to get the right price in the products catalog. task-3373589 closes odoo/odoo#135502 Related: odoo/upgrade#5223 Related: odoo/enterprise#48375 Signed-off-by: Arnold Moyaux (arm) --- addons/product/models/product_product.py | 10 +++- addons/product/models/product_supplierinfo.py | 6 ++ addons/purchase/models/purchase_order.py | 6 +- addons/purchase/models/purchase_order_line.py | 8 ++- .../purchase/tests/test_purchase_invoice.py | 55 +++++++++++++++++++ 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/addons/product/models/product_product.py b/addons/product/models/product_product.py index 5a607058936..01fa220e3c8 100644 --- a/addons/product/models/product_product.py +++ b/addons/product/models/product_product.py @@ -3,6 +3,7 @@ import re from collections import defaultdict +from operator import itemgetter from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError @@ -676,13 +677,18 @@ class ProductProduct(models.Model): sellers |= seller return sellers - def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, ordered_by='price', params=False): + def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, ordered_by='price_discounted', params=False): + # Always sort by discounted price but another field can take the primacy through the `ordered_by` param. + sort_key = itemgetter('price_discounted', 'sequence', 'id') + if ordered_by != 'price_discounted': + sort_key = itemgetter(ordered_by, 'price_discounted', 'sequence', 'id') + sellers = self._get_filtered_sellers(partner_id=partner_id, quantity=quantity, date=date, uom_id=uom_id, params=params) res = self.env['product.supplierinfo'] for seller in sellers: if not res or res.partner_id == seller.partner_id: res |= seller - return res and res.sorted(ordered_by)[:1] + return res and res.sorted(sort_key)[:1] def _get_product_price_context(self, combination): self.ensure_one() diff --git a/addons/product/models/product_supplierinfo.py b/addons/product/models/product_supplierinfo.py index 5545895383e..6d27f248026 100644 --- a/addons/product/models/product_supplierinfo.py +++ b/addons/product/models/product_supplierinfo.py @@ -39,6 +39,7 @@ class SupplierInfo(models.Model): price = fields.Float( 'Price', default=0.0, digits='Product Price', required=True, help="The price to purchase a product") + price_discounted = fields.Float('Discounted Price', compute='_compute_price_discounted') company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env.company.id, index=1) @@ -67,6 +68,11 @@ class SupplierInfo(models.Model): digits='Discount', readonly=False) + @api.depends('discount', 'price') + def _compute_price_discounted(self): + for rec in self: + rec.price_discounted = rec.price * (1 - rec.discount / 100) + @api.onchange('product_tmpl_id') def _onchange_product_tmpl_id(self): if self.product_id and self.product_tmpl_id and self.product_id not in self.product_tmpl_id.product_variant_ids: diff --git a/addons/purchase/models/purchase_order.py b/addons/purchase/models/purchase_order.py index 5dd2203c37f..3ffcc6f5a54 100644 --- a/addons/purchase/models/purchase_order.py +++ b/addons/purchase/models/purchase_order.py @@ -877,7 +877,7 @@ class PurchaseOrder(models.Model): ) if seller: product_infos.update( - price=seller.price, + price=seller.price_discounted, min_qty=seller.min_qty, ) # Check if the product uses some packaging. @@ -990,8 +990,8 @@ class PurchaseOrder(models.Model): uom_id=pol.product_uom) if seller: # Fix the PO line's price on the seller's one. - pol.price_unit = seller.price - return pol.price_unit + pol.price_unit = seller.price_discounted + return pol.price_unit_discounted def _create_update_date_activity(self, updated_dates): note = Markup('

%s

\n') % _('%s modified receipt dates for the following products:', self.partner_id.name) diff --git a/addons/purchase/models/purchase_order_line.py b/addons/purchase/models/purchase_order_line.py index 989e640a5ff..820fac3707b 100644 --- a/addons/purchase/models/purchase_order_line.py +++ b/addons/purchase/models/purchase_order_line.py @@ -38,6 +38,7 @@ class PurchaseOrderLine(models.Model): price_unit = fields.Float( string='Unit Price', required=True, digits='Product Price', compute="_compute_price_unit_and_date_planned_and_name", readonly=False, store=True) + price_unit_discounted = fields.Float('Unit Price (Discounted)', compute='_compute_price_unit_discounted') price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) @@ -126,6 +127,11 @@ class PurchaseOrderLine(models.Model): taxes = line.product_id.supplier_taxes_id.filtered_domain(self.env['account.tax']._check_company_domain(line.company_id)) line.taxes_id = fpos.map_tax(taxes) + @api.depends('discount', 'price_unit') + def _compute_price_unit_discounted(self): + for line in self: + line.price_unit_discounted = line.price_unit * (1 - line.discount / 100) + @api.depends('invoice_lines.move_id.state', 'invoice_lines.quantity', 'qty_received', 'product_uom_qty', 'order_id.state') def _compute_qty_invoiced(self): for line in self: @@ -497,7 +503,7 @@ class PurchaseOrderLine(models.Model): } catalog_info.update( quantity=self.product_qty, - price=self.price_unit, + price=self.price_unit * (1 - self.discount / 100), readOnly=self.order_id._is_readonly(), uom=uom, ) diff --git a/addons/purchase/tests/test_purchase_invoice.py b/addons/purchase/tests/test_purchase_invoice.py index f7d58d56917..75b39fdb0bf 100644 --- a/addons/purchase/tests/test_purchase_invoice.py +++ b/addons/purchase/tests/test_purchase_invoice.py @@ -648,6 +648,61 @@ class TestPurchaseToInvoice(TestPurchaseToInvoiceCommon): po_line.write({'product_qty': 4}) self.assertEqual(40.0, po_line.price_unit, "Unit price should be set to 40.0 for 4 quantity") + def test_supplier_discounted_price(self): + """ Check the lower price (discount included) is used. + """ + uom_dozen = self.env.ref('uom.product_uom_dozen') + supplierinfo_common_vals = { + 'partner_id': self.partner_a.id, + 'product_id': self.product_order.id, + 'product_tmpl_id': self.product_order.product_tmpl_id.id, + } + supplierinfo = self.env['product.supplierinfo'].create([ + { + **supplierinfo_common_vals, + 'price': 100.0, + 'discount': 0, # Real price by unit: 100.00 + }, { + **supplierinfo_common_vals, + 'price': 120.0, + 'discount': 20, # Real price by unit: 96.00 + }, { + **supplierinfo_common_vals, + 'min_qty': 10, + 'price': 140.0, + 'discount': 50, # Real price by unit: 70.00 + }, + ]) + self.assertRecordValues(supplierinfo, [ + {'price_discounted': 100}, + {'price_discounted': 96}, + {'price_discounted': 70}, + ]) + + po_form = Form(self.env['purchase.order']) + po_form.partner_id = self.partner_a + with po_form.order_line.new() as po_line_form: + po_line_form.product_id = self.product_order + po_line_form.product_qty = 1 + po = po_form.save() + po_line = po.order_line[0] + self.assertEqual(120.0, po_line.price_unit) + self.assertEqual(20, po_line.discount) + self.assertEqual(96.0, po_line.price_unit_discounted) + self.assertEqual(96.0, po_line.price_subtotal) + + # Increase the PO line quantity: it should take another price if min. qty. is reached. + po_form = Form(po) + with po_form.order_line.edit(0) as po_line_form: + po_line_form.product_uom = uom_dozen + po_line_form.product_qty = 3 + po = po_form.save() + po_line = po.order_line[0] + + self.assertEqual(1680.0, po_line.price_unit, "140.0 * 12 = 1680.0") + self.assertEqual(50, po_line.discount) + self.assertEqual(840.0, po_line.price_unit_discounted, "1680.0 * 0.5 = 840.0") + self.assertEqual(2520.0, po_line.price_subtotal, "840.0 * 3 = 2520.0") @tagged('post_install', '-at_install') class TestInvoicePurchaseMatch(TestPurchaseToInvoiceCommon):