Initial commit: Hr packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 62531cd146
2820 changed files with 1432848 additions and 0 deletions

View file

@ -0,0 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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_expenses_tour

View file

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
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)
group_expense_manager = cls.env.ref('hr_expense.group_hr_expense_manager')
cls.expense_user_employee = mail_new_test_user(
cls.env,
name='expense_user_employee',
login='expense_user_employee',
email='expense_user_employee@example.com',
notification_type='email',
groups='base.group_user',
company_ids=[(6, 0, cls.env.companies.ids)],
)
cls.expense_user_manager = mail_new_test_user(
cls.env,
name='Expense manager',
login='expense_manager_1',
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)],
)
cls.expense_employee = cls.env['hr.employee'].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,
})
# Allow the current accounting user to access the expenses.
cls.env.user.groups_id |= group_expense_manager
# Create analytic account
cls.analytic_plan = cls.env['account.analytic.plan'].create({'name': 'Plan Test', 'company_id': False})
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'analytic_account_1',
'plan_id': cls.analytic_plan.id,
})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
'name': 'analytic_account_2',
'plan_id': cls.analytic_plan.id,
})
cls.product_c = cls.env['product.product'].create({
'name': 'product_c with no cost',
'uom_id': cls.env.ref('uom.product_uom_dozen').id,
'lst_price': 200.0,
'property_account_income_id': cls.copy_account(cls.company_data['default_account_revenue']).id,
'property_account_expense_id': cls.copy_account(cls.company_data['default_account_expense']).id,
'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,
})
# Ensure products can be expensed.
(cls.product_a + cls.product_b).write({'can_be_expensed': True})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,150 @@
# -*- 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.exceptions import AccessError, UserError
from odoo.tests import tagged
@tagged('-at_install', 'post_install')
class TestExpensesAccessRights(TestExpenseCommon):
def test_expense_access_rights(self):
''' The expense employee can't be able to create an expense for someone else.'''
expense_employee_2 = self.env['hr.employee'].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,
})
with self.assertRaises(AccessError):
self.env['hr.expense'].with_user(self.expense_user_employee).create({
'name': "Superboy costume washing",
'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,
'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'
# 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+
# 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+
def test_expense_sheet_access_rights_approve(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.env.flush_all()
self.assertRecordValues(expense_sheet, [{'state': 'draft'}])
# The expense employee shouldn't be able to bypass the submit state.
with self.assertRaises(UserError):
expense_sheet.with_user(self.expense_user_employee).state = 'approve'
# 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 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.
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'}])

View file

@ -0,0 +1,224 @@
# -*- 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
@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': [],
}
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,
}])
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"""
user = self.expense_user_employee
company_2 = user.company_ids[1]
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({
'name': 'expense_employee_2',
'company_id': company_2.id,
'user_id': user.id,
'work_email': user.email,
})
message_parsed = {
'message_id': "the-world-is-a-ghetto",
'subject': 'New expense',
'email_from': user.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_parsed)
self.assertRecordValues(expense, [{
'employee_id': company_2_employee.id,
}])
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"""
employee = self.expense_employee
employee.user_id = False
message_parsed = {
'message_id': "the-world-is-a-ghetto",
'subject': 'New expense',
'email_from': employee.work_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_parsed)
self.assertRecordValues(expense, [{
'employee_id': employee.id,
}])
def test_import_expense_from_email_no_product(self):
message_parsed = {
'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_parsed)
self.assertRecordValues(expense, [{
'product_id': False,
'total_amount': 800.0,
'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)\
._parse_expense_subject(subject, currencies)
self.assertEqual(product, exp_product)
self.assertAlmostEqual(amount, exp_amount)
self.assertEqual(description, exp_description)
# Without Multi currency access
assertParsedValues(
"product_a bar $1205.91 electro wizard",
self.company_data['currency'],
"bar electro wizard",
1205.91,
self.product_a,
)
# 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,
self.company_data['currency'],
"foo bar %s royal giant" % self.currency_data['currency'].symbol,
1406.91,
self.env['product.product'],
)
# With Multi currency access
self.expense_user_employee.groups_id |= 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,
)
# 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'],
"chhota bheem",
2510.90,
self.product_a,
)
# 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'],
"foo bar spear goblins",
109.96,
self.env['product.product'],
)
# subject with currency symbol at end
assertParsedValues(
"product_a foo bar 2910.94$ inferno dragon",
self.company_data['currency'] + self.currency_data['currency'],
"foo bar inferno dragon",
2910.94,
self.product_a,
)
# subject with no amount and product
assertParsedValues(
"foo bar mega knight",
self.company_data['currency'] + self.currency_data['currency'],
"foo bar mega knight",
0.0,
self.env['product.product'],
)
# price with a comma
assertParsedValues(
"foo bar 291,56$ mega knight",
self.company_data['currency'] + self.currency_data['currency'],
"foo bar mega knight",
291.56,
self.env['product.product'],
)
# price without decimals
assertParsedValues(
"foo bar 291$ mega knight",
self.company_data['currency'] + self.currency_data['currency'],
"foo bar mega knight",
291.0,
self.env['product.product'],
)
assertParsedValues(
"product_a foo bar 291.5$ mega knight",
self.company_data['currency'] + self.currency_data['currency'],
"foo bar mega knight",
291.5,
self.product_a,
)

View file

@ -0,0 +1,116 @@
# -*- 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

@ -0,0 +1,79 @@
# 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

@ -0,0 +1,313 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, Command
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged
@tagged('-at_install', 'post_install')
class TestExpensesStates(TestExpenseCommon):
@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({
'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({
'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,
})],
})
cls.expense_states_sheets = cls.expense_states_employee_sheet + cls.expense_states_company_sheet
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'},
{'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)
# 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'},
])
# 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'},
{'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)
# 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'},
])
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'},
])
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()
# 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'},
])
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'},
])
# 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'},
])
# 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)}],
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'},
])
# 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()
# 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.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', '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'},
])
# 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'},
])
# 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_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()
# 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.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'},
])
# 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'},
])
# 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'},
])

View file

@ -0,0 +1,7 @@
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")