From 7f4b8910d6f3a9c0f903e100c49a32404637313d Mon Sep 17 00:00:00 2001 From: esam Date: Mon, 22 Dec 2025 10:44:05 -0500 Subject: [PATCH] f --- attendances/tests/__init__.py | 1 + .../test_attendance_report_calculations.py | 189 +++++++++++++ .../test_attendance_transaction_timing.py | 252 +++++++++++++++++ hr_base/tests/__init__.py | 1 + .../tests/test_employee_service_duration.py | 79 ++++++ hr_holidays_community/tests/__init__.py | 1 + .../tests/test_holidays_overlap_constraint.py | 258 ++++++++++++++++++ 7 files changed, 781 insertions(+) create mode 100644 attendances/tests/test_attendance_transaction_timing.py create mode 100644 hr_base/tests/__init__.py create mode 100644 hr_base/tests/test_employee_service_duration.py create mode 100644 hr_holidays_community/tests/__init__.py create mode 100644 hr_holidays_community/tests/test_holidays_overlap_constraint.py diff --git a/attendances/tests/__init__.py b/attendances/tests/__init__.py index 35e59db..1a4d289 100644 --- a/attendances/tests/__init__.py +++ b/attendances/tests/__init__.py @@ -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 diff --git a/attendances/tests/test_attendance_report_calculations.py b/attendances/tests/test_attendance_report_calculations.py index dd88a27..dc27c3d 100644 --- a/attendances/tests/test_attendance_report_calculations.py +++ b/attendances/tests/test_attendance_report_calculations.py @@ -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 + ) diff --git a/attendances/tests/test_attendance_transaction_timing.py b/attendances/tests/test_attendance_transaction_timing.py new file mode 100644 index 0000000..d5b8e90 --- /dev/null +++ b/attendances/tests/test_attendance_transaction_timing.py @@ -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) diff --git a/hr_base/tests/__init__.py b/hr_base/tests/__init__.py new file mode 100644 index 0000000..adad8d5 --- /dev/null +++ b/hr_base/tests/__init__.py @@ -0,0 +1 @@ +from . import test_employee_service_duration \ No newline at end of file diff --git a/hr_base/tests/test_employee_service_duration.py b/hr_base/tests/test_employee_service_duration.py new file mode 100644 index 0000000..a3b2f1c --- /dev/null +++ b/hr_base/tests/test_employee_service_duration.py @@ -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)) \ No newline at end of file diff --git a/hr_holidays_community/tests/__init__.py b/hr_holidays_community/tests/__init__.py new file mode 100644 index 0000000..5129ec2 --- /dev/null +++ b/hr_holidays_community/tests/__init__.py @@ -0,0 +1 @@ +from . import test_holidays_overlap_constraint \ No newline at end of file diff --git a/hr_holidays_community/tests/test_holidays_overlap_constraint.py b/hr_holidays_community/tests/test_holidays_overlap_constraint.py new file mode 100644 index 0000000..3380583 --- /dev/null +++ b/hr_holidays_community/tests/test_holidays_overlap_constraint.py @@ -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)