wbhuman_resources 1.54.7__py2.py3-none-any.whl → 1.61.5__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/factories/absence.py +1 -0
- wbhuman_resources/factories/calendars.py +1 -0
- wbhuman_resources/factories/employee.py +3 -0
- wbhuman_resources/factories/kpi.py +2 -0
- wbhuman_resources/filters/calendars.py +0 -1
- wbhuman_resources/locale/de/LC_MESSAGES/django.mo +0 -0
- wbhuman_resources/locale/de/LC_MESSAGES/django.po +252 -257
- wbhuman_resources/locale/en/LC_MESSAGES/django.po +248 -252
- wbhuman_resources/locale/fr/LC_MESSAGES/django.po +251 -256
- wbhuman_resources/migrations/0023_alter_employeehumanresource_weekly_off_periods.py +18 -0
- wbhuman_resources/migrations/0024_alter_absencerequestperiods_unique_together_and_more.py +49 -0
- wbhuman_resources/models/absence.py +19 -3
- wbhuman_resources/models/calendars.py +6 -6
- wbhuman_resources/models/employee.py +65 -18
- wbhuman_resources/models/kpi.py +17 -1
- wbhuman_resources/models/review.py +35 -26
- wbhuman_resources/permissions/backend.py +2 -4
- wbhuman_resources/serializers/__init__.py +1 -0
- wbhuman_resources/serializers/absence.py +5 -0
- wbhuman_resources/serializers/calendars.py +2 -1
- wbhuman_resources/serializers/employee.py +3 -3
- wbhuman_resources/signals.py +4 -0
- wbhuman_resources/tasks.py +37 -31
- wbhuman_resources/tests/test_permission.py +19 -15
- wbhuman_resources/utils.py +4 -1
- wbhuman_resources/viewsets/absence.py +18 -26
- wbhuman_resources/viewsets/absence_charts.py +4 -6
- wbhuman_resources/viewsets/buttons/kpis.py +3 -3
- wbhuman_resources/viewsets/buttons/review.py +3 -3
- wbhuman_resources/viewsets/display/calendars.py +2 -3
- wbhuman_resources/viewsets/display/kpis.py +1 -1
- wbhuman_resources/viewsets/display/review.py +5 -5
- wbhuman_resources/viewsets/endpoints/absence.py +4 -1
- wbhuman_resources/viewsets/kpi.py +2 -2
- wbhuman_resources/viewsets/menu/absence.py +11 -7
- wbhuman_resources/viewsets/menu/calendars.py +4 -5
- wbhuman_resources/viewsets/menu/employee.py +5 -6
- wbhuman_resources/viewsets/menu/kpis.py +2 -3
- wbhuman_resources/viewsets/menu/review.py +12 -13
- wbhuman_resources/viewsets/review.py +12 -14
- {wbhuman_resources-1.54.7.dist-info → wbhuman_resources-1.61.5.dist-info}/METADATA +2 -2
- {wbhuman_resources-1.54.7.dist-info → wbhuman_resources-1.61.5.dist-info}/RECORD +43 -40
- {wbhuman_resources-1.54.7.dist-info → wbhuman_resources-1.61.5.dist-info}/WHEEL +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 5.2.9 on 2025-12-09 08:52
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('wbhuman_resources', '0022_remove_review_editable_mode'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='employeehumanresource',
|
|
15
|
+
name='weekly_off_periods',
|
|
16
|
+
field=models.ManyToManyField(related_name='employees_off', through='wbhuman_resources.EmployeeWeeklyOffPeriods', through_fields=('employee', 'period'), to='wbhuman_resources.defaultdailyperiod', verbose_name='Weekly off periods'),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Generated by Django 5.2.9 on 2025-12-16 15:26
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('wbhuman_resources', '0023_alter_employeehumanresource_weekly_off_periods'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterUniqueTogether(
|
|
14
|
+
name='absencerequestperiods',
|
|
15
|
+
unique_together=set(),
|
|
16
|
+
),
|
|
17
|
+
migrations.AlterUniqueTogether(
|
|
18
|
+
name='balancehourlyallowance',
|
|
19
|
+
unique_together=set(),
|
|
20
|
+
),
|
|
21
|
+
migrations.AlterUniqueTogether(
|
|
22
|
+
name='dayoffcalendar',
|
|
23
|
+
unique_together=set(),
|
|
24
|
+
),
|
|
25
|
+
migrations.AlterUniqueTogether(
|
|
26
|
+
name='employeeweeklyoffperiods',
|
|
27
|
+
unique_together=set(),
|
|
28
|
+
),
|
|
29
|
+
migrations.AlterUniqueTogether(
|
|
30
|
+
name='employeeyearbalance',
|
|
31
|
+
unique_together=set(),
|
|
32
|
+
),
|
|
33
|
+
migrations.AddConstraint(
|
|
34
|
+
model_name='balancehourlyallowance',
|
|
35
|
+
constraint=models.UniqueConstraint(fields=('balance', 'period_index'), name='unique_balanceallowance'),
|
|
36
|
+
),
|
|
37
|
+
migrations.AddConstraint(
|
|
38
|
+
model_name='dayoffcalendar',
|
|
39
|
+
constraint=models.UniqueConstraint(fields=('resource', 'timezone'), name='unique_calendar'),
|
|
40
|
+
),
|
|
41
|
+
migrations.AddConstraint(
|
|
42
|
+
model_name='employeeweeklyoffperiods',
|
|
43
|
+
constraint=models.UniqueConstraint(fields=('employee', 'period', 'weekday'), name='unique_weeklyoffperiod'),
|
|
44
|
+
),
|
|
45
|
+
migrations.AddConstraint(
|
|
46
|
+
model_name='employeeyearbalance',
|
|
47
|
+
constraint=models.UniqueConstraint(fields=('employee', 'year'), name='unique_employeeyearbalance'),
|
|
48
|
+
),
|
|
49
|
+
]
|
|
@@ -2,7 +2,6 @@ from datetime import timedelta
|
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
from django.contrib import admin
|
|
5
|
-
from django.contrib.auth import get_user_model
|
|
6
5
|
from django.contrib.auth.models import Group
|
|
7
6
|
from django.contrib.postgres.constraints import ExclusionConstraint
|
|
8
7
|
from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators
|
|
@@ -20,6 +19,7 @@ from django_fsm import FSMField, transition
|
|
|
20
19
|
from pandas._libs.tslibs.offsets import BDay
|
|
21
20
|
from psycopg.types.range import TimestamptzRange
|
|
22
21
|
from wbcore.contrib.agenda.models import CalendarItem
|
|
22
|
+
from wbcore.contrib.authentication.models.users import User
|
|
23
23
|
from wbcore.contrib.geography.models import Geography
|
|
24
24
|
from wbcore.contrib.icons import WBIcon
|
|
25
25
|
from wbcore.contrib.notifications.dispatch import send_notification
|
|
@@ -35,7 +35,21 @@ from wbcore.utils.models import CalendarItemTypeMixin
|
|
|
35
35
|
from .calendars import DayOff, DefaultDailyPeriod
|
|
36
36
|
from .preferences import get_previous_year_balance_expiration_date
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
def get_valid_employee_human_resource(user: "User"):
|
|
40
|
+
if (employee := getattr(user.profile, "human_resources", None)) and employee.is_active and user.profile:
|
|
41
|
+
return employee
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def can_edit_request(instance: "AbsenceRequest", user: "User") -> bool:
|
|
45
|
+
if employee := get_valid_employee_human_resource(user):
|
|
46
|
+
requester = instance.employee
|
|
47
|
+
return instance.status == AbsenceRequest.Status.DRAFT and (
|
|
48
|
+
user.has_perm("wbhuman_resources.administrate_absencerequest")
|
|
49
|
+
or employee.is_manager_of(requester)
|
|
50
|
+
or user.profile == requester.profile
|
|
51
|
+
)
|
|
52
|
+
return False
|
|
39
53
|
|
|
40
54
|
|
|
41
55
|
def can_cancel_request(instance: "AbsenceRequest", user: "User") -> bool:
|
|
@@ -643,7 +657,9 @@ class AbsenceRequestPeriods(models.Model):
|
|
|
643
657
|
class Meta:
|
|
644
658
|
verbose_name = _("Absence Request Period")
|
|
645
659
|
verbose_name_plural = _("Absence Request Periods")
|
|
646
|
-
|
|
660
|
+
constraints = (
|
|
661
|
+
models.UniqueConstraint(name="unique_requestperiod", fields=("employee", "default_period", "date")),
|
|
662
|
+
)
|
|
647
663
|
indexes = [
|
|
648
664
|
models.Index(fields=["employee", "default_period", "date"]),
|
|
649
665
|
]
|
|
@@ -76,10 +76,10 @@ class DayOffCalendar(WBModel):
|
|
|
76
76
|
DayOff.objects.get_or_create(
|
|
77
77
|
date=holiday_date, calendar=self, defaults={"count_as_holiday": True, "title": holiday_title}
|
|
78
78
|
)
|
|
79
|
-
except ModuleNotFoundError:
|
|
80
|
-
raise InvalidDayOffCalendarResourceError(_("The continent you've supplied is invalid."))
|
|
81
|
-
except AttributeError:
|
|
82
|
-
raise InvalidDayOffCalendarResourceError(_("The region you've supplied is invalid."))
|
|
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
83
|
|
|
84
84
|
def get_day_off_per_employee_df(self, start: date, end: date, employees) -> pd.DataFrame:
|
|
85
85
|
"""
|
|
@@ -210,7 +210,7 @@ class DayOffCalendar(WBModel):
|
|
|
210
210
|
class Meta:
|
|
211
211
|
verbose_name = _("Day Off Calendar")
|
|
212
212
|
verbose_name_plural = _("Days Off Calendar")
|
|
213
|
-
|
|
213
|
+
constraints = (models.UniqueConstraint(name="unique_calendar", fields=("resource", "timezone")),)
|
|
214
214
|
|
|
215
215
|
@classmethod
|
|
216
216
|
def get_endpoint_basename(cls) -> str:
|
|
@@ -302,7 +302,7 @@ class DefaultDailyPeriod(models.Model):
|
|
|
302
302
|
verbose_name = _("Default Daily Period")
|
|
303
303
|
verbose_name_plural = _("Default Daily Periods")
|
|
304
304
|
constraints = [
|
|
305
|
-
CheckConstraint(
|
|
305
|
+
CheckConstraint(condition=Q(upper_time__gt=F("lower_time")), name="check_lower_time_lt_upper_time"),
|
|
306
306
|
ExclusionConstraint(
|
|
307
307
|
expressions=[("timespan", RangeOperators.OVERLAPS), ("calendar", RangeOperators.EQUAL)],
|
|
308
308
|
name="check_no_overlapping_default_periods_time",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import math
|
|
2
2
|
import operator
|
|
3
|
+
from contextlib import suppress
|
|
3
4
|
from datetime import date, datetime, timedelta
|
|
4
5
|
from functools import reduce
|
|
5
6
|
from typing import Generator, List, Optional, Tuple, Type, TypeVar
|
|
@@ -8,7 +9,6 @@ import pandas as pd
|
|
|
8
9
|
from celery import shared_task
|
|
9
10
|
from colorfield.fields import ColorField
|
|
10
11
|
from django.contrib import admin
|
|
11
|
-
from django.contrib.auth import get_user_model
|
|
12
12
|
from django.contrib.auth.models import Group, Permission
|
|
13
13
|
from django.contrib.contenttypes.models import ContentType
|
|
14
14
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
@@ -38,6 +38,7 @@ from django.utils.translation import gettext_lazy as _
|
|
|
38
38
|
from dynamic_preferences.registries import global_preferences_registry
|
|
39
39
|
from mptt.models import MPTTModel, TreeForeignKey
|
|
40
40
|
from psycopg.types.range import TimestamptzRange
|
|
41
|
+
from wbcore.contrib.authentication.models.users import User
|
|
41
42
|
from wbcore.contrib.directory.models import (
|
|
42
43
|
Company,
|
|
43
44
|
EmployerEmployeeRelationship,
|
|
@@ -49,7 +50,11 @@ from wbcore.contrib.notifications.dispatch import send_notification
|
|
|
49
50
|
from wbcore.contrib.notifications.utils import create_notification_type
|
|
50
51
|
from wbcore.models import WBModel
|
|
51
52
|
from wbcore.models.fields import YearField
|
|
52
|
-
from wbcore.
|
|
53
|
+
from wbcore.signals import pre_merge
|
|
54
|
+
from wbcore.utils.models import ComplexToStringMixin, MergeError
|
|
55
|
+
from wbcore.workers import Queue
|
|
56
|
+
|
|
57
|
+
from wbhuman_resources.signals import add_employee_activity_to_daily_brief
|
|
53
58
|
|
|
54
59
|
from .absence import AbsenceRequest, AbsenceRequestPeriods
|
|
55
60
|
from .calendars import DayOff, DayOffCalendar, DefaultDailyPeriod
|
|
@@ -61,8 +66,6 @@ from .preferences import (
|
|
|
61
66
|
long_vacation_number_of_days,
|
|
62
67
|
)
|
|
63
68
|
|
|
64
|
-
User = get_user_model()
|
|
65
|
-
|
|
66
69
|
|
|
67
70
|
class ActiveEmployeeManager(models.Manager):
|
|
68
71
|
"""Custom Manager for filtering directly Active Employees. Exclude objects without reverse related field user_account and profile"""
|
|
@@ -235,11 +238,6 @@ class EmployeeHumanResource(ComplexToStringMixin, WBModel):
|
|
|
235
238
|
title="Vacation Notification",
|
|
236
239
|
help_text="Notifies you when there are Vacation days that you still have to take",
|
|
237
240
|
),
|
|
238
|
-
create_notification_type(
|
|
239
|
-
code="wbhuman_resources.employeehumanresource.birthday",
|
|
240
|
-
title="Birthday Notification",
|
|
241
|
-
help_text="Notifies you when today one of your colleagues is celebrating their birthday",
|
|
242
|
-
),
|
|
243
241
|
]
|
|
244
242
|
|
|
245
243
|
def unassign_position_groups(self):
|
|
@@ -545,8 +543,7 @@ class EmployeeHumanResource(ComplexToStringMixin, WBModel):
|
|
|
545
543
|
The administrator user accounts (as queryset)
|
|
546
544
|
"""
|
|
547
545
|
return (
|
|
548
|
-
|
|
549
|
-
.objects.filter(is_active=True, profile__isnull=False)
|
|
546
|
+
User.objects.filter(is_active=True, profile__isnull=False)
|
|
550
547
|
.filter(
|
|
551
548
|
Q(groups__permissions__codename="administrate_absencerequest")
|
|
552
549
|
| Q(user_permissions__codename="administrate_absencerequest")
|
|
@@ -824,14 +821,17 @@ class BalanceHourlyAllowance(models.Model):
|
|
|
824
821
|
class Meta:
|
|
825
822
|
verbose_name = _("Monthly Allowance")
|
|
826
823
|
verbose_name_plural = _("Monthly Allowance")
|
|
827
|
-
|
|
824
|
+
constraints = (models.UniqueConstraint(name="unique_balanceallowance", fields=("balance", "period_index")),)
|
|
828
825
|
indexes = [
|
|
829
826
|
models.Index(fields=["balance"]),
|
|
830
827
|
models.Index(fields=["balance", "period_index"]),
|
|
831
828
|
]
|
|
832
829
|
|
|
830
|
+
def __str__(self) -> str:
|
|
831
|
+
return f"{self.balance} - {self.period_index} Period Index: {self.hourly_allowance}"
|
|
832
|
+
|
|
833
833
|
|
|
834
|
-
class EmployeeYearBalance(ComplexToStringMixin
|
|
834
|
+
class EmployeeYearBalance(ComplexToStringMixin):
|
|
835
835
|
employee = models.ForeignKey(
|
|
836
836
|
EmployeeHumanResource,
|
|
837
837
|
related_name="balances",
|
|
@@ -854,7 +854,7 @@ class EmployeeYearBalance(ComplexToStringMixin, models.Model):
|
|
|
854
854
|
class Meta:
|
|
855
855
|
verbose_name = _("Employee Year Balance")
|
|
856
856
|
verbose_name_plural = _("Employee Year Balances")
|
|
857
|
-
|
|
857
|
+
constraints = (models.UniqueConstraint(name="unique_employeeyearbalance", fields=("employee", "year")),)
|
|
858
858
|
indexes = [
|
|
859
859
|
models.Index(fields=["employee", "year"]),
|
|
860
860
|
]
|
|
@@ -935,7 +935,7 @@ class EmployeeYearBalance(ComplexToStringMixin, models.Model):
|
|
|
935
935
|
return "wbhuman_resources:employeeyearbalancerepresentation-list"
|
|
936
936
|
|
|
937
937
|
|
|
938
|
-
class EmployeeWeeklyOffPeriods(ComplexToStringMixin
|
|
938
|
+
class EmployeeWeeklyOffPeriods(ComplexToStringMixin):
|
|
939
939
|
employee = models.ForeignKey(
|
|
940
940
|
"wbhuman_resources.EmployeeHumanResource",
|
|
941
941
|
related_name="default_periods_relationships",
|
|
@@ -975,7 +975,9 @@ class EmployeeWeeklyOffPeriods(ComplexToStringMixin, models.Model):
|
|
|
975
975
|
class Meta:
|
|
976
976
|
verbose_name = _("Employee Weekly off Period")
|
|
977
977
|
verbose_name_plural = _("Employee Weekly off Periods")
|
|
978
|
-
|
|
978
|
+
constraints = (
|
|
979
|
+
models.UniqueConstraint(name="unique_weeklyoffperiod", fields=("employee", "period", "weekday")),
|
|
980
|
+
)
|
|
979
981
|
indexes = [
|
|
980
982
|
models.Index(fields=["employee", "period", "weekday"]),
|
|
981
983
|
]
|
|
@@ -1184,7 +1186,7 @@ def position_groups_to_employee(
|
|
|
1184
1186
|
user.groups.remove(group)
|
|
1185
1187
|
|
|
1186
1188
|
|
|
1187
|
-
@shared_task
|
|
1189
|
+
@shared_task(queue=Queue.DEFAULT.value)
|
|
1188
1190
|
def deactivate_profile_as_task(requester_id: int, employee_id: int, substitute_id: Optional[int] = None):
|
|
1189
1191
|
"""
|
|
1190
1192
|
Call the deactivation method as a async task
|
|
@@ -1195,7 +1197,7 @@ def deactivate_profile_as_task(requester_id: int, employee_id: int, substitute_i
|
|
|
1195
1197
|
substitute_id: The potential Profile id to replace this employee's resources
|
|
1196
1198
|
|
|
1197
1199
|
"""
|
|
1198
|
-
requester =
|
|
1200
|
+
requester = User.objects.get(id=requester_id)
|
|
1199
1201
|
employee = EmployeeHumanResource.objects.get(id=employee_id)
|
|
1200
1202
|
substitute = None
|
|
1201
1203
|
if substitute_id:
|
|
@@ -1210,3 +1212,48 @@ def deactivate_profile_as_task(requester_id: int, employee_id: int, substitute_i
|
|
|
1210
1212
|
body=gettext("The following actions have been done: \n{messages}").format(messages=messages),
|
|
1211
1213
|
user=requester,
|
|
1212
1214
|
)
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
@receiver(add_employee_activity_to_daily_brief, sender="directory.Person")
|
|
1218
|
+
def daily_birthday(sender, instance: Person, val_date: date, **kwargs) -> tuple[str, str] | None:
|
|
1219
|
+
"""
|
|
1220
|
+
Cron task supposed to be ran every day. Check and notify employee about a colleague's birthday.
|
|
1221
|
+
"""
|
|
1222
|
+
|
|
1223
|
+
# If daily brief is a monday, we get the birthday that happen during the weekend as well
|
|
1224
|
+
if val_date.weekday() == 0:
|
|
1225
|
+
sunday = val_date - timedelta(days=1)
|
|
1226
|
+
saturday = val_date - timedelta(days=2)
|
|
1227
|
+
conditions = (
|
|
1228
|
+
(Q(profile__birthday__day=val_date.day) & Q(profile__birthday__month=val_date.month))
|
|
1229
|
+
| (Q(profile__birthday__day=sunday.day) & Q(profile__birthday__month=sunday.month))
|
|
1230
|
+
| (Q(profile__birthday__day=saturday.day) & Q(profile__birthday__month=saturday.month))
|
|
1231
|
+
)
|
|
1232
|
+
else:
|
|
1233
|
+
conditions = Q(profile__birthday__day=val_date.day) & Q(profile__birthday__month=val_date.month)
|
|
1234
|
+
birthday_firstnames = list(
|
|
1235
|
+
EmployeeHumanResource.active_internal_employees.filter(conditions)
|
|
1236
|
+
.exclude(profile=instance)
|
|
1237
|
+
.values_list("profile__first_name", flat=True)
|
|
1238
|
+
)
|
|
1239
|
+
if birthday_firstnames:
|
|
1240
|
+
birthday_firstnames_humanized = f"{', '.join(birthday_firstnames[:-1])} and {birthday_firstnames[-1]}"
|
|
1241
|
+
return "Today Birthdays", _("Today is {}'s birthday, Which them a happy birthday!").format(
|
|
1242
|
+
birthday_firstnames_humanized
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
@receiver(pre_merge, sender="directory.Person")
|
|
1247
|
+
def pre_merge_person_employee(sender: models.Model, merged_object, main_object, **kwargs):
|
|
1248
|
+
Position.objects.filter(manager=merged_object).update(manager=main_object)
|
|
1249
|
+
EmployeeHumanResource.objects.filter(direct_manager=merged_object).update(direct_manager=main_object)
|
|
1250
|
+
|
|
1251
|
+
with suppress(EmployeeHumanResource.DoesNotExist):
|
|
1252
|
+
hr = EmployeeHumanResource.objects.get(profile=merged_object)
|
|
1253
|
+
if EmployeeHumanResource.objects.filter(profile=main_object).exists():
|
|
1254
|
+
raise MergeError(
|
|
1255
|
+
f"We cannot safely merge directory profile {merged_object} into {main_object}: A HR profile already exists for {main_object}"
|
|
1256
|
+
)
|
|
1257
|
+
else:
|
|
1258
|
+
hr.profile = main_object
|
|
1259
|
+
hr.save()
|
wbhuman_resources/models/kpi.py
CHANGED
|
@@ -8,9 +8,11 @@ from django.conf import settings
|
|
|
8
8
|
from django.contrib.postgres.fields import DateRangeField
|
|
9
9
|
from django.db import models
|
|
10
10
|
from django.db.models.query import QuerySet
|
|
11
|
+
from django.dispatch import receiver
|
|
11
12
|
from django.utils.translation import gettext_lazy as _
|
|
12
13
|
from wbcore.models import WBModel
|
|
13
14
|
from wbcore.serializers.serializers import ModelSerializer
|
|
15
|
+
from wbcore.signals import pre_merge
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class KPIHandler(Protocol):
|
|
@@ -82,7 +84,8 @@ class KPI(WBModel):
|
|
|
82
84
|
|
|
83
85
|
@staticmethod
|
|
84
86
|
def get_all_handlers() -> Iterator[KPIHandler]:
|
|
85
|
-
for
|
|
87
|
+
for param in settings.KPI_HANDLERS:
|
|
88
|
+
handler_path = param[0]
|
|
86
89
|
*module, handler = handler_path.split(".")
|
|
87
90
|
module_path = ".".join(module)
|
|
88
91
|
yield getattr(importlib.import_module(module_path), handler)()
|
|
@@ -177,6 +180,9 @@ class Evaluation(models.Model):
|
|
|
177
180
|
verbose_name = _("Evaluation")
|
|
178
181
|
verbose_name_plural = _("Evaluations")
|
|
179
182
|
|
|
183
|
+
def __str__(self) -> str:
|
|
184
|
+
return f"{self.kpi} - {self.evaluation_date}"
|
|
185
|
+
|
|
180
186
|
def get_rating(self):
|
|
181
187
|
list_date = list(
|
|
182
188
|
pd.date_range(
|
|
@@ -193,3 +199,13 @@ class Evaluation(models.Model):
|
|
|
193
199
|
rating = (self.evaluated_score / goal_expected * 100).round(2)
|
|
194
200
|
rating = round(rating / 100 * 4)
|
|
195
201
|
return 1 if rating < 1 else rating if rating < 4 else 4
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@receiver(pre_merge, sender="directory.Person")
|
|
205
|
+
def pre_merge_person_kpi(sender: models.Model, merged_object, main_object, **kwargs):
|
|
206
|
+
Evaluation.objects.filter(person=merged_object).update(person=main_object)
|
|
207
|
+
|
|
208
|
+
for kpi in KPI.objects.filter(evaluated_persons=merged_object):
|
|
209
|
+
kpi.evaluated_persons.remove(merged_object)
|
|
210
|
+
if main_object not in kpi.evaluated_persons.all():
|
|
211
|
+
kpi.evaluated_persons.add(main_object)
|
|
@@ -4,7 +4,6 @@ from typing import TypeVar
|
|
|
4
4
|
|
|
5
5
|
from celery import shared_task
|
|
6
6
|
from django.conf import settings
|
|
7
|
-
from django.contrib.auth import get_user_model
|
|
8
7
|
from django.contrib.sites.models import Site
|
|
9
8
|
from django.core.files.base import ContentFile
|
|
10
9
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
@@ -30,6 +29,7 @@ from django.utils.translation import gettext
|
|
|
30
29
|
from django.utils.translation import gettext_lazy as _
|
|
31
30
|
from django_fsm import FSMField, transition
|
|
32
31
|
from slugify import slugify
|
|
32
|
+
from wbcore.contrib.authentication.models.users import User
|
|
33
33
|
from wbcore.contrib.color.enums import WBColor
|
|
34
34
|
from wbcore.contrib.directory.models import Person
|
|
35
35
|
from wbcore.contrib.documents.models import Document, DocumentType
|
|
@@ -42,14 +42,15 @@ from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
|
|
|
42
42
|
from wbcore.models import WBModel
|
|
43
43
|
from wbcore.models.fields import YearField
|
|
44
44
|
from wbcore.models.orderable import OrderableModel
|
|
45
|
+
from wbcore.signals import pre_merge
|
|
45
46
|
from wbcore.utils.models import CloneMixin, ComplexToStringMixin
|
|
47
|
+
from wbcore.workers import Queue
|
|
46
48
|
from weasyprint import HTML
|
|
47
49
|
|
|
48
50
|
from wbhuman_resources.models.employee import get_main_company
|
|
49
51
|
from wbhuman_resources.models.kpi import Evaluation
|
|
50
52
|
|
|
51
53
|
SelfReview = TypeVar("SelfReview", bound="Review")
|
|
52
|
-
User = get_user_model()
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def can_trigger_review(instance, user):
|
|
@@ -105,7 +106,7 @@ class Review(CloneMixin, ComplexToStringMixin, WBModel):
|
|
|
105
106
|
WBColor.GREEN_LIGHT.value,
|
|
106
107
|
WBColor.GREEN_DARK.value,
|
|
107
108
|
]
|
|
108
|
-
return [choice for choice in zip(cls, colors)]
|
|
109
|
+
return [choice for choice in zip(cls, colors, strict=False)]
|
|
109
110
|
|
|
110
111
|
class Type(models.TextChoices):
|
|
111
112
|
ANNUAL = "ANNUAL", _("Annual")
|
|
@@ -238,14 +239,10 @@ class Review(CloneMixin, ComplexToStringMixin, WBModel):
|
|
|
238
239
|
|
|
239
240
|
@classmethod
|
|
240
241
|
def get_administrators(cls) -> QuerySet[User]:
|
|
241
|
-
return (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
| Q(user_permissions__codename="administrate_review")
|
|
246
|
-
)
|
|
247
|
-
.distinct()
|
|
248
|
-
)
|
|
242
|
+
return User.objects.filter(
|
|
243
|
+
Q(groups__permissions__codename="administrate_review")
|
|
244
|
+
| Q(user_permissions__codename="administrate_review")
|
|
245
|
+
).distinct()
|
|
249
246
|
|
|
250
247
|
@transition(
|
|
251
248
|
field=status,
|
|
@@ -373,14 +370,10 @@ class Review(CloneMixin, ComplexToStringMixin, WBModel):
|
|
|
373
370
|
},
|
|
374
371
|
)
|
|
375
372
|
def validation(self, by=None, description=None, **kwargs):
|
|
376
|
-
users = (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
| Q(user_permissions__codename="administrate_review")
|
|
381
|
-
)
|
|
382
|
-
.distinct()
|
|
383
|
-
)
|
|
373
|
+
users = User.objects.filter(
|
|
374
|
+
Q(groups__permissions__codename="administrate_review")
|
|
375
|
+
| Q(user_permissions__codename="administrate_review")
|
|
376
|
+
).distinct()
|
|
384
377
|
for user in users:
|
|
385
378
|
send_review_report_via_mail.delay(user.id, self.id)
|
|
386
379
|
|
|
@@ -721,6 +714,9 @@ class ReviewAnswer(models.Model):
|
|
|
721
714
|
)
|
|
722
715
|
answer_text = models.TextField(null=True, blank=True, verbose_name=_("Comment"))
|
|
723
716
|
|
|
717
|
+
def __str__(self) -> str:
|
|
718
|
+
return f"{self.question} - {self.answered_by}"
|
|
719
|
+
|
|
724
720
|
@classmethod
|
|
725
721
|
def get_endpoint_basename(cls):
|
|
726
722
|
return "wbhuman_resources:reviewanswer"
|
|
@@ -754,7 +750,7 @@ def pre_save_review(sender, instance, **kwargs):
|
|
|
754
750
|
instance.validation()
|
|
755
751
|
|
|
756
752
|
|
|
757
|
-
@shared_task
|
|
753
|
+
@shared_task(queue=Queue.DEFAULT.value)
|
|
758
754
|
def finalize_review(review_id):
|
|
759
755
|
review = Review.objects.get(id=review_id)
|
|
760
756
|
ReviewAnswer.objects.filter(
|
|
@@ -765,9 +761,9 @@ def finalize_review(review_id):
|
|
|
765
761
|
).update(answer_number=1)
|
|
766
762
|
|
|
767
763
|
|
|
768
|
-
@shared_task
|
|
764
|
+
@shared_task(queue=Queue.DEFAULT.value)
|
|
769
765
|
def submit_reviews_from_group(group_id, user_id):
|
|
770
|
-
user =
|
|
766
|
+
user = User.objects.get(id=user_id)
|
|
771
767
|
|
|
772
768
|
reviews = Review.objects.filter(
|
|
773
769
|
Q(review_group__id=group_id)
|
|
@@ -780,7 +776,7 @@ def submit_reviews_from_group(group_id, user_id):
|
|
|
780
776
|
review.save()
|
|
781
777
|
|
|
782
778
|
|
|
783
|
-
@shared_task
|
|
779
|
+
@shared_task(queue=Queue.DEFAULT.value)
|
|
784
780
|
def create_review_from_template(
|
|
785
781
|
template_id, from_date, to_date, review_deadline, auto_apply_deadline, employees, include_kpi
|
|
786
782
|
):
|
|
@@ -881,7 +877,7 @@ What do you think of this result?
|
|
|
881
877
|
ReviewQuestion.objects.create(**kwargs)
|
|
882
878
|
|
|
883
879
|
|
|
884
|
-
@shared_task
|
|
880
|
+
@shared_task(queue=Queue.DEFAULT.value)
|
|
885
881
|
def submit_review(review_id):
|
|
886
882
|
review = Review.objects.get(id=review_id)
|
|
887
883
|
questions = ReviewQuestion.objects.filter(review=review)
|
|
@@ -953,9 +949,9 @@ def submit_review(review_id):
|
|
|
953
949
|
)
|
|
954
950
|
|
|
955
951
|
|
|
956
|
-
@shared_task
|
|
952
|
+
@shared_task(queue=Queue.DEFAULT.value)
|
|
957
953
|
def send_review_report_via_mail(user_id, review_id):
|
|
958
|
-
user =
|
|
954
|
+
user = User.objects.get(id=user_id)
|
|
959
955
|
review = Review.objects.get(id=review_id)
|
|
960
956
|
pdf_content = review.generate_pdf()
|
|
961
957
|
filename = f"{slugify(str(review))}.pdf"
|
|
@@ -977,3 +973,16 @@ def send_review_report_via_mail(user_id, review_id):
|
|
|
977
973
|
document.link(review)
|
|
978
974
|
|
|
979
975
|
document.send_email(to_emails=user.email, as_link=True, subject=gettext("Review PDF - ") + review.computed_str)
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
@receiver(pre_merge, sender="directory.Person")
|
|
979
|
+
def pre_merge_person_review(sender: models.Model, merged_object, main_object, **kwargs):
|
|
980
|
+
ReviewAnswer.objects.filter(answered_by=merged_object).update(answered_by=main_object)
|
|
981
|
+
Review.objects.filter(reviewee=merged_object).update(reviewee=main_object)
|
|
982
|
+
Review.objects.filter(reviewer=merged_object).update(reviewer=main_object)
|
|
983
|
+
Review.objects.filter(moderator=merged_object).update(moderator=main_object)
|
|
984
|
+
|
|
985
|
+
for group in ReviewGroup.objects.filter(employees=merged_object):
|
|
986
|
+
group.employees.remove(merged_object)
|
|
987
|
+
if main_object not in group.employees.all():
|
|
988
|
+
group.employees.add(main_object)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
from django.contrib.auth import get_user_model
|
|
2
1
|
from django.db.models import Q, QuerySet
|
|
3
|
-
from wbcore.
|
|
2
|
+
from wbcore.contrib.authentication.models.users import User
|
|
3
|
+
from wbcore.contrib.permission.internal.backend import UserBackend as BaseUserBackend
|
|
4
4
|
|
|
5
5
|
from wbhuman_resources.models.employee import EmployeeHumanResource
|
|
6
6
|
|
|
7
|
-
User = get_user_model()
|
|
8
|
-
|
|
9
7
|
|
|
10
8
|
class UserBackend(BaseUserBackend):
|
|
11
9
|
"""
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .absence import (
|
|
2
2
|
AbsenceRequestCrossBorderCountryModelSerializer,
|
|
3
3
|
AbsenceRequestModelSerializer,
|
|
4
|
+
ReadOnlyAbsenceRequestModelSerializer,
|
|
4
5
|
AbsenceRequestPeriodsModelSerializer,
|
|
5
6
|
AbsenceRequestTypeModelSerializer,
|
|
6
7
|
AbsenceRequestTypeRepresentationSerializer,
|
|
@@ -269,6 +269,11 @@ class AbsenceRequestModelSerializer(wb_serializers.ModelSerializer):
|
|
|
269
269
|
]
|
|
270
270
|
|
|
271
271
|
|
|
272
|
+
class ReadOnlyAbsenceRequestModelSerializer(AbsenceRequestModelSerializer):
|
|
273
|
+
class Meta(AbsenceRequestModelSerializer.Meta):
|
|
274
|
+
read_only_fields = AbsenceRequestModelSerializer.Meta.fields
|
|
275
|
+
|
|
276
|
+
|
|
272
277
|
class EmployeeAbsenceDaysModelSerializer(wb_serializers.ModelSerializer):
|
|
273
278
|
year = wb_serializers.YearField(read_only=True)
|
|
274
279
|
absence_type = wb_serializers.ChoiceField(read_only=True, choices=AbsenceRequestType.get_choices())
|
|
@@ -58,10 +58,11 @@ class DayOffCalendarModelSerializer(wb_serializers.ModelSerializer):
|
|
|
58
58
|
|
|
59
59
|
class DefaultDailyPeriodModelSerializer(wb_serializers.ModelSerializer):
|
|
60
60
|
_calendar = DayOffCalendarRepresentationSerializer(source="calendar")
|
|
61
|
+
timespan = wb_serializers.TimeRange(timerange_fields=("lower_time", "upper_time"))
|
|
61
62
|
|
|
62
63
|
class Meta:
|
|
63
64
|
model = DefaultDailyPeriod
|
|
64
|
-
fields = ("id", "
|
|
65
|
+
fields = ("id", "timespan", "title", "total_hours", "calendar", "_calendar")
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
class DayOffModelSerializer(wb_serializers.ModelSerializer):
|
|
@@ -167,9 +167,9 @@ class EmployeeModelSerializer(wb_serializers.ModelSerializer):
|
|
|
167
167
|
_position_manager = PersonRepresentationSerializer(
|
|
168
168
|
source="position_manager", filter_params={"is_internal_profile": True}
|
|
169
169
|
)
|
|
170
|
-
top_position_repr = wb_serializers.CharField(read_only=True
|
|
171
|
-
primary_email = wb_serializers.CharField(
|
|
172
|
-
primary_address = wb_serializers.CharField(
|
|
170
|
+
top_position_repr = wb_serializers.CharField(read_only=True)
|
|
171
|
+
primary_email = wb_serializers.CharField(label=_("Primary Email"), allow_null=True, read_only=True)
|
|
172
|
+
primary_address = wb_serializers.CharField(label=_("Primary Address"), allow_null=True, read_only=True)
|
|
173
173
|
primary_telephone = wb_serializers.TelephoneField(label=_("Primary Telephone"), allow_null=True, read_only=True)
|
|
174
174
|
|
|
175
175
|
@wb_serializers.register_resource()
|