Files
enterprise-addons/l10n_ae_hr_payroll_account/tests/test_salary_rules.py
T

457 lines
20 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.hr_payroll_account.tests.common import TestPayslipValidationCommon
@tagged('post_install', 'post_install_l10n', '-at_install', 'payslips_validation')
class TestPayslipValidation(TestPayslipValidationCommon):
@classmethod
@TestPayslipValidationCommon.setup_country('ae')
def setUpClass(cls):
super().setUpClass()
cls.env.user.group_ids += cls.env.ref('hr_payroll.group_hr_payroll_user')
cls._setup_common(
country=cls.env.ref('base.ae'),
structure=cls.env.ref('l10n_ae_hr_payroll.uae_employee_payroll_structure'),
structure_type=cls.env.ref('l10n_ae_hr_payroll.uae_employee_payroll_structure_type'),
contract_fields={
'wage': 40000.0,
'l10n_ae_housing_allowance': 400.0,
'l10n_ae_transportation_allowance': 220.0,
'l10n_ae_other_allowances': 100.0,
'l10n_ae_is_dews_applied': True,
}
)
cls.work_entry_types = {
entry_type.code: entry_type
for entry_type in cls.env['hr.work.entry.type'].search([])
}
def _get_input_line_amount(self, payslip, code):
input_lines = payslip.input_line_ids.filtered(lambda line: line.code == code)
amounts = input_lines.mapped('amount')
return len(amounts), sum(amounts)
@classmethod
def _create_worked_days(cls, name=False, code=False, number_of_days=0, number_of_hours=0):
return Command.create({
'name': name,
'work_entry_type_id': cls.work_entry_types[code].id,
'code': code,
'number_of_days': number_of_days,
'number_of_hours': number_of_hours,
})
def _test_eos_calculation(self, start_date, departure_date, payslip_start, payslip_end,
expected_eos, has_unpaid_leave=False, wage=15000.0):
"""Helper method to test end of service salary rule calculations."""
employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'structure_type_id': self.env.ref('l10n_ae_hr_payroll.uae_employee_payroll_structure_type').id,
'country_id': self.env.ref('base.ae').id,
'wage': wage,
'date_version': start_date,
'contract_date_start': start_date,
})
if has_unpaid_leave:
unpaid_leave = self.env['hr.leave'].create({
'name': 'Unpaid Leave',
'employee_id': employee.id,
'holiday_status_id': self.env.ref('hr_holidays.leave_type_unpaid').id,
'request_date_from': date(2025, 8, 4),
'request_date_to': date(2025, 8, 8),
})
unpaid_leave.action_approve()
employee.version_id.generate_work_entries(payslip_start, payslip_end)
departure_notice = self.env['hr.departure.wizard'].create({
'employee_ids': [Command.link(employee.id)],
'departure_date': departure_date,
'departure_description': 'foo',
})
departure_notice.with_context(employee_termination=True).action_register_departure()
payslip = self._generate_payslip(payslip_start, payslip_end, employee_id=employee.id, version_id=employee.version_id.id)
payslip.compute_sheet()
self.assertEqual(
payslip._get_line_values(['EOS'])['EOS'][payslip.id]['total'],
expected_eos,
"End of Service calculation is incorrect"
)
def test_payslip_1(self):
payslip = self._generate_payslip(date(2024, 1, 1), date(2024, 1, 31))
payslip_results = {'BASIC': 40000.0, 'HOUALLOW': 400.0, 'TRAALLOW': 220.0, 'OTALLOW': 100.0, 'EOSP': 3333.33, 'ALP': 3393.33, 'GROSS': 40720.0, 'SICC': 5090.0, 'SIEC': -2036.0, 'DEWS': -3332.0, 'NET': 35352.0, 'NETCOST': 45810.0}
self._validate_payslip(payslip, payslip_results)
def test_payslip_2(self):
payslip = self._generate_payslip(date(2024, 1, 1), date(2024, 1, 31))
other_inputs_to_add = [
(self.env.ref('l10n_ae_hr_payroll.input_salary_arrears'), 1000),
(self.env.ref('l10n_ae_hr_payroll.input_other_earnings'), 2000),
(self.env.ref('l10n_ae_hr_payroll.input_salary_deduction'), 500),
(self.env.ref('l10n_ae_hr_payroll.input_other_deduction'), 200),
(self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_overtime_allowance'), 300),
(self.env.ref('l10n_ae_hr_payroll.input_bonus_earnings'), 400),
(self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_other_allowance'), 600),
(self.env.ref('l10n_ae_hr_payroll.input_airfare_allowance_earnings'), 700),
]
for other_input, amount in other_inputs_to_add:
self._add_other_input(payslip, other_input, amount)
payslip.compute_sheet()
payslip_results = {'BASIC': 40000.0, 'HOUALLOW': 400.0, 'TRAALLOW': 220.0, 'OTALLOW': 100.0, 'SALARY_ARREARS': 1000.0, 'OTHER_EARNINGS': 2000.0, 'SALARY_DEDUCTIONS': -500.0, 'OTHER_DEDUCTIONS': -200.0, 'OVERTIMEALLOWINP': 300.0, 'BONUS': 400.0, 'OTALLOWINP': 600.0, 'AIRFARE_ALLOWANCE': 700.0, 'EOSP': 3333.33, 'ALP': 3393.33, 'GROSS': 45720.0, 'SICC': 5090.0, 'SIEC': -2036.0, 'DEWS': -3332.0, 'NET': 39652.0, 'NETCOST': 50110.0}
self._validate_payslip(payslip, payslip_results)
def test_instant_pay_payslip_generation(self):
instant_pay_structure = self.env.ref('l10n_ae_hr_payroll.l10n_ae_uae_instant_pay')
payslip = self._generate_payslip(date(2023, 3, 1), date(2023, 3, 31), struct_id=instant_pay_structure.id)
other_inputs_to_add = [
(self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_allowance'), 1000),
(self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_commission'), 800),
(self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_salary_advance'), 1500),
(self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_loan_advance'), 1200),
(self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_deduction'), 700),
]
for other_input, amount in other_inputs_to_add:
self._add_other_input(payslip, other_input, amount)
payslip.compute_sheet()
payslip_results = {'ALLOW': 1000.00, 'COMM': 800.00, 'ADV': 1500.00, 'LOAN': 1200.00, 'DED': -700.00, 'NET': 3800.00}
self._validate_payslip(payslip, payslip_results)
def test_salary_advance(self):
instant_pay_structure = self.env.ref('l10n_ae_hr_payroll.l10n_ae_uae_instant_pay')
uae_employee_structure = self.env.ref('l10n_ae_hr_payroll.uae_employee_payroll_structure')
salary_advance_other_input = self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_salary_advance')
# First salary advance payslip of 500 on 01/09/2024 and setting the advance amount to 500 and validate the payslip
test_saladv_payslip1 = self._generate_payslip(
date(2024, 9, 1), date(2024, 9, 30), struct_id=instant_pay_structure.id
)
self._add_other_input(test_saladv_payslip1, salary_advance_other_input, 500)
test_saladv_payslip1.compute_sheet()
test_saladv_payslip1.action_payslip_done()
# Second salary advance payslip of 200 on 15/09/2024
test_saladv_payslip2 = self._generate_payslip(
date(2024, 9, 15), date(2024, 9, 30), struct_id=instant_pay_structure.id
)
self._add_other_input(test_saladv_payslip2, salary_advance_other_input, 200)
test_saladv_payslip2.compute_sheet()
test_saladv_payslip2.action_payslip_done()
# September monthly payslip
test_payslip_sept = self._generate_payslip(
date(2024, 9, 1), date(2024, 9, 30), struct_id=uae_employee_structure.id
)
test_payslip_sept._compute_input_line_ids()
# September monthly pay should have salary advance recovery = 700 by default
nbr_rec, amount_rec = self._get_input_line_amount(test_payslip_sept, "ADVREC")
self.assertEqual(nbr_rec, 1)
self.assertEqual(amount_rec, 700)
# Changing the recovery amount to 500 and validate the payslip
test_payslip_sept.input_line_ids.filtered(lambda line: line.code == "ADVREC").write({
"amount": 500
})
test_payslip_sept.compute_sheet()
test_payslip_sept.action_payslip_done()
nbr_rec, amount_rec = self._get_input_line_amount(test_payslip_sept, "ADVREC")
self.assertEqual(nbr_rec, 1)
self.assertEqual(amount_rec, 500)
# Third salary advance payslip of 300 on 1/10/2024
test_saladv_payslip3 = self._generate_payslip(
date(2024, 10, 1), date(2024, 10, 31), struct_id=instant_pay_structure.id
)
self._add_other_input(test_saladv_payslip3, salary_advance_other_input, 300)
test_saladv_payslip3.compute_sheet()
test_saladv_payslip3.action_payslip_done()
# October monthly pay should have salary advance recovery = 500 (200+300) by default
test_payslip_oct = self._generate_payslip(
date(2024, 10, 1), date(2024, 10, 31), struct_id=uae_employee_structure.id
)
test_payslip_oct._compute_input_line_ids()
test_payslip_oct.compute_sheet()
test_payslip_oct.action_payslip_done()
nbr_rec, amount_rec = self._get_input_line_amount(test_payslip_oct, "ADVREC")
self.assertEqual(nbr_rec, 1)
self.assertEqual(amount_rec, 500)
# November monthly pay should have salary advance recovery = 0
test_payslip_nov = self._generate_payslip(
date(2024, 11, 1), date(2024, 11, 30), struct_id=uae_employee_structure.id
)
test_payslip_nov._compute_input_line_ids()
test_payslip_nov.compute_sheet()
test_payslip_nov.action_payslip_done()
nbr_rec, amount_rec = self._get_input_line_amount(test_payslip_nov, "ADVREC")
self.assertEqual(nbr_rec, 0)
self.assertEqual(amount_rec, 0)
def test_end_of_service_salary_rule_1(self):
"""Case: Employee worked 2 years, 8 months, and 15 days
Expected: Calculate EOS for 2 years, 8 months and 15 days
(2 years * 12 month / year) + 8 months = 32 months
because the total is less than 6 years, ratio = 21 / 30
(15_000 $/year) * ratio * (1/12 year/month) = 875 $/month
(15_000 $/year) * ratio * (1/365 year/day) ~ 28.77 $/day
total = 32 months * 875 $/month = 28_000 $
15 days * 28.77 $/day ~ 432 $ +
= 28_432 $
"""
self._test_eos_calculation(
start_date=date(2014, 6, 4),
departure_date=date(2017, 2, 19),
payslip_start=date(2017, 2, 1),
payslip_end=date(2017, 2, 28),
expected_eos=28_432.0
)
def test_end_of_service_salary_rule_2(self):
"""Case: Employee worked 5 years, 5 months, and 17 days
Expected: Calculate EOS for 5 years, 5 months and 17 days
(5 years * 12 month / year) + 5 months = 65 months
because the total is less than 6 years, ratio = 21 / 30
(15_000 $/year) * ratio * (1/12 year/month) = 875 $/month
(15_000 $/year) * ratio * (1/365 year/day) ~ 28.77 $/day
total = 65 months * 875 $/month = 56_875 $
17 days * 28.77 $/day ~ 490 $ +
= 57_365 $
"""
self._test_eos_calculation(
start_date=date(2019, 7, 22),
departure_date=date(2025, 1, 8),
payslip_start=date(2025, 1, 1),
payslip_end=date(2025, 1, 31),
expected_eos=57_365.0
)
def test_end_of_service_salary_rule_3(self):
"""Case: Employee worked 6 years, 5 months, and 17 days
Expected: Calculate EOS for 6 years, 5 months and 17 days
(6 years * 12 month / year) + 5 months = 77 months
because the total is greater than 6 years, ratio = 1
(15_000 $/year) * ratio * (1/12 year/month) = 1250 $/month
(15_000 $/year) * ratio * (1/365 year/day) ~ 41.1 $/day
total = 60 months * 1250 * (21/30) $/month = 52_500 $
17 months * 1250 $/month = 21_250 $
17 days * 41.1 $/day ~ 699 $ +
= 74_449 $
"""
self._test_eos_calculation(
start_date=date(2018, 7, 22),
departure_date=date(2025, 1, 8),
payslip_start=date(2025, 1, 1),
payslip_end=date(2025, 1, 31),
expected_eos=74_449.0
)
def test_end_of_service_salary_rule_4(self):
"""Case: Employee worked 2 years, 8 months, and 15 days with 5 unpaid days
Expected: Calculate EOS for 2 years, 8 months and 10 days
(2 years * 12 month / year) + 8 months = 32 months
because the total is less than 6 years, ratio = 21 / 30
(15_000 $/year) * ratio * (1/12 year/month) = 875 $/month
(15_000 $/year) * ratio * (1/365 year/day) ~ 28.77 $/day
total = 32 months * 875 $/month = 28000 $
10 days * 28.77 $/day ~ 288 $ +
= 28_288 $
"""
self._test_eos_calculation(
start_date=date(2022, 12, 16),
departure_date=date(2025, 8, 31),
payslip_start=date(2025, 8, 1),
payslip_end=date(2025, 8, 31),
expected_eos=28_288.0,
has_unpaid_leave=True
)
def test_end_of_service_salary_rule_5(self):
"""Case: Employee worked 6 years and 3 days with 5 unpaid days
Expected: Calculate EOS for 5 years, 11 months and 29 days
(5 years * 12 month / year) + 11 months = 71 months
because the total is less than 6 years, ratio = 21 / 30
(15_000 $/year) * ratio * (1/12 year/month) = 875 $/month
(15_000 $/year) * ratio * (1/365 year/day) ~ 28.77 $/day
total = 71 months * 875 $/month = 62_125 $
29 days * 28.77 $/day ~ 835 $ +
= 62_960 $
"""
self._test_eos_calculation(
start_date=date(2019, 8, 28),
departure_date=date(2025, 8, 31),
payslip_start=date(2025, 8, 1),
payslip_end=date(2025, 8, 31),
expected_eos=62_960.0,
has_unpaid_leave=True
)
def test_end_of_service_salary_rule_6(self):
"""Case: Employee worked 7 years and 3 days with 5 unpaid days
Expected: Calculate EOS for 6 years, 11 months and 29 days
(6 years * 12 month / year) + 11 months = 83 months
because the total is greater than 6 years, ratio = 1
(15_000 $/year) * ratio * (1/12 year/month) = 1250 $/month
(15_000 $/year) * ratio * (1/365 year/day) ~ 41.1 $/day
total = 60 months * 1250 * (21/30) $/month = 52_500 $
23 months * 1250 $/month = 28_750 $
29 days * 41.1 $/day ~ 1192 $ +
= 82_442 $
"""
self._test_eos_calculation(
start_date=date(2018, 8, 28),
departure_date=date(2025, 8, 31),
payslip_start=date(2025, 8, 1),
payslip_end=date(2025, 8, 31),
expected_eos=82_442.0,
has_unpaid_leave=True
)
def test_payslip_attendance_1(self):
if self.env['ir.module.module']._get('hr_payroll_attendance').state != 'installed':
self.skipTest("Skipping test because hr_payroll_attendance is not installed.")
self.employee.country_id = False
self.contract.write({
'contract_date_start': '2025-01-01',
'work_entry_source': 'attendance',
'wage': 5000,
'wage_type': 'monthly',
'l10n_ae_housing_allowance': 2000,
'l10n_ae_transportation_allowance': 1000,
'l10n_ae_other_allowances': 100,
'l10n_ae_is_dews_applied': False,
})
worked_days_vals = [
{'name': 'Unpaid', 'code': 'LEAVE90', 'number_of_hours': 16, 'number_of_days': 2},
{'name': 'Paid Time Off', 'code': 'LEAVE120', 'number_of_hours': 24, 'number_of_days': 3},
{'name': 'Sick Leave 50', 'code': 'AESICKLEAVE50', 'number_of_hours': 24, 'number_of_days': 3},
{'name': 'Out of Contract', 'code': 'OUT', 'number_of_hours': 32, 'number_of_days': 4},
{'name': 'Attendance', 'code': 'WORK100', 'number_of_hours': 88, 'number_of_days': 11},
]
payslip = self._generate_payslip('2025-07-01', '2025-07-31')
payslip.write({
"worked_days_line_ids": [self._create_worked_days(**vals) for vals in worked_days_vals],
})
payslip.compute_sheet()
payslip_results = {
'BASIC': 2391.30,
'HOUALLOW': 956.52,
'TRAALLOW': 478.26,
'OTALLOW': 47.83,
'EOSP': 188.73,
'ALP': 436.76,
'AEPAID': 1056.48,
'AESPAID50': 528.24,
'GROSS': 5458.63,
'NET': 5458.63,
'NETCOST': 5458.63,
}
self._validate_payslip(payslip, payslip_results)
def test_payslip_attendance_2(self):
if self.env['ir.module.module']._get('hr_payroll_attendance').state != 'installed':
self.skipTest("Skipping test because hr_payroll_attendance is not installed.")
self.employee.country_id = False
self.contract.write({
'contract_date_start': '2025-01-01',
'work_entry_source': 'attendance',
'wage': 5000,
'hourly_wage': 44.02,
'wage_type': 'hourly',
'l10n_ae_housing_allowance': 2000,
'l10n_ae_transportation_allowance': 1000,
'l10n_ae_other_allowances': 100,
'l10n_ae_is_dews_applied': False,
})
worked_days_vals = [
{'name': 'Unpaid', 'code': 'LEAVE90', 'number_of_hours': 16, 'number_of_days': 2},
{'name': 'Paid Time Off', 'code': 'LEAVE120', 'number_of_hours': 24, 'number_of_days': 3},
{'name': 'Sick Leave 50', 'code': 'AESICKLEAVE50', 'number_of_hours': 24, 'number_of_days': 3},
{'name': 'Out of Contract', 'code': 'OUT', 'number_of_hours': 32, 'number_of_days': 4},
{'name': 'Attendance', 'code': 'WORK100', 'number_of_hours': 72, 'number_of_days': 9},
]
payslip = self._generate_payslip('2025-06-01', '2025-06-30')
payslip.write({
"worked_days_line_ids": [self._create_worked_days(**vals) for vals in worked_days_vals]
})
payslip.compute_sheet()
payslip_results = {
'BASIC': 2142.86,
'EOSP': 175.0,
'ALP': 405.0,
'AEPAID': 1056.48,
'AESPAID50': 528.24,
'GROSS': 3727.58,
'NET': 3727.58,
'NETCOST': 3727.58,
}
self._validate_payslip(payslip, payslip_results)
def test_payslip_attendance_3(self):
if self.env['ir.module.module']._get('hr_payroll_attendance').state != 'installed':
self.skipTest("Skipping test because hr_payroll_attendance is not installed.")
self.employee.country_id = False
self.contract.write({
'contract_date_start': '2025-01-01',
'work_entry_source': 'attendance',
'wage': 10000,
'wage_type': 'monthly',
'l10n_ae_housing_allowance': 2000,
'l10n_ae_transportation_allowance': 1000,
'l10n_ae_other_allowances': 100,
'l10n_ae_is_dews_applied': False,
})
worked_days_vals = [
{'name': 'Attendance', 'code': 'WORK100', 'number_of_hours': 80, 'number_of_days': 20},
]
payslip = self._generate_payslip('2025-11-01', '2025-11-30')
payslip.write({
"worked_days_line_ids": [self._create_worked_days(**vals) for vals in worked_days_vals],
})
payslip.compute_sheet()
payslip_results = {
'BASIC': 5000.0,
'HOUALLOW': 1000.0,
'TRAALLOW': 500.0,
'OTALLOW': 50.0,
'EOSP': 583.33,
'ALP': 1091.67,
'AEPAID': 0.0,
'AESPAID50': 0.0,
'GROSS': 6550.0,
'NET': 6550.0,
'NETCOST': 6550.0,
}
self._validate_payslip(payslip, payslip_results)