summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorRémy Baranx (bar) <bar@odoo.com>2020-03-06 08:16:17 +0000
committerYannick Tivisse <yti@odoo.com>2020-03-31 13:47:27 +0000
commit840c18d23095e7e73910aede7853f4d55db602ea (patch)
tree441662c418e7ccb617d116e0b99fb8b8e5556eea /addons
parent532c2b44ff38a29710fa06d1ea62782cc223dc34 (diff)
[IMP] sale_management: remove price_unit from quotation template
Purpose ======= Having price and discount on quotation template is quite confusing as it is more the job of pricelists. In order to make it more simple for the user, we should remove the fields price_unit and discount from quotation template. Implementation ======== Now, the public price of a product is used instead of the price from the quotation template. If a pricelist exists, it is taken into account as before. + Add new tests to check the sale order template behavior. closes odoo/odoo#47186 Taskid: 47186 Related: odoo/upgrade#972 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/sale_management/data/sale_order_template_demo.xml4
-rw-r--r--addons/sale_management/models/sale_order.py45
-rw-r--r--addons/sale_management/models/sale_order_template.py25
-rw-r--r--addons/sale_management/tests/__init__.py1
-rw-r--r--addons/sale_management/tests/test_sale_order.py281
-rw-r--r--addons/sale_management/views/sale_order_template_views.xml9
-rw-r--r--addons/sale_quotation_builder/views/sale_order_template_views.xml2
7 files changed, 312 insertions, 55 deletions
diff --git a/addons/sale_management/data/sale_order_template_demo.xml b/addons/sale_management/data/sale_order_template_demo.xml
index 37b02e50f2d..056d1590a67 100644
--- a/addons/sale_management/data/sale_order_template_demo.xml
+++ b/addons/sale_management/data/sale_order_template_demo.xml
@@ -16,8 +16,6 @@
<field name="product_id" ref="product.consu_delivery_03"/>
<field name="product_uom_qty">1</field>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
- <field name="price_unit">150.0</field>
- <field name="discount">10.00</field>
</record>
<record id="sale_order_template_option_1" model="sale.order.template.option">
@@ -26,7 +24,5 @@
<field name="product_id" ref="product.product_delivery_01"/>
<field name="quantity">4</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
- <field name="price_unit">45.0</field>
- <field name="discount">10.0</field>
</record>
</odoo>
diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py
index a625011cac9..c59b69baea0 100644
--- a/addons/sale_management/models/sale_order.py
+++ b/addons/sale_management/models/sale_order.py
@@ -58,17 +58,24 @@ class SaleOrder(models.Model):
}
def _compute_option_data_for_template_change(self, option):
+ price = option.product_id.lst_price
+ discount = 0
+
if self.pricelist_id:
- price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
- else:
- price = option.price_unit
+ pricelist_price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
+
+ if self.pricelist_id.discount_policy == 'without_discount' and price:
+ discount = max(0, (price - pricelist_price) * 100 / price)
+ else:
+ price = pricelist_price
+
return {
'product_id': option.product_id.id,
'name': option.name,
'quantity': option.quantity,
'uom_id': option.uom_id.id,
'price_unit': price,
- 'discount': option.discount,
+ 'discount': discount
}
def update_prices(self):
@@ -80,49 +87,51 @@ class SaleOrder(models.Model):
@api.onchange('sale_order_template_id')
def onchange_sale_order_template_id(self):
+
if not self.sale_order_template_id:
self.require_signature = self._get_default_require_signature()
self.require_payment = self._get_default_require_payment()
return
+
template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
+ # --- first, process the list of products from the template
order_lines = [(5, 0, 0)]
for line in template.sale_order_template_line_ids:
data = self._compute_line_data_for_template_change(line)
+
if line.product_id:
+ price = line.product_id.lst_price
discount = 0
+
if self.pricelist_id:
- price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False)
- if self.pricelist_id.discount_policy == 'without_discount' and line.price_unit:
- discount = (line.price_unit - price) / line.price_unit * 100
- # negative discounts (= surcharge) are included in the display price
- if discount < 0:
- discount = 0
- else:
- price = line.price_unit
- elif line.price_unit:
- price = line.price_unit
-
- else:
- price = line.price_unit
+ pricelist_price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False)
+
+ if self.pricelist_id.discount_policy == 'without_discount' and price:
+ discount = max(0, (price - pricelist_price) * 100 / price)
+ else:
+ price = pricelist_price
data.update({
'price_unit': price,
- 'discount': 100 - ((100 - discount) * (100 - line.discount) / 100),
+ 'discount': discount,
'product_uom_qty': line.product_uom_qty,
'product_id': line.product_id.id,
'product_uom': line.product_uom_id.id,
'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id),
})
+
order_lines.append((0, 0, data))
self.order_line = order_lines
self.order_line._compute_tax_id()
+ # then, process the list of optional products from the template
option_lines = [(5, 0, 0)]
for option in template.sale_order_template_option_ids:
data = self._compute_option_data_for_template_change(option)
option_lines.append((0, 0, data))
+
self.sale_order_option_ids = option_lines
if template.number_of_days > 0:
diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py
index 8538b75518a..5eaed0176f0 100644
--- a/addons/sale_management/models/sale_order_template.py
+++ b/addons/sale_management/models/sale_order_template.py
@@ -108,8 +108,6 @@ class SaleOrderTemplateLine(models.Model):
product_id = fields.Many2one(
'product.product', 'Product', check_company=True,
domain=[('sale_ok', '=', True)])
- price_unit = fields.Float('Unit Price', required=True, digits='Product Price')
- discount = fields.Float('Discount (%)', digits='Discount', default=0.0)
product_uom_qty = fields.Float('Quantity', required=True, digits='Product UoS', default=1)
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id', readonly=True)
@@ -122,19 +120,13 @@ class SaleOrderTemplateLine(models.Model):
def _onchange_product_id(self):
self.ensure_one()
if self.product_id:
- self.price_unit = self.product_id.lst_price
self.product_uom_id = self.product_id.uom_id.id
self.name = self.product_id.get_product_multiline_description_sale()
- @api.onchange('product_uom_id')
- def _onchange_product_uom(self):
- if self.product_id and self.product_uom_id:
- self.price_unit = self.product_id.uom_id._compute_price(self.product_id.lst_price, self.product_uom_id)
-
@api.model
def create(self, values):
if values.get('display_type', self.default_get(['display_type'])['display_type']):
- values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom_id=False)
+ values.update(product_id=False, product_uom_qty=0, product_uom_id=False)
return super(SaleOrderTemplateLine, self).create(values)
def write(self, values):
@@ -148,7 +140,7 @@ class SaleOrderTemplateLine(models.Model):
"Missing required product and UoM on accountable sale quote line."),
('non_accountable_fields_null',
- "CHECK(display_type IS NULL OR (product_id IS NULL AND price_unit = 0 AND product_uom_qty = 0 AND product_uom_id IS NULL))",
+ "CHECK(display_type IS NULL OR (product_id IS NULL AND product_uom_qty = 0 AND product_uom_id IS NULL))",
"Forbidden product, unit price, quantity, and UoM on non-accountable sale quote line"),
]
@@ -165,8 +157,6 @@ class SaleOrderTemplateOption(models.Model):
product_id = fields.Many2one(
'product.product', 'Product', domain=[('sale_ok', '=', True)],
required=True, check_company=True)
- price_unit = fields.Float('Unit Price', required=True, digits='Product Price')
- discount = fields.Float('Discount (%)', digits='Discount')
uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True, domain="[('category_id', '=', product_uom_category_id)]")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id', readonly=True)
quantity = fields.Float('Quantity', required=True, digits='Product UoS', default=1)
@@ -175,16 +165,5 @@ class SaleOrderTemplateOption(models.Model):
def _onchange_product_id(self):
if not self.product_id:
return
- self.price_unit = self.product_id.list_price
self.uom_id = self.product_id.uom_id
self.name = self.product_id.get_product_multiline_description_sale()
-
- @api.onchange('uom_id')
- def _onchange_product_uom(self):
- if not self.product_id:
- return
- if not self.uom_id:
- self.price_unit = 0.0
- return
- if self.uom_id.id != self.product_id.uom_id.id:
- self.price_unit = self.product_id.uom_id._compute_price(self.price_unit, self.uom_id)
diff --git a/addons/sale_management/tests/__init__.py b/addons/sale_management/tests/__init__.py
index ec46cdcd6ca..096550808bd 100644
--- a/addons/sale_management/tests/__init__.py
+++ b/addons/sale_management/tests/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_sale_ui
+from . import test_sale_order
diff --git a/addons/sale_management/tests/test_sale_order.py b/addons/sale_management/tests/test_sale_order.py
new file mode 100644
index 00000000000..551e916b3c0
--- /dev/null
+++ b/addons/sale_management/tests/test_sale_order.py
@@ -0,0 +1,281 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.sale.tests.test_sale_common import TestCommonSaleNoChart
+
+
+class TestSaleOrder(TestCommonSaleNoChart):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestSaleOrder, cls).setUpClass()
+
+ Pricelist = cls.env['product.pricelist']
+ Product = cls.env['product.product']
+ SaleOrder = cls.env['sale.order']
+ SaleOrderTemplate = cls.env['sale.order.template']
+ SaleOrderTemplateLine = cls.env['sale.order.template.line']
+ SaleOrderTemplateOption = cls.env['sale.order.template.option']
+
+ # some variables to ease asserts in tests
+ cls.pub_product_price = 100.0
+ cls.pl_product_price = 80.0
+ cls.tpl_discount = 10.0
+ cls.pl_discount = (cls.pub_product_price - cls.pl_product_price) * 100 / cls.pub_product_price
+ cls.merged_discount = 100.0 - (100.0 - cls.pl_discount) * (100.0 - cls.tpl_discount) / 100.0
+
+ cls.pub_option_price = 200.0
+ cls.pl_option_price = 100.0
+ cls.tpl_option_discount = 20.0
+ cls.pl_option_discount = (cls.pub_option_price - cls.pl_option_price) * 100 / cls.pub_option_price
+ cls.merged_option_discount = 100.0 - (100.0 - cls.pl_option_discount) * (100.0 - cls.tpl_option_discount) / 100.0
+
+ # create some products
+ cls.product_1 = Product.create({
+ 'name': 'Product 1',
+ 'lst_price': cls.pub_product_price,
+ })
+
+ cls.optional_product = Product.create({
+ 'name': 'Optional product',
+ 'lst_price': cls.pub_option_price,
+ })
+
+ # create some quotation templates
+ cls.quotation_template_no_discount = SaleOrderTemplate.create({
+ 'name': 'A quotation template without discount'
+ })
+
+ SaleOrderTemplateLine.create({
+ 'name': 'Product 1',
+ 'sale_order_template_id': cls.quotation_template_no_discount.id,
+ 'product_id': cls.product_1.id,
+ 'product_uom_id': cls.product_1.uom_id.id
+ })
+
+ SaleOrderTemplateOption.create({
+ 'name': 'Optional product 1',
+ 'sale_order_template_id': cls.quotation_template_no_discount.id,
+ 'product_id': cls.optional_product.id,
+ 'uom_id': cls.optional_product.uom_id.id
+ })
+
+ # create some pricelists
+ cls.discount_included_price_list = Pricelist.create({
+ 'name': 'Discount included Pricelist',
+ 'discount_policy': 'with_discount',
+ 'item_ids': [
+ (0, 0, {
+ 'name': 'Product 1 premium price',
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.product_1.product_tmpl_id.id,
+ 'compute_price': 'fixed',
+ 'fixed_price': cls.pl_product_price
+ }),
+ (0, 0, {
+ 'name': 'Optional product premium price',
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.optional_product.product_tmpl_id.id,
+ 'compute_price': 'fixed',
+ 'fixed_price': cls.pl_option_price
+ })]
+ })
+
+ cls.discount_excluded_price_list = Pricelist.create({
+ 'name': 'Discount excluded Pricelist',
+ 'discount_policy': 'without_discount',
+ 'item_ids': [
+ (0, 0, {
+ 'name': 'Product 1 premium price',
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.product_1.product_tmpl_id.id,
+ 'compute_price': 'fixed',
+ 'fixed_price': cls.pl_product_price
+ }),
+ (0, 0, {
+ 'name': 'Optional product premium price',
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.optional_product.product_tmpl_id.id,
+ 'compute_price': 'fixed',
+ 'fixed_price': cls.pl_option_price
+ })]
+ })
+
+ # create some sale orders
+ cls.sale_order = SaleOrder.create({
+ 'partner_id': cls.partner_customer_usd.id,
+ })
+
+ cls.sale_order_no_price_list = SaleOrder.create({
+ 'partner_id': cls.partner_customer_usd.id,
+ })
+
+ def test_01_template_without_pricelist(self):
+ """
+ This test checks that without any price list, the public price
+ of the product is used in the sale order after selecting a
+ quotation template.
+ """
+ # first case, without discount in the quotation template
+ self.sale_order_no_price_list.write({
+ 'sale_order_template_id': self.quotation_template_no_discount.id
+ })
+ self.sale_order_no_price_list.onchange_sale_order_template_id()
+
+ self.assertEqual(
+ len(self.sale_order_no_price_list.order_line),
+ 1,
+ "The sale order shall contains the same number of products than"
+ "the quotation template.")
+
+ self.assertEqual(
+ self.sale_order_no_price_list.order_line[0].product_id.id,
+ self.product_1.id,
+ "The sale order shall contains the same products than the"
+ "quotation template.")
+
+ self.assertEqual(
+ self.sale_order_no_price_list.order_line[0].price_unit,
+ self.pub_product_price,
+ "Without any price list and discount, the public price of"
+ "the product shall be used.")
+
+ self.assertEqual(
+ len(self.sale_order_no_price_list.sale_order_option_ids),
+ 1,
+ "The sale order shall contains the same number of optional products than"
+ "the quotation template.")
+
+ self.assertEqual(
+ self.sale_order_no_price_list.sale_order_option_ids[0].product_id.id,
+ self.optional_product.id,
+ "The sale order shall contains the same optional products than the"
+ "quotation template.")
+
+ self.assertEqual(
+ self.sale_order_no_price_list.sale_order_option_ids[0].price_unit,
+ self.pub_option_price,
+ "Without any price list and discount, the public price of"
+ "the optional product shall be used.")
+
+ # add the option to the order
+ self.sale_order_no_price_list.sale_order_option_ids[0].button_add_to_order()
+
+ self.assertEqual(
+ len(self.sale_order_no_price_list.order_line),
+ 2,
+ "When an option is added, a new order line is created")
+
+ self.assertEqual(
+ self.sale_order_no_price_list.order_line[1].product_id.id,
+ self.optional_product.id,
+ "The sale order shall contains the same products than the"
+ "quotation template.")
+
+ self.assertEqual(
+ self.sale_order_no_price_list.order_line[1].price_unit,
+ self.pub_option_price,
+ "Without any price list and discount, the public price of"
+ "the optional product shall be used.")
+
+ def test_02_template_with_discount_included_pricelist(self):
+ """
+ This test checks that with a 'discount included' price list,
+ the price used in the sale order is computed according to the
+ price list.
+ """
+
+ # first case, without discount in the quotation template
+ self.sale_order.write({
+ 'pricelist_id': self.discount_included_price_list.id,
+ 'sale_order_template_id': self.quotation_template_no_discount.id
+ })
+ self.sale_order.onchange_sale_order_template_id()
+
+ self.assertEqual(
+ self.sale_order.order_line[0].price_unit,
+ self.pl_product_price,
+ "If a pricelist is set, the product price shall be computed"
+ "according to it.")
+
+ self.assertEqual(
+ self.sale_order.sale_order_option_ids[0].price_unit,
+ self.pl_option_price,
+ "If a pricelist is set, the optional product price shall"
+ "be computed according to it.")
+
+ # add the option to the order
+ self.sale_order.sale_order_option_ids[0].button_add_to_order()
+
+ self.assertEqual(
+ self.sale_order.order_line[1].price_unit,
+ self.pl_option_price,
+ "If a pricelist is set, the optional product price shall"
+ "be computed according to it.")
+
+ def test_03_template_with_discount_excluded_pricelist(self):
+ """
+ This test checks that with a 'discount excluded' price list,
+ the price used in the sale order is the product public price and
+ the discount is computed according to the price list.
+ """
+
+ self.sale_order.write({
+ 'pricelist_id': self.discount_excluded_price_list.id,
+ 'sale_order_template_id': self.quotation_template_no_discount.id
+ })
+ self.sale_order.onchange_sale_order_template_id()
+
+ self.assertEqual(
+ self.sale_order.order_line[0].price_unit,
+ self.pub_product_price,
+ "If a pricelist is set without discount included, the unit "
+ "price shall be the public product price.")
+
+ self.assertEqual(
+ self.sale_order.order_line[0].price_subtotal,
+ self.pl_product_price,
+ "If a pricelist is set without discount included, the subtotal "
+ "price shall be the price computed according to the price list.")
+
+ self.assertEqual(
+ self.sale_order.order_line[0].discount,
+ self.pl_discount,
+ "If a pricelist is set without discount included, the discount "
+ "shall be computed according to the price unit and the subtotal."
+ "price")
+
+ self.assertEqual(
+ self.sale_order.sale_order_option_ids[0].price_unit,
+ self.pub_option_price,
+ "If a pricelist is set without discount included, the unit "
+ "price shall be the public optional product price.")
+
+ self.assertEqual(
+ self.sale_order.sale_order_option_ids[0].discount,
+ self.pl_option_discount,
+ "If a pricelist is set without discount included, the discount "
+ "shall be computed according to the optional price unit and"
+ "the subtotal price.")
+
+ # add the option to the order
+ self.sale_order.sale_order_option_ids[0].button_add_to_order()
+
+ self.assertEqual(
+ self.sale_order.order_line[1].price_unit,
+ self.pub_option_price,
+ "If a pricelist is set without discount included, the unit "
+ "price shall be the public optional product price.")
+
+ self.assertEqual(
+ self.sale_order.order_line[1].price_subtotal,
+ self.pl_option_price,
+ "If a pricelist is set without discount included, the subtotal "
+ "price shall be the price computed according to the price list.")
+
+ self.assertEqual(
+ self.sale_order.order_line[1].discount,
+ self.pl_option_discount,
+ "If a pricelist is set without discount included, the discount "
+ "shall be computed according to the price unit and the subtotal."
+ "price")
diff --git a/addons/sale_management/views/sale_order_template_views.xml b/addons/sale_management/views/sale_order_template_views.xml
index a2f456f7d53..cac1d4d5add 100644
--- a/addons/sale_management/views/sale_order_template_views.xml
+++ b/addons/sale_management/views/sale_order_template_views.xml
@@ -56,11 +56,6 @@
<div>
<field name="product_uom_qty" class="oe_inline"/>
</div>
- <field name="price_unit"/>
- <label for="discount" groups="product.group_discount_per_so_line"/>
- <div groups="product.group_discount_per_so_line">
- <field name="discount" class="oe_inline"/> %%
- </div>
</group>
</group>
<notebook colspan="4" name="description">
@@ -93,8 +88,6 @@
groups="uom.group_uom"
attrs="{'required': [('display_type', '=', False)]}"
/>
- <field name="discount" groups="product.group_discount_per_so_line"/>
- <field name="price_unit"/>
</tree>
</field>
</page>
@@ -106,8 +99,6 @@
<field name="quantity"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="uom_id" groups="uom.group_uom"/>
- <field name="price_unit"/>
- <field name="discount" groups="product.group_discount_per_so_line"/>
</tree>
</field>
</page>
diff --git a/addons/sale_quotation_builder/views/sale_order_template_views.xml b/addons/sale_quotation_builder/views/sale_order_template_views.xml
index c64d8a9143f..339f5d6abbf 100644
--- a/addons/sale_quotation_builder/views/sale_order_template_views.xml
+++ b/addons/sale_quotation_builder/views/sale_order_template_views.xml
@@ -19,7 +19,7 @@
</page>
</xpath>
- <xpath expr="//tree/field[@name='price_unit']" position="after">
+ <xpath expr="//tree/field[@name='product_uom_id']" position="after">
<field name="website_description" invisible="1"/>
</xpath>