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()