mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-23 14:52:06 +02:00
1114 lines
53 KiB
Python
1114 lines
53 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import date
|
|
|
|
from freezegun import freeze_time
|
|
|
|
from odoo import Command, fields
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.tests import Form, tagged
|
|
|
|
from odoo.addons.hr_expense.tests.common import TestExpenseCommon
|
|
|
|
|
|
@tagged('-at_install', 'post_install')
|
|
class TestExpenses(TestExpenseCommon):
|
|
#############################################
|
|
# Test Expense flows
|
|
#############################################
|
|
def test_expense_main_flow(self):
|
|
"""
|
|
Test the main flows of expense
|
|
This includes:
|
|
- Approval flows for expense paid by company and employee up to reconciliation
|
|
- price_unit, total_amount_currency and quantity computation
|
|
- Split payments into one payment per expense when paid by company
|
|
- Override account on expense
|
|
- Payment states and payment terms
|
|
- Unlinking payments reverts to approved state
|
|
- Cannot delete an analytic account if linked to an expense
|
|
"""
|
|
|
|
self.expense_employee.user_partner_id.property_supplier_payment_term_id = self.env.ref('account.account_payment_term_30days')
|
|
expenses_by_employee = self.create_expenses([
|
|
{
|
|
'name': 'Employee PA 2*800 + 15%', # Taxes are included
|
|
'employee_id': self.expense_employee.id,
|
|
'account_id': self.expense_account.id, # Test with a specific account override
|
|
'product_id': self.product_a.id,
|
|
'quantity': 2,
|
|
'payment_mode': 'own_account',
|
|
'company_id': self.company_data['company'].id,
|
|
'date': '2021-10-14',
|
|
'analytic_distribution': {self.analytic_account_1.id: 100},
|
|
}, {
|
|
'name': 'Employee PB 160 + 2*15%', # Taxes are included
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_b.id,
|
|
'payment_mode': 'own_account',
|
|
'company_id': self.company_data['company'].id,
|
|
'date': '2021-10-13',
|
|
'analytic_distribution': {self.analytic_account_2.id: 100},
|
|
},
|
|
])
|
|
expenses_by_company = self.create_expenses([
|
|
{
|
|
'name': 'Company PC 1000 + 15%', # Taxes are included
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount_currency': 1000.00,
|
|
'date': '2021-10-12',
|
|
'payment_mode': 'company_account',
|
|
'company_id': self.company_data['company'].id,
|
|
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
|
}, {
|
|
'name': 'Company PB 160 + 2*15%', # Taxes are included
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_b.id,
|
|
'payment_mode': 'company_account',
|
|
'company_id': self.company_data['company'].id,
|
|
'date': '2021-10-11',
|
|
},
|
|
])
|
|
all_expenses = expenses_by_employee | expenses_by_company
|
|
|
|
# Checking expense values at creation
|
|
self.assertRecordValues(all_expenses, [
|
|
{'name': 'Employee PA 2*800 + 15%', 'total_amount_currency': 1600.00, 'untaxed_amount_currency': 1391.30, 'price_unit': 800.00, 'tax_amount_currency': 208.70, 'state': 'draft'},
|
|
{'name': 'Employee PB 160 + 2*15%', 'total_amount_currency': 160.00, 'untaxed_amount_currency': 123.08, 'price_unit': 160.00, 'tax_amount_currency': 36.92, 'state': 'draft'},
|
|
{'name': 'Company PC 1000 + 15%', 'total_amount_currency': 1000.00, 'untaxed_amount_currency': 869.57, 'price_unit': 1000.00, 'tax_amount_currency': 130.43, 'state': 'draft'},
|
|
{'name': 'Company PB 160 + 2*15%', 'total_amount_currency': 160.00, 'untaxed_amount_currency': 123.08, 'price_unit': 160.00, 'tax_amount_currency': 36.92, 'state': 'draft'},
|
|
])
|
|
|
|
# Submitting properly change states
|
|
all_expenses.action_submit()
|
|
self.assertRecordValues(all_expenses, [
|
|
{'state': 'submitted'},
|
|
{'state': 'submitted'},
|
|
{'state': 'submitted'},
|
|
{'state': 'submitted'},
|
|
])
|
|
|
|
# Approving properly change states & create moves & payments
|
|
all_expenses.action_approve()
|
|
self.assertRecordValues(all_expenses, [
|
|
{'state': 'approved', 'account_move_id': False},
|
|
{'state': 'approved', 'account_move_id': False},
|
|
{'state': 'approved', 'account_move_id': False},
|
|
{'state': 'approved', 'account_move_id': False},
|
|
])
|
|
# Post a payment for 'company_account' (and its move(s)) and a receipt for 'own_account'
|
|
expenses_by_company.action_post()
|
|
self.post_expenses_with_wizard(expenses_by_employee[0], date=date(2021, 10, 10))
|
|
self.post_expenses_with_wizard(expenses_by_employee[1], date=date(2021, 10, 31))
|
|
self.assertRecordValues(all_expenses, [
|
|
# As the payment is not done yet those are still in "posted"
|
|
{'payment_mode': 'own_account', 'state': 'posted'},
|
|
{'payment_mode': 'own_account', 'state': 'posted'},
|
|
# Expenses paid by company don't use accounting date since they are already paid and posted directly
|
|
{'payment_mode': 'company_account', 'state': 'paid'},
|
|
{'payment_mode': 'company_account', 'state': 'paid'},
|
|
])
|
|
|
|
employee_partner_id = self.expense_user_employee.partner_id.id
|
|
self.assertRecordValues(expenses_by_employee.account_move_id, [
|
|
{'amount_total': 1600.00, 'ref': 'Employee PA 2*800 + 15%', 'state': 'posted', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10), 'partner_id': employee_partner_id},
|
|
{'amount_total': 160.00, 'ref': 'Employee PB 160 + 2*15%', 'state': 'posted', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31), 'partner_id': employee_partner_id},
|
|
])
|
|
|
|
self.assertRecordValues(expenses_by_company.account_move_id, [
|
|
{'amount_total': 1000.00, 'ref': 'Company PC 1000 + 15%', 'state': 'posted', 'date': date(2021, 10, 12), 'invoice_date': False, 'partner_id': False},
|
|
{'amount_total': 160.00, 'ref': 'Company PB 160 + 2*15%', 'state': 'posted', 'date': date(2021, 10, 11), 'invoice_date': False, 'partner_id': False},
|
|
])
|
|
|
|
tax_account_id = self.company_data['default_account_tax_purchase'].id
|
|
default_account_payable_id = self.company_data['default_account_payable'].id
|
|
product_b_account_id = self.product_b.property_account_expense_id.id
|
|
product_c_account_id = self.product_c.property_account_expense_id.id
|
|
company_payment_account_id = self.outbound_payment_method_line.payment_account_id.id
|
|
# One payment per expense
|
|
self.assertRecordValues(all_expenses.account_move_id.line_ids.sorted(lambda line: (line.move_id, line)), [
|
|
# own_account expense 1 move
|
|
{'balance': 1391.30, 'account_id': self.expense_account.id, 'name': 'expense_employee: Employee PA 2*800 + 15%', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10)},
|
|
{'balance': 208.70, 'account_id': tax_account_id, 'name': '15%', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10)},
|
|
{'balance': -1600.00, 'account_id': default_account_payable_id, 'name': False, 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10)},
|
|
|
|
# own_account expense 2 move
|
|
{'balance': 123.08, 'account_id': product_b_account_id, 'name': 'expense_employee: Employee PB 160 + 2*15%', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},
|
|
{'balance': 18.46, 'account_id': tax_account_id, 'name': '15%', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},
|
|
{'balance': 18.46, 'account_id': tax_account_id, 'name': '15% (copy)', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},
|
|
{'balance': -160.00, 'account_id': default_account_payable_id, 'name': False, 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},
|
|
|
|
# company_account expense 1 move
|
|
{'balance': 869.57, 'account_id': product_c_account_id, 'name': 'expense_employee: Company PC 1000 + 15%', 'date': date(2021, 10, 12), 'invoice_date': False},
|
|
{'balance': 130.43, 'account_id': tax_account_id, 'name': '15%', 'date': date(2021, 10, 12), 'invoice_date': False},
|
|
{'balance': -1000.00, 'account_id': company_payment_account_id, 'name': 'expense_employee: Company PC 1000 + 15%', 'date': date(2021, 10, 12), 'invoice_date': False},
|
|
|
|
# company_account expense 2 move
|
|
{'balance': 123.08, 'account_id': product_b_account_id, 'name': 'expense_employee: Company PB 160 + 2*15%', 'date': date(2021, 10, 11), 'invoice_date': False},
|
|
{'balance': 18.46, 'account_id': tax_account_id, 'name': '15%', 'date': date(2021, 10, 11), 'invoice_date': False},
|
|
{'balance': 18.46, 'account_id': tax_account_id, 'name': '15% (copy)', 'date': date(2021, 10, 11), 'invoice_date': False},
|
|
{'balance': -160.00, 'account_id': company_payment_account_id, 'name': 'expense_employee: Company PB 160 + 2*15%', 'date': date(2021, 10, 11), 'invoice_date': False},
|
|
|
|
])
|
|
|
|
# Check lines partners:
|
|
self.assertRecordValues(expenses_by_employee.account_move_id.line_ids, [
|
|
# If the test fails, it is probably because the partner is the company's partner instead of the employee's one
|
|
{'partner_id': employee_partner_id},
|
|
{'partner_id': employee_partner_id},
|
|
{'partner_id': employee_partner_id},
|
|
{'partner_id': employee_partner_id},
|
|
{'partner_id': employee_partner_id},
|
|
{'partner_id': employee_partner_id},
|
|
{'partner_id': employee_partner_id},
|
|
])
|
|
|
|
in_payment_state = expenses_by_employee.account_move_id._get_invoice_in_payment_state()
|
|
first_expense_by_employee = expenses_by_employee[0]
|
|
first_expense_by_company = expenses_by_company[0]
|
|
|
|
# Own_account partial payment
|
|
payment_1 = self.get_new_payment(first_expense_by_employee, 1000.0)
|
|
liquidity_lines1 = payment_1._seek_for_lines()[0]
|
|
self.assertEqual(first_expense_by_employee.state, in_payment_state)
|
|
|
|
# own_account remaining payment
|
|
payment_2 = self.get_new_payment(first_expense_by_employee, 600.0)
|
|
liquidity_lines2 = payment_2._seek_for_lines()[0]
|
|
self.assertEqual(first_expense_by_employee.state, in_payment_state)
|
|
|
|
# Reconciling own_account
|
|
statement_line = self.env['account.bank.statement.line'].create({
|
|
'journal_id': self.company_data['default_journal_bank'].id,
|
|
'payment_ref': 'pay_ref',
|
|
'amount': -1600.0,
|
|
'partner_id': self.expense_employee.work_contact_id.id,
|
|
})
|
|
|
|
# Reconcile without the bank reconciliation widget since the widget is in enterprise.
|
|
_trash, st_suspense_lines, _trash = 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(first_expense_by_employee.state, 'paid')
|
|
|
|
# Trying to delete analytic accounts should be forbidden if linked to an expense
|
|
with self.assertRaises(UserError):
|
|
(self.analytic_account_1 | self.analytic_account_2).unlink()
|
|
|
|
# Unlinking moves
|
|
(payment_1 | payment_2).action_draft()
|
|
(payment_1 | payment_2).move_id.line_ids.remove_move_reconcile()
|
|
self.assertEqual(first_expense_by_employee.state, 'posted')
|
|
expenses_by_employee.account_move_id.button_draft()
|
|
expenses_by_employee.account_move_id.unlink()
|
|
self.assertFalse(expenses_by_employee.account_move_id)
|
|
|
|
first_expense_by_company.account_move_id.origin_payment_id.unlink()
|
|
self.assertFalse(first_expense_by_company.account_move_id)
|
|
|
|
self.assertRecordValues(first_expense_by_employee | first_expense_by_company, [
|
|
{'payment_mode': 'own_account', 'state': 'approved'},
|
|
{'payment_mode': 'company_account', 'state': 'approved'},
|
|
])
|
|
|
|
first_expense_by_employee.action_reset()
|
|
self.assertEqual(first_expense_by_employee.state, 'draft')
|
|
first_expense_by_employee.unlink()
|
|
# Only possible if no expense linked to the account
|
|
self.analytic_account_1.unlink()
|
|
|
|
def test_expense_split_flow(self):
|
|
""" Check Split Expense flow. """
|
|
# Grant Analytic Accounting rights, to be able to modify analytic_distribution from the wizard
|
|
self.env.user.group_ids += self.env.ref('analytic.group_analytic_accounting')
|
|
|
|
expense = self.create_expenses({
|
|
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
|
'analytic_distribution': {self.analytic_account_1.id: 100}
|
|
})
|
|
|
|
wizard = self.env['hr.expense.split.wizard'].browse(expense.action_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_currency': expense.total_amount_currency / 2,
|
|
'tax_amount_currency': 65.22,
|
|
'employee_id': expense.employee_id.id,
|
|
'company_id': expense.company_id.id,
|
|
'currency_id': expense.currency_id.id,
|
|
'analytic_distribution': expense.analytic_distribution,
|
|
}] * 2)
|
|
self.assertRecordValues(wizard, [{'split_possible': True, 'total_amount_currency': expense.total_amount_currency}])
|
|
|
|
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_currency = 200.00
|
|
line.tax_ids.clear()
|
|
line.analytic_distribution = {}
|
|
self.assertEqual(line.total_amount_currency, 200.00)
|
|
self.assertEqual(line.tax_amount_currency, 0.00)
|
|
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_currency = 300.00
|
|
self.assertEqual(line.total_amount_currency, 300.00)
|
|
self.assertEqual(line.tax_amount_currency, 39.13)
|
|
self.assertDictEqual(line.analytic_distribution, expense.analytic_distribution)
|
|
self.assertEqual(form.split_possible, False)
|
|
self.assertEqual(form.total_amount_currency, 500.00)
|
|
|
|
# Check adding tax_ids and setting analytic_distribution
|
|
with form.expense_split_line_ids.new() as line:
|
|
line.total_amount_currency = 500.00
|
|
line.tax_ids.add(self.tax_purchase_b)
|
|
line.analytic_distribution = {self.analytic_account_2.id: 100}
|
|
self.assertEqual(line.total_amount_currency, 500.00)
|
|
self.assertEqual(line.tax_amount_currency, 115.38)
|
|
|
|
# Check wizard values
|
|
self.assertRecordValues(wizard, [
|
|
{'total_amount_currency': 1000.00, 'total_amount_currency_original': 1000.00, 'tax_amount_currency': 154.51, '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_currency'), [
|
|
{
|
|
'name': expense.name,
|
|
'employee_id': expense.employee_id.id,
|
|
'product_id': expense.product_id.id,
|
|
'total_amount_currency': 200.00,
|
|
'tax_ids': [],
|
|
'tax_amount_currency': 0.00,
|
|
'untaxed_amount_currency': 200.00,
|
|
'analytic_distribution': False,
|
|
'split_expense_origin_id': expense.id,
|
|
}, {
|
|
'name': expense.name,
|
|
'employee_id': expense.employee_id.id,
|
|
'product_id': expense.product_id.id,
|
|
'total_amount_currency': 300.00,
|
|
'tax_ids': [self.tax_purchase_a.id],
|
|
'tax_amount_currency': 39.13,
|
|
'untaxed_amount_currency': 260.87,
|
|
'analytic_distribution': {str(self.analytic_account_1.id): 100},
|
|
'split_expense_origin_id': expense.id,
|
|
}, {
|
|
'name': expense.name,
|
|
'employee_id': expense.employee_id.id,
|
|
'product_id': expense.product_id.id,
|
|
'total_amount_currency': 500.00,
|
|
'tax_ids': [self.tax_purchase_a.id, self.tax_purchase_b.id],
|
|
'tax_amount_currency': 115.38,
|
|
'untaxed_amount_currency': 384.62,
|
|
'analytic_distribution': {str(self.analytic_account_2.id): 100},
|
|
'split_expense_origin_id': expense.id,
|
|
}
|
|
])
|
|
|
|
#############################################
|
|
# Test Multi-currency
|
|
#############################################
|
|
|
|
def test_expense_multi_currencies(self):
|
|
"""
|
|
Checks that the currency rate is recomputed properly when the total in company currency is set to a new value
|
|
"""
|
|
foreign_currency_1 = self.other_currency
|
|
foreign_currency_2 = self.setup_other_currency('GBP', rounding=0.01, rates=([('2016-01-01', 1 / 1.52)]))
|
|
foreign_sale_journal = self.company_data['default_journal_sale'].copy()
|
|
foreign_sale_journal.currency_id = foreign_currency_2.id
|
|
|
|
foreign_expense_1, foreign_expense_2, foreign_expense_3 = self.create_expenses([{
|
|
'name': 'foreign_expense_1',
|
|
'payment_mode': 'company_account',
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount_currency': 1000.00,
|
|
'date': self.frozen_today,
|
|
'company_id': self.company_data['company'].id,
|
|
'currency_id': foreign_currency_1.id, # rate is 1:2
|
|
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
|
},
|
|
{
|
|
'name': 'foreign_expense_2',
|
|
'payment_mode': 'company_account',
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount_currency': 1000.00,
|
|
'date': self.frozen_today,
|
|
'company_id': self.company_data['company'].id,
|
|
'currency_id': foreign_currency_2.id, # rate is 1:1.52
|
|
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
|
},
|
|
{
|
|
'name': 'foreign_expense_3',
|
|
'payment_mode': 'company_account',
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount_currency': 1000.00,
|
|
'total_amount': 3000.00,
|
|
'date': self.frozen_today,
|
|
'company_id': self.company_data['company'].id,
|
|
'currency_id': foreign_currency_2.id, # default rate is 1:1.52, should be overridden to 1:3
|
|
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
|
},
|
|
]).sorted('name')
|
|
all_expenses = foreign_expense_1 | foreign_expense_2 | foreign_expense_3
|
|
self.assertRecordValues(all_expenses, [
|
|
{'total_amount': 500.00, 'total_amount_currency': 1000.00, 'currency_rate': 0.50},
|
|
{'total_amount': 1520.00, 'total_amount_currency': 1000.00, 'currency_rate': 1.52},
|
|
{'total_amount': 3000.00, 'total_amount_currency': 1000.00, 'currency_rate': 3.00},
|
|
])
|
|
|
|
# Manually changing rate on the two first expenses after creation to check they recompute properly
|
|
# Back-end override
|
|
foreign_expense_1.total_amount = 1000.00
|
|
self.assertRecordValues(foreign_expense_1, [
|
|
{'total_amount': 1000.00, 'total_amount_currency': 1000.00, 'currency_rate': 1.0},
|
|
])
|
|
|
|
# Front-end override
|
|
with Form(foreign_expense_2) as expense_form:
|
|
expense_form.total_amount = 2000.00
|
|
self.assertRecordValues(foreign_expense_2, [
|
|
{'total_amount': 2000.00, 'total_amount_currency': 1000.00, 'currency_rate': 2.0},
|
|
])
|
|
|
|
# Move creation should not touch the rates anymore
|
|
all_expenses.action_submit()
|
|
all_expenses._do_approve() # Skip duplicate wizard
|
|
self.post_expenses_with_wizard(all_expenses, journal=foreign_sale_journal)
|
|
self.assertRecordValues(all_expenses.account_move_id.sorted('id'), [
|
|
{'amount_total_in_currency_signed': 1000.00, 'amount_total_signed': 1000.00, 'currency_id': foreign_currency_1.id},
|
|
{'amount_total_in_currency_signed': 1000.00, 'amount_total_signed': 2000.00, 'currency_id': foreign_currency_2.id},
|
|
{'amount_total_in_currency_signed': 1000.00, 'amount_total_signed': 3000.00, 'currency_id': foreign_currency_2.id},
|
|
])
|
|
self.assertRecordValues(all_expenses.account_move_id.origin_payment_id.sorted('id'), [
|
|
{'amount': 1000.00, 'payment_type': 'outbound', 'currency_id': foreign_currency_1.id},
|
|
{'amount': 1000.00, 'payment_type': 'outbound', 'currency_id': foreign_currency_2.id},
|
|
{'amount': 1000.00, 'payment_type': 'outbound', 'currency_id': foreign_currency_2.id},
|
|
])
|
|
|
|
#############################################
|
|
# Test Corner Cases
|
|
#############################################
|
|
|
|
def test_expense_company_dates(self):
|
|
expenses = self.create_expenses([
|
|
{
|
|
'name': 'Car Travel Expenses',
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount': 350.00,
|
|
'payment_mode': 'company_account',
|
|
'date': '2024-01-01',
|
|
},
|
|
{
|
|
'name': 'Lunch expense',
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount': 90.00,
|
|
'payment_mode': 'company_account',
|
|
'date': '2024-01-12',
|
|
},
|
|
]).sorted() # By date desc
|
|
|
|
expenses.action_submit()
|
|
expenses.action_approve()
|
|
expenses.action_post()
|
|
|
|
move_twelve_january, move_first_january = expenses.account_move_id.sorted() # By date desc
|
|
|
|
self.assertEqual(
|
|
move_twelve_january.date,
|
|
fields.Date.to_date('2024-01-12'),
|
|
'move date should be the same as the expense date'
|
|
)
|
|
self.assertEqual(
|
|
move_first_january.date,
|
|
fields.Date.to_date('2024-01-01'),
|
|
'move date should be the same as the expense date'
|
|
)
|
|
self.assertTrue(90 == move_twelve_january.amount_total == move_twelve_january.origin_payment_id.amount)
|
|
self.assertTrue(350 == move_first_january.amount_total == move_first_january.origin_payment_id.amount)
|
|
self.assertRecordValues(expenses, [
|
|
{'date': fields.Date.from_string('2024-01-12'), 'total_amount': 90.00, 'state': 'paid'},
|
|
{'date': fields.Date.from_string('2024-01-01'),'total_amount': 350.00, 'state': 'paid'},
|
|
])
|
|
|
|
def test_corner_case_defaults_values_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.group_ids -= 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.filtered(lambda t: t.company_id == expense.company_id))
|
|
self.assertEqual(expense.account_id, product._get_product_accounts()['expense'])
|
|
|
|
def test_attachments_in_move_from_own_expense(self):
|
|
""" Checks that journal entries created form expense reports paid by employee have a copy of the attachments in the expense. """
|
|
expense = self.create_expenses({'name': 'Employee expense'})
|
|
expense_2 = self.create_expenses({'name': 'Employee expense 2'})
|
|
attachment = self.env['ir.attachment'].create({
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file1.png',
|
|
'res_model': 'hr.expense',
|
|
'res_id': expense.id,
|
|
})
|
|
attachment_2 = self.env['ir.attachment'].create({
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file2.png',
|
|
'res_model': 'hr.expense',
|
|
'res_id': expense_2.id,
|
|
})
|
|
|
|
expense.message_main_attachment_id = attachment
|
|
expense_2.message_main_attachment_id = attachment_2
|
|
expenses = expense | expense_2
|
|
|
|
expenses.action_submit()
|
|
expenses._do_approve() # Skip duplicate wizard
|
|
self.post_expenses_with_wizard(expenses)
|
|
|
|
self.assertRecordValues(expenses.account_move_id.attachment_ids.sorted('name'), [
|
|
{
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file1.png',
|
|
'res_model': 'account.move',
|
|
'res_id': expense.account_move_id.id,
|
|
},
|
|
{
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file2.png',
|
|
'res_model': 'account.move',
|
|
'res_id': expense_2.account_move_id.id,
|
|
}
|
|
])
|
|
|
|
def test_attachments_in_move_from_company_expense(self):
|
|
""" Checks that journal entries created form expense reports paid by company have a copy of the attachments in the expense. """
|
|
expense = self.create_expenses({
|
|
'name': 'Company expense',
|
|
'payment_mode': 'company_account',
|
|
})
|
|
expense_2 = self.create_expenses({
|
|
'name': 'Company expense 2',
|
|
'payment_mode': 'company_account',
|
|
})
|
|
attachment = self.env['ir.attachment'].create({
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file1.png',
|
|
'res_model': 'hr.expense',
|
|
'res_id': expense.id,
|
|
})
|
|
attachment_2 = self.env['ir.attachment'].create({
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file2.png',
|
|
'res_model': 'hr.expense',
|
|
'res_id': expense_2.id,
|
|
})
|
|
|
|
expense.message_main_attachment_id = attachment
|
|
expense_2.message_main_attachment_id = attachment_2
|
|
expenses = expense | expense_2
|
|
|
|
expenses.action_submit()
|
|
expenses._do_approve() # Skip duplicate wizard
|
|
expenses.action_post()
|
|
|
|
expense_move = expense.account_move_id
|
|
expense_2_move = expense_2.account_move_id
|
|
self.assertRecordValues(expense_move.attachment_ids, [{
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file1.png',
|
|
'res_model': 'account.move',
|
|
'res_id': expense_move.id
|
|
}])
|
|
|
|
self.assertRecordValues(expense_2_move.attachment_ids, [{
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': 'file2.png',
|
|
'res_model': 'account.move',
|
|
'res_id': expense_2_move.id
|
|
}])
|
|
|
|
def test_multiple_attachments_in_move_from_company_expense(self):
|
|
""" Checks that all attachments from expense are copied to their journal entries. """
|
|
expense = self.create_expenses({
|
|
'name': 'Company expense',
|
|
'payment_mode': 'company_account',
|
|
})
|
|
self.env['ir.attachment'].create([{
|
|
'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
|
|
'name': f'file{i}.png',
|
|
'res_model': 'hr.expense',
|
|
'res_id': expense.id,
|
|
} for i in range(1, 3)])
|
|
|
|
expense.action_submit()
|
|
expense._do_approve() # Skip duplicate wizard
|
|
expense.action_post()
|
|
|
|
self.assertEqual(len(expense.account_move_id.attachment_ids), 2)
|
|
|
|
def test_expense_payment_method(self):
|
|
default_payment_method_line = self.company_data['default_journal_bank'].outbound_payment_method_line_ids[0]
|
|
check_method = self.env['account.payment.method'].sudo().create({
|
|
'name': 'Print checks',
|
|
'code': 'check_printing_expense_test',
|
|
'payment_type': 'outbound',
|
|
})
|
|
new_payment_method_line = self.env['account.payment.method.line'].create({
|
|
'name': 'Check',
|
|
'payment_method_id': check_method.id,
|
|
'journal_id': self.company_data['default_journal_bank'].id,
|
|
'payment_account_id': self.inbound_payment_method_line.payment_account_id.id,
|
|
})
|
|
|
|
expense = self.create_expenses({
|
|
'payment_method_line_id': default_payment_method_line.id,
|
|
'payment_mode': 'company_account',
|
|
})
|
|
|
|
self.assertRecordValues(expense, [{'payment_method_line_id': default_payment_method_line.id}])
|
|
expense.payment_method_line_id = new_payment_method_line
|
|
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
expense.action_post()
|
|
self.assertRecordValues(expense.account_move_id.origin_payment_id, [{'payment_method_line_id': new_payment_method_line.id}])
|
|
|
|
@freeze_time('2024-01-01')
|
|
def test_expense_vendor(self):
|
|
""" This test will do a basic flow when a vendor is set on the expense """
|
|
vendor_a = self.env['res.partner'].create({'name': 'Ruben'})
|
|
expense = self.create_expenses({
|
|
'payment_mode': 'company_account',
|
|
'vendor_id': vendor_a.id,
|
|
})
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
expense.action_post()
|
|
|
|
self.assertEqual(vendor_a.id, expense.account_move_id.line_ids.partner_id.id)
|
|
|
|
def test_payment_edit_fields(self):
|
|
""" Test that some payment fields cannot be modified once linked with an expense """
|
|
expense = self.create_expenses({
|
|
'payment_mode': 'company_account',
|
|
'total_amount_currency': 1000.00,
|
|
})
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
expense.action_post()
|
|
payment = expense.account_move_id.origin_payment_id
|
|
|
|
with self.assertRaises(UserError, msg="Cannot edit payment amount after linking to an expense"):
|
|
payment.write({'amount': 500})
|
|
|
|
payment.write({'is_sent': True})
|
|
|
|
def test_corner_case_expense_submitted_cannot_be_zero(self):
|
|
"""
|
|
Test that the expenses are not submitted if the total amount is 0.0 nor able to be edited that way
|
|
unless unlinking it from the expense.
|
|
"""
|
|
expense = self.create_expenses({'total_amount': 0.0, 'total_amount_currency': 0.0})
|
|
|
|
# CASE 1: FORBIDS Trying to submit an expense with a total_amount(_currency) of 0.0
|
|
with self.assertRaises(UserError):
|
|
expense.action_submit()
|
|
|
|
# CASE 2: FORBIDS Trying to change the total_amount(_currency) to 0.0 when the expense is submitted to the manager
|
|
expense.total_amount_currency = 1000
|
|
expense.action_submit()
|
|
with self.assertRaises(UserError):
|
|
expense.total_amount_currency = 0.0
|
|
with self.assertRaises(UserError):
|
|
expense.total_amount = 0.0
|
|
|
|
# CASE 3: FORBIDS Trying to change the total_amount(_currency) to 0.0 when the expense is approved
|
|
expense.action_approve()
|
|
with self.assertRaises(UserError):
|
|
expense.total_amount_currency = 0.0
|
|
with self.assertRaises(UserError):
|
|
expense.total_amount = 0.0
|
|
|
|
# CASE 4: FORBIDS Trying to change the total_amount(_currency) to 0.0 when the expense is posted and the account move created
|
|
self.post_expenses_with_wizard(expense)
|
|
with self.assertRaises(UserError):
|
|
expense.total_amount_currency = 0.0
|
|
with self.assertRaises(UserError):
|
|
expense.total_amount = 0.0
|
|
|
|
# CASE 5: ALLOWS Changing the total_amount(_currency) to 0.0 when the expense is reset to draft
|
|
expense.account_move_id.button_draft()
|
|
expense.account_move_id.unlink()
|
|
expense.action_reset()
|
|
expense.write({'total_amount_currency': 0.0, 'total_amount': 0.0})
|
|
|
|
# CASE 6: FORBIDS Setting the amounts to 0 while submitting the expense
|
|
expense.write({'total_amount_currency': 1000.0, 'total_amount': 1000.0})
|
|
with self.assertRaises(UserError):
|
|
expense.write({'total_amount_currency': 0.0, 'state': 'submitted'})
|
|
with self.assertRaises(UserError):
|
|
expense.write({'total_amount': 0.0, 'state': 'submitted'})
|
|
|
|
# CASE 7: ALLOWS Setting the amounts to 0 while resetting the expense to draft
|
|
expense.write({'total_amount_currency': 0.0, 'total_amount': 0.0, 'state': 'draft'})
|
|
|
|
def test_foreign_currencies_total(self):
|
|
""" Check that the dashboard computes amount properly in company currency """
|
|
self.create_expenses([{
|
|
'name': 'Company expense',
|
|
'payment_mode': 'company_account',
|
|
'total_amount_currency': 1000.00,
|
|
'employee_id': self.expense_employee.id,
|
|
},
|
|
{
|
|
'name': 'Employee expense',
|
|
'payment_mode': 'own_account',
|
|
'currency_id': self.other_currency.id,
|
|
'total_amount_currency': 1000.00,
|
|
'total_amount': 2000.00,
|
|
'employee_id': self.expense_employee.id,
|
|
},
|
|
])
|
|
expense_data = self.env['hr.expense'].with_user(self.expense_user_employee).get_expense_dashboard()
|
|
self.assertEqual(expense_data['draft']['amount'], 3000.00)
|
|
|
|
def test_update_expense_price_on_product_standard_price(self):
|
|
"""
|
|
Tests that updating the standard price of a product will update all the un-submitted
|
|
expenses using that product as a category.
|
|
"""
|
|
product = self.env['product.product'].create({
|
|
'name': 'Product',
|
|
'standard_price': 100.0,
|
|
})
|
|
expenses = expense_no_update, expense_update = self.create_expenses([
|
|
{'name': name, 'product_id': product.id, 'total_amount': 100.0}
|
|
for name in ('test no update', 'test update')
|
|
]).sorted('name')
|
|
|
|
self.assertRecordValues(expenses.sorted('name'), [
|
|
{'name': 'test no update', 'price_unit': 100.0, 'quantity': 1, 'total_amount': 100.0},
|
|
{'name': 'test update', 'price_unit': 100.0, 'quantity': 1, 'total_amount': 100.0},
|
|
])
|
|
expense_no_update.action_submit() # No update when the expense is submitted
|
|
|
|
product.standard_price = 200.0
|
|
self.assertRecordValues(expenses.sorted('name'), [
|
|
{'name': 'test no update', 'price_unit': 100.0, 'quantity': 1.0, 'total_amount': 100.0},
|
|
{'name': 'test update', 'price_unit': 200.0, 'quantity': 1.0, 'total_amount': 200.0}, # total is updated
|
|
])
|
|
|
|
expense_update.quantity = 5
|
|
self.assertRecordValues(expenses.sorted('name'), [
|
|
{'name': 'test no update', 'price_unit': 100.0, 'quantity': 1, 'total_amount': 100.0},
|
|
{'name': 'test update', 'price_unit': 200.0, 'quantity': 5, 'total_amount': 1000.0}, # total is updated
|
|
])
|
|
|
|
product.standard_price = 0.0
|
|
self.assertRecordValues(expenses.sorted('name'), [
|
|
{'name': 'test no update', 'price_unit': 100.0, 'quantity': 1, 'total_amount': 100.0},
|
|
{'name': 'test update', 'price_unit': 1000.0, 'quantity': 1, 'total_amount': 1000.0}, # quantity & price_unit only are updated
|
|
])
|
|
|
|
expenses.action_submit() # This expense should not be updated any more
|
|
product.standard_price = 300.0
|
|
self.assertRecordValues(expenses.sorted('name'), [
|
|
{'name': 'test no update', 'price_unit': 100.0, 'quantity': 1, 'total_amount': 100.0},
|
|
{'name': 'test update', 'price_unit': 1000.0, 'quantity': 1, 'total_amount': 1000.0}, # no update
|
|
])
|
|
|
|
def test_expense_standard_price_update_warning(self):
|
|
expense_cat_A = self.env['product.product'].create({
|
|
'name': 'Category A',
|
|
'default_code': 'CA',
|
|
'standard_price': 0.0,
|
|
})
|
|
expense_cat_B = self.env['product.product'].create({
|
|
'name': 'Category B',
|
|
'default_code': 'CB',
|
|
'standard_price': 0.0,
|
|
})
|
|
expense_cat_C = self.env['product.product'].create({
|
|
'name': 'Category C',
|
|
'default_code': 'CC',
|
|
'standard_price': 0.0,
|
|
})
|
|
self.create_expenses([
|
|
{
|
|
'name': 'Expense 1',
|
|
'product_id': expense_cat_A.id,
|
|
'total_amount': 1,
|
|
},
|
|
{
|
|
'name': 'Expense 2',
|
|
'product_id': expense_cat_B.id,
|
|
'total_amount': 5,
|
|
},
|
|
])
|
|
|
|
# At first, there is no warning message on the categories because their prices are 0
|
|
self.assertFalse(expense_cat_A.standard_price_update_warning)
|
|
self.assertFalse(expense_cat_B.standard_price_update_warning)
|
|
self.assertFalse(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(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(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 their category, no message should appear as no expense is linked to it.
|
|
with Form(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):
|
|
"""
|
|
Test that the compute doesn't raise an error with mixed recordsets (products used in expenses and not used in expenses)
|
|
"""
|
|
product_expensed = self.env['product.product'].create({
|
|
'name': 'Category A',
|
|
'default_code': 'CA',
|
|
'standard_price': 0.0,
|
|
})
|
|
product_not_expensed = self.env['product.product'].create({
|
|
'name': 'Category B',
|
|
'default_code': 'CB',
|
|
'standard_price': 0.0,
|
|
})
|
|
self.env['hr.expense'].create({
|
|
'employee_id': self.expense_employee.id,
|
|
'name': 'Expense 1',
|
|
'product_id': product_expensed.id,
|
|
'total_amount': 1,
|
|
})
|
|
|
|
(product_expensed | product_not_expensed)._compute_standard_price_update_warning()
|
|
|
|
def test_expense_multi_company(self):
|
|
main_company = self.company_data['company']
|
|
other_company = self.company_data_2['company']
|
|
self.expense_employee.sudo().company_id = other_company
|
|
|
|
# The expense employee is able to create an expense for company_2.
|
|
# product_a needs a standard_price in company_2
|
|
self.product_a.with_context(allowed_company_ids=self.company_data_2['company'].ids).standard_price = 100
|
|
|
|
Expense = self.env['hr.expense'].with_user(self.expense_user_employee).with_context(allowed_company_ids=other_company.ids)
|
|
expense_approve = Expense.create([{
|
|
'name': 'First Expense for employee',
|
|
'employee_id': self.expense_employee.id,
|
|
'date': '2016-01-01',
|
|
'product_id': self.product_a.id,
|
|
'quantity': 1200.0,
|
|
}])
|
|
expense_refuse = Expense.create([{
|
|
'name': 'Second Expense for employee',
|
|
'employee_id': self.expense_employee.id,
|
|
'date': '2016-01-01',
|
|
'product_id': self.product_a.id,
|
|
'quantity': 1000.0,
|
|
}])
|
|
expenses = expense_approve | expense_refuse
|
|
self.assertRecordValues(expenses, [
|
|
{'company_id': self.company_data_2['company'].id},
|
|
{'company_id': self.company_data_2['company'].id},
|
|
])
|
|
|
|
# The expense employee is able to submit the expense.
|
|
expenses.with_user(self.expense_user_employee).action_submit()
|
|
|
|
# An expense manager is not able to approve nor refuse without access to company_2.
|
|
with self.assertRaises(UserError):
|
|
expense_approve \
|
|
.with_user(self.expense_user_manager) \
|
|
.with_context(allowed_company_ids=main_company.ids, company_id=main_company.id) \
|
|
.action_approve()
|
|
|
|
with self.assertRaises(UserError):
|
|
expense_refuse \
|
|
.with_user(self.expense_user_manager) \
|
|
.with_context(allowed_company_ids=main_company.ids) \
|
|
._do_refuse('failed')
|
|
|
|
# An expense manager is able to approve/refuse with access to company_2.
|
|
expense_approve \
|
|
.with_user(self.expense_user_manager) \
|
|
.with_context(allowed_company_ids=other_company.ids) \
|
|
.action_approve()
|
|
expense_refuse \
|
|
.with_user(self.expense_user_manager) \
|
|
.with_context(allowed_company_ids=other_company.ids) \
|
|
._do_refuse('failed')
|
|
|
|
# An expense manager having accounting access rights is not able to post the journal entry without access
|
|
# to company_2.
|
|
with self.assertRaises(UserError):
|
|
self.post_expenses_with_wizard(expense_approve.with_user(self.env.user).with_context(allowed_company_ids=main_company.ids))
|
|
|
|
# An expense manager having accounting access rights is able to post the journal entry with access to
|
|
# company_2.
|
|
self.post_expenses_with_wizard(expense_approve.with_user(self.env.user).with_context(allowed_company_ids=other_company.ids))
|
|
|
|
def test_tax_is_used_when_in_transactions(self):
|
|
""" Ensures that a tax is set to used when it is part of some transactions """
|
|
# Account.move is one type of transaction
|
|
tax_expense = self.env['account.tax'].create({
|
|
'name': 'test_is_used_expenses',
|
|
'amount': '100',
|
|
'include_base_amount': True,
|
|
})
|
|
|
|
self.create_expenses({'tax_ids': [Command.set(tax_expense.ids)]})
|
|
tax_expense.invalidate_model(fnames=['is_used'])
|
|
self.assertTrue(tax_expense.is_used)
|
|
|
|
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',
|
|
'reconcile': True,
|
|
})
|
|
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 = self.create_expenses({
|
|
'payment_mode': 'company_account',
|
|
'tax_ids': [Command.set(caba_tax.ids)],
|
|
})
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
expense.action_post()
|
|
moves = expense.account_move_id
|
|
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_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 = self.create_expenses({
|
|
'product_id': self.product_a.id,
|
|
'quantity': 350.00,
|
|
'payment_mode': 'company_account',
|
|
})
|
|
|
|
expense.action_submit()
|
|
with self.assertRaises(ValidationError, msg="One or more lines require a 100% analytic distribution."):
|
|
expense.with_context(validate_analytic=True).action_approve()
|
|
expense.analytic_distribution = {self.analytic_account_1.id: 100.00}
|
|
expense.with_context(validate_analytic=True).action_approve()
|
|
|
|
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 = self.create_expenses({'employee_id': self.expense_employee.id})
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
self.post_expenses_with_wizard(expense)
|
|
move = expense.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_set_total_amount_to_0(self):
|
|
""" Checks that amount fields are correctly updating when setting total_amount to 0 """
|
|
expense = self.create_expenses({
|
|
'product_id': self.product_c.id,
|
|
'total_amount_currency': 100.0,
|
|
})
|
|
expense.total_amount_currency = 0.0
|
|
self.assertTrue(expense.currency_id.is_zero(expense.tax_amount))
|
|
self.assertTrue(expense.company_currency_id.is_zero(expense.total_amount))
|
|
|
|
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.create_expenses({
|
|
'product_id': self.product_b.id,
|
|
'quantity': 10
|
|
})
|
|
expense.quantity = 0
|
|
self.assertTrue(expense.currency_id.is_zero(expense.total_amount_currency))
|
|
self.assertEqual(expense.company_currency_id.compare_amounts(expense.price_unit, self.product_b.standard_price), 0)
|
|
|
|
def test_employee_expense_in_foreign_currency(self):
|
|
""" Checks that the currency of the posted entries is always the company currency """
|
|
expense = self.create_expenses({
|
|
'payment_mode': 'own_account',
|
|
'currency_id': self.other_currency.id,
|
|
})
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
expense._post_without_wizard()
|
|
self.assertRecordValues(
|
|
expense.account_move_id,
|
|
[{'amount_total': 500.0, 'currency_id': expense.account_move_id.company_currency_id.id}],
|
|
)
|
|
|
|
def test_company_expense_sepa_ct_trust_bypass(self):
|
|
"""
|
|
Ensure company-paid expenses using SEPA CT post without requiring a trusted recipient bank account.
|
|
This validates the bypass in account.payment.action_post for expense-originated payments.
|
|
"""
|
|
self.env.ref('base.EUR').active = True
|
|
bank_journal = self.company_data['default_journal_bank']
|
|
bank = self.env['res.bank'].create({
|
|
'name': 'BNP Paribas',
|
|
'bic': 'GEBABEBB',
|
|
})
|
|
bank_journal.write({
|
|
'bank_id': bank.id,
|
|
'bank_acc_number': 'BE48363523682327',
|
|
'currency_id': self.env.ref('base.EUR').id,
|
|
})
|
|
|
|
sepa_ct_line = bank_journal.outbound_payment_method_line_ids.filtered(lambda l: l.code == 'sepa_ct')
|
|
if not sepa_ct_line:
|
|
self.skipTest("SEPA Credit Transfer payment method not available (account_sepa module not installed)")
|
|
|
|
expense = self.create_expenses({
|
|
'name': 'Hotel',
|
|
'payment_mode': 'company_account',
|
|
'payment_method_line_id': sepa_ct_line.id,
|
|
'total_amount_currency': 100.00,
|
|
'currency_id': self.env.ref('base.EUR').id,
|
|
})
|
|
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
# Should not raise trust validation error despite missing recipient partner bank
|
|
expense.action_post()
|
|
|
|
def test_expense_analytic_vendor_bill_count(self):
|
|
"""
|
|
Verify that purchase receipts appear when opening a vendor bill via the smart button,
|
|
and that the vendor bill count matches the records shown.
|
|
"""
|
|
expense = self.create_expenses([
|
|
{
|
|
'name': 'Employee PA 2*800 + 15%',
|
|
'analytic_distribution': {self.analytic_account_1.id: 100},
|
|
}])
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
self.post_expenses_with_wizard(expense)
|
|
|
|
self.assertEqual('in_receipt', expense.account_move_id.move_type)
|
|
|
|
vendor_bill_view = self.analytic_account_1.action_view_vendor_bill()
|
|
self.assertTrue(expense.account_move_id.id in vendor_bill_view['domain'][0][2])
|
|
|
|
def test_expense_paid_company_no_autobalancing_line(self):
|
|
"""
|
|
Test that when creating the move associated with an expense paid by company, no autobalancing line
|
|
appears when an analytic is added to a move line.
|
|
"""
|
|
expense = self.create_expenses({
|
|
'name': 'Expense for John Smith',
|
|
'employee_id': self.expense_employee.id,
|
|
'total_amount_currency': 100.0,
|
|
'product_id': self.product_c.id,
|
|
'payment_mode': 'company_account',
|
|
'company_id': self.company_data['company'].id,
|
|
'tax_ids': [self.tax_sale_a.id],
|
|
})
|
|
|
|
expense.action_submit()
|
|
expense.action_approve()
|
|
expense.analytic_distribution = {self.analytic_account_1.id: 100.00}
|
|
expense.action_post()
|
|
|
|
# Check that there is no fourth autobalancing line on the account move
|
|
self.assertEqual(expense.account_move_id.line_ids.mapped('balance'), [86.96, 13.04, -100.0])
|
|
|
|
def test_remove_company_id_from_hr_expense(self):
|
|
expense = self.create_expenses({
|
|
'name': 'Company PC 1000 + 15%',
|
|
'employee_id': self.expense_employee.id,
|
|
'product_id': self.product_c.id,
|
|
'total_amount_currency': 1000.00,
|
|
'date': '2021-10-12',
|
|
'payment_mode': 'company_account',
|
|
'company_id': self.company_data['company'].id,
|
|
'tax_ids': [Command.set(self.tax_purchase_a.ids)],
|
|
})
|
|
form = Form(expense)
|
|
form.company_id = self.env['res.company']
|
|
self.assertEqual(form.is_editable, False)
|
|
form.company_id = self.env.company
|
|
self.assertEqual(form.is_editable, True)
|