diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py index c9e25894013..108736d492f 100644 --- a/addons/account/tests/common.py +++ b/addons/account/tests/common.py @@ -444,6 +444,143 @@ class AccountTestNoChartCommon(SavepointCaseWithUserDemo): cls.user_manager.partner_id.write(account_values) cls.user_employee.partner_id.write(account_values) +class AccountTestNoChartCommonMultiCompany(AccountTestNoChartCommon): + """ Some tests required to be executed at module installation, and not 'post install', like moslty + of accounting tests, since a chart of account is required + This test setup class provides data for test suite to make business flow working without a chart + of account installed in a multi-company environment. + The class provide some helpers methods to create particular document types. Each test suite extending + this method can call thoses method to set up their testing environment in their own `setUpClass` method. + """ + + @classmethod + def setUpClass(cls): + """ This method set up the minimal requried part of chart of account """ + super(AccountTestNoChartCommonMultiCompany, cls).setUpClass() + cls.company_B = cls.env['res.company'].create({'name': 'Company B'}) + + # To speed up test, create object without mail tracking + context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True} + + # Create base account to simulate a chart of account + user_type_payable = cls.env.ref('account.data_account_type_payable') + cls.account_payable_company_B = cls.env['account.account'].create({ + 'code': 'NC1110', + 'name': 'Test Payable Account Company B', + 'user_type_id': user_type_payable.id, + 'reconcile': True, + 'company_id': cls.company_B.id + }) + + user_type_receivable = cls.env.ref('account.data_account_type_receivable') + cls.account_receivable_company_B = cls.env['account.account'].create({ + 'code': 'NC1111', + 'name': 'Test Receivable Account Company B', + 'user_type_id': user_type_receivable.id, + 'reconcile': True, + 'company_id': cls.company_B.id + }) + + # Create a customer for each company + Partner = cls.env['res.partner'].with_context(context_no_mail) + cls.partner_customer_company_B = Partner.create({ + 'name': 'Customer from the South', + 'email': 'customer@south.com', + 'property_account_payable_id': cls.account_payable_company_B.id, + 'property_account_receivable_id': cls.account_receivable_company_B.id, + 'company_id': cls.company_B.id + }) + + @classmethod + def setUpAdditionalAccounts(cls): + """ Set up some addionnal accounts: expenses, revenue, ... """ + super(AccountTestNoChartCommonMultiCompany, cls).setUpAdditionalAccounts() + user_type_income = cls.env.ref('account.data_account_type_direct_costs') + cls.account_income_company_B = cls.env['account.account'].create({ + 'code': 'NC1112', + 'name': 'Sale - Test Account Company B', + 'user_type_id': user_type_income.id, + 'company_id': cls.company_B.id, + }) + user_type_expense = cls.env.ref('account.data_account_type_expenses') + cls.account_expense_company_B = cls.env['account.account'].create({ + 'code': 'NC1113', + 'name': 'HR Expense - Test Purchase Account Company B', + 'user_type_id': user_type_expense.id, + 'company_id': cls.company_B.id, + }) + user_type_revenue = cls.env.ref('account.data_account_type_revenue') + cls.account_revenue_company_B = cls.env['account.account'].create({ + 'code': 'NC1114', + 'name': 'Sales - Test Sales Account Company B', + 'user_type_id': user_type_revenue.id, + 'reconcile': True, + 'company_id': cls.company_B.id, + }) + + @classmethod + def setUpAccountJournal(cls): + """ Set up some journals: sale, purchase, ... """ + super(AccountTestNoChartCommonMultiCompany, cls).setUpAccountJournal() + cls.journal_purchase_company_B = cls.env['account.journal'].create({ + 'name': 'Purchase Journal Company B - Test', + 'code': 'AJ-PURC', + 'type': 'purchase', + 'company_id': cls.company_B.id, + 'default_debit_account_id': cls.account_expense_company_B.id, + 'default_credit_account_id': cls.account_expense_company_B.id, + }) + cls.journal_sale_company_B = cls.env['account.journal'].create({ + 'name': 'Sale Journal Company B - Test', + 'code': 'AJ-SALE', + 'type': 'sale', + 'company_id': cls.company_B.id, + 'default_debit_account_id': cls.account_income_company_B.id, + 'default_credit_account_id': cls.account_income_company_B.id, + }) + cls.journal_general_company_B = cls.env['account.journal'].create({ + 'name': 'General Journal Company B - Test', + 'code': 'AJ-GENERAL', + 'type': 'general', + 'company_id': cls.company_B.id, + }) + + @classmethod + def setUpUsers(cls): + """ Create 2 users for each company: an employee and a manager. Both will have correct account configured + on their partner. Others access rigths should be given in extending test suites set up. + """ + super(AccountTestNoChartCommonMultiCompany, cls).setUpUsers() + group_employee = cls.env.ref('base.group_user') + Users = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True}) + cls.user_employee_company_B = Users.create({ + 'name': 'Gregor Clegane Employee', + 'login': 'gregor', + 'email': 'gregor@example.com', + 'notification_type': 'email', + 'groups_id': [(6, 0, [group_employee.id])], + 'company_id': cls.company_B.id, + 'company_ids': [cls.company_B.id], + }) + cls.user_manager_company_B = Users.create({ + 'name': 'Cersei Lannister Manager', + 'login': 'cersei', + 'email': 'cersei@example.com', + 'notification_type': 'email', + 'groups_id': [(6, 0, [group_employee.id])], + 'company_id': cls.company_B.id, + 'company_ids': [cls.company_B.id, cls.env.company.id], + }) + cls.user_manager.write({ + 'company_ids': [(6, 0, [cls.company_B.id, cls.env.company.id])], + }) + account_values_company_B = { + 'property_account_payable_id': cls.account_payable_company_B.id, + 'property_account_receivable_id': cls.account_receivable_company_B.id, + } + cls.user_manager_company_B.partner_id.write(account_values_company_B) + cls.user_employee_company_B.partner_id.write(account_values_company_B) + @tagged('post_install', '-at_install') class AccountTestInvoicingCommon(SavepointCase): diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index fd62bc344a1..f2037823216 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -158,6 +158,10 @@ class HrExpense(models.Model): if account: self.account_id = account + @api.onchange('company_id') + def _onchange_expense_company_id(self): + self.employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid), ('company_id', '=', self.company_id.id)]) + @api.onchange('product_uom_id') def _onchange_product_uom_id(self): if self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id: diff --git a/addons/hr_expense/tests/__init__.py b/addons/hr_expense/tests/__init__.py index 558af4cd575..cb3dbad9d88 100644 --- a/addons/hr_expense/tests/__init__.py +++ b/addons/hr_expense/tests/__init__.py @@ -4,3 +4,4 @@ from . import common from . import test_expenses from . import test_expenses_mail_subjects +from . import test_multicompany diff --git a/addons/hr_expense/tests/common.py b/addons/hr_expense/tests/common.py index df35c8d3e75..b5d0c0d8802 100644 --- a/addons/hr_expense/tests/common.py +++ b/addons/hr_expense/tests/common.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.account.tests.common import AccountTestNoChartCommon +from odoo.addons.account.tests.common import AccountTestNoChartCommon, AccountTestNoChartCommonMultiCompany class TestExpenseCommon(AccountTestNoChartCommon): @@ -120,3 +120,106 @@ class TestExpenseCommon(AccountTestNoChartCommon): '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, + }) diff --git a/addons/hr_expense/tests/test_multicompany.py b/addons/hr_expense/tests/test_multicompany.py new file mode 100644 index 00000000000..d8aaf8193f3 --- /dev/null +++ b/addons/hr_expense/tests/test_multicompany.py @@ -0,0 +1,75 @@ +# -*- 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/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 165f84ec53d..15db591d01e 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -7,7 +7,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -105,13 +105,13 @@ - + - - + + @@ -508,17 +508,17 @@ - + - - + + - + @@ -533,7 +533,8 @@ - + + @@ -551,8 +552,8 @@ - - + + diff --git a/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py b/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py index 15e6fb4b5f2..c13fec1939f 100644 --- a/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py +++ b/addons/hr_expense/wizard/hr_expense_sheet_register_payment.py @@ -104,6 +104,8 @@ class HrExpenseSheetRegisterPaymentWizard(models.TransientModel): def expense_post_payment(self): self.ensure_one() + company = self.company_id + self = self.with_company(company.id) context = dict(self._context or {}) active_ids = context.get('active_ids', []) expense_sheet = self.env['hr.expense.sheet'].browse(active_ids) diff --git a/addons/project/i18n/project.pot b/addons/project/i18n/project.pot index a18a0637528..5bc50f01d4e 100644 --- a/addons/project/i18n/project.pot +++ b/addons/project/i18n/project.pot @@ -2664,6 +2664,12 @@ msgid "" "system user is found for that address." msgstr "" +#. module: project +#: code:addons/project/models/project.py:0 +#, python-format +msgid "The parent task belongs to a company you do not have access to." +msgstr "" + #. module: project #: code:addons/project/models/project.py:0 #, python-format @@ -2673,6 +2679,12 @@ msgid "" "customers' in order to make it accessible by the recipient(s)." msgstr "" +#. module: project +#: code:addons/project/models/project.py:0 +#, python-format +msgid "The subtasks belong to a company you do not have access to." +msgstr "" + #. module: project #: code:addons/project/models/project.py:0 #, python-format diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 1a981c5a759..2c5c6a5c8b2 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -665,6 +665,11 @@ class Task(models.Model): self.company_id = self.project_id.company_id else: self.stage_id = False + + @api.onchange('company_id') + def _onchange_task_company(self): + if self.project_id.company_id != self.company_id: + self.project_id = False @api.constrains('parent_id', 'child_ids') def _check_subtask_level(self): @@ -943,8 +948,21 @@ class Task(models.Model): if not children: return self.env['project.task'] return children + children._get_all_subtasks() + def action_open_parent_task(self): + if self.sudo().parent_id and self.sudo().parent_id.company_id.id not in self.env.companies.ids: + raise UserError(_('The parent task belongs to a company you do not have access to.')) + return { + 'name': _('Parent Task'), + 'view_mode': 'form', + 'res_model': 'project.task', + 'res_id': self.parent_id.id, + 'type': 'ir.actions.act_window', + 'context': dict(self._context, create=False) + } def action_subtask(self): + if self.sudo().subtask_project_id and self.sudo().subtask_project_id.company_id.id not in self.env.companies.ids: + raise UserError(_('The subtasks belong to a company you do not have access to.')) action = self.env.ref('project.project_task_action_sub_task').read()[0] # display all subtasks of current task diff --git a/addons/project/tests/test_multicompany.py b/addons/project/tests/test_multicompany.py index a2c3144aca8..414738802e2 100644 --- a/addons/project/tests/test_multicompany.py +++ b/addons/project/tests/test_multicompany.py @@ -233,15 +233,6 @@ class TestMultiCompanyProject(TestMultiCompanyCommon): self.assertEqual(task.company_id, self.project_company_a.company_id, "The company of the task should be the one from its project.") - # create task in a different company than the project should raise - with self.allow_companies([self.company_a.id, self.company_b.id]): - with self.assertRaises(UserError): - with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form: - task_form.name = 'Test Task with company inconsistency' - task_form.project_id = self.project_company_a - task_form.company_id = self.company_b - task = task_form.save() - def test_move_task(self): with self.sudo('employee-a'): with self.allow_companies([self.company_a.id, self.company_b.id]): @@ -285,7 +276,6 @@ class TestMultiCompanyProject(TestMultiCompanyCommon): with self.sudo('employee-a'): with self.allow_companies([self.company_a.id, self.company_b.id]): - with Form(self.env['project.task'].with_context({'tracking_disable': True})) as task_form: task_form.name = 'Test Subtask in company B' task_form.parent_id = self.task_1 @@ -294,6 +284,15 @@ class TestMultiCompanyProject(TestMultiCompanyCommon): self.assertEqual(task.project_id, self.task_1.project_id.subtask_project_id, "The default project of a subtask should be the default subtask project of the project from the mother task") self.assertEqual(task.company_id, task.project_id.subtask_project_id.company_id, "The company of the orphan subtask should be the one from its project.") + self.assertEqual(self.task_1.child_ids.ids, [task.id]) + + # trying to access the subtask in a company not allowed should raise an error + with self.assertRaises(UserError): + self.task_1.with_context(allowed_company_ids=[self.company_a.id]).action_subtask() + + # trying to access the parent task in a company not allowed should raise an error + with self.assertRaises(UserError): + task.with_context(allowed_company_ids=[self.company_b.id]).action_open_parent_task() with self.sudo('employee-a'): with self.assertRaises(AccessError): diff --git a/addons/sale/tests/test_sale_common.py b/addons/sale/tests/test_sale_common.py index d609f952669..ef5199a5c95 100644 --- a/addons/sale/tests/test_sale_common.py +++ b/addons/sale/tests/test_sale_common.py @@ -3,8 +3,7 @@ from collections import OrderedDict from odoo.addons.account.tests.common import AccountTestCommon -from odoo.addons.account.tests.common import AccountTestNoChartCommon - +from odoo.addons.account.tests.common import AccountTestNoChartCommon, AccountTestNoChartCommonMultiCompany class TestSaleCommon(AccountTestCommon): @@ -263,3 +262,155 @@ class TestCommonSaleNoChart(AccountTestNoChartCommon): 'taxes_id': False, 'property_account_expense_id': cls.account_expense_for_products.id, }) + + +class TestCommonSaleMultiCompanyNoChart(AccountTestNoChartCommonMultiCompany, TestCommonSaleNoChart): + """ This class should be extended for test suite of sale flows with a minimal chart of accounting + installed. This test suite should be executed at module installation. + This class provides some method to generate testing data well configured, according to the minimal + chart of account, defined in `TestAccountNoChartCommon` class. + """ + + @classmethod + def setUpClass(cls): + super(TestCommonSaleMultiCompanyNoChart, cls).setUpClass() + cls.setUpAdditionalAccounts() + + @classmethod + def setUpClassicProducts(cls): + super(TestCommonSaleMultiCompanyNoChart, cls).setUpClassicProducts() + # Create an expense journal + user_type_income = cls.env.ref('account.data_account_type_direct_costs') + cls.account_income_product_company_B = cls.env['account.account'].create({ + 'code': 'INCOME_PROD111', + 'name': 'Income - Test Account Company B', + 'user_type_id': user_type_income.id, + 'company_id': cls.company_B.id, + }) + # Create category + cls.product_category_company_B = cls.env['product.category'].create({ + 'name': 'Product Category with Income account Company B', + 'property_account_income_categ_id': cls.account_income_product_company_B.id + }) + # Products + uom_unit = cls.env.ref('uom.product_uom_unit') + uom_hour = cls.env.ref('uom.product_uom_hour') + + cls.product_order_company_B = cls.env['product.product'].create({ + 'name': "Pigeon pie", + 'standard_price': 235.0, + 'list_price': 280.0, + 'type': 'consu', + 'uom_id': uom_unit.id, + 'uom_po_id': uom_unit.id, + 'invoice_policy': 'order', + 'expense_policy': 'no', + 'default_code': 'PROD_ORDER', + 'service_type': 'manual', + 'taxes_id': False, + 'categ_id': cls.product_category_company_B.id, + }) + + cls.service_deliver_company_B = cls.env['product.product'].create({ + 'name': "Golden Company Contract", + 'standard_price': 200.0, + 'list_price': 180.0, + 'type': 'service', + 'uom_id': uom_unit.id, + 'uom_po_id': uom_unit.id, + 'invoice_policy': 'delivery', + 'expense_policy': 'no', + 'default_code': 'SERV_DEL', + 'service_type': 'manual', + 'taxes_id': False, + 'categ_id': cls.product_category_company_B.id, + }) + + cls.service_order_company_B = cls.env['product.product'].create({ + 'name': "Maester Consulting", + 'standard_price': 40.0, + 'list_price': 90.0, + 'type': 'service', + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, + 'invoice_policy': 'order', + 'expense_policy': 'no', + 'default_code': 'PRE-PAID', + 'service_type': 'manual', + 'taxes_id': False, + 'categ_id': cls.product_category_company_B.id, + }) + + cls.product_deliver_company_B = cls.env['product.product'].create({ + 'name': "Swords", + 'standard_price': 55.0, + 'list_price': 70.0, + 'type': 'consu', + 'uom_id': uom_unit.id, + 'uom_po_id': uom_unit.id, + 'invoice_policy': 'delivery', + 'expense_policy': 'no', + 'default_code': 'PROD_DEL', + 'service_type': 'manual', + 'taxes_id': False, + 'categ_id': cls.product_category_company_B.id, + }) + + @classmethod + def setUpExpenseProducts(cls): + super(TestCommonSaleMultiCompanyNoChart, cls).setUpExpenseProducts() + cls.product_ordered_cost_company_B = 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_company_B.id, + 'company_id': cls.company_B.id, + }) + + cls.product_deliver_cost_company_B = 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_company_B.id, + 'company_id': cls.company_B.id, + }) + + cls.product_order_sales_price_company_B = 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_company_B.id, + 'company_id': cls.company_B.id, + }) + + cls.product_deliver_sales_price_company_B = 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_company_B.id, + 'company_id': cls.company_B.id, + }) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 9ee6d91fc52..59234024ceb 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -64,6 +64,12 @@ class SaleOrder(models.Model): expected_date = min(dates_list) if order.picking_policy == 'direct' else max(dates_list) order.expected_date = fields.Datetime.to_string(expected_date) + @api.model + def create(self, vals): + if 'warehouse_id' not in vals and 'company_id' in vals and vals.get('company_id') != self.env.company.id: + vals['warehouse_id'] = self.env['stock.warehouse'].search([('company_id', '=', vals.get('company_id'))], limit=1).id + return super(SaleOrder, self).create(vals) + def write(self, values): if values.get('order_line') and self.state == 'sale': for order in self: diff --git a/addons/sale_timesheet/tests/__init__.py b/addons/sale_timesheet/tests/__init__.py index 8c53a0cbb4d..4f78edeef69 100644 --- a/addons/sale_timesheet/tests/__init__.py +++ b/addons/sale_timesheet/tests/__init__.py @@ -1,5 +1,5 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. +# # -*- coding: utf-8 -*- +# # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import common from . import test_sale_timesheet @@ -8,3 +8,4 @@ from . import test_project_billing from . import test_reinvoice from . import test_reporting from . import test_project_overview +from . import test_project_billing_multicompany diff --git a/addons/sale_timesheet/tests/common.py b/addons/sale_timesheet/tests/common.py index 15584e9cfaa..f957d59165c 100644 --- a/addons/sale_timesheet/tests/common.py +++ b/addons/sale_timesheet/tests/common.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.sale.tests.test_sale_common import TestCommonSaleNoChart +from odoo.addons.sale.tests.test_sale_common import TestCommonSaleNoChart, TestCommonSaleMultiCompanyNoChart class TestCommonSaleTimesheetNoChart(TestCommonSaleNoChart): @@ -285,3 +285,35 @@ class TestCommonSaleTimesheetNoChart(TestCommonSaleNoChart): 'taxes_id': False, 'property_account_income_id': cls.account_sale.id, }) + + +class TestCommonSaleTimesheetMultiCompanyNoChart(TestCommonSaleMultiCompanyNoChart, TestCommonSaleTimesheetNoChart): + + @classmethod + def setUpEmployees(cls): + # Create employees + cls.setUpUsers() + super(TestCommonSaleTimesheetMultiCompanyNoChart, cls).setUpEmployees() + + cls.employee_company_B = cls.env['hr.employee'].create({ + 'name': 'Gregor Clegane', + 'user_id': cls.user_employee_company_B.id, + 'timesheet_cost': 15, + }) + + cls.manager_company_B = cls.env['hr.employee'].create({ + 'name': 'Cersei Lannister', + 'user_id': cls.user_manager_company_B.id, + 'timesheet_cost': 45, + }) + + @classmethod + def setUpServiceProducts(cls): + """ Create Service product for all kind, with each tracking policy. """ + super(TestCommonSaleTimesheetMultiCompanyNoChart, cls).setUpServiceProducts() + # Account and project + cls.analytic_account_sale_company_B = cls.env['account.analytic.account'].create({ + 'name': 'Project for selling timesheet Company B - AA', + 'code': 'AA-2030', + 'company_id': cls.company_B.id, + }) diff --git a/addons/sale_timesheet/tests/test_project_billing_multicompany.py b/addons/sale_timesheet/tests/test_project_billing_multicompany.py new file mode 100644 index 00000000000..65c08c9a0d8 --- /dev/null +++ b/addons/sale_timesheet/tests/test_project_billing_multicompany.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheetMultiCompanyNoChart + +class TestProjectBillingMulticompany(TestCommonSaleTimesheetMultiCompanyNoChart): + + @classmethod + def setUpClass(cls): + super(TestProjectBillingMulticompany, cls).setUpClass() + + cls.setUpServiceProducts() + + Project = cls.env['project.project'].with_context(tracking_disable=True) + cls.project_non_billable = Project.create({ + 'name': "Non Billable Project", + 'allow_timesheets': True, + 'billable_type': 'no', + 'company_id': cls.env.company.id, + }) + + def test_makeBillable_multiCompany(self): + wizard = self.env['project.create.sale.order'].with_context(allowed_company_ids=[self.company_B.id, self.env.company.id], company_id=self.company_B.id, active_id=self.project_non_billable.id, active_model='project.project').create({ + 'product_id': self.product_delivery_timesheet3.id, # product creates new Timesheet in new Project + 'price_unit': self.product_delivery_timesheet3.list_price, + 'billable_type': 'project_rate', + 'partner_id': self.partner_customer_usd.id, + }) + + action = wizard.action_create_sale_order() + sale_order = self.env['sale.order'].browse(action['res_id']) + + self.assertEqual(sale_order.company_id.id, self.project_non_billable.company_id.id, "The company on the sale order should be the same as the one on the project") diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index f18f3535d04..9a91e329fc7 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -27,6 +27,7 @@ class ProjectCreateSalesOrder(models.TransientModel): return result project_id = fields.Many2one('project.project', "Project", domain=[('sale_line_id', '=', False)], help="Project for which we are creating a sales order", required=True) + company_id = fields.Many2one(related='project_id.company_id') partner_id = fields.Many2one('res.partner', string="Customer", required=True, help="Customer of the sales order") product_id = fields.Many2one('product.product', domain=[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')], string="Service", help="Product of the sales order item. Must be a service invoiced based on timesheets on tasks.") price_unit = fields.Float("Unit Price", help="Unit price of the sales order item.") diff --git a/addons/sale_timesheet/wizard/project_create_sale_order_views.xml b/addons/sale_timesheet/wizard/project_create_sale_order_views.xml index 6ba91018e66..db90b4b5402 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order_views.xml +++ b/addons/sale_timesheet/wizard/project_create_sale_order_views.xml @@ -9,7 +9,8 @@ - + + diff --git a/addons/sale_timesheet/wizard/project_task_create_sale_order.py b/addons/sale_timesheet/wizard/project_task_create_sale_order.py index 3d7306a902f..9b5e6c38f0a 100644 --- a/addons/sale_timesheet/wizard/project_task_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_task_create_sale_order.py @@ -64,6 +64,7 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): # create SO sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_id.id, + 'company_id': self.task_id.company_id.id, 'analytic_account_id': self.task_id.project_id.analytic_account_id.id, }) sale_order.onchange_partner_id()