wbhuman_resources 1.58.4__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wbhuman_resources/__init__.py +1 -0
- wbhuman_resources/admin/__init__.py +5 -0
- wbhuman_resources/admin/absence.py +113 -0
- wbhuman_resources/admin/calendars.py +37 -0
- wbhuman_resources/admin/employee.py +109 -0
- wbhuman_resources/admin/kpi.py +21 -0
- wbhuman_resources/admin/review.py +157 -0
- wbhuman_resources/apps.py +23 -0
- wbhuman_resources/dynamic_preferences_registry.py +119 -0
- wbhuman_resources/factories/__init__.py +38 -0
- wbhuman_resources/factories/absence.py +109 -0
- wbhuman_resources/factories/calendars.py +60 -0
- wbhuman_resources/factories/employee.py +80 -0
- wbhuman_resources/factories/kpi.py +155 -0
- wbhuman_resources/filters/__init__.py +20 -0
- wbhuman_resources/filters/absence.py +109 -0
- wbhuman_resources/filters/absence_graphs.py +85 -0
- wbhuman_resources/filters/calendars.py +28 -0
- wbhuman_resources/filters/employee.py +81 -0
- wbhuman_resources/filters/kpi.py +35 -0
- wbhuman_resources/filters/review.py +134 -0
- wbhuman_resources/filters/signals.py +27 -0
- wbhuman_resources/locale/de/LC_MESSAGES/django.mo +0 -0
- wbhuman_resources/locale/de/LC_MESSAGES/django.po +2207 -0
- wbhuman_resources/locale/de/LC_MESSAGES/django.po.translated +2456 -0
- wbhuman_resources/locale/en/LC_MESSAGES/django.mo +0 -0
- wbhuman_resources/locale/en/LC_MESSAGES/django.po +2091 -0
- wbhuman_resources/locale/fr/LC_MESSAGES/django.mo +0 -0
- wbhuman_resources/locale/fr/LC_MESSAGES/django.po +2093 -0
- wbhuman_resources/management/__init__.py +23 -0
- wbhuman_resources/migrations/0001_initial_squashed_squashed_0015_alter_absencerequest_calendaritem_ptr_and_more.py +949 -0
- wbhuman_resources/migrations/0016_alter_employeehumanresource_options.py +20 -0
- wbhuman_resources/migrations/0017_absencerequest_crossborder_country_and_more.py +55 -0
- wbhuman_resources/migrations/0018_remove_position_group_position_groups.py +32 -0
- wbhuman_resources/migrations/0019_alter_absencerequest_options_alter_kpi_options_and_more.py +44 -0
- wbhuman_resources/migrations/0020_alter_employeeyearbalance_year_alter_review_year.py +27 -0
- wbhuman_resources/migrations/0021_alter_position_color.py +18 -0
- wbhuman_resources/migrations/0022_remove_review_editable_mode.py +64 -0
- wbhuman_resources/migrations/__init__.py +0 -0
- wbhuman_resources/models/__init__.py +23 -0
- wbhuman_resources/models/absence.py +903 -0
- wbhuman_resources/models/calendars.py +370 -0
- wbhuman_resources/models/employee.py +1241 -0
- wbhuman_resources/models/kpi.py +199 -0
- wbhuman_resources/models/preferences.py +40 -0
- wbhuman_resources/models/review.py +982 -0
- wbhuman_resources/permissions/__init__.py +0 -0
- wbhuman_resources/permissions/backend.py +26 -0
- wbhuman_resources/serializers/__init__.py +49 -0
- wbhuman_resources/serializers/absence.py +308 -0
- wbhuman_resources/serializers/calendars.py +73 -0
- wbhuman_resources/serializers/employee.py +267 -0
- wbhuman_resources/serializers/kpi.py +80 -0
- wbhuman_resources/serializers/review.py +415 -0
- wbhuman_resources/signals.py +4 -0
- wbhuman_resources/tasks.py +195 -0
- wbhuman_resources/templates/review/review_report.html +322 -0
- wbhuman_resources/tests/__init__.py +1 -0
- wbhuman_resources/tests/conftest.py +96 -0
- wbhuman_resources/tests/models/__init__.py +0 -0
- wbhuman_resources/tests/models/test_absences.py +478 -0
- wbhuman_resources/tests/models/test_calendars.py +209 -0
- wbhuman_resources/tests/models/test_employees.py +502 -0
- wbhuman_resources/tests/models/test_review.py +103 -0
- wbhuman_resources/tests/models/test_utils.py +110 -0
- wbhuman_resources/tests/signals.py +108 -0
- wbhuman_resources/tests/test_permission.py +64 -0
- wbhuman_resources/tests/test_tasks.py +74 -0
- wbhuman_resources/urls.py +221 -0
- wbhuman_resources/utils.py +43 -0
- wbhuman_resources/viewsets/__init__.py +61 -0
- wbhuman_resources/viewsets/absence.py +312 -0
- wbhuman_resources/viewsets/absence_charts.py +328 -0
- wbhuman_resources/viewsets/buttons/__init__.py +7 -0
- wbhuman_resources/viewsets/buttons/absence.py +32 -0
- wbhuman_resources/viewsets/buttons/employee.py +44 -0
- wbhuman_resources/viewsets/buttons/kpis.py +16 -0
- wbhuman_resources/viewsets/buttons/review.py +195 -0
- wbhuman_resources/viewsets/calendars.py +103 -0
- wbhuman_resources/viewsets/display/__init__.py +39 -0
- wbhuman_resources/viewsets/display/absence.py +334 -0
- wbhuman_resources/viewsets/display/calendars.py +83 -0
- wbhuman_resources/viewsets/display/employee.py +254 -0
- wbhuman_resources/viewsets/display/kpis.py +92 -0
- wbhuman_resources/viewsets/display/review.py +429 -0
- wbhuman_resources/viewsets/employee.py +210 -0
- wbhuman_resources/viewsets/endpoints/__init__.py +42 -0
- wbhuman_resources/viewsets/endpoints/absence.py +57 -0
- wbhuman_resources/viewsets/endpoints/calendars.py +18 -0
- wbhuman_resources/viewsets/endpoints/employee.py +51 -0
- wbhuman_resources/viewsets/endpoints/kpis.py +53 -0
- wbhuman_resources/viewsets/endpoints/review.py +191 -0
- wbhuman_resources/viewsets/kpi.py +280 -0
- wbhuman_resources/viewsets/menu/__init__.py +22 -0
- wbhuman_resources/viewsets/menu/absence.py +50 -0
- wbhuman_resources/viewsets/menu/administration.py +15 -0
- wbhuman_resources/viewsets/menu/calendars.py +33 -0
- wbhuman_resources/viewsets/menu/employee.py +44 -0
- wbhuman_resources/viewsets/menu/kpis.py +18 -0
- wbhuman_resources/viewsets/menu/review.py +97 -0
- wbhuman_resources/viewsets/mixins.py +14 -0
- wbhuman_resources/viewsets/review.py +837 -0
- wbhuman_resources/viewsets/titles/__init__.py +18 -0
- wbhuman_resources/viewsets/titles/absence.py +30 -0
- wbhuman_resources/viewsets/titles/employee.py +18 -0
- wbhuman_resources/viewsets/titles/kpis.py +15 -0
- wbhuman_resources/viewsets/titles/review.py +62 -0
- wbhuman_resources/viewsets/utils.py +28 -0
- wbhuman_resources-1.58.4.dist-info/METADATA +8 -0
- wbhuman_resources-1.58.4.dist-info/RECORD +111 -0
- wbhuman_resources-1.58.4.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import zoneinfo
|
|
2
|
+
from datetime import date, datetime, time
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from dateutil import rrule
|
|
8
|
+
from django.contrib.postgres.constraints import ExclusionConstraint
|
|
9
|
+
from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.db.models import CheckConstraint, F, Q, Sum
|
|
12
|
+
from django.db.models.signals import post_migrate
|
|
13
|
+
from django.dispatch import receiver
|
|
14
|
+
from django.utils import timezone
|
|
15
|
+
from django.utils.translation import gettext_lazy as _
|
|
16
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
17
|
+
from psycopg.types.range import TimestamptzRange
|
|
18
|
+
from timezone_field import TimeZoneField
|
|
19
|
+
from wbcore.contrib.agenda.models import CalendarItem
|
|
20
|
+
from wbcore.contrib.icons import WBIcon
|
|
21
|
+
from wbcore.models import WBModel
|
|
22
|
+
|
|
23
|
+
SelfDefaultDailyPeriod = TypeVar("SelfDefaultDailyPeriod", bound="DefaultDailyPeriod")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidDayOffCalendarResourceError(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DayOffCalendar(WBModel):
|
|
31
|
+
title = models.CharField(max_length=255)
|
|
32
|
+
resource = models.CharField(
|
|
33
|
+
max_length=255, null=True, blank=True, help_text=_("Used to fetch the days off from an API.")
|
|
34
|
+
)
|
|
35
|
+
timezone = TimeZoneField(default="UTC")
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
return f"{self.title}"
|
|
39
|
+
|
|
40
|
+
def get_period_start_choices(self) -> list[str]:
|
|
41
|
+
"""
|
|
42
|
+
Get a text choices datastructure from the possible Default Periods starts time
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
a list of tuple containing the string representation of the periods lower time
|
|
46
|
+
"""
|
|
47
|
+
return [str(t) for t in self.default_periods.order_by("lower_time").values_list("lower_time", flat=True)]
|
|
48
|
+
|
|
49
|
+
def get_period_end_choices(self) -> list[str]:
|
|
50
|
+
"""
|
|
51
|
+
Get a text choices datastructure from the possible Default Periods ends time
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
a list of tuple containing the string representation of the periods upper time
|
|
55
|
+
"""
|
|
56
|
+
return [str(t) for t in self.default_periods.order_by("upper_time").values_list("upper_time", flat=True)]
|
|
57
|
+
|
|
58
|
+
def create_public_holidays(self, year: int):
|
|
59
|
+
"""
|
|
60
|
+
Utility function that generate all holiday provided by the "workalendar" package as DayOff for the given year
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
year: Year to generate holidays from
|
|
64
|
+
Raises:
|
|
65
|
+
InvalidDayOffCalendarResourceError if calendar's resource is wrongly formatted.
|
|
66
|
+
"""
|
|
67
|
+
# If no resource is specified, then we exit early
|
|
68
|
+
if self.resource is None or self.resource == "":
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
continent, region = self.resource.split(".")
|
|
72
|
+
try:
|
|
73
|
+
workalendar = import_module(f"workalendar.{continent}")
|
|
74
|
+
holidays = getattr(workalendar, region)()
|
|
75
|
+
for holiday_date, holiday_title in holidays.holidays(year):
|
|
76
|
+
DayOff.objects.get_or_create(
|
|
77
|
+
date=holiday_date, calendar=self, defaults={"count_as_holiday": True, "title": holiday_title}
|
|
78
|
+
)
|
|
79
|
+
except ModuleNotFoundError as e:
|
|
80
|
+
raise InvalidDayOffCalendarResourceError(_("The continent you've supplied is invalid.")) from e
|
|
81
|
+
except AttributeError as e:
|
|
82
|
+
raise InvalidDayOffCalendarResourceError(_("The region you've supplied is invalid.")) from e
|
|
83
|
+
|
|
84
|
+
def get_day_off_per_employee_df(self, start: date, end: date, employees) -> pd.DataFrame:
|
|
85
|
+
"""
|
|
86
|
+
Utility function that reshape the day off for a given calendar and a list of employees among a certain date range
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
start: Start filter
|
|
90
|
+
end: End filter
|
|
91
|
+
employees: Queryset of employees
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A dataframe whose columns are [employee, period, date, type, status] with type HOLIDAY and status APPROVED
|
|
95
|
+
"""
|
|
96
|
+
employee_df = pd.DataFrame(employees.values("id")).rename(columns={"id": "employee"})
|
|
97
|
+
day_off_df = pd.DataFrame(self.days_off.filter(date__gte=start, date__lte=end).values("date"))
|
|
98
|
+
periods_df = pd.DataFrame(self.default_periods.values("id")).rename(columns={"id": "period"})
|
|
99
|
+
df = employee_df.merge(day_off_df, how="cross").merge(periods_df, how="cross")
|
|
100
|
+
df["type"] = "Holiday"
|
|
101
|
+
df["status"] = "APPROVED"
|
|
102
|
+
return df
|
|
103
|
+
|
|
104
|
+
def get_daily_hours(self) -> float:
|
|
105
|
+
"""
|
|
106
|
+
Utility function to return the total number of hours that a typical working day spans from this calendar
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
A float number corresponding to the total working hours
|
|
110
|
+
"""
|
|
111
|
+
return self.default_periods.aggregate(s=Sum("total_hours"))["s"] or 0
|
|
112
|
+
|
|
113
|
+
def get_unworked_time_range(self, start_time: time = None) -> list[tuple[int, int]]:
|
|
114
|
+
"""
|
|
115
|
+
Utility function that return a list of unworked time range. These time range are the exclusion of the periods defined in the default period table and a typical earth time range.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
start_time: If specified, starts the day from that time. Default to time(0,0,0)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
A list of time range tuple
|
|
122
|
+
"""
|
|
123
|
+
if not start_time:
|
|
124
|
+
start_time = time(0, 0, 0)
|
|
125
|
+
rules = rrule.rruleset()
|
|
126
|
+
rules.rrule(
|
|
127
|
+
rrule.rrule(
|
|
128
|
+
freq=rrule.MINUTELY,
|
|
129
|
+
dtstart=datetime(1, 1, 1, 0, 0),
|
|
130
|
+
until=datetime(1, 1, 1, 23, 59),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
for period in self.default_periods.order_by("lower_time"):
|
|
134
|
+
rules.exrule(
|
|
135
|
+
rrule.rrule(
|
|
136
|
+
rrule.MINUTELY,
|
|
137
|
+
dtstart=datetime(1, 1, 1, period.lower_time.hour, period.lower_time.minute, 0),
|
|
138
|
+
until=datetime(1, 1, 1, period.upper_time.hour, period.upper_time.minute, 0),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
non_workable_minutes = list(rules)
|
|
143
|
+
pivot_index = next(x[0] for x in enumerate(non_workable_minutes) if x[1].time() >= start_time)
|
|
144
|
+
non_workable_minutes = non_workable_minutes[pivot_index:] + non_workable_minutes[:pivot_index]
|
|
145
|
+
|
|
146
|
+
start_range = None
|
|
147
|
+
before_minute = None
|
|
148
|
+
for minute in non_workable_minutes:
|
|
149
|
+
if not start_range:
|
|
150
|
+
start_range = minute
|
|
151
|
+
if before_minute and minute.minute != (before_minute.minute + 1) % 60:
|
|
152
|
+
yield start_range.time(), before_minute.time()
|
|
153
|
+
start_range = minute
|
|
154
|
+
before_minute = minute
|
|
155
|
+
yield start_range.time(), before_minute.time()
|
|
156
|
+
|
|
157
|
+
def save(self, *args, **kwargs):
|
|
158
|
+
if not self.resource:
|
|
159
|
+
self.resource = global_preferences_registry.manager()[
|
|
160
|
+
"wbhuman_resources__calendar_default_public_holiday_package"
|
|
161
|
+
]
|
|
162
|
+
if not self.timezone:
|
|
163
|
+
self.timezone = zoneinfo.ZoneInfo(
|
|
164
|
+
global_preferences_registry.manager()["wbhuman_resources__calendar_default_timezone"]
|
|
165
|
+
)
|
|
166
|
+
super().save(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
def get_default_fullday_period(self, val_date):
|
|
169
|
+
return TimestamptzRange(
|
|
170
|
+
lower=self.default_periods.earliest("lower_time").get_lower_datetime(val_date),
|
|
171
|
+
upper=self.default_periods.latest("lower_time").get_upper_datetime(val_date),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def normalize_period(self, period: TimestamptzRange) -> TimestamptzRange:
|
|
175
|
+
"""
|
|
176
|
+
Given a aware range of datetime, ensure that the local time of each corresponds to a valid time choices
|
|
177
|
+
Args:
|
|
178
|
+
period: The period to normalize
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The normalize period
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def _get_closest_time(periods, ts):
|
|
185
|
+
return sorted(
|
|
186
|
+
map(lambda x: (x[0], abs(ts - x[0].hour * 3600 + x[0].minute * 60 + x[0].second)), periods),
|
|
187
|
+
key=lambda x: x[1],
|
|
188
|
+
)[0][0]
|
|
189
|
+
|
|
190
|
+
local_lower_datetime = period.lower.astimezone(self.timezone)
|
|
191
|
+
local_upper_datetime = period.upper.astimezone(self.timezone)
|
|
192
|
+
if str(local_lower_datetime.astimezone(self.timezone).time()) not in self.get_period_start_choices():
|
|
193
|
+
closest_time = _get_closest_time(
|
|
194
|
+
self.default_periods.values_list("lower_time"),
|
|
195
|
+
local_lower_datetime.time().hour * 3600
|
|
196
|
+
+ local_lower_datetime.time().minute * 60
|
|
197
|
+
+ local_lower_datetime.time().second,
|
|
198
|
+
)
|
|
199
|
+
local_lower_datetime = datetime.combine(period.lower.date(), closest_time, tzinfo=self.timezone)
|
|
200
|
+
if str(local_upper_datetime.astimezone(self.timezone).time()) not in self.get_period_end_choices():
|
|
201
|
+
closest_time = _get_closest_time(
|
|
202
|
+
self.default_periods.exclude(lower_time__lt=local_lower_datetime.time()).values_list("upper_time"),
|
|
203
|
+
local_upper_datetime.time().hour * 3600
|
|
204
|
+
+ local_upper_datetime.time().minute * 60
|
|
205
|
+
+ local_upper_datetime.time().second,
|
|
206
|
+
)
|
|
207
|
+
local_upper_datetime = datetime.combine(period.upper.date(), closest_time, tzinfo=self.timezone)
|
|
208
|
+
return TimestamptzRange(lower=local_lower_datetime, upper=local_upper_datetime)
|
|
209
|
+
|
|
210
|
+
class Meta:
|
|
211
|
+
verbose_name = _("Day Off Calendar")
|
|
212
|
+
verbose_name_plural = _("Days Off Calendar")
|
|
213
|
+
unique_together = ("resource", "timezone")
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def get_endpoint_basename(cls) -> str:
|
|
217
|
+
return "wbhuman_resources:dayoffcalendar"
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def get_representation_endpoint(cls) -> str:
|
|
221
|
+
return "wbhuman_resources:dayoffcalendarrepresentation-list"
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def get_representation_value_key(cls) -> str:
|
|
225
|
+
return "id"
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def get_representation_label_key(cls) -> str:
|
|
229
|
+
return "{{title}}"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class DayOff(CalendarItem):
|
|
233
|
+
date = models.DateField(verbose_name=_("Date"))
|
|
234
|
+
count_as_holiday = models.BooleanField(
|
|
235
|
+
default=True,
|
|
236
|
+
verbose_name=_("Count as Holiday"),
|
|
237
|
+
help_text=_("If true, there is no work but the day counts towards the employees' vacation balance"),
|
|
238
|
+
)
|
|
239
|
+
calendar = models.ForeignKey(
|
|
240
|
+
to="wbhuman_resources.DayOffCalendar",
|
|
241
|
+
related_name="days_off",
|
|
242
|
+
on_delete=models.PROTECT,
|
|
243
|
+
verbose_name=_("Calendar"),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
class Meta:
|
|
247
|
+
verbose_name = _("Day Off")
|
|
248
|
+
verbose_name_plural = _("Days Off")
|
|
249
|
+
constraints = [models.UniqueConstraint(fields=["date", "calendar"], name="unique_date_for_calendar")]
|
|
250
|
+
|
|
251
|
+
def __str__(self) -> str:
|
|
252
|
+
return f"{self.title} ({self.date})"
|
|
253
|
+
|
|
254
|
+
def get_color(self) -> str:
|
|
255
|
+
return "#211ae9" # dark blue
|
|
256
|
+
|
|
257
|
+
def get_icon(self) -> str:
|
|
258
|
+
return WBIcon.DAY_OFF.icon
|
|
259
|
+
|
|
260
|
+
def save(self, *args, **kwargs) -> TimestamptzRange:
|
|
261
|
+
default_periods = self.calendar.default_periods
|
|
262
|
+
self.all_day = True
|
|
263
|
+
if default_periods.exists():
|
|
264
|
+
self.period = self.calendar.get_default_fullday_period(self.date)
|
|
265
|
+
super().save(*args, **kwargs)
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def get_endpoint_basename(cls) -> str:
|
|
269
|
+
return "wbhuman_resources:dayoff"
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def get_representation_endpoint(cls) -> str:
|
|
273
|
+
return "wbhuman_resources:dayoffresentation-list"
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def get_representation_value_key(cls) -> str:
|
|
277
|
+
return "id"
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def get_representation_label_key(cls) -> str:
|
|
281
|
+
return "{{title}}"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class DefaultDailyPeriod(models.Model):
|
|
285
|
+
lower_time = models.TimeField()
|
|
286
|
+
upper_time = models.TimeField()
|
|
287
|
+
|
|
288
|
+
timespan = (
|
|
289
|
+
DateTimeRangeField()
|
|
290
|
+
) # "readonly" field that default to combined time with the epoch date, Used to apply database constraint
|
|
291
|
+
title = models.CharField(max_length=128)
|
|
292
|
+
total_hours = models.FloatField()
|
|
293
|
+
|
|
294
|
+
calendar = models.ForeignKey(
|
|
295
|
+
to="wbhuman_resources.DayOffCalendar",
|
|
296
|
+
related_name="default_periods",
|
|
297
|
+
on_delete=models.PROTECT,
|
|
298
|
+
verbose_name=_("Calendar"),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
class Meta:
|
|
302
|
+
verbose_name = _("Default Daily Period")
|
|
303
|
+
verbose_name_plural = _("Default Daily Periods")
|
|
304
|
+
constraints = [
|
|
305
|
+
CheckConstraint(check=Q(upper_time__gt=F("lower_time")), name="check_lower_time_lt_upper_time"),
|
|
306
|
+
ExclusionConstraint(
|
|
307
|
+
expressions=[("timespan", RangeOperators.OVERLAPS), ("calendar", RangeOperators.EQUAL)],
|
|
308
|
+
name="check_no_overlapping_default_periods_time",
|
|
309
|
+
),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
def save(self, *args, **kwargs):
|
|
313
|
+
if not hasattr(self, "calendar") or not self.calendar:
|
|
314
|
+
self.calendar = DayOffCalendar.objects.first()
|
|
315
|
+
self.timespan = TimestamptzRange(
|
|
316
|
+
lower=self.get_lower_datetime(date(1970, 1, 1)),
|
|
317
|
+
upper=self.get_upper_datetime(date(1970, 1, 1)),
|
|
318
|
+
)
|
|
319
|
+
if not self.total_hours:
|
|
320
|
+
self.total_hours = (self.timespan.upper - self.timespan.lower).total_seconds() / 3600
|
|
321
|
+
super().save(*args, **kwargs)
|
|
322
|
+
|
|
323
|
+
def get_lower_datetime(self, val_date: date) -> datetime:
|
|
324
|
+
"""
|
|
325
|
+
Getter function to build a datetime object from this lower period time and the given date
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
val_date: The date to combine the time with
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
A datetime
|
|
332
|
+
"""
|
|
333
|
+
return datetime.combine(val_date, self.lower_time, tzinfo=self.calendar.timezone)
|
|
334
|
+
|
|
335
|
+
def get_upper_datetime(self, val_date: date) -> datetime:
|
|
336
|
+
"""
|
|
337
|
+
Getter function to build a datetime object from this uper period time and the given date
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
val_date: The date to combine the time with
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
A datetime
|
|
344
|
+
"""
|
|
345
|
+
return datetime.combine(val_date, self.upper_time, tzinfo=self.calendar.timezone)
|
|
346
|
+
|
|
347
|
+
def __str__(self) -> str:
|
|
348
|
+
return f"{self.title} ({self.calendar}) [{self.lower_time:%H:%M} - {self.upper_time:%H:%M}]"
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def get_representation_endpoint(cls) -> str:
|
|
352
|
+
return "wbhuman_resources:defaultdailyperiodrepresentation-list"
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def get_representation_value_key(cls) -> str:
|
|
356
|
+
return "id"
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def get_representation_label_key(cls) -> str:
|
|
360
|
+
return "{{title}}"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@receiver(post_migrate, sender=DayOffCalendar)
|
|
364
|
+
def post_migrate_day_off_calendar(sender, verbosity, interactive, stdout, using, plan, apps, **kwargs):
|
|
365
|
+
"""
|
|
366
|
+
After migration, we check if DayOffCalendar have at least one element. OOtherwise, we create a default one
|
|
367
|
+
"""
|
|
368
|
+
if not DayOffCalendar.objects.exists():
|
|
369
|
+
calendar = DayOffCalendar.objects.create(title="Default Calendar", timezone=timezone.get_current_timezone())
|
|
370
|
+
global_preferences_registry["wbhuman_resources__employee_default_calendar"] = calendar
|