19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

View file

@ -4,7 +4,6 @@ from . import common
from . import test_expenses
from . import test_expenses_access_rights
from . import test_expenses_mail_import
from . import test_expenses_multi_company
from . import test_expenses_standard_price_update_warning
from . import test_expenses_states
from . import test_ui
from . import test_expenses_tour

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo import Command
from freezegun import freeze_time
from odoo import Command, fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.mail.tests.common import mail_new_test_user
@ -10,8 +12,11 @@ from odoo.addons.mail.tests.common import mail_new_test_user
class TestExpenseCommon(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.company_data_2 = cls.setup_other_company()
cls.other_currency = cls.setup_other_currency('HRK')
group_expense_manager = cls.env.ref('hr_expense.group_hr_expense_manager')
@ -22,7 +27,7 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
email='expense_user_employee@example.com',
notification_type='email',
groups='base.group_user',
company_ids=[(6, 0, cls.env.companies.ids)],
company_ids=[Command.set(cls.env.companies.ids)],
)
cls.expense_user_manager = mail_new_test_user(
cls.env,
@ -31,38 +36,39 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
email='expense_manager_1@example.com',
notification_type='email',
groups='base.group_user,hr_expense.group_hr_expense_manager',
company_ids=[(6, 0, cls.env.companies.ids)],
)
cls.accountant_user = mail_new_test_user(
cls.env,
name='Accountant',
login='accountant_1',
email='accountant_1@example.com',
notification_type='email',
groups='account.group_account_user',
company_ids=[(6, 0, cls.env.companies.ids)],
company_ids=[Command.set(cls.env.companies.ids)],
)
cls.expense_employee = cls.env['hr.employee'].create({
cls.expense_user_manager_2 = mail_new_test_user(
cls.env,
name='Expense manager',
login='expense_manager_2',
email='expense_manager_2@example.com',
notification_type='email',
groups='base.group_user,hr_expense.group_hr_expense_manager',
company_ids=[Command.set(cls.env.companies.ids)],
)
cls.expense_employee = cls.env['hr.employee'].sudo().create({
'name': 'expense_employee',
'user_id': cls.expense_user_employee.id,
'address_home_id': cls.expense_user_employee.partner_id.id,
'address_id': cls.expense_user_employee.partner_id.id,
})
cls.product_zero_cost = cls.env['product.product'].create({
'name': 'General',
'default_code': 'EXP_GEN',
'standard_price': 0.0,
'can_be_expensed': True,
})
'expense_manager_id': cls.expense_user_manager.id,
'work_contact_id': cls.expense_user_employee.partner_id.id,
'bank_account_ids': [
Command.create({
'acc_number': 'BE68539007547034',
'allow_out_payment': True,
'partner_id': cls.expense_user_employee.partner_id.id,
})]
}).sudo(False)
cls.expense_employee.user_partner_id.parent_id = cls.env.company.partner_id
# Allow the current accounting user to access the expenses.
cls.env.user.groups_id |= group_expense_manager
cls.env.user.group_ids |= group_expense_manager
# Create analytic account
cls.analytic_plan = cls.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
cls.analytic_plan = cls.env['account.analytic.plan'].create({'name': 'Expense Plan Test'})
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'analytic_account_1',
'plan_id': cls.analytic_plan.id,
@ -72,6 +78,7 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
'plan_id': cls.analytic_plan.id,
})
# Create product without cost
cls.product_c = cls.env['product.product'].create({
'name': 'product_c with no cost',
'uom_id': cls.env.ref('uom.product_uom_dozen').id,
@ -81,7 +88,64 @@ class TestExpenseCommon(AccountTestInvoicingCommon):
'taxes_id': [Command.set((cls.tax_sale_a + cls.tax_sale_b).ids)],
'supplier_taxes_id': [Command.set((cls.tax_purchase_a + cls.tax_purchase_b).ids)],
'can_be_expensed': True,
'default_code': 'product_c',
})
# Ensure products can be expensed.
# Ensure Invoicing tests products can be expensed and their code is properly set.
(cls.product_a + cls.product_b).write({'can_be_expensed': True})
cls.product_a.default_code = 'product_a'
cls.product_b.default_code = 'product_b'
cls.frozen_today = datetime(year=2022, month=1, day=25, hour=0, minute=0, second=0)
# create expense account
cls.expense_account = cls.env['account.account'].create({
'code': '610010',
'name': 'Expense Account 1'
})
@classmethod
def create_expenses(cls, values=None):
if values is None or isinstance(values, dict):
values = [values or {}]
default_values = {
'employee_id': cls.expense_employee.id,
'date': cls.frozen_today.isoformat(),
'company_id': cls.company_data['company'].id,
'currency_id': cls.company_data['currency'].id,
}
default_product_values = {
'product_id': cls.product_c.id,
'total_amount_currency': 1000.00,
}
create_values = []
for value_dict in values:
if 'product_id' not in value_dict:
default_values.update(default_product_values)
value_dict = {**default_values, **(value_dict or {})}
create_values.append(value_dict)
return cls.env['hr.expense'].create(create_values).sorted()
@classmethod
def post_expenses_with_wizard(cls, expenses, journal=None, date=None):
action = expenses.action_post()
if action:
wizard = expenses.env['hr.expense.post.wizard'].with_context(action['context']).browse(action['res_id'])
if journal:
wizard.employee_journal_id = journal.id
wizard.accounting_date = date or fields.Date.context_today(expenses)
wizard.action_post_entry()
def get_new_payment(self, expenses, amount):
""" Helper to create payments """
ctx = {'active_model': 'account.move', 'active_ids': expenses.account_move_id.ids}
with freeze_time(self.frozen_today):
payment_register = self.env['account.payment.register'].with_context(**ctx).create({
'amount': amount,
'journal_id': self.company_data['default_journal_bank'].id,
'payment_method_line_id': self.inbound_payment_method_line.id,
})
self.assertEqual(payment_register.partner_bank_id.partner_id, expenses.employee_id.work_contact_id)
return payment_register._create_payments()

View file

@ -1,22 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo import Command
from odoo.exceptions import AccessError, UserError
from odoo.tests import tagged
from odoo.tests import HttpCase, tagged, new_test_user
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
@tagged('-at_install', 'post_install')
class TestExpensesAccessRights(TestExpenseCommon):
class TestExpensesAccessRights(TestExpenseCommon, HttpCase):
def test_expense_access_rights(self):
''' The expense employee can't be able to create an expense for someone else.'''
""" The expense employee can't be able to create an expense for someone else. """
expense_employee_2 = self.env['hr.employee'].create({
expense_employee_2 = self.env['hr.employee'].sudo().create({
'name': 'expense_employee_2',
'user_id': self.env.user.id,
'address_home_id': self.env.user.partner_id.id,
'address_id': self.env.user.partner_id.id,
})
'work_contact_id': self.env.user.partner_id.id,
}).sudo(False)
with self.assertRaises(AccessError):
self.env['hr.expense'].with_user(self.expense_user_employee).create({
@ -24,127 +25,80 @@ class TestExpensesAccessRights(TestExpenseCommon):
'employee_id': expense_employee_2.id,
'product_id': self.product_a.id,
'quantity': 1,
'unit_amount': 1,
})
expense = self.env['hr.expense'].with_user(self.expense_user_employee).create({
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'quantity': 10.0,
'employee_id': self.expense_employee.id,
})
# The expense employee shouldn't be able to bypass the submit state.
with self.assertRaises(UserError):
expense.with_user(self.expense_user_employee).state = 'approve'
expense.with_user(self.expense_user_employee).state = 'approved'
expense.with_user(self.expense_user_employee).state = 'draft' # Should not raise
# Employee can report & submit their expense
expense_sheet = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({
'name': 'expense sheet for employee',
'expense_line_ids': expense,
'payment_mode': expense.payment_mode,
})
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
self.assertEqual(expense.state, 'reported') # Todo change in 17.0+
expense.with_user(self.expense_user_employee).action_submit()
self.assertEqual(expense.state, 'submitted')
# Employee can also revert from the submitted state to a draft state
expense_sheet.with_user(self.expense_user_employee).reset_expense_sheets()
self.assertEqual(expense.state, 'draft') # Todo change in 17.0+
expense.with_user(self.expense_user_employee).action_reset()
self.assertEqual(expense.state, 'draft')
def test_expense_sheet_access_rights_approve(self):
def test_expense_access_rights_user(self):
# The expense base user (without other rights) is able to create and read sheet
# The expense employee is able to a create an expense sheet.
user = new_test_user(self.env, login='test-expense', groups='base.group_user')
expense_employee = self.env['hr.employee'].sudo().create({
'name': 'expense_employee_base_user',
'user_id': user.id,
'work_contact_id': user.partner_id.id,
'expense_manager_id': self.expense_user_manager.id,
'address_id': user.partner_id.id,
}).sudo(False)
expense_sheet = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({
expense = self.env['hr.expense'].with_user(user).create({
'name': 'First Expense for employee',
'employee_id': self.expense_employee.id,
'journal_id': self.company_data['default_journal_purchase'].id,
'accounting_date': '2017-01-01',
'expense_line_ids': [
(0, 0, {
# Expense without foreign currency but analytic account.
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1000.0,
'employee_id': self.expense_employee.id,
}),
],
'employee_id': expense_employee.id,
# Expense without foreign currency but analytic account.
'product_id': self.product_a.id,
'price_unit': 1000.0,
})
self.env.flush_all()
self.start_tour("/odoo", 'hr_expense_access_rights_test_tour', login="test-expense")
self.assertRecordValues(expense, [{'state': 'submitted'}])
self.assertRecordValues(expense_sheet, [{'state': 'draft'}])
# The expense employee shouldn't be able to bypass the submit state.
def test_expense_user_cant_approve_own_expense(self):
expense = self.env['hr.expense'].with_user(self.expense_user_employee).create({
'name': "Superboy costume washing",
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'quantity': 1,
'price_unit': 1,
})
expense.with_user(self.expense_user_employee).action_submit()
with self.assertRaises(UserError):
expense_sheet.with_user(self.expense_user_employee).state = 'approve'
expense.with_user(self.expense_user_employee).action_approve()
# The expense employee is able to submit the expense sheet.
def test_expense_team_approver_cant_approve_expense_of_employee_he_does_not_manage(self):
another_standard_user = new_test_user(self.env, login='another_standard_user', groups='base.group_user')
another_standard_user_team_approver = new_test_user(self.env, login='another_standard_user_manager', groups='base.group_user,hr_expense.group_hr_expense_team_approver')
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
# The expense employee is not able to approve itself the expense sheet.
with self.assertRaises(UserError):
expense_sheet.with_user(self.expense_user_employee).approve_expense_sheets()
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
# An expense manager is required for this step.
expense_sheet.with_user(self.expense_user_manager).approve_expense_sheets()
self.assertRecordValues(expense_sheet, [{'state': 'approve'}])
# The expense employee shouldn't be able to modify an approved expense.
with self.assertRaises(UserError):
expense_sheet.expense_line_ids[0].with_user(self.expense_user_employee).total_amount = 1000.0
# An expense manager is not able to create the journal entry.
another_employee = self.env['hr.employee'].sudo().create({
'name': 'another_employee',
'user_id': another_standard_user.id,
'work_contact_id': another_standard_user.partner_id.id,
'expense_manager_id': self.expense_user_manager.id,
}).sudo(False)
expense = self.env['hr.expense'].with_user(another_standard_user).create({
'name': "Superboy costume washing",
'employee_id': another_employee.id,
'product_id': self.product_a.id,
'quantity': 1,
'price_unit': 1,
})
expense.with_user(another_standard_user).action_submit()
with self.assertRaises(AccessError):
expense_sheet.with_user(self.expense_user_manager).action_sheet_move_create()
self.assertRecordValues(expense_sheet, [{'state': 'approve'}])
# An expense manager having accounting access rights is able to create the journal entry.
expense_sheet.with_user(self.accountant_user).action_sheet_move_create()
self.assertRecordValues(expense_sheet, [{'state': 'post'}])
def test_expense_sheet_access_rights_refuse(self):
# The expense employee is able to a create an expense sheet.
expense_sheet = self.env['hr.expense.sheet'].with_user(self.expense_user_employee).create({
'name': 'First Expense for employee',
'employee_id': self.expense_employee.id,
'journal_id': self.company_data['default_journal_purchase'].id,
'accounting_date': '2017-01-01',
'expense_line_ids': [
(0, 0, {
# Expense without foreign currency but analytic account.
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1000.0,
'employee_id': self.expense_employee.id,
}),
],
})
self.assertRecordValues(expense_sheet, [{'state': 'draft'}])
# The expense employee is able to submit the expense sheet.
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
# The expense employee is not able to refuse itself the expense sheet.
with self.assertRaises(UserError):
expense_sheet.with_user(self.expense_user_employee).refuse_sheet('')
self.assertRecordValues(expense_sheet, [{'state': 'submit'}])
# An expense manager is required for this step.
expense_sheet.with_user(self.expense_user_manager).refuse_sheet('')
self.assertRecordValues(expense_sheet, [{'state': 'cancel'}])
expense.with_user(another_standard_user_team_approver).action_approve()

View file

@ -1,35 +1,46 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged
from odoo.exceptions import UserError
@tagged('-at_install', 'post_install')
class TestExpensesMailImport(TestExpenseCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.product_a.default_code = 'product_a'
cls.product_b.default_code = 'product_b'
def test_import_expense_from_email(self):
message_parsed = {
'message_id': "the-world-is-a-ghetto",
'subject': '%s %s' % (self.product_a.default_code, self.product_a.standard_price),
'email_from': self.expense_user_employee.email,
'to': 'catchall@yourcompany.com',
'body': "Don't you know, that for me, and for you",
'attachments': [],
}
messages = (
{
'message_id': "the-world-is-a-ghetto",
'subject': f'{self.product_a.default_code} {self.product_a.standard_price}',
'email_from': self.expense_user_employee.email,
'to': 'catchall@yourcompany.com',
'body': "Don't you know, that for me, and for you",
'attachments': [],
}, {
'message_id': "the-world-is-a-ghetto",
'subject': 'no product code 800',
'email_from': self.expense_user_employee.email,
'to': 'catchall@yourcompany.com',
'body': "Don't you know, that for me, and for you",
'attachments': [],
}, {
'message_id': "test",
'subject': 'product_c my description 100',
'email_from': self.expense_user_employee.email,
'to': 'catchall@yourcompany.com',
'body': "test",
'attachments': [],
}
)
expenses = self.env['hr.expense']
for message in messages:
expenses |= self.env['hr.expense'].message_new(message)
expense = self.env['hr.expense'].message_new(message_parsed)
self.assertRecordValues(expense, [{
'product_id': self.product_a.id,
'total_amount': 800.0,
'employee_id': self.expense_employee.id,
}])
self.assertRecordValues(expenses, [
{'product_id': self.product_a.id, 'total_amount_currency': 800.0, 'employee_id': self.expense_employee.id},
{'product_id': False, 'total_amount_currency': 800.0, 'employee_id': self.expense_employee.id},
{'product_id': self.product_c.id, 'total_amount_currency': 100.0, 'employee_id': self.expense_employee.id},
])
def test_import_expense_from_email_several_employees(self):
"""When a user has several employees' profiles from different companies, the right record should be selected"""
@ -38,12 +49,12 @@ class TestExpensesMailImport(TestExpenseCommon):
user.company_id = company_2.id
# Create a second employee linked to the user for another company
company_2_employee = self.env['hr.employee'].create({
company_2_employee = self.env['hr.employee'].sudo().create({
'name': 'expense_employee_2',
'company_id': company_2.id,
'user_id': user.id,
'work_email': user.email,
})
}).sudo(False)
message_parsed = {
'message_id': "the-world-is-a-ghetto",
@ -59,9 +70,9 @@ class TestExpensesMailImport(TestExpenseCommon):
}])
def test_import_expense_from_email_employee_without_user(self):
"""When an employee is not linked to a user, he has to be able to create expenses from email"""
""" When an employee is not linked to a user, he has to be able to create expenses from email """
employee = self.expense_employee
employee.user_id = False
employee.sudo().user_id = False
message_parsed = {
'message_id': "the-world-is-a-ghetto",
@ -95,42 +106,16 @@ class TestExpensesMailImport(TestExpenseCommon):
'employee_id': self.expense_employee.id,
}])
def test_import_expense_from_email_product_no_cost(self):
"""
We have to compute a value for the total amount
even if the product has no cost.
"""
product_no_cost = self.env['product.product'].create({
'name': 'Product No Cost',
'standard_price': 0.0,
'can_be_expensed': True,
'default_code': 'product_no_cost',
})
message_parsed = {
'message_id': "test",
'subject': 'product_no_cost my description 100',
'email_from': self.expense_user_employee.email,
'to': 'catchall@yourcompany.com',
'body': "test",
'attachments': [],
}
expense = self.env['hr.expense'].message_new(message_parsed)
self.assertRecordValues(expense, [{
'product_id': product_no_cost.id,
'total_amount': 100.0,
'employee_id': self.expense_employee.id,
}])
def test_import_expense_from_mail_parsing_subjects(self):
def assertParsedValues(subject, currencies, exp_description, exp_amount, exp_product):
product, amount, currency_id, description = self.env['hr.expense']\
.with_user(self.expense_user_employee)\
def assertParsedValues(subject, currencies, exp_description, exp_amount, exp_product, exp_currency):
product, amount, currency_id, description = self.env['hr.expense'] \
.with_user(self.expense_user_employee) \
._parse_expense_subject(subject, currencies)
self.assertEqual(product, exp_product)
self.assertAlmostEqual(amount, exp_amount)
self.assertEqual(description, exp_description)
self.assertEqual(currency_id, exp_currency)
# Without Multi currency access
assertParsedValues(
@ -139,86 +124,107 @@ class TestExpensesMailImport(TestExpenseCommon):
"bar electro wizard",
1205.91,
self.product_a,
self.company_data['currency'],
)
# subject having other currency then company currency, it should ignore other currency then company currency
assertParsedValues(
"foo bar %s1406.91 royal giant" % self.currency_data['currency'].symbol,
f'foo bar {self.other_currency.symbol}1406.91 royal giant',
self.company_data['currency'],
"foo bar %s royal giant" % self.currency_data['currency'].symbol,
f'foo bar {self.other_currency.symbol} royal giant',
1406.91,
self.env['product.product'],
self.company_data['currency'],
)
# With Multi currency access
self.expense_user_employee.groups_id |= self.env.ref('base.group_multi_currency')
self.expense_user_employee.group_ids |= self.env.ref('base.group_multi_currency')
assertParsedValues(
"product_a foo bar $2205.92 elite barbarians",
self.company_data['currency'],
"foo bar elite barbarians",
2205.92,
self.product_a,
self.company_data['currency'],
)
# subject having other currency then company currency, it should accept other currency because multi currency is activated
assertParsedValues(
"product_a %s2510.90 chhota bheem" % self.currency_data['currency'].symbol,
self.company_data['currency'] + self.currency_data['currency'],
f'product_a {self.other_currency.symbol}2510.90 chhota bheem',
self.company_data['currency'] + self.other_currency,
"chhota bheem",
2510.90,
self.product_a,
self.other_currency,
)
# subject without product and currency, should take company currency and default product
assertParsedValues(
"foo bar 109.96 spear goblins",
self.company_data['currency'] + self.currency_data['currency'],
self.company_data['currency'] + self.other_currency,
"foo bar spear goblins",
109.96,
self.env['product.product'],
self.company_data['currency'],
)
# subject with currency symbol at end
assertParsedValues(
"product_a foo bar 2910.94$ inferno dragon",
self.company_data['currency'] + self.currency_data['currency'],
self.company_data['currency'] + self.other_currency,
"foo bar inferno dragon",
2910.94,
self.product_a,
self.company_data['currency'],
)
# subject with no amount and product
assertParsedValues(
"foo bar mega knight",
self.company_data['currency'] + self.currency_data['currency'],
self.company_data['currency'] + self.other_currency,
"foo bar mega knight",
0.0,
self.env['product.product'],
self.company_data['currency'],
)
# price with a comma
assertParsedValues(
"foo bar 291,56$ mega knight",
self.company_data['currency'] + self.currency_data['currency'],
self.company_data['currency'] + self.other_currency,
"foo bar mega knight",
291.56,
self.env['product.product'],
self.company_data['currency'],
)
# price without decimals
# price different decimals than currency
assertParsedValues(
"foo bar 291$ mega knight",
self.company_data['currency'] + self.currency_data['currency'],
self.company_data['currency'] + self.other_currency,
"foo bar mega knight",
291.0,
self.env['product.product'],
self.company_data['currency'],
)
assertParsedValues(
"product_a foo bar 291.5$ mega knight",
self.company_data['currency'] + self.currency_data['currency'],
self.company_data['currency'] + self.other_currency,
"foo bar mega knight",
291.5,
self.product_a,
self.company_data['currency'],
)
def test_import_expense_from_mail_action_submit_errors(self):
""" Make sure we get the expected UserError when trying to validate an expense with no product """
message = {
'message_id': "the-world-is-a-ghetto",
'subject': 'no product code 800',
'email_from': self.expense_user_employee.email,
'to': 'catchall@yourcompany.com',
'body': "Don't you know, that for me, and for you",
'attachments': [],
}
expense = self.env['hr.expense'].message_new(message)
self.assertRaisesRegex(UserError, r"You can not submit an expense without a category\.", expense.action_submit)

View file

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged
from odoo.exceptions import UserError
@tagged('post_install', '-at_install')
class TestExpenseMultiCompany(TestExpenseCommon):
def test_expense_sheet_multi_company_approve(self):
self.expense_employee.company_id = self.company_data_2['company']
# The expense employee is able to a create an expense sheet for company_2.
expense_sheet = self.env['hr.expense.sheet']\
.with_user(self.expense_user_employee)\
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
.create({
'name': 'First Expense for employee',
'employee_id': self.expense_employee.id,
'journal_id': self.company_data_2['default_journal_purchase'].id,
'accounting_date': '2017-01-01',
'expense_line_ids': [
(0, 0, {
# Expense without foreign currency but analytic account.
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1000.0,
'employee_id': self.expense_employee.id,
}),
],
})
self.assertRecordValues(expense_sheet, [{'company_id': self.company_data_2['company'].id}])
# The expense employee is able to submit the expense sheet.
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
# An expense manager is not able to approve without access to company_2.
with self.assertRaises(UserError):
expense_sheet\
.with_user(self.expense_user_manager)\
.with_context(allowed_company_ids=self.company_data['company'].ids)\
.approve_expense_sheets()
# An expense manager is able to approve with access to company_2.
expense_sheet\
.with_user(self.expense_user_manager)\
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
.approve_expense_sheets()
# An expense manager having accounting access rights is not able to create the journal entry without access
# to company_2.
with self.assertRaises(UserError):
expense_sheet\
.with_user(self.env.user)\
.with_context(allowed_company_ids=self.company_data['company'].ids)\
.action_sheet_move_create()
# An expense manager having accounting access rights is able to create the journal entry with access to
# company_2.
expense_sheet\
.with_user(self.env.user)\
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
.action_sheet_move_create()
def test_expense_sheet_multi_company_refuse(self):
self.expense_employee.company_id = self.company_data_2['company']
# The expense employee is able to a create an expense sheet for company_2.
expense_sheet = self.env['hr.expense.sheet']\
.with_user(self.expense_user_employee)\
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
.create({
'name': 'First Expense for employee',
'employee_id': self.expense_employee.id,
'journal_id': self.company_data_2['default_journal_purchase'].id,
'accounting_date': '2017-01-01',
'expense_line_ids': [
(0, 0, {
# Expense without foreign currency but analytic account.
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1000.0,
'employee_id': self.expense_employee.id,
}),
],
})
self.assertRecordValues(expense_sheet, [{'company_id': self.company_data_2['company'].id}])
# The expense employee is able to submit the expense sheet.
expense_sheet.with_user(self.expense_user_employee).action_submit_sheet()
# An expense manager is not able to approve without access to company_2.
with self.assertRaises(UserError):
expense_sheet\
.with_user(self.expense_user_manager)\
.with_context(allowed_company_ids=self.company_data['company'].ids)\
.refuse_sheet('')
# An expense manager is able to approve with access to company_2.
expense_sheet\
.with_user(self.expense_user_manager)\
.with_context(allowed_company_ids=self.company_data_2['company'].ids)\
.refuse_sheet('')

View file

@ -1,79 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged, Form
@tagged('post_install', '-at_install')
class TestExpenseStandardPriceUpdateWarning(TestExpenseCommon):
def test_expense_standard_price_update_warning(self):
self.expense_cat_A = self.env['product.product'].create({
'name': 'Category A',
'default_code': 'CA',
'standard_price': 0.0,
})
self.expense_cat_B = self.env['product.product'].create({
'name': 'Category B',
'default_code': 'CB',
'standard_price': 0.0,
})
self.expense_cat_C = self.env['product.product'].create({
'name': 'Category C',
'default_code': 'CC',
'standard_price': 0.0,
})
self.expense_1 = self.env['hr.expense'].create({
'employee_id': self.expense_employee.id,
'name': 'Expense 1',
'product_id': self.expense_cat_A.id,
'total_amount': 1,
})
self.expense_2 = self.env['hr.expense'].create({
'employee_id': self.expense_employee.id,
'name': 'Expense 2',
'product_id': self.expense_cat_B.id,
'total_amount': 5,
})
# At first, there is no warning message on the categories because their prices are 0
self.assertFalse(self.expense_cat_A.standard_price_update_warning)
self.assertFalse(self.expense_cat_B.standard_price_update_warning)
self.assertFalse(self.expense_cat_C.standard_price_update_warning)
# When modifying the price of the first category, a message should appear as a an expense will be modified.
with Form(self.expense_cat_A, view="hr_expense.product_product_expense_form_view") as form:
form.standard_price = 5
self.assertTrue(form.standard_price_update_warning)
# When modifying the price of the second category, no message should appear as the price of the linked
# expense is the price of the category that is going to be saved.
with Form(self.expense_cat_B, view="hr_expense.product_product_expense_form_view") as form:
form.standard_price = 5
self.assertFalse(form.standard_price_update_warning)
# When modifying the price of the thirs category, no message should appear as no expense is linked to it.
with Form(self.expense_cat_C, view="hr_expense.product_product_expense_form_view") as form:
form.standard_price = 5
self.assertFalse(form.standard_price_update_warning)
def test_compute_standard_price_update_warning_product_with_and_without_expense(self):
self.product_expensed = self.env['product.product'].create({
'name': 'Category A',
'default_code': 'CA',
'standard_price': 0.0,
})
self.product_not_expensed = self.env['product.product'].create({
'name': 'Category B',
'default_code': 'CB',
'standard_price': 0.0,
})
self.expense_1 = self.env['hr.expense'].create({
'employee_id': self.expense_employee.id,
'name': 'Expense 1',
'product_id': self.product_expensed.id,
'total_amount': 1,
})
# Having expensed and not expensed product in a recordset can cause a key error in the compute
# as it looks for the product's origin id in the mapp dict, wich doesn't exists if the product
# wasn't expensed.
(self.product_expensed | self.product_not_expensed)._compute_standard_price_update_warning()

View file

@ -1,313 +1,214 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, Command
from odoo.addons.mail.tests.common import MailCase
from odoo import fields
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged
@tagged('-at_install', 'post_install')
class TestExpensesStates(TestExpenseCommon):
class TestExpensesStates(TestExpenseCommon, MailCase):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.expense_states_employee_sheet = cls.env['hr.expense.sheet'].create({
def setUpClass(cls):
super().setUpClass()
cls.expenses_employee = cls.create_expenses({
'name': 'Expense Employee 1',
'employee_id': cls.expense_employee.id,
'expense_line_ids': [Command.create({
'name': 'Expense Employee 1',
'employee_id': cls.expense_employee.id,
'product_id': cls.product_c.id,
'total_amount': 100.00,
})],
})
cls.expense_states_company_sheet = cls.env['hr.expense.sheet'].create({
cls.expenses_company = cls.create_expenses({
'name': 'Expense Company 1',
'employee_id': cls.expense_employee.id,
'expense_line_ids': [Command.create({
'name': 'Expense Company 1',
'payment_mode': 'company_account',
'employee_id': cls.expense_employee.id,
'product_id': cls.product_c.id,
'total_amount': 100.00,
})],
'payment_mode': 'company_account',
# To avoid duplicated expense wizard
'total_amount_currency': 1000,
'date': '2017-01-01',
})
cls.expense_states_sheets = cls.expense_states_employee_sheet + cls.expense_states_company_sheet
cls.expenses_all = cls.expenses_employee + cls.expenses_company
cls.paid_or_in_payment_state = cls.env['account.move']._get_invoice_in_payment_state()
def test_expense_state_synchro_1_regular_flow(self):
# STEP 1: Draft
self.assertRecordValues(self.expense_states_sheets.expense_line_ids, [
{'payment_mode': 'own_account', 'state': 'draft'},
self.assertRecordValues(self.expenses_all, [
{'payment_mode': 'own_account', 'state': 'draft'},
{'payment_mode': 'company_account', 'state': 'draft'},
])
self.assertRecordValues(self.expense_states_sheets, [
{'payment_mode': 'own_account', 'state': 'draft', 'payment_state': 'not_paid'},
{'payment_mode': 'company_account', 'state': 'draft', 'payment_state': 'not_paid'},
])
self.assertFalse(self.expense_states_sheets.account_move_id)
self.assertFalse(self.expenses_all.account_move_id)
# STEP 2: Submit
self.expense_states_sheets.action_submit_sheet()
self.assertRecordValues(self.expense_states_sheets.expense_line_ids, [
{'payment_mode': 'own_account', 'state': 'reported'},
{'payment_mode': 'company_account', 'state': 'reported'},
])
self.assertRecordValues(self.expense_states_sheets, [
{'payment_mode': 'own_account', 'state': 'submit', 'payment_state': 'not_paid'},
{'payment_mode': 'company_account', 'state': 'submit', 'payment_state': 'not_paid'},
self.expenses_all.action_submit()
self.assertRecordValues(self.expenses_all, [
{'payment_mode': 'own_account', 'state': 'submitted'},
{'payment_mode': 'company_account', 'state': 'submitted'},
])
self.assertFalse(self.expenses_all.account_move_id)
# STEP 3: Approve
self.expense_states_sheets.approve_expense_sheets()
self.assertRecordValues(self.expense_states_sheets.expense_line_ids, [
{'payment_mode': 'own_account', 'state': 'approved'},
self.expenses_all.action_approve()
self.assertRecordValues(self.expenses_all, [
{'payment_mode': 'own_account', 'state': 'approved'},
{'payment_mode': 'company_account', 'state': 'approved'},
])
self.assertRecordValues(self.expense_states_sheets, [
{'payment_mode': 'own_account', 'state': 'approve', 'payment_state': 'not_paid'},
{'payment_mode': 'company_account', 'state': 'approve', 'payment_state': 'not_paid'},
])
self.assertFalse(self.expense_states_sheets.account_move_id)
self.assertFalse(self.expenses_all.account_move_id)
# STEP 4: Post
self.expense_states_sheets.action_sheet_move_create()
self.assertRecordValues(self.expense_states_sheets.expense_line_ids, [
{'payment_mode': 'own_account', 'state': 'approved'},
{'payment_mode': 'company_account', 'state': 'done'},
# STEP 4: Post (create moves)
self.post_expenses_with_wizard(self.expenses_all)
self.assertRecordValues(self.expenses_all, [
{'payment_mode': 'own_account', 'state': 'posted'},
{'payment_mode': 'company_account', 'state': 'paid'},
])
self.assertRecordValues(self.expense_states_sheets, [
{'payment_mode': 'own_account', 'state': 'post', 'payment_state': 'not_paid'},
{'payment_mode': 'company_account', 'state': 'done', 'payment_state': 'paid'},
])
self.assertRecordValues(self.expense_states_sheets.account_move_id, [
{'state': 'posted'},
{'state': 'posted'},
self.assertRecordValues(self.expenses_all.account_move_id, [
{'state': 'posted', 'payment_state': 'not_paid'},
{'state': 'posted', 'payment_state': 'not_paid'},
])
def test_expense_state_synchro_2_employee_specific_flow(self):
self.expense_states_sheets.action_submit_sheet()
self.expense_states_sheets.approve_expense_sheets()
self.expense_states_sheets.action_sheet_move_create()
self.assertEqual('in_process', self.expenses_company.account_move_id.origin_payment_id.state)
self.assertFalse(self.expenses_employee.account_move_id.origin_payment_id)
# STEP 1: ER posted -> Reset move to draft
self.expense_states_employee_sheet.account_move_id.button_draft()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'approved'},
def test_expense_state_synchro_1_cancel_move(self):
""" Posted -> Cancel move (Back to approved) """
self.expenses_all.action_submit()
self.expenses_all.action_approve()
self.post_expenses_with_wizard(self.expenses_all)
self.expenses_all.account_move_id.button_draft()
self.expenses_all.account_move_id.button_cancel()
self.assertRecordValues(self.expenses_all, [
{'state': 'approved', 'account_move_id': False},
{'state': 'approved', 'account_move_id': False},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'post', 'payment_state': 'not_paid'},
])
self.assertRecordValues(self.expense_states_employee_sheet.account_move_id, [
{'state': 'draft'},
self.assertFalse(self.expenses_all.account_move_id)
def test_expense_state_synchro_1_unlink_move(self):
""" Posted -> Unlink move/payment (Back to approved) """
self.expenses_all.action_submit()
self.expenses_all.action_approve()
self.post_expenses_with_wizard(self.expenses_all)
self.expenses_all.account_move_id.button_draft()
self.expenses_all.account_move_id.origin_payment_id.unlink()
self.expenses_all.account_move_id.unlink()
self.assertRecordValues(self.expenses_all, [
{'state': 'approved', 'account_move_id': False},
{'state': 'approved', 'account_move_id': False},
])
# STEP 2: ER posted with draft move -> Cancel move (nothing changes)
self.expense_states_employee_sheet.account_move_id.button_cancel()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'approved'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'post', 'payment_state': 'not_paid'},
])
self.assertRecordValues(self.expense_states_employee_sheet.account_move_id, [
{'state': 'cancel'},
])
def test_expense_state_synchro_1_reverse_move(self):
""" Posted -> Reverse move (Back to approved) """
self.expenses_all.action_submit()
self.expenses_all.action_approve()
self.post_expenses_with_wizard(self.expenses_all)
# Change move state to draft
self.expense_states_employee_sheet.account_move_id.button_draft()
# STEP 3: ER posted with draft move -> unlink move (Reverts to approve state)
self.expense_states_employee_sheet.account_move_id.unlink()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'approved'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'approve', 'payment_state': 'not_paid'},
])
self.assertFalse(self.expense_states_employee_sheet.account_move_id)
# Re-create posted move
self.expense_states_employee_sheet.action_sheet_move_create()
# STEP 4: ER with draft move -> Reverse move (Reverts to approve state)
self.expense_states_employee_sheet.account_move_id._reverse_moves(
default_values_list=[{'invoice_date': fields.Date.context_today(self.expense_states_employee_sheet)}],
self.expenses_all.account_move_id._reverse_moves(
default_values_list=[{'invoice_date': fields.Date.context_today(self.expenses_all)}],
cancel=True,
)
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'approved'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'approve', 'payment_state': 'not_paid'},
])
self.assertFalse(self.expense_states_employee_sheet.account_move_id)
# Change the report state to a partially paid one
self.expense_states_employee_sheet.action_sheet_move_create()
action_context = self.expense_states_employee_sheet.action_register_payment()['context']
self.env['account.payment.register'].with_context(action_context).create({'amount': 1})._create_payments()
# STEP 5: ER Done (partially paid) -> Reset move to draft
self.expense_states_employee_sheet.account_move_id.button_draft()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'approved'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'post', 'payment_state': 'not_paid'},
])
self.assertRecordValues(self.expense_states_employee_sheet.account_move_id, [
{'state': 'draft'},
self.assertRecordValues(self.expenses_all, [
{'state': 'approved', 'account_move_id': False},
{'state': 'approved', 'account_move_id': False},
])
# Re-post the move & partially pay it
self.expense_states_employee_sheet.account_move_id.action_post()
action_context = self.expense_states_employee_sheet.action_register_payment()['context']
self.env['account.payment.register'].with_context(action_context).create({'amount': 1})._create_payments()
def test_expense_state_synchro_2_employee_specific_flow_1(self):
""" Posted -> Reset move to draft (No change)"""
self.expenses_employee.action_submit()
self.expenses_employee.action_approve()
self.post_expenses_with_wizard(self.expenses_employee)
# STEP 6: ER Done (partially paid) -> fully paid
action_context = self.expense_states_employee_sheet.action_register_payment()['context']
self.env['account.payment.register'].with_context(action_context).create(
{'amount': self.expense_states_employee_sheet.amount_residual}
)._create_payments()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'done'},
self.expenses_employee.account_move_id.button_draft()
self.assertEqual(self.expenses_employee.state, 'posted')
self.assertRecordValues(self.expenses_employee.account_move_id, [
{'state': 'draft', 'payment_state': 'not_paid'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'done', 'payment_state': self.paid_or_in_payment_state},
])
self.assertRecordValues(self.expense_states_employee_sheet.account_move_id, [
def test_expense_state_synchro_2_employee_specific_flow_2(self):
""" Posted -> Paid in one payment (Set to paid) """
self.expenses_employee.action_submit()
self.expenses_employee.action_approve()
self.post_expenses_with_wizard(self.expenses_employee)
self.get_new_payment(self.expenses_employee, self.expenses_employee.total_amount)
self.assertEqual(self.expenses_employee.state, self.paid_or_in_payment_state)
self.assertRecordValues(self.expenses_employee.account_move_id, [
{'state': 'posted', 'payment_state': self.paid_or_in_payment_state},
])
# STEP 7: ER Done (fully paid) -> Reset move to draft
self.expense_states_employee_sheet.account_move_id.button_draft()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'approved'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'post', 'payment_state': 'not_paid'},
])
self.assertRecordValues(self.expense_states_employee_sheet.account_move_id, [
{'state': 'draft'},
def test_expense_state_synchro_2_employee_specific_flow_3(self):
""" Posted -> Paid in several payment (Set to paid, even when partially)"""
self.expenses_employee.action_submit()
self.expenses_employee.action_approve()
self.post_expenses_with_wizard(self.expenses_employee)
self.get_new_payment(self.expenses_employee, 1)
self.assertEqual(self.expenses_employee.state, self.paid_or_in_payment_state)
self.assertRecordValues(self.expenses_employee.account_move_id, [
{'state': 'posted', 'payment_state': 'partial'},
])
# Change the report state to a paid one
self.expense_states_employee_sheet.account_move_id.unlink()
self.expense_states_employee_sheet.action_sheet_move_create()
action_context = self.expense_states_employee_sheet.action_register_payment()['context']
payment = self.env['account.payment.register'].with_context(action_context).create({})._create_payments()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'done'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'done', 'payment_state': self.paid_or_in_payment_state},
])
self.assertRecordValues(self.expense_states_employee_sheet.account_move_id, [
{'state': 'posted'},
self.get_new_payment(self.expenses_employee, self.expenses_employee.total_amount - 1)
self.assertEqual(self.expenses_employee.state, self.paid_or_in_payment_state)
self.assertRecordValues(self.expenses_employee.account_move_id, [
{'state': 'posted', 'payment_state': self.paid_or_in_payment_state},
])
# STEP 8: ER Done (fully paid) -> Reset to draft payment (Reverts to post state)
payment.action_draft()
self.assertRecordValues(self.expense_states_employee_sheet.expense_line_ids, [
{'state': 'approved'},
])
self.assertRecordValues(self.expense_states_employee_sheet, [
{'state': 'post', 'payment_state': 'not_paid'},
])
self.assertRecordValues(self.expense_states_employee_sheet.account_move_id, [
{'state': 'posted'},
def test_expense_state_synchro_2_employee_specific_flow_4(self):
""" (Partially/) Paid -> Reset move to draft (Back to posted) """
self.expenses_employee.action_submit()
self.expenses_employee.action_approve()
self.post_expenses_with_wizard(self.expenses_employee)
self.get_new_payment(self.expenses_employee, self.expenses_employee.total_amount)
self.expenses_employee.account_move_id.button_draft()
self.expenses_employee.account_move_id.line_ids.remove_move_reconcile()
self.assertEqual(self.expenses_employee.state, 'posted')
self.assertRecordValues(self.expenses_employee.account_move_id, [
{'state': 'draft', 'payment_state': 'not_paid'},
])
def test_expense_state_synchro_3_company_specific_flow(self):
self.expense_states_company_sheet.action_submit_sheet()
self.expense_states_company_sheet.approve_expense_sheets()
self.expense_states_company_sheet.action_sheet_move_create()
""" Paid -> Reset move or payment to draft (Stay paid) """
self.expenses_company.action_submit()
self.expenses_company.action_approve()
self.expenses_company.action_post()
# STEP 1: ER Done & paid -> Reset move or payment to draft (nothing changes)
self.expense_states_company_sheet.account_move_id.button_draft()
self.assertRecordValues(self.expense_states_company_sheet.expense_line_ids, [
{'state': 'done'},
])
self.assertRecordValues(self.expense_states_company_sheet, [
{'state': 'done', 'payment_state': 'paid'},
])
self.assertRecordValues(self.expense_states_company_sheet.account_move_id, [
{'state': 'draft'},
self.expenses_company.account_move_id.button_draft()
self.assertEqual(self.expenses_company.state, 'paid')
self.assertRecordValues(self.expenses_company.account_move_id, [
{'state': 'draft', 'payment_state': 'not_paid'},
])
self.expense_states_company_sheet.account_move_id.action_post()
self.expense_states_company_sheet.account_move_id.payment_id.action_draft()
self.assertRecordValues(self.expense_states_company_sheet.expense_line_ids, [
{'state': 'done'},
])
self.assertRecordValues(self.expense_states_company_sheet, [
{'state': 'done', 'payment_state': 'paid'},
])
self.assertRecordValues(self.expense_states_company_sheet.account_move_id, [
{'state': 'draft'},
self.expenses_company.account_move_id.action_post()
self.assertEqual(self.expenses_company.state, 'paid')
self.expenses_company.account_move_id.origin_payment_id.action_draft()
self.assertEqual(self.expenses_company.state, 'paid')
self.assertRecordValues(self.expenses_company.account_move_id, [
{'state': 'draft', 'payment_state': 'not_paid'},
])
# STEP 2: ER Done & paid (draft move) -> Cancel move or payment (nothing changes)
self.expense_states_company_sheet.account_move_id.button_cancel()
self.assertRecordValues(self.expense_states_company_sheet.expense_line_ids, [
{'state': 'done'},
])
self.assertRecordValues(self.expense_states_company_sheet, [
{'state': 'done', 'payment_state': 'paid'},
])
self.assertRecordValues(self.expense_states_company_sheet.account_move_id, [
{'state': 'cancel'},
])
self.expense_states_company_sheet.account_move_id.button_draft()
self.expense_states_company_sheet.account_move_id.payment_id.action_cancel()
self.assertRecordValues(self.expense_states_company_sheet.expense_line_ids, [
{'state': 'done'},
])
self.assertRecordValues(self.expense_states_company_sheet, [
{'state': 'done', 'payment_state': 'paid'},
])
self.assertRecordValues(self.expense_states_company_sheet.account_move_id, [
{'state': 'cancel'},
])
def test_expense_state_autovalidation(self):
""" Test the auto-validation flow skips 'submitted' state when there is no manager"""
self.expense_employee.sudo().expense_manager_id = False
self.expenses_all.sudo().manager_id = False
self.expenses_all.action_submit()
self.assertSequenceEqual(['approved', 'approved'], self.expenses_all.mapped('state'))
# Change move state to draft
self.expense_states_company_sheet.account_move_id.button_draft()
# STEP 3: ER draft & paid -> Delete move (Back to approve state)
self.expense_states_company_sheet.account_move_id.unlink()
self.assertRecordValues(self.expense_states_company_sheet.expense_line_ids, [
{'state': 'approved'},
])
self.assertRecordValues(self.expense_states_company_sheet, [
{'state': 'approve', 'payment_state': 'not_paid'},
])
self.assertFalse(self.expense_states_company_sheet.account_move_id)
# Re-create posted move
self.expense_states_company_sheet.action_sheet_move_create()
self.assertRecordValues(self.expense_states_company_sheet.expense_line_ids, [
{'state': 'done'},
])
self.assertRecordValues(self.expense_states_company_sheet, [
{'payment_mode': 'company_account', 'state': 'done', 'payment_state': 'paid'},
])
self.assertRecordValues(self.expense_states_company_sheet.account_move_id, [
{'state': 'posted'},
])
# STEP 4: ER Done & paid -> Reverse move (Change payment state to 'reversed')
self.expense_states_company_sheet.account_move_id._reverse_moves(
default_values_list=[{'invoice_date': fields.Date.context_today(self.expense_states_company_sheet)}],
cancel=True,
)
self.assertRecordValues(self.expense_states_company_sheet.expense_line_ids, [
{'state': 'done'},
])
self.assertRecordValues(self.expense_states_company_sheet, [
{'state': 'done', 'payment_state': 'reversed'},
])
self.assertRecordValues(self.expense_states_company_sheet.account_move_id, [
{'state': 'posted'},
])
def test_expense_next_activity(self):
""" Test next activity is assigned to the right manager, no notification is sent, but validation email is sent"""
self.expenses_employee.manager_id = self.expense_user_manager_2
with self.mock_mail_gateway():
self.expenses_employee.action_submit()
self.env['hr.expense']._cron_send_submitted_expenses_mail()
mail_activity = self.env['mail.activity'].search([('res_model', '=', 'hr.expense'), ('res_id', '=', self.expenses_employee.id)])
self.assertEqual(mail_activity.user_id.id, self.expense_user_manager_2.id)
# No notification should be sent
notification_message = self.env['mail.message'].search([('partner_ids', 'in', self.expense_user_manager_2.partner_id.ids), ('display_name', '=', mail_activity.res_name)])
self.assertFalse(notification_message)
# Expenses submitted email is sent via cron
expenses_submitted = self.env['mail.mail'].search(
[('email_to', '=', self.expense_user_manager_2.email),
('subject', '=', "New expenses waiting for your approval")]
)
self.assertEqual(len(expenses_submitted), 1)

View file

@ -1,7 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged, HttpCase
@tagged('post_install', '-at_install')
class TestExpensesTour(HttpCase):
def test_tour_expenses(self):
self.start_tour("/web", "hr_expense_test_tour", login="admin")
self.start_tour("/odoo", "hr_expense_test_tour", login="admin")

View file

@ -0,0 +1,33 @@
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged, HttpCase
from odoo.tools import mute_logger
@tagged('-at_install', 'post_install')
class TestUi(TestExpenseCommon, HttpCase):
browser_size = "1920,1080"
def test_expense_manager_can_always_set_employee(self):
"""Test that users with access rights to `hr.expense` can set the employee on them
by using the usual form view, even if they do not have access rights to `hr.employee`
"""
employee_1 = self.expense_employee
employee_2 = self.env['hr.employee'].sudo().create({'name': 'employee2'})
expense = self.env['hr.expense'].create({
'name': 'expense_for_tour_0',
'employee_id': employee_2.id,
'product_id': self.product_c.id,
'total_amount': 1,
})
self.start_tour('/odoo', 'create_expense_no_employee_access_tour', login=self.expense_user_manager.login)
self.assertEqual(expense.employee_id.id, employee_1.id, "Employee should have been changed by tour")
def test_no_zero_amount_expense_in_expense(self):
"""
The test ensures that attempting to submit an expense with a zero amount fails as expected
and that a valid amount can be set subsequently.
"""
expense = self.create_expenses({'name': 'expense_for_tour'})
with mute_logger("odoo.http"):
self.start_tour('/odoo', 'do_not_create_zero_amount_expense', login=self.expense_user_manager.login)
self.assertEqual(expense.total_amount_currency, 10.0, "Expense amount should have been set by tour")