[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:
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import common
|
||||
from . import test_expenses
|
||||
from . import test_expenses_mail_subjects
|
||||
from . import test_multicompany
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user