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,199 @@
1
+ import importlib
2
+ from datetime import date
3
+ from typing import Iterator, Protocol, Type
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from django.conf import settings
8
+ from django.contrib.postgres.fields import DateRangeField
9
+ from django.db import models
10
+ from django.db.models.query import QuerySet
11
+ from django.utils.translation import gettext_lazy as _
12
+ from wbcore.models import WBModel
13
+ from wbcore.serializers.serializers import ModelSerializer
14
+
15
+
16
+ class KPIHandler(Protocol):
17
+ """
18
+ This is a protocol to ensure that two methods are present in the implemented handlers.
19
+ A handler makes sure that we have a valid serializer and a valid evaluate method to customize
20
+ KPIs.
21
+ """
22
+
23
+ def get_name(self) -> str: ...
24
+
25
+ def get_serializer(self) -> Type[ModelSerializer]: ...
26
+
27
+ def annotate_parameters(self, queryset: QuerySet["KPI"]) -> QuerySet["KPI"]: ...
28
+
29
+ def evaluate(self, kpi: "KPI") -> int: ...
30
+
31
+
32
+ def default_additional_data() -> dict[str, dict | list]:
33
+ return {
34
+ "serializer_data": {},
35
+ "list_data": [],
36
+ }
37
+
38
+
39
+ class KPI(WBModel):
40
+ """
41
+ Key Performance Indicator. Stores 1 KPI for either a person or a group of people. It defines a goal and a method to evaluate this goal.
42
+ Additional parameters are set through a custom serializer which stores its result in a JSON field. The visualizion of it is done by
43
+ passing the json field in the custom serializer. The addition parameters in the json field need to have to keys, one for the raw data that
44
+ is passed into the form serializer and one for a list to show the parameters in human readable form in a list field.
45
+ """
46
+
47
+ class Interval(models.TextChoices):
48
+ DAILY = "DAILY", _("Daily")
49
+ WEEKLY = "WEEKLY", _("Weekly")
50
+ MONTHLY = "MONTHLY", _("Monthly")
51
+ QUARTERLY = "QUARTERLY", _("Quarterly")
52
+
53
+ @classmethod
54
+ def get_frequence_correspondance(cls, name):
55
+ _map = {
56
+ "DAILY": "D",
57
+ "WEEKLY": "W",
58
+ "MONTHLY": "ME",
59
+ "QUARTERLY": "3M",
60
+ }
61
+
62
+ return _map[name]
63
+
64
+ name = models.CharField(max_length=255)
65
+ goal = models.PositiveIntegerField(verbose_name=_("Goal"))
66
+ period = DateRangeField(verbose_name=_("Period"))
67
+
68
+ evaluated_persons = models.ManyToManyField(
69
+ to="directory.Person", related_name="wbhuman_resources_kpis", verbose_name=_("Evaluated Persons")
70
+ )
71
+ evaluated_intervals = models.CharField(
72
+ max_length=16, choices=Interval.choices, default=Interval.MONTHLY, verbose_name=_("Evaluated Intervals")
73
+ )
74
+
75
+ handler = models.CharField(max_length=255)
76
+ additional_data = models.JSONField(default=default_additional_data)
77
+ last_update = models.DateTimeField(
78
+ auto_now=True, verbose_name=_("Last update"), help_text=_("Date of latest change (automatically computed)")
79
+ )
80
+ individual_evaluation = models.BooleanField(default=True, verbose_name=_("Individual Evaluation"))
81
+ is_active = models.BooleanField(default=True, verbose_name=_("Is Active"))
82
+
83
+ @staticmethod
84
+ def get_all_handlers() -> Iterator[KPIHandler]:
85
+ for param in settings.KPI_HANDLERS:
86
+ handler_path = param[0]
87
+ *module, handler = handler_path.split(".")
88
+ module_path = ".".join(module)
89
+ yield getattr(importlib.import_module(module_path), handler)()
90
+
91
+ @staticmethod
92
+ def get_all_handler_choices() -> Iterator[tuple[str, str]]:
93
+ yield from getattr(settings, "KPI_HANDLERS", [])
94
+
95
+ def get_handler(self) -> KPIHandler:
96
+ *module, handler = self.handler.split(".")
97
+ module_path = ".".join(module)
98
+ return getattr(importlib.import_module(module_path), handler)()
99
+
100
+ @classmethod
101
+ def is_administrator(cls, user):
102
+ user_groups = user.groups
103
+ user_permission = user.user_permissions
104
+ return (
105
+ user_groups.filter(permissions__codename="administrate_kpi").exists()
106
+ or user_permission.filter(codename="administrate_kpi").exists()
107
+ or user.is_superuser
108
+ )
109
+
110
+ def generate_evaluation(self, evaluation_date: date):
111
+ if self.individual_evaluation:
112
+ persons = self.evaluated_persons.all()
113
+ else:
114
+ persons = [None]
115
+
116
+ for person in persons:
117
+ if evaluation_date <= date.today() and evaluation_date > self.period.lower:
118
+ Evaluation.objects.update_or_create(
119
+ evaluation_date=evaluation_date,
120
+ person=person,
121
+ kpi=self,
122
+ defaults={
123
+ "evaluated_period": [self.period.lower, evaluation_date],
124
+ "evaluated_score": self.get_handler().evaluate(
125
+ self, evaluated_person=person, evaluation_date=evaluation_date
126
+ ),
127
+ },
128
+ )
129
+
130
+ class Meta:
131
+ verbose_name = _("Key Performance Indicator")
132
+ verbose_name_plural = _("Key Performance Indicators")
133
+ permissions = [("administrate_kpi", "Can Administrate KPI")]
134
+
135
+ def __str__(self):
136
+ return f"{self.name}"
137
+
138
+ @classmethod
139
+ def get_representation_endpoint(cls):
140
+ return "wbhuman_resources:kpi-list"
141
+
142
+ @classmethod
143
+ def get_representation_value_key(cls):
144
+ return "id"
145
+
146
+ @classmethod
147
+ def get_representation_label_key(cls):
148
+ return "{{name}}"
149
+
150
+ @classmethod
151
+ def get_endpoint_basename(cls):
152
+ return "wbhuman_resources:kpi"
153
+
154
+
155
+ class Evaluation(models.Model):
156
+ """
157
+ Stores the evaluated result of a KPI on the evaluation day for the previous period for 1 person.
158
+ """
159
+
160
+ kpi = models.ForeignKey(
161
+ to="wbhuman_resources.KPI", related_name="evaluations", on_delete=models.CASCADE, verbose_name=_("KPI")
162
+ )
163
+ person = models.ForeignKey(
164
+ to="directory.Person",
165
+ related_name="wbhuman_resources_evaluations",
166
+ on_delete=models.CASCADE,
167
+ blank=True,
168
+ null=True,
169
+ )
170
+ evaluated_score = models.IntegerField(null=True, blank=True)
171
+ evaluated_period = DateRangeField()
172
+ evaluation_date = models.DateField()
173
+ last_update = models.DateTimeField(
174
+ auto_now=True, verbose_name=_("Last update"), help_text=_("Date of latest change (automatically computed)")
175
+ )
176
+
177
+ class Meta:
178
+ verbose_name = _("Evaluation")
179
+ verbose_name_plural = _("Evaluations")
180
+
181
+ def __str__(self) -> str:
182
+ return f"{self.kpi} - {self.evaluation_date}"
183
+
184
+ def get_rating(self):
185
+ list_date = list(
186
+ pd.date_range(
187
+ start=self.kpi.period.lower,
188
+ end=self.kpi.period.upper,
189
+ freq=KPI.Interval.get_frequence_correspondance(self.kpi.evaluated_intervals),
190
+ )
191
+ )
192
+ list_date = [_date.date() for _date in list_date]
193
+ index = list_date.index(self.evaluation_date)
194
+ nb_intervals = len(list_date)
195
+ goals_expected = (self.kpi.goal * np.ones(nb_intervals) / nb_intervals).cumsum().round(2)
196
+ goal_expected = goals_expected[index]
197
+ rating = (self.evaluated_score / goal_expected * 100).round(2)
198
+ rating = round(rating / 100 * 4)
199
+ return 1 if rating < 1 else rating if rating < 4 else 4
@@ -0,0 +1,40 @@
1
+ from datetime import date
2
+
3
+ from django.db.utils import ProgrammingError
4
+ from dynamic_preferences.registries import global_preferences_registry
5
+ from pandas.tseries.offsets import MonthBegin
6
+
7
+ from wbhuman_resources.dynamic_preferences_registry import (
8
+ NumberOfMonthsBeforeBalanceExpiration,
9
+ )
10
+
11
+
12
+ def default_vacation_days_per_year():
13
+ return global_preferences_registry.manager()["wbhuman_resources__default_vacation_days"]
14
+
15
+
16
+ def long_vacation_number_of_days():
17
+ return global_preferences_registry.manager()["wbhuman_resources__long_vacation_number_of_days"]
18
+
19
+
20
+ def get_main_company():
21
+ return global_preferences_registry.manager()["directory__main_company"]
22
+
23
+
24
+ def get_previous_year_balance_expiration_date(year: int) -> date:
25
+ next_year_date = date(year + 1, 1, 1)
26
+ try:
27
+ number_of_month_before_balance_expiration = global_preferences_registry.manager()[
28
+ "wbhuman_resources__number_of_month_before_balance_expiration"
29
+ ]
30
+ except (RuntimeError, ProgrammingError):
31
+ number_of_month_before_balance_expiration = NumberOfMonthsBeforeBalanceExpiration.default
32
+
33
+ return (next_year_date + MonthBegin(number_of_month_before_balance_expiration)).date()
34
+
35
+
36
+ def get_is_external_considered_as_internal():
37
+ try:
38
+ return global_preferences_registry.manager()["wbhuman_resources__is_external_considered_as_internal"]
39
+ except (RuntimeError, ProgrammingError):
40
+ return False