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,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,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
|
+
)
|