[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) <arm@odoo.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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('<p>%s</p>\n') % _('%s modified receipt dates for the following products:', self.partner_id.name)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user