678 lines
30 KiB
Python
678 lines
30 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import datetime
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
from odoo import Command
|
|
from odoo.tests import HttpCase, tagged
|
|
from odoo.addons.hr_payroll.tests.common import TestPayslipBase
|
|
from odoo.addons.mail.tests.common import mail_new_test_user
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests import Form
|
|
|
|
|
|
class TestPayslipFlow(TestPayslipBase):
|
|
|
|
def test_00_payslip_flow(self):
|
|
""" Testing payslip flow and report printing """
|
|
|
|
# I create an employee Payslip
|
|
richard_payslip = self.env['hr.payslip'].create({
|
|
'name': 'Payslip of Richard',
|
|
'employee_id': self.richard_emp.id,
|
|
})
|
|
|
|
payslip_input = self.env['hr.payslip.input'].search([('payslip_id', '=', richard_payslip.id)])
|
|
# I assign the amount to Input data
|
|
payslip_input.write({'amount': 5.0})
|
|
|
|
# I verify the payslip is in draft state
|
|
self.assertEqual(richard_payslip.state, 'draft', 'State not changed!')
|
|
|
|
richard_payslip.compute_sheet()
|
|
|
|
# Then I click on the 'Confirm' button on payslip
|
|
richard_payslip.action_payslip_done()
|
|
|
|
# I verify that the payslip is in validated state
|
|
self.assertEqual(richard_payslip.state, 'validated', 'State not changed!')
|
|
|
|
# Then I click on the 'Mark as paid' button on payslip
|
|
richard_payslip.action_payslip_paid()
|
|
|
|
# I verify that the payslip is in paid state
|
|
self.assertEqual(richard_payslip.state, 'paid', 'State not changed!')
|
|
|
|
# I want to check refund payslip so I click on refund button.
|
|
richard_payslip.refund_sheet()
|
|
|
|
# I check on new payslip Credit Note is checked or not.
|
|
payslip_refund = self.env['hr.payslip'].search([('name', 'like', 'Refund: '+ richard_payslip.name), ('credit_note', '=', True)])
|
|
self.assertTrue(bool(payslip_refund), "Payslip not refunded!")
|
|
|
|
# I want to generate a payslip from Payslip run.
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_end': '2011-09-30',
|
|
'date_start': '2011-09-01',
|
|
'name': 'Payslip for Employee'
|
|
})
|
|
|
|
# I create record for generating the payslip for this Payslip run.
|
|
|
|
# The contract of Richard starts in 2018, the payrun is for 2011, no valid versions can be found.
|
|
# So the generate_payslip without versions must raise an error
|
|
with self.assertRaises(UserError):
|
|
payslip_run.generate_payslips(employee_ids=[self.richard_emp.id])
|
|
|
|
def test_01_batch_with_specific_structure(self):
|
|
""" Generate payslips for the employee whose running contract is based on the same Salary Structure Type"""
|
|
|
|
specific_structure_type = self.env['hr.payroll.structure.type'].create({
|
|
'name': 'Structure Type Test'
|
|
})
|
|
|
|
specific_structure = self.env['hr.payroll.structure'].create({
|
|
'name': 'End of the Year Bonus - Test',
|
|
'type_id': specific_structure_type.id,
|
|
})
|
|
|
|
# 13th month pay
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=8, day=31),
|
|
'structure_id': specific_structure.id,
|
|
'name': 'End of the year bonus',
|
|
})
|
|
|
|
with self.assertRaises(UserError):
|
|
payslip_run.generate_payslips(payslip_run._get_valid_version_ids())
|
|
|
|
# Update the structure type and generate payslips again
|
|
specific_structure_type.default_struct_id = specific_structure.id
|
|
self.richard_emp.version_ids[0].structure_type_id = specific_structure_type.id
|
|
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=8, day=31),
|
|
'structure_id': specific_structure.id,
|
|
'name': 'Batch for Structure',
|
|
})
|
|
|
|
payslip_run.generate_payslips(payslip_run._get_valid_version_ids())
|
|
|
|
self.richard_emp.structure_type_id = specific_structure_type.id
|
|
|
|
self.assertTrue(payslip_run.slip_ids)
|
|
self.assertTrue(self.richard_emp.id in payslip_run.slip_ids.employee_id.ids)
|
|
|
|
self.assertEqual(len(payslip_run.slip_ids), 1)
|
|
self.assertEqual(payslip_run.slip_ids.struct_id.id, specific_structure.id)
|
|
|
|
def test_03_payslip_batch_with_payment_process(self):
|
|
'''
|
|
Test to check if some payslips in the batch are already paid,
|
|
the batch status can be updated to 'paid' without affecting
|
|
those already paid payslips.
|
|
'''
|
|
self.contract_jules = self.jules_emp.version_id.write({
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'name': 'Contract for Jules',
|
|
'wage': 5000.33,
|
|
'employee_id': self.jules_emp.id,
|
|
'structure_type_id': self.structure_type.id,
|
|
})
|
|
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=8, day=31),
|
|
'name': 'Payment Test',
|
|
})
|
|
|
|
payslip_run.generate_payslips(employee_ids=[self.richard_emp.id, self.jules_emp.id])
|
|
|
|
payslip_run.action_validate()
|
|
|
|
self.assertEqual(len(payslip_run.slip_ids), 2)
|
|
self.assertTrue(all(payslip.state == 'validated' for payslip in payslip_run.slip_ids), 'State not changed!')
|
|
|
|
# Mark the first payslip as paid and store the paid date
|
|
payslip_run.slip_ids[0].action_payslip_paid()
|
|
paid_date = payslip_run.slip_ids[0].paid_date
|
|
|
|
self.assertEqual(payslip_run.slip_ids[0].state, 'paid', 'State not changed!')
|
|
self.assertEqual(payslip_run.slip_ids[1].state, 'validated', 'State not changed!')
|
|
|
|
payslip_run.action_paid()
|
|
|
|
self.assertEqual(payslip_run.state, '03_paid', 'State not changed!')
|
|
self.assertTrue(all(payslip.state == 'paid' for payslip in payslip_run.slip_ids), 'State not changed!')
|
|
self.assertEqual(payslip_run.slip_ids[0].paid_date, paid_date, 'payslip paid date should not be changed')
|
|
|
|
def test_04_payslip_batch_wizard_for_employee_selection_mode(self):
|
|
'''
|
|
Test employee_ids selection in batch payslip generating wizard
|
|
based on employee selection mode fields
|
|
'''
|
|
self.richard_emp.version_ids[0].contract_date_end = False
|
|
self.contract_jules = self.jules_emp.version_id.write({
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'name': 'Contract for Jules',
|
|
'wage': 5000.33,
|
|
'employee_id': self.jules_emp.id,
|
|
})
|
|
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=8, day=31),
|
|
'name': 'Payment Test',
|
|
})
|
|
|
|
payslip_run.generate_payslips(employee_ids=[self.richard_emp.id, self.jules_emp.id])
|
|
|
|
self.assertEqual(len(payslip_run.slip_ids.employee_id.ids), 2)
|
|
self.assertTrue(all(employee in [self.richard_emp.id, self.jules_emp.id] for employee in payslip_run.slip_ids.employee_id.ids))
|
|
|
|
def test_05_payslip_batch_wizard_for_department_selection_mode(self):
|
|
'''
|
|
Test employee_ids selection in batch payslip generating wizard
|
|
based on department selection mode fields
|
|
'''
|
|
self.richard_emp.version_ids[0].date_end = False
|
|
self.contract_jules = self.jules_emp.version_id.write({
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'name': 'Contract for Jules',
|
|
'wage': 5000.33,
|
|
'employee_id': self.jules_emp.id,
|
|
})
|
|
self.richard_emp.department_id = False
|
|
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=8, day=31),
|
|
'name': 'Payment Test',
|
|
})
|
|
|
|
employees = self.env["hr.employee"].search([('department_id', 'in', [self.dep_rd.id])])
|
|
|
|
payslip_run.generate_payslips(employee_ids=employees.ids)
|
|
|
|
self.assertEqual(len(employees.ids), 1)
|
|
self.assertEqual(employees.ids[0], self.jules_emp.id)
|
|
|
|
def test_06_payslip_batch_wizard_for_job_selection_mode(self):
|
|
'''
|
|
Test employee_ids selection in batch payslip generating wizard
|
|
based on job selection mode fields
|
|
'''
|
|
self.richard_emp.version_ids[0].date_end = False
|
|
job_developer = self.env['hr.job'].create({
|
|
'name': 'Experienced Developer',
|
|
'department_id': self.dep_rd.id,
|
|
'no_of_recruitment': 5,
|
|
})
|
|
self.richard_emp.job_id = job_developer.id
|
|
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=8, day=31),
|
|
'name': 'Payment Test',
|
|
})
|
|
|
|
employees = self.env["hr.employee"].search([('job_id', 'in', [job_developer.id])])
|
|
|
|
payslip_run.generate_payslips(employee_ids=employees.ids)
|
|
|
|
self.assertEqual(len(employees.ids), 1)
|
|
self.assertTrue(self.richard_emp in employees)
|
|
|
|
def test_07_payslip_batch_wizard_for_category_selection_mode(self):
|
|
'''
|
|
Test employee_ids selection in batch payslip generating wizard
|
|
based on category selection mode fields
|
|
'''
|
|
self.richard_emp.version_ids[0].date_end = False
|
|
self.contract_jules = self.jules_emp.version_id.write({
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'name': 'Contract for Jules',
|
|
'wage': 5000.33,
|
|
'employee_id': self.jules_emp.id,
|
|
})
|
|
category_tag = self.env['hr.employee.category'].create({'name': 'Test category'})
|
|
self.jules_emp.category_ids = [category_tag.id]
|
|
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=8, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=8, day=31),
|
|
'name': 'Payment Test',
|
|
})
|
|
|
|
employees = self.env["hr.employee"].search([('category_ids', 'in', [category_tag.id])])
|
|
|
|
payslip_run.generate_payslips(employee_ids=employees.ids)
|
|
|
|
self.assertEqual(len(employees.ids), 1)
|
|
self.assertEqual(employees.ids[0], self.jules_emp.id)
|
|
|
|
def test_08_payslip_batch_wizard_for_structure_type_selection_mode_with_multiple_contract(self):
|
|
structure_typeA, structure_typeB = self.env['hr.payroll.structure.type'].create([
|
|
{'name': 'Test A'},
|
|
{'name': 'Test B'},
|
|
])
|
|
|
|
structureA, structureB = self.env['hr.payroll.structure'].create([
|
|
{
|
|
'name': 'Test structure A',
|
|
'type_id': structure_typeA.id,
|
|
},
|
|
{
|
|
'name': 'Test structure B',
|
|
'type_id': structure_typeB.id,
|
|
},
|
|
])
|
|
|
|
payslip_runA, payslip_runB, payslip_runC = self.env['hr.payslip.run'].create([
|
|
{
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=2, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=2, day=31),
|
|
'structure_id': structureA.id,
|
|
'name': 'Payslip RUN A'
|
|
},
|
|
{
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=4, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=4, day=31),
|
|
'structure_id': structureB.id,
|
|
'name': 'Payslip RUN B'
|
|
},
|
|
{
|
|
'date_start': datetime.date.today() + relativedelta(years=-1, month=3, day=1),
|
|
'date_end': datetime.date.today() + relativedelta(years=-1, month=3, day=31),
|
|
'structure_id': structureB.id,
|
|
'name': 'Payslip RUN C'
|
|
},
|
|
])
|
|
|
|
employee_timmy, employee_gerard, employee_michel = self.env['hr.employee'].create([
|
|
{
|
|
'name': 'Timmy',
|
|
'sex': 'male',
|
|
'birthday': '1984-05-01',
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=1, day=1),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=1, day=1),
|
|
'contract_date_end': datetime.date.today() + relativedelta(years=-1, month=3, day=15),
|
|
'wage': 5000.33,
|
|
'structure_type_id': structure_typeA.id,
|
|
},
|
|
{
|
|
'name': 'Gerard',
|
|
'sex': 'male',
|
|
'birthday': '1964-01-23',
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=1, day=1),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=1, day=1),
|
|
'wage': 5000.33,
|
|
'structure_type_id': structure_typeA.id,
|
|
},
|
|
{
|
|
'name': 'Michel',
|
|
'sex': 'male',
|
|
'birthday': '1975-10-06',
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=1, day=1),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=1, day=1),
|
|
'wage': 7000.33,
|
|
'structure_type_id': structure_typeB.id,
|
|
},
|
|
])
|
|
|
|
employee_timmy.create_version({
|
|
'date_version': datetime.date.today() + relativedelta(years=-1, month=3, day=16),
|
|
'contract_date_start': datetime.date.today() + relativedelta(years=-1, month=3, day=16),
|
|
'contract_date_end': datetime.date.today() + relativedelta(years=1, month=3, day=31),
|
|
'wage': 7000.33,
|
|
'employee_id': employee_timmy.id,
|
|
'structure_type_id': structure_typeB.id,
|
|
})
|
|
|
|
# Batch A for only structure A
|
|
# For february YEAR-1
|
|
# Expected employees/contracts
|
|
# Timmy contract A (YEAR-1/01/01 -> YEAR-1/03/15)
|
|
# Gerard contract A (YEAR-1/01/01 -> no end date )
|
|
payslip_runA.generate_payslips(payslip_runA._get_valid_version_ids())
|
|
self.assertEqual(len(payslip_runA.slip_ids.employee_id.ids), 2)
|
|
self.assertEqual(len(payslip_runA.slip_ids.ids), 2)
|
|
self.assertTrue(all(
|
|
payslip.version_id.structure_type_id == payslip.struct_id.type_id and
|
|
payslip.struct_id.type_id == payslip_runA.structure_id.type_id
|
|
for payslip in payslip_runA.slip_ids))
|
|
|
|
# Batch B for only structure B
|
|
# For april YEAR-1
|
|
# Expected employees/contracts
|
|
# Timmy contract B (YEAR-1/03/16 -> no end date)
|
|
# Michel contract B (YEAR-1/01/01 -> no end date)
|
|
payslip_runB.generate_payslips(payslip_runB._get_valid_version_ids())
|
|
self.assertEqual(len(payslip_runB.slip_ids.employee_id.ids), 2)
|
|
self.assertEqual(len(payslip_runB.slip_ids.ids), 2)
|
|
self.assertTrue(all(
|
|
payslip.version_id.structure_type_id == payslip.struct_id.type_id and
|
|
payslip.struct_id.type_id == payslip_runB.structure_id.type_id
|
|
for payslip in payslip_runB.slip_ids))
|
|
|
|
# Batch C for two structures
|
|
# For march YEAR-1
|
|
# Expected employees/contracts
|
|
# Timmy| contract A (YEAR-1/01/01 -> YEAR-1/03/15)
|
|
# | contract B (YEAR-1/03/16 -> no end date )
|
|
# Gerard contract A (YEAR-1/01/01 -> no end date )
|
|
# Michel contract B (YEAR-1/01/01 -> no end date )
|
|
payslip_runC.generate_payslips(payslip_runC._get_valid_version_ids())
|
|
self.assertEqual(len(payslip_runC.slip_ids.employee_id.ids), 2)
|
|
self.assertEqual(len(payslip_runC.slip_ids.ids), 2)
|
|
self.assertTrue(all(
|
|
payslip.version_id.structure_type_id == payslip.struct_id.type_id and
|
|
payslip.struct_id.type_id == payslip_runC.structure_id.type_id
|
|
for payslip in payslip_runC.slip_ids))
|
|
|
|
def test_09_payslip_creation_with_employee_without_contract(self):
|
|
employee = self.env['hr.employee'].create({
|
|
'name': 'Johnny',
|
|
})
|
|
payslip_form = Form(self.env['hr.payslip'])
|
|
payslip_form.employee_id = employee
|
|
payslip_form.save()
|
|
self.assertTrue(payslip_form)
|
|
|
|
def test_10_related_payslip_not_flagged_as_duplicate(self):
|
|
"""Refund/Correction payslips must NOT be treated as duplicates."""
|
|
|
|
original = self.env['hr.payslip'].create({
|
|
'name': 'Original Payslip',
|
|
'employee_id': self.richard_emp.id,
|
|
'struct_id': self.richard_emp.structure_type_id.default_struct_id.id,
|
|
'date_from': datetime.date(2025, 12, 1),
|
|
'date_to': datetime.date(2025, 12, 31),
|
|
})
|
|
original.compute_sheet()
|
|
original.action_payslip_done()
|
|
original.correct_sheet()
|
|
related = original.related_payslip_ids
|
|
refund = related.filtered(lambda p: p.is_refund_payslip)
|
|
correction = related - refund
|
|
|
|
warnings = refund._get_warnings_by_slip()[refund]
|
|
self.assertFalse(
|
|
any("Similar payslips found" in (w.get('message') or "") for w in warnings),
|
|
"Refund payslip incorrectly flagged as duplicate."
|
|
)
|
|
|
|
correction_warnings = correction._get_warnings_by_slip()[correction]
|
|
self.assertFalse(
|
|
any("Similar payslips found" in (w.get('message') or "") for w in correction_warnings),
|
|
"Correction payslip incorrectly flagged as duplicate."
|
|
)
|
|
|
|
duplicate = self.env['hr.payslip'].create({
|
|
'name': 'Actual Duplicate Payslip',
|
|
'employee_id': self.richard_emp.id,
|
|
'struct_id': original.struct_id.id,
|
|
'date_from': original.date_from,
|
|
'date_to': original.date_to,
|
|
})
|
|
duplicate_warnings = duplicate._get_warnings_by_slip()[duplicate]
|
|
self.assertTrue(
|
|
any("Similar payslips found" in (w.get('message') or "") for w in duplicate_warnings),
|
|
"Unrelated duplicate payslip should still trigger the warning."
|
|
)
|
|
|
|
def test_04_cancel_a_done_payslip_with_payroll_admin(self):
|
|
"""Cancel a validated payslip using a new user with Payroll Admin access."""
|
|
test_user = mail_new_test_user(
|
|
self.env, name="Test user", login="test_user",
|
|
groups="hr_payroll.group_hr_payroll_manager"
|
|
)
|
|
richard_payslip = self.env['hr.payslip'].create({
|
|
'name': 'Payslip of Richard',
|
|
'employee_id': self.richard_emp.id,
|
|
})
|
|
richard_payslip.action_payslip_done()
|
|
self.assertEqual(richard_payslip.state, 'validated')
|
|
richard_payslip.with_user(test_user).action_payslip_cancel()
|
|
self.assertEqual(richard_payslip.state, 'cancel')
|
|
|
|
def test_hr_payslip_without_date_to(self):
|
|
"""Ensure payslip creation fails if date_to is missing."""
|
|
employee = self.env['hr.employee'].create({
|
|
'name': 'Jethalal Gada',
|
|
'date_version': '2025-10-01',
|
|
'contract_date_start': '2025-10-01',
|
|
})
|
|
|
|
payslip_form = Form(self.env['hr.payslip'])
|
|
payslip_form.employee_id = employee
|
|
payslip_form.date_to = False
|
|
with self.assertRaises(AssertionError):
|
|
payslip_form.save()
|
|
self.assertTrue(payslip_form)
|
|
|
|
def test_05_fully_flexible_contracts_payslip(self):
|
|
""" Test payslip generation for fully flexible contracts (no working schedule) with attendance-based work entries """
|
|
|
|
if not self.env["ir.module.module"].search([("name", "=", "hr_work_entry_attendance"), ("state", "=", "installed")]):
|
|
self.skipTest("Module 'hr_work_entry_attendance' is not installed!")
|
|
|
|
attendance_work_entry_type = self.env.ref('hr_work_entry.work_entry_type_attendance')
|
|
|
|
# Case 1: contract with no calendar
|
|
date_from = datetime.date.today()
|
|
date_to = date_from + relativedelta(months=1)
|
|
|
|
employee_with_calendar = self.env['hr.employee'].create({
|
|
'name': 'employee 1',
|
|
'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
|
|
'work_entry_source': 'attendance',
|
|
'wage': 5000,
|
|
'structure_type_id': self.structure_type.id,
|
|
'date_version': date_from - relativedelta(months=2),
|
|
'contract_date_start': date_from - relativedelta(months=2),
|
|
})
|
|
|
|
flexible_contract_1 = employee_with_calendar.version_id
|
|
flexible_contract_1.resource_calendar_id = False
|
|
|
|
for day in range(7):
|
|
work_date = date_from + relativedelta(days=day)
|
|
if work_date.weekday() < 5:
|
|
self.env['hr.work.entry'].create({
|
|
'name': f'attendance {day + 1}',
|
|
'employee_id': employee_with_calendar.id,
|
|
'version_id': flexible_contract_1.id,
|
|
'work_entry_type_id': attendance_work_entry_type.id,
|
|
'date': work_date,
|
|
'duration': 8,
|
|
})
|
|
|
|
payslip_1 = self.env['hr.payslip'].create({
|
|
'name': "payslip of employee 1",
|
|
'employee_id': employee_with_calendar.id,
|
|
'date_from': date_from,
|
|
'date_to': date_to,
|
|
})
|
|
|
|
payslip_1.compute_sheet()
|
|
|
|
self.assertTrue(payslip_1.worked_days_line_ids, 'worked days should be generated for fully flexible contract')
|
|
attendance_line_1 = payslip_1.worked_days_line_ids.filtered(lambda l: l.work_entry_type_id == attendance_work_entry_type)
|
|
self.assertTrue(attendance_line_1, 'attendance worked days should be present')
|
|
self.assertEqual(attendance_line_1.number_of_days, 5, 'payslip should record 5 worked days')
|
|
self.assertEqual(attendance_line_1.number_of_hours, 40, 'payslip should record 40 hours')
|
|
|
|
# Case 2: employee with no calendar, contract with no calendar (full flexibility in both)
|
|
employee_no_calendar = self.env['hr.employee'].create({
|
|
'name': 'employee 2',
|
|
'resource_calendar_id': False,
|
|
'work_entry_source': 'attendance',
|
|
'wage': 6000,
|
|
'structure_type_id': self.structure_type.id,
|
|
'date_version': date_from - relativedelta(months=2),
|
|
'contract_date_start': date_from - relativedelta(months=2),
|
|
})
|
|
|
|
flexible_contract_2 = employee_no_calendar.version_id
|
|
|
|
for day in range(7):
|
|
work_date = date_from + relativedelta(days=day + 10)
|
|
if work_date.weekday() < 5:
|
|
self.env['hr.work.entry'].create({
|
|
'name': f'Attendance {day + 1}',
|
|
'employee_id': employee_no_calendar.id,
|
|
'version_id': flexible_contract_2.id,
|
|
'work_entry_type_id': attendance_work_entry_type.id,
|
|
'date': work_date,
|
|
'duration': 8,
|
|
})
|
|
|
|
payslip_2 = self.env['hr.payslip'].create({
|
|
'name': 'payslip 2',
|
|
'employee_id': employee_no_calendar.id,
|
|
'date_from': date_from,
|
|
'date_to': date_to,
|
|
})
|
|
|
|
payslip_2.compute_sheet()
|
|
|
|
self.assertTrue(payslip_2.worked_days_line_ids, 'worked days should be generated for fully flexible contract')
|
|
attendance_line_2 = payslip_2.worked_days_line_ids.filtered(lambda l: l.work_entry_type_id == attendance_work_entry_type)
|
|
self.assertTrue(attendance_line_2, 'attendance worked days should be present')
|
|
self.assertGreater(attendance_line_2.number_of_hours, 0, 'payslip record attendance hours')
|
|
|
|
def test_06_pay_run_payslip_name(self):
|
|
"""
|
|
This test checks that the name of the payslip contains the name and the period for which the pay run is
|
|
being run.
|
|
"""
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_end': '2025-11-30',
|
|
'date_start': '2025-11-01',
|
|
'name': 'Payslip for Employee'
|
|
})
|
|
payslip_run.generate_payslips(employee_ids=[self.richard_emp.id])
|
|
self.assertEqual(payslip_run.slip_ids.name, 'Salary Slip - Richard - November 2025')
|
|
|
|
def test_10_translation_title_multiple_employee_payslip(self):
|
|
self.env['res.lang']._activate_lang('it_IT')
|
|
|
|
self.richard_emp.write({
|
|
'contract_date_start': datetime.date(2025, 1, 1),
|
|
'contract_date_end': datetime.date.today() + relativedelta(years=2),
|
|
'wage': 6000.33,
|
|
'lang': 'en_US',
|
|
})
|
|
self.jules_emp.write({
|
|
'contract_date_start': datetime.date(2025, 1, 1),
|
|
'contract_date_end': datetime.date.today() + relativedelta(years=2),
|
|
'wage': 6000.33,
|
|
'lang': 'it_IT',
|
|
})
|
|
payslip_run = self.env['hr.payslip.run'].create({
|
|
'date_start': datetime.date(2026, 8, 1),
|
|
'date_end': datetime.date(2026, 8, 31),
|
|
'name': 'Payment Test',
|
|
})
|
|
|
|
payslip_run.generate_payslips(employee_ids=[self.richard_emp.id, self.jules_emp.id])
|
|
richard_slip = payslip_run.slip_ids.filtered(lambda s: s.employee_id == self.richard_emp)
|
|
jules_slip = payslip_run.slip_ids.filtered(lambda s: s.employee_id == self.jules_emp)
|
|
richard_slip.ensure_one()
|
|
jules_slip.ensure_one()
|
|
|
|
self.assertEqual(richard_slip.name, 'Salary Slip - Richard - August 2026')
|
|
self.assertEqual(jules_slip.name, 'Cedolino retribuzione - Jules - agosto 2026')
|
|
|
|
def test_payslip_duplicate_warning_ignores_different_contract(self):
|
|
"""
|
|
Test that the duplicate payslip warning does not appear when the existing payslips for the period
|
|
are linked to a different contract than the one on the payslip being computed.
|
|
"""
|
|
structure_type = self.env['hr.payroll.structure.type'].create({
|
|
'name': 'Test Type',
|
|
})
|
|
employee = self.env['hr.employee'].create({
|
|
'name': 'Test Employee',
|
|
})
|
|
contract1 = self.env['hr.version'].create({
|
|
'employee_id': employee.id,
|
|
'structure_type_id': structure_type.id,
|
|
'date_version': datetime.date(2025, 3, 1),
|
|
'contract_date_start': datetime.date(2025, 3, 1),
|
|
'contract_date_end': datetime.date(2025, 3, 15),
|
|
'wage': 1000.0,
|
|
})
|
|
contract2 = self.env['hr.version'].create({
|
|
'employee_id': employee.id,
|
|
'structure_type_id': structure_type.id,
|
|
'date_version': datetime.date(2025, 3, 16),
|
|
'contract_date_start': datetime.date(2025, 3, 16),
|
|
'wage': 1000.0,
|
|
})
|
|
|
|
payslip_with_contract_1 = self.env['hr.payslip'].create({
|
|
'name': 'Payslip March Contract 1',
|
|
'employee_id': employee.id,
|
|
'struct_id': self.structure_type.default_struct_id.id,
|
|
'date_from': datetime.date(2025, 3, 1),
|
|
'date_to': datetime.date(2025, 3, 31),
|
|
'version_id': contract1.id,
|
|
})
|
|
payslip_with_contract_1.compute_sheet()
|
|
payslip_with_contract_1.action_payslip_done()
|
|
|
|
payslip_with_contract_2 = self.env['hr.payslip'].create({
|
|
'name': 'Payslip March Contract 2',
|
|
'employee_id': employee.id,
|
|
'struct_id': self.structure_type.default_struct_id.id,
|
|
'date_from': datetime.date(2025, 3, 1),
|
|
'date_to': datetime.date(2025, 3, 31),
|
|
'version_id': contract2.id,
|
|
})
|
|
payslip_with_contract_2.compute_sheet()
|
|
|
|
warning_messages = [
|
|
warning.get('message') or ''
|
|
for warning in payslip_with_contract_2._get_warnings_by_slip()[payslip_with_contract_2]
|
|
]
|
|
self.assertFalse(
|
|
any("Similar payslips found" in msg for msg in warning_messages),
|
|
f"Warning incorrectly appeared. Found messages: {warning_messages}"
|
|
)
|
|
payslip_with_contract_2.action_payslip_done()
|
|
|
|
# Payslips linked to the same contract should still trigger the warning
|
|
payslip2_with_contract2 = self.env['hr.payslip'].create({
|
|
'name': 'Payslip 2 March Contract 2',
|
|
'employee_id': employee.id,
|
|
'struct_id': self.structure_type.default_struct_id.id,
|
|
'date_from': datetime.date(2025, 3, 1),
|
|
'date_to': datetime.date(2025, 3, 31),
|
|
'version_id': contract2.id,
|
|
})
|
|
payslip2_with_contract2.compute_sheet()
|
|
|
|
warning_messages = [
|
|
warning.get('message') or ''
|
|
for warning in payslip2_with_contract2._get_warnings_by_slip()[payslip2_with_contract2]
|
|
]
|
|
self.assertTrue(
|
|
any("Similar payslips found" in msg for msg in warning_messages),
|
|
f"Warning did NOT appear. Found messages: {warning_messages}"
|
|
)
|
|
|
|
|
|
@tagged('-at_install', 'post_install')
|
|
class TestPayslipUi(HttpCase):
|
|
def test_tour_date_input(self):
|
|
"""Test payslip form date input."""
|
|
self.start_tour("/odoo", 'hr_payroll_form_view_date_input_tour', login='admin')
|