make unit test
This commit is contained in:
parent
dee476153c
commit
74550c21a4
|
|
@ -35,4 +35,5 @@
|
|||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'test_tags': ['standard', 'at_install'],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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'")
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
Loading…
Reference in New Issue