mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-26 00:12:00 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_hr_attendance_constraints
|
||||
from . import test_hr_attendance_overtime
|
||||
from . import test_hr_attendance_undertime
|
||||
from . import test_hr_attendance_process
|
||||
from . import test_hr_attendance_domain_translation
|
||||
from . import test_load_scenario
|
||||
from . import test_hr_attendance_kiosk
|
||||
from . import test_hr_attendance_rulesets
|
||||
from . import test_performance
|
||||
from . import test_hr_attendance_manager
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from freezegun import freeze_time
|
||||
import time
|
||||
|
||||
from odoo.tests.common import tagged, TransactionCase
|
||||
|
|
@ -13,7 +14,7 @@ class TestHrAttendance(TransactionCase):
|
|||
def setUpClass(cls):
|
||||
super(TestHrAttendance, cls).setUpClass()
|
||||
cls.attendance = cls.env['hr.attendance']
|
||||
cls.test_employee = cls.env['hr.employee'].create({'name': "Jacky"})
|
||||
cls.test_employee = cls.env['hr.employee'].create({'name': "Jacky", 'ruleset_id': False})
|
||||
# demo data contains set up for cls.test_employee
|
||||
cls.open_attendance = cls.attendance.create({
|
||||
'employee_id': cls.test_employee.id,
|
||||
|
|
@ -62,3 +63,39 @@ class TestHrAttendance(TransactionCase):
|
|||
self.open_attendance.write({
|
||||
'check_out': time.strftime('%Y-%m-10 11:30'),
|
||||
})
|
||||
|
||||
@freeze_time("2024-02-05 11:00:00")
|
||||
def test_attendance_in_the_future(self):
|
||||
employee = self.env['hr.employee'].create({'name': "Test"})
|
||||
self.attendance.create({
|
||||
'employee_id': employee.id,
|
||||
'check_in': time.strftime('2024-02-10 11:00'),
|
||||
'check_out': time.strftime('2024-02-10 12:00'),
|
||||
})
|
||||
open_attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': employee.id,
|
||||
'check_in': time.strftime('2024-02-05 10:00'),
|
||||
})
|
||||
|
||||
self.assertEqual(employee.attendance_state, 'checked_in')
|
||||
|
||||
open_attendance.write({
|
||||
'check_out': time.strftime('2024-02-05 11:30'),
|
||||
})
|
||||
|
||||
self.assertEqual(employee.attendance_state, 'checked_out')
|
||||
|
||||
def test_time_format_attendance(self):
|
||||
self.env.user.tz = 'UTC'
|
||||
self.env['res.lang']._activate_lang('en_US')
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
lang.time_format = "%I:%M:%S %p" # here "%I:%M:%S %p" represents AM:PM format
|
||||
attendance_id = self.attendance.create({
|
||||
'employee_id': self.test_employee.id,
|
||||
'check_in': time.strftime('%Y-%m-28 08:00'),
|
||||
'check_out': time.strftime('%Y-%m-28 09:00'),
|
||||
})
|
||||
self.assertEqual(attendance_id.display_name, "01:00 (08:00:00 AM-09:00:00 AM)")
|
||||
lang.time_format = "%H:%M:%S"
|
||||
attendance_id._compute_display_name()
|
||||
self.assertEqual(attendance_id.display_name, "01:00 (08:00:00-09:00:00)")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import time
|
||||
|
||||
from odoo.tests.common import tagged, TransactionCase
|
||||
|
||||
|
||||
@tagged('attendance_searchbar_user_domain')
|
||||
class TestHrAttendanceDomainTranslation(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.hr_attendance = cls.env['hr.attendance']
|
||||
cls.hr_employee = cls.env['hr.employee']
|
||||
cls.employee_musa, employee_tecna = cls.hr_employee.create([{'name': 'Musa'}, {'name': 'Tecna'}])
|
||||
cls.hr_attendance.create({
|
||||
'employee_id': employee_tecna.id,
|
||||
'check_in': time.strftime('%Y-%m-10 10:00'),
|
||||
})
|
||||
|
||||
def test_searchbar_with_user_domain(self):
|
||||
companies_ids = self.env['res.company'].search([]).ids
|
||||
|
||||
# Checks that this domain returns no attendance
|
||||
self.assertEqual(
|
||||
self.hr_attendance.search([
|
||||
'&',
|
||||
('check_out', "!=", False),
|
||||
'|',
|
||||
('employee_id', 'ilike', 'Musa'),
|
||||
('employee_id', 'ilike', 'Flora')
|
||||
]),
|
||||
self.hr_attendance
|
||||
)
|
||||
|
||||
# Ensure that if an employee is searched with the search bar even if he doesn't have any attendance,
|
||||
# he will be returned.
|
||||
self.assertEqual(
|
||||
self.hr_attendance.with_context(
|
||||
allowed_company_ids=companies_ids,
|
||||
user_domain=[
|
||||
'|',
|
||||
('employee_id', 'ilike', 'Musa'),
|
||||
('employee_id', 'ilike', 'Flora')
|
||||
])._read_group_employee_id(self.hr_employee, [
|
||||
'&',
|
||||
('check_out', "!=", False),
|
||||
'|',
|
||||
('employee_id', 'ilike', 'Musa'),
|
||||
('employee_id', 'ilike', 'Flora')
|
||||
]),
|
||||
self.employee_musa
|
||||
)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tests.common import tagged, HttpCase
|
||||
from unittest.mock import patch
|
||||
from odoo.http import Request
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'hr_attendance_overtime')
|
||||
class TestHrAttendanceKiosk(HttpCase):
|
||||
""" Tests for kiosk """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.company_A = cls.env['res.company'].create({'name': 'company_A'})
|
||||
cls.company_B = cls.env['res.company'].create({'name': 'company_B'})
|
||||
|
||||
cls.department_A = cls.env['hr.department'].create({'name': 'department_A', 'company_id': cls.company_B.id})
|
||||
|
||||
cls.employee_A = cls.env['hr.employee'].create({
|
||||
'name': 'employee_A',
|
||||
'company_id': cls.company_B.id,
|
||||
'department_id': cls.department_A.id,
|
||||
})
|
||||
cls.employee_B = cls.env['hr.employee'].create({
|
||||
'name': 'employee_B',
|
||||
'company_id': cls.company_A.id,
|
||||
'department_id': cls.department_A.id,
|
||||
})
|
||||
|
||||
def test_employee_count_kiosk(self):
|
||||
# the mock need to return a None value which can be converted into a Reponse object
|
||||
with patch.object(Request, "render", return_value=None) as render:
|
||||
self.url_open(self.company_B.attendance_kiosk_url)
|
||||
|
||||
render.assert_called_once()
|
||||
_template, kiosk_info = render.call_args[0]
|
||||
kiosk_info = kiosk_info['kiosk_backend_info']
|
||||
self.assertEqual(kiosk_info['company_name'], 'company_B')
|
||||
self.assertEqual(kiosk_info['departments'][0]['count'], 1)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import new_test_user
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAttendanceManager(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Create an attendance manager
|
||||
cls.luisa = new_test_user(cls.env, login='luisa', groups='hr_attendance.group_hr_attendance_manager')
|
||||
|
||||
# Create a normal user
|
||||
cls.marc = new_test_user(cls.env, login='marc', groups='base.group_user')
|
||||
cls.marc_employee = cls.env['hr.employee'].create({
|
||||
'name': 'Marc Employee',
|
||||
'user_id': cls.marc.id,
|
||||
})
|
||||
cls.marc_employee.attendance_manager_id = cls.marc
|
||||
|
||||
# Create another employee
|
||||
cls.abigail_employee, cls.ryan_employee = cls.env['hr.employee'].create([
|
||||
{
|
||||
'name': 'Abigail Employee',
|
||||
'attendance_manager_id': cls.marc.id,
|
||||
},
|
||||
{
|
||||
'name': 'Ryan Employee',
|
||||
'attendance_manager_id': cls.luisa.id,
|
||||
},
|
||||
])
|
||||
|
||||
# Create an attendance for Marc Demo's employee
|
||||
cls.attendance = cls.env['hr.attendance'].create({
|
||||
'employee_id': cls.marc_employee.id,
|
||||
'check_in': '2025-09-09 08:00:00',
|
||||
'check_out': '2025-09-09 12:00:00',
|
||||
})
|
||||
|
||||
def test_attendance_officer_rights(self):
|
||||
"""Marc Demo should NOT be able to change the employee on his attendance
|
||||
if he is not assigned as attendance manager of that employee.
|
||||
"""
|
||||
attendance_as_marc = self.attendance.with_user(self.marc)
|
||||
|
||||
# Marc can change the employee to Abigail
|
||||
attendance_as_marc.write({'employee_id': self.abigail_employee.id})
|
||||
self.assertEqual(self.attendance.employee_id, self.abigail_employee)
|
||||
|
||||
# Marc cannot change the employee to Ryan
|
||||
with self.assertRaises(AccessError):
|
||||
attendance_as_marc.write({'employee_id': self.ryan_employee.id})
|
||||
|
||||
def test_attendance_manager_rights(self):
|
||||
"""Luisa should be able to change the employee on attendance without the need
|
||||
of being set as attendance_manager since she has the attendance_manager group.
|
||||
"""
|
||||
attendance_as_luisa = self.attendance.with_user(self.luisa)
|
||||
|
||||
attendance_as_luisa.write({'employee_id': self.abigail_employee.id})
|
||||
self.assertEqual(self.attendance.employee_id, self.abigail_employee)
|
||||
|
||||
attendance_as_luisa.write({'employee_id': self.ryan_employee.id})
|
||||
self.assertEqual(self.attendance.employee_id, self.ryan_employee)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -5,8 +5,8 @@ from datetime import datetime
|
|||
from unittest.mock import patch
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests import new_test_user
|
||||
from odoo.tests.common import tagged, TransactionCase
|
||||
from odoo.tests import Form, new_test_user
|
||||
from odoo.tests.common import tagged, TransactionCase, freeze_time
|
||||
|
||||
|
||||
@tagged('attendance_process')
|
||||
|
|
@ -16,12 +16,13 @@ class TestHrAttendance(TransactionCase):
|
|||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestHrAttendance, cls).setUpClass()
|
||||
cls.user = new_test_user(cls.env, login='fru', groups='base.group_user,hr_attendance.group_hr_attendance_use_pin')
|
||||
cls.user = new_test_user(cls.env, login='fru', groups='base.group_user')
|
||||
cls.user_no_pin = new_test_user(cls.env, login='gru', groups='base.group_user')
|
||||
cls.test_employee = cls.env['hr.employee'].create({
|
||||
'name': "François Russie",
|
||||
'user_id': cls.user.id,
|
||||
'pin': '1234',
|
||||
'ruleset_id': False,
|
||||
})
|
||||
cls.employee_kiosk = cls.env['hr.employee'].create({
|
||||
'name': "Machiavel",
|
||||
|
|
@ -41,56 +42,42 @@ class TestHrAttendance(TransactionCase):
|
|||
self.test_employee._attendance_action_change()
|
||||
assert self.test_employee.attendance_state == 'checked_out'
|
||||
|
||||
def test_checkin_self_without_pin(self):
|
||||
""" Employee can check in/out without pin with his own account """
|
||||
employee = self.test_employee.with_user(self.user)
|
||||
employee.with_user(self.user).attendance_manual({}, entered_pin=None)
|
||||
self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in without pin")
|
||||
employee.attendance_manual({}, entered_pin=None)
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out without pin")
|
||||
def test_employee_group_id(self):
|
||||
# Create attendance for one of them
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee_kiosk.id,
|
||||
'check_in': '2025-08-01 08:00:00',
|
||||
'check_out': '2025-08-01 17:00:00',
|
||||
})
|
||||
context = self.env.context.copy()
|
||||
context['read_group_expand'] = True
|
||||
|
||||
def test_checkin_self_with_pin(self):
|
||||
""" Employee can check in/out with pin with his own account """
|
||||
employee = self.test_employee.with_user(self.user)
|
||||
employee.attendance_manual({}, entered_pin='1234')
|
||||
self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in with his pin")
|
||||
employee.attendance_manual({}, entered_pin='1234')
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out with his pin")
|
||||
groups = self.env['hr.attendance'].with_context(**context).web_read_group(
|
||||
domain=[],
|
||||
groupby=['employee_id']
|
||||
)
|
||||
groups = groups['groups']
|
||||
|
||||
def test_checkin_self_wrong_pin(self):
|
||||
""" Employee cannot check in/out with wrong pin with his own account """
|
||||
employee = self.test_employee.with_user(self.user)
|
||||
action = employee.attendance_manual({}, entered_pin='9999')
|
||||
self.assertNotEqual(employee.attendance_state, 'checked_in', "He should not be able to check in with a wrong pin")
|
||||
self.assertTrue(action.get('warning'))
|
||||
grouped_employee_ids = [g['employee_id'][0] for g in groups]
|
||||
|
||||
def test_checkin_kiosk_with_pin(self):
|
||||
""" Employee can check in/out with his pin in kiosk """
|
||||
employee = self.employee_kiosk.with_user(self.user)
|
||||
employee.attendance_manual({}, entered_pin='5678')
|
||||
self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in with his pin")
|
||||
employee.attendance_manual({}, entered_pin='5678')
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out with his pin")
|
||||
self.assertNotIn(self.test_employee.id, grouped_employee_ids)
|
||||
self.assertIn(self.employee_kiosk.id, grouped_employee_ids)
|
||||
|
||||
def test_checkin_kiosk_with_wrong_pin(self):
|
||||
""" Employee cannot check in/out with wrong pin in kiosk """
|
||||
employee = self.employee_kiosk.with_user(self.user)
|
||||
action = employee.attendance_manual({}, entered_pin='8888')
|
||||
self.assertNotEqual(employee.attendance_state, 'checked_in', "He should not be able to check in with a wrong pin")
|
||||
self.assertTrue(action.get('warning'))
|
||||
# Specific to gantt view.
|
||||
context['gantt_start_date'] = fields.Datetime.now()
|
||||
context['allowed_company_ids'] = [self.env.company.id]
|
||||
|
||||
def test_checkin_kiosk_without_pin(self):
|
||||
""" Employee cannot check in/out without his pin in kiosk """
|
||||
employee = self.employee_kiosk.with_user(self.user)
|
||||
action = employee.attendance_manual({}, entered_pin=None)
|
||||
self.assertNotEqual(employee.attendance_state, 'checked_in', "He should not be able to check in with no pin")
|
||||
self.assertTrue(action.get('warning'))
|
||||
groups = self.env['hr.attendance'].with_context(**context).web_read_group(
|
||||
domain=[],
|
||||
groupby=['employee_id']
|
||||
)
|
||||
groups = groups['groups']
|
||||
|
||||
def test_checkin_kiosk_no_pin_mode(self):
|
||||
""" Employee cannot check in/out without pin in kiosk when user has not group `use_pin` """
|
||||
employee = self.employee_kiosk.with_user(self.user_no_pin)
|
||||
employee.attendance_manual({}, entered_pin=None)
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He shouldn't be able to check in without")
|
||||
grouped_employee_ids = [g['employee_id'][0] for g in groups]
|
||||
|
||||
# Check that both employees appears
|
||||
self.assertIn(self.test_employee.id, grouped_employee_ids)
|
||||
self.assertIn(self.employee_kiosk.id, grouped_employee_ids)
|
||||
|
||||
def test_hours_today(self):
|
||||
""" Test day start is correctly computed according to the employee's timezone """
|
||||
|
|
@ -113,3 +100,43 @@ class TestHrAttendance(TransactionCase):
|
|||
# now = 2019/3/2 14:00 in the employee's timezone
|
||||
with patch.object(fields.Datetime, 'now', lambda: tz_datetime(2019, 3, 2, 14, 0).astimezone(pytz.utc).replace(tzinfo=None)):
|
||||
self.assertEqual(employee.hours_today, 5, "It should have counted 5 hours")
|
||||
|
||||
def test_remove_check_in_value_from_attendance(self):
|
||||
attendance_form = Form(self.env['hr.attendance'])
|
||||
attendance_form.employee_id = self.test_employee
|
||||
attendance_form.check_in = False
|
||||
with self.assertRaises(AssertionError):
|
||||
attendance_form.save()
|
||||
|
||||
# @freeze_time("2024-02-1")
|
||||
# def test_change_in_out_mode_when_manual_modification(self):
|
||||
# TODO naja: cron should work eventually when the adjustment feature is back
|
||||
# company = self.env['res.company'].create({
|
||||
# 'name': 'Monsters, Inc.',
|
||||
# 'absence_management': True,
|
||||
# })
|
||||
|
||||
# employee = self.env['hr.employee'].create({
|
||||
# 'name': "James P. Sullivan",
|
||||
# 'company_id': company.id,
|
||||
# 'date_version': date(2021, 1, 1),
|
||||
# 'contract_date_start': date(2021, 1, 1),
|
||||
# })
|
||||
# breakpoint()
|
||||
|
||||
# self.env['hr.attendance']._cron_absence_detection()
|
||||
|
||||
# attendance = self.env['hr.attendance'].search([('employee_id', '=', employee.id)])
|
||||
|
||||
# self.assertEqual(attendance.in_mode, 'technical')
|
||||
# self.assertEqual(attendance.out_mode, 'technical')
|
||||
# self.assertEqual(attendance.color, 1)
|
||||
|
||||
# attendance.write({
|
||||
# 'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
# 'check_out': datetime(2021, 1, 4, 17, 0),
|
||||
# })
|
||||
|
||||
# self.assertEqual(attendance.in_mode, 'manual')
|
||||
# self.assertEqual(attendance.out_mode, 'manual')
|
||||
# self.assertEqual(attendance.color, 0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import date, datetime
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import new_test_user, Form
|
||||
from odoo.tests.common import tagged, TransactionCase
|
||||
|
||||
|
||||
@tagged('hr_attendance_overtime_ruleset')
|
||||
class TestHrAttendanceOvertime(TransactionCase):
|
||||
""" Tests for overtime """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.ruleset = cls.env['hr.attendance.overtime.ruleset'].create({
|
||||
'name': 'Ruleset schedule quantity',
|
||||
'rule_ids': [
|
||||
Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'quantity_period': 'day',
|
||||
'expected_hours': 8,
|
||||
'paid': True,
|
||||
'amount_rate': 150,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'quantity_period': 'day',
|
||||
'expected_hours': 10,
|
||||
'paid': True,
|
||||
'amount_rate': 200,
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'quantity_period': 'week',
|
||||
'expected_hours': 40,
|
||||
'paid': True,
|
||||
'amount_rate': 150,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.company = cls.env['res.company'].create({
|
||||
'name': 'SweatChipChop Inc.',
|
||||
'attendance_overtime_validation': 'no_validation',
|
||||
})
|
||||
cls.company.resource_calendar_id = cls.env.company.resource_calendar_id = cls.env['resource.calendar'].create({
|
||||
'name': 'Standard 40 hours/week (No Lunch)',
|
||||
'company_id': cls.env.company.id,
|
||||
'hours_per_day': 7.6,
|
||||
'full_time_required_hours': 38,
|
||||
'attendance_ids': [
|
||||
(5, 0, 0), # Clear existing attendances
|
||||
(0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 16, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 16, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 16, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 16, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 16, 'day_period': 'morning'}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.user = new_test_user(cls.env, login='fru', groups='base.group_user,hr_attendance.group_hr_attendance_manager', company_id=cls.company.id).with_company(cls.company)
|
||||
cls.employee = cls.env['hr.employee'].create({
|
||||
'name': "Marie-Edouard De La Court",
|
||||
'user_id': cls.user.id,
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'UTC',
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'resource_calendar_id': cls.company.resource_calendar_id.id,
|
||||
'ruleset_id': cls.ruleset.id
|
||||
})
|
||||
|
||||
def test_daily_overtime_8_hours_rule(self):
|
||||
with freeze_time("2021-01-04"):
|
||||
# Attendance: 10 hours (8 expected + 2 overtime at 150%)
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 18, 0)
|
||||
})
|
||||
|
||||
self.assertAlmostEqual(attendance.employee_id.total_overtime, 2, 2, msg="2 hours overtime at 150% should yield 2 hours total overtime")
|
||||
|
||||
def test_daily_overtime_10_hours_rule(self):
|
||||
""" Test daily overtime for the 10-hour rule """
|
||||
with freeze_time("2021-01-04"):
|
||||
# Attendance: 12 hours (10 expected + 2 overtime at 200%)
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 20, 0)
|
||||
})
|
||||
|
||||
self.assertAlmostEqual(attendance.employee_id.total_overtime, 4.0, 2, msg="2 hours overtime at 200% should yield 4 hours total overtime")
|
||||
|
||||
def test_no_overtime(self):
|
||||
""" Test no overtime when working expected hours or less """
|
||||
with freeze_time("2021-01-04"):
|
||||
# Attendance: 8 hours (exactly 8 expected, no overtime)
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 16, 0)
|
||||
})
|
||||
self.assertAlmostEqual(attendance.employee_id.total_overtime, 0.0, 2, msg="No overtime should be recorded for 8 hours or less")
|
||||
|
||||
def test_weekly_overtime(self):
|
||||
""" Test weekly overtime for the 40-hour rule """
|
||||
with freeze_time("2021-01-04"):
|
||||
# Week: Mon-Fri, 10 hours/day = 50 hours total (40 expected + 10 overtime at 200%)
|
||||
[
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id, # This employee have a calendar with no lunch
|
||||
'check_in': datetime(2021, 1, day, 8, 0),
|
||||
'check_out': datetime(2021, 1, day, 18, 0)
|
||||
}) for day in range(4, 9) # Monday to Friday
|
||||
]
|
||||
self.assertAlmostEqual(self.employee.total_overtime, 10, 2, msg="He should work from 8-16h so each day he did 2 hours of overtime")
|
||||
|
||||
def test_multiple_attendances_same_day(self):
|
||||
""" Test multiple attendances in one day """
|
||||
with freeze_time("2021-01-04"):
|
||||
# Two attendances: 6 hours + 6 hours = 12 hours (10 expected + 2 overtime at 200%)
|
||||
self.env['hr.attendance'].create([
|
||||
{
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 14, 0)
|
||||
},
|
||||
{
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 14, 0),
|
||||
'check_out': datetime(2021, 1, 4, 20, 0)
|
||||
}
|
||||
])
|
||||
|
||||
self.assertAlmostEqual(self.employee.total_overtime, 4.0, 2, msg="2 hours overtime at 200% should yield 4 hours total overtime")
|
||||
|
||||
def test_partial_week(self):
|
||||
""" Test partial week with overtime """
|
||||
with freeze_time("2021-01-04"):
|
||||
# Week: Mon-Wed, 12 hours/day = 36 hours total (no weekly overtime, daily overtime applies)
|
||||
[
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, day, 8, 0),
|
||||
'check_out': datetime(2021, 1, day, 20, 0)
|
||||
}) for day in range(4, 7) # Monday to Wednesday
|
||||
]
|
||||
|
||||
self.assertAlmostEqual(self.employee.total_overtime, 12.0, 2, msg="3 days of 2 hours overtime at 200% should yield 12 hours total overtime")
|
||||
|
||||
def test_access_ruleset_on_employee(self):
|
||||
"""
|
||||
Test the access rights of the ruleset on the employee
|
||||
Only the employee admin should be able to see and change the ruleset on the employee
|
||||
"""
|
||||
user = new_test_user(self.env, login='usr', groups='hr.group_hr_user', company_id=self.company.id).with_company(self.company)
|
||||
employee = self.env['hr.employee'].with_company(self.company).create({'name': "Employee Test"})
|
||||
with Form(employee.with_user(user)) as employee_form:
|
||||
self.assertFalse("ruleset_id" in employee_form._view['fields'])
|
||||
|
||||
# HR Mangers should be able to see the ruleset on the employee
|
||||
user.group_ids |= self.env.ref('hr.group_hr_manager')
|
||||
# fix le truc chelou de pas pouvoir ecrire la surrement les access rule
|
||||
with Form(employee.with_user(user)) as employee_form:
|
||||
self.assertTrue("ruleset_id" in employee_form._view['fields'])
|
||||
employee_form.record.ruleset_id = self.ruleset.id
|
||||
|
||||
def test_is_manager_with_overtime(self):
|
||||
""" Test the computation of is_manager with overtime """
|
||||
user = new_test_user(self.env, login='usr', groups='hr_attendance.group_hr_attendance_officer', company_id=self.company.id).with_company(self.company)
|
||||
self.employee.attendance_manager_id = user.id
|
||||
attendance = self.env['hr.attendance'].with_company(self.company).create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 20, 0)
|
||||
})
|
||||
self.assertTrue(attendance.with_user(user).linked_overtime_ids.is_manager)
|
||||
|
|
@ -0,0 +1,553 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import Form, HttpCase, new_test_user
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
|
||||
@tagged('hr_attendance_overtime')
|
||||
class TestHrAttendanceUndertime(HttpCase):
|
||||
""" Tests for undertime """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def set_calendar_and_tz(employee, tz):
|
||||
employee.resource_calendar_id = cls.env['resource.calendar'].create({
|
||||
'name': f'Default Calendar ({tz})',
|
||||
'tz': tz,
|
||||
})
|
||||
super().setUpClass()
|
||||
|
||||
cls.company = cls.env['res.company'].create({
|
||||
'name': 'SweatChipChop Inc.',
|
||||
'attendance_overtime_validation': 'no_validation',
|
||||
'absence_management': True,
|
||||
})
|
||||
cls.company.resource_calendar_id.tz = 'Europe/Brussels'
|
||||
cls.ruleset = cls.env['hr.attendance.overtime.ruleset'].with_company(cls.company).create({
|
||||
'name': 'Ruleset schedule quantity',
|
||||
'rule_ids': [Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'expected_hours_from_contract': True,
|
||||
'quantity_period': 'day',
|
||||
})],
|
||||
})
|
||||
|
||||
cls.company_1 = cls.env['res.company'].create({
|
||||
'name': 'Overtime Inc.',
|
||||
'absence_management': True,
|
||||
})
|
||||
cls.company_1.resource_calendar_id.tz = 'Europe/Brussels'
|
||||
cls.ruleset_1 = cls.env['hr.attendance.overtime.ruleset'].with_company(cls.company_1).create({
|
||||
'name': 'Ruleset schedule quantity',
|
||||
'rule_ids': [Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'expected_hours_from_contract': True,
|
||||
'quantity_period': 'day',
|
||||
})],
|
||||
})
|
||||
|
||||
cls.user = new_test_user(cls.env, login='fru', groups='base.group_user,hr_attendance.group_hr_attendance_manager', company_id=cls.company.id).with_company(cls.company)
|
||||
cls.employee = cls.env['hr.employee'].create({
|
||||
'name': "Marie-Edouard De La Court",
|
||||
'user_id': cls.user.id,
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'UTC',
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'resource_calendar_id': cls.company.resource_calendar_id.id,
|
||||
'ruleset_id': cls.ruleset.id,
|
||||
})
|
||||
cls.other_employee = cls.env['hr.employee'].create({
|
||||
'name': 'Yolanda',
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'UTC',
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'resource_calendar_id': cls.company.resource_calendar_id.id,
|
||||
'ruleset_id': cls.ruleset.id,
|
||||
})
|
||||
cls.jpn_employee = cls.env['hr.employee'].create({
|
||||
'name': 'Sacha',
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'Asia/Tokyo',
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'resource_calendar_id': cls.company.resource_calendar_id.id,
|
||||
'ruleset_id': cls.ruleset.id,
|
||||
})
|
||||
set_calendar_and_tz(cls.jpn_employee, 'Asia/Tokyo')
|
||||
|
||||
cls.honolulu_employee = cls.env['hr.employee'].create({
|
||||
'name': 'Susan',
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'Pacific/Honolulu',
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'resource_calendar_id': cls.company.resource_calendar_id.id,
|
||||
'ruleset_id': cls.ruleset.id,
|
||||
})
|
||||
set_calendar_and_tz(cls.honolulu_employee, 'Pacific/Honolulu')
|
||||
|
||||
cls.europe_employee = cls.env['hr.employee'].with_company(cls.company_1).create({
|
||||
'name': 'Schmitt',
|
||||
'company_id': cls.company_1.id,
|
||||
'tz': 'Europe/Brussels',
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'resource_calendar_id': cls.company_1.resource_calendar_id.id,
|
||||
'ruleset_id': cls.ruleset_1.id,
|
||||
})
|
||||
set_calendar_and_tz(cls.europe_employee, 'Europe/Brussels')
|
||||
|
||||
cls.no_contract_employee = cls.env['hr.employee'].create({
|
||||
'name': 'No Contract',
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'UTC',
|
||||
'resource_calendar_id': cls.company.resource_calendar_id.id,
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': False,
|
||||
})
|
||||
cls.future_contract_employee = cls.env['hr.employee'].create({
|
||||
'name': 'Future contract',
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'UTC',
|
||||
'resource_calendar_id': cls.company.resource_calendar_id.id,
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2030, 1, 1),
|
||||
})
|
||||
|
||||
cls.calendar_flex_40h = cls.env['resource.calendar'].create({
|
||||
'name': 'Flexible 40 hours/week',
|
||||
'company_id': cls.company.id,
|
||||
'hours_per_day': 8,
|
||||
'hours_per_week': 40,
|
||||
'flexible_hours': True,
|
||||
'full_time_required_hours': 40,
|
||||
})
|
||||
|
||||
cls.flexible_employee = cls.env['hr.employee'].create({
|
||||
'name': 'Flexi',
|
||||
'company_id': cls.company.id,
|
||||
'tz': 'UTC',
|
||||
'resource_calendar_id': cls.calendar_flex_40h.id,
|
||||
'date_version': date(2020, 1, 1),
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'ruleset_id': cls.ruleset.id,
|
||||
})
|
||||
|
||||
def test_overtime_company_settings(self):
|
||||
self.company.write({
|
||||
"attendance_overtime_validation": "by_manager",
|
||||
})
|
||||
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 20, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(attendance.overtime_status, 'to_approve')
|
||||
self.assertAlmostEqual(attendance.validated_overtime_hours, 0, 2)
|
||||
self.assertEqual(attendance.employee_id.total_overtime, 0)
|
||||
|
||||
attendance.action_approve_overtime()
|
||||
|
||||
self.assertEqual(attendance.overtime_status, 'approved')
|
||||
self.assertAlmostEqual(attendance.validated_overtime_hours, 3, 2)
|
||||
self.assertAlmostEqual(attendance.employee_id.total_overtime, 3, 2)
|
||||
|
||||
attendance.action_refuse_overtime()
|
||||
self.assertEqual(attendance.employee_id.total_overtime, 0, 0)
|
||||
|
||||
def test_simple_undertime(self):
|
||||
checkin_am = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
})
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.other_employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 15, 0),
|
||||
})
|
||||
|
||||
overtime = self.env['hr.attendance.overtime.line'].search([('employee_id', '=', self.employee.id), ('date', '=', date(2021, 1, 4))])
|
||||
self.assertFalse(overtime, 'No overtime record should exist for that employee')
|
||||
|
||||
checkin_am.write({'check_out': datetime(2021, 1, 4, 12, 0)})
|
||||
overtime = checkin_am._linked_overtimes()
|
||||
self.assertTrue(overtime, 'An overtime record should be created')
|
||||
self.assertEqual(overtime.duration, -4)
|
||||
|
||||
checkin_pm = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 13, 0),
|
||||
})
|
||||
overtime = checkin_pm._linked_overtimes()
|
||||
self.assertFalse(overtime.exists(), 'Overtime duration should not exist when an attendance has not been checked out.')
|
||||
checkin_pm.write({'check_out': datetime(2021, 1, 4, 18, 0)})
|
||||
overtime = self.env['hr.attendance.overtime.line'].search([('employee_id', '=', self.employee.id), ('date', '=', date(2021, 1, 4))])
|
||||
self.assertAlmostEqual(overtime.duration, 1)
|
||||
self.assertAlmostEqual(self.employee.total_overtime, 1)
|
||||
|
||||
def test_simple_undertime_multiple_rules(self):
|
||||
""" Checks that only the least consequent undertime of the rules is considered."""
|
||||
ruleset = self.env['hr.attendance.overtime.ruleset'].with_company(self.company).create({
|
||||
'name': 'Ruleset schedule quantity',
|
||||
'rule_ids': [Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'expected_hours_from_contract': False,
|
||||
'expected_hours': 8.0,
|
||||
'quantity_period': 'day',
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'expected_hours_from_contract': False,
|
||||
'expected_hours': 10.0,
|
||||
'quantity_period': 'day',
|
||||
})],
|
||||
})
|
||||
self.employee.ruleset_id = ruleset
|
||||
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 13, 0),
|
||||
'check_out': datetime(2021, 1, 4, 18, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(attendance.overtime_hours, -3.0)
|
||||
overtime = attendance._linked_overtimes()
|
||||
self.assertEqual(len(overtime), 1, 'Only one overtime record should be created')
|
||||
self.assertEqual(overtime.duration, -3.0)
|
||||
|
||||
def test_simple_undertime_multiple_rules_on_several_periods(self):
|
||||
"""Whatever the period type, only the least consequent undertime of the rules is considered.
|
||||
"""
|
||||
ruleset = self.env['hr.attendance.overtime.ruleset'].with_company(self.company).create({
|
||||
'name': 'Ruleset schedule quantity',
|
||||
'rule_ids': [Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'expected_hours_from_contract': False,
|
||||
'expected_hours': 8.0,
|
||||
'quantity_period': 'day',
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'expected_hours_from_contract': False,
|
||||
'expected_hours': 40.0,
|
||||
'quantity_period': 'week',
|
||||
})],
|
||||
})
|
||||
self.employee.ruleset_id = ruleset
|
||||
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 13, 0),
|
||||
'check_out': datetime(2021, 1, 4, 18, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(attendance.overtime_hours, -3.0)
|
||||
overtime = attendance._linked_overtimes()
|
||||
self.assertEqual(len(overtime), 1, 'Only one overtime record should be created')
|
||||
self.assertEqual(overtime.duration, -3.0)
|
||||
|
||||
def test_undertime_change_employee(self):
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 7, 0),
|
||||
'check_out': datetime(2021, 1, 4, 15, 0),
|
||||
})
|
||||
|
||||
self.assertEqual(self.employee.total_overtime, -1)
|
||||
self.assertEqual(self.other_employee.total_overtime, 0)
|
||||
|
||||
self.other_employee.ruleset_id = self.ruleset
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.other_employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 7, 0),
|
||||
'check_out': datetime(2021, 1, 4, 15, 0),
|
||||
})
|
||||
attendance.unlink()
|
||||
self.assertEqual(self.other_employee.total_overtime, -1)
|
||||
self.assertEqual(self.employee.total_overtime, 0)
|
||||
|
||||
def test_undertime_far_timezones(self):
|
||||
# Since dates have to be stored in utc these are the tokyo timezone times for 7-12 / 13-18 (UTC+9)
|
||||
(self.jpn_employee | self.honolulu_employee).ruleset_id = self.ruleset
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.jpn_employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 1, 0),
|
||||
'check_out': datetime(2021, 1, 4, 4, 0),
|
||||
})
|
||||
# Lunch time is at 3-4 UTC in Tokyo, and we don't work during lunch.
|
||||
|
||||
# Same but for alaskan times (UTC-10)
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.honolulu_employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 17, 0),
|
||||
'check_out': datetime(2021, 1, 4, 20, 0),
|
||||
})
|
||||
self.assertAlmostEqual(self.jpn_employee.total_overtime, -6, 2)
|
||||
self.assertAlmostEqual(self.honolulu_employee.total_overtime, -5, 2)
|
||||
|
||||
def test_undertime_lunch(self):
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 0),
|
||||
'check_out': datetime(2021, 1, 4, 13, 0),
|
||||
})
|
||||
self.assertEqual(self.employee.total_overtime, -4, 'There should be only -4 since the employee did not work through the lunch period.')
|
||||
|
||||
def test_undertime_hours_with_multiple_attendance(self):
|
||||
m_attendance_1 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2023, 1, 3, 8, 0),
|
||||
'check_out': datetime(2023, 1, 3, 12, 0),
|
||||
})
|
||||
self.assertAlmostEqual(m_attendance_1.overtime_hours, -4, 2)
|
||||
|
||||
m_attendance_2 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2023, 1, 3, 13, 0),
|
||||
'check_out': datetime(2023, 1, 3, 17, 0),
|
||||
})
|
||||
self.assertAlmostEqual(m_attendance_1.overtime_hours, 0, 2)
|
||||
self.assertAlmostEqual(m_attendance_2.overtime_hours, 0, 2)
|
||||
|
||||
m_attendance_3 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2023, 1, 3, 18, 0),
|
||||
'check_out': datetime(2023, 1, 3, 19, 0),
|
||||
})
|
||||
self.assertAlmostEqual(m_attendance_1.overtime_hours, 0, 2)
|
||||
self.assertAlmostEqual(m_attendance_2.overtime_hours, 0, 2)
|
||||
self.assertAlmostEqual(m_attendance_3.overtime_hours, 1, 2)
|
||||
|
||||
overtime_2 = self.env['hr.attendance.overtime.line'].search([('employee_id', '=', self.employee.id),
|
||||
('date', '=', datetime(2023, 1, 3))])
|
||||
# Total overtime for that day : 5 hours
|
||||
self.assertEqual(len(overtime_2), 1, "Only one overtime record should be created for that day.")
|
||||
self.assertEqual(overtime_2.duration, 1)
|
||||
|
||||
# Attendance Modification case
|
||||
|
||||
m_attendance_3.write({
|
||||
'check_out': datetime(2023, 1, 3, 20, 00),
|
||||
})
|
||||
self.assertAlmostEqual(m_attendance_3.overtime_hours, 2, 2)
|
||||
|
||||
# Deleting previous attendances should update correctly the overtime hours in other attendances
|
||||
m_attendance_2.unlink()
|
||||
self.assertAlmostEqual(m_attendance_3.overtime_hours, -2, 2)
|
||||
|
||||
def test_undertime_across_days_timezones(self):
|
||||
self.europe_employee.ruleset_id = self.ruleset
|
||||
|
||||
early_attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.europe_employee.id,
|
||||
'check_in': datetime(2024, 5, 27, 23, 30),
|
||||
'check_out': datetime(2024, 5, 28, 4, 30),
|
||||
})
|
||||
self.assertAlmostEqual(early_attendance.overtime_hours, -3, 2)
|
||||
|
||||
# Total overtime for that day : -3 hours
|
||||
overtime_record = early_attendance.linked_overtime_ids
|
||||
self.assertAlmostEqual(overtime_record.duration, -3, 2)
|
||||
|
||||
# Check that the calendar's timezones take priority and that overtimes and attendances dates are consistent
|
||||
self.europe_employee.resource_calendar_id.tz = 'America/New_York'
|
||||
|
||||
early_attendance2 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.europe_employee.id,
|
||||
'check_in': datetime(2024, 5, 30, 3, 0), # 23:00 NY prev day
|
||||
'check_out': datetime(2024, 5, 30, 10, 0), # 6:00 NY
|
||||
})
|
||||
# First day you only work 1 hour and second day you work 6 hours, that's -1 hours of overtime
|
||||
self.assertAlmostEqual(early_attendance2.overtime_hours, -1, 2)
|
||||
|
||||
overtime_record2 = early_attendance2.linked_overtime_ids
|
||||
self.assertEqual(len(overtime_record2), 1, "One undertime records should be created for that attendance.")
|
||||
self.assertAlmostEqual(overtime_record2.duration, -1, 2)
|
||||
|
||||
early_attendance3 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.europe_employee.id,
|
||||
'check_in': datetime(2024, 5, 31, 4, 0), # 00:00 NY
|
||||
'check_out': datetime(2024, 5, 31, 10, 0), # 6:00 NY
|
||||
})
|
||||
self.assertAlmostEqual(early_attendance3.overtime_hours, -2, 2)
|
||||
|
||||
def test_undertime_hours_flexible_resource(self):
|
||||
""" Test the computation of overtime hours for a single flexible resource with 8 hours_per_day.
|
||||
=========
|
||||
Test Case
|
||||
1) | 8:00 | 16:00 | -> No overtime
|
||||
2) | 12:00 | 18:00 | -> -2 hours of overtime
|
||||
3) | 10:00 | 22:00 | -> 4 hours of overtime
|
||||
"""
|
||||
self.flexible_employee.ruleset_id = self.ruleset
|
||||
# 1) 8:00 - 16:00 should contain 0 hours of overtime
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.flexible_employee.id,
|
||||
'check_in': datetime(2023, 1, 2, 8, 0),
|
||||
'check_out': datetime(2023, 1, 2, 16, 0),
|
||||
})
|
||||
self.assertEqual(attendance.overtime_hours, 0, 'There should be no overtime for the flexible resource.')
|
||||
|
||||
# 2) 12:00 - 18:00 should contain -2 hours of overtime
|
||||
# as we expect the employee to work 8 hours per day
|
||||
attendance.write({
|
||||
'check_in': datetime(2023, 1, 3, 12, 0),
|
||||
'check_out': datetime(2023, 1, 3, 18, 0),
|
||||
})
|
||||
self.assertAlmostEqual(attendance.overtime_hours, -2, 2, 'There should be -2 hours of overtime for the flexible resource.')
|
||||
|
||||
# 3) 10:00 - 22:00 should contain 4 hours of overtime
|
||||
attendance.write({
|
||||
'check_in': datetime(2023, 1, 4, 10, 0),
|
||||
'check_out': datetime(2023, 1, 4, 22, 0),
|
||||
})
|
||||
self.assertAlmostEqual(attendance.overtime_hours, 4, 2, 'There should be 4 hours of overtime for the flexible resource.')
|
||||
|
||||
def test_undertime_hours_multiple_flexible_resources(self):
|
||||
""" Test the computation of overtime hours for multiple flexible resources on a single workday with 8 hours_per_day.
|
||||
=========
|
||||
|
||||
We should see that the overtime hours are recomputed correctly when new attendance records are created.
|
||||
|
||||
Test Case
|
||||
1) | 8:00 | 12:00 | -> -4 hours of overtime
|
||||
2) (| 8:00 | 12:00 |, | 13:00 | 15:00 |) -> (0, -2) hours of overtime
|
||||
3) (| 8:00 | 12:00 |, | 13:00 | 15:00 |, | 16:00 | 18:00 |) -> (0, 0, 0) hours of overtime
|
||||
"""
|
||||
self.flexible_employee.ruleset_id = self.ruleset
|
||||
|
||||
# 1) 8:00 - 12:00 should contain -4 hours of overtime
|
||||
attendance_1 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.flexible_employee.id,
|
||||
'check_in': datetime(2023, 1, 2, 8, 0),
|
||||
'check_out': datetime(2023, 1, 2, 12, 0),
|
||||
})
|
||||
self.assertAlmostEqual(attendance_1.overtime_hours, -4, 2, 'There should be -4 hours of overtime for the flexible resource.')
|
||||
|
||||
# 2) 8:00 - 12:00 and 13:00 - 15:00 should contain 0 and -2 hours of overtime
|
||||
attendance_2 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.flexible_employee.id,
|
||||
'check_in': datetime(2023, 1, 2, 13, 0),
|
||||
'check_out': datetime(2023, 1, 2, 15, 0),
|
||||
})
|
||||
self.assertEqual(attendance_1.overtime_hours, 0, 'There should be no overtime for the flexible resource.')
|
||||
self.assertAlmostEqual(attendance_2.overtime_hours, -2, 2, 'There should be -2 hours of overtime for the flexible resource.')
|
||||
|
||||
# 3) 8:00 - 12:00, 13:00 - 15:00 and 16:00 - 18:00 should contain 0, 0 and 0 hours of overtime
|
||||
attendance_3 = self.env['hr.attendance'].create({
|
||||
'employee_id': self.flexible_employee.id,
|
||||
'check_in': datetime(2023, 1, 2, 16, 0),
|
||||
'check_out': datetime(2023, 1, 2, 18, 0),
|
||||
})
|
||||
self.assertEqual(attendance_1.overtime_hours, 0, 'There should be no overtime for the flexible resource.')
|
||||
self.assertEqual(attendance_2.overtime_hours, 0, 'There should be no overtime for the flexible resource.')
|
||||
self.assertEqual(attendance_3.overtime_hours, 0, 'There should be no overtime for the flexible resource.')
|
||||
|
||||
def test_refuse_overtime(self):
|
||||
self.company.write({
|
||||
"attendance_overtime_validation": "by_manager",
|
||||
})
|
||||
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2023, 1, 2, 8, 0),
|
||||
'check_out': datetime(2023, 1, 2, 12, 0),
|
||||
})
|
||||
|
||||
overtime = self.env['hr.attendance.overtime.line'].search([('employee_id', '=', self.employee.id)])
|
||||
overtime.action_approve()
|
||||
|
||||
self.assertEqual(attendance.validated_overtime_hours, -4)
|
||||
self.assertEqual(attendance.overtime_hours, attendance.validated_overtime_hours)
|
||||
|
||||
attendance.action_refuse_overtime()
|
||||
self.assertEqual(attendance.validated_overtime_hours, 0)
|
||||
|
||||
def test_no_validation_extra_hours_change(self):
|
||||
"""
|
||||
In case of attendances requiring no validation, check that extra hours are not recomputed
|
||||
if the value is different from `validated_hours` (meaning it has been modified by the user).
|
||||
"""
|
||||
self.company.attendance_overtime_validation = "no_validation"
|
||||
|
||||
attendance = self.env['hr.attendance']
|
||||
# Form is used here as it will send a `validated_overtime_hours` value of 0 when saved.
|
||||
# This should not be considered as a manual edition of the field by the user.
|
||||
with Form(attendance) as attendance_form:
|
||||
attendance_form.employee_id = self.employee
|
||||
attendance_form.check_in = datetime(2023, 1, 2, 8, 0)
|
||||
attendance_form.check_out = datetime(2023, 1, 2, 15, 0)
|
||||
attendance = attendance_form.save()
|
||||
|
||||
self.assertAlmostEqual(attendance.overtime_hours, -2, 2)
|
||||
self.assertAlmostEqual(attendance.validated_overtime_hours, -2, 2)
|
||||
|
||||
attendance.linked_overtime_ids.manual_duration = previous = -1.5
|
||||
self.assertNotEqual(attendance.validated_overtime_hours, attendance.overtime_hours)
|
||||
|
||||
# Create another attendance for the same employee
|
||||
self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2023, 1, 4, 8, 0),
|
||||
'check_out': datetime(2023, 1, 4, 18, 0),
|
||||
})
|
||||
self.assertEqual(attendance.validated_overtime_hours, previous, "Extra hours shouldn't be recomputed")
|
||||
|
||||
def test_overtime_employee_tolerance(self):
|
||||
self.ruleset.rule_ids[0].employee_tolerance = 10 / 60
|
||||
self.env['hr.attendance'].create([
|
||||
{
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 8, 5),
|
||||
'check_out': datetime(2021, 1, 4, 12, 0),
|
||||
},
|
||||
{
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 4, 13, 0),
|
||||
'check_out': datetime(2021, 1, 4, 16, 55),
|
||||
}
|
||||
])
|
||||
|
||||
overtime = self.env['hr.attendance.overtime.line'].search([('employee_id', '=', self.employee.id)])
|
||||
self.assertFalse(overtime, 'No overtime should be counted because of the tolerance.')
|
||||
|
||||
self.ruleset.rule_ids[0].employee_tolerance = 4 / 60
|
||||
self.ruleset.action_regenerate_overtimes()
|
||||
|
||||
overtime = self.env['hr.attendance.overtime.line'].search([('employee_id', '=', self.employee.id)])
|
||||
self.assertTrue(overtime, 'Overtime entry should exist since the tolerance has been lowered.')
|
||||
self.assertAlmostEqual(overtime.duration, -(10 / 60), places=2, msg='Overtime should be equal to -10 minutes.')
|
||||
|
||||
def test_overtime_on_multiple_days(self):
|
||||
attendance = self.env['hr.attendance'].create({
|
||||
'employee_id': self.employee.id,
|
||||
'check_in': datetime(2021, 1, 8, 8, 0), # Friday 8 AM - 17 PM work, 17 - 24 overtime (7 hours)
|
||||
'check_out': datetime(2021, 1, 9, 3, 0), # Saturday 0-3 AM overtime (3 hours)
|
||||
})
|
||||
|
||||
overtime = attendance._linked_overtimes()
|
||||
self.assertEqual(len(overtime), 2, 'There should be 2 overtime records for that attendance.')
|
||||
self.assertEqual(sum(overtime.mapped('duration')), 10, 'There should be a total of 10 hours of overtime for that attendance.')
|
||||
|
||||
attendance.write({
|
||||
'check_out': datetime(2021, 1, 8, 20, 0),
|
||||
})
|
||||
|
||||
overtime = attendance._linked_overtimes()
|
||||
self.assertEqual(len(overtime), 1, 'There should have only 1 overtime for that attendance after modification.')
|
||||
self.assertEqual(sum(overtime.mapped('duration')), 3, 'There should be a total of 3 hours of overtime for that attendance after modification.')
|
||||
|
||||
all_overtimes = self.env['hr.attendance.overtime.line'].search([('employee_id', '=', self.employee.id)])
|
||||
self.assertEqual(len(all_overtimes), 1, 'There should be only 1 overtime record in total for that employee.')
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestHrAttendanceScenario(TransactionCase):
|
||||
def test_load_scenario(self):
|
||||
self.env['hr.attendance']._load_demo_data()
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from dateutil.rrule import DAILY, rrule
|
||||
import logging
|
||||
import time
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'hr_attendance_perf')
|
||||
class TestHrAttendancePerformance(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.env.user.company_id = cls.env['res.company'].create({'name': 'Flower Corporation'})
|
||||
cls.calendar_38h = cls.env['resource.calendar'].create({
|
||||
'name': 'Standard 38 hours/week',
|
||||
'tz': 'Europe/Brussels',
|
||||
'company_id': False,
|
||||
'hours_per_day': 7.6,
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Monday Lunch', 'dayofweek': '0', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
|
||||
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday Lunch', 'dayofweek': '1', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
|
||||
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday Lunch', 'dayofweek': '2', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
|
||||
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
|
||||
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday Lunch', 'dayofweek': '4', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
|
||||
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'})
|
||||
],
|
||||
})
|
||||
|
||||
cls.ruleset = cls.env['hr.attendance.overtime.ruleset'].create({
|
||||
'name': 'Ruleset schedule quantity',
|
||||
'rule_ids': [
|
||||
Command.create({
|
||||
'name': 'Rule schedule quantity',
|
||||
'base_off': 'quantity',
|
||||
'expected_hours_from_contract': True,
|
||||
'quantity_period': 'day',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
employees = cls.env['hr.employee'].create([{
|
||||
'name': f'Employee {i}',
|
||||
'sex': 'male',
|
||||
'birthday': '1982-08-01',
|
||||
'country_id': cls.env.ref('base.us').id,
|
||||
'wage': 5000.0,
|
||||
'date_version': date.today() - relativedelta(months=2),
|
||||
'contract_date_start': date.today() - relativedelta(months=2),
|
||||
'contract_date_end': False,
|
||||
'resource_calendar_id': cls.calendar_38h.id,
|
||||
'ruleset_id': cls.ruleset.id,
|
||||
} for i in range(100)])
|
||||
for employee in employees:
|
||||
employee.create_version({'date_version': date.today() - relativedelta(months=1, days=15), 'wage': 5500})
|
||||
employee.create_version({'date_version': date.today() - relativedelta(months=1), 'wage': 6000})
|
||||
|
||||
vals = []
|
||||
for employee in employees:
|
||||
for day in rrule(DAILY, dtstart=date.today() - relativedelta(months=2), until=date.today()):
|
||||
vals.append({
|
||||
'employee_id': employee.id,
|
||||
'check_in': day.replace(hour=8, minute=0),
|
||||
'check_out': day.replace(hour=17, minute=36),
|
||||
})
|
||||
cls.attendances = cls.env['hr.attendance'].create(vals)
|
||||
|
||||
def test_regenerate_overtime_line(self):
|
||||
t0 = time.time()
|
||||
with self.assertQueryCount(1700):
|
||||
self.ruleset.action_regenerate_overtimes()
|
||||
t1 = time.time()
|
||||
_logger.info("Regenerated overtime for %s hr.attendance records in %s seconds.",
|
||||
len(self.attendances.ids), t1 - t0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue