mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 20:32:06 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
|
|
@ -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
|
||||
|
|
@ -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 ")
|
||||
|
|
@ -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)
|
||||
102
odoo-bringout-oca-ocb-resource/resource/tests/test_utils.py
Normal file
102
odoo-bringout-oca-ocb-resource/resource/tests/test_utils.py
Normal 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))
|
||||
Loading…
Add table
Add a link
Reference in a new issue