Odoo18-Base/addons/hr_expense/tests/test_expenses.py
2025-03-10 11:12:23 +07:00

1574 lines
72 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from freezegun import freeze_time
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
from odoo.tests import tagged, Form
from odoo.tools.misc import formatLang, format_date
from odoo import fields, Command
from odoo.exceptions import UserError, ValidationError
@tagged('-at_install', 'post_install')
class TestExpenses(TestExpenseCommon):
def test_expense_sheet_changing_employee(self):
""" Test changing an employee on the expense that is linked with the sheet.
- In case sheet has only one expense linked with it, than changing an employee
on expense should trigger changing an employee on the sheet itself.
- In case sheet has more than one expense linked with it, than changing an employee
on one of the expenses, should cause unlinking the expense from the sheet."""
employee = self.env['hr.employee'].create({
'name': 'Gabriel Iglesias',
})
expense1 = self.env['hr.expense'].create({
'name': 'Dinner with client - Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 350.00,
})
expense2 = self.env['hr.expense'].create({
'name': 'Team building at Huy',
'employee_id': employee.id,
'product_id': self.product_a.id,
'unit_amount': 2500.00,
})
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Expense for Jannette',
'employee_id': self.expense_employee.id,
'expense_line_ids': expense1,
})
expense1.employee_id = employee
self.assertEqual(expense_sheet.employee_id, employee, 'Employee should have changed on the sheet')
expense_sheet.expense_line_ids |= expense2
expense2.employee_id = self.expense_employee.id
self.assertEqual(expense2.sheet_id.id, False, 'Sheet should be unlinked from the expense')
def test_expense_sheet_payment_state(self):
''' Test expense sheet payment states when partially paid, in payment and paid. '''
def get_payment(expense_sheet, amount):
ctx = {'active_model': 'account.move', 'active_ids': expense_sheet.account_move_id.ids}
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,
})
return payment_register._create_payments()
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Expense for John Smith',
'employee_id': self.expense_employee.id,
'accounting_date': '2021-01-01',
'expense_line_ids': [(0, 0, {
'name': 'Car Travel Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 350.00,
})]
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
payment = get_payment(expense_sheet, 100.0)
liquidity_lines1 = payment._seek_for_lines()[0]
self.assertEqual(expense_sheet.payment_state, 'partial', 'payment_state should be partial')
payment = get_payment(expense_sheet, 250.0)
liquidity_lines2 = payment._seek_for_lines()[0]
in_payment_state = expense_sheet.account_move_id._get_invoice_in_payment_state()
self.assertEqual(expense_sheet.payment_state, in_payment_state, 'payment_state should be ' + in_payment_state)
statement_line = self.env['account.bank.statement.line'].create({
'journal_id': self.company_data['default_journal_bank'].id,
'payment_ref': 'pay_ref',
'amount': -350.0,
'partner_id': self.expense_employee.address_home_id.id,
})
# Reconcile without the bank reconciliation widget since the widget is in enterprise.
_st_liquidity_lines, st_suspense_lines, _st_other_lines = statement_line\
.with_context(skip_account_move_synchronization=True)\
._seek_for_lines()
st_suspense_lines.account_id = liquidity_lines1.account_id
(st_suspense_lines + liquidity_lines1 + liquidity_lines2).reconcile()
self.assertEqual(expense_sheet.payment_state, 'paid', 'payment_state should be paid')
def test_expense_sheet_company_payment_state(self):
''' Test expense sheet company payment states'''
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Expense for John Smith',
'employee_id': self.expense_employee.id,
'accounting_date': '2021-01-01',
'expense_line_ids': [(0, 0, {
'name': 'Car Travel Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 350.00,
'payment_mode': 'company_account',
})]
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
self.assertEqual(expense_sheet.payment_state, 'paid', 'payment_state should be paid')
liquidity_line = expense_sheet.account_move_id.payment_id._seek_for_lines()[0]
statement_line = self.env['account.bank.statement.line'].create({
'journal_id': self.company_data['default_journal_bank'].id,
'payment_ref': 'pay_ref',
'amount': -350.0,
'partner_id': self.expense_employee.address_home_id.id,
})
# Reconcile without the bank reconciliation widget since the widget is in enterprise.
_st_liquidity_lines, st_suspense_lines, _st_other_lines = statement_line\
.with_context(skip_account_move_synchronization=True)\
._seek_for_lines()
st_suspense_lines.account_id = liquidity_line.account_id
(st_suspense_lines + liquidity_line).reconcile()
self.assertEqual(expense_sheet.payment_state, 'paid', 'payment_state should be paid')
def test_expense_values(self):
""" Checking accounting move entries and analytic entries when submitting expense """
# The expense employee is able to a create an expense sheet.
# The total should be 1500.0 because:
# - first line: 1000.0 (unit amount), 130.43 (tax). But taxes are included in total thus - 1000
# - second line: (1500.0 (unit amount), 195.652 (tax)) - 65.22 (tax in company currency). total 1500.0 * 1/3 (rate) = 500
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'First Expense for employee',
'employee_id': self.expense_employee.id,
'journal_id': self.company_data['default_journal_purchase'].id,
'expense_line_ids': [
(0, 0, {
# Expense without foreign currency.
'name': 'expense_company_currency',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1000.0,
'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)],
'analytic_distribution': {self.analytic_account_1.id: 100},
'employee_id': self.expense_employee.id,
}),
(0, 0, {
# Expense with foreign currency (rate 1:3).
'name': 'expense_foreign_currency',
'date': '2016-01-01',
'product_id': self.product_c.id, # product with no cost, else not possible to enter amount in different currency
'total_amount': 1500.0,
'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)],
'analytic_distribution': {self.analytic_account_2.id: 100},
'currency_id': self.currency_data['currency'].id,
'employee_id': self.expense_employee.id,
}),
],
})
self.assertRecordValues(expense_sheet, [{'state': 'draft', 'total_amount': 1500.0}])
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
# Check expense sheet journal entry values.
self.assertRecordValues(expense_sheet.account_move_id.line_ids.sorted('balance'), [
# Receivable line (company currency):
{
'debit': 0.0,
'credit': 1500.0,
'amount_currency': -1500.0,
'account_id': self.company_data['default_account_payable'].id,
'product_id': False,
'currency_id': self.company_data['currency'].id,
'tax_line_id': False,
'analytic_distribution': False,
},
# Tax line (foreign currency):
{
'debit': 65.22,
'credit': 0.0,
'amount_currency': 65.22,
'account_id': self.company_data['default_account_tax_purchase'].id,
'product_id': False,
'currency_id': self.company_data['currency'].id,
'tax_line_id': self.company_data['default_tax_purchase'].id,
'analytic_distribution': False,
},
# Tax line (company currency):
{
'debit': 130.43,
'credit': 0.0,
'amount_currency': 130.43,
'account_id': self.company_data['default_account_tax_purchase'].id,
'product_id': False,
'currency_id': self.company_data['currency'].id,
'tax_line_id': self.company_data['default_tax_purchase'].id,
'analytic_distribution': False,
},
# Product line (foreign currency):
{
'debit': 434.78, # 1500 * 1:3 (rate) / 1.15 (incl. tax)
'credit': 0.0,
'amount_currency': 434.78, # untaxed amount
'account_id': self.product_c.property_account_expense_id.id,
'product_id': self.product_c.id,
'currency_id': self.company_data['currency'].id,
'tax_line_id': False,
'analytic_distribution': {str(self.analytic_account_2.id): 100},
},
# Product line (company currency):
{
'debit': 869.57, # 1000 * 1:1 (rate) / 1.15 (incl. tax)
'credit': 0.0,
'amount_currency': 869.57,
'account_id': self.company_data['default_account_expense'].id,
'product_id': self.product_a.id,
'currency_id': self.company_data['currency'].id,
'tax_line_id': False,
'analytic_distribution': {str(self.analytic_account_1.id): 100},
},
])
# Check expense analytic lines.
self.assertRecordValues(expense_sheet.account_move_id.line_ids.analytic_line_ids.sorted('amount'), [
{
'amount': -869.57,
'date': fields.Date.from_string('2016-01-01'),
'account_id': self.analytic_account_1.id,
'currency_id': self.company_data['currency'].id,
},
{
'amount': -434.78,
'date': fields.Date.from_string('2016-01-01'),
'account_id': self.analytic_account_2.id,
'currency_id': self.company_data['currency'].id,
},
])
def test_expense_company_account(self):
""" Create an expense with payment mode 'Company' and post it (it should not fail) """
with Form(self.env['hr.expense']) as expense_form:
expense_form.name = 'Company expense'
expense_form.date = '2022-11-17'
expense_form.total_amount = 1000.0
expense_form.payment_mode = 'company_account'
expense_form.employee_id = self.expense_employee
expense_form.product_id = self.product_a
expense = expense_form.save()
with Form(self.env['hr.expense.sheet']) as expense_sheet_form:
# Use same values that will be used by action_submit_expenses
expense_sheet_form.employee_id = expense.employee_id
expense_sheet_form.name = expense.name
expense_sheet_form.expense_line_ids.add(expense)
expense_sheet = expense_sheet_form.save()
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
def test_account_entry_multi_currency(self):
""" Checking accounting move entries and analytic entries when submitting expense. With
multi-currency. And taxes. """
expense = self.env['hr.expense.sheet'].create({
'name': 'Expense for Dick Tracy',
'employee_id': self.expense_employee.id,
})
tax = self.env['account.tax'].create({
'name': 'Tax Expense 10%',
'amount': 10,
'amount_type': 'percent',
'type_tax_use': 'purchase',
'price_include': True,
})
self.env['hr.expense'].create({
'name': 'Choucroute Saucisse',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'total_amount': 700.0,
'tax_ids': [(6, 0, tax.ids)],
'sheet_id': expense.id,
'analytic_distribution': {self.analytic_account_1.id: 100},
'currency_id': self.currency_data['currency'].id, # rate is 1:2
})
# State should default to draft
self.assertEqual(expense.state, 'draft', 'Expense should be created in Draft state')
# Submitted to Manager
expense.action_submit_sheet()
self.assertEqual(expense.state, 'submit', 'Expense is not in Reported state')
# Approve
expense.approve_expense_sheets()
self.assertEqual(expense.state, 'approve', 'Expense is not in Approved state')
# Create Expense Entries
expense.action_sheet_move_create()
self.assertEqual(expense.state, 'post', 'Expense is not in Waiting Payment state')
# Should get this result [(0.0, 350.0, -700.0), (318.18, 0.0, 636.36), (31.82, 0.0, 63.64)]
analytic_line = expense.account_move_id.line_ids.analytic_line_ids
self.assertEqual(len(analytic_line), 1)
# Expenses paid by the employee are always translated in company currency
self.assertInvoiceValues(expense.account_move_id, [
{
'balance': 318.18, # 700 * 1:2 (rate) / 1.1 (incl. tax)
'amount_currency': 318.18,
'product_id': self.product_c.id,
'price_unit': 350.0,
'price_subtotal': 318.18,
'price_total': 350.0,
'analytic_line_ids': analytic_line.ids,
}, {
'balance': 31.82,
'amount_currency': 31.82,
'product_id': False,
'price_unit': 0.0,
'price_subtotal': 0.0,
'price_total': 0.0,
'analytic_line_ids': [],
}, {
'balance': -350.0,
'amount_currency': -350.0,
'product_id': False,
'price_unit': 0.0,
'price_subtotal': 0.0,
'price_total': 0.0,
'analytic_line_ids': [],
},
], {
'amount_total': 350.0,
})
def test_account_entry_multi_currency_company_account(self):
""" Checking accounting payment entry when payment_mode is 'Company'. With multi-currency."""
expense = self.env['hr.expense'].create({
'name': 'Company expense',
'date': '2022-11-17',
'total_amount': 1000.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': self.currency_data['currency'].id, # rate is 1:2
})
foreign_bank_journal = self.company_data['default_journal_bank'].copy()
foreign_bank_journal.currency_id = self.currency_data['currency'].id
foreign_bank_journal_account = foreign_bank_journal.default_account_id.copy()
foreign_bank_journal_account.currency_id = self.currency_data['currency'].id
foreign_bank_journal.default_account_id = foreign_bank_journal_account.id
expense_sheet = self.env['hr.expense.sheet'].create({
'name': "test_account_entry_multi_currency_own_account",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'bank_journal_id': foreign_bank_journal.id,
'expense_line_ids': [Command.set(expense.ids)],
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
self.assertRecordValues(expense_sheet.account_move_id.payment_id, [{
'currency_id': self.currency_data['currency'].id,
}])
self.assertRecordValues(expense_sheet.account_move_id.line_ids, [
{'currency_id': self.currency_data['currency'].id},
{'currency_id': self.currency_data['currency'].id},
{'currency_id': self.currency_data['currency'].id},
{'currency_id': self.currency_data['currency'].id},
])
def test_account_entry_mixed_multi_currency_company_account(self):
"""
Checking accounting payment entry when payment_mode is 'Company'. With multi-currency.
When several different currencies are found in the expense.
"""
expenses = self.env['hr.expense'].create([{
'name': 'Company expense foreign currency',
'date': '2022-11-17',
'total_amount': 1000.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': self.currency_data['currency'].id, # rate is 1:2
}, {
'name': 'Company expense local currency',
'date': '2022-11-15',
'total_amount': 1000.0,
'payment_mode': 'company_account',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': self.company_data['currency'].id,
}])
foreign_bank_journal = self.company_data['default_journal_bank'].copy()
foreign_bank_journal.currency_id = self.currency_data['currency'].id
foreign_bank_journal_account = foreign_bank_journal.default_account_id.copy()
foreign_bank_journal_account.currency_id = self.currency_data['currency'].id
foreign_bank_journal.default_account_id = foreign_bank_journal_account.id
expense_sheet = self.env['hr.expense.sheet'].create({
'name': "test_account_entry_multi_currency_own_account",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'bank_journal_id': foreign_bank_journal.id,
'expense_line_ids': [Command.set(expenses.ids)],
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
self.assertRecordValues(expense_sheet.account_move_id.payment_id, [{
'currency_id': self.company_data['currency'].id, # Should override to company currency
}])
self.assertRecordValues(expense_sheet.account_move_id.line_ids, [
{'currency_id': self.company_data['currency'].id}, # Should override to company currency
{'currency_id': self.company_data['currency'].id}, # Should override to company currency
{'currency_id': self.company_data['currency'].id}, # Should override to company currency
{'currency_id': self.company_data['currency'].id}, # Should override to company currency
{'currency_id': self.company_data['currency'].id}, # Should override to company currency
{'currency_id': self.company_data['currency'].id}, # Should override to company currency
{'currency_id': self.company_data['currency'].id}, # Should override to company currency
])
def test_account_entry_multi_currency_own_account(self):
""" Checking accounting payment entry when payment_mode is 'Company'. With multi-currency."""
expense = self.env['hr.expense'].create({
'name': 'Company expense',
'date': '2022-11-17',
'payment_mode': 'own_account',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'currency_id': self.currency_data['currency'].id, # rate is 1:2
})
foreign_sale_journal = self.company_data['default_journal_sale'].copy()
foreign_sale_journal.currency_id = self.currency_data['currency'].id
expense_sheet = self.env['hr.expense.sheet'].create({
'name': "test_account_entry_multi_currency_own_account",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'journal_id': foreign_sale_journal.id,
'expense_line_ids': [Command.set(expense.ids)],
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
self.assertRecordValues(expense_sheet.account_move_id, [{
'currency_id': expense_sheet.company_id.currency_id.id,
}])
self.assertRecordValues(expense_sheet.account_move_id.line_ids, [
{'currency_id': expense_sheet.company_id.currency_id.id},
{'currency_id': expense_sheet.company_id.currency_id.id},
{'currency_id': expense_sheet.company_id.currency_id.id},
])
def test_multicurrencies_rounding_consistency(self):
# pylint: disable=bad-whitespace
foreign_currency = self.env['res.currency'].create({
'name': 'Exposure',
'symbol': ' ',
'rounding': 0.01,
'position': 'after',
'currency_unit_label': 'Nothing',
'currency_subunit_label': 'Smaller Nothing',
})
self.env['res.currency.rate'].create({
'name': '2016-01-01',
'rate': 1/0.148431,
'currency_id': foreign_currency.id,
'company_id': self.company_data['company'].id,
})
foreign_sale_journal = self.company_data['default_journal_sale'].copy()
foreign_sale_journal.currency_id = foreign_currency.id
tax = self.env['account.tax'].create({
'name': 'Tax Expense 15%',
'amount': 15,
'amount_type': 'percent',
'type_tax_use': 'purchase',
'price_include': True,
})
taxes = tax + tax.copy()
expense_sheet_own_1_tax = self.env['hr.expense.sheet'].create({
'name': "own expense 1 tax",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'journal_id': foreign_sale_journal.id,
'expense_line_ids': [Command.create({
'name': 'Own expense',
'date': '2022-11-16',
'payment_mode': 'own_account',
'total_amount': 100,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': foreign_currency.id, # rate is 1:0.148431
'tax_ids': [Command.set(tax.ids)],
})],
})
expense_sheet_own_2_tax = self.env['hr.expense.sheet'].create({
'name': "own expense 2 taxes",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'journal_id': foreign_sale_journal.id,
'expense_line_ids': [Command.create({
'name': 'Own expense',
'date': '2022-11-17',
'payment_mode': 'own_account',
'total_amount': 100,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': foreign_currency.id, # rate is 1:0.148431
'tax_ids': [Command.set(taxes.ids)],
})],
})
expense_sheet_company_1_tax = self.env['hr.expense.sheet'].create({
'name': "company expense 1 taxes",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'journal_id': foreign_sale_journal.id,
'expense_line_ids': [Command.create({
'name': 'Company expense',
'date': '2022-11-18',
'payment_mode': 'company_account',
'total_amount': 100,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': foreign_currency.id, # rate is 1:0.148431
'tax_ids': [Command.set(tax.ids)],
})],
})
expense_sheet_company_2_tax = self.env['hr.expense.sheet'].create({
'name': "company expense 2 taxes",
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01',
'journal_id': foreign_sale_journal.id,
'expense_line_ids': [Command.create({
'name': 'Company expense',
'date': '2022-11-19',
'payment_mode': 'company_account',
'total_amount': 100,
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'currency_id': foreign_currency.id, # rate is 1:0.148431
'tax_ids': [Command.set(taxes.ids)],
})],
})
sheets = expense_sheet_own_1_tax + expense_sheet_own_2_tax + expense_sheet_company_1_tax + expense_sheet_company_2_tax
self.assertRecordValues(sheets.expense_line_ids, [
{'untaxed_amount': 86.96, 'total_amount': 100.00, 'total_amount_company': 14.84, 'amount_tax': 13.04, 'amount_tax_company': 1.94},
{'untaxed_amount': 76.92, 'total_amount': 100.00, 'total_amount_company': 14.84, 'amount_tax': 23.08, 'amount_tax_company': 3.42},
{'untaxed_amount': 86.96, 'total_amount': 100.00, 'total_amount_company': 14.84, 'amount_tax': 13.04, 'amount_tax_company': 1.94},
{'untaxed_amount': 76.92, 'total_amount': 100.00, 'total_amount_company': 14.84, 'amount_tax': 23.08, 'amount_tax_company': 3.42},
])
sheets.action_submit_sheet()
sheets.approve_expense_sheets()
sheets.action_sheet_move_create()
self.assertRecordValues(expense_sheet_own_1_tax.account_move_id.line_ids, [
{'balance': 12.90, 'amount_currency': 12.90, 'currency_id': self.company_data['currency'].id},
{'balance': 1.94, 'amount_currency': 1.94, 'currency_id': self.company_data['currency'].id},
{'balance': -14.84, 'amount_currency': -14.84, 'currency_id': self.company_data['currency'].id},
])
self.assertRecordValues(expense_sheet_own_2_tax.account_move_id.line_ids, [
{'balance': 11.42, 'amount_currency': 11.42, 'currency_id': self.company_data['currency'].id},
{'balance': 1.71, 'amount_currency': 1.71, 'currency_id': self.company_data['currency'].id},
{'balance': 1.71, 'amount_currency': 1.71, 'currency_id': self.company_data['currency'].id}, # == 3.42 amount_tax_company
{'balance': -14.84, 'amount_currency': -14.84, 'currency_id': self.company_data['currency'].id},
])
self.assertRecordValues(expense_sheet_company_1_tax.account_move_id.line_ids, [
{'balance': 12.90, 'amount_currency': 86.96, 'currency_id': foreign_currency.id},
{'balance': 1.94, 'amount_currency': 13.04, 'currency_id': foreign_currency.id},
{'balance': -14.84, 'amount_currency': -100.00, 'currency_id': foreign_currency.id},
])
self.assertRecordValues(expense_sheet_company_2_tax.account_move_id.line_ids, [
{'balance': 11.42, 'amount_currency': 76.92, 'currency_id': foreign_currency.id},
{'balance': 1.71, 'amount_currency': 11.54, 'currency_id': foreign_currency.id}, # == 3.42 amount_tax_company & 23.08 amount_tax
{'balance': 1.71, 'amount_currency': 11.54, 'currency_id': foreign_currency.id}, # One cent more in currency due to rounding
{'balance': -14.84, 'amount_currency': -100.00, 'currency_id': foreign_currency.id},
])
def test_expenses_with_tax_and_lockdate(self):
''' Test creating a journal entry for multiple expenses using taxes. A lock date is set in order to trigger
the recomputation of the taxes base amount.
'''
self.env.company.tax_lock_date = '2020-02-01'
expense = self.env['hr.expense.sheet'].create({
'name': 'Expense for John Smith',
'employee_id': self.expense_employee.id,
'accounting_date': '2020-01-01'
})
for i in range(2):
expense_line = self.env['hr.expense'].create({
'name': 'Car Travel Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 350.00,
'tax_ids': [(6, 0, [self.tax_purchase_a.id])],
'sheet_id': expense.id,
'analytic_distribution': {str(self.analytic_account_1.id): 100},
})
expense.action_submit_sheet()
expense.approve_expense_sheets()
# Assert not "Cannot create unbalanced journal entry" error.
expense.action_sheet_move_create()
def test_reconcile_payment(self):
tax = self.env['account.tax'].create({
'name': 'tax abc',
'type_tax_use': 'purchase',
'amount_type': 'percent',
'amount': 15,
'price_include': False,
'include_base_amount': False,
'tax_exigibility': 'on_payment'
})
company = self.env.company.id
tax.cash_basis_transition_account_id = self.env['account.account'].create({
'name': "test",
'code': 999991,
'reconcile': True,
'account_type': 'asset_current',
'company_id': company,
}).id
sheet = self.env['hr.expense.sheet'].create({
'company_id': company,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
(0, 0, {
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'employee_id': self.expense_employee.id,
'tax_ids': tax
}),
(0, 0, {
'name': 'expense_2',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1.0,
'employee_id': self.expense_employee.id,
'tax_ids': tax
}),
],
})
#actions
sheet.action_submit_sheet()
sheet.approve_expense_sheets()
sheet.action_sheet_move_create()
action_data = sheet.action_register_payment()
wizard = Form(self.env['account.payment.register'].with_context(action_data['context'])).save()
action = wizard.action_create_payments()
self.assertEqual(sheet.state, 'done', 'all account.move.line linked to expenses must be reconciled after payment')
move = self.env['account.payment'].browse(action['res_id']).move_id
move.button_cancel()
self.assertEqual(sheet.state, 'done', 'Sheet state must not change when the payment linked to that sheet is canceled')
def test_expense_amount_total_signed_compute(self):
sheet = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
(0, 0, {
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'employee_id': self.expense_employee.id
}),
],
})
#actions
sheet.action_submit_sheet()
sheet.approve_expense_sheets()
sheet.action_sheet_move_create()
action_data = sheet.action_register_payment()
wizard = Form(self.env['account.payment.register'].with_context(action_data['context'])).save()
action = wizard.action_create_payments()
move = self.env['account.payment'].browse(action['res_id']).move_id
self.assertEqual(move.amount_total_signed, 10.0, 'The total amount of the payment move is not correct')
def test_form_defaults_from_product(self):
"""
As soon as you set a product, the expense name, uom, taxes and account are set
according to the product.
"""
# Disable multi-uom
self.env.ref('base.group_user').implied_ids -= self.env.ref('uom.group_uom')
self.expense_user_employee.groups_id -= self.env.ref('uom.group_uom')
# Use the expense employee
Expense = self.env['hr.expense'].with_user(self.expense_user_employee)
# Make sure the multi-uom is correctly disabled for the user creating the expense
self.assertFalse(Expense.env.user.has_group('uom.group_uom'))
# Use a product not using the default uom "Unit(s)"
product = Expense.env.ref('hr_expense.expense_product_mileage')
expense_form = Form(Expense)
expense_form.product_id = product
expense = expense_form.save()
self.assertEqual(expense.name, product.display_name)
self.assertEqual(expense.product_uom_id, product.uom_id)
self.assertEqual(expense.tax_ids, product.supplier_taxes_id)
self.assertEqual(expense.account_id, product._get_product_accounts()['expense'])
def test_expense_account(self):
""" Checking accounting move entries for the accounts set on the expenses """
account_expense_1 = self.env['account.account'].create({
'code': '610010',
'name': 'Expense Account 1'
})
account_expense_2 = self.env['account.account'].create({
'code': '610020',
'name': 'Expense Account 2'
})
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'First Expense for employee',
'employee_id': self.expense_employee.id,
'journal_id': self.company_data['default_journal_purchase'].id,
'accounting_date': '2022-01-20',
'expense_line_ids': [
Command.create({
# Expense on Expense Account 1
'name': 'expense_1',
'date': '2022-01-05',
'account_id': account_expense_1.id,
'product_id': self.product_a.id,
'unit_amount': 115.0,
'employee_id': self.expense_employee.id,
}),
Command.create({
# Expense on Expense Account 2
'name': 'expense_2',
'date': '2022-01-08',
'account_id': account_expense_2.id,
'product_id': self.product_a.id,
'unit_amount': 230.0,
'employee_id': self.expense_employee.id,
}),
],
})
self.assertRecordValues(expense_sheet, [{'state': 'draft', 'total_amount': 345.0}])
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
# Check expense sheet journal entry values.
self.assertRecordValues(expense_sheet.account_move_id.line_ids.sorted('balance'), [
# Receivable lines:
{
'balance': -345.0, # 115 + 230
'account_id': self.company_data['default_account_payable'].id,
},
# Tax lines:
{
'balance': 15.0,
'account_id': self.company_data['default_account_tax_purchase'].id,
},
{
'balance': 30.0,
'account_id': self.company_data['default_account_tax_purchase'].id,
},
# Expense line 1:
{
'balance': 100.0, # 115 / 1.15 (tax incl.)
'account_id': account_expense_1.id,
},
# Expense line 2:
{
'balance': 200.0, # 230 / 1.15 (tax incl.)
'account_id': account_expense_2.id,
},
])
def test_employee_supplier(self):
""" Checking accounting move entries for the supplier set to the employee """
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'First Expense for employee',
'employee_id': self.expense_employee.id,
'journal_id': self.company_data['default_journal_purchase'].id,
'accounting_date': '2022-01-20',
'expense_line_ids': [
Command.create({
# Expense on Expense Account 1
'name': 'expense_1',
'date': '2022-01-05',
'product_id': self.product_a.id,
'unit_amount': 115.0,
'employee_id': self.expense_employee.id,
}),
Command.create({
# Expense on Expense Account 2
'name': 'expense_2',
'date': '2022-01-08',
'product_id': self.product_a.id,
'unit_amount': 230.0,
'employee_id': self.expense_employee.id,
}),
],
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
# Check whether employee is set as supplier on the receipt
self.assertRecordValues(expense_sheet.account_move_id, [{
'partner_id': self.expense_user_employee.partner_id.id,
}])
def test_print_expense_check(self):
"""
Test the check content when printing a check
that comes from an expense
"""
sheet = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
(0, 0, {
'name': 'expense_1',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 10.0,
'employee_id': self.expense_employee.id,
}),
(0, 0, {
'name': 'expense_2',
'date': '2016-01-01',
'product_id': self.product_a.id,
'unit_amount': 1.0,
'employee_id': self.expense_employee.id,
}),
],
})
#actions
sheet.action_submit_sheet()
sheet.approve_expense_sheets()
sheet.action_sheet_move_create()
action_data = sheet.action_register_payment()
payment_method_line = self.env.company.bank_journal_ids.outbound_payment_method_line_ids.filtered(lambda m: m.code == 'check_printing')
with Form(self.env[action_data['res_model']].with_context(action_data['context'])) as wiz_form:
wiz_form.payment_method_line_id = payment_method_line
wizard = wiz_form.save()
action = wizard.action_create_payments()
self.assertEqual(sheet.state, 'done', 'all account.move.line linked to expenses must be reconciled after payment')
payments = self.env[action['res_model']].browse(action['res_id'])
for payment in payments:
pages = payment._check_get_pages()
stub_line = pages[0]['stub_lines'][:1]
self.assertTrue(stub_line)
move = self.env[action_data['context']['active_model']].browse(action_data['context']['active_ids'])
self.assertDictEqual(stub_line[0], {
'due_date': payment.date.strftime("%m/%d/%Y"),
'number': ' - '.join([move.name, move.ref] if move.ref else [move.name]),
'amount_total': formatLang(self.env, move.amount_total, currency_obj=self.env.company.currency_id),
'amount_residual': '-',
'amount_paid': formatLang(self.env, payment.amount_total, currency_obj=self.env.company.currency_id),
'currency': self.env.company.currency_id
})
def test_hr_expense_split(self):
"""
Check Split Expense flow.
"""
expense = self.env['hr.expense'].create({
'name': 'Expense To Test Split - Diego, libre dans sa tête',
'employee_id': self.expense_employee.id,
'product_id': self.product_zero_cost.id,
'total_amount': 100.00,
'tax_ids': [(6, 0, [self.tax_purchase_a.id])],
'analytic_distribution': {self.analytic_account_1.id: 100},
})
split_wizard = expense.action_split_wizard()
wizard = self.env['hr.expense.split.wizard'].browse(split_wizard['res_id'])
# Check default hr.expense.split values
self.assertRecordValues(wizard.expense_split_line_ids, [
{
'name': expense.name,
'wizard_id': wizard.id,
'expense_id': expense.id,
'product_id': expense.product_id.id,
'tax_ids': expense.tax_ids.ids,
'total_amount': expense.total_amount / 2,
'amount_tax': 6.52,
'employee_id': expense.employee_id.id,
'company_id': expense.company_id.id,
'currency_id': expense.currency_id.id,
'analytic_distribution': expense.analytic_distribution,
} for i in range(0, 2)])
self.assertEqual(wizard.split_possible, True)
self.assertEqual(wizard.total_amount, expense.total_amount)
# Grant Analytic Accounting rights, to be able to modify analytic_distribution from the wizard
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
with Form(wizard) as form:
form.expense_split_line_ids.remove(index=0)
self.assertEqual(form.split_possible, False)
# Check removing tax_ids and analytic_distribution
with form.expense_split_line_ids.edit(0) as line:
line.total_amount = 20
line.tax_ids.clear()
line.analytic_distribution = {}
self.assertEqual(line.total_amount, 20)
self.assertEqual(line.amount_tax, 0)
self.assertEqual(form.split_possible, False)
# This line should have the same tax_ids and analytic_distribution as original expense
with form.expense_split_line_ids.new() as line:
line.total_amount = 30
self.assertEqual(line.total_amount, 30)
self.assertEqual(line.amount_tax, 3.91)
self.assertEqual(form.split_possible, False)
self.assertEqual(form.total_amount, 50)
# Check adding tax_ids and setting analytic_distribution
with form.expense_split_line_ids.new() as line:
line.total_amount = 50
line.tax_ids.add(self.tax_purchase_b)
line.analytic_distribution = {self.analytic_account_2.id: 100}
self.assertEqual(line.total_amount, 50)
self.assertAlmostEqual(line.amount_tax, 11.54)
# Check wizard values
self.assertEqual(form.total_amount, 100)
self.assertEqual(form.total_amount_original, 100)
self.assertAlmostEqual(form.total_amount_taxes, 15.45)
self.assertEqual(form.split_possible, True)
wizard.action_split_expense()
# Check that split resulted into expenses with correct values
expenses_after_split = self.env['hr.expense'].search(
[
('name', '=', expense.name)
]
)
self.assertRecordValues(expenses_after_split.sorted('total_amount'), [
{
'name': expense.name,
'employee_id': expense.employee_id.id,
'product_id': expense.product_id.id,
'total_amount': 20.0,
'tax_ids': [],
'amount_tax': 0,
'untaxed_amount': 20,
'analytic_distribution': False,
},
{
'name': expense.name,
'employee_id': expense.employee_id.id,
'product_id': expense.product_id.id,
'total_amount': 30,
'tax_ids': [self.tax_purchase_a.id],
'amount_tax': 3.91,
'untaxed_amount': 26.09,
'analytic_distribution': {str(self.analytic_account_1.id): 100},
},
{
'name': expense.name,
'employee_id': expense.employee_id.id,
'product_id': expense.product_id.id,
'total_amount': 50,
'tax_ids': [self.tax_purchase_a.id, self.tax_purchase_b.id],
'amount_tax': 11.54,
'untaxed_amount': 38.46,
'analytic_distribution': {str(self.analytic_account_2.id): 100},
}
])
def test_analytic_account_deleted(self):
""" Test that an analytic account cannot be deleted if it is used in an expense """
expense = self.env['hr.expense.sheet'].create({
'name': 'Expense for Dick Tracy',
'employee_id': self.expense_employee.id,
})
expense = self.env['hr.expense'].create({
'name': 'Choucroute Saucisse',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 700.00,
'sheet_id': expense.id,
'analytic_distribution': {
self.analytic_account_1.id: 50,
self.analytic_account_2.id: 50,
},
})
with self.assertRaises(UserError):
(self.analytic_account_1 | self.analytic_account_2).unlink()
expense.unlink()
self.analytic_account_1.unlink()
def test_reset_move_to_draft(self):
"""
Test the state of an expense and its report
after resetting the paid move to draft
"""
expense_sheet = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [
(0, 0, {
'name': 'expense_1',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 1000.00,
}),
],
})
expense = expense_sheet.expense_line_ids
self.assertEqual(expense.state, 'draft', 'Expense state must be draft before sheet submission')
self.assertEqual(expense_sheet.state, 'draft', 'Sheet state must be draft before submission')
# Submit report
expense_sheet.action_submit_sheet()
self.assertEqual(expense.state, 'reported', 'Expense state must be reported after sheet submission')
self.assertEqual(expense_sheet.state, 'submit', 'Sheet state must be submit after submission')
# Approve report
expense_sheet.approve_expense_sheets()
self.assertEqual(expense.state, 'approved', 'Expense state must be draft after sheet approval')
self.assertEqual(expense_sheet.state, 'approve', 'Sheet state must be draft after approval')
# Create move
expense_sheet.action_sheet_move_create()
self.assertEqual(expense.state, 'approved', 'Expense state must be draft after posting move')
self.assertEqual(expense_sheet.state, 'post', 'Sheet state must be draft after posting move')
# Pay move
move = expense_sheet.account_move_id
self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({
'amount': 1000.0,
})._create_payments()
self.assertEqual(expense.state, 'done', 'Expense state must be done after payment')
self.assertEqual(expense_sheet.state, 'done', 'Sheet state must be done after payment')
# Reset move to draft
move.button_draft()
self.assertEqual(expense.state, 'approved', 'Expense state must be approved after resetting move to draft')
self.assertEqual(expense_sheet.state, 'post', 'Sheet state must be done after resetting move to draft')
# Post and pay move again
move.action_post()
self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({
'amount': 1000.0,
})._create_payments()
self.assertEqual(expense.state, 'done', 'Expense state must be done after payment')
self.assertEqual(expense_sheet.state, 'done', 'Sheet state must be done after payment')
def test_expense_sheet_due_date(self):
""" Test expense sheet bill due date """
self.expense_employee.user_partner_id.property_supplier_payment_term_id = self.env.ref('account.account_payment_term_30days')
with freeze_time('2021-01-01'):
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Expense for John Smith',
'employee_id': self.expense_employee.id,
'expense_line_ids': [Command.create({
'name': 'Car Travel Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 350.00,
'date': '2021-01-01',
})]
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
move = expense_sheet.account_move_id
expected_date = fields.Date.from_string('2021-01-31')
self.assertEqual(move.invoice_date_due, expected_date, 'Bill due date should follow employee payment terms')
def test_inverse_total_amount(self):
""" Test if the inverse method works correctly """
expense = self.env['hr.expense'].create({
'name': 'Choucroute Saucisse',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'total_amount': 60,
'unit_amount': 0,
'tax_ids': [self.tax_purchase_a.id, self.tax_purchase_b.id],
'analytic_distribution': {
self.analytic_account_1.id: 50,
self.analytic_account_2.id: 50,
},
})
expense.total_amount = 90
self.assertEqual(expense.unit_amount, 90, 'Unit amount should be the same as total amount was written to')
def test_expense_from_attachments(self):
# avoid passing through extraction when installed
if 'hr.expense.extract.words' in self.env:
self.env.company.expense_extract_show_ocr_option_selection = 'no_send'
self.env.user.employee_id = self.expense_employee.id
attachment = self.env['ir.attachment'].create({
'datas': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
'name': 'file.png',
'res_model': 'hr.expense',
})
product = self.env['product.product'].search([('can_be_expensed', '=', True)])
# reproduce the same way we get the product by default
if product:
product = product.filtered(lambda p: p.default_code == "EXP_GEN") or product[0]
product.property_account_expense_id = self.company_data['default_account_payable']
self.env['hr.expense'].create_expense_from_attachments(attachment.id)
expense = self.env['hr.expense'].search([], order='id desc', limit=1)
self.assertEqual(expense.account_id, product.property_account_expense_id, "The expense account should be the default one of the product")
def test_expense_sheet_attachments_sync(self):
"""
Test that the hr.expense.sheet attachments stay in sync with the attachments associated with the expense lines
Syncing should happen when:
- When adding/removing expense_line_ids on a hr.expense.sheet <-> changing sheet_id on an expense
- When deleting an expense that is associated with an hr.expense.sheet
- When adding/removing an attachment of an expense that is associated with an hr.expense.sheet
"""
def assert_attachments_are_synced(sheet, attachments_on_sheet, sheet_has_attachment):
if sheet_has_attachment:
self.assertTrue(bool(attachments_on_sheet), "Attachment that belongs to the hr.expense.sheet only was removed unexpectedly")
self.assertSetEqual(
set(sheet.expense_line_ids.attachment_ids.mapped('checksum')),
set((sheet.attachment_ids - attachments_on_sheet).mapped('checksum')),
"Attachments between expenses and their sheet is not in sync.",
)
for sheet_has_attachment in (False, True):
expense_1, expense_2, expense_3 = self.env['hr.expense'].create([{
'name': 'expense_1',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'total_amount': 1000,
}, {
'name': 'expense_2',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'total_amount': 999,
}, {
'name': 'expense_3',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'total_amount': 998,
}])
self.env['ir.attachment'].create([{
'name': "test_file_1.txt",
'datas': base64.b64encode(b'content'),
'res_id': expense_1.id,
'res_model': 'hr.expense',
}, {
'name': "test_file_2.txt",
'datas': base64.b64encode(b'other content'),
'res_id': expense_2.id,
'res_model': 'hr.expense',
}, {
'name': "test_file_3.txt",
'datas': base64.b64encode(b'different content'),
'res_id': expense_3.id,
'res_model': 'hr.expense',
}])
sheet = self.env['hr.expense.sheet'].create({
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': 'test sheet',
'expense_line_ids': [Command.set([expense_1.id, expense_2.id, expense_3.id])],
})
sheet_attachment = self.env['ir.attachment'].create({
'name': "test_file_4.txt",
'datas': base64.b64encode(b'yet another different content'),
'res_id': sheet.id,
'res_model': 'hr.expense.sheet',
}) if sheet_has_attachment else self.env['ir.attachment']
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
expense_1.attachment_ids.unlink()
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
self.env['ir.attachment'].create({
'name': "test_file_1.txt",
'datas': base64.b64encode(b'content'),
'res_id': expense_1.id,
'res_model': 'hr.expense',
})
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
expense_2.sheet_id = False
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
expense_2.sheet_id = sheet
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
sheet.expense_line_ids = [Command.set([expense_1.id, expense_3.id])]
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
expense_3.unlink()
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
sheet.attachment_ids.filtered(
lambda att: att.checksum in sheet.expense_line_ids.attachment_ids.mapped('checksum')
).unlink()
assert_attachments_are_synced(sheet, sheet_attachment, sheet_has_attachment)
def test_create_report_name(self):
"""
When an expense sheet is created from one or more expense, the report name is generated through the expense name or date.
As the expense sheet is created directly from the hr.expense._get_default_expense_sheet_values method,
we only need to test the method.
"""
expense_with_date_1, expense_with_date_2, expense_without_date = self.env['hr.expense'].create([{
'company_id': self.company_data['company'].id,
'name': f'test expense {i}',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': self.product_a.standard_price,
'date': '2021-01-01',
'quantity': i + 1,
} for i in range(3)])
expense_without_date.date = False
# CASE 1: only one expense with or without date -> expense name
sheet_name = expense_with_date_1._get_default_expense_sheet_values()[0]['name']
self.assertEqual(expense_with_date_1.name, sheet_name, "The report name should be the same as the expense name")
sheet_name = expense_without_date._get_default_expense_sheet_values()[0]['name']
self.assertEqual(expense_without_date.name, sheet_name, "The report name should be the same as the expense name")
# CASE 2: two expenses with the same date -> expense date
expenses = expense_with_date_1 | expense_with_date_2
sheet_name = expenses._get_default_expense_sheet_values()[0]['name']
self.assertEqual(format_date(self.env, expense_with_date_1.date), sheet_name, "The report name should be the same as the expense date")
# CASE 3: two expenses with different dates -> date range
expense_with_date_2.date = '2021-01-02'
sheet_name = expenses._get_default_expense_sheet_values()[0]['name']
self.assertEqual(
f"{format_date(self.env, expense_with_date_1.date)} - {format_date(self.env, expense_with_date_2.date)}",
sheet_name,
"The report name should be the date range of the expenses",
)
# CASE 4: One or more expense doesn't have a date (single sheet) -> No fallback name
expenses |= expense_without_date
sheet_name = expenses._get_default_expense_sheet_values()[0]['name']
self.assertFalse(
sheet_name,
"The report (with the empty expense date) name should be empty as a fallback when several reports are created",
)
expenses.date = False
sheet_name = expenses._get_default_expense_sheet_values()[0]['name']
self.assertFalse(sheet_name, "The report name should be empty as a fallback")
# CASE 5: One or more expense doesn't have a date (multiple sheets) -> Fallback name
expenses |= self.env['hr.expense'].create([{
'company_id': self.company_data['company'].id,
'name': f'test expense by company {i}',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': self.product_a.standard_price,
'payment_mode': 'company_account',
'date': '2021-01-01',
'quantity': i + 1,
} for i in range(3)])
sheet_names = [sheet['name'] for sheet in expenses._get_default_expense_sheet_values()]
self.assertSequenceEqual(
("New Expense Report, paid by employee", format_date(self.env, expenses[-1].date)),
sheet_names,
"The report name should be 'New Expense Report, paid by (employee|company)' as a fallback",
)
def test_expense_product_update(self):
""" Test that the expense line is correctly updated or not when its product price is updated."""
#pylint: disable=bad-whitespace
product = self.env['product.product'].create({
'name': 'product',
'uom_id': self.env.ref('uom.product_uom_unit').id,
'lst_price': 100.0,
'standard_price': 0.0,
'property_account_income_id': self.company_data['default_account_revenue'].id,
'property_account_expense_id': self.company_data['default_account_expense'].id,
'supplier_taxes_id': False,
})
sheet_no_update, sheet_update = sheets = self.env['hr.expense.sheet'].create([{
'company_id': self.env.company.id,
'employee_id': self.expense_employee.id,
'name': name,
'expense_line_ids': [
Command.create({
'name': name,
'date': '2016-01-01',
'product_id': product.id,
'total_amount': 100.0,
'employee_id': self.expense_employee.id
}),
],
} for name in ('test sheet no update', 'test sheet update')])
sheet_no_update.action_submit_sheet() # No update when sheet is submitted
self.assertRecordValues(sheets.expense_line_ids.sorted('name'), [
{'name': 'test sheet no update', 'unit_amount': 100.0, 'quantity': 1, 'total_amount': 100.0},
{'name': 'test sheet update', 'unit_amount': 100.0, 'quantity': 1, 'total_amount': 100.0},
])
product.standard_price = 50.0
self.assertRecordValues(sheets.expense_line_ids.sorted('name'), [
{'name': 'test sheet no update', 'unit_amount': 100.0, 'quantity': 1, 'total_amount': 100.0},
{'name': 'test sheet update', 'unit_amount': 50.0, 'quantity': 1, 'total_amount': 50.0}, # unit_amount is updated
])
sheet_update.expense_line_ids.quantity = 5
self.assertRecordValues(sheets.expense_line_ids.sorted('name'), [
{'name': 'test sheet no update', 'unit_amount': 100.0, 'quantity': 1, 'total_amount': 100.0},
{'name': 'test sheet update', 'unit_amount': 50.0, 'quantity': 5, 'total_amount': 250.0}, # quantity & total are updated
])
product.standard_price = 0.0
self.assertRecordValues(sheets.expense_line_ids.sorted('name'), [
{'name': 'test sheet no update', 'unit_amount': 100.0, 'quantity': 1, 'total_amount': 100.0},
{'name': 'test sheet update', 'unit_amount': 250.0, 'quantity': 1, 'total_amount': 250.0}, # quantity & unit_amount only are updated
])
sheet_update.action_submit_sheet() # This sheet should not be updated any more
product.standard_price = 300.0
self.assertRecordValues(sheets.expense_line_ids.sorted('name'), [
{'name': 'test sheet no update', 'unit_amount': 100.0, 'quantity': 1, 'total_amount': 100.0},
{'name': 'test sheet update', 'unit_amount': 250.0, 'quantity': 1, 'total_amount': 250.0}, # no update
])
def test_expense_mandatory_analytic_plan_product_category(self):
"""
Check that when an analytic plan has a mandatory applicability matching
product category this is correctly triggered
"""
self.env['account.analytic.applicability'].create({
'business_domain': 'expense',
'analytic_plan_id': self.analytic_plan.id,
'applicability': 'mandatory',
'product_categ_id': self.product_a.categ_id.id,
})
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Expense for John Smith',
'employee_id': self.expense_employee.id,
'accounting_date': '2021-01-01',
'expense_line_ids': [Command.create({
'name': 'Car Travel Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'unit_amount': 350.00,
'payment_mode': 'company_account',
})]
})
expense_sheet.action_submit_sheet()
with self.assertRaises(ValidationError, msg="One or more lines require a 100% analytic distribution."):
expense_sheet.with_context(validate_analytic=True).approve_expense_sheets()
expense_sheet.expense_line_ids.analytic_distribution = {self.analytic_account_1.id: 100.00}
expense_sheet.with_context(validate_analytic=True).approve_expense_sheets()
def test_expense_no_stealing_from_employees(self):
"""
Test to check that the company doesn't steal their employee when the commercial_partner_id of the employee partner
is the company
"""
self.expense_employee.user_partner_id.parent_id = self.env.company.partner_id
self.assertEqual(self.env.company.partner_id, self.expense_employee.user_partner_id.commercial_partner_id)
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Company Cash Basis Expense Report',
'employee_id': self.expense_employee.id,
'payment_mode': 'own_account',
'state': 'approve',
'expense_line_ids': [Command.create({
'name': 'Company Cash Basis Expense',
'product_id': self.product_c.id,
'payment_mode': 'own_account',
'total_amount': 20.0,
'employee_id': self.expense_employee.id,
})]
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
move = expense_sheet.account_move_id
self.assertNotEqual(move.commercial_partner_id, self.env.company.partner_id)
self.assertEqual(move.partner_id, self.expense_employee.user_partner_id)
self.assertEqual(move.commercial_partner_id, self.expense_employee.user_partner_id)
def test_expense_bank_account_of_employee_on_entry_and_register_payment(self):
"""
Test that the bank account defined on the employee form is correctly set on the entry and on the register payment
when having multiple bank accounts defined on the partner
"""
self.partner_bank_account_1 = self.env['res.partner.bank'].create({
'acc_number': "987654321",
'partner_id': self.expense_employee.user_partner_id.id,
'acc_type': 'bank',
})
self.partner_bank_account_2 = self.env['res.partner.bank'].create({
'acc_number': "123456789",
'partner_id': self.expense_employee.user_partner_id.id,
'acc_type': 'bank',
})
# Set the second bank account for the employee
self.expense_employee.bank_account_id = self.partner_bank_account_2
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Expense for John Smith',
'employee_id': self.expense_employee.id,
'payment_mode': 'own_account',
'state': 'approve',
'expense_line_ids': [Command.create({
'name': 'Car Travel Expenses',
'employee_id': self.expense_employee.id,
'product_id': self.product_a.id,
'payment_mode': 'own_account',
'unit_amount': 350.00,
})]
})
expense_sheet.action_submit_sheet()
expense_sheet.approve_expense_sheets()
expense_sheet.action_sheet_move_create()
move_bank_acc = expense_sheet.account_move_id.partner_bank_id
self.assertEqual(move_bank_acc, self.partner_bank_account_2)
action_data = expense_sheet.action_register_payment()
with Form(self.env['account.payment.register'].with_context(action_data['context'])) as pay_form:
self.assertEqual(pay_form.partner_bank_id, self.partner_bank_account_2)
def test_expense_by_company_with_caba_tax(self):
"""When using cash basis tax in an expense paid by the company, the transition account should not be used."""
caba_tag = self.env['account.account.tag'].create({
'name': 'Cash Basis Tag Final Account',
'applicability': 'taxes',
})
caba_transition_account = self.env['account.account'].create({
'name': 'Cash Basis Tax Transition Account',
'account_type': 'asset_current',
'code': '131001',
})
caba_tax = self.env['account.tax'].create({
'name': 'Cash Basis Tax',
'tax_exigibility': 'on_payment',
'amount': 15,
'cash_basis_transition_account_id': caba_transition_account.id,
'invoice_repartition_line_ids': [
Command.create({
'factor_percent': 100,
'repartition_type': 'base',
}),
Command.create({
'factor_percent': 100,
'repartition_type': 'tax',
'tag_ids': caba_tag.ids,
}),
]
})
expense_sheet = self.env['hr.expense.sheet'].create({
'name': 'Company Cash Basis Expense Report',
'employee_id': self.expense_employee.id,
'payment_mode': 'company_account',
'state': 'approve',
'expense_line_ids': [Command.create({
'name': 'Company Cash Basis Expense',
'product_id': self.product_c.id,
'payment_mode': 'company_account',
'total_amount': 20.0,
'employee_id': self.expense_employee.id,
'tax_ids': [Command.set(caba_tax.ids)],
})]
})
moves = expense_sheet.action_sheet_move_create()
tax_lines = moves.line_ids.filtered(lambda line: line.tax_line_id == caba_tax)
self.assertNotEqual(tax_lines.account_id, caba_transition_account, "The tax should not be on the transition account")
self.assertEqual(tax_lines.tax_tag_ids, caba_tag, "The tax should still retrieve its tags")
def test_expense_set_total_amount_to_0(self):
"""Checks that amount fields are correctly updating when setting total_amount to 0"""
expense = self.env['hr.expense'].create({
'name': 'Expense with amount',
'employee_id': self.expense_employee.id,
'product_id': self.product_c.id,
'total_amount': 100.0,
'tax_ids': self.tax_purchase_a.ids,
})
expense.total_amount = 0.0
self.assertTrue(expense.currency_id.is_zero(expense.amount_tax))
self.assertTrue(expense.company_currency_id.is_zero(expense.total_amount_company))
def test_expense_set_quantity_to_0(self):
"""Checks that amount fields except for unit_amount are correctly updating when setting quantity to 0"""
expense = self.env['hr.expense'].create({
'name': 'Expense with amount',
'employee_id': self.expense_employee.id,
'product_id': self.product_b.id,
'quantity': 10
})
expense.quantity = 0
self.assertTrue(expense.currency_id.is_zero(expense.total_amount))
self.assertEqual(expense.company_currency_id.compare_amounts(expense.unit_amount, self.product_b.standard_price), 0)