summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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>