diff --git a/addons/hr_expense/tests/__init__.py b/addons/hr_expense/tests/__init__.py index fb87f7b9832..0d18c30d3de 100644 --- a/addons/hr_expense/tests/__init__.py +++ b/addons/hr_expense/tests/__init__.py @@ -1 +1,5 @@ -from . import test_expenses \ No newline at end of file +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common +from . import test_expenses diff --git a/addons/hr_expense/tests/common.py b/addons/hr_expense/tests/common.py new file mode 100644 index 00000000000..5df38a278c1 --- /dev/null +++ b/addons/hr_expense/tests/common.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import common + + +class CommonTest(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(CommonTest, cls).setUpClass() + + # Create payable account for the expense + user_type = cls.env.ref('account.data_account_type_payable') + cls.account_payable = cls.env['account.account'].create({ + 'code': 'X1111', + 'name': 'HR Expense - Test Payable Account', + 'user_type_id': user_type.id, + 'reconcile': True + }) + + # Create expenses account for the expense + user_type = cls.env.ref('account.data_account_type_expenses') + cls.account_expense = cls.env['account.account'].create({ + 'code': 'X2120', + 'name': 'HR Expense - Test Purchase Account', + 'user_type_id': user_type.id + }) + + # User groups + user_group_employee = cls.env.ref('base.group_user') + user_group_manager = cls.env.ref('hr_expense.group_hr_expense_manager') + + # User and Employee Data + Users = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True}) + cls.user_employee = Users.create({ + 'name': 'Johnny Employee', + 'login': 'john', + 'email': 'john@example.com', + 'notification_type': 'email', + 'groups_id': [(6, 0, [user_group_employee.id])], + }) + cls.user_manager = Users.create({ + 'name': 'Robert Manager', + 'login': 'rob', + 'email': 'rob@example.com', + 'notification_type': 'email', + 'groups_id': [(6, 0, [user_group_manager.id])], + }) + cls.employee = cls.env['hr.employee'].create({ + 'name': 'Johnny Employee', + 'address_home_id': cls.user_employee.partner_id.id, + 'address_id': cls.user_employee.partner_id.id, + }) + cls.user_manager.partner_id.write({'property_account_payable_id': cls.account_payable.id}) + cls.user_employee.partner_id.write({'property_account_payable_id': cls.account_payable.id}) + + # Expense Products + cls.product_ordered_cost = cls.env['product.product'].create({ + 'name': "Ordered at cost", + 'standard_price': 8, + 'list_price': 10, + 'type': 'consu', + 'invoice_policy': 'order', + 'expense_policy': 'cost', + 'default_code': 'CONSU-ORDERED1', + 'service_type': 'manual', + 'taxes_id': False, + 'property_account_expense_id': cls.account_expense.id, + }) + + cls.product_deliver_cost = cls.env['product.product'].create({ + 'name': "Delivered at cost", + 'standard_price': 8, + 'list_price': 10, + 'type': 'consu', + 'invoice_policy': 'delivery', + 'expense_policy': 'cost', + 'default_code': 'CONSU-DELI1', + 'service_type': 'manual', + 'taxes_id': False, + 'property_account_expense_id': cls.account_expense.id, + }) + + cls.product_order_sales_price = cls.env['product.product'].create({ + 'name': "Ordered at sales price", + 'standard_price': 8, + 'list_price': 10, + 'type': 'consu', + 'invoice_policy': 'order', + 'expense_policy': 'sales_price', + 'default_code': 'CONSU-ORDERED2', + 'service_type': 'manual', + 'taxes_id': False, + 'property_account_expense_id': cls.account_expense.id, + }) + + cls.product_deliver_sales_price = cls.env['product.product'].create({ + 'name': "Delivered at sales price", + 'standard_price': 8, + 'list_price': 10, + 'type': 'consu', + 'invoice_policy': 'delivery', + 'expense_policy': 'sales_price', + 'default_code': 'CONSU-DELI2', + 'service_type': 'manual', + 'taxes_id': False, + 'property_account_expense_id': cls.account_expense.id, + }) + + cls.product_no_expense = cls.env['product.product'].create({ + 'name': "No expense", + 'standard_price': 8, + 'list_price': 10, + 'type': 'consu', + 'invoice_policy': 'delivery', + 'expense_policy': 'no', + 'default_code': 'CONSU-NO', + 'service_type': 'manual', + 'taxes_id': False, + 'property_account_expense_id': cls.account_expense.id, + }) + + # Expense report + cls.journal = cls.env['account.journal'].create({ + 'name': 'Purchase Journal - Test', + 'code': 'HRTPJ', + 'type': 'purchase', + 'company_id': cls.env.user.company_id.id, + }) + cls.expense_sheet = cls.env['hr.expense.sheet'].create({ + 'name': 'Expense for Johnny Employee', + 'employee_id': cls.employee.id, + 'journal_id': cls.journal.id, + }) diff --git a/addons/sale_expense/tests/__init__.py b/addons/sale_expense/tests/__init__.py index 20cd548feae..c770fdfb34d 100644 --- a/addons/sale_expense/tests/__init__.py +++ b/addons/sale_expense/tests/__init__.py @@ -1 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from . import test_sale_expense +from . import test_reinvoice diff --git a/addons/sale_expense/tests/test_reinvoice.py b/addons/sale_expense/tests/test_reinvoice.py new file mode 100644 index 00000000000..b25d421ec93 --- /dev/null +++ b/addons/sale_expense/tests/test_reinvoice.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.hr_expense.tests.common import CommonTest + + +class TestReInvoice(CommonTest): + + @classmethod + def setUpClass(cls): + super(TestReInvoice, cls).setUpClass() + + cls.partner_customer = cls.env['res.partner'].create({ + 'name': 'Ze Client', + 'email': 'client@agrolait.com', + 'property_account_payable_id': cls.account_payable.id, + }) + + cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({ + 'partner_id': cls.partner_customer.id, + 'partner_invoice_id': cls.partner_customer.id, + 'partner_shipping_id': cls.partner_customer.id, + }) + + def test_at_cost(self): + """ Test invoicing expenses at cost for product based on delivered and ordered quantities. """ + # create SO line and confirm SO (with only one line) + sale_order_line = self.env['sale.order.line'].create({ + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'product_uom_qty': 2, + 'product_uom': self.product_ordered_cost.uom_id.id, + 'price_unit': self.product_ordered_cost.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line.product_id_change() + self.sale_order.onchange_partner_id() + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + self.assertTrue(self.sale_order.analytic_account_id, "Confirming SO with an expense product should trigger the analytic account creation") + + # create expense lines + expense1 = self.env['hr.expense'].create({ + 'name': 'Expense for ordered product', + 'employee_id': self.employee.id, + 'product_id': self.product_ordered_cost.id, + 'unit_amount': 12, + 'quantity': 2, + 'sheet_id': self.expense_sheet.id, + 'sale_order_id': self.sale_order.id, + 'analytic_account_id': self.sale_order.analytic_account_id.id, + }) + expense1._onchange_product_id() + expense2 = self.env['hr.expense'].create({ + 'name': 'Expense for delivered product', + 'employee_id': self.employee.id, + 'product_id': self.product_deliver_cost.id, + 'unit_amount': 15, + 'quantity': 1, + 'sheet_id': self.expense_sheet.id, + 'sale_order_id': self.sale_order.id, + 'analytic_account_id': self.sale_order.analytic_account_id.id, + }) + expense2._onchange_product_id() + + # approve and generate entries + self.expense_sheet.approve_expense_sheets() + self.expense_sheet.action_sheet_move_create() + + self.assertEquals(len(self.sale_order.order_line), 3, "There should be 3 lines on the SO") + self.assertEquals(sale_order_line.qty_delivered, 0, "Exising SO line should not be impacted by reinvoicing product at cost") + + sol_ordered = self.sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_ordered_cost and sol != sale_order_line) + self.assertTrue(sol_ordered, "A new line with ordered expense should have been created on expense report posting") + self.assertEquals(sol_ordered.price_unit, expense1.unit_amount, "The unit price of new SO line should be the one from the expense (at cost)") + self.assertEquals(sol_ordered.product_uom_qty, 0, "The ordered quantity of new SO line should be zero") + self.assertEquals(sol_ordered.qty_delivered, expense1.quantity, "The delivered quantity of new SO line should be the one from the expense") + + sol_deliver = self.sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_deliver_cost and sol != sale_order_line) + self.assertTrue(sol_deliver, "A new line with delivered expense should have been created on expense report posting") + self.assertEquals(sol_deliver.price_unit, expense2.unit_amount, "The unit price of new SO line should be the one from the expense (at cost)") + self.assertEquals(sol_deliver.product_uom_qty, 0, "The ordered quantity of new SO line should be zero") + self.assertEquals(sol_deliver.qty_delivered, expense2.quantity, "The delivered quantity of new SO line should be the one from the expense") + + def test_sales_price_ordered(self): + """ Test invoicing expenses at sales price for product based on ordered quantities. """ + # confirm SO (with no line) + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + self.assertFalse(self.sale_order.analytic_account_id, "Confirming SO with no expense product should not trigger the analytic account creation") + + # create expense lines + expense1 = self.env['hr.expense'].create({ + 'name': 'Expense for ordered product at sales price', + 'employee_id': self.employee.id, + 'product_id': self.product_order_sales_price.id, + 'unit_amount': 15, + 'quantity': 2, + 'sheet_id': self.expense_sheet.id, + 'sale_order_id': self.sale_order.id, + 'analytic_account_id': self.sale_order.analytic_account_id.id, + }) + expense1._onchange_product_id() + + # approve and generate entries + self.expense_sheet.approve_expense_sheets() + self.expense_sheet.action_sheet_move_create() + + self.assertTrue(self.sale_order.analytic_account_id, "Posting expense with an expense product should trigger the analytic account creation on SO") + self.assertEquals(self.sale_order.analytic_account_id, expense1.analytic_account_id, "SO analytic account should be the same for the expense") + self.assertEquals(len(self.sale_order.order_line), 1, "A new So line should have been created on expense report posting") + + sol_ordered = self.sale_order.order_line.filtered(lambda sol: sol.product_id == expense1.product_id) + self.assertTrue(sol_ordered, "A new line with ordered expense should have been created on expense report posting") + self.assertEquals(sol_ordered.price_unit, 10, "The unit price of new SO line should be the one from the expense (at sales price)") + self.assertEquals(sol_ordered.product_uom_qty, 0, "The ordered quantity of new SO line should be zero") + self.assertEquals(sol_ordered.qty_delivered, expense1.quantity, "The delivered quantity of new SO line should be the one from the expense") + + def test_sales_price_delivered(self): + """ Test invoicing expenses at sales price for product based on delivered quantities. Check the existing SO line is incremented. """ + # create SO line and confirm SO (with only one line) + sale_order_line = self.env['sale.order.line'].create({ + 'name': self.product_deliver_sales_price.name, + 'product_id': self.product_deliver_sales_price.id, + 'product_uom_qty': 2, + 'product_uom': self.product_deliver_sales_price.uom_id.id, + 'price_unit': self.product_deliver_sales_price.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line.product_id_change() + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + self.assertTrue(self.sale_order.analytic_account_id, "Confirming SO with an expense product should trigger the analytic account creation") + + # create expense lines + expense1 = self.env['hr.expense'].create({ + 'name': 'Expense for delivered product at sales price', + 'employee_id': self.employee.id, + 'product_id': self.product_deliver_sales_price.id, + 'unit_amount': 15, + 'quantity': 3, + 'sheet_id': self.expense_sheet.id, + 'sale_order_id': self.sale_order.id, + 'analytic_account_id': self.sale_order.analytic_account_id.id, + }) + expense1._onchange_product_id() + + # approve and generate entries + self.expense_sheet.approve_expense_sheets() + self.expense_sheet.action_sheet_move_create() + + self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) on expense report posting") + + self.assertEquals(sale_order_line.price_unit, 10, "The unit price of SO line should be the same") + self.assertEquals(sale_order_line.product_uom_qty, 2, "The ordered quantity of new SO line should be zero") + self.assertEquals(sale_order_line.qty_delivered, expense1.quantity, "The delivered quantity of SO line should have been incremented") + + def test_no_expense(self): + """ Test invoicing expenses with no policy. Check nothing happen. """ + # confirm SO + sale_order_line = self.env['sale.order.line'].create({ + 'name': self.product_no_expense.name, + 'product_id': self.product_no_expense.id, + 'product_uom_qty': 2, + 'product_uom': self.product_no_expense.uom_id.id, + 'price_unit': self.product_no_expense.list_price, + 'order_id': self.sale_order.id, + }) + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + self.assertFalse(self.sale_order.analytic_account_id, "Confirming SO with an no-expense product should not trigger the analytic account creation") + + # create expense lines + expense1 = self.env['hr.expense'].create({ + 'name': 'Expense for no expense product', + 'employee_id': self.employee.id, + 'product_id': self.product_no_expense.id, + 'unit_amount': 15, + 'quantity': 3, + 'sheet_id': self.expense_sheet.id, + 'sale_order_id': self.sale_order.id, + 'analytic_account_id': self.sale_order.analytic_account_id.id, + }) + expense1._onchange_product_id() + + # approve and generate entries + self.expense_sheet.approve_expense_sheets() + self.expense_sheet.action_sheet_move_create() + + self.assertTrue(self.sale_order.analytic_account_id, "Posting expense with an expense product (even with no expense pilocy) should trigger the analytic account creation") + self.assertEquals(self.sale_order.analytic_account_id, expense1.analytic_account_id, "SO analytic account should be the same for the expense") + self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) on expense report posting") + + self.assertEquals(sale_order_line.price_unit, self.product_no_expense.list_price, "The unit price of SO line should be the same") + self.assertEquals(sale_order_line.product_uom_qty, 2, "The ordered quantity of SO line should be two") + self.assertEquals(sale_order_line.qty_delivered, 0, "The delivered quantity of SO line should have been incremented")