diff options
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> |
