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) + # 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() - 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") + # 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, - }) + # 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') - order_line = self.env['sale.order.line'].new({ - 'order_id': so.id, - 'product_id': support_product.id, - }) + # 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 - # 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") + 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, }) - # 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({ + # 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 + } + + 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 }) - 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() + + 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' + }) + 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')