This commit is contained in:
parent
57fc66b6bc
commit
7f4b8910d6
|
|
@ -5,3 +5,4 @@ from . import test_signin_signout_constraints
|
|||
from . import test_flexible_hours_computation
|
||||
from . import test_calendar_update_flow
|
||||
from . import test_attendance_report_calculations
|
||||
from . import test_attendance_transaction_timing
|
||||
|
|
|
|||
|
|
@ -163,3 +163,192 @@ class TestFlexibleAttendanceReport(TransactionCase):
|
|||
|
||||
self.assertAlmostEqual(result['missing_punch_hours'], 4.0, msg="Missing punch hours failed")
|
||||
|
||||
|
||||
|
||||
class TestHrAttendanceReportApproved(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestHrAttendanceReportApproved, self).setUp()
|
||||
|
||||
self.manager_user = self.env['hr.employee'].create({
|
||||
'name': 'Manager',
|
||||
'finger_print': False
|
||||
})
|
||||
self.dept = self.env['hr.department'].create({
|
||||
'name': 'IT',
|
||||
'manager_id': self.manager_user.id
|
||||
})
|
||||
|
||||
self.calendar = self.env['resource.calendar'].create({
|
||||
'name': 'Test Calendar',
|
||||
'is_full_day': True,
|
||||
'full_min_sign_in': 8.0,
|
||||
'full_max_sign_in': 9.0,
|
||||
'full_min_sign_out': 16.0,
|
||||
'full_max_sign_out': 17.0,
|
||||
'working_hours': 8.0,
|
||||
'working_days': 5,
|
||||
'state': 'confirm',
|
||||
})
|
||||
|
||||
self.salary_category = self.env['hr.salary.rule.category'].create({
|
||||
'name': 'Test Deductions',
|
||||
'code': 'TEST_DED_CAT'
|
||||
})
|
||||
|
||||
self.deduction_rule = self.env['hr.salary.rule'].create({
|
||||
'name': 'Test Deduction Rule',
|
||||
'code': 'TEST_DED',
|
||||
'category_id': self.salary_category.id,
|
||||
})
|
||||
self.calendar.deduction_rule = self.deduction_rule
|
||||
|
||||
self.employee = self.env['hr.employee'].create({
|
||||
'name': 'Ahmed Ali',
|
||||
'department_id': self.dept.id,
|
||||
'finger_print': True,
|
||||
'state': 'open',
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
|
||||
self.contract = self.env['hr.contract'].create({
|
||||
'name': 'Test Contract',
|
||||
'employee_id': self.employee.id,
|
||||
'total_allowance': 4000.0,
|
||||
'state': 'program_directory',
|
||||
'date_start': date(2023, 1, 1),
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
self.employee.contract_id = self.contract
|
||||
|
||||
self.create_attendance_transactions()
|
||||
self.report = self.env['hr.attendance.report'].create({
|
||||
'name': 'Test Approved Report',
|
||||
'date_from': date(2023, 10, 1),
|
||||
'date_to': date(2023, 10, 5),
|
||||
'deduct_date_from': date(2023, 10, 1),
|
||||
'deduct_date_to': date(2023, 10, 31),
|
||||
'selected_employee_ids': [(4, self.employee.id)]
|
||||
})
|
||||
|
||||
def create_attendance_transactions(self):
|
||||
self.env['hr.attendance.transaction'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar.id,
|
||||
'attending_type': 'in_cal',
|
||||
'sequence': 1,
|
||||
'date': date(2023, 10, 1),
|
||||
'plan_hours': 8.0,
|
||||
'is_absent': True
|
||||
})
|
||||
|
||||
def test_01_approved_creates_advantage_record(self):
|
||||
self.report.generate_report()
|
||||
line = self.report.line_ids[0]
|
||||
|
||||
self.assertGreater(line.total_deduction, 0.0, "Must have deduction")
|
||||
self.assertFalse(line.advantage_id, "No advantage before approved")
|
||||
|
||||
self.report.approved()
|
||||
|
||||
self.assertEqual(self.report.state, 'approved', "State must be approved")
|
||||
self.assertTrue(line.advantage_id, "Advantage record must be created")
|
||||
|
||||
advantage = line.advantage_id
|
||||
|
||||
self.assertEqual(advantage.employee_id, self.employee)
|
||||
self.assertEqual(advantage.amount, line.total_deduction)
|
||||
self.assertEqual(advantage.benefits_discounts.id, self.deduction_rule.id)
|
||||
self.assertEqual(advantage.contract_advantage_id, self.contract)
|
||||
self.assertEqual(advantage.date_from, self.report.deduct_date_from)
|
||||
self.assertEqual(advantage.date_to, self.report.deduct_date_to)
|
||||
self.assertEqual(advantage.state, 'confirm')
|
||||
self.assertEqual(advantage.comments, 'Absence Deduction')
|
||||
self.assertTrue(advantage.out_rule)
|
||||
|
||||
def test_02_approved_no_contract_no_advantage(self):
|
||||
employee_no_contract = self.env['hr.employee'].create({
|
||||
'name': 'No Contract Employee',
|
||||
'finger_print': True,
|
||||
'state': 'open',
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
|
||||
self.env['hr.attendance.transaction'].create({
|
||||
'employee_id': employee_no_contract.id,
|
||||
'calendar_id': self.calendar.id,
|
||||
'attending_type': 'in_cal',
|
||||
'sequence': 1,
|
||||
'date': date(2023, 10, 1),
|
||||
'plan_hours': 8.0,
|
||||
'is_absent': True
|
||||
})
|
||||
|
||||
report_no_contract = self.env['hr.attendance.report'].create({
|
||||
'name': 'No Contract Report',
|
||||
'date_from': date(2023, 10, 1),
|
||||
'date_to': date(2023, 10, 5),
|
||||
'deduct_date_from': date(2023, 10, 1),
|
||||
'deduct_date_to': date(2023, 10, 31),
|
||||
'selected_employee_ids': [(4, employee_no_contract.id)]
|
||||
})
|
||||
|
||||
report_no_contract.generate_report()
|
||||
line = report_no_contract.line_ids[0]
|
||||
|
||||
report_no_contract.approved()
|
||||
|
||||
self.assertFalse(line.advantage_id, "No advantage without contract")
|
||||
|
||||
def test_03_approved_no_fingerprint_no_advantage(self):
|
||||
self.employee.finger_print = False
|
||||
|
||||
self.report.generate_report()
|
||||
line = self.report.line_ids[0]
|
||||
|
||||
self.report.approved()
|
||||
|
||||
self.assertFalse(line.advantage_id, "No advantage without fingerprint")
|
||||
|
||||
def test_04_multiple_lines_multiple_advantages(self):
|
||||
employee2 = self.env['hr.employee'].create({
|
||||
'name': 'Second Employee',
|
||||
'finger_print': True,
|
||||
'state': 'open',
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
'department_id': self.dept.id
|
||||
})
|
||||
|
||||
contract2 = self.env['hr.contract'].create({
|
||||
'name': 'Second Contract',
|
||||
'employee_id': employee2.id,
|
||||
'total_allowance': 3000.0,
|
||||
'state': 'program_directory',
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
employee2.contract_id = contract2
|
||||
|
||||
self.env['hr.attendance.transaction'].create({
|
||||
'employee_id': employee2.id,
|
||||
'calendar_id': self.calendar.id,
|
||||
'attending_type': 'in_cal',
|
||||
'sequence': 1,
|
||||
'date': date(2023, 10, 2),
|
||||
'plan_hours': 8.0,
|
||||
'is_absent': True
|
||||
})
|
||||
|
||||
self.report.write({'selected_employee_ids': [(4, employee2.id)]})
|
||||
self.report.generate_report()
|
||||
|
||||
self.assertFalse(self.report.line_ids[0].advantage_id)
|
||||
self.assertFalse(self.report.line_ids[1].advantage_id)
|
||||
|
||||
self.report.approved()
|
||||
|
||||
self.assertTrue(self.report.line_ids[0].advantage_id)
|
||||
self.assertTrue(self.report.line_ids[1].advantage_id)
|
||||
self.assertNotEqual(
|
||||
self.report.line_ids[0].advantage_id.id,
|
||||
self.report.line_ids[1].advantage_id.id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
from odoo.tests.common import TransactionCase
|
||||
from datetime import date
|
||||
|
||||
from odoo import fields
|
||||
|
||||
|
||||
class TestAttendanceTransactionTiming(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAttendanceTransactionTiming, self).setUp()
|
||||
|
||||
self.employee = self.env['hr.employee'].create({'name': 'Test Employee'})
|
||||
self.calendar_full = self.env['resource.calendar'].create({
|
||||
'name': 'Full Day Calendar',
|
||||
'is_full_day': True,
|
||||
'full_min_sign_in': 8.0,
|
||||
'full_max_sign_in': 9.0,
|
||||
'full_min_sign_out': 16.0,
|
||||
'full_max_sign_out': 17.0,
|
||||
'working_hours': 8.0,
|
||||
'state': 'confirm',
|
||||
})
|
||||
self.calendar_shift1 = self.env['resource.calendar'].create({
|
||||
'name': 'Shift 1 Calendar',
|
||||
'is_full_day': False,
|
||||
'shift_one_min_sign_in': 7.0,
|
||||
'shift_one_max_sign_in': 8.0,
|
||||
'shift_one_min_sign_out': 15.0,
|
||||
'shift_one_max_sign_out': 16.0,
|
||||
'shift_one_working_hours': 8.0,
|
||||
'state': 'confirm',
|
||||
})
|
||||
self.employee.resource_calendar_id = self.calendar_full
|
||||
|
||||
def test_scenario1_lateness_full_day(self):
|
||||
transaction = self.env['hr.attendance.transaction'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar_full.id,
|
||||
'date': date(2025, 1, 1),
|
||||
'sign_in': 9.5,
|
||||
'sign_out': 17.0,
|
||||
'sequence': 1,
|
||||
})
|
||||
|
||||
transaction.set_lateness_and_exit(transaction)
|
||||
|
||||
self.assertEqual(transaction.lateness, 0.5)
|
||||
self.assertTrue(transaction.approve_lateness)
|
||||
self.assertFalse(transaction.approve_exit_out)
|
||||
|
||||
def test_scenario2_early_exit_shift1(self):
|
||||
transaction = self.env['hr.attendance.transaction'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar_shift1.id,
|
||||
'date': date(2025, 1, 2),
|
||||
'sign_in': 7.5,
|
||||
'sign_out': 14.0,
|
||||
'sequence': 1,
|
||||
})
|
||||
|
||||
transaction.set_lateness_and_exit(transaction)
|
||||
|
||||
self.assertEqual(transaction.early_exit, 1.5)
|
||||
self.assertTrue(transaction.approve_exit_out)
|
||||
self.assertFalse(transaction.approve_lateness)
|
||||
|
||||
|
||||
|
||||
class TestAttendanceAdditionalHours(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAttendanceAdditionalHours, self).setUp()
|
||||
self.employee = self.env['hr.employee'].sudo().create({'name': 'Test Employee'})
|
||||
self.calendar = self.env['resource.calendar'].sudo().create({
|
||||
'name': 'Test Calendar',
|
||||
'working_hours': 0.0,
|
||||
'working_days': 5,
|
||||
'state': 'confirm',
|
||||
})
|
||||
|
||||
def test_overtime_calculation(self):
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar.id,
|
||||
'plan_hours': 8.0,
|
||||
'office_hours': 10.0,
|
||||
'sign_in': 8.0,
|
||||
})
|
||||
|
||||
transaction.get_additional_hours()
|
||||
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().browse(transaction.id)
|
||||
|
||||
expected = 2.0
|
||||
self.assertEqual(transaction.additional_hours, expected)
|
||||
|
||||
def test_no_overtime(self):
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar.id,
|
||||
'plan_hours': 8.0,
|
||||
'office_hours': 7.0,
|
||||
})
|
||||
|
||||
transaction.get_additional_hours()
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().browse(transaction.id)
|
||||
|
||||
self.assertEqual(transaction.additional_hours, 0.0)
|
||||
|
||||
def test_mission_hours(self):
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar.id,
|
||||
'sign_in': False,
|
||||
'plan_hours': 8.0,
|
||||
'total_mission_hours': 9.0,
|
||||
})
|
||||
|
||||
transaction.get_additional_hours()
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().browse(transaction.id)
|
||||
|
||||
self.assertEqual(transaction.office_hours, 9.0)
|
||||
self.assertEqual(transaction.official_hours, 9.0)
|
||||
self.assertEqual(transaction.additional_hours, 1.0)
|
||||
|
||||
def test_sign_in_zero_mission(self):
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar.id,
|
||||
'sign_in': 0.0,
|
||||
'plan_hours': 8.0,
|
||||
'office_hours': 6.0,
|
||||
})
|
||||
|
||||
transaction.get_additional_hours()
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().browse(transaction.id)
|
||||
|
||||
self.assertEqual(transaction.additional_hours, 0.0)
|
||||
|
||||
|
||||
class TestDayTimingCalculation(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDayTimingCalculation, self).setUp()
|
||||
self.employee = self.env['hr.employee'].sudo().create({'name': 'Test Employee'})
|
||||
|
||||
self.calendar_full = self.env['resource.calendar'].sudo().create({
|
||||
'name': 'Full Day Calendar',
|
||||
'working_hours': 8.0,
|
||||
'break_duration': 1.0,
|
||||
'full_min_sign_in': 8.0,
|
||||
'full_max_sign_in': 9.0,
|
||||
'full_min_sign_out': 16.0,
|
||||
'full_max_sign_out': 17.0,
|
||||
'is_full_day': True,
|
||||
})
|
||||
|
||||
self.calendar_shift = self.env['resource.calendar'].sudo().create({
|
||||
'name': 'Shift Calendar',
|
||||
'shift_one_working_hours': 4.0,
|
||||
'shift_one_break_duration': 0.5,
|
||||
'shift_one_min_sign_in': 6.0,
|
||||
'shift_one_max_sign_in': 7.0,
|
||||
'shift_one_min_sign_out': 10.0,
|
||||
'shift_one_max_sign_out': 11.0,
|
||||
'shift_two_working_hours': 4.0,
|
||||
'shift_two_break_duration': 0.5,
|
||||
'shift_two_min_sign_in': 14.0,
|
||||
'shift_two_max_sign_in': 15.0,
|
||||
'shift_two_min_sign_out': 18.0,
|
||||
'shift_two_max_sign_out': 19.0,
|
||||
'is_full_day': False,
|
||||
})
|
||||
|
||||
def test_full_day_normal_timing(self):
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar_full.id,
|
||||
})
|
||||
result = transaction.get_day_timing(self.calendar_full, 'monday', '2025-12-22')
|
||||
time_list, planed_hours = result
|
||||
self.assertEqual(planed_hours, {'one': 7.0, 'two': 0})
|
||||
self.assertEqual(time_list, [8.0, 9.0, 16.0, 17.0])
|
||||
|
||||
def test_full_day_special_timing(self):
|
||||
special_day = self.env['attendance.special.days'].sudo().create({
|
||||
'special_days_attendance': self.calendar_full.id,
|
||||
'name': 'monday',
|
||||
'working_hours': 6.0,
|
||||
'start_sign_in': 7.0,
|
||||
'end_sign_in': 8.0,
|
||||
'start_sign_out': 13.0,
|
||||
'end_sign_out': 14.0,
|
||||
'date_from': fields.Date.to_date('2025-12-22'),
|
||||
})
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar_full.id,
|
||||
})
|
||||
result = transaction.get_day_timing(self.calendar_full, 'monday', '2025-12-22')
|
||||
_, planed_hours = result
|
||||
self.assertEqual(planed_hours, {'one': 5.0, 'two': 0})
|
||||
|
||||
def test_shift_special_one_timing(self):
|
||||
special_day_shift1 = self.env['attendance.special.days'].sudo().create({
|
||||
'special_days_attendance': self.calendar_shift.id,
|
||||
'name': 'monday',
|
||||
'shift': 'one',
|
||||
'working_hours': 5.0,
|
||||
'start_sign_in': 5.0,
|
||||
'end_sign_in': 6.0,
|
||||
'start_sign_out': 10.0,
|
||||
'end_sign_out': 11.0,
|
||||
'date_from': fields.Date.to_date('2025-12-22'),
|
||||
})
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar_shift.id,
|
||||
})
|
||||
result = transaction.get_day_timing(self.calendar_shift, 'monday', '2025-12-22')
|
||||
_, planed_hours = result
|
||||
self.assertEqual(planed_hours, {'one': 4.5, 'two': 3.5})
|
||||
|
||||
def test_special_day_date_range(self):
|
||||
special_day = self.env['attendance.special.days'].sudo().create({
|
||||
'special_days_attendance': self.calendar_full.id,
|
||||
'name': 'tuesday',
|
||||
'working_hours': 10.0,
|
||||
'date_from': fields.Date.to_date('2025-12-23'),
|
||||
'date_to': fields.Date.to_date('2025-12-25'),
|
||||
})
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar_full.id,
|
||||
})
|
||||
result_inside = transaction.get_day_timing(self.calendar_full, 'tuesday', '2025-12-23')
|
||||
_, planned_inside = result_inside
|
||||
self.assertEqual(planned_inside['one'], 9.0)
|
||||
|
||||
def test_special_day_no_dates(self):
|
||||
special_day = self.env['attendance.special.days'].sudo().create({
|
||||
'special_days_attendance': self.calendar_full.id,
|
||||
'name': 'wednesday',
|
||||
'working_hours': 12.0,
|
||||
})
|
||||
transaction = self.env['hr.attendance.transaction'].sudo().create({
|
||||
'employee_id': self.employee.id,
|
||||
'calendar_id': self.calendar_full.id,
|
||||
})
|
||||
result = transaction.get_day_timing(self.calendar_full, 'wednesday', '2025-12-24')
|
||||
_, planned_hours = result
|
||||
self.assertEqual(planned_hours['one'], 11.0)
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import test_employee_service_duration
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
from odoo.tests.common import TransactionCase
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
class TestEmployeeServiceDuration(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEmployeeServiceDuration, self).setUp()
|
||||
self.employee = self.env['hr.employee'].create({
|
||||
'name': 'Test Employee',
|
||||
})
|
||||
|
||||
def test_service_duration_current(self):
|
||||
self.employee.first_hiring_date = date(2023, 1, 1)
|
||||
self.employee._compute_service_duration()
|
||||
|
||||
today = date.today()
|
||||
expected = relativedelta(today, date(2023, 1, 1))
|
||||
|
||||
self.assertEqual(self.employee.service_year, expected.years)
|
||||
self.assertEqual(self.employee.service_month, expected.months)
|
||||
self.assertEqual(self.employee.service_day, expected.days)
|
||||
|
||||
def test_service_duration_leaving_date(self):
|
||||
hire_date = date(2023, 1, 1)
|
||||
leave_date = date(2024, 6, 15)
|
||||
|
||||
self.employee.first_hiring_date = hire_date
|
||||
self.employee.leaving_date = leave_date
|
||||
self.employee._compute_service_duration()
|
||||
|
||||
expected = relativedelta(leave_date, hire_date)
|
||||
|
||||
self.assertEqual(self.employee.service_year, expected.years)
|
||||
self.assertEqual(self.employee.service_month, expected.months)
|
||||
self.assertEqual(self.employee.service_day, expected.days)
|
||||
|
||||
def test_no_hiring_date(self):
|
||||
self.employee.first_hiring_date = False
|
||||
self.employee.leaving_date = False
|
||||
self.employee._compute_service_duration()
|
||||
|
||||
self.assertEqual(self.employee.service_year, 0)
|
||||
self.assertEqual(self.employee.service_month, 0)
|
||||
self.assertEqual(self.employee.service_day, 0)
|
||||
|
||||
|
||||
|
||||
class TestEmployeeUniqueConstraints(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestEmployeeUniqueConstraints, self).setUp()
|
||||
self.employee = self.env['hr.employee'].create({
|
||||
'name': 'Test Employee',
|
||||
'emp_no': 'EMP001',
|
||||
})
|
||||
|
||||
def test_duplicate_emp_no(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['hr.employee'].create({
|
||||
'name': 'Duplicate Employee',
|
||||
'emp_no': 'EMP001',
|
||||
})
|
||||
|
||||
def test_future_birthday(self):
|
||||
with self.assertRaises(UserError):
|
||||
self.env['hr.employee'].create({
|
||||
'name': 'Future Birthday',
|
||||
'birthday': date(2026, 1, 1),
|
||||
})
|
||||
|
||||
def test_valid_birthday(self):
|
||||
employee = self.env['hr.employee'].create({
|
||||
'name': 'Valid Birthday',
|
||||
'birthday': date(1990, 1, 1),
|
||||
})
|
||||
self.assertEqual(employee.birthday, date(1990, 1, 1))
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import test_holidays_overlap_constraint
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class TestHolidaysOverlapConstraint(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestHolidaysOverlapConstraint, self).setUp()
|
||||
self.employee = self.env['hr.employee'].sudo().create({'name': 'Test Employee'})
|
||||
self.leave_type = self.env['hr.holidays.status'].sudo().create({
|
||||
'name': 'Annual Leave',
|
||||
})
|
||||
|
||||
def test_overlapping_leaves_forbidden(self):
|
||||
self.env['hr.holidays'].sudo().create({
|
||||
'name': 'First Leave',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 10, 9, 0),
|
||||
'date_to': datetime(2025, 1, 12, 18, 0),
|
||||
'type': 'remove',
|
||||
'state': 'confirm', # ليست cancel/refuse
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError, msg="Overlapping leaves should be forbidden"):
|
||||
self.env['hr.holidays'].sudo().create({
|
||||
'name': 'Overlapping Leave',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 11, 9, 0),
|
||||
'date_to': datetime(2025, 1, 13, 18, 0),
|
||||
'type': 'remove',
|
||||
'state': 'confirm',
|
||||
})
|
||||
|
||||
def test_non_overlapping_allowed(self):
|
||||
self.env['hr.holidays'].sudo().create({
|
||||
'name': 'First Leave',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 10, 9, 0),
|
||||
'date_to': datetime(2025, 1, 12, 18, 0),
|
||||
'type': 'remove',
|
||||
'state': 'confirm',
|
||||
})
|
||||
|
||||
leave2 = self.env['hr.holidays'].sudo().create({
|
||||
'name': 'Second Leave',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 13, 9, 0),
|
||||
'date_to': datetime(2025, 1, 15, 18, 0),
|
||||
'type': 'remove',
|
||||
'state': 'confirm',
|
||||
})
|
||||
self.assertTrue(leave2)
|
||||
|
||||
def test_cancelled_leave_ignored(self):
|
||||
self.env['hr.holidays'].sudo().create({
|
||||
'name': 'Cancelled Leave',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 10, 9, 0),
|
||||
'date_to': datetime(2025, 1, 12, 18, 0),
|
||||
'type': 'remove',
|
||||
'state': 'cancel',
|
||||
})
|
||||
|
||||
leave2 = self.env['hr.holidays'].sudo().create({
|
||||
'name': 'New Leave',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 11, 9, 0),
|
||||
'date_to': datetime(2025, 1, 13, 18, 0),
|
||||
'type': 'remove',
|
||||
'state': 'confirm',
|
||||
})
|
||||
self.assertTrue(leave2)
|
||||
|
||||
def test_different_type_allowed(self):
|
||||
self.env['hr.holidays'].sudo().create({
|
||||
'name': 'Leave Request',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 10, 9, 0),
|
||||
'date_to': datetime(2025, 1, 12, 18, 0),
|
||||
'type': 'remove',
|
||||
'state': 'confirm',
|
||||
})
|
||||
|
||||
allocation = self.env['hr.holidays'].sudo().create({
|
||||
'name': 'Allocation',
|
||||
'employee_id': self.employee.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': datetime(2025, 1, 11, 9, 0),
|
||||
'date_to': datetime(2025, 1, 13, 18, 0),
|
||||
'type': 'add',
|
||||
'state': 'confirm',
|
||||
})
|
||||
self.assertTrue(allocation)
|
||||
|
||||
|
||||
|
||||
|
||||
class TestHolidaysNumberOfDays(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestHolidaysNumberOfDays, self).setUp()
|
||||
self.leave_type = self.env['hr.holidays.status'].sudo().create({
|
||||
'name': 'Annual Leave',
|
||||
})
|
||||
|
||||
self.calendar = self.env['resource.calendar'].sudo().create({
|
||||
'name': 'Standard 40h/week',
|
||||
'tz': 'UTC',
|
||||
'attendance_ids': [
|
||||
(0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 9.0, 'hour_to': 18.0}),
|
||||
(0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 9.0, 'hour_to': 18.0}),
|
||||
(0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 9.0, 'hour_to': 18.0}),
|
||||
(0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 9.0, 'hour_to': 18.0}),
|
||||
(0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 9.0, 'hour_to': 18.0}),
|
||||
]
|
||||
})
|
||||
|
||||
self.employee = self.env['hr.employee'].sudo().create({
|
||||
'name': 'Test Employee',
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
|
||||
def _calculate_work_days(self, date_from, date_to, calendar):
|
||||
total_hours = 0.0
|
||||
current_date = date_from.date()
|
||||
end_date = date_to.date()
|
||||
|
||||
while current_date <= end_date:
|
||||
dayofweek = str(current_date.weekday())
|
||||
attendances = calendar.attendance_ids.filtered(
|
||||
lambda a: a.dayofweek == dayofweek
|
||||
)
|
||||
|
||||
if attendances:
|
||||
hour_from_int = int(attendances[0].hour_from)
|
||||
minute_from_int = int((attendances[0].hour_from % 1) * 60)
|
||||
hour_to_int = int(attendances[0].hour_to)
|
||||
minute_to_int = int((attendances[0].hour_to % 1) * 60)
|
||||
|
||||
day_start = datetime.combine(current_date, datetime.min.time()).replace(
|
||||
hour=hour_from_int, minute=minute_from_int)
|
||||
day_end = datetime.combine(current_date, datetime.min.time()).replace(
|
||||
hour=hour_to_int, minute=minute_to_int)
|
||||
|
||||
work_start = max(date_from, day_start)
|
||||
work_end = min(date_to, day_end)
|
||||
|
||||
if work_start < work_end:
|
||||
hours = (work_end - work_start).total_seconds() / 3600.0
|
||||
total_hours += min(hours, 8.0)
|
||||
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return total_hours / 8.0
|
||||
|
||||
def test_simple_datetime_diff(self):
|
||||
holiday = self.env['hr.holidays'].sudo().new()
|
||||
days = holiday._get_number_of_days(
|
||||
datetime(2025, 1, 1, 9, 0),
|
||||
datetime(2025, 1, 5, 18, 0),
|
||||
False
|
||||
)
|
||||
self.assertEqual(days, 5.0)
|
||||
|
||||
def test_string_input(self):
|
||||
holiday = self.env['hr.holidays'].sudo().new()
|
||||
days = holiday._get_number_of_days(
|
||||
'2025-01-01T09:00:00',
|
||||
'2025-01-05T18:00:00',
|
||||
False
|
||||
)
|
||||
self.assertEqual(days, 5.0)
|
||||
|
||||
def test_datetime_input(self):
|
||||
holiday = self.env['hr.holidays'].sudo().new()
|
||||
days = holiday._get_number_of_days(
|
||||
datetime(2025, 1, 1, 9, 0),
|
||||
datetime(2025, 1, 3, 18, 0),
|
||||
False
|
||||
)
|
||||
self.assertEqual(days, 3.0)
|
||||
|
||||
def test_string_datetime_mixed(self):
|
||||
holiday = self.env['hr.holidays'].sudo().new()
|
||||
days = holiday._get_number_of_days(
|
||||
'2025-01-01T09:00:00',
|
||||
datetime(2025, 1, 2, 18, 0),
|
||||
False
|
||||
)
|
||||
self.assertEqual(days, 2.0)
|
||||
|
||||
def test_isoformat_zulu_time(self):
|
||||
holiday = self.env['hr.holidays'].sudo().new()
|
||||
days = holiday._get_number_of_days(
|
||||
'2025-01-01T09:00:00Z',
|
||||
'2025-01-03T18:00:00Z',
|
||||
False
|
||||
)
|
||||
self.assertEqual(days, 3.0)
|
||||
|
||||
def test_partial_day(self):
|
||||
holiday = self.env['hr.holidays'].sudo().new()
|
||||
days = holiday._get_number_of_days(
|
||||
datetime(2025, 1, 1, 9, 0),
|
||||
datetime(2025, 1, 1, 17, 0),
|
||||
False
|
||||
)
|
||||
self.assertEqual(days, 1.0)
|
||||
|
||||
def test_precise_seconds(self):
|
||||
holiday = self.env['hr.holidays'].sudo().new()
|
||||
days = holiday._get_number_of_days(
|
||||
datetime(2025, 1, 1, 9, 0, 0),
|
||||
datetime(2025, 1, 1, 17, 30, 0),
|
||||
False
|
||||
)
|
||||
self.assertEqual(days, 1.0)
|
||||
|
||||
def test_calendar_work_days_weekdays(self):
|
||||
days = self._calculate_work_days(
|
||||
datetime(2025, 1, 1, 9, 0), # Wednesday
|
||||
datetime(2025, 1, 3, 18, 0), # Friday
|
||||
self.calendar
|
||||
)
|
||||
self.assertEqual(days, 3.0)
|
||||
|
||||
def test_calendar_work_days_weekend(self):
|
||||
days = self._calculate_work_days(
|
||||
datetime(2025, 1, 4, 9, 0), # Saturday
|
||||
datetime(2025, 1, 5, 18, 0), # Sunday
|
||||
self.calendar
|
||||
)
|
||||
self.assertEqual(days, 0.0)
|
||||
|
||||
def test_calendar_partial_work_day(self):
|
||||
days = self._calculate_work_days(
|
||||
datetime(2025, 1, 1, 9, 0), # 9:00
|
||||
datetime(2025, 1, 1, 12, 0), # 12:00
|
||||
self.calendar
|
||||
)
|
||||
self.assertEqual(round(days, 2), 0.38)
|
||||
|
||||
def test_calendar_week_with_weekend(self):
|
||||
days = self._calculate_work_days(
|
||||
datetime(2025, 1, 6, 9, 0), # Monday
|
||||
datetime(2025, 1, 12, 18, 0), # Sunday
|
||||
self.calendar
|
||||
)
|
||||
self.assertEqual(days, 5.0)
|
||||
Loading…
Reference in New Issue