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.
Files changed (111) hide show
  1. wbhuman_resources/__init__.py +1 -0
  2. wbhuman_resources/admin/__init__.py +5 -0
  3. wbhuman_resources/admin/absence.py +113 -0
  4. wbhuman_resources/admin/calendars.py +37 -0
  5. wbhuman_resources/admin/employee.py +109 -0
  6. wbhuman_resources/admin/kpi.py +21 -0
  7. wbhuman_resources/admin/review.py +157 -0
  8. wbhuman_resources/apps.py +23 -0
  9. wbhuman_resources/dynamic_preferences_registry.py +119 -0
  10. wbhuman_resources/factories/__init__.py +38 -0
  11. wbhuman_resources/factories/absence.py +109 -0
  12. wbhuman_resources/factories/calendars.py +60 -0
  13. wbhuman_resources/factories/employee.py +80 -0
  14. wbhuman_resources/factories/kpi.py +155 -0
  15. wbhuman_resources/filters/__init__.py +20 -0
  16. wbhuman_resources/filters/absence.py +109 -0
  17. wbhuman_resources/filters/absence_graphs.py +85 -0
  18. wbhuman_resources/filters/calendars.py +28 -0
  19. wbhuman_resources/filters/employee.py +81 -0
  20. wbhuman_resources/filters/kpi.py +35 -0
  21. wbhuman_resources/filters/review.py +134 -0
  22. wbhuman_resources/filters/signals.py +27 -0
  23. wbhuman_resources/locale/de/LC_MESSAGES/django.mo +0 -0
  24. wbhuman_resources/locale/de/LC_MESSAGES/django.po +2207 -0
  25. wbhuman_resources/locale/de/LC_MESSAGES/django.po.translated +2456 -0
  26. wbhuman_resources/locale/en/LC_MESSAGES/django.mo +0 -0
  27. wbhuman_resources/locale/en/LC_MESSAGES/django.po +2091 -0
  28. wbhuman_resources/locale/fr/LC_MESSAGES/django.mo +0 -0
  29. wbhuman_resources/locale/fr/LC_MESSAGES/django.po +2093 -0
  30. wbhuman_resources/management/__init__.py +23 -0
  31. wbhuman_resources/migrations/0001_initial_squashed_squashed_0015_alter_absencerequest_calendaritem_ptr_and_more.py +949 -0
  32. wbhuman_resources/migrations/0016_alter_employeehumanresource_options.py +20 -0
  33. wbhuman_resources/migrations/0017_absencerequest_crossborder_country_and_more.py +55 -0
  34. wbhuman_resources/migrations/0018_remove_position_group_position_groups.py +32 -0
  35. wbhuman_resources/migrations/0019_alter_absencerequest_options_alter_kpi_options_and_more.py +44 -0
  36. wbhuman_resources/migrations/0020_alter_employeeyearbalance_year_alter_review_year.py +27 -0
  37. wbhuman_resources/migrations/0021_alter_position_color.py +18 -0
  38. wbhuman_resources/migrations/0022_remove_review_editable_mode.py +64 -0
  39. wbhuman_resources/migrations/__init__.py +0 -0
  40. wbhuman_resources/models/__init__.py +23 -0
  41. wbhuman_resources/models/absence.py +903 -0
  42. wbhuman_resources/models/calendars.py +370 -0
  43. wbhuman_resources/models/employee.py +1241 -0
  44. wbhuman_resources/models/kpi.py +199 -0
  45. wbhuman_resources/models/preferences.py +40 -0
  46. wbhuman_resources/models/review.py +982 -0
  47. wbhuman_resources/permissions/__init__.py +0 -0
  48. wbhuman_resources/permissions/backend.py +26 -0
  49. wbhuman_resources/serializers/__init__.py +49 -0
  50. wbhuman_resources/serializers/absence.py +308 -0
  51. wbhuman_resources/serializers/calendars.py +73 -0
  52. wbhuman_resources/serializers/employee.py +267 -0
  53. wbhuman_resources/serializers/kpi.py +80 -0
  54. wbhuman_resources/serializers/review.py +415 -0
  55. wbhuman_resources/signals.py +4 -0
  56. wbhuman_resources/tasks.py +195 -0
  57. wbhuman_resources/templates/review/review_report.html +322 -0
  58. wbhuman_resources/tests/__init__.py +1 -0
  59. wbhuman_resources/tests/conftest.py +96 -0
  60. wbhuman_resources/tests/models/__init__.py +0 -0
  61. wbhuman_resources/tests/models/test_absences.py +478 -0
  62. wbhuman_resources/tests/models/test_calendars.py +209 -0
  63. wbhuman_resources/tests/models/test_employees.py +502 -0
  64. wbhuman_resources/tests/models/test_review.py +103 -0
  65. wbhuman_resources/tests/models/test_utils.py +110 -0
  66. wbhuman_resources/tests/signals.py +108 -0
  67. wbhuman_resources/tests/test_permission.py +64 -0
  68. wbhuman_resources/tests/test_tasks.py +74 -0
  69. wbhuman_resources/urls.py +221 -0
  70. wbhuman_resources/utils.py +43 -0
  71. wbhuman_resources/viewsets/__init__.py +61 -0
  72. wbhuman_resources/viewsets/absence.py +312 -0
  73. wbhuman_resources/viewsets/absence_charts.py +328 -0
  74. wbhuman_resources/viewsets/buttons/__init__.py +7 -0
  75. wbhuman_resources/viewsets/buttons/absence.py +32 -0
  76. wbhuman_resources/viewsets/buttons/employee.py +44 -0
  77. wbhuman_resources/viewsets/buttons/kpis.py +16 -0
  78. wbhuman_resources/viewsets/buttons/review.py +195 -0
  79. wbhuman_resources/viewsets/calendars.py +103 -0
  80. wbhuman_resources/viewsets/display/__init__.py +39 -0
  81. wbhuman_resources/viewsets/display/absence.py +334 -0
  82. wbhuman_resources/viewsets/display/calendars.py +83 -0
  83. wbhuman_resources/viewsets/display/employee.py +254 -0
  84. wbhuman_resources/viewsets/display/kpis.py +92 -0
  85. wbhuman_resources/viewsets/display/review.py +429 -0
  86. wbhuman_resources/viewsets/employee.py +210 -0
  87. wbhuman_resources/viewsets/endpoints/__init__.py +42 -0
  88. wbhuman_resources/viewsets/endpoints/absence.py +57 -0
  89. wbhuman_resources/viewsets/endpoints/calendars.py +18 -0
  90. wbhuman_resources/viewsets/endpoints/employee.py +51 -0
  91. wbhuman_resources/viewsets/endpoints/kpis.py +53 -0
  92. wbhuman_resources/viewsets/endpoints/review.py +191 -0
  93. wbhuman_resources/viewsets/kpi.py +280 -0
  94. wbhuman_resources/viewsets/menu/__init__.py +22 -0
  95. wbhuman_resources/viewsets/menu/absence.py +50 -0
  96. wbhuman_resources/viewsets/menu/administration.py +15 -0
  97. wbhuman_resources/viewsets/menu/calendars.py +33 -0
  98. wbhuman_resources/viewsets/menu/employee.py +44 -0
  99. wbhuman_resources/viewsets/menu/kpis.py +18 -0
  100. wbhuman_resources/viewsets/menu/review.py +97 -0
  101. wbhuman_resources/viewsets/mixins.py +14 -0
  102. wbhuman_resources/viewsets/review.py +837 -0
  103. wbhuman_resources/viewsets/titles/__init__.py +18 -0
  104. wbhuman_resources/viewsets/titles/absence.py +30 -0
  105. wbhuman_resources/viewsets/titles/employee.py +18 -0
  106. wbhuman_resources/viewsets/titles/kpis.py +15 -0
  107. wbhuman_resources/viewsets/titles/review.py +62 -0
  108. wbhuman_resources/viewsets/utils.py +28 -0
  109. wbhuman_resources-1.58.4.dist-info/METADATA +8 -0
  110. wbhuman_resources-1.58.4.dist-info/RECORD +111 -0
  111. wbhuman_resources-1.58.4.dist-info/WHEEL +5 -0
@@ -0,0 +1,415 @@
1
+ from datetime import datetime
2
+
3
+ from django.db.models import Q
4
+ from django.utils.translation import gettext
5
+ from django.utils.translation import gettext_lazy as _
6
+ from rest_framework import serializers as rf_serializers
7
+ from rest_framework.reverse import reverse
8
+ from wbcore import serializers as wb_serializers
9
+ from wbcore.contrib.directory.models import Person
10
+ from wbcore.contrib.directory.serializers import PersonRepresentationSerializer
11
+
12
+ from wbhuman_resources.models import (
13
+ Review,
14
+ ReviewAnswer,
15
+ ReviewGroup,
16
+ ReviewQuestion,
17
+ ReviewQuestionCategory,
18
+ )
19
+
20
+
21
+ class ReviewGroupRepresentationSerializer(wb_serializers.RepresentationSerializer):
22
+ _detail = wb_serializers.HyperlinkField(reverse_name="wbhuman_resources:reviewgroup-detail")
23
+
24
+ class Meta:
25
+ model = ReviewGroup
26
+ fields = ("id", "name", "_detail")
27
+
28
+
29
+ class ReviewRepresentationSerializer(wb_serializers.RepresentationSerializer):
30
+ id_repr = wb_serializers.CharField(source="id", read_only=True, label=_("ID"))
31
+ _detail = wb_serializers.HyperlinkField(reverse_name="wbhuman_resources:review-detail")
32
+
33
+ class Meta:
34
+ model = Review
35
+ fields = ("id", "id_repr", "computed_str", "_detail")
36
+
37
+
38
+ class ReviewQuestionCategoryRepresentationSerializer(wb_serializers.RepresentationSerializer):
39
+ class Meta:
40
+ model = ReviewQuestionCategory
41
+ fields = (
42
+ "id",
43
+ "name",
44
+ )
45
+
46
+
47
+ class ReviewQuestionRepresentationSerializer(wb_serializers.RepresentationSerializer):
48
+ class Meta:
49
+ model = ReviewQuestion
50
+ fields = (
51
+ "id",
52
+ "computed_str",
53
+ "question",
54
+ )
55
+
56
+
57
+ class ReviewAnswerRepresentationSerializer(wb_serializers.RepresentationSerializer):
58
+ class Meta:
59
+ model = ReviewAnswer
60
+ fields = ("id",)
61
+
62
+
63
+ class ReviewGroupModelSerializer(wb_serializers.ModelSerializer):
64
+ @wb_serializers.register_resource()
65
+ def register_history_resource(self, instance, request, user):
66
+ resources = {
67
+ "review": reverse("wbhuman_resources:reviewgroup-review-list", args=[instance.id], request=request),
68
+ }
69
+
70
+ if Review.objects.filter(
71
+ Q(review_group=instance) & Q(status=Review.Status.PREPARATION_OF_REVIEW) & Q(moderator=user.profile)
72
+ ):
73
+ resources["submitreviews"] = reverse(
74
+ "wbhuman_resources:reviewgroup-submitreviews", args=[instance.id], request=request
75
+ )
76
+
77
+ return resources
78
+
79
+ _employees = PersonRepresentationSerializer(many=True, source="employees")
80
+
81
+ class Meta:
82
+ model = ReviewGroup
83
+ fields = ("id", "name", "employees", "_employees", "_additional_resources")
84
+
85
+
86
+ class ReviewModelSerializer(wb_serializers.ModelSerializer):
87
+ @wb_serializers.register_resource()
88
+ def register_history_resource(self, instance, request, user):
89
+ resources = {}
90
+ categories = instance.get_question_categories()
91
+ if instance.status == Review.Status.PREPARATION_OF_REVIEW:
92
+ resources["category"] = reverse("wbhuman_resources:reviewquestioncategory-list", args=[], request=request)
93
+ resources["questionnocategory"] = reverse(
94
+ "wbhuman_resources:review-reviewquestionnocategory-list", args=[instance.id], request=request
95
+ )
96
+ for category in categories:
97
+ resources[f"questioncategory{category.id}"] = reverse(
98
+ "wbhuman_resources:review-reviewquestioncategory-list",
99
+ args=[instance.id, category.id],
100
+ request=request,
101
+ )
102
+ else:
103
+ resources["reviewanswerquestionnocategory"] = reverse(
104
+ "wbhuman_resources:review-reviewanswerquestionnocategory-list", args=[instance.id], request=request
105
+ )
106
+ for category in categories:
107
+ resources[f"reviewanswerquestioncategory{category.id}"] = reverse(
108
+ "wbhuman_resources:review-reviewanswerquestioncategory-list",
109
+ args=[instance.id, category.id],
110
+ request=request,
111
+ )
112
+ resources["progress"] = reverse(
113
+ "wbhuman_resources:review-progress-list", args=[instance.id], request=request
114
+ )
115
+
116
+ if instance.is_template and instance.review_group and instance.moderator == user.profile:
117
+ resources["generate"] = reverse("wbhuman_resources:review-generate", args=[instance.id], request=request)
118
+
119
+ if instance.status == Review.Status.FILL_IN_REVIEW:
120
+ if instance.reviewee == user.profile and not instance.completely_filled_reviewee:
121
+ resources["completelyfilledreviewee"] = reverse(
122
+ "wbhuman_resources:review-completelyfilled-reviewee", args=[instance.id], request=request
123
+ )
124
+ if instance.reviewer == user.profile and not instance.completely_filled_reviewer:
125
+ resources["completelyfilledreviewer"] = reverse(
126
+ "wbhuman_resources:review-completelyfilled-reviewer", args=[instance.id], request=request
127
+ )
128
+
129
+ if instance.status in [Review.Status.EVALUATION, Review.Status.VALIDATION]:
130
+ resources["rating_review_answer_key"] = (
131
+ reverse("wbhuman_resources:review-reviewanswerpandasview-list", args=[instance.id], request=request)
132
+ + "?answer_type=RATING"
133
+ )
134
+ resources["text_review_answer_key"] = (
135
+ reverse("wbhuman_resources:review-reviewanswerpandasview-list", args=[instance.id], request=request)
136
+ + "?answer_type=TEXT"
137
+ )
138
+
139
+ if instance.status == Review.Status.EVALUATION:
140
+ if instance.reviewee == user.profile and not instance.signed_reviewee:
141
+ resources["signaturereviewee"] = reverse(
142
+ "wbhuman_resources:review-signature-reviewee", args=[instance.id], request=request
143
+ )
144
+ if instance.reviewer == user.profile and not instance.signed_reviewer:
145
+ resources["signaturereviewer"] = reverse(
146
+ "wbhuman_resources:review-signature-reviewer", args=[instance.id], request=request
147
+ )
148
+
149
+ if instance.status == Review.Status.VALIDATION and (
150
+ instance.moderator == user.profile or user.has_perm("wbhuman_resources.administrate_review")
151
+ ):
152
+ resources["generate_pdf"] = reverse(
153
+ "wbhuman_resources:review-generate-pdf", args=[instance.id], request=request
154
+ )
155
+
156
+ return resources
157
+
158
+ _reviewee = PersonRepresentationSerializer(source="reviewee")
159
+ _reviewer = PersonRepresentationSerializer(source="reviewer")
160
+ moderator = wb_serializers.PrimaryKeyRelatedField(
161
+ default=wb_serializers.CurrentUserDefault("profile"), queryset=Person.objects.all(), label=_("Moderator")
162
+ )
163
+ _moderator = PersonRepresentationSerializer(source="moderator")
164
+
165
+ _review_group = ReviewGroupRepresentationSerializer(source="review_group")
166
+
167
+ global_rating = wb_serializers.DecimalField(
168
+ read_only=True,
169
+ max_digits=14,
170
+ decimal_places=2,
171
+ help_text=_("Only the rating questions you answered are taken into account"),
172
+ )
173
+ year = wb_serializers.YearField(default=lambda: datetime.now().year)
174
+ is_template = wb_serializers.BooleanField(default=True, label=_("Is Template"))
175
+
176
+ reviewee = wb_serializers.PrimaryKeyRelatedField(
177
+ default=wb_serializers.DefaultAttributeFromObject(source="reviewee"),
178
+ read_only=lambda view: not view.is_modifiable,
179
+ queryset=Person.objects.all(),
180
+ )
181
+ reviewer = wb_serializers.PrimaryKeyRelatedField(
182
+ default=wb_serializers.DefaultAttributeFromObject(source="reviewer"),
183
+ read_only=lambda view: not view.is_modifiable,
184
+ queryset=Person.objects.all(),
185
+ )
186
+
187
+ class Meta:
188
+ model = Review
189
+ fields = (
190
+ "id",
191
+ "computed_str",
192
+ "from_date",
193
+ "to_date",
194
+ "review_deadline",
195
+ "review",
196
+ "auto_apply_deadline",
197
+ "status",
198
+ "reviewee",
199
+ "_reviewee",
200
+ "reviewer",
201
+ "_reviewer",
202
+ "moderator",
203
+ "_moderator",
204
+ "review_group",
205
+ "_review_group",
206
+ "is_template",
207
+ "feedback_reviewee",
208
+ "feedback_reviewer",
209
+ "_additional_resources",
210
+ "signed_reviewee",
211
+ "signed_reviewer",
212
+ "year",
213
+ "type",
214
+ "completely_filled_reviewee",
215
+ "completely_filled_reviewer",
216
+ "global_rating",
217
+ "changed",
218
+ )
219
+ read_only_fields = (
220
+ "signed_reviewee",
221
+ "signed_reviewer",
222
+ "completely_filled_reviewee",
223
+ "completely_filled_reviewer",
224
+ )
225
+
226
+ def validate(self, data):
227
+ obj = self.instance
228
+ errors = {}
229
+ reviewer = data.get("reviewer", obj.reviewer if obj else None)
230
+ reviewee = data.get("reviewee", obj.reviewee if obj else None)
231
+ if data.get("is_template", obj.is_template if obj else None):
232
+ dict_fields = {
233
+ "from_date": data.get("from_date", obj.from_date if obj else None),
234
+ "to_date": data.get("to_date", obj.to_date if obj else None),
235
+ "review_deadline": data.get("review_deadline", obj.review_deadline if obj else None),
236
+ "reviewee": data.get("reviewee", obj.reviewee if obj else None),
237
+ "reviewer": data.get("reviewer", obj.reviewer if obj else None),
238
+ }
239
+ for key, value in dict_fields.items():
240
+ if value:
241
+ errors[key] = [gettext("The field has to be empty if is_template is True")]
242
+ elif reviewer == reviewee is not None:
243
+ errors["reviewer"] = gettext("reviewer must be different from reviewee")
244
+ errors["reviewee"] = gettext("reviewer must be different from reviewee")
245
+
246
+ if len(errors.keys()) > 0:
247
+ raise rf_serializers.ValidationError(errors)
248
+
249
+ return super().validate(data)
250
+
251
+
252
+ class ReviewListModelSerializer(ReviewModelSerializer):
253
+ class Meta:
254
+ model = Review
255
+ fields = (
256
+ "id",
257
+ "from_date",
258
+ "to_date",
259
+ "review_deadline",
260
+ "review",
261
+ "auto_apply_deadline",
262
+ "status",
263
+ "reviewee",
264
+ "_reviewee",
265
+ "reviewer",
266
+ "_reviewer",
267
+ "moderator",
268
+ "_moderator",
269
+ "review_group",
270
+ "_review_group",
271
+ "is_template",
272
+ "year",
273
+ "type",
274
+ "changed",
275
+ "_additional_resources",
276
+ )
277
+
278
+
279
+ class ReviewReadOnlyModelSerializer(ReviewModelSerializer):
280
+ @wb_serializers.register_resource()
281
+ def register_history_resource(self, instance, request, user):
282
+ return {}
283
+
284
+
285
+ class ReviewQuestionCategoryModelSerializer(wb_serializers.ModelSerializer):
286
+ @wb_serializers.register_resource()
287
+ def register_history_resource(self, instance, request, user):
288
+ resources = {
289
+ "reviewquestion": reverse(
290
+ "wbhuman_resources:reviewquestioncategory-reviewquestion-list", args=[instance.id], request=request
291
+ ),
292
+ }
293
+ return resources
294
+
295
+ class Meta:
296
+ model = ReviewQuestionCategory
297
+ fields = ("id", "name", "order", "weight", "_additional_resources")
298
+
299
+
300
+ class ReviewQuestionModelSerializer(wb_serializers.ModelSerializer):
301
+ _review = ReviewRepresentationSerializer(source="review")
302
+ category = wb_serializers.PrimaryKeyRelatedField(
303
+ default=wb_serializers.DefaultFromKwargs("category_id"), queryset=ReviewQuestionCategory.objects.all()
304
+ )
305
+ _category = ReviewQuestionCategoryRepresentationSerializer(source="category")
306
+
307
+ review_for = wb_serializers.CharField(label=_("Review For"), read_only=True)
308
+ question = wb_serializers.TextAreaField(label=_("Question"), allow_blank=True)
309
+
310
+ @wb_serializers.register_resource()
311
+ def register_resource(self, instance, request, user):
312
+ resources = {
313
+ "review_answers_table": reverse("wbhuman_resources:reviewanswer-list", args=[], request=request)
314
+ + f"?question={instance.id}",
315
+ }
316
+ return resources
317
+
318
+ class Meta:
319
+ model = ReviewQuestion
320
+ fields = (
321
+ "id",
322
+ "review",
323
+ "_review",
324
+ "category",
325
+ "_category",
326
+ "question",
327
+ "computed_str",
328
+ "mandatory",
329
+ "answer_type",
330
+ "for_reviewee",
331
+ "for_reviewer",
332
+ "for_department_peers",
333
+ "for_company_peers",
334
+ "order",
335
+ "weight",
336
+ "review_for",
337
+ "_additional_resources",
338
+ )
339
+
340
+ def validate(self, data):
341
+ obj = self.instance
342
+ for_department_peers = data.get("for_department_peers", obj.for_department_peers if obj else None)
343
+ for_company_peers = data.get("for_company_peers", obj.for_company_peers if obj else None)
344
+ errors = {}
345
+ if not obj:
346
+ qs = ReviewQuestion.objects.filter(
347
+ Q(review=data.get("review"))
348
+ & Q(category=data.get("category"))
349
+ & ~Q(answer_type=data.get("answer_type"))
350
+ )
351
+ if qs.exists():
352
+ errors["answer_type"] = gettext("Questions in a section must be of the same type")
353
+
354
+ if for_department_peers and for_company_peers:
355
+ errors["non_field_errors"] = [
356
+ gettext(
357
+ "Both 'For department peers' and 'For company peers' true does not make any sense. Only 1 can be active"
358
+ )
359
+ ]
360
+
361
+ if len(errors.keys()) > 0:
362
+ raise rf_serializers.ValidationError(errors)
363
+
364
+ return super().validate(data)
365
+
366
+
367
+ class ReviewAnswerModelSerializer(wb_serializers.ModelSerializer):
368
+ _question = ReviewQuestionRepresentationSerializer(source="question")
369
+ _answered_by = PersonRepresentationSerializer(source="answered_by")
370
+ answer_number = wb_serializers.EmojiRatingField(label=_("Rating"))
371
+ answer_text = wb_serializers.TextAreaField(label=_("Comment"), allow_blank=True, allow_null=True)
372
+ weight = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=1)
373
+ mandatory = wb_serializers.BooleanField(
374
+ label=_("Mandatory"),
375
+ read_only=True,
376
+ )
377
+
378
+ question_name = wb_serializers.TextAreaField(read_only=True, label=_("Question"))
379
+
380
+ class Meta:
381
+ model = ReviewAnswer
382
+ fields = (
383
+ "id",
384
+ "question",
385
+ "_question",
386
+ "answered_by",
387
+ "_answered_by",
388
+ "answered_anonymized",
389
+ "answer_number",
390
+ "answer_text",
391
+ "weight",
392
+ "question_name",
393
+ "mandatory",
394
+ )
395
+ read_only_fields = ("question", "answered_by", "answered_anonymized")
396
+
397
+ def validate(self, data):
398
+ errors = {}
399
+ if obj := self.instance:
400
+ if obj.question.mandatory:
401
+ answer_number = data.get("answer_number", obj.answer_number if obj else None)
402
+ answer_text = data.get("answer_text", obj.answer_text if obj else None)
403
+ if not answer_text and obj.question.answer_type == ReviewQuestion.ANSWERTYPE.TEXT:
404
+ errors["answer_text"] = gettext(
405
+ "Comment cannot be empty, a response to this question is mandatory"
406
+ )
407
+ if not answer_number and obj.question.answer_type == ReviewQuestion.ANSWERTYPE.RATING:
408
+ errors["answer_number"] = gettext(
409
+ "Rating cannot be empty, a response to this question is mandatory"
410
+ )
411
+
412
+ if len(errors.keys()) > 0:
413
+ raise rf_serializers.ValidationError(errors)
414
+
415
+ return super().validate(data)
@@ -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)
@@ -0,0 +1,195 @@
1
+ from __future__ import absolute_import, unicode_literals
2
+
3
+ from datetime import date, datetime, timedelta
4
+ from io import BytesIO
5
+
6
+ import pandas as pd
7
+ from celery import shared_task
8
+ from django.core.mail import EmailMultiAlternatives
9
+ from django.db.models import Q
10
+ from django.template.loader import get_template
11
+ from django.utils.translation import gettext
12
+ from django.utils.translation import gettext_lazy as _
13
+ from dynamic_preferences.registries import global_preferences_registry
14
+ from wbcore.contrib.directory.models import Person
15
+ from wbcore.contrib.notifications.dispatch import send_notification
16
+ from wbcore.utils.date import current_month_date_end
17
+ from wbcore.utils.html import convert_html2text
18
+
19
+ from wbhuman_resources.models import KPI, DayOffCalendar, EmployeeHumanResource, Review
20
+
21
+ from .models.preferences import get_previous_year_balance_expiration_date
22
+ from .signals import add_employee_activity_to_daily_brief
23
+
24
+
25
+ @shared_task
26
+ def create_future_public_holiday(today: date | None = None, forecast_year: int = 5):
27
+ if not today:
28
+ today = date.today()
29
+ for calendar in DayOffCalendar.objects.all():
30
+ for year in range(today.year, today.year + forecast_year):
31
+ calendar.create_public_holidays(year)
32
+
33
+
34
+ @shared_task
35
+ def assign_balance(today=None):
36
+ """
37
+ Yearly periodic cron tasks that increase for an employee
38
+ """
39
+ if not today:
40
+ today = datetime.now().date()
41
+
42
+ for employee in EmployeeHumanResource.active_internal_employees.all():
43
+ [start_period, end_period] = EmployeeHumanResource.ExtraDaysBalanceFrequency[
44
+ employee.extra_days_frequency
45
+ ].get_date_range(today)
46
+ employee.assign_vacation_allowance_from_range(start_period.date(), end_period.date())
47
+
48
+
49
+ @shared_task
50
+ def check_and_warn_user_with_previous_year_available_balance(year=None):
51
+ """
52
+ When this task run, it will send a reminder Notification to user with still available balance for the previous year
53
+ """
54
+ if not year:
55
+ year = date.today().year
56
+
57
+ for employee in EmployeeHumanResource.active_internal_employees.all():
58
+ previous_year_balance = employee.get_or_create_balance(year - 1)[0]
59
+ if (previous_year_remaining_days := previous_year_balance.total_vacation_hourly_balance) > 0:
60
+ send_notification(
61
+ code="wbhuman_resources.employeehumanresource.vacation",
62
+ title=_("You still have {} hours of vacation to take from {}").format(
63
+ previous_year_remaining_days, year - 1
64
+ ),
65
+ body=_(
66
+ "Please take vacation from now to {:%d.%m.%Y} (excluded). Your balance will not be available after that date"
67
+ ).format(get_previous_year_balance_expiration_date(year=year)),
68
+ user=employee.profile.user_account,
69
+ )
70
+
71
+
72
+ @shared_task
73
+ def send_mail_to_accounting():
74
+ global_preferences = global_preferences_registry.manager()
75
+ accounting_company_emails = list(
76
+ filter(None, global_preferences["wbhuman_resources__accounting_company_emails"].split(";"))
77
+ )
78
+ cc_emails = list(EmployeeHumanResource.get_administrators().values_list("email", flat=True))
79
+
80
+ end_of_month = current_month_date_end()
81
+
82
+ output = BytesIO()
83
+ EmployeeHumanResource.get_end_of_month_employee_balance_report_df(
84
+ EmployeeHumanResource.active_internal_employees.all(), end_of_month
85
+ ).to_excel(output, engine="xlsxwriter", index=False)
86
+
87
+ html = get_template("notifications/email_template.html")
88
+
89
+ notification = {
90
+ "message": gettext(
91
+ "Please find the vacation days balance valid to the end of the month as attached excel file"
92
+ ),
93
+ "title": gettext("Vacation balance summary at {:%d.%m.%Y}").format(end_of_month),
94
+ }
95
+ html_content = html.render({"notification": notification})
96
+ mail_text = convert_html2text(html_content)
97
+ msg = EmailMultiAlternatives(
98
+ gettext("Vacation Report {:%d.%m.%Y}").format(end_of_month),
99
+ body=mail_text,
100
+ from_email=global_preferences["wbhuman_resources__default_from_email_address"],
101
+ to=accounting_company_emails,
102
+ cc=cc_emails,
103
+ )
104
+ msg.attach_alternative(html_content, "text/html")
105
+ output.seek(0)
106
+ msg.attach(
107
+ "vacation_report_{:%m-%d-%Y}.xlsx".format(end_of_month),
108
+ output.read(),
109
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
110
+ )
111
+ msg.send()
112
+
113
+
114
+ @shared_task
115
+ def daily_automatic_application_deadline():
116
+ for review in Review.objects.filter(
117
+ Q(status=Review.Status.FILL_IN_REVIEW) & Q(review_deadline__lte=datetime.now().date())
118
+ ):
119
+ review.save()
120
+
121
+ week = datetime.now().date() - timedelta(days=7)
122
+ for review in Review.objects.filter(Q(status=Review.Status.FILL_IN_REVIEW) & Q(review_deadline=week)):
123
+ persons = [review.reviewee, review.reviewer]
124
+ for person in persons:
125
+ if hasattr(person, "user_account"):
126
+ msg = gettext("Dear {} {}, <p>you only have one more week to complete the review.</p>").format(
127
+ person.first_name, person.last_name
128
+ )
129
+ review.send_review_notification(
130
+ title=gettext("Stage 2: Fill in review - {}").format(str(review)),
131
+ message=msg,
132
+ recipient=person.user_account,
133
+ )
134
+
135
+ # Daily notification
136
+ for review in Review.objects.filter(
137
+ Q(status=Review.Status.FILL_IN_REVIEW) & Q(review_deadline=datetime.now().date() - timedelta(days=1))
138
+ ):
139
+ persons = [review.reviewee, review.reviewer]
140
+ for person in persons:
141
+ if hasattr(person, "user_account"):
142
+ msg = gettext("Dear {} {}, <p>you only have one more day to complete the review.</p>").format(
143
+ person.first_name, person.last_name
144
+ )
145
+
146
+ review.send_review_notification(
147
+ title=gettext("Stage 2: Fill in review - {}").format(str(review)),
148
+ message=msg,
149
+ recipient=person.user_account,
150
+ )
151
+
152
+
153
+ @shared_task
154
+ def periodic_updating_kpi_task(kpi_id: list | None = None, start=None, end=None):
155
+ intervals = {elt.name: KPI.Interval.get_frequence_correspondance(elt.name) for elt in KPI.Interval}
156
+ for key, value in intervals.items():
157
+ kpis = (
158
+ KPI.objects.filter(id__in=kpi_id, evaluated_intervals=key)
159
+ if kpi_id
160
+ else KPI.objects.filter(evaluated_intervals=key, is_active=True)
161
+ )
162
+ for kpi in kpis:
163
+ if not start:
164
+ start = kpi.period.lower
165
+ if not end:
166
+ end = kpi.period.upper
167
+ for date_evaluation in pd.date_range(start=start, end=end, freq=value):
168
+ kpi.generate_evaluation(date_evaluation.date())
169
+
170
+
171
+ @shared_task
172
+ def daily_brief(today: date | None = None, **kwargs):
173
+ """Creates a summary of the daily brief for all internal employees
174
+ Args:
175
+ today (date | None, optional): Date of today. Defaults to None.
176
+ """
177
+
178
+ if not today:
179
+ today = date.today()
180
+ for employee in EmployeeHumanResource.active_internal_employees.filter(profile__user_account__isnull=False):
181
+ daily_brief = ""
182
+ for receiver, res in add_employee_activity_to_daily_brief.send( # noqa: B007
183
+ sender=Person, instance=employee.profile, val_date=today, **kwargs
184
+ ):
185
+ if res:
186
+ title, html = res
187
+ daily_brief += f"<h2 text-align: center;>{title}</h2>\n<div style='margin-bottom: 1.5em; text-align: left;'>{html}</div>\n"
188
+
189
+ if daily_brief:
190
+ send_notification(
191
+ code="wbcrm.activity.daily_brief",
192
+ title=_("Your Daily Brief"),
193
+ body=daily_brief,
194
+ user=employee.profile.user_account,
195
+ )