19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -0,0 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# -*- coding: utf-8 -*-
from . import test_utils
from . import test_resource_calendar
from . import test_flexible_resource_calendar

View file

@ -0,0 +1,139 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime, date
from odoo.tests.common import TransactionCase
UTC = pytz.timezone('UTC')
class TestFlexibleResourceCalendar(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.flex_calendar = cls.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'tz': 'UTC',
'hours_per_day': 8.0,
'flexible_hours': True,
})
cls.fully_flex_resource, cls.flex_resource = cls.env['resource.resource'].create([{
'name': 'Wade Wilson',
'calendar_id': False,
'tz': 'UTC',
}, {
'name': 'Wade Wilson',
'calendar_id': cls.flex_calendar.id,
'tz': 'UTC',
}])
cls.env['resource.calendar.leaves'].create([
{
'resource_id': cls.flex_resource.id,
'date_from': datetime(2025, 7, 29, 8),
'date_to': datetime(2025, 7, 29, 17),
},
{
'resource_id': cls.flex_resource.id,
'date_from': datetime(2025, 7, 31, 8),
'date_to': datetime(2025, 8, 1, 17),
},
{
'resource_id': cls.fully_flex_resource.id,
'date_from': datetime(2025, 7, 29, 8),
'date_to': datetime(2025, 7, 29, 17),
},
{
'resource_id': cls.fully_flex_resource.id,
'date_from': datetime(2025, 7, 31, 8),
'date_to': datetime(2025, 8, 1, 17),
},
{
'calendar_id': cls.flex_calendar.id,
'date_from': datetime(2025, 8, 4, 8),
'date_to': datetime(2025, 8, 4, 17),
},
{
'calendar_id': False,
'date_from': datetime(2025, 8, 5, 8),
'date_to': datetime(2025, 8, 5, 17),
},
])
cls.resources = cls.flex_resource | cls.fully_flex_resource
def test_flexible_resource_hours_per_week(self):
cal = self.env['resource.calendar'].create({
'name': 'Flexible 38h',
'tz': 'UTC',
'hours_per_day': 7.6,
'flexible_hours': True,
'hours_per_week': 38.0,
'full_time_required_hours': 40.0,
})
self.assertAlmostEqual(cal.work_time_rate, 95.0, 2)
resource = self.env['resource.resource'].create({
'name': 'flexpt',
'tz': 'UTC',
'calendar_id': cal.id,
})
start_dt = datetime(2025, 7, 28).astimezone(UTC)
end_dt = datetime(2025, 8, 3).astimezone(UTC)
work_intervals, hours_per_day, hours_per_week = resource._get_flexible_resource_valid_work_intervals(start_dt, end_dt)
hours = resource._get_flexible_resource_work_hours(work_intervals[resource.id], hours_per_day[resource.id], hours_per_week[resource.id])
self.assertAlmostEqual(hours, 38.0, 2)
self.assertAlmostEqual(hours_per_week[resource.id][2025, 31], 38.0, 2)
def test_flexible_resource_work_intervals(self):
start_dt = datetime(2025, 7, 28).astimezone(UTC)
end_dt = datetime(2025, 8, 3, 17, 0).astimezone(UTC)
work_intervals, hours_per_day, hours_per_week = self.resources._get_flexible_resource_valid_work_intervals(start_dt, end_dt)
self.maxDiff = None
for resource in self.resources:
self.assertEqual(work_intervals[resource.id]._items, [
(datetime(2025, 7, 28, 0, 0, tzinfo=UTC), datetime(2025, 7, 28, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 7, 30, 0, 0, tzinfo=UTC), datetime(2025, 7, 30, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 2, 0, 0, tzinfo=UTC), datetime(2025, 8, 2, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
(datetime(2025, 8, 3, 0, 0, tzinfo=UTC), datetime(2025, 8, 3, 17, tzinfo=UTC), self.env['resource.calendar.attendance']),
], "resource not available on 29, 31 and 01, for other days, resource can do his hours at any moment of the day (from 00:00:00 to 23:59:59)")
self.assertDictEqual(hours_per_day[self.flex_resource.id], {
date(2025, 7, 28): 8.0,
date(2025, 7, 29): 0.0,
date(2025, 7, 30): 8.0,
date(2025, 7, 31): 0.0,
date(2025, 8, 1): 0.0,
date(2025, 8, 2): 8.0,
date(2025, 8, 3): 8.0,
}, "0 hours when the resource is not available, hours_per_day from the calendar for working days")
self.assertDictEqual(hours_per_week[self.flex_resource.id], {
(2025, 31): 16.0,
(2025, 32): 24.0,
}, "3 days off (24 hours), remaining 16h, 2 days off (16 hours) for second week")
self.assertTrue(self.fully_flex_resource.id not in hours_per_day and self.fully_flex_resource not in hours_per_week, "no daily and weekly limit")
start_dt = datetime(2025, 8, 4).astimezone(UTC)
end_dt = datetime(2025, 8, 5, 17, 0).astimezone(UTC)
work_intervals, hours_per_day, hours_per_week = self.resources._get_flexible_resource_valid_work_intervals(start_dt, end_dt)
self.assertEqual(work_intervals[self.flex_resource.id]._items, [], "flex calendar have a public holidays on day 4, and there's a public holiday on day 5 for all calendars")
self.assertEqual(work_intervals[self.fully_flex_resource.id]._items, [
(datetime(2025, 8, 4, 0, 0, tzinfo=UTC), datetime(2025, 8, 4, 23, 59, 59, 999999, tzinfo=UTC), self.env['resource.calendar.attendance']),
], "fully flex resource doesn't have a calendar, he should not follow the flex calendar public holiday, he follows holidays without a calendar")
def test_hours_per_week_for_different_years(self):
start_dt = datetime(2025, 12, 26).astimezone(UTC)
end_dt = datetime(2026, 1, 1, 17).astimezone(UTC)
_, _, hours_per_week = self.resources._get_flexible_resource_valid_work_intervals(start_dt, end_dt)
self.assertDictEqual(hours_per_week[self.flex_resource.id], {
(2025, 52): 40.0,
(2026, 1): 40.0,
}, "weeks are well computed when ")

View file

@ -0,0 +1,194 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime
from odoo.tests import Form
from odoo.tests.common import TransactionCase
class TestResourceCalendar(TransactionCase):
def test_fully_flexible_attendance_interval_duration(self):
"""
Test that the duration of a fully flexible attendance interval is correctly computed.
"""
calendar = self.env['resource.calendar'].create({
'name': 'Standard Calendar',
'two_weeks_calendar': False,
})
resource = self.env['resource.resource'].create({
'name': 'Wade Wilson',
'calendar_id': False, # Fully-flexible because no calendar is set
'tz': 'America/New_York', # -04:00 UTC offset in the summer
})
self.env['resource.calendar.attendance'].create({
'name': 'TEMP',
'calendar_id': calendar.id,
'dayofweek': '2', # Wednesday
'hour_from': 14, # 18:00 UTC
'hour_to': 17, # 21:00 UTC
})
UTC = pytz.timezone('UTC')
start_dt = datetime(2025, 6, 4, 18, 0, 0).astimezone(UTC)
end_dt = datetime(2025, 6, 4, 21, 0, 0).astimezone(UTC)
result_per_resource_id = calendar._attendance_intervals_batch(
start_dt, end_dt, resource
)
start, end, attendance = result_per_resource_id[resource.id]._items[0]
# For a flexible resource, we expect the output times to match the
# input times exactly, since the resource has no fixed calendar.
# Further, the dummy attendance that is created should have a duration
# equal to the difference between the start and end times.
self.assertEqual(start, start_dt, "Output start time should match the input start time")
self.assertEqual(end, end_dt, "Output end time should match the input end time")
self.assertEqual(attendance.duration_hours, 3.0, "Attendance duration should be 3 hours")
self.assertEqual(attendance.duration_days, 0.125, "Attendance duration should be 0.125 days (3 hours)")
def test_flexible_calendar_attendance_interval_duration(self):
"""
Test that the duration of an attendance interval for flexible calendar is correctly computed.
"""
flexible_calendar = self.env['resource.calendar'].create({
'name': 'Flexible Calendar',
'hours_per_day': 7.0,
'hours_per_week': 30,
'full_time_required_hours': 30,
'flexible_hours': True,
'tz': 'UTC',
})
# Case 1: get attendances for the full week.
# Expected: 7-7-7-7-2 (30 hours total)
expected_hours = [7, 7, 7, 7, 2]
start_dt = datetime(2025, 6, 2, 0, 0, 0).astimezone(pytz.UTC)
end_dt = datetime(2025, 6, 7, 23, 59, 59).astimezone(pytz.UTC)
result_per_resource_id = flexible_calendar._attendance_intervals_batch(
start_dt, end_dt
)
self.assertEqual(expected_hours, [(end - start).total_seconds() / 3600 for start, end, dummy_attendance in result_per_resource_id[0]._items])
self.assertEqual(expected_hours, [dummy_attendance.duration_hours for start, end, dummy_attendance in result_per_resource_id[0]._items])
# Case 2: check attendances are all contained between start_dt and end_dt
start_dt = datetime(2025, 6, 2, 11, 0, 0).astimezone(pytz.UTC)
end_dt = datetime(2025, 6, 7, 13, 0, 0).astimezone(pytz.UTC)
result_per_resource_id = flexible_calendar._attendance_intervals_batch(
start_dt, end_dt
)
self.assertTrue(start_dt <= result_per_resource_id[0]._items[0][0], "First attendance interval should not start before start_dt")
self.assertTrue(end_dt >= result_per_resource_id[0]._items[4][1], "Last attendance interval should not end after end_dt")
def test_public_holiday_calendar_no_company(self):
self.env['resource.calendar.leaves'].create([{
'name': "Public Holiday for company",
'company_id': self.env.company.id,
'date_from': datetime(2019, 5, 29, 0, 0, 0),
'date_to': datetime(2019, 5, 30, 0, 0, 0),
'resource_id': False,
'time_type': "leave",
}])
calendar = self.env['resource.calendar'].create({
'name': '40 hours/week',
'hours_per_day': 8,
'full_time_required_hours': 40,
})
calendar.company_id = False
date_from = datetime(2019, 5, 27, 0, 0, 0).astimezone(pytz.UTC)
date_to = datetime(2019, 5, 31, 23, 59, 59).astimezone(pytz.UTC)
days = calendar._get_unusual_days(date_from, date_to, self.env.company)
expected_res = {
'2019-05-27': False,
'2019-05-28': False,
'2019-05-29': True,
'2019-05-30': False,
'2019-05-31': False,
}
self.assertEqual(days, expected_res)
def test_resource_calendar_form_view(self):
calendar = self.env['resource.calendar'].create({
'name': 'Test calendar',
'attendance_ids': [(5, 0, 0),
(0, 0, {
'name': 'Monday',
'hour_from': 8,
'hour_to': 16,
'day_period': 'full_day',
'dayofweek': '0',
}),
(0, 0, {
'name': 'Tuesday',
'hour_from': 8,
'hour_to': 16,
'day_period': 'full_day',
'dayofweek': '1',
}),
(0, 0, {
'name': 'Wednesday',
'hour_from': 8,
'hour_to': 16,
'day_period': 'full_day',
'dayofweek': '2',
})],
})
calendar_form = Form(calendar)
self.assertEqual(calendar_form.hours_per_day, 8)
self.assertEqual(calendar_form.hours_per_week, 24)
with calendar_form.attendance_ids.edit(0) as line_form:
line_form.hour_from = 11
line_form.save()
# The calendar form values should be recomputed
self.assertEqual(calendar_form.hours_per_day, 7)
self.assertEqual(calendar_form.hours_per_week, 21)
calendar_form.save()
self.assertEqual(calendar.hours_per_day, 7)
self.assertEqual(calendar.hours_per_week, 21)
def test_resource_calendar_form_view_duration_based(self):
calendar = self.env['resource.calendar'].create({
'name': 'Test calendar',
'attendance_ids': [(5, 0, 0),
(0, 0, {
'name': 'Monday',
'hour_from': 8,
'hour_to': 16,
'day_period': 'full_day',
'dayofweek': '0',
}),
(0, 0, {
'name': 'Tuesday',
'hour_from': 8,
'hour_to': 16,
'day_period': 'full_day',
'dayofweek': '1',
}),
(0, 0, {
'name': 'Wednesday',
'hour_from': 8,
'hour_to': 16,
'day_period': 'full_day',
'dayofweek': '2',
})],
})
calendar.duration_based = True
calendar_form = Form(calendar)
self.assertEqual(calendar_form.hours_per_day, 8)
self.assertEqual(calendar_form.hours_per_week, 24)
with calendar_form.attendance_ids.edit(0) as line_form:
line_form.duration_hours = 5
line_form.save()
# The calendar form values should be recomputed
self.assertEqual(calendar_form.hours_per_day, 7)
self.assertEqual(calendar_form.hours_per_week, 21)
calendar_form.save()
self.assertEqual(calendar.hours_per_day, 7)
self.assertEqual(calendar.hours_per_week, 21)

View file

@ -0,0 +1,102 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo.fields import Datetime, Domain
from odoo.tests.common import TransactionCase
from odoo.addons.resource.models import utils
from odoo.tests import Form
class TestExpression(TransactionCase):
def test_filter_domain_leaf(self):
domains = [
['|', ('skills', '=', 1), ('admin', '=', True)],
['|', ('skills', '=', 1), ('admin', '=', True), '|', ('skills', '=', 2), ('admin', '=', True)],
['|', ('skills', '=', 1), ('skills', '=', 2), '|', ('skills', '=', 2), ('admin', '=', True)],
['|', '|', ('skills', '=', 1), ('skills', '=', True), '|', ('skills', '=', 2), ('admin', '=', True)],
['|', '|', ('admin', '=', 1), ('admin', '=', True), '&', ('skills', '=', 2), ('admin', '=', True)],
['|', '|', '!', ('admin', '=', 1), ('admin', '=', True), '!', '&', '!', ('skills', '=', 2), ('admin', '=', True)],
['&', '!', ('skills', '=', 2), ('admin', '=', True)],
[['start_datetime', '<=', '2022-12-17 22:59:59'], ['end_datetime', '>=', '2022-12-10 23:00:00']],
[('admin', '=', 1), ('admin', '=', 1), '|', ('admin', '=', 1), ('admin', '=', 1), ('skills', '=', 2)]
]
fields_to_remove = [['skills'], ['admin', 'skills']]
expected_results = [
[
[('admin', '=', True)],
[('admin', '=', True), ('admin', '=', True)],
[('admin', '=', True)],
[('admin', '=', True)],
['|', '|', ('admin', '=', 1), ('admin', '=', True), ('admin', '=', True)],
['|', '|', '!', ('admin', '=', 1), ('admin', '=', True), '!', ('admin', '=', True)],
[('admin', '=', True)],
[['start_datetime', '<=', '2022-12-17 22:59:59'], ['end_datetime', '>=', '2022-12-10 23:00:00']],
[('admin', '=', 1), ('admin', '=', 1), '|', ('admin', '=', 1), ('admin', '=', 1)],
],
[
[],
[],
[],
[],
[],
[],
[],
[['start_datetime', '<=', '2022-12-17 22:59:59'], ['end_datetime', '>=', '2022-12-10 23:00:00']],
[],
],
]
for idx, fields in enumerate(fields_to_remove):
results = [utils.filter_domain_leaf(dom, lambda field: field not in fields) for dom in domains]
self.assertEqual(results, [Domain(expected) for expected in expected_results[idx]])
# Testing field mapping 1
self.assertEqual(
Domain('field4', '!=', 'test'),
utils.filter_domain_leaf(
['|', ('field1', 'in', [1, 2]), '!', ('field2', '=', False), ('field3', '!=', 'test')],
lambda field: field == 'field3',
field_name_mapping={'field3': 'field4'},
)
)
def test_resource_calendar_leave_compute_date_to(self):
"""
Test date_to is computed when date_from is changed,
except when it already has a valid value.
"""
date_from = Datetime.from_string('2024-05-01 00:00:00')
date_to = Datetime.from_string('2024-05-03 23:59:59')
leave = self.env['resource.calendar.leaves'].create({
'date_from': date_from,
'date_to': date_to,
})
leave.date_from -= relativedelta(minutes=5)
self.assertEqual(leave.date_to, date_to, "date_to shouldn't get recomputed if still valid")
leave.date_from += relativedelta(years=5)
self.assertGreater(leave.date_to, date_to, "date_to should get recomputed when invalid")
def test_resource_creation_with_date_from(self):
"""
Test resource creation with a date_from.
AssertError is raised when date_from is not provided.
"""
with self.assertRaises(AssertionError):
with Form(self.env['resource.calendar.leaves']) as res:
res.date_from = False
res.date_to = Datetime.now()
with Form(self.env['resource.calendar.leaves']) as res:
date_from = Datetime.now()
date_to = Datetime.now() + relativedelta(hours=24)
res.date_from = date_from
res.date_to = date_to
self.assertFalse(res.id, 'The resource does not have an id before saving')
res.save()
self.assertTrue(res.id, 'The resource was successfully created')
self.assertEqual(res.date_from, Datetime.to_string(date_from))
self.assertEqual(res.date_to, Datetime.to_string(date_to))