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.
Files changed (43) hide show
  1. wbhuman_resources/factories/absence.py +1 -0
  2. wbhuman_resources/factories/calendars.py +1 -0
  3. wbhuman_resources/factories/employee.py +3 -0
  4. wbhuman_resources/factories/kpi.py +2 -0
  5. wbhuman_resources/filters/calendars.py +0 -1
  6. wbhuman_resources/locale/de/LC_MESSAGES/django.mo +0 -0
  7. wbhuman_resources/locale/de/LC_MESSAGES/django.po +252 -257
  8. wbhuman_resources/locale/en/LC_MESSAGES/django.po +248 -252
  9. wbhuman_resources/locale/fr/LC_MESSAGES/django.po +251 -256
  10. wbhuman_resources/migrations/0023_alter_employeehumanresource_weekly_off_periods.py +18 -0
  11. wbhuman_resources/migrations/0024_alter_absencerequestperiods_unique_together_and_more.py +49 -0
  12. wbhuman_resources/models/absence.py +19 -3
  13. wbhuman_resources/models/calendars.py +6 -6
  14. wbhuman_resources/models/employee.py +65 -18
  15. wbhuman_resources/models/kpi.py +17 -1
  16. wbhuman_resources/models/review.py +35 -26
  17. wbhuman_resources/permissions/backend.py +2 -4
  18. wbhuman_resources/serializers/__init__.py +1 -0
  19. wbhuman_resources/serializers/absence.py +5 -0
  20. wbhuman_resources/serializers/calendars.py +2 -1
  21. wbhuman_resources/serializers/employee.py +3 -3
  22. wbhuman_resources/signals.py +4 -0
  23. wbhuman_resources/tasks.py +37 -31
  24. wbhuman_resources/tests/test_permission.py +19 -15
  25. wbhuman_resources/utils.py +4 -1
  26. wbhuman_resources/viewsets/absence.py +18 -26
  27. wbhuman_resources/viewsets/absence_charts.py +4 -6
  28. wbhuman_resources/viewsets/buttons/kpis.py +3 -3
  29. wbhuman_resources/viewsets/buttons/review.py +3 -3
  30. wbhuman_resources/viewsets/display/calendars.py +2 -3
  31. wbhuman_resources/viewsets/display/kpis.py +1 -1
  32. wbhuman_resources/viewsets/display/review.py +5 -5
  33. wbhuman_resources/viewsets/endpoints/absence.py +4 -1
  34. wbhuman_resources/viewsets/kpi.py +2 -2
  35. wbhuman_resources/viewsets/menu/absence.py +11 -7
  36. wbhuman_resources/viewsets/menu/calendars.py +4 -5
  37. wbhuman_resources/viewsets/menu/employee.py +5 -6
  38. wbhuman_resources/viewsets/menu/kpis.py +2 -3
  39. wbhuman_resources/viewsets/menu/review.py +12 -13
  40. wbhuman_resources/viewsets/review.py +12 -14
  41. {wbhuman_resources-1.54.7.dist-info → wbhuman_resources-1.61.5.dist-info}/METADATA +2 -2
  42. {wbhuman_resources-1.54.7.dist-info → wbhuman_resources-1.61.5.dist-info}/RECORD +43 -40
  43. {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
- User = get_user_model()
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
- unique_together = ("employee", "default_period", "date")
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
- unique_together = ("resource", "timezone")
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(check=Q(upper_time__gt=F("lower_time")), name="check_lower_time_lt_upper_time"),
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.utils.models import ComplexToStringMixin
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
- get_user_model()
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
- unique_together = ("balance", "period_index")
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, models.Model):
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
- unique_together = ("employee", "year")
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, models.Model):
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
- unique_together = ("employee", "period", "weekday")
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 = get_user_model().objects.get(id=requester_id)
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()
@@ -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 handler_path, name in settings.KPI_HANDLERS:
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
- get_user_model()
243
- .objects.filter(
244
- Q(groups__permissions__codename="administrate_review")
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
- get_user_model()
378
- .objects.filter(
379
- Q(groups__permissions__codename="administrate_review")
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 = get_user_model().objects.get(id=user_id)
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 = get_user_model().objects.get(id=user_id)
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.permissions.backend import UserBackend as BaseUserBackend
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", "lower_time", "upper_time", "timespan", "title", "total_hours", "calendar", "_calendar")
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, default="")
171
- primary_email = wb_serializers.CharField(default="", label=_("Primary Email"), allow_null=True, read_only=True)
172
- primary_address = wb_serializers.CharField(default="", label=_("Primary Address"), allow_null=True, read_only=True)
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()
@@ -0,0 +1,4 @@
1
+ from django.db.models.signals import ModelSignal
2
+
3
+ # this signal gathers all activity report needed to be inserted into the daily brief
4
+ add_employee_activity_to_daily_brief = ModelSignal(use_caching=True)