summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiral Bhavsar <hbh@odoo.com>2018-01-31 19:07:59 +0530
committerjem-odoo <jem@openerp.com>2018-05-22 11:52:51 +0200
commit0ccf63cae4e66c8e3736e3d2f72f27753e01f7ad (patch)
tree07419f18d21c3a360a2cf22307ce48a49da94f34
parentef37686b2ac54353ecf6c5ec23367026b732cde0 (diff)
[IMP] sale: new test cases for sale
This commit provides a test suite for sale flow and unit tests. We split tests in different files to try regrouping them by testing purpose: 1/ test_access_rights.py checks employee, sale person, sale manager and portal access rights. It is executes in post-install mode to be sure other addons does not break the security rules. 2/ test_onchange.py checks the onchange using the SFF mecanism in order to simulate what a sale person really does in its day-to-day job. 3/ test_sale_pricelist.py execute a sale flow with different pricelist, as this is a delicate aspect. 4/ test_sale_to_invoice.py contains tests about refund, discount and downpayment, as none of those were tested. Task #43928
-rw-r--r--addons/sale/tests/__init__.py2
-rw-r--r--addons/sale/tests/test_access_rights.py153
-rw-r--r--addons/sale/tests/test_onchange.py76
-rw-r--r--addons/sale/tests/test_sale_order.py43
-rw-r--r--addons/sale/tests/test_sale_pricelist.py178
-rw-r--r--addons/sale/tests/test_sale_to_invoice.py208
6 files changed, 561 insertions, 99 deletions
diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py
index 5700032a542..7401ded6d8d 100644
--- a/addons/sale/tests/__init__.py
+++ b/addons/sale/tests/__init__.py
@@ -3,5 +3,7 @@
from . import test_sale_to_invoice
from . import test_sale_order
+from . import test_sale_pricelist
from . import test_onchange
from . import test_reinvoice
+from . import test_access_rights
diff --git a/addons/sale/tests/test_access_rights.py b/addons/sale/tests/test_access_rights.py
new file mode 100644
index 00000000000..757acdd8b04
--- /dev/null
+++ b/addons/sale/tests/test_access_rights.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import AccessError, UserError
+from odoo.tests import tagged
+from .test_sale_common import TestCommonSaleNoChart
+
+
+@tagged('post_install', '-at_install')
+class TestAccessRights(TestCommonSaleNoChart):
+
+ def setUp(self):
+ super(TestAccessRights, self).setUp()
+
+ Users = self.env['res.users'].with_context(no_reset_password=True)
+
+ group_user = self.env.ref('sales_team.group_sale_salesman')
+ # Create a users
+ self.user_manager = Users.create({
+ 'name': 'Andrew Manager',
+ 'login': 'manager',
+ 'email': 'a.m@example.com',
+ 'groups_id': [(6, 0, [self.env.ref('sales_team.group_sale_manager').id])]
+ })
+ self.user_salesperson = Users.create({
+ 'name': 'Mark User',
+ 'login': 'user',
+ 'email': 'm.u@example.com',
+ 'groups_id': [(6, 0, [group_user.id])]
+ })
+ self.user_salesperson_1 = Users.create({
+ 'name': 'Noemie User',
+ 'login': 'noemie',
+ 'email': 'n.n@example.com',
+ 'groups_id': [(6, 0, [group_user.id])]
+ })
+ self.user_portal = Users.create({
+ 'name': 'Chell Gladys',
+ 'login': 'chell',
+ 'email': 'chell@gladys.portal',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])]
+ })
+ self.user_employee = Users.create({
+ 'name': 'Bert Tartignole',
+ 'login': 'bert',
+ 'email': 'b.t@example.com',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])]
+ })
+
+ # Create a sales channel
+ self.sales_channel = self.env['crm.team'].with_context(tracking_disable=True).create({
+ 'name': 'Test Channel',
+ })
+
+ # Create the SO with a specific salesperson
+ self.order = self.env['sale.order'].with_context(tracking_disable=True).create({
+ 'partner_id': self.partner_customer_usd.id,
+ 'user_id': self.user_salesperson.id
+ })
+
+ def test_access_sales_manager(self):
+ """ Test sales manager's access rights """
+ SaleOrder = self.env['sale.order'].with_context(tracking_disable=True)
+ # Manager can see the SO which is assigned to another salesperson
+ self.order.sudo(self.user_manager).read()
+ # Manager can change a salesperson of the SO
+ self.order.sudo(self.user_manager).write({'user_id': self.user_salesperson_1.id})
+ # Manager can create the SO for other salesperson
+ sale_order = SaleOrder.sudo(self.user_manager).create({
+ 'partner_id': self.partner_customer_usd.id,
+ 'user_id': self.user_salesperson_1.id
+ })
+ self.assertIn(sale_order.id, SaleOrder.search([]).ids, 'Sales manager should be able to create the SO of other salesperson')
+ # Manager can confirm the SO
+ sale_order.sudo(self.user_manager).action_confirm()
+ # Manager can not delete confirmed SO
+ with self.assertRaises(UserError):
+ sale_order.sudo(self.user_manager).unlink()
+ # Manager can delete the SO of other salesperson if SO is in 'draft' or 'cancel' state
+ self.order.sudo(self.user_manager).unlink()
+ self.assertNotIn(self.order.id, SaleOrder.search([]).ids, 'Sales manager should be able to delete the SO')
+
+ # Manager can create a sales channel
+ india_channel = self.env['crm.team'].with_context(tracking_disable=True).sudo(self.user_manager).create({
+ 'name': 'India',
+ })
+ self.assertIn(india_channel.id, self.env['crm.team'].search([]).ids, 'Sales manager should be able to create a sales channel')
+ # Manager can edit a sales channel
+ india_channel.sudo(self.user_manager).write({'dashboard_graph_group': 'week'})
+ self.assertEquals(india_channel.dashboard_graph_group, 'week', 'Sales manager should be able to edit a sales channel')
+ # Manager can delete a sales channel
+ india_channel.sudo(self.user_manager).unlink()
+ self.assertNotIn(india_channel.id, self.env['crm.team'].search([]).ids, 'Sales manager should be able to delete a sales channel')
+
+ def test_access_sales_person(self):
+ """ Test Salesperson's access rights """
+ # Salesperson can see only their own sales order
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_salesperson_1).read()
+ # Now assign the SO to themselves
+ self.order.write({'user_id': self.user_salesperson_1.id})
+ self.order.sudo(self.user_salesperson_1).read()
+ # Salesperson can change a sales channel of SO
+ self.order.sudo(self.user_salesperson_1).write({'team_id': self.sales_channel.id})
+ # Salesperson can't create the SO of other salesperson
+ with self.assertRaises(AccessError):
+ self.env['sale.order'].sudo(self.user_salesperson_1).create({
+ 'partner_id': self.partner_customer_usd.id,
+ 'user_id': self.user_salesperson.id
+ })
+ # Salesperson can't delete the SO
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_salesperson_1).unlink()
+ # Salesperson can confirm the SO
+ self.order.sudo(self.user_salesperson_1).action_confirm()
+
+ def test_access_portal_user(self):
+ """ Test portal user's access rights """
+ # Portal user can see the confirmed SO for which they are assigned as a customer
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_portal).read()
+
+ self.order.write({'partner_id': self.user_portal.partner_id.id})
+ self.order.action_confirm()
+ # Portal user can't edit the SO
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_portal).write({'team_id': self.sales_channel.id})
+ # Portal user can't create the SO
+ with self.assertRaises(AccessError):
+ self.env['sale.order'].sudo(self.user_portal).create({
+ 'partner_id': self.partner_customer_usd.id,
+ })
+ # Portal user can't delete the SO which is in 'draft' or 'cancel' state
+ self.order.action_cancel()
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_portal).unlink()
+
+ def test_access_employee(self):
+ """ Test classic employee's access rights """
+ # Employee can't see any SO
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_employee).read()
+ # Employee can't edit the SO
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_employee).write({'team_id': self.sales_channel.id})
+ # Employee can't create the SO
+ with self.assertRaises(AccessError):
+ self.env['sale.order'].sudo(self.user_employee).create({
+ 'partner_id': self.partner_customer_usd.id,
+ })
+ # Employee can't delete the SO
+ with self.assertRaises(AccessError):
+ self.order.sudo(self.user_employee).unlink()
diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py
index 7bc43e00ef0..4c5169d1bc6 100644
--- a/addons/sale/tests/test_onchange.py
+++ b/addons/sale/tests/test_onchange.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.tests import Form
from odoo.tests.common import TransactionCase
@@ -48,26 +49,21 @@ class TestOnchangeProductId(TransactionCase):
fp_tax_id = self.fiscal_position_tax_model.create(dict(position_id=fp_id.id,
tax_src_id=tax_include_id.id,
tax_dest_id=tax_exclude_id.id))
- so_vals = {
- 'partner_id': partner_id.id,
- 'pricelist_id': pricelist.id,
- 'fiscal_position_id': fp_id.id,
- 'order_line': [
- (0, 0, {
- 'name': product_id.name,
- 'product_id': product_id.id,
- 'product_uom_qty': 1.0,
- 'product_uom': uom_id.id,
- 'price_unit': 121.0
- })
- ]
- }
-
- so = self.so_model.create(so_vals)
-
- so_line = so.order_line[0]
- so_line.product_id_change()
- self.assertEquals(100, so_line.price_unit, "The included tax must be subtracted to the price")
+
+ # Create the SO with one SO line and apply a pricelist and fiscal position on it
+ order_form = Form(self.env['sale.order'].with_context(tracking_disable=True))
+ order_form.partner_id = partner_id
+ order_form.pricelist_id = pricelist
+ order_form.fiscal_position_id = fp_id
+ with order_form.order_line.new() as line:
+ line.name = product_id.name
+ line.product_id = product_id
+ line.product_uom_qty = 1.0
+ line.product_uom = uom_id
+ sale_order = order_form.save()
+
+ # Check the unit price of SO line
+ self.assertEquals(100, sale_order.order_line[0].price_unit, "The included tax must be subtracted to the price")
def test_pricelist_application(self):
""" Test different prices are correctly applied based on dates """
@@ -96,23 +92,23 @@ class TestOnchangeProductId(TransactionCase):
})]
})
- so = self.env['sale.order'].create({
- 'partner_id': partner.id,
- 'date_order': '2017-12-20',
- 'pricelist_id': christmas_pricelist.id,
- })
-
- order_line = self.env['sale.order.line'].new({
- 'order_id': so.id,
- 'product_id': support_product.id,
- })
-
- # force compute uom and prices
- order_line.product_id_change()
- order_line.product_uom_change()
-
- self.assertEqual(order_line.price_unit, 80, "First date pricelist rule not applied")
-
- so.date_order = '2017-12-30'
- order_line.product_id_change()
- self.assertEqual(order_line.price_unit, 50, "Second date pricelist rule not applied")
+ # Create the SO with pricelist based on date
+ order_form = Form(self.env['sale.order'].with_context(tracking_disable=True))
+ order_form.partner_id = partner
+ order_form.date_order = '2017-12-20'
+ order_form.pricelist_id = christmas_pricelist
+ with order_form.order_line.new() as line:
+ line.product_id = support_product
+ so = order_form.save()
+ # Check the unit price and subtotal of SO line
+ self.assertEqual(so.order_line[0].price_unit, 80, "First date pricelist rule not applied")
+ self.assertEquals(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity')
+
+ # Change order date of the SO and check the unit price and subtotal of SO line
+ with Form(so) as order:
+ order.date_order = '2017-12-30'
+ with order.order_line.edit(0) as line:
+ line.product_id = support_product
+
+ self.assertEqual(so.order_line[0].price_unit, 50, "Second date pricelist rule not applied")
+ self.assertEquals(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity')
diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py
index a68c992438b..a8d7ea85509 100644
--- a/addons/sale/tests/test_sale_order.py
+++ b/addons/sale/tests/test_sale_order.py
@@ -3,6 +3,8 @@
import odoo
from odoo.exceptions import UserError, AccessError
+from odoo.tests import Form
+from odoo.tools import float_compare
from .test_sale_common import TestCommonSaleNoChart
@@ -13,6 +15,8 @@ class TestSaleOrder(TestCommonSaleNoChart):
def setUpClass(cls):
super(TestSaleOrder, cls).setUpClass()
+ SaleOrder = cls.env['sale.order'].with_context(tracking_disable=True)
+
# set up users
cls.setUpUsers()
group_salemanager = cls.env.ref('sales_team.group_sale_manager')
@@ -25,8 +29,8 @@ class TestSaleOrder(TestCommonSaleNoChart):
cls.setUpClassicProducts()
cls.setUpAccountJournal()
- # create a generic Sale Order with all classical products
- cls.sale_order = cls.env['sale.order'].create({
+ # create a generic Sale Order with all classical products and empty pricelist
+ cls.sale_order = SaleOrder.create({
'partner_id': cls.partner_customer_usd.id,
'partner_invoice_id': cls.partner_customer_usd.id,
'partner_shipping_id': cls.partner_customer_usd.id,
@@ -59,7 +63,7 @@ class TestSaleOrder(TestCommonSaleNoChart):
'order_id': cls.sale_order.id,
'tax_id': False,
})
- cls.sol_prod_deliver = cls.env['sale.order.line'].create({
+ cls.sol_product_deliver = cls.env['sale.order.line'].create({
'name': cls.product_deliver.name,
'product_id': cls.product_deliver.id,
'product_uom_qty': 2,
@@ -196,3 +200,36 @@ class TestSaleOrder(TestCommonSaleNoChart):
sol = so.order_line.filtered(lambda l: l.product_id == serv_cost)
self.assertTrue(sol, 'Sale: cost invoicing does not add lines when confirming vendor invoice')
self.assertEquals((sol.price_unit, sol.qty_delivered, sol.product_uom_qty, sol.qty_invoiced), (160, 2, 0, 0), 'Sale: line is wrong after confirming vendor invoice')
+
+ def test_sale_with_taxes(self):
+ """ Test SO with taxes applied on its lines and check subtotal applied on its lines and total applied on the SO """
+ # Create a tax with price included
+ tax_include = self.env['account.tax'].create({
+ 'name': 'Tax with price include',
+ 'amount': 10,
+ 'price_include': True
+ })
+ # Create a tax with price not included
+ tax_exclude = self.env['account.tax'].create({
+ 'name': 'Tax with no price include',
+ 'amount': 10,
+ })
+
+ # Apply taxes on the sale order lines
+ self.sol_product_order.write({'tax_id': [(4, tax_include.id)]})
+ self.sol_serv_deliver.write({'tax_id': [(4, tax_include.id)]})
+ self.sol_serv_order.write({'tax_id': [(4, tax_exclude.id)]})
+ self.sol_product_deliver.write({'tax_id': [(4, tax_exclude.id)]})
+
+ # Trigger onchange to reset discount, unit price, subtotal, ...
+ for line in self.sale_order.order_line:
+ line.product_id_change()
+ line._onchange_discount()
+
+ for line in self.sale_order.order_line:
+ if line.tax_id.price_include:
+ price = line.price_unit * line.product_uom_qty - line.price_tax
+ self.assertEquals(float_compare(line.price_subtotal, price, precision_digits=2), 0, 'Tax should be included on an order line')
+ else:
+ self.assertEquals(line.price_subtotal, line.price_unit * line.product_uom_qty, 'Tax should not be included on an order line')
+ self.assertEquals(self.sale_order.amount_total, self.sale_order.amount_untaxed + self.sale_order.amount_tax, 'Taxes should be applied')
diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py
new file mode 100644
index 00000000000..3ad91d7dccc
--- /dev/null
+++ b/addons/sale/tests/test_sale_pricelist.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from .test_sale_common import TestCommonSaleNoChart
+
+
+class TestSaleOrder(TestCommonSaleNoChart):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestSaleOrder, cls).setUpClass()
+
+ Pricelist = cls.env['product.pricelist']
+ PricelistItem = cls.env['product.pricelist.item']
+ SaleOrder = cls.env['sale.order'].with_context(tracking_disable=True)
+ SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True)
+
+ # set up accounts and products and journals
+ cls.setUpAdditionalAccounts()
+ cls.setUpClassicProducts()
+ cls.setUpAccountJournal()
+
+ # Create a product category
+ cls.product_category_1 = cls.env['product.category'].create({
+ 'name': 'Product Category for pricelist',
+ })
+
+ # Create a pricelist with discount policy: percentage on services, fixed price for product_order
+ cls.pricelist_discount_incl = Pricelist.create({
+ 'name': 'Pricelist A',
+ 'discount_policy': 'with_discount',
+ })
+ PricelistItem.create({
+ 'pricelist_id': cls.pricelist_discount_incl.id,
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.service_order.product_tmpl_id.id,
+ 'compute_price': 'percentage',
+ 'percent_price': 10
+ })
+ PricelistItem.create({
+ 'pricelist_id': cls.pricelist_discount_incl.id,
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.service_deliver.product_tmpl_id.id,
+ 'compute_price': 'percentage',
+ 'percent_price': 20
+ })
+ cls.pricelist_discount_incl_item3 = PricelistItem.create({
+ 'pricelist_id': cls.pricelist_discount_incl.id,
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.product_order.product_tmpl_id.id,
+ 'compute_price': 'fixed',
+ 'fixed_price': 211
+ })
+
+ # Create a pricelist without discount policy: formula for product_category_1 category, percentage for service_order
+ cls.pricelist_discount_excl = Pricelist.create({
+ 'name': 'Pricelist B',
+ 'discount_policy': 'without_discount',
+ })
+ PricelistItem.create({
+ 'pricelist_id': cls.pricelist_discount_excl.id,
+ 'applied_on': '2_product_category',
+ 'categ_id': cls.product_category_1.id,
+ 'compute_price': 'formula',
+ 'base': 'standard_price',
+ 'price_discount': 10
+ })
+ PricelistItem.create({
+ 'pricelist_id': cls.pricelist_discount_excl.id,
+ 'applied_on': '1_product',
+ 'product_tmpl_id': cls.service_order.product_tmpl_id.id,
+ 'compute_price': 'percentage',
+ 'percent_price': 20
+ })
+
+ # create a generic Sale Order with all classical products and empty pricelist
+ cls.sale_order = SaleOrder.create({
+ 'partner_id': cls.partner_customer_usd.id,
+ 'partner_invoice_id': cls.partner_customer_usd.id,
+ 'partner_shipping_id': cls.partner_customer_usd.id,
+ 'pricelist_id': cls.pricelist_usd.id,
+ })
+ cls.sol_product_order = SaleOrderLine.create({
+ 'name': cls.product_order.name,
+ 'product_id': cls.product_order.id,
+ 'product_uom_qty': 2,
+ 'product_uom': cls.product_order.uom_id.id,
+ 'price_unit': cls.product_order.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
+ })
+ cls.sol_serv_deliver = SaleOrderLine.create({
+ 'name': cls.service_deliver.name,
+ 'product_id': cls.service_deliver.id,
+ 'product_uom_qty': 2,
+ 'product_uom': cls.service_deliver.uom_id.id,
+ 'price_unit': cls.service_deliver.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
+ })
+ cls.sol_serv_order = SaleOrderLine.create({
+ 'name': cls.service_order.name,
+ 'product_id': cls.service_order.id,
+ 'product_uom_qty': 2,
+ 'product_uom': cls.service_order.uom_id.id,
+ 'price_unit': cls.service_order.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
+ })
+ cls.sol_prod_deliver = SaleOrderLine.create({
+ 'name': cls.product_deliver.name,
+ 'product_id': cls.product_deliver.id,
+ 'product_uom_qty': 2,
+ 'product_uom': cls.product_deliver.uom_id.id,
+ 'price_unit': cls.product_deliver.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
+ })
+
+ def test_sale_with_pricelist_empty(self):
+ # Trigger onchange to reset discount, unit price, subtotal, ...
+ self.sale_order.onchange_partner_id()
+ for line in self.sale_order.order_line:
+ line.product_id_change()
+ line._onchange_discount()
+ # Check that pricelist of the SO has been applied on the sale order lines or not
+ for line in self.sale_order.order_line:
+ self.assertEquals(line.price_unit, line.product_id.list_price, 'Pricelist of the SO should not be applied on an order line %s' % (line.name,))
+
+ def test_sale_with_pricelist_discount_included(self):
+ """ Test SO with the pricelist and check unit price appeared on its lines """
+ # Change the pricelist
+ self.sale_order.write({'pricelist_id': self.pricelist_discount_incl.id})
+ # Trigger onchange to reset discount, unit price, subtotal, ...
+ for line in self.sale_order.order_line:
+ line.product_id_change()
+ line._onchange_discount()
+ # Check that pricelist of the SO has been applied on the sale order lines or not
+ for line in self.sale_order.order_line:
+ if self.sale_order.pricelist_id in line.product_id.item_ids.mapped('pricelist_id'): # concerned by pricelist (all except product_deliver)
+ if line.product_id == self.product_order:
+ self.assertEquals(line.price_unit, self.pricelist_discount_incl_item3.fixed_price, 'Price of product_order should be %s applied on the order line' % (self.pricelist_discount_incl_item3.fixed_price,))
+ else: # only services (service_order and service_deliver)
+ for item in self.sale_order.pricelist_id.item_ids.filtered(lambda l: l.product_tmpl_id == line.product_id.product_tmpl_id):
+ price = item.percent_price
+ self.assertEquals(price, (line.product_id.list_price - line.price_unit) / line.product_id.list_price * 100, 'Pricelist of the SO should be applied on an order line %s' % (line.product_id.name,))
+ else:
+ self.assertEquals(line.price_unit, line.product_id.list_price, 'Pricelist of the SO should not be applied on an order line')
+
+ def test_sale_with_pricelist_discount_excluded(self):
+ """ Test SO with the pricelist 'discount displayed' and check discount and unit price appeared on its lines """
+ # Add group 'Discount on Lines' to the user
+ self.env.user.write({'groups_id': [(4, self.env.ref('sale.group_discount_per_so_line').id)]})
+
+ # Set product category on consumable products (for the pricelist item applying on this category)
+ self.product_order.write({'categ_id': self.product_category_1.id})
+ self.product_deliver.write({'categ_id': self.product_category_1.id})
+
+ # Change the pricelist
+ self.sale_order.write({'pricelist_id': self.pricelist_discount_excl.id})
+ # Trigger onchange to reset discount, unit price, subtotal, ...
+ for line in self.sale_order.order_line:
+ line.product_id_change()
+ line._onchange_discount()
+
+ # Check pricelist of the SO apply or not on order lines where pricelist contains formula that add 15% on the cost price
+ for line in self.sale_order.order_line:
+ if line.product_id.categ_id in self.sale_order.pricelist_id.item_ids.mapped('categ_id'): # reduction per category (consummable only)
+ for item in self.sale_order.pricelist_id.item_ids.filtered(lambda l: l.categ_id == line.product_id.categ_id):
+ self.assertEquals(line.discount, item.price_discount, "Discount should be displayed on order line %s since its category get some discount" % (line.name,))
+ self.assertEquals(line.price_unit, line.product_id.standard_price, "Price unit should be the cost price for product %s" % (line.name,))
+ else:
+ if line.product_id == self.service_order: # reduction for this product
+ self.assertEquals(line.discount, 20.0, "Discount for product %s should be 20 percent with pricelist %s" % (line.name, self.pricelist_discount_excl.name))
+ self.assertEquals(line.price_unit, line.product_id.list_price, 'Unit price of order line should be a sale price as the pricelist not applied on the other category\'s product')
+ else: # no discount for the rest
+ self.assertEquals(line.discount, 0.0, 'Pricelist of SO should not be applied on an order line')
+ self.assertEquals(line.price_unit, line.product_id.list_price, 'Unit price of order line should be a sale price as the pricelist not applied on the other category\'s product')
diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py
index 45301e65e2f..83a72ab30fe 100644
--- a/addons/sale/tests/test_sale_to_invoice.py
+++ b/addons/sale/tests/test_sale_to_invoice.py
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from datetime import datetime
-
-from odoo.tools import mute_logger
+from odoo.tools import pycompat
from .test_sale_common import TestCommonSaleNoChart
@@ -12,63 +10,161 @@ class TestSaleToInvoice(TestCommonSaleNoChart):
@classmethod
def setUpClass(cls):
super(TestSaleToInvoice, cls).setUpClass()
+
+ cls.setUpClassicProducts()
cls.setUpAdditionalAccounts()
cls.setUpAccountJournal()
- @mute_logger('odoo.addons.base.models.ir_model', 'odoo.osv.orm')
- def setUp(self):
- super(TestSaleToInvoice, self).setUp()
-
- def test_sale_to_invoice(self):
- """ Testing for invoice create,validate and pay with invoicing and payment user."""
- # Usefull models
- IrModelData = self.env['ir.model.data']
- partner_obj = self.env['res.partner']
- journal_obj = self.env['account.journal']
- # Usefull record id
- group_id = IrModelData.xmlid_to_res_id('account.group_account_invoice') or False
- company_id = IrModelData.xmlid_to_res_id('base.main_company') or False
-
- # Usefull accounts
- account_recv_id = self.account_receivable
- account_rev_id = self.account_revenue
-
- # Add account to product
- product_template_id = self.env.ref('sale.advance_product_0').product_tmpl_id
- product_template_id.write({'property_account_income_id': account_rev_id})
-
- # In order to test, I create new user and applied Invoicing & Payments group.
- user = self.env['res.users'].create({
- 'name': 'Test User',
- 'login': 'test@test.com',
- 'company_id': 1,
- 'groups_id': [(6, 0, [group_id])]})
- assert user, "User will not created."
- # I create partner for sales order.
- partner = partner_obj.create({
- 'name': 'Test Customer',
- 'email': 'testcustomer@test.com',
- 'property_account_receivable_id': account_recv_id,
+ # Create the SO with four order lines
+ cls.sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
+ 'partner_id': cls.partner_customer_usd.id,
+ 'partner_invoice_id': cls.partner_customer_usd.id,
+ 'partner_shipping_id': cls.partner_customer_usd.id,
+ 'pricelist_id': cls.pricelist_usd.id,
+ })
+ SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True)
+ cls.sol_prod_order = SaleOrderLine.create({
+ 'name': cls.product_order.name,
+ 'product_id': cls.product_order.id,
+ 'product_uom_qty': 5,
+ 'product_uom': cls.product_order.uom_id.id,
+ 'price_unit': cls.product_order.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
+ })
+ cls.sol_serv_deliver = SaleOrderLine.create({
+ 'name': cls.service_deliver.name,
+ 'product_id': cls.service_deliver.id,
+ 'product_uom_qty': 4,
+ 'product_uom': cls.service_deliver.uom_id.id,
+ 'price_unit': cls.service_deliver.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
})
+ cls.sol_serv_order = SaleOrderLine.create({
+ 'name': cls.service_order.name,
+ 'product_id': cls.service_order.id,
+ 'product_uom_qty': 3,
+ 'product_uom': cls.service_order.uom_id.id,
+ 'price_unit': cls.service_order.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
+ })
+ cls.sol_prod_deliver = SaleOrderLine.create({
+ 'name': cls.product_deliver.name,
+ 'product_id': cls.product_deliver.id,
+ 'product_uom_qty': 2,
+ 'product_uom': cls.product_deliver.uom_id.id,
+ 'price_unit': cls.product_deliver.list_price,
+ 'order_id': cls.sale_order.id,
+ 'tax_id': False,
+ })
+
+ # Context
+ cls.context = {
+ 'active_model': 'sale.order',
+ 'active_ids': [cls.sale_order.id],
+ 'active_id': cls.sale_order.id,
+ 'default_journal_id': cls.journal_sale
+ }
- # In order to test I create sales order and confirmed it.
- order = self.env['sale.order'].create({
- 'partner_id': partner.id,
- 'partner_invoice_id': partner.id,
- 'partner_shipping_id': partner.id,
- 'date_order': datetime.today(),
- 'pricelist_id': self.env.ref('product.list0').id})
- assert order, "Sales order will not created."
- context = {"active_model": 'sale.order', "active_ids": [order.id], "active_id": order.id}
- order.with_context(context).action_confirm()
- # Now I create invoice.
- payment = self.env['sale.advance.payment.inv'].create({
+ def test_downpayment(self):
+ """ Test invoice with a way of downpayment and check downpayment's SO line is created
+ and also check a total amount of invoice is equal to a respective sale order's total amount
+ """
+ # Confirm the SO
+ self.sale_order.action_confirm()
+ # Let's do an invoice for a deposit of 100
+ payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
'advance_payment_method': 'fixed',
- 'amount': 5,
- 'product_id': self.env.ref('sale.advance_product_0').id,
+ 'amount': 100,
+ 'deposit_account_id': self.account_income.id
+ })
+ payment.create_invoices()
+
+ self.assertEquals(len(self.sale_order.invoice_ids), 1, 'Invoice should be created for the SO')
+ downpayment_line = self.sale_order.order_line.filtered(lambda l: l.is_downpayment)
+ self.assertEquals(len(downpayment_line), 1, 'SO line downpayment should be created on SO')
+
+ # Update delivered quantity of SO lines
+ self.sol_serv_deliver.write({'qty_delivered': 4.0})
+ self.sol_prod_deliver.write({'qty_delivered': 2.0})
+
+ # Let's do an invoice with refunds
+ payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
+ 'advance_payment_method': 'all',
+ 'deposit_account_id': self.account_income.id
+ })
+ payment.create_invoices()
+
+ self.assertEquals(len(self.sale_order.invoice_ids), 2, 'Invoice should be created for the SO')
+
+ invoice = self.sale_order.invoice_ids[0]
+ self.assertEquals(len(invoice.invoice_line_ids), len(self.sale_order.order_line), 'All lines should be invoiced')
+ self.assertEquals(invoice.amount_total, self.sale_order.amount_total - downpayment_line.price_unit, 'Downpayment should be applied')
+
+ def test_invoice_with_discount(self):
+ """ Test invoice with a discount and check discount applied on both SO lines and an invoice lines """
+ # Update discount and delivered quantity on SO lines
+ self.sol_prod_order.write({'discount': 20.0})
+ self.sol_serv_deliver.write({'discount': 20.0, 'qty_delivered': 4.0})
+ self.sol_serv_order.write({'discount': -10.0})
+ self.sol_prod_deliver.write({'qty_delivered': 2.0})
+
+ for line in self.sale_order.order_line.filtered(lambda l: l.discount):
+ product_price = line.price_unit * line.product_uom_qty
+ self.assertEquals(line.discount, (product_price - line.price_subtotal) / product_price * 100, 'Discount should be applied on order line')
+
+ self.sale_order.action_confirm()
+
+ # Let's do an invoice with invoiceable lines
+ payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
+ 'advance_payment_method': 'delivered'
+ })
+ payment.create_invoices()
+ invoice = self.sale_order.invoice_ids[0]
+ invoice.action_invoice_open()
+
+ # Check discount appeared on both SO lines and invoice lines
+ for line, inv_line in pycompat.izip(self.sale_order.order_line, invoice.invoice_line_ids):
+ self.assertEquals(line.discount, inv_line.discount, 'Discount on lines of order and invoice should be same')
+
+ def test_invoice_refund(self):
+ """ Test invoice with a refund and check customer invoices credit note is created from respective invoice """
+ # Confirm the SO
+ self.sale_order.action_confirm()
+ # Take only invoicable line
+ order_line = self.sale_order.order_line.filtered(lambda l: l.product_id.invoice_policy == 'order')
+ # Check ordered quantity, quantity to invoice and invoiced quantity of SO lines
+ for line in order_line:
+ self.assertEquals(line.qty_to_invoice, line.product_uom_qty, 'Quantity to invoice should be same as ordered quantity')
+ self.assertEquals(line.qty_invoiced, 0.0, 'Invoiced quantity should be zero as no any invoice created for SO')
+
+ # Let's do an invoice with invoiceable lines
+ payment = self.env['sale.advance.payment.inv'].with_context(self.context).create({
+ 'advance_payment_method': 'delivered'
})
- invoice = payment.with_context(context).create_invoices()
- assert order.invoice_ids, "No any invoice is created for this sales order"
- # Now I validate pay invoice wihth Test User(invoicing and payment).
- for invoice in order.invoice_ids:
- invoice.with_context(context).invoice_validate()
+ payment.create_invoices()
+
+ invoice = self.sale_order.invoice_ids[0]
+
+ # Update quantity of an invoice lines
+ invoice.invoice_line_ids[0].write({'quantity': 3.0})
+ invoice.invoice_line_ids[1].write({'quantity': 2.0})
+ invoice.action_invoice_open()
+
+ # Check quantity to invoice on SO lines
+ for line in order_line:
+ self.assertEquals(line.qty_to_invoice, line.product_uom_qty - line.qty_invoiced, 'Quantity to invoice should be a difference between ordered quantity and invoiced quantity')
+
+ # Make a credit note
+ credit_note = self.env['account.invoice.refund'].with_context({'active_ids': [invoice.id], 'active_id': invoice.id}).create({
+ 'filter_refund': 'refund',
+ 'description': 'test'
+ })
+ credit_note.invoice_refund()
+ invoice_credit_note = self.sale_order.invoice_ids[1]
+
+ # Check invoice's type and number
+ self.assertEquals(invoice_credit_note.type, 'out_refund', 'Invoice type should be a customer credit note')
+ self.assertEquals(invoice.number, invoice_credit_note.origin, 'Customer invoices credit note should be created from respective invoice')