From 74550c21a4ea8d9e293529d3c59b2773a9180b5a Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Thu, 18 Dec 2025 20:06:42 +0300 Subject: [PATCH] make unit test --- attendances/__manifest__.py | 1 + attendances/tests/__init__.py | 7 + .../test_attendance_report_calculations.py | 165 +++++++++++++++ .../tests/test_calendar_update_flow.py | 190 ++++++++++++++++++ .../tests/test_flexible_hours_computation.py | 135 +++++++++++++ .../tests/test_presence_state_computation.py | 168 ++++++++++++++++ .../tests/test_shift_time_calculation.py | 127 ++++++++++++ .../tests/test_signin_signout_constraints.py | 144 +++++++++++++ .../tests/test_working_hours_constraints.py | 135 +++++++++++++ 9 files changed, 1072 insertions(+) create mode 100644 attendances/tests/__init__.py create mode 100644 attendances/tests/test_attendance_report_calculations.py create mode 100644 attendances/tests/test_calendar_update_flow.py create mode 100644 attendances/tests/test_flexible_hours_computation.py create mode 100644 attendances/tests/test_presence_state_computation.py create mode 100644 attendances/tests/test_shift_time_calculation.py create mode 100644 attendances/tests/test_signin_signout_constraints.py create mode 100644 attendances/tests/test_working_hours_constraints.py diff --git a/attendances/__manifest__.py b/attendances/__manifest__.py index 1a65757..c6e7ab7 100644 --- a/attendances/__manifest__.py +++ b/attendances/__manifest__.py @@ -35,4 +35,5 @@ 'installable': True, 'application': True, 'auto_install': False, + 'test_tags': ['standard', 'at_install'], } diff --git a/attendances/tests/__init__.py b/attendances/tests/__init__.py new file mode 100644 index 0000000..35e59db --- /dev/null +++ b/attendances/tests/__init__.py @@ -0,0 +1,7 @@ +from . import test_presence_state_computation +from . import test_working_hours_constraints +from . import test_shift_time_calculation +from . import test_signin_signout_constraints +from . import test_flexible_hours_computation +from . import test_calendar_update_flow +from . import test_attendance_report_calculations diff --git a/attendances/tests/test_attendance_report_calculations.py b/attendances/tests/test_attendance_report_calculations.py new file mode 100644 index 0000000..dd88a27 --- /dev/null +++ b/attendances/tests/test_attendance_report_calculations.py @@ -0,0 +1,165 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from datetime import date +from unittest.mock import MagicMock + + +class TestHrAttendanceReport(TransactionCase): + + def setUp(self): + super(TestHrAttendanceReport, 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': 'Standard 8h', + '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.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': '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, + }) + + def test_01_generate_report_deduction_logic(self): + + common_vals = { + 'employee_id': self.employee.id, + 'calendar_id': self.calendar.id, + 'attending_type': 'in_cal', + 'sequence': 1, + 'approve_lateness': True, + 'approve_exit_out': True, + } + + trans1 = self.env['hr.attendance.transaction'].create({ + **common_vals, 'date': date(2023, 10, 1), 'plan_hours': 8.0, 'is_absent': True, + }) + trans2 = self.env['hr.attendance.transaction'].create({ + **common_vals, 'date': date(2023, 10, 2), 'plan_hours': 8.0, 'official_hours': 6.0, 'lateness': 2.0, + 'is_absent': False, + }) + + + type(trans1).public_holiday = property(lambda self: False) + type(trans1).normal_leave = property(lambda self: False) + # ------------------------------------------ + + # 5. توليد التقرير + report = self.env['hr.attendance.report'].create({ + 'name': 'Report', + 'date_from': date(2023, 10, 1), + 'date_to': date(2023, 10, 5), + 'selected_employee_ids': [(4, self.employee.id)] + }) + + report.generate_report() + + self.assertTrue(report.line_ids, "No record") + line = report.line_ids[0] + + self.assertAlmostEqual(line.total_hours, 10.0, msg="fild hours") + self.assertAlmostEqual(line.total_deduction, 1000.0, msg="mins") + self.assertEqual(report.state, 'generated') + + +from odoo.tests.common import TransactionCase +from datetime import date +from unittest.mock import MagicMock + + +class TestFlexibleAttendanceReport(TransactionCase): + + def setUp(self): + super(TestFlexibleAttendanceReport, self).setUp() + + self.manager = self.env['hr.employee'].create({'name': 'Manager'}) + self.dept = self.env['hr.department'].create({'name': 'IT', 'manager_id': self.manager.id}) + self.calendar_flex = self.env['resource.calendar'].create({ + 'name': 'Flexible 40h', + 'is_flexible': True, + 'is_full_day': True, + 'number_of_flexi_days': 5, + 'working_hours': 8.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, + 'state': 'confirm', + }) + self.employee = self.env['hr.employee'].create({ + 'name': 'Flex Employee', + 'department_id': self.dept.id, + 'resource_calendar_id': self.calendar_flex.id, + 'finger_print': True, + 'state': 'open' + }) + + def test_flexible_calculation_logic(self): + + def create_mock_transaction(plan, office, is_absent=False, sign_in=True, sign_out=True): + mock = MagicMock() + mock.plan_hours = plan + mock.office_hours = office + mock.is_absent = is_absent + mock.public_holiday = False + mock.normal_leave = False + mock.personal_permission_id = False + mock.official_id = False + mock.approve_personal_permission = False + mock.total_permission_hours = 0.0 + mock.total_leave_hours = 0.0 + mock.total_mission_hours = 0.0 + mock.sign_in = sign_in + mock.sign_out = sign_out + mock.early_exit = 0.0 + mock.lateness = 0.0 + mock.break_duration = 0.0 + mock.approve_exit_out = False + mock.approve_lateness = False + return mock + + all_trans_list = [ + create_mock_transaction(8.0, 6.0), + create_mock_transaction(8.0, 0.0, is_absent=True), + create_mock_transaction(8.0, 4.0, sign_out=False) + ] + + def create_mock_recordset(items): + m = MagicMock() + m.__iter__.return_value = iter(items) + m.filtered.side_effect = lambda func: create_mock_recordset([i for i in items if func(i)]) + m.mapped.side_effect = lambda name: [getattr(i, name) for i in items] + m.__len__.return_value = len(items) + return m + + transactions = create_mock_recordset(all_trans_list) + + report = self.env['hr.attendance.report'].new() + result = report.calcualte_flexible_transaction(transactions) + + self.assertAlmostEqual(result['missed_hours'], 14.0, msg="Missed hours calculation failed") + self.assertEqual(result['actual_absent_days'], 1, msg="Absent days count failed") + + self.assertAlmostEqual(result['missing_punch_hours'], 4.0, msg="Missing punch hours failed") + diff --git a/attendances/tests/test_calendar_update_flow.py b/attendances/tests/test_calendar_update_flow.py new file mode 100644 index 0000000..edba6c3 --- /dev/null +++ b/attendances/tests/test_calendar_update_flow.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +from odoo.tests import TransactionCase + + +class TestCalendarUpdateFlow(TransactionCase): + """Test cases for calendar update and inheritance flow in resource.calendar""" + + def setUp(self): + """Setup test data - parent and child calendars""" + super(TestCalendarUpdateFlow, self).setUp() + + # Create parent calendar + self.parent_calendar = self.env['resource.calendar'].create({ + 'name': 'Parent Calendar', + 'is_full_day': True, + 'full_min_sign_in': 8.0, + 'full_max_sign_in': 9.0, + 'full_min_sign_out': 17.0, + 'full_max_sign_out': 18.0, + 'working_hours': 9.0, + 'working_days': 5, + 'state': 'confirm', + }) + + # Create test employee with state='open' + self.test_employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + 'resource_calendar_id': self.parent_calendar.id, + 'state': 'open', # ADD THIS LINE + }) + + def test_act_update_creates_child_calendar(self): + """ + Arrange: Confirmed parent calendar + Act: Call act_update + Assert: New draft calendar created with parent_calendar_id set + """ + # Arrange + initial_count = self.env['resource.calendar'].search_count([]) + + # Act + result = self.parent_calendar.act_update() + + # Assert + self.assertEqual(result['res_model'], 'resource.calendar', + "Should return calendar action") + new_calendar = self.env['resource.calendar'].browse(result['res_id']) + self.assertEqual(new_calendar.parent_calendar_id, self.parent_calendar, + "New calendar should have parent_calendar_id set") + self.assertEqual(new_calendar.state, 'draft', + "New calendar should be in draft state") + self.assertEqual(self.parent_calendar.state, 'update', + "Parent calendar state should change to 'update'") + + def test_act_confirm_without_parent_sets_confirmed_state(self): + # Arrange + new_calendar = self.env['resource.calendar'].create({ + 'name': 'Standalone Calendar', + 'is_full_day': True, + 'full_min_sign_in': 8.0, + 'full_max_sign_in': 9.0, # Add + 'full_min_sign_out': 16.0, + 'full_max_sign_out': 17.0, # Add + 'working_hours': 8.0, + 'state': 'draft', + }) + + # Act + new_calendar.act_confirm() + + # Assert + self.assertEqual(new_calendar.state, 'confirm') + + def test_act_confirm_with_changes_creates_new_version(self): + """ + Arrange: Child calendar with major changes and employee using parent + Act: Call act_confirm + Assert: Employee updated to new calendar, parent archived + """ + # Arrange + child_calendar = self.env['resource.calendar'].create({ + 'name': 'Updated Calendar', + 'parent_calendar_id': self.parent_calendar.id, + 'is_full_day': True, + 'full_min_sign_in': 7.0, # Changed + 'full_max_sign_in': 8.0, + 'full_min_sign_out': 16.0, # Changed + 'full_max_sign_out': 17.0, + 'working_hours': 9.0, + 'working_days': 5, + 'state': 'draft', + }) + + # Verify employee is linked to parent before confirmation + self.assertEqual(self.test_employee.resource_calendar_id, self.parent_calendar) + self.assertIn(self.test_employee, self.parent_calendar.employee_ids, + "Employee should be in parent's employee_ids") + + # Act + child_calendar.act_confirm() + + # Refresh to get updated values from database + self.test_employee.invalidate_recordset(['resource_calendar_id']) + child_calendar.invalidate_recordset(['parent_calendar_id']) + self.parent_calendar.invalidate_recordset(['active', 'employee_ids']) + + # Assert + self.assertEqual(child_calendar.state, 'confirm', + "Child calendar should be confirmed") + self.assertFalse(self.parent_calendar.active, + "Parent calendar should be archived") + self.assertEqual(self.test_employee.resource_calendar_id.id, child_calendar.id, + "Employee should be assigned to new calendar") + self.assertIn(self.test_employee, child_calendar.employee_ids, + "Employee should be in child's employee_ids") + + def test_act_confirm_without_changes_updates_parent(self): + """ + Arrange: Child calendar with no significant changes + Act: Call act_confirm + Assert: Parent should be updated, child deleted + """ + # Arrange + child_calendar = self.env['resource.calendar'].create({ + 'name': 'Minor Update', + 'parent_calendar_id': self.parent_calendar.id, + 'is_full_day': self.parent_calendar.is_full_day, + 'full_min_sign_in': self.parent_calendar.full_min_sign_in, + 'full_max_sign_in': self.parent_calendar.full_max_sign_in, + 'full_min_sign_out': self.parent_calendar.full_min_sign_out, + 'full_max_sign_out': self.parent_calendar.full_max_sign_out, + 'working_hours': self.parent_calendar.working_hours, + 'working_days': self.parent_calendar.working_days, + 'deduction_rule': False, # Minor change + 'state': 'draft', + }) + child_id = child_calendar.id + + # Act + result = child_calendar.act_confirm() + + # Assert + self.assertEqual(result['res_model'], 'resource.calendar', + "Should return to parent calendar") + self.assertEqual(result['res_id'], self.parent_calendar.id, + "Should redirect to parent calendar") + self.assertFalse(self.env['resource.calendar'].search([('id', '=', child_id)]), + "Child calendar should be deleted") + + def test_action_back_to_confirm_resets_state(self): + """ + Arrange: Calendar in 'update' state + Act: Call action_back_to_confirm + Assert: State should return to 'confirm' + """ + # Arrange + self.parent_calendar.state = 'update' + + # Act + self.parent_calendar.action_back_to_confirm() + + # Assert + self.assertEqual(self.parent_calendar.state, 'confirm', + "Calendar state should return to 'confirm'") + + def test_name_search_filters_by_confirmed_state(self): + """ + Arrange: Mix of draft and confirmed calendars + Act: Call name_search + Assert: Only confirmed calendars should be returned + """ + # Arrange + draft_calendar = self.env['resource.calendar'].create({ + 'name': 'Draft Calendar', + 'state': 'draft', + }) + confirmed_calendar = self.env['resource.calendar'].create({ + 'name': 'Confirmed Calendar', + 'state': 'confirm', + }) + + # Act + results = self.env['resource.calendar'].name_search(name='Calendar') + result_ids = [r[0] for r in results] + + # Assert + self.assertIn(confirmed_calendar.id, result_ids, + "Confirmed calendar should appear in search") + self.assertNotIn(draft_calendar.id, result_ids, + "Draft calendar should not appear in search") diff --git a/attendances/tests/test_flexible_hours_computation.py b/attendances/tests/test_flexible_hours_computation.py new file mode 100644 index 0000000..87edc5f --- /dev/null +++ b/attendances/tests/test_flexible_hours_computation.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +from odoo.tests import TransactionCase + + +class TestFlexibleHoursComputation(TransactionCase): + """Test cases for flexible hours calculation in resource.calendar""" + + def setUp(self): + """Setup test data - calendar for flexible hours""" + super(TestFlexibleHoursComputation, self).setUp() + + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Flexible Hours Test Calendar', + }) + + def test_flexible_hours_full_day_calculation(self): + """ + Arrange: Full day calendar with flexible days enabled + Act: Trigger compute + Assert: total_flexible_hours = working_hours * number_of_flexi_days + """ + # Arrange + self.calendar.write({ + 'is_flexible': True, + 'is_full_day': True, + 'working_hours': 8.0, + 'number_of_flexi_days': 5, + 'full_min_sign_in': 8.0, # Add + 'full_max_sign_in': 9.0, # Add + 'full_min_sign_out': 16.0, # Add + 'full_max_sign_out': 17.0, # Add + }) + + # Act + self.calendar.compute_flexible_hours() + + # Assert + self.assertEqual(self.calendar.total_flexible_hours, 40.0, + "Flexible hours for full day should be 8 * 5 = 40 hours") + + + def test_flexible_hours_disabled_returns_zero(self): + """Calendar with is_flexible=False should return 0""" + # Arrange - Add ALL fields to pass constraints + self.calendar.write({ + 'is_flexible': False, + 'is_full_day': True, + 'full_min_sign_in': 8.0, + 'full_max_sign_in': 9.0, # Add + 'full_min_sign_out': 16.0, + 'full_max_sign_out': 17.0, # Add + 'working_hours': 8.0, # Must match time difference + 'number_of_flexi_days': 5, + }) + + # Act + self.calendar.compute_flexible_hours() + + # Assert + self.assertEqual(self.calendar.total_flexible_hours, 0.0) + + def test_flexible_hours_shift_mode_calculation(self): + """Shift calendar flexible hours""" + # Arrange - Add ALL shift fields + self.calendar.write({ + 'is_flexible': True, + 'is_full_day': False, + 'shift_one_min_sign_in': 6.0, + 'shift_one_max_sign_in': 7.0, # Add + 'shift_one_min_sign_out': 14.0, + 'shift_one_max_sign_out': 15.0, # Add + 'shift_one_working_hours': 8.0, + 'shift_two_min_sign_in': 14.0, + 'shift_two_max_sign_in': 15.0, # Add + 'shift_two_min_sign_out': 22.0, + 'shift_two_max_sign_out': 23.0, # Add + 'shift_two_working_hours': 8.0, + 'number_of_flexi_days': 3, + }) + + # Act + self.calendar.compute_flexible_hours() + + # Assert + expected = (8.0 + 8.0) * 3 + self.assertEqual(self.calendar.total_flexible_hours, expected) + + def test_flexible_hours_with_zero_days(self): + """Zero flexible days should return 0""" + # Arrange + self.calendar.write({ + 'is_flexible': True, + 'is_full_day': True, + 'full_min_sign_in': 8.0, + 'full_max_sign_in': 9.0, # Add + 'full_min_sign_out': 16.0, + 'full_max_sign_out': 17.0, # Add + 'working_hours': 8.0, + 'number_of_flexi_days': 0, + }) + + # Act + self.calendar.compute_flexible_hours() + + # Assert + self.assertEqual(self.calendar.total_flexible_hours, 0.0) + + def test_flexible_hours_recompute_on_working_hours_change(self): + """Flexible hours should update when working hours change""" + # Arrange - Add ALL fields + self.calendar.write({ + 'is_flexible': True, + '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, + 'number_of_flexi_days': 5, + }) + self.calendar.compute_flexible_hours() + initial_hours = self.calendar.total_flexible_hours + + # Act - Change times to match 10 hours + self.calendar.write({ + 'full_min_sign_out': 18.0, # Change to 10 hours + 'full_max_sign_out': 19.0, + 'working_hours': 10.0, + }) + self.calendar.compute_flexible_hours() + + # Assert + self.assertEqual(initial_hours, 40.0) + self.assertEqual(self.calendar.total_flexible_hours, 50.0) + diff --git a/attendances/tests/test_presence_state_computation.py b/attendances/tests/test_presence_state_computation.py new file mode 100644 index 0000000..260b8f3 --- /dev/null +++ b/attendances/tests/test_presence_state_computation.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from datetime import date +from ast import literal_eval + + +class TestPresenceStateComputation(TransactionCase): + """Test cases for hr_presence_state computation in hr.employee.base""" + + def setUp(self): + """Setup test data - موظف، مستخدم، وإعدادات النظام""" + super(TestPresenceStateComputation, self).setUp() + + # إنشاء مستخدم للموظف + self.test_user = self.env['res.users'].create({ + 'name': 'Test Employee User', + 'login': 'test_employee@test.com', + 'email': 'test_employee@test.com', + }) + + # إنشاء موظف مرتبط بالمستخدم + self.test_employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + 'user_id': self.test_user.id, + }) + + # إنشاء موظف بدون مستخدم (للاختبارات) + self.employee_without_user = self.env['hr.employee'].create({ + 'name': 'Employee Without User', + 'user_id': False, + }) + + # إعداد تاريخ اليوم + self.today = date.today() + + # الحصول على config parameter + self.config_param = self.env['ir.config_parameter'].sudo() + + def test_presence_state_to_define_when_login_check_disabled(self): + + # Arrange + self.config_param.set_param('hr.hr_presence_control_login', 'False') + + # Act + self.test_employee._compute_presence_state() + + # Assert + self.assertEqual( + self.test_employee.hr_presence_state, + 'to_define', + "Presence state should be 'present' when attendance record exists for today" + ) + + def test_presence_state_present_when_attendance_exists(self): + + # Arrange + self.config_param.set_param('hr.hr_presence_control_login', 'True') + + self.env['attendance.attendance'].sudo().create({ + 'employee_id': self.test_employee.id, + 'action_date': self.today, + }) + + # Act + self.test_employee._compute_presence_state() + + # Assert + self.assertEqual( + self.test_employee.hr_presence_state, + 'present', + "Presence state should be 'to_define' when no attendance record exists" + ) + + def test_presence_state_to_define_when_no_attendance_exists(self): + + # Arrange + self.config_param.set_param('hr.hr_presence_control_login', 'True') + + existing_attendance = self.env['attendance.attendance'].sudo().search([ + ('employee_id', '=', self.test_employee.id), + ('action_date', '=', self.today) + ]) + existing_attendance.unlink() + + # Act + self.test_employee._compute_presence_state() + + # Assert + self.assertEqual( + self.test_employee.hr_presence_state, + 'to_define', + "حالة الحضور يجب أن تكون 'to_define' عند عدم وجود سجل حضور" + ) + + def test_presence_state_multiple_employees(self): + + # Arrange + self.config_param.set_param('hr.hr_presence_control_login', 'True') + + user2 = self.env['res.users'].create({ + 'name': 'Test Employee 2', + 'login': 'test_employee2@test.com', + }) + employee2 = self.env['hr.employee'].create({ + 'name': 'Test Employee 2', + 'user_id': user2.id, + }) + self.env['attendance.attendance'].sudo().create({ + 'employee_id': employee2.id, + 'action_date': self.today, + }) + + user3 = self.env['res.users'].create({ + 'name': 'Test Employee 3', + 'login': 'test_employee3@test.com', + }) + employee3 = self.env['hr.employee'].create({ + 'name': 'Test Employee 3', + 'user_id': user3.id, + }) + + employees = self.test_employee | employee2 | employee3 + + # Act + employees._compute_presence_state() + + # Assert + self.assertEqual(employee2.hr_presence_state, 'present', + "Second employee should have state 'present'") + self.assertEqual(employee3.hr_presence_state, 'to_define', + "Third employee should have state 'to_define'") + + def test_presence_state_employee_without_user(self): + + # Arrange + self.config_param.set_param('hr.hr_presence_control_login', 'True') + + # Act + self.employee_without_user._compute_presence_state() + + # Assert + self.assertEqual( + self.employee_without_user.hr_presence_state, + 'to_define', + "the employee with not user must be'to_define'" + ) + + def test_presence_state_updates_correctly_on_multiple_calls(self): + + # Arrange + self.config_param.set_param('hr.hr_presence_control_login', 'True') + + self.test_employee._compute_presence_state() + state_before = self.test_employee.hr_presence_state + + self.env['attendance.attendance'].sudo().create({ + 'employee_id': self.test_employee.id, + 'action_date': self.today, + }) + + self.test_employee._compute_presence_state() + state_after = self.test_employee.hr_presence_state + + # Assert + self.assertEqual(state_before, 'to_define', + "The one case must be 'to_define'") + self.assertEqual(state_after, 'present', + "the one case after present must be 'present'") diff --git a/attendances/tests/test_shift_time_calculation.py b/attendances/tests/test_shift_time_calculation.py new file mode 100644 index 0000000..4b6edb3 --- /dev/null +++ b/attendances/tests/test_shift_time_calculation.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +from odoo.tests import TransactionCase + + +class TestShiftTimeCalculation(TransactionCase): + """Test cases for shift working hours calculation in resource.calendar""" + + def setUp(self): + """Setup test data - calendar for shift calculations""" + super(TestShiftTimeCalculation, self).setUp() + + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Shift Test Calendar', + 'is_full_day': False, + 'noke': False, + }) + + def test_get_shift_working_hour_standard_8_hours(self): + """ + Arrange: Standard 8-hour shift (08:00 to 16:00) + Act: Calculate working hours + Assert: Should return 8.0 hours + """ + # Arrange & Act + result = self.calendar.get_shift_working_hour(8.0, 16.0) + + # Assert + self.assertEqual(result, 8.0, + "Standard 8-hour shift should return 8.0 hours") + + def test_get_shift_working_hour_with_decimal_minutes(self): + """ + Arrange: Shift with decimal time (08:30 to 17:15) + Act: Calculate working hours + Assert: Should return correct hours with decimal precision + """ + # Arrange & Act + result = self.calendar.get_shift_working_hour(8.5, 17.25) + + # Assert + self.assertAlmostEqual(result, 8.75, places=2, + msg="Shift from 08:30 to 17:15 should return 8.75 hours") + + def test_shift_one_working_hours_auto_compute_on_change(self): + """ + Arrange: Set shift one min sign in and out + Act: Trigger onchange method + Assert: shift_one_working_hours should be automatically calculated + """ + # Arrange + self.calendar.write({ + 'shift_one_min_sign_in': 7.0, + 'shift_one_max_sign_in': 8.0, # Add + 'shift_one_min_sign_out': 15.0, + 'shift_one_max_sign_out': 16.0, # Add + }) + + # Act + self.calendar.work_hours() + + # Assert + self.assertEqual(self.calendar.shift_one_working_hours, 8.0, + "Shift one working hours should auto-calculate to 8.0") + + def test_shift_two_working_hours_auto_compute_on_change(self): + """ + Arrange: Set shift two min sign in and out + Act: Trigger onchange method + Assert: shift_two_working_hours should be automatically calculated + """ + # Arrange - Add ALL required fields + self.calendar.write({ + 'shift_two_min_sign_in': 15.0, + 'shift_two_max_sign_in': 16.0, # Add this + 'shift_two_min_sign_out': 23.0, + 'shift_two_max_sign_out': 24.0, # Add this + }) + + # Act + self.calendar.work_hours() + + # Assert + self.assertEqual(self.calendar.shift_two_working_hours, 8.0, + "Shift two working hours should auto-calculate to 8.0") + + def test_both_shifts_working_hours_computed_together(self): + """ + Arrange: Set both shifts' times + Act: Trigger onchange + Assert: Both shift working hours should be calculated + """ + # Arrange - Add max fields + self.calendar.write({ + 'shift_one_min_sign_in': 6.0, + 'shift_one_max_sign_in': 7.0, # Add + 'shift_one_min_sign_out': 14.0, + 'shift_one_max_sign_out': 15.0, # Add + 'shift_two_min_sign_in': 14.0, + 'shift_two_max_sign_in': 15.0, # Add + 'shift_two_min_sign_out': 22.0, + 'shift_two_max_sign_out': 23.0, # Add + }) + + # Act + self.calendar.work_hours() + + # Assert + self.assertEqual(self.calendar.shift_one_working_hours, 8.0, + "Shift one should be 8 hours") + self.assertEqual(self.calendar.shift_two_working_hours, 8.0, + "Shift two should be 8 hours") + + def test_overnight_shift_calculation(self): + """ + Arrange: Overnight shift (22:00 to 06:00) + Act: Calculate working hours + Assert: Should correctly handle overnight shifts + """ + # Arrange & Act + result = self.calendar.get_shift_working_hour(22.0, 6.0) + + # Assert + # Note: This depends on implementation - may need adjustment + expected_hours = 8.0 if result > 0 else -16.0 + self.assertGreater(result, 0, + "Overnight shift should return positive hours") + diff --git a/attendances/tests/test_signin_signout_constraints.py b/attendances/tests/test_signin_signout_constraints.py new file mode 100644 index 0000000..ed3bb10 --- /dev/null +++ b/attendances/tests/test_signin_signout_constraints.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +from odoo.tests import TransactionCase +from odoo.exceptions import ValidationError + + +class TestSignInSignOutConstraints(TransactionCase): + """Test cases for sign in/out time constraints in resource.calendar""" + + def setUp(self): + """Setup test data - calendar for time validation""" + super(TestSignInSignOutConstraints, self).setUp() + + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Time Constraints Test Calendar', + 'noke': False, + }) + + def test_full_day_max_sign_in_less_than_min_raises_error(self): + # Arrange & Act & Assert + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + 'is_full_day': True, + 'full_min_sign_in': 9.0, + 'full_min_sign_out': 8.0, + 'full_max_sign_in': 8.0, # Before min + 'full_max_sign_out': 18.0, + }) + + self.assertIn('Max sign in should be greater than or equal min sign in', + str(context.exception)) + + def test_full_day_min_sign_out_before_min_sign_in_raises_error(self): + """ + Arrange: Set min sign out before min sign in for full day + Act: Try to write invalid data + Assert: ValidationError raised automatically by @api.constrains + """ + # Act & Assert - wrap the write() call + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + 'is_full_day': True, + 'full_min_sign_in': 10.0, + 'full_max_sign_in': 11.0, + 'full_min_sign_out': 9.0, # Invalid: before min sign in + 'full_max_sign_out': 18.0, + 'working_hours': 8.0, # Add this to avoid other constraints + }) + + # Optional: verify error message + self.assertIn('min sign out should be greater than or equal min sign in', + str(context.exception)) + + def test_shift_one_max_sign_in_less_than_min_raises_error(self): + """ + Arrange: Set invalid shift one times (max < min) + Act: Try to write + Assert: ValidationError raised + """ + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + 'is_full_day': False, + 'shift_one_min_sign_in': 10.0, # min = 10 + 'shift_one_max_sign_in': 9.0, # max = 9 (INVALID!) + 'shift_one_min_sign_out': 16.0, + 'shift_one_max_sign_out': 17.0, + 'shift_one_working_hours': 6.0, # Add working hours + }) + + self.assertIn('Max sign in should be greater than or equal min sign in', + str(context.exception)) + + def test_shift_two_min_sign_out_before_min_sign_in_raises_error(self): + """ + Arrange: Set invalid shift two times + Act: Try to write + Assert: ValidationError raised + """ + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + 'is_full_day': False, + 'shift_two_min_sign_in': 16.0, + 'shift_two_max_sign_in': 17.0, + 'shift_two_min_sign_out': 15.0, # Invalid: before min sign in + 'shift_two_max_sign_out': 24.0, + 'shift_two_working_hours': 8.0, # Add working hours + }) + + self.assertIn('shift two', str(context.exception).lower()) + + def test_full_day_max_sign_out_less_than_min_sign_out_raises_error(self): + # This test is CORRECT - error is expected and happening! + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + 'is_full_day': True, + 'full_min_sign_in': 8.0, + 'full_max_sign_in': 9.0, + 'full_min_sign_out': 17.0, + 'full_max_sign_out': 16.0, # Before min - ERROR EXPECTED + }) + + self.assertIn('Max sign out should be greater', str(context.exception)) + + def test_full_day_valid_times_do_not_raise_error(self): + """ + Arrange: Set valid full day times + Act: Save calendar + Assert: No validation error should be raised + """ + # Arrange + self.calendar.write({ + 'is_full_day': True, + 'full_min_sign_in': 8.0, + 'full_max_sign_in': 9.0, + 'full_min_sign_out': 17.0, + 'full_max_sign_out': 18.0, + }) + + # Act & Assert + try: + self.calendar.full_time_constrains() + except ValidationError: + self.fail("Valid full day times should not raise ValidationError") + + def test_noke_mode_bypasses_time_constraints(self): + """ + Arrange: Enable noke mode with invalid times + Act: Save calendar + Assert: No validation error (noke mode disables checks) + """ + # Arrange + self.calendar.write({ + 'is_full_day': True, + 'noke': True, + 'full_min_sign_in': 10.0, + 'full_max_sign_in': 8.0, # Invalid, but noke=True + 'full_min_sign_out': 17.0, + 'full_max_sign_out': 16.0, + }) + + # Act & Assert + try: + self.calendar.full_time_constrains() + except ValidationError: + self.fail("Noke mode should bypass time constraints validation") diff --git a/attendances/tests/test_working_hours_constraints.py b/attendances/tests/test_working_hours_constraints.py new file mode 100644 index 0000000..0f43052 --- /dev/null +++ b/attendances/tests/test_working_hours_constraints.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +from odoo.tests import TransactionCase +from odoo.exceptions import ValidationError + + +class TestWorkingHoursConstraints(TransactionCase): + """Test cases for working hours validation constraints in resource.calendar""" + + def setUp(self): + super(TestWorkingHoursConstraints, self).setUp() + + # Create base calendar + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Test Calendar', + 'is_full_day': True, + 'noke': False, + }) + + def test_full_day_working_hours_mismatch_raises_error(self): + """ + Arrange: Set full day sign in/out times with mismatched working hours + Act: Try to save the calendar + Assert: ValidationError should be raised + """ + # Arrange & Act & Assert - Error happens during write() + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + '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': 7.0, # Wrong value - should be 8.0 + }) + + self.assertIn('8.0', str(context.exception), + "Error message should mention the correct working hours (8.0)") + + def test_shift_one_working_hours_match_time_difference(self): + """ + Arrange: Set shift one times with matching working hours + Act: Save the calendar + Assert: No validation error should be raised + """ + # Arrange + self.calendar.write({ + 'is_full_day': False, + 'shift_one_min_sign_in': 7.0, + 'shift_one_max_sign_in': 8.0, # Add max + 'shift_one_min_sign_out': 15.0, + 'shift_one_max_sign_out': 16.0, # Add max + 'shift_one_working_hours': 8.0, + 'shift_two_min_sign_in': 15.0, # Add shift two to avoid errors + 'shift_two_max_sign_in': 16.0, + 'shift_two_min_sign_out': 23.0, + 'shift_two_max_sign_out': 24.0, + 'shift_two_working_hours': 8.0, + }) + + # Act & Assert + try: + self.calendar.result_greed_constrains() + except ValidationError: + self.fail("ValidationError should not be raised for valid shift one working hours") + + def test_shift_one_working_hours_mismatch_raises_error(self): + """ + Arrange: Set shift one times with mismatched working hours + Act: Try to save the calendar + Assert: ValidationError should be raised + """ + # Arrange & Act & Assert + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + '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': 9.0, # Wrong - should be 8.0 + 'shift_two_min_sign_in': 15.0, + 'shift_two_max_sign_in': 16.0, + 'shift_two_min_sign_out': 23.0, + 'shift_two_max_sign_out': 24.0, + 'shift_two_working_hours': 8.0, + }) + + self.assertIn('8.0', str(context.exception), + "Error message should mention the correct shift one working hours") + + def test_shift_two_working_hours_exceeds_maximum_raises_error(self): + """ + Arrange: Set shift two working hours greater than time difference + Act: Try to save the calendar + Assert: ValidationError should be raised + """ + # Arrange & Act & Assert + with self.assertRaises(ValidationError) as context: + self.calendar.write({ + '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, + 'shift_two_min_sign_in': 15.0, + 'shift_two_max_sign_in': 16.0, + 'shift_two_min_sign_out': 23.0, + 'shift_two_max_sign_out': 24.0, + 'shift_two_working_hours': 10.0, # Greater than 8 hours + }) + + self.assertIn('8.0', str(context.exception), + "Error message should mention the maximum allowed shift two working hours") + + + def test_full_day_working_hours_match_time_difference(self): + """ + Arrange: Set full day sign in/out times with matching working hours + Act: Save the calendar + Assert: No validation error should be raised + """ + # Arrange - Add ALL required fields to pass other constraints + self.calendar.write({ + 'full_min_sign_in': 8.0, + 'full_max_sign_in': 9.0, # Add this + 'full_min_sign_out': 16.0, + 'full_max_sign_out': 17.0, # Add this + 'working_hours': 8.0, + }) + + # Act & Assert + try: + self.calendar.result_greed_constrains() + except ValidationError: + self.fail("ValidationError should not be raised when working hours match time difference")