Files

392 lines
21 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo import Command
from odoo.addons.hr_payroll_expense.tests.test_payroll_expense import TestPayrollExpense
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestPayrollExpenseBatched(TestPayrollExpense):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.group_ids |= cls.env.ref('hr_payroll.group_hr_payroll_manager')
cls.company.batch_payroll_move_lines = True
expenses_vals = []
payslips_vals = []
for idx, employee_vals in ((idx, {'name': f'Clone n°{idx:0>2}', 'email': f'clone_{idx}@clone.clone'}) for idx in range(10)):
new_partner = cls.env['res.partner'].create(employee_vals)
new_employee = cls.env['hr.employee'].create({
'name': employee_vals['name'],
'company_id': cls.company_data['company'].id,
'country_id': cls.company_data['company'].country_id.id,
'department_id': cls.dep_rd.id,
'work_contact_id': new_partner.id,
'date_version': '2020-01-01',
'contract_date_start': '2020-01-01',
'contract_date_end': '2062-01-25',
'wage': 5000.33,
'structure_type_id': cls.hr_structure_type.id,
})
expenses_vals.append({
'name': f"Test Expense {employee_vals['name']}",
'employee_id': new_employee.id,
'product_id': cls.product_c.id,
'total_amount_currency': 1000 + idx,
'tax_ids': [Command.set(cls.tax_purchase_a.ids)],
})
payslips_vals.append(
{
'name': f"Payslip for {employee_vals['name']}",
'employee_id': new_employee.id,
'struct_id': cls.expense_hr_structure.id,
'version_id': new_employee.version_id.id,
'payslip_run_id': cls.payslip_run.id,
'date_from': '2022-01-01',
'date_to': '2022-01-25',
'company_id': cls.company_data['company'].id
}
)
cls.batched_expenses = cls.env['hr.expense'].create(expenses_vals)
cls.batched_expenses.action_submit()
cls.batched_expenses._do_approve() # Skip duplicate wizard
cls.post_expenses_with_wizard(cls.batched_expenses, date='2022-01-15')
cls.batched_expenses.action_report_in_next_payslip()
cls.batched_payslips = cls.env['hr.payslip'].create(payslips_vals)
@freeze_time('2022-01-25')
def test_corner_case_batched_payslips_simple_case(self):
""" Test that the link is still properly done when payslips are accounted for in batches """
self.batched_payslips.compute_sheet()
self.payslip_run.action_validate()
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.account_move_id.mapped('state'),
)
self.payslip_run.slip_ids.move_id.action_post()
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.account_move_id.mapped('state'),
"All expense moves should be automatically posted when posting the payslip one",
)
self.assertEqual(1, len(self.payslip_run.slip_ids.move_id))
self.assertSequenceEqual(
['paid'] * 10,
self.batched_expenses.mapped('state'),
"All expense moves should be paid and reconciled with the payslips move",
)
# Check reconciliation
# Get the corresponding account.partial.reconcile lines
expenses_lines_to_reconcile, payslips_lines_to_reconcile = \
self.get_all_amls_to_be_reconciled(self.batched_expenses, self.batched_payslips)
reconciliation_lines = self.get_reconciliation_lines_from_accounts(
(expenses_lines_to_reconcile | payslips_lines_to_reconcile).account_id.ids
)
misc_move = reconciliation_lines.debit_move_id.move_id - (self.batched_expenses.account_move_id | self.batched_payslips.move_id)
self.assertEqual(
len(misc_move),
1,
"Because the expenses & the payslips moves don't have the same account, there should be a misc entry generated",
)
misc_move_lines = misc_move.line_ids.sorted('balance')
expenses_lines_to_reconcile = expenses_lines_to_reconcile.sorted('balance')
self.assertRecordValues(reconciliation_lines.sorted('amount'), [
{'amount': 1000.0, 'debit_move_id': misc_move_lines.ids[1], 'credit_move_id': expenses_lines_to_reconcile.ids[9]},
{'amount': 1001.0, 'debit_move_id': misc_move_lines.ids[2], 'credit_move_id': expenses_lines_to_reconcile.ids[8]},
{'amount': 1002.0, 'debit_move_id': misc_move_lines.ids[3], 'credit_move_id': expenses_lines_to_reconcile.ids[7]},
{'amount': 1003.0, 'debit_move_id': misc_move_lines.ids[4], 'credit_move_id': expenses_lines_to_reconcile.ids[6]},
{'amount': 1004.0, 'debit_move_id': misc_move_lines.ids[5], 'credit_move_id': expenses_lines_to_reconcile.ids[5]},
{'amount': 1005.0, 'debit_move_id': misc_move_lines.ids[6], 'credit_move_id': expenses_lines_to_reconcile.ids[4]},
{'amount': 1006.0, 'debit_move_id': misc_move_lines.ids[7], 'credit_move_id': expenses_lines_to_reconcile.ids[3]},
{'amount': 1007.0, 'debit_move_id': misc_move_lines.ids[8], 'credit_move_id': expenses_lines_to_reconcile.ids[2]},
{'amount': 1008.0, 'debit_move_id': misc_move_lines.ids[9], 'credit_move_id': expenses_lines_to_reconcile.ids[1]},
{'amount': 1009.0, 'debit_move_id': misc_move_lines.ids[10], 'credit_move_id': expenses_lines_to_reconcile.ids[0]},
{'amount': 10045.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': misc_move_lines.ids[0]},
])
@freeze_time('2022-01-25')
def test_corner_case_batched_payslips_with_same_accounts_between_expenses_and_payslip(self):
"""
Test that posting the payslip move is properly reconciled with the expenses ones,
in the case where the payable account is the same for both the payslip & the expenses moves
"""
# Sets the expense rule on the payroll expense rule
payable_account_id = self.batched_expenses[0]._get_expense_account_destination()
self.expense_salary_rule.account_debit = payable_account_id
self.batched_payslips.compute_sheet()
self.payslip_run.action_validate()
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.account_move_id.mapped('state'),
)
self.payslip_run.slip_ids.move_id.action_post()
self.assertEqual(1, len(self.payslip_run.slip_ids.move_id))
self.assertSequenceEqual(
['paid'] * 10,
self.batched_expenses.mapped('state'),
"All expense moves should be paid and reconciled with the payslips move",
)
# Check reconciliation
# Get the corresponding account.partial.reconcile lines
expenses_lines_to_reconcile, payslips_lines_to_reconcile = \
self.get_all_amls_to_be_reconciled(self.batched_expenses, self.batched_payslips)
reconciliation_lines = self.get_reconciliation_lines_from_accounts([payable_account_id])
misc_move = reconciliation_lines.debit_move_id.move_id - (self.batched_expenses.account_move_id | self.batched_payslips.move_id)
self.assertFalse(
misc_move,
"Because the expenses & the payslips moves have the same account, there should be no misc entry generated",
)
expenses_lines_to_reconcile = expenses_lines_to_reconcile.sorted('balance')
self.assertRecordValues(reconciliation_lines.sorted('amount'), [
{'amount': 1000.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[9]},
{'amount': 1001.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[8]},
{'amount': 1002.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[7]},
{'amount': 1003.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[6]},
{'amount': 1004.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[5]},
{'amount': 1005.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[4]},
{'amount': 1006.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[3]},
{'amount': 1007.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[2]},
{'amount': 1008.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[1]},
{'amount': 1009.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[0]},
])
@freeze_time('2022-01-25')
def test_corner_case_batched_payslips_with_too_many_different_accounts(self):
"""
Test that posting the payslip move cannot automatically reconcile but prepares it to be manually reconciled,
in the case where there would be more than 2 different accounts for the payslip & the expenses moves
"""
# Unlink the actual moves, so they're re-generated with the new accounts
self.batched_expenses.account_move_id.button_draft()
self.batched_expenses.account_move_id.unlink()
self.batched_expenses.action_reset()
# Generate a different account for each partner
default_payable_account = self.env['account.account'].browse(self.batched_expenses[0]._get_expense_account_destination())
for idx, employee_partner in enumerate(self.batched_expenses.employee_id.work_contact_id.sorted('name')):
employee_partner.property_account_payable_id = self.env['account.account'].create({
'name': employee_partner.name,
'code': f'{default_payable_account.code}00{idx}',
'account_type': 'liability_payable',
})
self.batched_expenses.action_submit()
self.batched_expenses._do_approve() # Skip duplicate wizard
self.post_expenses_with_wizard(self.batched_expenses)
self.assertEqual(
10,
len(self.batched_expenses.account_move_id.line_ids.filtered(lambda line: line.display_type == 'payment_term').account_id),
"We should now have a different account per expense",
)
# Re-link payslips with the expenses, as the reset of the expenses unlinked them
self.batched_expenses.action_report_in_next_payslip()
self.batched_payslips.action_payslip_draft()
self.assertSetEqual(set(self.batched_expenses.payslip_id.ids), set(self.batched_payslips.ids))
self.batched_payslips.compute_sheet()
self.payslip_run.action_validate()
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.account_move_id.mapped('state'),
)
self.payslip_run.slip_ids.move_id.action_post()
self.assertEqual(1, len(self.payslip_run.slip_ids.move_id))
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.mapped('state'),
"All expense moves should still be set to 'not_paid'",
)
# Check the moves are NOT reconciled
# Get the corresponding account.partial.reconcile lines
expenses_lines_to_reconcile, payslips_lines_to_reconcile = \
self.get_all_amls_to_be_reconciled(self.batched_expenses, self.batched_payslips)
reconciliation_lines = self.get_reconciliation_lines_from_accounts(
(expenses_lines_to_reconcile | payslips_lines_to_reconcile).account_id.ids
)
self.assertFalse(
reconciliation_lines,
"Because the expenses & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.mapped('state'),
"Because the expenses & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertSequenceEqual(
['not_paid'] * 10,
self.batched_expenses.account_move_id.mapped('payment_state'),
"Because the expenses & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertEqual(
10045.0,
sum(self.batched_expenses.account_move_id.mapped('amount_residual')),
"Because the expenses & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertSequenceEqual(
[f'I0000000000-{self.batched_payslips.move_id.id}-{min(self.batched_expenses.account_move_id.ids)}'] * 11,
expenses_lines_to_reconcile.mapped('matching_number') + [payslips_lines_to_reconcile.matching_number],
"A temporary matching number should still be present on the account move lines to help manually reconcile them"
)
@freeze_time('2022-01-25')
def test_corner_case_batched_payslips_with_different_accounts_mix(self):
"""
Test that the link is still properly done when payslips are accounted for in batches,
in the case where there would be 2 different accounts for the payslip & the expenses moves,
but the accounts difference is only on the expense side
(mix between `-with_same_accounts_between_expenses_and_payslip` & `-with_too_many_different_accounts`)
"""
# Sets the payslip move expense line account to be the default expense one
self.expense_salary_rule.account_debit = self.batched_expenses[0]._get_expense_account_destination()
# Take one expense and make it use a different account
different_account_expense = self.batched_expenses[-1]
different_account_expense_original_payslip = different_account_expense.payslip_id
different_account_expense.account_move_id.button_draft()
different_account_expense.account_move_id.unlink()
different_account = self.env['account.account'].create({
'name': 'Different account',
'code': '111111111111111111111111111111111',
'account_type': 'liability_payable',
})
different_account_expense.employee_id.work_contact_id.property_account_payable_id = different_account
# Put the expense back on the payslip
self.post_expenses_with_wizard(different_account_expense)
different_account_expense.action_report_in_next_payslip()
different_account_expense_original_payslip.action_payslip_draft()
self.assertEqual(
different_account_expense_original_payslip.id,
different_account_expense.payslip_id.id,
'The expense should be re-linked to a payslip',
)
self.batched_payslips.compute_sheet()
self.payslip_run.action_validate()
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.account_move_id.mapped('state'),
)
self.payslip_run.slip_ids.move_id.action_post()
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.account_move_id.mapped('state'),
"All expense moves should be automatically posted when posting the payslip one",
)
self.assertEqual(1, len(self.payslip_run.slip_ids.move_id))
self.assertSequenceEqual(
['paid'] * 10,
self.batched_expenses.mapped('state'),
"All expense moves should be paid and reconciled with the payslips move",
)
# Check reconciliation
# Get the corresponding account.partial.reconcile lines
expenses_lines_to_reconcile, payslips_lines_to_reconcile = \
self.get_all_amls_to_be_reconciled(self.batched_expenses, self.batched_payslips)
reconciliation_lines = self.get_reconciliation_lines_from_accounts((self.expense_salary_rule.account_debit | different_account).ids)
misc_move = reconciliation_lines.debit_move_id.move_id - (self.batched_expenses.account_move_id | self.batched_payslips.move_id)
self.assertEqual(
len(misc_move),
1,
"Because the last expense & the payslips moves don't have the same account, there should be a misc entry generated",
)
misc_move_lines = misc_move.line_ids.sorted('balance')
expenses_lines_to_reconcile = expenses_lines_to_reconcile.sorted('balance')
# Sorting by name to avoid non-deterministic order of the two last lines, names of the lines sould be "Reimb.." & "Transfer..."
self.assertRecordValues(reconciliation_lines.sorted(lambda prl: (prl.amount, prl.debit_move_id.name)), [
{'amount': 1000.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[9]},
{'amount': 1001.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[8]},
{'amount': 1002.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[7]},
{'amount': 1003.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[6]},
{'amount': 1004.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[5]},
{'amount': 1005.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[4]},
{'amount': 1006.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[3]},
{'amount': 1007.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[2]},
{'amount': 1008.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': expenses_lines_to_reconcile.ids[1]},
# Only the last expense is reconciled with a misc move as it uses a different account
{'amount': 1009.0, 'debit_move_id': payslips_lines_to_reconcile.id, 'credit_move_id': misc_move_lines.ids[0]},
{'amount': 1009.0, 'debit_move_id': misc_move_lines.ids[1], 'credit_move_id': expenses_lines_to_reconcile.ids[0]},
])
@freeze_time('2022-01-25')
def test_corner_case_batched_payslips_with_edited_expense_moves(self):
edited_expense = self.batched_expenses[0]
# Editing the expense move
edited_expense.account_move_id.button_draft()
edited_expense.account_move_id.invoice_line_ids.filtered(lambda aml: aml.product_id == self.product_c).quantity = 2
self.assertEqual(
edited_expense.total_amount,
1000,
"The move should have a different amount than the expense",
)
self.assertEqual(
edited_expense.account_move_id.amount_total,
2000,
"The move should have a different amount than the expense",
)
# Post the moves
self.batched_payslips.compute_sheet()
self.payslip_run.action_validate()
self.batched_payslips.move_id.action_post()
# Check the two moves are NOT reconciled
# Get the corresponding account.partial.reconcile lines
expenses_lines_to_reconcile, payslips_lines_to_reconcile = \
self.get_all_amls_to_be_reconciled(self.batched_expenses, self.batched_payslips)
reconciliation_lines = self.get_reconciliation_lines_from_accounts(
(expenses_lines_to_reconcile | payslips_lines_to_reconcile).account_id.ids
)
self.assertFalse(
reconciliation_lines,
"Because the expense & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertSequenceEqual(
['posted'] * 10,
self.batched_expenses.mapped('state'),
"Because the expense & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertSequenceEqual(
['not_paid'] * 10,
self.batched_expenses.account_move_id.mapped('payment_state'),
"Because the expense & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertNotEqual(
0.0,
sum(self.batched_expenses.account_move_id.mapped('amount_residual')),
"Because the expense & the payslip moves don't have the same amount, there should be no reconciliation",
)
self.assertSequenceEqual(
[f'I0000000000-{self.batched_payslips.move_id.id}-{min(self.batched_expenses.account_move_id.ids)}'] * 11,
expenses_lines_to_reconcile.mapped('matching_number') + [payslips_lines_to_reconcile.matching_number],
"A temporary matching number should still be present on the account move lines to help manually reconcile them"
)