19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

View file

@ -1,3 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_hr_work_entry
from . import (
test_global_time_off,
test_hr_work_entry,
test_work_entry,
test_work_entry_type_data,
)

View file

@ -0,0 +1,73 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo.fields import Date
from odoo.tests.common import TransactionCase
class TestWorkEntryBase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.tz = 'Europe/Brussels'
cls.env.ref('resource.resource_calendar_std').tz = 'Europe/Brussels'
cls.dep_rd = cls.env['hr.department'].create({
'name': 'Research & Development - Test',
})
# I create a new employee "Richard"
cls.richard_emp = cls.env['hr.employee'].create({
'name': 'Richard',
'sex': 'male',
'birthday': '1984-05-01',
'country_id': cls.env.ref('base.be').id,
'department_id': cls.dep_rd.id,
'wage': 5000.0,
'date_version': Date.to_date('2018-01-01'),
'contract_date_start': Date.to_date('2018-01-01'),
'contract_date_end': Date.today() + relativedelta(years=2),
})
cls.work_entry_type = cls.env['hr.work.entry.type'].create({
'name': 'Extra attendance',
'is_leave': False,
'code': 'WORKTEST200',
})
cls.work_entry_type_unpaid = cls.env['hr.work.entry.type'].create({
'name': 'Unpaid Time Off',
'is_leave': True,
'code': 'LEAVETEST300',
})
cls.work_entry_type_leave = cls.env['hr.work.entry.type'].create({
'name': 'Time Off',
'is_leave': True,
'code': 'LEAVETEST100'
})
def create_work_entry(self, start, stop, work_entry_type=None):
work_entry_type = work_entry_type or self.work_entry_type
return self.create_work_entries([(start, stop, work_entry_type)])
def create_work_entries(self, intervals):
default_work_entry_type = self.work_entry_type
create_vals = []
for interval in intervals:
start = interval[0]
stop = interval[1]
work_entry_type = interval[2] if len(interval) == 3\
else default_work_entry_type
create_vals.append({
'version_id': self.richard_emp.version_ids[0].id,
'name': 'Work entry %s-%s' % (start, stop),
'date_start': start,
'date_stop': stop,
'employee_id': self.richard_emp.id,
'work_entry_type_id': work_entry_type.id,
})
create_vals = self.env['hr.version']._generate_work_entries_postprocess(create_vals)
return self.env['hr.work.entry'].create(create_vals)

View file

@ -0,0 +1,51 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from .common import TestWorkEntryBase
from datetime import datetime
from odoo.tests import tagged
@tagged('-at_install', 'post_install')
class TestGlobalTimeOff(TestWorkEntryBase):
def test_gto_other_calendar(self):
# Tests that a global time off in another calendar does not affect work entry generation
# for other calendars
other_calendar = self.env['resource.calendar'].create({
'name': 'other calendar',
})
start = datetime(2018, 1, 1, 0, 0, 0)
end = datetime(2018, 1, 1, 23, 59, 59)
leave = self.env['resource.calendar.leaves'].create({
'date_from': start,
'date_to': end,
'calendar_id': other_calendar.id,
'work_entry_type_id': self.work_entry_type_leave.id,
})
contract = self.richard_emp.version_id
contract.date_generated_from = start
contract.date_generated_to = start
work_entries = contract.generate_work_entries(start.date(), end.date())
self.assertEqual(work_entries.work_entry_type_id.id, contract._get_default_work_entry_type_id())
work_entries.unlink()
contract.date_generated_from = start
contract.date_generated_to = start
leave.calendar_id = contract.resource_calendar_id
work_entries = contract.generate_work_entries(start.date(), end.date())
self.assertEqual(work_entries.work_entry_type_id, leave.work_entry_type_id)
def test_gto_no_calendar(self):
start = datetime(2018, 1, 1, 0, 0, 0)
end = datetime(2018, 1, 1, 23, 59, 59)
leave = self.env['resource.calendar.leaves'].create({
'date_from': start,
'date_to': end,
'work_entry_type_id': self.work_entry_type_leave.id,
})
contract = self.richard_emp.version_id
contract.date_generated_from = start
contract.date_generated_to = start
work_entries = contract.generate_work_entries(start.date(), end.date())
self.assertEqual(work_entries.work_entry_type_id, leave.work_entry_type_id)

View file

@ -1,6 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger
class TestHrWorkEntry(TransactionCase):
@ -17,10 +21,15 @@ class TestHrWorkEntry(TransactionCase):
cls.employee_a = cls.env['hr.employee'].create({
'name': 'Employee A',
'company_id': cls.company_a.id,
'contract_date_start': '2023-01-01',
'date_version': '2023-01-01',
})
cls.employee_a_first_version = cls.employee_a.version_ids[0]
cls.employee_b = cls.env['hr.employee'].create({
'name': 'Employee B',
'company_id': cls.company_b.id,
'contract_date_start': '2023-01-01',
'date_version': '2023-01-01',
})
# Create a work entry type
cls.work_entry_type = cls.env['hr.work.entry.type'].create({
@ -34,11 +43,141 @@ class TestHrWorkEntry(TransactionCase):
'name': 'Test Work Entry',
'employee_id': self.employee_b.id,
'work_entry_type_id': self.work_entry_type.id,
'date_start': '2024-01-01 08:00:00',
'date_stop': '2024-01-01 16:00:00',
'date': date(2024, 1, 1),
'duration': 8,
})
self.assertEqual(
work_entry.company_id, self.employee_b.company_id,
"Work entry should use the employee's company not the current user's company.",
)
def test_work_entry_conflict_no_we_type(self):
"""Test that work entry conflicts with no work entry type."""
work_entry = self.env['hr.work.entry'].create({
'name': 'Test Work Entry',
'work_entry_type_id': False,
'employee_id': self.employee_b.id,
'date': date(2024, 1, 1),
'duration': 8,
})
self.assertEqual(
work_entry.state, 'conflict',
"Work entry should conflict with no work entry type.",
)
work_entry = self.env['hr.work.entry'].create({
'name': 'Test Work Entry',
'work_entry_type_id': self.work_entry_type.id,
'employee_id': self.employee_b.id,
'date': date(2024, 1, 1),
'duration': 8,
})
self.assertEqual(
work_entry.state, 'draft',
"Work entry should not conflict with a work entry type.",
)
work_entry.write({'work_entry_type_id': False})
self.assertEqual(
work_entry.state, 'conflict',
"Work entry should conflict with no work entry type.",
)
def test_work_entry_conflict_sum_duration(self):
"""Test that work entry conflicts when the duration for one day is <= 0h or > 24h."""
with self.assertRaises(ValidationError), mute_logger('odoo.sql_db'):
self.env['hr.work.entry'].create({
'name': 'Test Work Entry',
'work_entry_type_id': False,
'employee_id': self.employee_b.id,
'date': date(2024, 1, 1),
'duration': 0,
})
work_entry = self.env['hr.work.entry'].create({
'name': 'Test Work Entry',
'work_entry_type_id': self.work_entry_type.id,
'employee_id': self.employee_b.id,
'date': date(2024, 1, 1),
'duration': 8,
})
self.assertEqual(
work_entry.state, 'draft',
"Work entry should be in draft.",
)
work_entry_2 = self.env['hr.work.entry'].create({
'name': 'Test Work Entry 2',
'work_entry_type_id': self.work_entry_type.id,
'employee_id': self.employee_b.id,
'date': date(2024, 1, 1),
'duration': 17,
})
self.assertEqual(
(work_entry | work_entry_2).mapped('state'), ['conflict', 'conflict'],
"Work entries with a total duration for a same day <= 0h or > 24h should conflict.",
)
work_entry_2.write({
'duration': 16,
})
self.assertEqual(
(work_entry | work_entry_2).mapped('state'), ['draft', 'draft'],
"Work entries with a total duration for a same day > 0h and <= 24h should not conflict.",
)
def test_nullify_work_entry_tz(self):
"""
Test that the work entries of the previous month are not affected when regenerating the next month work entries
no matter what's the timezone of the employee
"""
self.employee_a.tz = 'Europe/Brussels'
self.employee_a.resource_calendar_id.tz = 'Europe/Brussels'
january_work_entries = self.employee_a.generate_work_entries(date(2024, 1, 1), date(2024, 1, 31), force=True)
self.employee_a.generate_work_entries(date(2024, 2, 1), date(2024, 2, 28), force=True)
new_january_work_entries = self.env['hr.work.entry'].search([
('employee_id', '=', self.employee_a.id),
('date', '>=', date(2024, 1, 1)),
('date', '<=', date(2024, 1, 31)),
])
self.assertEqual(january_work_entries, new_january_work_entries)
def test_nullify_work_entry(self):
"""
Test that we correctly nullify the work entries that were previously generated when we add a new version
"""
january_work_entries = self.employee_a.generate_work_entries(date(2024, 1, 1), date(2024, 1, 31))
self.assertTrue(all(we.version_id == self.employee_a_first_version for we in january_work_entries))
second_version = self.employee_a.create_version({
'date_version': date(2023, 12, 1)
})
self.employee_a.generate_work_entries(date(2024, 1, 1), date(2024, 1, 31))
all_january_work_entries = self.env['hr.work.entry'].search([
('employee_id', '=', self.employee_a.id),
('date', '>=', date(2024, 1, 1)),
('date', '<=', date(2024, 1, 31)),
])
self.assertEqual(len(all_january_work_entries), 23)
self.assertTrue(all(we.version_id == second_version for we in all_january_work_entries))
def test_work_entry_version_id(self):
"""
Test that we correctly set the version_id field of the work entry depending on the date
"""
second_version = self.employee_a.create_version({
'date_version': date(2023, 12, 1)
})
v1_we, v2_we = self.env['hr.work.entry'].create([
{
'date': date(2023, 10, 1),
'employee_id': self.employee_a.id,
},
{
'date': date(2024, 1, 1),
'employee_id': self.employee_a.id,
}
])
self.assertEqual(v1_we.version_id, self.employee_a_first_version)
self.assertEqual(v2_we.version_id, second_version)

View file

@ -0,0 +1,507 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
import pytz
from odoo.tests.common import tagged
from odoo.addons.hr_work_entry.tests.common import TestWorkEntryBase
@tagged('work_entry')
class TestWorkEntry(TestWorkEntryBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.tz = pytz.timezone(cls.richard_emp.tz)
cls.start = datetime(2015, 11, 1, 1, 0, 0)
cls.end = datetime(2015, 11, 30, 23, 59, 59)
cls.resource_calendar_id = cls.env['resource.calendar'].create({'name': 'My Calendar'})
cls.richard_emp.create_version({
'date_version': cls.start.date() - relativedelta(days=5),
'contract_date_start': cls.start.date() - relativedelta(days=5),
'contract_date_end': cls.end.date() + relativedelta(days=5),
'name': 'dodo',
'resource_calendar_id': cls.resource_calendar_id.id,
'wage': 1000,
'date_generated_from': cls.end.date() + relativedelta(days=5),
})
def test_no_duplicate(self):
self.richard_emp.generate_work_entries(self.start, self.end)
pou1 = self.env['hr.work.entry'].search_count([])
self.richard_emp.generate_work_entries(self.start, self.end)
pou2 = self.env['hr.work.entry'].search_count([])
self.assertEqual(pou1, pou2, "Work entries should not be duplicated")
def test_work_entry(self):
self.richard_emp.generate_work_entries(self.start, self.end)
attendance_nb = len(self.resource_calendar_id._attendance_intervals_batch(self.start.replace(tzinfo=pytz.utc), self.end.replace(tzinfo=pytz.utc))[False])
work_entry_nb = self.env['hr.work.entry'].search_count([
('employee_id', '=', self.richard_emp.id),
('date', '>=', self.start),
('date', '<=', self.end)])
self.assertEqual(attendance_nb / 2, work_entry_nb, "One work_entry should be generated for each pair of calendar attendance per day")
def test_validate_undefined_work_entry(self):
work_entry1 = self.env['hr.work.entry'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'version_id': self.richard_emp.version_id.id,
'date': self.start.date(),
'duration': 4,
})
work_entry1.work_entry_type_id = False
self.assertFalse(work_entry1.action_validate(), "It should not validate work_entries without a type")
self.assertEqual(work_entry1.state, 'conflict', "It should change to conflict state")
work_entry1.work_entry_type_id = self.work_entry_type
self.assertTrue(work_entry1.action_validate(), "It should validate work_entries")
def test_outside_calendar(self):
""" Test leave work entries outside schedule are conflicting """
work_entries = self.create_work_entries([
# Outside but not a leave
(datetime(2018, 10, 13, 3, 0), datetime(2018, 10, 13, 4, 0)),
# Outside and a leave
(datetime(2018, 10, 13, 1, 0), datetime(2018, 10, 13, 2, 0), self.work_entry_type_leave),
])
work_entries._mark_leaves_outside_schedule()
self.assertEqual(len(work_entries), 1, "The second work entry should not be generated. As it is a leave outside of the working schedule")
self.assertNotEqual(work_entries.state, 'conflict', "It should not conflict")
def test_work_entry_timezone(self):
""" Test work entries with different timezone """
hk_resource_calendar_id = self.env['resource.calendar'].create({
'name': 'HK Calendar',
'tz': 'Asia/Hong_Kong',
'hours_per_day': 8,
'attendance_ids': [(5, 0, 0),
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
]
})
hk_employee = self.env['hr.employee'].create({
'name': 'HK Employee',
'resource_calendar_id': hk_resource_calendar_id.id,
'date_version': datetime(2023, 8, 1),
'contract_date_start': datetime(2023, 8, 1),
'contract_date_end': False,
'wage': 1000,
})
self.env['resource.calendar.leaves'].create({
'date_from': pytz.timezone('Asia/Hong_Kong').localize(datetime(2023, 8, 2, 0, 0, 0)).astimezone(pytz.utc).replace(tzinfo=None),
'date_to': pytz.timezone('Asia/Hong_Kong').localize(datetime(2023, 8, 2, 23, 59, 59)).astimezone(pytz.utc).replace(tzinfo=None),
'calendar_id': hk_resource_calendar_id.id,
'work_entry_type_id': self.work_entry_type_leave.id,
})
self.env.company.resource_calendar_id = hk_resource_calendar_id
hk_employee.generate_work_entries(datetime(2023, 8, 1), datetime(2023, 8, 2))
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', hk_employee.id)])
self.assertEqual(len(work_entries), 2)
self.assertEqual(work_entries[0].date, date(2023, 8, 1))
self.assertEqual(work_entries[0].duration, 8)
self.assertEqual(work_entries[1].date, date(2023, 8, 2))
self.assertEqual(work_entries[1].duration, 8)
def test_separate_overlapping_work_entries_by_type(self):
calendar = self.env['resource.calendar'].create({'name': 'Calendar', 'tz': 'Europe/Brussels'})
employee = self.env['hr.employee'].create({
'name': 'Test',
'resource_calendar_id': calendar.id,
'date_version': datetime(2024, 9, 1),
'contract_date_start': datetime(2024, 9, 1),
'contract_date_end': datetime(2024, 9, 30),
'wage': 5000.0,
})
calendar.attendance_ids -= calendar.attendance_ids.filtered(lambda attendance: attendance.dayofweek == '0')
entry_type_1, entry_type_2 = self.env['hr.work.entry.type'].create([
{'name': 'Work type 1', 'is_leave': False, 'code': 'ENTRY_TYPE1'},
{'name': 'Work type 2', 'is_leave': False, 'code': 'ENTRY_TYPE2'},
])
self.env['resource.calendar.attendance'].create([
{
'calendar_id': calendar.id,
'dayofweek': '0',
'name': 'Same type 1',
'hour_from': 8,
'hour_to': 11,
'day_period': 'morning',
'work_entry_type_id': entry_type_1.id,
},
{
'calendar_id': calendar.id,
'dayofweek': '0',
'name': 'Same type 2',
'hour_from': 11,
'hour_to': 12,
'day_period': 'morning',
'work_entry_type_id': entry_type_1.id,
},
{
'calendar_id': calendar.id,
'dayofweek': '0',
'name': 'Different types 1',
'hour_from': 13,
'hour_to': 16,
'day_period': 'afternoon',
'work_entry_type_id': entry_type_1.id,
},
{
'calendar_id': calendar.id,
'dayofweek': '0',
'name': 'Different types 2',
'hour_from': 16,
'hour_to': 17,
'day_period': 'afternoon',
'work_entry_type_id': entry_type_2.id,
},
])
employee.generate_work_entries(datetime(2024, 9, 2), datetime(2024, 9, 2))
result_entries = self.env['hr.work.entry'].search([('employee_id', '=', employee.id)])
work_entry_types = [entry.work_entry_type_id for entry in result_entries]
self.assertEqual(len(result_entries), 2, 'A shift should be created for each pair of attendance by day')
self.assertEqual(work_entry_types, [entry_type_1, entry_type_2])
def test_work_entry_duration(self):
""" Test the duration of a work entry is rounded to the nearest minute and correctly calculated """
vals_list = [{
'name': 'Test Work Entry',
'employee_id': self.richard_emp.id,
'version_id': self.richard_emp.version_id.id,
'date_start': datetime(2023, 10, 1, 9, 0, 0),
'date_stop': datetime(2023, 10, 1, 9, 59, 59, 999999),
'work_entry_type_id': self.work_entry_type.id,
}]
vals_list = self.env['hr.version']._generate_work_entries_postprocess(vals_list)
work_entry = self.env['hr.work.entry'].create(vals_list)
self.assertEqual(work_entry.duration, 1, "The duration should be 1 hour")
def test_work_entry_different_calendars(self):
""" Test work entries are correctly created for employees with versions that have different calendar types. """
flexible_calendar = self.env['resource.calendar'].create({
'name': 'flexible calendar',
'flexible_hours': True,
'full_time_required_hours': 21,
'hours_per_day': 3,
'hours_per_week': 21,
})
# create 4 employees that have versions corresponding to these 4 cases:
# flexible calendar then standard calendar
# standard calendar then flexible calendar
# no calendar (fully flexible) then standard calendar
# standard calendar then no calendar (fully flexible)
# the cases of flexible then fully flexible and fully flexible then flexible are similar in logic to the last 2
# so they should work if last 2 are working properly
emp_flex_std, emp_std_flex, emp_fullyflex_std, emp_std_fullyflex = self.env['hr.employee'].create([
{'name': 'emp flex std'},
{'name': 'emp std flex'},
{'name': 'emp fullyflex std'},
{'name': 'emp fullyflex std'},
])
self.env['hr.version'].create([{
'employee_id': emp_flex_std.id,
'resource_calendar_id': flexible_calendar.id,
'date_version': datetime(2025, 9, 1),
'contract_date_start': datetime(2025, 9, 1),
'contract_date_end': datetime(2025, 9, 15),
'name': 'Flex Contract',
'wage': 5000.0,
'active': True,
},
{
'employee_id': emp_flex_std.id,
'resource_calendar_id': self.resource_calendar_id.id,
'date_version': datetime(2025, 9, 16),
'contract_date_start': datetime(2025, 9, 16),
'contract_date_end': datetime(2025, 9, 30),
'name': 'Std Contract',
'wage': 5000.0,
'active': True,
},
{
'employee_id': emp_std_flex.id,
'resource_calendar_id': self.resource_calendar_id.id,
'date_version': datetime(2025, 9, 1),
'contract_date_start': datetime(2025, 9, 1),
'contract_date_end': datetime(2025, 9, 15),
'name': 'Std Contract',
'wage': 5000.0,
'active': True,
},
{
'employee_id': emp_std_flex.id,
'resource_calendar_id': flexible_calendar.id,
'date_version': datetime(2025, 9, 16),
'contract_date_start': datetime(2025, 9, 16),
'contract_date_end': datetime(2025, 9, 30),
'name': 'Flex Contract',
'wage': 5000.0,
'active': True,
},
{
'employee_id': emp_fullyflex_std.id,
'resource_calendar_id': False,
'date_version': datetime(2025, 9, 1),
'contract_date_start': datetime(2025, 9, 1),
'contract_date_end': datetime(2025, 9, 15),
'name': 'FullyFlex Contract',
'wage': 5000.0,
'active': True,
},
{
'employee_id': emp_fullyflex_std.id,
'resource_calendar_id': self.resource_calendar_id.id,
'date_version': datetime(2025, 9, 16),
'contract_date_start': datetime(2025, 9, 16),
'contract_date_end': datetime(2025, 9, 30),
'name': 'Std Contract',
'wage': 5000.0,
'active': True,
},
{
'employee_id': emp_std_fullyflex.id,
'resource_calendar_id': self.resource_calendar_id.id,
'date_version': datetime(2025, 9, 1),
'contract_date_start': datetime(2025, 9, 1),
'contract_date_end': datetime(2025, 9, 15),
'name': 'Std Contract',
'wage': 5000.0,
'active': True,
},
{
'employee_id': emp_std_fullyflex.id,
'resource_calendar_id': False,
'date_version': datetime(2025, 9, 16),
'contract_date_start': datetime(2025, 9, 16),
'contract_date_end': datetime(2025, 9, 30),
'name': 'FullyFlex Contract',
'wage': 5000.0,
'active': True,
}])
half1_date_start = date(2025, 9, 1)
half1_date_end = date(2025, 9, 15)
half2_date_start = date(2025, 9, 16)
half2_date_end = date(2025, 9, 30)
# generate work entries for the 4 employees between (2025, 9, 1) and (2025, 9, 30)
# then split them into 2 halves (each half corresponding to the work entries generated by 1 contract)
# the timezones are considered in the split
all_work_entries = (emp_flex_std + emp_std_flex + emp_fullyflex_std + emp_std_fullyflex).generate_work_entries(date(2025, 9, 1), date(2025, 9, 30))
flex_std_work_entries = all_work_entries.filtered(lambda e: e.employee_id == emp_flex_std)
self.assertEqual(len(flex_std_work_entries), 26)
half1_entries = flex_std_work_entries.filtered(lambda e:
e.date >= half1_date_start
and e.date <= half1_date_end)
half2_entries = flex_std_work_entries.filtered(lambda e:
e.date >= half2_date_start
and e.date <= half2_date_end)
self.assertEqual(len(half1_entries), 15) # 1 work entry per day (including weekend)
self.assertTrue(all(entry.duration == 3 for entry in half1_entries))
self.assertTrue(all(entry.version_id.name == 'Flex Contract' for entry in half1_entries))
self.assertEqual(len(half2_entries), 11) # 1 work entries per day, no work entries for weekend
self.assertTrue(all(entry.duration == 8 for entry in half2_entries))
self.assertTrue(all(entry.version_id.name == 'Std Contract' for entry in half2_entries))
std_flex_work_entries = all_work_entries.filtered(lambda e: e.employee_id == emp_std_flex)
self.assertEqual(len(std_flex_work_entries), 26)
half1_entries = std_flex_work_entries.filtered(lambda e:
e.date >= half1_date_start
and e.date <= half1_date_end)
half2_entries = std_flex_work_entries.filtered(lambda e:
e.date >= half2_date_start
and e.date <= half2_date_end)
self.assertEqual(len(half1_entries), 11) # 1 work entries per day, no work entries for weekend
self.assertTrue(all(entry.duration == 8 for entry in half1_entries))
self.assertTrue(all(entry.version_id.name == 'Std Contract' for entry in half1_entries))
self.assertEqual(len(half2_entries), 15) # 1 work entry per day (including weekend)
self.assertTrue(all(entry.duration == 3 for entry in half2_entries))
self.assertTrue(all(entry.version_id.name == 'Flex Contract' for entry in half2_entries))
fullyflex_std_work_entries = all_work_entries.filtered(lambda e: e.employee_id == emp_fullyflex_std)
self.assertEqual(len(fullyflex_std_work_entries), 26)
half1_entries = fullyflex_std_work_entries.filtered(lambda e:
e.date >= half1_date_start
and e.date <= half1_date_end)
half2_entries = fullyflex_std_work_entries.filtered(lambda e:
e.date >= half2_date_start
and e.date <= half2_date_end)
self.assertEqual(len(half1_entries), 15) # work entries cover the entire duration
self.assertTrue(all(entry.duration == 24 for entry in half1_entries))
self.assertTrue(all(entry.version_id.name == 'FullyFlex Contract' for entry in half1_entries))
self.assertEqual(len(half2_entries), 11) # 1 work entries per day, no work entries for weekend
self.assertTrue(all(entry.duration == 8 for entry in half2_entries))
self.assertTrue(all(entry.version_id.name == 'Std Contract' for entry in half2_entries))
std_fullyflex_work_entries = all_work_entries.filtered(lambda e: e.employee_id == emp_std_fullyflex)
self.assertEqual(len(std_fullyflex_work_entries), 26)
half1_entries = std_fullyflex_work_entries.filtered(lambda e:
e.date >= half1_date_start
and e.date <= half1_date_end)
half2_entries = std_fullyflex_work_entries.filtered(lambda e:
e.date >= half2_date_start
and e.date <= half2_date_end)
self.assertEqual(len(half1_entries), 11) # 1 work entries per day, no work entries for weekend
self.assertTrue(all(entry.duration == 8 for entry in half1_entries))
self.assertTrue(all(entry.version_id.name == 'Std Contract' for entry in half1_entries))
self.assertEqual(len(half2_entries), 15) # work entries cover the entire duration
self.assertTrue(all(entry.duration == 24 for entry in half2_entries))
self.assertTrue(all(entry.version_id.name == 'FullyFlex Contract' for entry in half2_entries))
def test_work_entry_version_changed_after_generation(self):
"""
When you generate work entries for a version, and you add a new version to the employee starting during the period of already generated work entries,
The previous version work entries date generation to should be updated to the new version date and new work entries should be generated.
Previous ones should be deleted
"""
calendar_40h = self.env['resource.calendar'].create({
'name': '40h Calendar',
'tz': 'Europe/Brussels',
'hours_per_day': 8,
'attendance_ids': [(5, 0, 0),
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
]
})
calendar_35h = self.env['resource.calendar'].create({
'name': '35h Calendar',
'tz': 'Europe/Brussels',
'hours_per_day': 7,
'attendance_ids': [(5, 0, 0),
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
]
})
# first version with a 40h calendar
employee = self.env['hr.employee'].create({
'name': 'Test',
'resource_calendar_id': calendar_40h.id,
'date_version': datetime(2025, 1, 1),
'contract_date_start': datetime(2025, 1, 1),
})
employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', employee.id)])
self.assertEqual(len(work_entries), 23, "23 attendance")
self.assertEqual(sum(work_entries.mapped("duration")), 184, "23 * 8h")
# new version with a different calendar (35h) set in the tier of the month
employee.create_version({
'resource_calendar_id': calendar_35h.id,
'date_version': datetime(2025, 1, 10),
})
employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', employee.id)])
self.assertEqual(len(work_entries), 23, "23 attendance")
self.assertEqual(sum(work_entries.mapped("duration")), 168, "7 * 8h + 16 * 7h")
# new version with a different calendar (40h) set in the second tier of the month
employee.create_version({
'resource_calendar_id': calendar_40h.id,
'date_version': datetime(2025, 1, 20),
})
employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', employee.id)])
self.assertEqual(len(work_entries), 23, "23 attendance")
self.assertEqual(sum(work_entries.mapped("duration")), 178, "7 * 8h + 6 * 7h + 10 * 8h")
def test_work_entry_version_changed_after_generation2(self):
"""
When you generate work entries for a version, and you add a new version to the employee starting during the period of already generated work entries,
The previous version work entries date generation to should be updated to the new version date and new work entries should be generated.
Previous ones should be deleted
"""
calendar_40h = self.env['resource.calendar'].create({
'name': '40h Calendar',
'tz': 'Europe/Brussels',
'hours_per_day': 8,
'attendance_ids': [(5, 0, 0),
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
]
})
calendar_35h = self.env['resource.calendar'].create({
'name': '35h Calendar',
'tz': 'Europe/Brussels',
'hours_per_day': 7,
'attendance_ids': [(5, 0, 0),
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 7, 'hour_to': 11, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
]
})
# first version with a 40h calendar
employee = self.env['hr.employee'].create({
'name': 'Test',
'resource_calendar_id': calendar_40h.id,
'date_version': datetime(2025, 1, 1),
'contract_date_start': datetime(2025, 1, 1),
})
employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', employee.id)])
self.assertEqual(len(work_entries), 23, "23 attendance")
self.assertEqual(sum(work_entries.mapped("duration")), 184, "23 * 8h")
# new version with a different calendar (40h) set in the tier of the month
employee.create_version({
'resource_calendar_id': calendar_40h.id,
'date_version': datetime(2025, 1, 20),
})
employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', employee.id)])
self.assertEqual(len(work_entries), 23, "23 attendance")
self.assertEqual(sum(work_entries.mapped("duration")), 184, "13 * 8h + 10 * 8h")
# new version with a different calendar (35h) set in the second tier of the month
employee.create_version({
'resource_calendar_id': calendar_35h.id,
'date_version': datetime(2025, 1, 10),
})
employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
work_entries = self.env['hr.work.entry'].search([('employee_id', '=', employee.id)])
self.assertEqual(len(work_entries), 23, "23 attendance")
self.assertEqual(sum(work_entries.mapped("duration")), 178, "7 * 8h + 6 * 7h + 10 * 8h")

View file

@ -0,0 +1,25 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import ValidationError
from odoo.release import version_info
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('-at_install', 'post_install', 'post_install_l10n')
class TestWorkEntryTypeData(TransactionCase):
def test_ensure_work_entry_type_definition(self):
# Make sure work entry types are defined in hr_work_entry in master (and not in other modules)
# In the case this tests breaks during a forward port, move the work entry type definition
# to hr_work_entry and make a upgrade script accordingly.
if version_info[3] != 'alpha':
return
work_entry_types_xmlids = self.env['hr.work.entry.type'].search([])._get_external_ids()
invalid_xmlids = []
for xmlids in work_entry_types_xmlids.values():
for xmlid in xmlids:
module = xmlid.split('.')[0]
if module not in ['hr_work_entry', '__export__', '__custom__'] and not module.startswith('test_'):
invalid_xmlids.append(xmlid)
if invalid_xmlids:
raise ValidationError("Some work entry types are defined outside of module hr_work_entry.\n%s" % '\n'.join(invalid_xmlids))