[MERGE] project, hr_expense: Fix several multi-company issues + tests

Purpose
Fix multi-company access issues following the big changes that happened
(the 13.0). Make the usability better in order to avoid potential
multi-company issues for the user.

Specification
Check the related commits to see the different fixes that have been
made. For more information about the technical/functional considerations,
check the related task.

Taskid: 2088891
X-original-commit: 5c8df3a5e5f7960e5eab6439e0a32a06224ba826
This commit is contained in:
fw-bot
2020-01-15 15:11:50 +01:00
committed by jdoutreloux
parent 1dc3301332
commit fccaa703c4
18 changed files with 607 additions and 29 deletions
+137
View File
@@ -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):
+4
View File
@@ -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:
+1
View File
@@ -4,3 +4,4 @@
from . import common
from . import test_expenses
from . import test_expenses_mail_subjects
from . import test_multicompany
+104 -1
View File
@@ -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,
})
@@ -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()
+13 -12
View File
@@ -7,7 +7,7 @@
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='managers']" position="inside">
<field name="expense_manager_id"/>
<field name="expense_manager_id" context="{'default_company_id': company_id}"/>
</xpath>
</field>
</record>
@@ -29,7 +29,7 @@
<field name="inherit_id" ref="hr.res_users_view_form_profile" />
<field name="arch" type="xml">
<xpath expr="//group[@name='managers']" position="inside">
<field name="expense_manager_id" attrs="{'readonly': [('can_edit', '=', False)]}"/>
<field name="expense_manager_id" attrs="{'readonly': [('can_edit', '=', False)]}" context="{'default_company_id': company_id}"/>
</xpath>
</field>
</record>
@@ -105,13 +105,13 @@
<field name="quantity" class="oe_inline"/>
<field name="product_uom_id" required="1" widget="selection" class="oe_inline" groups="uom.group_uom"/>
</div>
<field name="tax_ids" widget="many2many_tags" groups="account.group_account_user" attrs="{'readonly': [('is_editable', '=', False)]}"/>
<field name="tax_ids" widget="many2many_tags" groups="account.group_account_user" attrs="{'readonly': [('is_editable', '=', False)]}" context="{'default_company_id': company_id}"/>
<field name="total_amount" widget='monetary' options="{'currency_field': 'currency_id'}"/>
</group><group>
<field name="reference" attrs="{'readonly': [('is_ref_editable', '=', False)]}"/>
<field name="date"/>
<field name="account_id" options="{'no_create': True}" domain="[('internal_type', '=', 'other'), ('company_id', '=', company_id)]" groups="account.group_account_user" attrs="{'readonly': [('is_editable', '=', False)]}"/>
<field name="employee_id" groups="hr_expense.group_hr_expense_team_approver"/>
<field name="account_id" options="{'no_create': True}" domain="[('internal_type', '=', 'other'), ('company_id', '=', company_id)]" groups="account.group_account_user" attrs="{'readonly': [('is_editable', '=', False)]}" context="{'default_company_id': company_id}"/>
<field name="employee_id" groups="hr_expense.group_hr_expense_team_approver" context="{'default_company_id': company_id}"/>
<field name="sheet_id" invisible="1"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="analytic_account_id" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]" groups="analytic.group_analytic_accounting" attrs="{'readonly': [('is_editable', '=', False)]}"/>
@@ -508,17 +508,17 @@
</div>
<group>
<group name="employee_details">
<field name="employee_id"/>
<field name="employee_id" context="{'default_company_id': company_id}"/>
<field name="payment_mode"/>
<field name="user_id"/>
<field name="address_id" invisible="1"/>
<field name="department_id" invisible="1"/>
<field name="address_id" invisible="1" context="{'default_company_id': company_id}"/>
<field name="department_id" invisible="1" context="{'default_company_id': company_id}"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page name="expenses" string="Expense">
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft'), ('employee_id', '=', employee_id), ('company_id', '=', company_id)]" options="{'reload_on_button': True}" context="{'form_view_ref' : 'hr_expense.hr_expense_view_form_without_header'}">
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft'), ('employee_id', '=', employee_id), ('company_id', '=', company_id)]" options="{'reload_on_button': True}" context="{'form_view_ref' : 'hr_expense.hr_expense_view_form_without_header', 'default_company_id': company_id}">
<tree decoration-danger="is_refused">
<field name="date" optional="show"/>
<field name="name"/>
@@ -533,7 +533,8 @@
<field name="unit_amount" optional="hide"/>
<field name="currency_id" optional="hide"/>
<field name="quantity" optional="hide"/>
<field name="tax_ids" optional="hide" widget="many2many_tags" groups="account.group_account_user"/>
<field name="company_id"/>
<field name="tax_ids" optional="hide" widget="many2many_tags" groups="account.group_account_user" context="{'default_company_id': company_id}"/>
<field name="total_amount" optional="show"/>
<field name="company_currency_id" invisible="1"/>
<field name="total_amount_company" optional="show" groups="base.group_multi_currency"/>
@@ -551,8 +552,8 @@
<page name="other_info" string="Other Info">
<group>
<group>
<field name="journal_id" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}"/>
<field name="bank_journal_id" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'company_account')]}"/>
<field name="journal_id" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'own_account')]}" context="{'default_company_id': company_id}"/>
<field name="bank_journal_id" groups="account.group_account_user" options="{'no_open': True, 'no_create': True}" attrs="{'invisible': [('payment_mode', '!=', 'company_account')]}" context="{'default_company_id': company_id}"/>
<field name="accounting_date" groups="account.group_account_user" attrs="{'invisible': [('state', 'not in', ['approve', 'post', 'done'])]}"/>
</group>
<group>
@@ -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)
+12
View File
@@ -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
+18
View File
@@ -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
+9 -10
View File
@@ -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):
+153 -2
View File
@@ -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,
})
+6
View File
@@ -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:
+3 -2
View File
@@ -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
+33 -1
View File
@@ -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,
})
@@ -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")
@@ -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.")
@@ -9,7 +9,8 @@
<group>
<group>
<field name="project_id" readonly="1"/>
<field name="partner_id"/>
<field name="company_id" invisible="1"/>
<field name="partner_id" domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"/>
</group>
<group>
<field name="billable_type" widget="radio"/>
@@ -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()