From 09afbf16084184f9527495adb284e8ab5a1c8232 Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Fri, 17 Jul 2020 14:13:37 +0000 Subject: [PATCH] [IMP] hr_expense,sale_expense: Remove dependency to AccountTestCommon l10n runbot builds are all failing when running at least one test depending of AccountTestCommon because it: - doesn't create a sandboxed testing environnement to manage the multi-currency, multi-company, the default company's currency, the exchange rates... - doesn't setup a testing user then all tests are done using the superuser. - doesn't provide a fully setup chart of accounts: exchange difference journal is not set, accounts have bad types, etc... - is run sometimes at-install. --task: 2301180 --- addons/hr_expense/tests/__init__.py | 5 +- addons/hr_expense/tests/common.py | 244 ++------ addons/hr_expense/tests/test_expenses.py | 581 ++++-------------- .../tests/test_expenses_access_rights.py | 115 ++++ .../tests/test_expenses_mail_import.py | 153 +++++ .../tests/test_expenses_mail_subjects.py | 103 ---- .../tests/test_expenses_multi_company.py | 116 ++++ addons/hr_expense/tests/test_multicompany.py | 75 --- addons/sale_expense/tests/test_reinvoice.py | 325 ++++------ 9 files changed, 662 insertions(+), 1055 deletions(-) create mode 100644 addons/hr_expense/tests/test_expenses_access_rights.py create mode 100644 addons/hr_expense/tests/test_expenses_mail_import.py delete mode 100644 addons/hr_expense/tests/test_expenses_mail_subjects.py create mode 100644 addons/hr_expense/tests/test_expenses_multi_company.py delete mode 100644 addons/hr_expense/tests/test_multicompany.py diff --git a/addons/hr_expense/tests/__init__.py b/addons/hr_expense/tests/__init__.py index cb3dbad9d88..54cba710445 100644 --- a/addons/hr_expense/tests/__init__.py +++ b/addons/hr_expense/tests/__init__.py @@ -3,5 +3,6 @@ from . import common from . import test_expenses -from . import test_expenses_mail_subjects -from . import test_multicompany +from . import test_expenses_access_rights +from . import test_expenses_mail_import +from . import test_expenses_multi_company diff --git a/addons/hr_expense/tests/common.py b/addons/hr_expense/tests/common.py index b5d0c0d8802..715cf29f149 100644 --- a/addons/hr_expense/tests/common.py +++ b/addons/hr_expense/tests/common.py @@ -1,225 +1,53 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.account.tests.common import AccountTestNoChartCommon, AccountTestNoChartCommonMultiCompany +from odoo.addons.account.tests.common import AccountTestInvoicingCommon -class TestExpenseCommon(AccountTestNoChartCommon): +class TestExpenseCommon(AccountTestInvoicingCommon): @classmethod - def setUpClass(cls): - super(TestExpenseCommon, cls).setUpClass() + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) - cls.setUpUsers() + Users = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True}) + group_user = cls.env.ref('base.group_user') + group_expense_manager = cls.env.ref('hr_expense.group_hr_expense_manager') - # The user manager is only expense manager - user_group_manager = cls.env.ref('hr_expense.group_hr_expense_manager') - cls.user_manager.write({ - 'groups_id': [(6, 0, [user_group_manager.id, cls.env.ref('base.group_user').id])], + cls.expense_user_employee = Users.create({ + 'name': 'expense_user_employee', + 'login': 'expense_user_employee', + 'email': 'expense_user_employee@example.com', + 'notification_type': 'email', + 'groups_id': [(6, 0, group_user.ids)], + 'company_ids': [(6, 0, cls.env.companies.ids)], + }) + cls.expense_user_manager = Users.create({ + 'name': 'Expense manager', + 'login': 'expense_manager_1', + 'email': 'expense_manager_1@example.com', + 'notification_type': 'email', + 'groups_id': [(6, 0, (group_user + group_expense_manager).ids)], + 'company_ids': [(6, 0, cls.env.companies.ids)], }) - # create employee - cls.employee = cls.env['hr.employee'].create({ - 'name': 'Johnny Employee', - 'user_id': cls.user_employee.id, - 'address_home_id': cls.user_employee.partner_id.id, - 'address_id': cls.user_employee.partner_id.id, + cls.expense_employee = cls.env['hr.employee'].create({ + 'name': 'expense_employee', + 'user_id': cls.expense_user_employee.id, + 'address_home_id': cls.expense_user_employee.partner_id.id, + 'address_id': cls.expense_user_employee.partner_id.id, }) - # Create tax - cls.tax = cls.env['account.tax'].create({ - 'name': 'Expense 10%', - 'amount': 10, - 'amount_type': 'percent', - 'type_tax_use': 'purchase', - 'price_include': True, - }) + # Allow the current accounting user to access the expenses. + cls.env.user.groups_id |= group_expense_manager # Create analytic account - cls.analytic_account = cls.env['account.analytic.account'].create({ - 'name': 'Test Analytic Account for Expenses', + cls.analytic_account_1 = cls.env['account.analytic.account'].create({ + 'name': 'analytic_account_1', + }) + cls.analytic_account_2 = cls.env['account.analytic.account'].create({ + 'name': 'analytic_account_2', }) - # Expense reports - cls.journal = cls.env['account.journal'].create({ - 'name': 'Purchase Journal - Test', - 'code': 'HRTPJ', - 'type': 'purchase', - 'company_id': cls.env.company.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, - }) - cls.expense_sheet2 = cls.env['hr.expense.sheet'].create({ - 'name': 'Second Expense for Johnny Employee', - 'employee_id': cls.employee.id, - 'journal_id': cls.journal.id, - }) - - Users = cls.env['res.users'].with_context(no_reset_password=True) - - # Find Employee group - group_employee_id = cls.env.ref('base.group_user').id - - cls.user_emp2 = Users.create({ - 'name': 'Superboy Employee', - 'login': 'superboy', - 'email': 'superboy@example.com', - 'groups_id': [(6, 0, [group_employee_id])] - }) - - cls.user_officer = Users.create({ - 'name': 'Batman Officer', - 'login': 'batman', - 'email': 'batman.hero@example.com', - 'groups_id': [(6, 0, [group_employee_id, cls.env.ref('hr_expense.group_hr_expense_team_approver').id])] - }) - - cls.emp_emp2 = cls.env['hr.employee'].create({ - 'name': 'Superboy', - 'user_id': cls.user_emp2.id, - }) - - cls.emp_officer = cls.env['hr.employee'].create({ - 'name': 'Batman', - 'user_id': cls.user_officer.id, - }) - - cls.emp_manager = cls.env['hr.employee'].create({ - 'name': 'Superman', - 'user_id': cls.user_manager.id, - }) - - cls.rd = cls.env['hr.department'].create({ - 'name': 'R&D', - 'manager_id': cls.emp_officer.id, - 'member_ids': [(6, 0, [cls.employee.id])], - }) - - cls.ps = cls.env['hr.department'].create({ - 'name': 'PS', - 'manager_id': cls.emp_manager.id, - 'member_ids': [(6, 0, [cls.emp_emp2.id])], - }) - - cls.uom_unit = cls.env.ref('uom.product_uom_unit').id - cls.uom_dozen = cls.env.ref('uom.product_uom_dozen').id - - cls.product_1 = cls.env['product.product'].create({ - 'name': 'Batmobile repair', - 'type': 'service', - 'uom_id': cls.uom_unit, - 'uom_po_id': cls.uom_unit, - }) - - cls.product_2 = cls.env['product.product'].create({ - 'name': 'Superboy costume washing', - 'type': 'service', - 'uom_id': cls.uom_unit, - 'uom_po_id': cls.uom_unit, - }) - - -class TestExpenseMultiCompanyCommon(AccountTestNoChartCommonMultiCompany): - - @classmethod - def setUpClass(cls): - super(TestExpenseMultiCompanyCommon, cls).setUpClass() - - cls.setUpAdditionalAccounts() - cls.setUpUsers() - - # The user manager is only expense manager - user_group_manager = cls.env.ref('hr_expense.group_hr_expense_manager') - cls.user_manager.write({ - 'groups_id': [(6, 0, [user_group_manager.id, cls.env.ref('base.group_user').id])], - }) - cls.user_manager_company_B.write({ - 'groups_id': [(6, 0, [user_group_manager.id, cls.env.ref('base.group_user').id])], - }) - - # create employee - cls.employee = cls.env['hr.employee'].create({ - 'name': 'Tyrion Lannister', - 'user_id': cls.user_employee.id, - 'address_home_id': cls.user_employee.partner_id.id, - 'address_id': cls.user_employee.partner_id.id, - }) - - cls.employee_company_B = cls.env['hr.employee'].create({ - 'name': 'Gregor Clegane', - 'user_id': cls.user_employee_company_B.id, - 'address_home_id': cls.user_employee_company_B.partner_id.id, - 'address_id': cls.user_employee_company_B.partner_id.id, - }) - - # Create tax - cls.tax = cls.env['account.tax'].create({ - 'name': 'Expense 10%', - 'amount': 10, - 'amount_type': 'percent', - 'type_tax_use': 'purchase', - 'price_include': True, - 'company_id': cls.env.company.id - }) - cls.tax_company_B = cls.env['account.tax'].create({ - 'name': 'Expense 10%', - 'amount': 10, - 'amount_type': 'percent', - 'type_tax_use': 'purchase', - 'price_include': True, - 'company_id': cls.company_B.id - }) - - # Create analytic account - cls.analytic_account = cls.env['account.analytic.account'].create({ - 'name': 'Test Analytic Account for Expenses', - 'company_id': cls.env.company.id, - }) - cls.analytic_account_company_B = cls.env['account.analytic.account'].create({ - 'name': 'Test Analytic Account for Expenses', - 'company_id': cls.company_B.id, - }) - - # Expense reports - cls.journal = cls.env['account.journal'].create({ - 'name': 'Purchase Journal - Test', - 'code': 'HRTPJ', - 'type': 'purchase', - 'company_id': cls.env.company.id, - }) - cls.journal_company_B = cls.env['account.journal'].create({ - 'name': 'Purchase Journal Company B - Test', - 'code': 'HRTPJ', - 'type': 'purchase', - 'company_id': cls.company_B.id, - }) - - cls.expense_sheet = cls.env['hr.expense.sheet'].create({ - 'name': 'Expense for Tyrion', - 'employee_id': cls.employee.id, - 'journal_id': cls.journal.id, - }) - cls.expense_sheet2 = cls.env['hr.expense.sheet'].create({ - 'name': 'Second Expense for Tyrion', - 'employee_id': cls.employee.id, - 'journal_id': cls.journal.id, - }) - - cls.product_1 = cls.env['product.product'].create({ - 'name': 'Sword sharpening', - 'type': 'service', - 'uom_id': cls.env.ref('uom.product_uom_unit').id, - 'uom_po_id': cls.env.ref('uom.product_uom_unit').id, - 'property_account_expense_id': cls.account_expense.id, - }) - - cls.product_2 = cls.env['product.product'].create({ - 'name': 'Armor cleaning', - 'type': 'service', - 'uom_id': cls.env.ref('uom.product_uom_unit').id, - 'uom_po_id': cls.env.ref('uom.product_uom_unit').id, - 'property_account_expense_id': cls.account_expense.id, - }) + # Ensure products can be expensed. + (cls.product_a + cls.product_b).write({'can_be_expensed': True}) diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py index 0e12df38cfb..3a0224a0907 100644 --- a/addons/hr_expense/tests/test_expenses.py +++ b/addons/hr_expense/tests/test_expenses.py @@ -1,469 +1,140 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.exceptions import UserError, AccessError - from odoo.addons.hr_expense.tests.common import TestExpenseCommon +from odoo.tests import tagged +from odoo import fields -class TestAccountEntry(TestExpenseCommon): - """ - Check journal entries when the expense product is having tax which is tax included. - """ +@tagged('-at_install', 'post_install') +class TestExpenses(TestExpenseCommon): - def setUp(self): - super(TestAccountEntry, self).setUp() - - self.setUpAdditionalAccounts() - - self.product_expense = self.env['product.product'].create({ - 'name': "Delivered at cost", - 'standard_price': 700, - 'list_price': 700, - 'type': 'consu', - 'supplier_taxes_id': [(6, 0, [self.tax.id])], - 'default_code': 'CONSU-DELI-COST', - 'taxes_id': False, - 'property_account_expense_id': self.account_expense.id, - }) - - def test_account_entry(self): + def test_expense_values(self): """ Checking accounting move entries and analytic entries when submitting expense """ - expense = self.env['hr.expense.sheet'].create({ - 'name': 'Expense for John Smith', - 'employee_id': self.employee.id, - }) - expense_line = self.env['hr.expense'].create({ - 'name': 'Car Travel Expenses', - 'employee_id': self.employee.id, - 'product_id': self.product_expense.id, - 'unit_amount': 700.00, - 'tax_ids': [(6, 0, [self.tax.id])], - 'sheet_id': expense.id, - 'analytic_account_id': self.analytic_account.id, - }) - # State should default to draft - self.assertEqual(expense.state, 'draft', 'Expense should be created in Draft state') - # Submitted to Manager - expense.action_submit_sheet() - self.assertEqual(expense.state, 'submit', 'Expense is not in Reported state') - # Approve - expense.approve_expense_sheets() - self.assertEqual(expense.state, 'approve', 'Expense is not in Approved state') - # Create Expense Entries - expense.action_sheet_move_create() - self.assertEqual(expense.state, 'post', 'Expense is not in Waiting Payment state') - self.assertTrue(expense.account_move_id.id, 'Expense Journal Entry is not created') - # [(line.debit, line.credit, line.tax_line_id.id) for line in self.expense.expense_line_ids.account_move_id.line_ids] - # should git this result [(0.0, 700.0, False), (63.64, 0.0, 179), (636.36, 0.0, False)] - for line in expense.account_move_id.line_ids: - if line.credit: - self.assertAlmostEqual(line.credit, 700.00) - self.assertEqual(len(line.analytic_line_ids), 0, "The credit move line should not have analytic lines") - self.assertFalse(line.product_id, "Product of credit move line should be false") - else: - if not line.tax_line_id == self.tax: - self.assertAlmostEqual(line.debit, 636.36) - self.assertEqual(len(line.analytic_line_ids), 1, "The debit move line should have 1 analytic lines") - self.assertEqual(line.product_id, self.product_expense, "Product of debit move line should be the one from the expense") - else: - self.assertAlmostEqual(line.debit, 63.64) - self.assertEqual(len(line.analytic_line_ids), 0, "The tax move line should not have analytic lines") - self.assertFalse(line.product_id, "Product of tax move line should be false") + # The expense employee is able to a create an expense sheet. + # The total should be 1725.0 because: + # - first line: 1000.0 (unit amount) + 150.0 (tax) = 1150.0 + # - second line: (1500.0 (unit amount) + 225.0 (tax)) * 1/3 (rate) = 575.0. - self.assertEqual(self.analytic_account.line_ids, expense.account_move_id.mapped('line_ids.analytic_line_ids')) - self.assertEqual(len(self.analytic_account.line_ids), 1, "Analytic Account should have only one line") - self.assertAlmostEqual(self.analytic_account.line_ids[0].amount, -636.36, "Amount on the only AAL is wrong") - self.assertEqual(self.analytic_account.line_ids[0].product_id, self.product_expense, "Product of AAL should be the one from the expense") - - def test_account_entry_multi_currency(self): - """ Checking accounting move entries and analytic entries when submitting expense. With - multi-currency. And taxes. """ - - # Clean-up the rates - self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.env.ref('base.USD').id, self.env.company.id]) - self.env['res.currency.rate'].search([]).unlink() - self.env['res.currency.rate'].create({ - 'currency_id': self.env.ref('base.EUR').id, - 'company_id': self.env.company.id, - 'rate': 2.0, - 'name': '2010-01-01', + expense_sheet = self.env['hr.expense.sheet'].create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data['default_journal_purchase'].id, + 'accounting_date': '2017-01-01', + 'expense_line_ids': [ + (0, 0, { + # Expense without foreign currency. + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_a.id, + 'unit_amount': 1000.0, + 'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)], + 'analytic_account_id': self.analytic_account_1.id, + 'employee_id': self.expense_employee.id, + }), + (0, 0, { + # Expense with foreign currency (rate 1:3). + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_b.id, + 'unit_amount': 1500.0, + 'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)], + 'analytic_account_id': self.analytic_account_2.id, + 'currency_id': self.currency_data['currency'].id, + 'employee_id': self.expense_employee.id, + }), + ], }) - expense = self.env['hr.expense.sheet'].create({ - 'name': 'Expense for Dick Tracy', - 'employee_id': self.employee.id, - }) - expense_line = self.env['hr.expense'].create({ - 'name': 'Choucroute Saucisse', - 'employee_id': self.employee.id, - 'product_id': self.product_expense.id, - 'unit_amount': 700.00, - 'tax_ids': [(6, 0, [self.tax.id])], - 'sheet_id': expense.id, - 'analytic_account_id': self.analytic_account.id, - 'currency_id': self.env.ref('base.EUR').id, - }) - # State should default to draft - self.assertEqual(expense.state, 'draft', 'Expense should be created in Draft state') - # Submitted to Manager - expense.action_submit_sheet() - self.assertEqual(expense.state, 'submit', 'Expense is not in Reported state') - # Approve - expense.approve_expense_sheets() - self.assertEqual(expense.state, 'approve', 'Expense is not in Approved state') - # Create Expense Entries - expense.action_sheet_move_create() - self.assertEqual(expense.state, 'post', 'Expense is not in Waiting Payment state') - self.assertTrue(expense.account_move_id.id, 'Expense Journal Entry is not created') + # Check expense sheet values. + self.assertRecordValues(expense_sheet, [{'state': 'draft', 'total_amount': 1725.0}]) - # Should get this result [(0.0, 350.0, -700.0), (318.18, 0.0, 636.36), (31.82, 0.0, 63.64)] - for line in expense.account_move_id.line_ids: - if line.credit: - self.assertAlmostEqual(line.credit, 350.0) - self.assertAlmostEqual(line.amount_currency, -700.0) - self.assertEqual(len(line.analytic_line_ids), 0, "The credit move line should not have analytic lines") - self.assertFalse(line.product_id, "Product of credit move line should be false") - else: - if not line.tax_line_id == self.tax: - self.assertAlmostEqual(line.debit, 318.18) - self.assertAlmostEqual(line.amount_currency, 636.36) - self.assertEqual(len(line.analytic_line_ids), 1, "The debit move line should have 1 analytic lines") - self.assertEqual(line.product_id, self.product_expense, "Product of debit move line should be the one from the expense") - else: - self.assertAlmostEqual(line.debit, 31.82) - self.assertAlmostEqual(line.amount_currency, 63.64) - self.assertEqual(len(line.analytic_line_ids), 0, "The tax move line should not have analytic lines") - self.assertFalse(line.product_id, "Product of tax move line should be false") + expense_sheet.action_submit_sheet() + expense_sheet.approve_expense_sheets() + expense_sheet.action_sheet_move_create() - self.assertEqual(self.analytic_account.line_ids, expense.account_move_id.mapped('line_ids.analytic_line_ids')) - self.assertEqual(len(self.analytic_account.line_ids), 1, "Analytic Account should have only one line") - self.assertAlmostEqual(self.analytic_account.line_ids[0].amount, -318.18, "Amount on the only AAL is wrong") - self.assertAlmostEqual(self.analytic_account.line_ids[0].currency_id, self.env.company.currency_id, "Currency on the only AAL is wrong") - self.assertEqual(self.analytic_account.line_ids[0].product_id, self.product_expense, "Product of AAL should be the one from the expense") + # Check expense sheet journal entry values. + self.assertRecordValues(expense_sheet.account_move_id.line_ids.sorted('balance'), [ + # Receivable line (company currency): + { + 'debit': 0.0, + 'credit': 1150.0, + 'amount_currency': 0.0, + 'account_id': self.company_data['default_account_payable'].id, + 'product_id': False, + 'currency_id': False, + 'tax_line_id': False, + 'analytic_account_id': False, + }, + # Receivable line (foreign currency): + { + 'debit': 0.0, + 'credit': 862.5, + 'amount_currency': -1725.0, + 'account_id': self.company_data['default_account_payable'].id, + 'product_id': False, + 'currency_id': self.currency_data['currency'].id, + 'tax_line_id': False, + 'analytic_account_id': False, + }, + # Tax line (foreign currency): + { + 'debit': 112.5, + 'credit': 0.0, + 'amount_currency': 225.0, + 'account_id': self.company_data['default_account_tax_purchase'].id, + 'product_id': False, + 'currency_id': self.currency_data['currency'].id, + 'tax_line_id': self.company_data['default_tax_purchase'].id, + 'analytic_account_id': False, + }, + # Tax line (company currency): + { + 'debit': 150.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'account_id': self.company_data['default_account_tax_purchase'].id, + 'product_id': False, + 'currency_id': False, + 'tax_line_id': self.company_data['default_tax_purchase'].id, + 'analytic_account_id': False, + }, + # Product line (foreign currency): + { + 'debit': 750.0, + 'credit': 0.0, + 'amount_currency': 1500.0, + 'account_id': self.company_data['default_account_expense'].id, + 'product_id': self.product_b.id, + 'currency_id': self.currency_data['currency'].id, + 'tax_line_id': False, + 'analytic_account_id': self.analytic_account_2.id, + }, + # Product line (company currency): + { + 'debit': 1000.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'account_id': self.company_data['default_account_expense'].id, + 'product_id': self.product_a.id, + 'currency_id': False, + 'tax_line_id': False, + 'analytic_account_id': self.analytic_account_1.id, + }, + ]) - def test_expense_from_email(self): - user_marc = self.env['res.users'].create({ - 'name': 'Marc User', - 'login': 'Marc', - 'email': 'marc.user@example.com', - }) - self.env['hr.employee'].create({ - 'name': 'Marc Demo', - 'user_id': user_marc.id, - }) - air_ticket = self.env['product.product'].create({ - 'name': 'Air Flight', - 'type': 'service', - 'default_code': 'TESTREF', - 'can_be_expensed': True, - }) - - self.tax.price_include = False - - message_parsed = { - 'message_id': 'the-world-is-a-ghetto', - 'subject': 'TESTREF 9876', - 'email_from': 'marc.user@example.com', - 'to': 'catchall@yourcompany.com', - 'body': "Don't you know, that for me, and for you", - 'attachments': [], - } - - expense = self.env['hr.expense'].message_new(message_parsed) - - self.assertEqual(expense.product_id, air_ticket) - self.assertEqual(expense.tax_ids.ids, []) - self.assertEqual(expense.total_amount, 9876.0) - self.assertTrue(expense.employee_id in user_marc.employee_ids) - - def test_expense_from_email_without_product(self): - user_marc = self.env['res.users'].create({ - 'name': 'Marc User', - 'login': 'Marc', - 'email': 'marc.user@example.com', - }) - self.env['hr.employee'].create({ - 'name': 'Marc Demo', - 'user_id': user_marc.id, - }) - - self.tax.price_include = False - - message_parsed = { - 'message_id': 'the-world-is-a-ghetto', - 'subject': 'no product code 9876', - 'email_from': 'marc.user@example.com', - 'to': 'catchall@yourcompany.com', - 'body': "Don't you know, that for me, and for you", - 'attachments': [], - } - - expense = self.env['hr.expense'].message_new(message_parsed) - - self.assertFalse(expense.product_id, "No product should be linked") - self.assertEqual(expense.tax_ids.ids, []) - self.assertEqual(expense.total_amount, 9876.0) - self.assertTrue(expense.employee_id in user_marc.employee_ids) - - def test_partial_payment_multiexpense(self): - bank_journal = self.env['account.journal'].create({ - 'name': 'Payment Journal', - 'code': 'PAY', - 'type': 'bank', - 'company_id': self.env.company.id, - }) - - outbound_pay_method = self.env['account.payment.method'].create({ - 'name': 'outbound', - 'code': 'out', - 'payment_type': 'outbound', - }) - - expense = self.env['hr.expense.sheet'].create({ - 'name': 'Expense for John Smith', - 'employee_id': self.employee.id, - }) - expense_line = self.env['hr.expense'].create({ - 'name': 'Car Travel Expenses', - 'employee_id': self.employee.id, - 'product_id': self.product_expense.id, - 'unit_amount': 200.00, - 'tax_ids': [(6, 0, [self.tax.id])], - 'sheet_id': expense.id, - 'analytic_account_id': self.analytic_account.id, - }) - expense_line.copy({ - 'sheet_id': expense.id - }) - expense.approve_expense_sheets() - expense.action_sheet_move_create() - - exp_move_lines = expense.account_move_id.line_ids - payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') - self.assertEqual(len(payable_move_lines), 2) - - WizardRegister = self.env["hr.expense.sheet.register.payment.wizard"].with_context( - active_model=expense._name, active_id=expense.id, active_ids=expense.ids - ) - - register_pay1 = WizardRegister.create({ - 'journal_id': bank_journal.id, - 'payment_method_id': outbound_pay_method.id, - 'amount': 300, - }) - register_pay1.expense_post_payment() - - exp_move_lines = expense.account_move_id.line_ids - payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') - self.assertEqual(len(payable_move_lines.filtered(lambda l: l.reconciled)), 1) - - register_pay2 = WizardRegister.create({ - 'journal_id': bank_journal.id, - 'payment_method_id': outbound_pay_method.id, - 'amount': 100, - }) - register_pay2.expense_post_payment() - exp_move_lines = expense.account_move_id.line_ids - payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') - self.assertEqual(len(payable_move_lines.filtered(lambda l: l.reconciled)), 2) - - full_reconcile = payable_move_lines.mapped('full_reconcile_id') - self.assertEqual(len(full_reconcile), 1) - - -class TestExpenseRights(TestExpenseCommon): - - @classmethod - def setUpClass(cls): - super(TestExpenseRights, cls).setUpClass() - - def test_expense_create(self): - # Employee should be able to create an Expense - self.env['hr.expense'].with_user(self.user_employee).create({ - 'name': 'Batmobile repair', - 'employee_id': self.employee.id, - 'product_id': self.product_1.id, - 'unit_amount': 1, - 'quantity': 1, - }) - - # Employee should not be able to create an Expense for someone else - with self.assertRaises(AccessError): - self.env['hr.expense'].with_user(self.user_employee).create({ - 'name': 'Superboy costume washing', - 'employee_id': self.emp_emp2.id, - 'product_id': self.product_2.id, - 'unit_amount': 1, - 'quantity': 1, - }) - - def test_expense_approve(self): - sheet = self.env['hr.expense.sheet'].create({ - 'name': 'Furnitures', - 'employee_id': self.emp_officer.id, - }) - - sheet_2 = self.env['hr.expense.sheet'].create({ - 'name': 'Services', - 'employee_id': self.employee.id, - }) - - sheet_3 = self.env['hr.expense.sheet'].create({ - 'name': 'Services 2', - 'employee_id': self.emp_emp2.id, - }) - - # Employee should not be able to approve expense sheet - with self.assertRaises(UserError): - sheet.with_user(self.user_officer).approve_expense_sheets() - # Officer should not be able to approve own expense sheet - with self.assertRaises(UserError): - sheet.with_user(self.user_officer).approve_expense_sheets() - sheet.with_user(self.user_manager).approve_expense_sheets() - - # Officer should be able to approve expense from his department - sheet_2.with_user(self.user_officer).approve_expense_sheets() - - # Officer should not be able to approve expense sheet from another department - with self.assertRaises(AccessError): - sheet_3.with_user(self.user_officer).approve_expense_sheets() - sheet_3.with_user(self.user_manager).approve_expense_sheets() - - def test_expense_refuse(self): - sheet = self.env['hr.expense.sheet'].create({ - 'name': 'Furnitures', - 'employee_id': self.emp_officer.id, - }) - - sheet_2 = self.env['hr.expense.sheet'].create({ - 'name': 'Services', - 'employee_id': self.employee.id, - }) - - sheet_3 = self.env['hr.expense.sheet'].create({ - 'name': 'Services 2', - 'employee_id': self.emp_emp2.id, - }) - - sheet.with_user(self.user_manager).approve_expense_sheets() - sheet_2.with_user(self.user_manager).approve_expense_sheets() - sheet_3.with_user(self.user_manager).approve_expense_sheets() - - # Employee should not be able to refuse expense sheet - with self.assertRaises(UserError): - sheet.with_user(self.user_employee).refuse_sheet('') - # Officer should not be able to refuse own expense sheet - with self.assertRaises(UserError): - sheet.with_user(self.user_officer).refuse_sheet('') - sheet.with_user(self.user_manager).refuse_sheet('') - - # Officer should be able to refuse expense from his department - sheet_2.with_user(self.user_officer).refuse_sheet('') - - # Officer should not be able to refuse expense sheet from another department - with self.assertRaises(AccessError): - sheet_3.with_user(self.user_officer).refuse_sheet('') - sheet_3.with_user(self.user_manager).refuse_sheet('') - - -class TestExpenseLinesRights(TestExpenseCommon): - - def setUp(self): - super(TestExpenseLinesRights, self).setUp() - - self.setUpAdditionalAccounts() - - self.product_expense = self.env['product.product'].create({ - 'name': "Delivered at cost", - 'standard_price': 700, - 'list_price': 700, - 'type': 'consu', - 'supplier_taxes_id': [(6, 0, [self.tax.id])], - 'default_code': 'CONSU-DELI-COST', - 'taxes_id': False, - 'property_account_expense_id': self.account_expense.id, - }) - - self.user_manager.write({ - 'groups_id': [(4, self.env.ref('account.group_account_user').id)], - }) - - def test_expense_lines_rights(self): - expense = self.env['hr.expense.sheet'].with_user(self.user_employee).create({ - 'name': 'Expense for John Smith', - 'employee_id': self.employee.id, - }) - expense_line = self.env['hr.expense'].with_user(self.user_employee).create({ - 'name': 'Car Travel Expenses', - 'employee_id': self.employee.id, - 'product_id': self.product_expense.id, - 'unit_amount': 700.00, - 'tax_ids': [(6, 0, [self.tax.id])], - 'sheet_id': expense.id, - 'analytic_account_id': self.analytic_account.id, - }) - expense.with_user(self.user_employee).action_submit_sheet() - - # STATE APPROVE - - expense.with_user(self.user_manager).approve_expense_sheets() - # Test User without Accountant Rights - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'reference': 'Test Reference'}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'tax_ids': [(6, 0, [self.tax.id])]}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'account_id': self.account_expense.id}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'analytic_account_id': self.analytic_account.id}) - # Test User with Accountant Rights - expense_line.with_user(self.user_manager).write({'reference': 'Test Reference'}) - expense_line.with_user(self.user_manager).write({'tax_ids': [(6, 0, [self.tax.id])]}) - expense_line.with_user(self.user_manager).write({'account_id': self.account_expense.id}) - expense_line.with_user(self.user_manager).write({'analytic_account_id': self.analytic_account.id}) - expense_line.invalidate_cache() - - # STATE POST - - expense.with_user(self.env.user).action_sheet_move_create() - # Test User without Accountant Rights - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'reference': 'Test Reference'}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'tax_ids': [(6, 0, [self.tax.id])]}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'account_id': self.account_expense.id}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'analytic_account_id': self.analytic_account.id}) - # Test User with Accountant Rights - expense_line.with_user(self.user_manager).write({'reference': 'Test Reference'}) - expense_line.invalidate_cache() - with self.assertRaises(UserError): - expense_line.with_user(self.user_manager).write({'tax_ids': [(6, 0, [self.tax.id])]}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_manager).write({'account_id': self.account_expense.id}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_manager).write({'analytic_account_id': self.analytic_account.id}) - - # STATE DONE - - expense.set_to_paid() - # Test User without Accountant Rights - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'reference': 'Test Reference'}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'tax_ids': [(6, 0, [self.tax.id])]}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'account_id': self.account_expense.id}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_employee).write({'analytic_account_id': self.analytic_account.id}) - # Test User with Accountant Rights - expense_line.with_user(self.user_manager).write({'reference': 'Test Reference'}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_manager).write({'tax_ids': [(6, 0, [self.tax.id])]}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_manager).write({'account_id': self.account_expense.id}) - with self.assertRaises(UserError): - expense_line.with_user(self.user_manager).write({'analytic_account_id': self.analytic_account.id}) + # Check expense analytic lines. + self.assertRecordValues(expense_sheet.account_move_id.line_ids.analytic_line_ids.sorted('amount'), [ + { + 'amount': -1000.0, + 'date': fields.Date.from_string('2017-01-01'), + 'account_id': self.analytic_account_1.id, + 'currency_id': self.company_data['currency'].id, + }, + { + 'amount': -750.0, + 'date': fields.Date.from_string('2017-01-01'), + 'account_id': self.analytic_account_2.id, + 'currency_id': self.company_data['currency'].id, + }, + ]) diff --git a/addons/hr_expense/tests/test_expenses_access_rights.py b/addons/hr_expense/tests/test_expenses_access_rights.py new file mode 100644 index 00000000000..01043a7393f --- /dev/null +++ b/addons/hr_expense/tests/test_expenses_access_rights.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.hr_expense.tests.common import TestExpenseCommon +from odoo.exceptions import AccessError, UserError +from odoo.tests import tagged + + +@tagged('-at_install', 'post_install') +class TestExpensesAccessRights(TestExpenseCommon): + + def test_expense_access_rights(self): + ''' The expense employee can't be able to create an expense for someone else.''' + + expense_employee_2 = self.env['hr.employee'].create({ + 'name': 'expense_employee_2', + 'user_id': self.env.user.id, + 'address_home_id': self.env.user.partner_id.id, + 'address_id': self.env.user.partner_id.id, + }) + + with self.assertRaises(AccessError): + self.env['hr.expense'].with_user(self.expense_user_employee).create({ + 'name': "Superboy costume washing", + 'employee_id': expense_employee_2.id, + 'product_id': self.product_a.id, + 'quantity': 1, + 'unit_amount': 1, + }) + + def test_expense_sheet_access_rights_approve(self): + + # The expense employee is able to a create an expense sheet. + + expense_sheet = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data['default_journal_purchase'].id, + 'accounting_date': '2017-01-01', + 'expense_line_ids': [ + (0, 0, { + # Expense without foreign currency but analytic account. + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_a.id, + 'unit_amount': 1000.0, + 'employee_id': self.expense_employee.id, + }), + ], + }) + self.assertRecordValues(expense_sheet, [{'state': 'draft'}]) + + # The expense employee is able to submit the expense sheet. + + expense_sheet.with_user(self.expense_user_employee).action_submit_sheet() + self.assertRecordValues(expense_sheet, [{'state': 'submit'}]) + + # The expense employee is not able to approve itself the expense sheet. + + with self.assertRaises(UserError): + expense_sheet.with_user(self.expense_user_employee).approve_expense_sheets() + self.assertRecordValues(expense_sheet, [{'state': 'submit'}]) + + # An expense manager is required for this step. + + expense_sheet.with_user(self.expense_user_manager).approve_expense_sheets() + self.assertRecordValues(expense_sheet, [{'state': 'approve'}]) + + # An expense manager is not able to create the journal entry. + + with self.assertRaises(UserError): + expense_sheet.with_user(self.expense_user_manager).action_sheet_move_create() + self.assertRecordValues(expense_sheet, [{'state': 'approve'}]) + + # An expense manager having accounting access rights is able to create the journal entry. + + expense_sheet.with_user(self.env.user).action_sheet_move_create() + self.assertRecordValues(expense_sheet, [{'state': 'post'}]) + + def test_expense_sheet_access_rights_refuse(self): + + # The expense employee is able to a create an expense sheet. + + expense_sheet = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data['default_journal_purchase'].id, + 'accounting_date': '2017-01-01', + 'expense_line_ids': [ + (0, 0, { + # Expense without foreign currency but analytic account. + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_a.id, + 'unit_amount': 1000.0, + 'employee_id': self.expense_employee.id, + }), + ], + }) + self.assertRecordValues(expense_sheet, [{'state': 'draft'}]) + + # The expense employee is able to submit the expense sheet. + + expense_sheet.with_user(self.expense_user_employee).action_submit_sheet() + self.assertRecordValues(expense_sheet, [{'state': 'submit'}]) + + # The expense employee is not able to refuse itself the expense sheet. + + with self.assertRaises(UserError): + expense_sheet.with_user(self.expense_user_employee).refuse_sheet('') + self.assertRecordValues(expense_sheet, [{'state': 'submit'}]) + + # An expense manager is required for this step. + + expense_sheet.with_user(self.expense_user_manager).refuse_sheet('') + self.assertRecordValues(expense_sheet, [{'state': 'cancel'}]) diff --git a/addons/hr_expense/tests/test_expenses_mail_import.py b/addons/hr_expense/tests/test_expenses_mail_import.py new file mode 100644 index 00000000000..9f6db49a09d --- /dev/null +++ b/addons/hr_expense/tests/test_expenses_mail_import.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.hr_expense.tests.common import TestExpenseCommon +from odoo.tests import tagged + + +@tagged('-at_install', 'post_install') +class TestExpensesMailImport(TestExpenseCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.product_a.default_code = 'product_a' + cls.product_b.default_code = 'product_b' + + def test_import_expense_from_email(self): + message_parsed = { + 'message_id': "the-world-is-a-ghetto", + 'subject': '%s %s' % (self.product_a.default_code, self.product_a.standard_price), + 'email_from': self.expense_user_employee.email, + 'to': 'catchall@yourcompany.com', + 'body': "Don't you know, that for me, and for you", + 'attachments': [], + } + + expense = self.env['hr.expense'].message_new(message_parsed) + + self.assertRecordValues(expense, [{ + 'product_id': self.product_a.id, + 'total_amount': 920.0, + 'employee_id': self.expense_employee.id, + }]) + + def test_import_expense_from_email_no_product(self): + message_parsed = { + 'message_id': "the-world-is-a-ghetto", + 'subject': 'no product code 800', + 'email_from': self.expense_user_employee.email, + 'to': 'catchall@yourcompany.com', + 'body': "Don't you know, that for me, and for you", + 'attachments': [], + } + + expense = self.env['hr.expense'].message_new(message_parsed) + + self.assertRecordValues(expense, [{ + 'product_id': False, + 'total_amount': 800.0, + 'employee_id': self.expense_employee.id, + }]) + + def test_import_expense_from_mail_parsing_subjects(self): + + def assertParsedValues(subject, currencies, exp_description, exp_amount, exp_product): + product, amount, currency_id, description = self.env['hr.expense']\ + .with_user(self.expense_user_employee)\ + ._parse_expense_subject(subject, currencies) + + self.assertEqual(product, exp_product) + self.assertAlmostEqual(amount, exp_amount) + self.assertEqual(description, exp_description) + + # Without Multi currency access + assertParsedValues( + "product_a bar $1205.91 electro wizard", + self.company_data['currency'], + "bar electro wizard", + 1205.91, + self.product_a, + ) + + # subject having other currency then company currency, it should ignore other currency then company currency + assertParsedValues( + "foo bar %s1406.91 royal giant" % self.currency_data['currency'].symbol, + self.company_data['currency'], + "foo bar %s royal giant" % self.currency_data['currency'].symbol, + 1406.91, + self.env['product.product'], + ) + + # With Multi currency access + self.expense_user_employee.groups_id |= self.env.ref('base.group_multi_currency') + + assertParsedValues( + "product_a foo bar $2205.92 elite barbarians", + self.company_data['currency'], + "foo bar elite barbarians", + 2205.92, + self.product_a, + ) + + # subject having other currency then company currency, it should accept other currency because multi currency is activated + assertParsedValues( + "product_a %s2510.90 chhota bheem" % self.currency_data['currency'].symbol, + self.company_data['currency'] + self.currency_data['currency'], + "chhota bheem", + 2510.90, + self.product_a, + ) + + # subject without product and currency, should take company currency and default product + assertParsedValues( + "foo bar 109.96 spear goblins", + self.company_data['currency'] + self.currency_data['currency'], + "foo bar spear goblins", + 109.96, + self.env['product.product'], + ) + + # subject with currency symbol at end + assertParsedValues( + "product_a foo bar 2910.94$ inferno dragon", + self.company_data['currency'] + self.currency_data['currency'], + "foo bar inferno dragon", + 2910.94, + self.product_a, + ) + + # subject with no amount and product + assertParsedValues( + "foo bar mega knight", + self.company_data['currency'] + self.currency_data['currency'], + "foo bar mega knight", + 0.0, + self.env['product.product'], + ) + + # price with a comma + assertParsedValues( + "foo bar 291,56$ mega knight", + self.company_data['currency'] + self.currency_data['currency'], + "foo bar mega knight", + 291.56, + self.env['product.product'], + ) + + # price without decimals + assertParsedValues( + "foo bar 291$ mega knight", + self.company_data['currency'] + self.currency_data['currency'], + "foo bar mega knight", + 291.0, + self.env['product.product'], + ) + + assertParsedValues( + "product_a foo bar 291.5$ mega knight", + self.company_data['currency'] + self.currency_data['currency'], + "foo bar mega knight", + 291.5, + self.product_a, + ) diff --git a/addons/hr_expense/tests/test_expenses_mail_subjects.py b/addons/hr_expense/tests/test_expenses_mail_subjects.py deleted file mode 100644 index 213060e6d48..00000000000 --- a/addons/hr_expense/tests/test_expenses_mail_subjects.py +++ /dev/null @@ -1,103 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.hr_expense.tests.common import TestExpenseCommon - - -class TestExpenseSubject(TestExpenseCommon): - """ - Check subject parsing while registering expense via mail. - """ - - def setUp(self): - super(TestExpenseSubject, self).setUp() - self.product_expense = self.env['product.product'].create({ - 'name': "Phone bill", - 'can_be_expensed': True, - 'standard_price': 700, - 'list_price': 700, - 'type': 'consu', - 'default_code': 'EXP-PHONE' - }) - - def test_expense_subjects(self): - employee = self.employee - employee.user_id = self.user_employee - parse_subject = self.env['hr.expense']._parse_expense_subject - company_currency = employee.company_id.currency_id - alternate_currency = self.env['res.currency'].create({'name': 'AAB', 'symbol': '#'}) - - # Without Multi currency access - subject = 'EXP-PHONE bar %s1205.91 electro wizard' % (company_currency.symbol,) - - product, price, currency_id, expense_description = parse_subject(subject, company_currency) - self.assertEqual(expense_description, 'bar electro wizard', "Should be remove price and product from subject") - self.assertAlmostEqual(price, 1205.91, "Price is not fetched correctly") - self.assertEqual(currency_id, company_currency, "Should fetch currency correctly") - self.assertEqual(product, self.product_expense, "Should fetch product correctly") - - # subject having other currency then company currency, it should ignore other currency then company currency - subject = 'foo bar %s1406.91 royal giant' % (alternate_currency.symbol,) - product, price, currency_id, expense_description = parse_subject(subject, company_currency) - self.assertEqual(expense_description, 'foo bar %s royal giant' % (alternate_currency.symbol,), "Should be remove price and product from subject but not currency symbol") - self.assertEqual(currency_id, company_currency, "Should fetch currency correctly") - - # With Multi currency access - group_multi_currency = self.env.ref('base.group_multi_currency') - self.user_employee.write({ - 'groups_id': [(4, group_multi_currency.id)], - }) - - subject = 'EXP-PHONE foo bar %s2205.92 elite barbarians' % (company_currency.symbol,) # with product code at start - product, price, currency_id, expense_description = parse_subject(subject, company_currency) - self.assertEqual(expense_description, 'foo bar elite barbarians', "Should be remove price and product from subject") - self.assertAlmostEqual(price, 2205.92, "Price is not fetched correctly") - self.assertEqual(currency_id, company_currency, "Should fetch currency correctly") - self.assertEqual(product, self.product_expense, "Should fetch product correctly") - - # subject having other currency then company currency, it should accept other currency because multi currency is activated - subject = 'EXP-PHONE %s2510.90 chhota bheem' % (alternate_currency.symbol,) - product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency) - self.assertEqual(expense_description, 'chhota bheem', "Should be remove price and product from subject but not currency symbol") - self.assertAlmostEqual(price, 2510.90, "Price is not fetched correctly") - self.assertEqual(currency_id, alternate_currency, "Should fetch currency correctly") - self.assertEqual(product, self.product_expense, "Should fetch product correctly") - - # subject without product and currency, should take company currency and default product - subject = 'foo bar 109.96 spear goblins' - product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency) - self.assertEqual(expense_description, 'foo bar spear goblins', "Should remove price") - self.assertAlmostEqual(price, 109.96, "Price is not fetched correctly") - self.assertIn(currency_id, company_currency | alternate_currency, "Should fetch company currency") - self.assertFalse(product, "Should not have parsed any product") - - # subject with currency symbol at end - subject = 'EXP-PHONE foo bar 2910.94%s inferno dragon' % (company_currency.symbol,) - product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency) - self.assertEqual(expense_description, 'foo bar inferno dragon', "Should be remove price and product from subject") - self.assertAlmostEqual(price, 2910.94, "Price is not fetched correctly") - self.assertEqual(currency_id, company_currency, "Should fetch currency correctly") - self.assertEqual(product, self.product_expense, "Should fetch product correctly") - - # subject with no amount and product - subject = 'foo bar mega knight' - product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency) - self.assertEqual(expense_description, 'foo bar mega knight', "Should be same as subject") - self.assertAlmostEqual(price, 0.0, "Price is not fetched correctly") - self.assertIn(currency_id, company_currency | alternate_currency, "Should fetch currency correctly") - self.assertFalse(product, "Should fetch product correctly") - - # price with a comma - subject = 'foo bar 291,56%s mega knight' % (company_currency.symbol,) - product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency) - self.assertAlmostEqual(price, 291.56, "Price is not fetched correctly") - self.assertEqual(currency_id, company_currency, "Should fetch currency correctly") - - # price without decimals - subject = 'foo bar 291%s mega knight' % (company_currency.symbol,) - product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency) - self.assertAlmostEqual(price, 291.0, "Price is not fetched correctly") - - subject = 'EXP-PHONE 2 foo bar 291.5%s mega knight' % (company_currency.symbol,) - product, price, currency_id, expense_description = parse_subject(subject, company_currency | alternate_currency) - self.assertAlmostEqual(price, 291.5, "Price is not fetched correctly") diff --git a/addons/hr_expense/tests/test_expenses_multi_company.py b/addons/hr_expense/tests/test_expenses_multi_company.py new file mode 100644 index 00000000000..9ad549088ff --- /dev/null +++ b/addons/hr_expense/tests/test_expenses_multi_company.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.hr_expense.tests.common import TestExpenseCommon +from odoo.tests import tagged +from odoo.exceptions import UserError + + +@tagged('post_install', '-at_install') +class TestExpenseMultiCompany(TestExpenseCommon): + + def test_expense_sheet_multi_company_approve(self): + self.expense_employee.company_id = self.company_data_2['company'] + + # The expense employee is able to a create an expense sheet for company_2. + + expense_sheet = self.env['hr.expense.sheet']\ + .with_user(self.expense_user_employee)\ + .with_context(allowed_company_ids=self.company_data_2['company'].ids)\ + .create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data_2['default_journal_purchase'].id, + 'accounting_date': '2017-01-01', + 'expense_line_ids': [ + (0, 0, { + # Expense without foreign currency but analytic account. + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_a.id, + 'unit_amount': 1000.0, + 'employee_id': self.expense_employee.id, + }), + ], + }) + self.assertRecordValues(expense_sheet, [{'company_id': self.company_data_2['company'].id}]) + + # The expense employee is able to submit the expense sheet. + + expense_sheet.with_user(self.expense_user_employee).action_submit_sheet() + + # An expense manager is not able to approve without access to company_2. + + with self.assertRaises(UserError): + expense_sheet\ + .with_user(self.expense_user_manager)\ + .with_context(allowed_company_ids=self.company_data['company'].ids)\ + .approve_expense_sheets() + + # An expense manager is able to approve with access to company_2. + + expense_sheet\ + .with_user(self.expense_user_manager)\ + .with_context(allowed_company_ids=self.company_data_2['company'].ids)\ + .approve_expense_sheets() + + # An expense manager having accounting access rights is not able to create the journal entry without access + # to company_2. + + with self.assertRaises(UserError): + expense_sheet\ + .with_user(self.env.user)\ + .with_context(allowed_company_ids=self.company_data['company'].ids)\ + .action_sheet_move_create() + + # An expense manager having accounting access rights is able to create the journal entry with access to + # company_2. + + expense_sheet\ + .with_user(self.env.user)\ + .with_context(allowed_company_ids=self.company_data_2['company'].ids)\ + .action_sheet_move_create() + + def test_expense_sheet_multi_company_refuse(self): + self.expense_employee.company_id = self.company_data_2['company'] + + # The expense employee is able to a create an expense sheet for company_2. + + expense_sheet = self.env['hr.expense.sheet']\ + .with_user(self.expense_user_employee)\ + .with_context(allowed_company_ids=self.company_data_2['company'].ids)\ + .create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data_2['default_journal_purchase'].id, + 'accounting_date': '2017-01-01', + 'expense_line_ids': [ + (0, 0, { + # Expense without foreign currency but analytic account. + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_a.id, + 'unit_amount': 1000.0, + 'employee_id': self.expense_employee.id, + }), + ], + }) + self.assertRecordValues(expense_sheet, [{'company_id': self.company_data_2['company'].id}]) + + # The expense employee is able to submit the expense sheet. + + expense_sheet.with_user(self.expense_user_employee).action_submit_sheet() + + # An expense manager is not able to approve without access to company_2. + + with self.assertRaises(UserError): + expense_sheet\ + .with_user(self.expense_user_manager)\ + .with_context(allowed_company_ids=self.company_data['company'].ids)\ + .refuse_sheet('') + + # An expense manager is able to approve with access to company_2. + + expense_sheet\ + .with_user(self.expense_user_manager)\ + .with_context(allowed_company_ids=self.company_data_2['company'].ids)\ + .refuse_sheet('') diff --git a/addons/hr_expense/tests/test_multicompany.py b/addons/hr_expense/tests/test_multicompany.py deleted file mode 100644 index d8aaf8193f3..00000000000 --- a/addons/hr_expense/tests/test_multicompany.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.hr_expense.tests.common import TestExpenseMultiCompanyCommon -from odoo.tests import tagged - -@tagged('post_install','-at_install') -class TestExpenseMultiCompany(TestExpenseMultiCompanyCommon): - - @classmethod - def setUpClass(cls): - super(TestExpenseMultiCompany, cls).setUpClass() - - cls.bank_journal = cls.env['account.journal'].create({ - 'name': 'Payment Journal', - 'code': 'PAY', - 'type': 'bank', - 'company_id': cls.env.company.id, - }) - - cls.outbound_pay_method = cls.env['account.payment.method'].create({ - 'name': 'outbound', - 'code': 'out', - 'payment_type': 'outbound', - }) - - def test_expense_multicompany_company_propagation(self): - # The company on the expense sheet should be the same as the one from the expense - expense = self.env['hr.expense.sheet'].create({ - 'name': 'Expense for employee a', - 'employee_id': self.employee.id, - 'journal_id': self.sale_journal0.id, - }) - - expense_line = self.env['hr.expense'].create({ - 'name': 'Sword Sharpening', - 'employee_id': self.employee.id, - 'product_id': self.product_1.id, - 'unit_amount': 1, - 'quantity': 1, - 'sheet_id': expense.id, - 'analytic_account_id': self.analytic_account.id, - }) - - self.assertEqual(expense_line.company_id.id, self.env.company.id) - self.assertEqual(expense.company_id.id, self.env.company.id) - - expense.with_context(allowed_company_ids=[self.company_B.id, self.env.company.id], company_id=self.company_B.id).action_submit_sheet() - self.assertEqual(expense.state, 'submit', 'Expense is not in Reported state') - self.assertEqual(expense.company_id.id, self.env.company.id) - - expense.with_context(allowed_company_ids=[self.company_B.id, self.env.company.id], company_id=self.company_B.id).approve_expense_sheets() - self.assertEqual(expense.state, 'approve', 'Expense is not in Approved state') - - expense.with_context(allowed_company_ids=[self.company_B.id, self.env.company.id], company_id=self.company_B.id).action_sheet_move_create() - self.assertEqual(expense.state, 'post', 'Expense is not in Waiting Payment state') - self.assertTrue(expense.account_move_id.id, 'Expense Journal Entry is not created') - - exp_move_lines = expense.account_move_id.line_ids - payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable') - self.assertEqual(len(payable_move_lines), 1) - self.assertEqual(payable_move_lines[0].company_id.id, expense.company_id.id, 'The company of the move line should be the same as the one from the expense.') - - #The company on the payment should be the same as the one on the expense, even if we are in a another company - WizardRegister = self.env["hr.expense.sheet.register.payment.wizard"].with_context( - active_model=expense._name, active_id=expense.id, active_ids=expense.ids, allowed_company_ids=[self.company_B.id, self.env.company.id], company_id=self.company_B.id - ) - - register_payement = WizardRegister.create({ - 'journal_id': self.bank_journal.id, - 'payment_method_id': self.outbound_pay_method.id, - 'amount': 300, - }) - self.assertEqual(register_payement.company_id.id, expense.company_id.id, 'The company of the payement should be the same as the one from the expense.') - register_payement.expense_post_payment() diff --git a/addons/sale_expense/tests/test_reinvoice.py b/addons/sale_expense/tests/test_reinvoice.py index 1a31d47c4a1..cb66751ec72 100644 --- a/addons/sale_expense/tests/test_reinvoice.py +++ b/addons/sale_expense/tests/test_reinvoice.py @@ -3,230 +3,131 @@ from odoo.addons.hr_expense.tests.common import TestExpenseCommon from odoo.addons.sale.tests.test_sale_common import TestCommonSaleNoChart +from odoo.tests import tagged +@tagged('-at_install', 'post_install') class TestReInvoice(TestExpenseCommon, TestCommonSaleNoChart): @classmethod - def setUpClass(cls): - super(TestReInvoice, cls).setUpClass() + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + super().setUpExpenseProducts() - cls.setUpExpenseProducts() - - cls.env.ref('product.list0').currency_id = cls.env.user.company_id.currency_id - - # partner and SO - cls.partner_customer = cls.env['res.partner'].create({ - 'name': 'Ze Client', - 'email': 'client@agrolait.com', - 'property_account_payable_id': cls.account_payable.id, + cls.price_list = cls.env['product.pricelist'].create({ + 'name': 'default_price_list_company_1', + 'sequence': 1, + 'company_id': cls.company_data['company'].id, + 'currency_id': cls.company_data['currency'].id, }) - cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=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, - }) - - def test_at_cost(self): - """ Test invoicing expenses at cost for product based on delivered and ordered quantities. """ + def test_expenses_reinvoice(self): # 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 = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({ + 'partner_id': self.partner_a.id, + 'partner_invoice_id': self.partner_a.id, + 'partner_shipping_id': self.partner_a.id, + 'order_line': [(0, 0, { + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'product_uom_qty': 2.0, + 'price_unit': 1000.0, + })], }) - sale_order_line.product_id_change() - self.sale_order.onchange_partner_id() - self.sale_order._compute_tax_id() - self.sale_order.action_confirm() + 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, - }) - 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, + expense_sheet = self.env['hr.expense.sheet'].create({ + 'name': 'First Expense for employee', + 'employee_id': self.expense_employee.id, + 'journal_id': self.company_data['default_journal_purchase'].id, + 'accounting_date': '2017-01-01', + 'expense_line_ids': [ + (0, 0, { + 'name': 'expense_1', + 'date': '2016-01-01', + 'product_id': self.product_ordered_cost.id, + 'unit_amount': self.product_ordered_cost.list_price, + 'analytic_account_id': self.analytic_account_1.id, + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id, + }), + (0, 0, { + 'name': 'expense_2', + 'date': '2016-01-01', + 'product_id': self.product_deliver_cost.id, + 'unit_amount': self.product_deliver_cost.list_price, + 'analytic_account_id': self.analytic_account_1.id, + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id, + }), + (0, 0, { + 'name': 'expense_3', + 'date': '2016-01-01', + 'product_id': self.product_order_sales_price.id, + 'unit_amount': self.product_order_sales_price.list_price, + 'analytic_account_id': self.analytic_account_1.id, + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id, + }), + (0, 0, { + 'name': 'expense_4', + 'date': '2016-01-01', + 'product_id': self.product_deliver_sales_price.id, + 'unit_amount': self.product_deliver_sales_price.list_price, + 'analytic_account_id': self.analytic_account_1.id, + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id, + }), + (0, 0, { + 'name': 'expense_5', + 'date': '2016-01-01', + 'product_id': self.product_no_expense.id, + 'unit_amount': self.product_no_expense.list_price, + 'analytic_account_id': self.analytic_account_1.id, + 'employee_id': self.expense_employee.id, + 'sale_order_id': sale_order.id, + }), + ], }) - # approve and generate entries - self.expense_sheet.approve_expense_sheets() - self.expense_sheet.action_sheet_move_create() + expense_sheet.approve_expense_sheets() + expense_sheet.action_sheet_move_create() - self.assertEqual(len(self.sale_order.order_line), 3, "There should be 3 lines on the SO") - self.assertEqual(sale_order_line.qty_delivered, 0, "Exising SO line should not be impacted by reinvoicing product at cost") + self.assertRecordValues(sale_order.order_line, [ + # Original SO line: + { + 'qty_delivered': 0.0, + 'product_uom_qty': 2.0, + 'is_expense': False, + }, + # Expense lines: + { + 'qty_delivered': 1.0, + 'product_uom_qty': 0.0, + 'is_expense': True, + }, + { + 'qty_delivered': 1.0, + 'product_uom_qty': 0.0, + 'is_expense': True, + }, + { + 'qty_delivered': 1.0, + 'product_uom_qty': 0.0, + 'is_expense': True, + }, + { + 'qty_delivered': 1.0, + 'product_uom_qty': 0.0, + 'is_expense': True, + }, + ]) - 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.is_expense, "The new SOL should be flag as coming from an expense") - self.assertEqual(sol_ordered.qty_delivered_method, 'analytic', 'Delivered quantity of expense SO line should be computed by analytic amount') - self.assertTrue(sol_ordered, "A new line with ordered expense should have been created on expense report posting") - self.assertEqual(sol_ordered.price_unit, expense1.unit_amount, "The unit price of new SO line should be the one from the expense (at cost)") - self.assertEqual(sol_ordered.product_uom_qty, 0, "The ordered quantity of new SO line should be zero") - self.assertEqual(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.is_expense, "The new SOL should be flag as coming from an expense") - self.assertEqual(sol_deliver.qty_delivered_method, 'analytic', 'Delivered quantity of expense SO line should be computed by analytic amount') - self.assertTrue(sol_deliver, "A new line with delivered expense should have been created on expense report posting") - self.assertEqual(sol_deliver.price_unit, expense2.unit_amount, "The unit price of new SO line should be the one from the expense (at cost)") - self.assertEqual(sol_deliver.product_uom_qty, 0, "The ordered quantity of new SO line should be zero") - self.assertEqual(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, - }) - - # 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.assertEqual(self.sale_order.analytic_account_id, expense1.analytic_account_id, "SO analytic account should be the same for the expense") - self.assertEqual(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.is_expense, "The new SOL should be flag as coming from an expense") - self.assertTrue(sol_ordered, "A new line with ordered expense should have been created on expense report posting") - self.assertEqual(sol_ordered.price_unit, 10, "The unit price of new SO line should be the one from the expense (at sales price)") - self.assertEqual(sol_ordered.product_uom_qty, 0, "The ordered quantity of new SO line should be zero") - self.assertEqual(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 not incremented, but a new - expense SOL will be created and incremented. - """ - # create SO line and confirm SO (with only one line) - sale_order_line1 = 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_line1.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, - }) - - # approve and generate entries - self.expense_sheet.approve_expense_sheets() - self.expense_sheet.action_sheet_move_create() - - self.assertEqual(len(self.sale_order.order_line), 2, "A new expense SOL should have been created.") - - sale_order_line2 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1) - self.assertTrue(sale_order_line2.is_expense, "The new SOL should be flag as coming from an expense") - self.assertEqual(sale_order_line2.price_unit, 10, "The unit price of SO line should be the same") - self.assertEqual(sale_order_line2.product_uom_qty, 0, "The ordered quantity of new SO line should be zero") - self.assertEqual(sale_order_line2.qty_delivered, expense1.quantity, "The delivered quantity of SO line should have be the same of the expense") - - # create 2nd expense lines - expense2 = 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_sheet2.id, - 'sale_order_id': self.sale_order.id, - 'analytic_account_id': self.sale_order.analytic_account_id.id, - }) - - # approve and generate entries - self.expense_sheet2.approve_expense_sheets() - self.expense_sheet2.action_sheet_move_create() - - self.assertEqual(len(self.sale_order.order_line), 2, "No new expense SOL should have been created.") - - self.assertEqual(sale_order_line2.price_unit, 10, "The unit price of SO line should be the same") - self.assertEqual(sale_order_line2.product_uom_qty, 0, "The ordered quantity of new SO line should remains zero") - self.assertEqual(sale_order_line2.qty_delivered, expense1.quantity + expense2.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. """ - analytic_account = self.env['account.analytic.account'].create({ - 'name': "AA to track expense", - }) - # 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, - 'unit_amount': 15, - 'quantity': 3, - 'sheet_id': self.expense_sheet.id, - 'sale_order_id': self.sale_order.id, - 'analytic_account_id': analytic_account.id, - }) - expense1.write({'product_id': self.product_no_expense.id}) - # approve and generate entries - self.expense_sheet.approve_expense_sheets() - self.expense_sheet.action_sheet_move_create() - self.assertFalse(expense1.sale_order_id, "None reinvoicable expense can not be linked to SO") - self.assertFalse(self.sale_order.analytic_account_id, "Posting expense with an non expense product should not trigger analytic Account creation from SO, since non reinvoicable expense can not be linked to SO") - - self.assertEqual(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) on expense report posting") - self.assertEqual(sale_order_line.price_unit, self.product_no_expense.list_price, "The unit price of SO line should be the same") - self.assertEqual(sale_order_line.product_uom_qty, 2, "The ordered quantity of SO line should be two") - self.assertEqual(sale_order_line.qty_delivered, 0, "The delivered quantity of SO line should have been incremented") + # 'qty_delivered_method' is not checked on the first line because it could be multiple things depending if + # 'sale_stock' is installed or not. + self.assertRecordValues(sale_order.order_line[1:], [ + {'qty_delivered_method': 'analytic'}, + {'qty_delivered_method': 'analytic'}, + {'qty_delivered_method': 'analytic'}, + {'qty_delivered_method': 'analytic'}, + ])