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,110 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
from faker import Faker
|
|
6
|
+
|
|
7
|
+
from wbhuman_resources.models import EmployeeHumanResource
|
|
8
|
+
|
|
9
|
+
fake = Faker()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.django_db
|
|
13
|
+
class TestUtil:
|
|
14
|
+
@pytest.mark.parametrize("test_date", [(fake.date_object())])
|
|
15
|
+
def test_get_employee_absence_periods_df(
|
|
16
|
+
self,
|
|
17
|
+
employee_human_resource,
|
|
18
|
+
absence_request_periods_factory,
|
|
19
|
+
day_off_factory,
|
|
20
|
+
employee_weekly_off_periods_factory,
|
|
21
|
+
test_date,
|
|
22
|
+
):
|
|
23
|
+
# Basic utility coverage test. The underlying functions are already tested.
|
|
24
|
+
p = absence_request_periods_factory.create(date=test_date, employee=employee_human_resource)
|
|
25
|
+
w = employee_weekly_off_periods_factory.create(
|
|
26
|
+
employee=employee_human_resource, weekday=(test_date + timedelta(days=1)).weekday()
|
|
27
|
+
)
|
|
28
|
+
day_off_factory.create(calendar=employee_human_resource.calendar, date=test_date + timedelta(days=2))
|
|
29
|
+
res = EmployeeHumanResource.get_employee_absence_periods_df(
|
|
30
|
+
employee_human_resource.calendar,
|
|
31
|
+
(test_date - pd.tseries.offsets.Week(1)).date(),
|
|
32
|
+
(test_date + pd.tseries.offsets.Week(1)).date(),
|
|
33
|
+
EmployeeHumanResource.active_employees.all(),
|
|
34
|
+
).set_index(["employee", "period", "date", "type"])
|
|
35
|
+
assert not res.loc[(employee_human_resource.id, p.default_period.id, test_date, "Vacation"), :].empty
|
|
36
|
+
assert not res.loc[
|
|
37
|
+
(employee_human_resource.id, w.period.id, test_date + timedelta(days=1), "Day Off"), :
|
|
38
|
+
].empty
|
|
39
|
+
assert not res.loc[
|
|
40
|
+
(
|
|
41
|
+
employee_human_resource.id,
|
|
42
|
+
employee_human_resource.calendar.default_periods.earliest("lower_time").id,
|
|
43
|
+
test_date + timedelta(days=2),
|
|
44
|
+
"Holiday",
|
|
45
|
+
),
|
|
46
|
+
:,
|
|
47
|
+
].empty
|
|
48
|
+
assert not res.loc[
|
|
49
|
+
(
|
|
50
|
+
employee_human_resource.id,
|
|
51
|
+
employee_human_resource.calendar.default_periods.latest("lower_time").id,
|
|
52
|
+
test_date + timedelta(days=2),
|
|
53
|
+
"Holiday",
|
|
54
|
+
),
|
|
55
|
+
:,
|
|
56
|
+
].empty
|
|
57
|
+
with pytest.raises(KeyError):
|
|
58
|
+
assert not res.loc[(employee_human_resource.id, 2, test_date + timedelta(days=3), "Holiday"), :].empty
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize(
|
|
61
|
+
"end_of_month_date",
|
|
62
|
+
[
|
|
63
|
+
(fake.date_between(date(2021, 1, 1), date(2021, 3, 31))),
|
|
64
|
+
(fake.date_between(date(2021, 4, 1), date(2021, 12, 31))),
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
def test_get_end_of_month_employee_balance_report_df(
|
|
68
|
+
self,
|
|
69
|
+
employee_human_resource,
|
|
70
|
+
employee_year_balance_factory,
|
|
71
|
+
balance_hourly_allowance_factory,
|
|
72
|
+
absence_request_periods_factory,
|
|
73
|
+
end_of_month_date,
|
|
74
|
+
):
|
|
75
|
+
end_of_month_date = (end_of_month_date + pd.tseries.offsets.MonthEnd(0)).date()
|
|
76
|
+
previous_balance = employee_year_balance_factory.create(
|
|
77
|
+
employee=employee_human_resource, year=end_of_month_date.year - 1, extra_balance=0
|
|
78
|
+
)
|
|
79
|
+
balance_hourly_allowance_factory.create(balance=previous_balance)
|
|
80
|
+
|
|
81
|
+
current_balance = employee_year_balance_factory.create(
|
|
82
|
+
employee=employee_human_resource, year=end_of_month_date.year, extra_balance=0
|
|
83
|
+
)
|
|
84
|
+
balance_hourly_allowance_factory.create(balance=current_balance)
|
|
85
|
+
previous_period = absence_request_periods_factory.create(
|
|
86
|
+
balance=previous_balance,
|
|
87
|
+
employee=employee_human_resource,
|
|
88
|
+
date=fake.date_between(date(end_of_month_date.year, 1, 1), end_of_month_date - timedelta(days=1)),
|
|
89
|
+
)
|
|
90
|
+
current_period = absence_request_periods_factory.create(
|
|
91
|
+
balance=current_balance,
|
|
92
|
+
employee=employee_human_resource,
|
|
93
|
+
date=fake.date_between(previous_period.date, end_of_month_date),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
res = EmployeeHumanResource.get_end_of_month_employee_balance_report_df(
|
|
97
|
+
EmployeeHumanResource.active_employees.all(), end_of_month_date
|
|
98
|
+
).to_dict("records")[0]
|
|
99
|
+
expected_total_balance = (
|
|
100
|
+
current_balance.balance_in_days
|
|
101
|
+
+ previous_balance.total_vacation_hourly_balance_in_days
|
|
102
|
+
+ previous_balance.total_vacation_hourly_usage_in_days
|
|
103
|
+
)
|
|
104
|
+
expected_usage = (previous_period.total_hours + current_period.total_hours) / 8
|
|
105
|
+
assert res == {
|
|
106
|
+
"Employee": employee_human_resource.computed_str,
|
|
107
|
+
"Total Balance": expected_total_balance,
|
|
108
|
+
"End of Month Usage": expected_usage,
|
|
109
|
+
"Available Balance End of Month": expected_total_balance - expected_usage,
|
|
110
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from django.db import connection
|
|
2
|
+
from django.dispatch import receiver
|
|
3
|
+
from wbcore.test.signals import (
|
|
4
|
+
custom_update_kwargs,
|
|
5
|
+
get_custom_factory,
|
|
6
|
+
get_custom_serializer,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from wbhuman_resources.factories import (
|
|
10
|
+
DefaultPersonKPIFactory,
|
|
11
|
+
ReviewQuestionNoCategoryFactory,
|
|
12
|
+
ReviewTemplateFactory,
|
|
13
|
+
)
|
|
14
|
+
from wbhuman_resources.models import AbsenceRequest, ReviewQuestion
|
|
15
|
+
from wbhuman_resources.serializers import KPIModelSerializer
|
|
16
|
+
from wbhuman_resources.viewsets import (
|
|
17
|
+
AbsenceTablePandasViewSet,
|
|
18
|
+
AbsenceTypeCountEmployeeModelViewSet,
|
|
19
|
+
KPIModelViewSet,
|
|
20
|
+
ReviewAnswerReviewNoCategoryModelViewSet,
|
|
21
|
+
ReviewAnswerReviewPandasViewSet,
|
|
22
|
+
ReviewAnswerReviewQuestionCategoryModelViewSet,
|
|
23
|
+
ReviewModelViewSet,
|
|
24
|
+
ReviewProgressReviewPandasViewSet,
|
|
25
|
+
ReviewQuestionReviewNoCategoryModelViewSet,
|
|
26
|
+
ReviewReviewGroupModelViewSet,
|
|
27
|
+
ReviewTemplateModelViewSet,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# =================================================================================================================
|
|
31
|
+
# CUSTOM FACTORY
|
|
32
|
+
# =================================================================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@receiver(get_custom_factory, sender=ReviewTemplateModelViewSet)
|
|
36
|
+
def receive_factory_review_template(sender, *args, **kwargs):
|
|
37
|
+
return ReviewTemplateFactory
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@receiver(get_custom_factory, sender=ReviewQuestionReviewNoCategoryModelViewSet)
|
|
41
|
+
def receive_factory_review_question_no_category(sender, *args, **kwargs):
|
|
42
|
+
return ReviewQuestionNoCategoryFactory
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@receiver(get_custom_factory, sender=KPIModelViewSet)
|
|
46
|
+
def receive_factory_kpi(sender, *args, **kwargs):
|
|
47
|
+
return DefaultPersonKPIFactory
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =================================================================================================================
|
|
51
|
+
# UPDATE KWARGS
|
|
52
|
+
# =================================================================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@receiver(custom_update_kwargs, sender=AbsenceTypeCountEmployeeModelViewSet)
|
|
56
|
+
def receive_kwargs_employee_absence(sender, *args, **kwargs):
|
|
57
|
+
if kwargs.get("obj_factory"):
|
|
58
|
+
abs_day = kwargs.get("obj_factory")
|
|
59
|
+
abs_day.request.status = AbsenceRequest.Status.APPROVED
|
|
60
|
+
abs_day.request.save()
|
|
61
|
+
abs_day.save()
|
|
62
|
+
return {"employee_id": abs_day.request.employee.id, "request_id": abs_day.request.id}
|
|
63
|
+
else:
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@receiver(custom_update_kwargs, sender=ReviewTemplateModelViewSet)
|
|
68
|
+
@receiver(custom_update_kwargs, sender=ReviewReviewGroupModelViewSet)
|
|
69
|
+
@receiver(custom_update_kwargs, sender=ReviewModelViewSet)
|
|
70
|
+
def receive_kwargs_review(sender, *args, **kwargs):
|
|
71
|
+
if (review := kwargs.get("obj_factory")) and (user := kwargs.get("user")):
|
|
72
|
+
review.moderator = user.profile
|
|
73
|
+
review.save()
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@receiver(custom_update_kwargs, sender=ReviewProgressReviewPandasViewSet)
|
|
78
|
+
@receiver(custom_update_kwargs, sender=ReviewAnswerReviewPandasViewSet)
|
|
79
|
+
@receiver(custom_update_kwargs, sender=ReviewAnswerReviewNoCategoryModelViewSet)
|
|
80
|
+
@receiver(custom_update_kwargs, sender=ReviewAnswerReviewQuestionCategoryModelViewSet)
|
|
81
|
+
def receive_kwargs_review_answer_question_category(sender, *args, **kwargs):
|
|
82
|
+
if question_id := kwargs.get("question_id"):
|
|
83
|
+
question = ReviewQuestion.objects.get(id=question_id)
|
|
84
|
+
if question.category:
|
|
85
|
+
return {"review_id": question.review.id, "category_id": question.category.id}
|
|
86
|
+
else:
|
|
87
|
+
return {"review_id": question.review.id}
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@receiver(custom_update_kwargs, sender=AbsenceTablePandasViewSet)
|
|
92
|
+
def receive_kwargs_employee_human_resource_absence(sender, *args, **kwargs):
|
|
93
|
+
return {"request_id": None, "profile": None, "user": None, "obj_factory": None}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# =================================================================================================================
|
|
97
|
+
# UPDATE SERIALIZER
|
|
98
|
+
# =================================================================================================================
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@receiver(get_custom_serializer, sender=KPIModelViewSet)
|
|
102
|
+
def receive_serializer_kpi(sender, *args, **kwargs):
|
|
103
|
+
return KPIModelSerializer
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def app_pre_migration(sender, app_config, **kwargs):
|
|
107
|
+
cur = connection.cursor()
|
|
108
|
+
cur.execute("CREATE EXTENSION IF NOT EXISTS btree_gist;")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django.contrib.auth import get_user_model
|
|
3
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
4
|
+
from faker import Faker
|
|
5
|
+
from rest_framework.test import APIRequestFactory
|
|
6
|
+
from wbcore.contrib.authentication.factories import UserFactory
|
|
7
|
+
from wbcore.permissions.shortcuts import is_internal_user
|
|
8
|
+
|
|
9
|
+
from wbhuman_resources.factories.employee import EmployeeHumanResourceFactory
|
|
10
|
+
from wbhuman_resources.models.employee import EmployeeHumanResource
|
|
11
|
+
|
|
12
|
+
User = get_user_model()
|
|
13
|
+
fake = Faker()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.django_db
|
|
17
|
+
class TestPermissionTasks:
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def request_user_external(self):
|
|
20
|
+
request = APIRequestFactory()
|
|
21
|
+
user = UserFactory()
|
|
22
|
+
EmployeeHumanResourceFactory.create(
|
|
23
|
+
profile=user.profile, contract_type=EmployeeHumanResource.ContractType.EXTERNAL
|
|
24
|
+
)
|
|
25
|
+
request.user = user
|
|
26
|
+
return request
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def request_user_active_internal(self):
|
|
30
|
+
request = APIRequestFactory()
|
|
31
|
+
user = UserFactory()
|
|
32
|
+
EmployeeHumanResourceFactory.create(
|
|
33
|
+
profile=user.profile, contract_type=EmployeeHumanResource.ContractType.INTERNAL
|
|
34
|
+
)
|
|
35
|
+
request.user = user
|
|
36
|
+
return request
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def request_user_inactive_internal(self):
|
|
40
|
+
request = APIRequestFactory()
|
|
41
|
+
user = UserFactory()
|
|
42
|
+
EmployeeHumanResourceFactory.create(
|
|
43
|
+
profile=user.profile, contract_type=EmployeeHumanResource.ContractType.INTERNAL, is_active=False
|
|
44
|
+
)
|
|
45
|
+
request.user = user
|
|
46
|
+
return request
|
|
47
|
+
|
|
48
|
+
def test_permission_active_internal(self, request_user_active_internal):
|
|
49
|
+
assert is_internal_user(request_user_active_internal.user) is True
|
|
50
|
+
|
|
51
|
+
def test_permission_inactive_internal(self, request_user_inactive_internal):
|
|
52
|
+
assert is_internal_user(request_user_inactive_internal.user) is False
|
|
53
|
+
|
|
54
|
+
def test_permission_external(self, request_user_external):
|
|
55
|
+
assert is_internal_user(request_user_external.user) is False
|
|
56
|
+
|
|
57
|
+
def test_permission_external_but_considered_internal(self, request_user_external):
|
|
58
|
+
from wbcore.permissions.registry import user_registry
|
|
59
|
+
|
|
60
|
+
user = request_user_external.user
|
|
61
|
+
global_preferences_registry.manager()["wbhuman_resources__is_external_considered_as_internal"] = True
|
|
62
|
+
user = User.objects.get(id=user.id) # reload to reset cached property
|
|
63
|
+
user_registry.reset_cache()
|
|
64
|
+
assert is_internal_user(user) is True
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytest
|
|
6
|
+
from django.contrib.auth.models import Permission
|
|
7
|
+
from django.contrib.contenttypes.models import ContentType
|
|
8
|
+
from django.core import mail
|
|
9
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
10
|
+
from faker import Faker
|
|
11
|
+
|
|
12
|
+
from wbhuman_resources.models import AbsenceRequest
|
|
13
|
+
from wbhuman_resources.models.employee import EmployeeHumanResource
|
|
14
|
+
from wbhuman_resources.tasks import assign_balance, send_mail_to_accounting
|
|
15
|
+
|
|
16
|
+
fake = Faker()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.django_db
|
|
20
|
+
class TestHumanResourceTasks:
|
|
21
|
+
@pytest.mark.parametrize("accounting_email_1,accounting_email_2", [(fake.email(), fake.email())])
|
|
22
|
+
@patch.object(EmployeeHumanResource, "get_end_of_month_employee_balance_report_df")
|
|
23
|
+
def test_send_mail_to_accounting_recipient(self, mock_fct, user_factory, accounting_email_1, accounting_email_2):
|
|
24
|
+
global_preferences_registry.manager()["wbhuman_resources__accounting_company_emails"] = (
|
|
25
|
+
f"{accounting_email_1};{accounting_email_2}"
|
|
26
|
+
)
|
|
27
|
+
mock_fct.return_value = pd.DataFrame(data=["a", "b"])
|
|
28
|
+
|
|
29
|
+
admin_user = user_factory.create(is_superuser=True)
|
|
30
|
+
content_type = ContentType.objects.get_for_model(AbsenceRequest)
|
|
31
|
+
permission = Permission.objects.get(
|
|
32
|
+
codename="administrate_absencerequest",
|
|
33
|
+
content_type=content_type,
|
|
34
|
+
)
|
|
35
|
+
admin_user.user_permissions.add(permission)
|
|
36
|
+
user_factory.create(
|
|
37
|
+
is_superuser=True
|
|
38
|
+
) # we create another superuser to check if administirators don't contain it
|
|
39
|
+
|
|
40
|
+
send_mail_to_accounting()
|
|
41
|
+
assert len(mail.outbox) == 1
|
|
42
|
+
last_mail = mail.outbox[-1]
|
|
43
|
+
|
|
44
|
+
assert set(last_mail.to) == {accounting_email_2, accounting_email_1}
|
|
45
|
+
assert set(last_mail.cc) == {admin_user.email}
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize("year_str", [(fake.year())])
|
|
48
|
+
def test_assign_yearly_balance(self, year_str, employee_human_resource_factory):
|
|
49
|
+
year = int(year_str)
|
|
50
|
+
test_date = date(year, 1, 1)
|
|
51
|
+
active_employee = employee_human_resource_factory.create(
|
|
52
|
+
enrollment_at=test_date,
|
|
53
|
+
is_active=True,
|
|
54
|
+
occupancy_rate=1,
|
|
55
|
+
extra_days_frequency=EmployeeHumanResource.ExtraDaysBalanceFrequency.YEARLY,
|
|
56
|
+
)
|
|
57
|
+
unactive_employee = employee_human_resource_factory.create(
|
|
58
|
+
enrollment_at=test_date,
|
|
59
|
+
is_active=False,
|
|
60
|
+
occupancy_rate=1,
|
|
61
|
+
extra_days_frequency=EmployeeHumanResource.ExtraDaysBalanceFrequency.YEARLY,
|
|
62
|
+
)
|
|
63
|
+
assert not active_employee.balances.exists()
|
|
64
|
+
assign_balance(test_date)
|
|
65
|
+
assert not unactive_employee.balances.exists()
|
|
66
|
+
assert active_employee.balances.count() == 1 # check that we get only this year balance
|
|
67
|
+
employee_balance = active_employee.balances.get(year=test_date.year)
|
|
68
|
+
allowance = employee_balance.monthly_allowances.first()
|
|
69
|
+
assert (
|
|
70
|
+
employee_balance.monthly_allowances.count() == 1
|
|
71
|
+
) # check that a unique allowance was granted to the employee
|
|
72
|
+
assert (
|
|
73
|
+
allowance.hourly_allowance == 25 * 8
|
|
74
|
+
) # check the the hourly amount correspond ot the base 25 days per year in hours
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from django.urls import include, path
|
|
4
|
+
from wbcore.routers import WBCoreRouter
|
|
5
|
+
|
|
6
|
+
from wbhuman_resources import viewsets
|
|
7
|
+
|
|
8
|
+
router = WBCoreRouter()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Calendar viewsets
|
|
12
|
+
|
|
13
|
+
router.register(
|
|
14
|
+
"defaultdailyperiodrepresentation",
|
|
15
|
+
viewsets.DefaultDailyPeriodRepresentationViewSet,
|
|
16
|
+
basename="defaultdailyperiodrepresentation",
|
|
17
|
+
)
|
|
18
|
+
router.register(
|
|
19
|
+
"employeeweeklyoffperiodrepresentation",
|
|
20
|
+
viewsets.EmployeeWeeklyOffPeriodsRepresentationViewSet,
|
|
21
|
+
basename="employeeweeklyoffperiodrepresentation",
|
|
22
|
+
)
|
|
23
|
+
router.register("dayoffrepresentation", viewsets.DayOffRepresentationViewSet, basename="dayoffrepresentation")
|
|
24
|
+
router.register(
|
|
25
|
+
"dayoffcalendarrepresentation",
|
|
26
|
+
viewsets.DayOffCalendarRepresentationViewSet,
|
|
27
|
+
basename="dayoffcalendarrepresentation",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
router.register("dayoffcalendar", viewsets.DayOffCalendarModelViewSet, basename="dayoffcalendar")
|
|
31
|
+
router.register("dayoff", viewsets.DayOffModelViewSet, basename="dayoff")
|
|
32
|
+
|
|
33
|
+
calendar_router = WBCoreRouter()
|
|
34
|
+
calendar_router.register("dayoff", viewsets.DayOffDayOffCalendarModelViewSet, basename="calendar-dayoff")
|
|
35
|
+
calendar_router.register(
|
|
36
|
+
"defaultperiod", viewsets.DefaultDailyPeriodDayOffCalendarModelViewSet, basename="calendar-defaultperiod"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# absence request type router
|
|
40
|
+
absencerequesttype_router = WBCoreRouter()
|
|
41
|
+
absencerequesttype_router.register(
|
|
42
|
+
"crossbordercountry",
|
|
43
|
+
viewsets.AbsenceRequestCrossBorderCountryModelViewSet,
|
|
44
|
+
basename="absencerequesttype-crossbordercountry",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Employee routers
|
|
49
|
+
router.register(
|
|
50
|
+
r"employeehumanresourcerepresentation",
|
|
51
|
+
viewsets.EmployeeHumanResourceRepresentationViewSet,
|
|
52
|
+
basename="employeehumanresourcerepresentation",
|
|
53
|
+
)
|
|
54
|
+
router.register(r"positionrepresentation", viewsets.PositionRepresentationViewSet, basename="positionrepresentation")
|
|
55
|
+
router.register(
|
|
56
|
+
"employeeyearbalancerepresentation",
|
|
57
|
+
viewsets.EmployeeYearBalanceRepresentationViewSet,
|
|
58
|
+
basename="employeeyearbalancerepresentation",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
router.register(r"employeebalance", viewsets.EmployeeBalanceModelViewSet, basename="employeebalance")
|
|
62
|
+
router.register(r"employee", viewsets.EmployeeModelViewSet, basename="employee")
|
|
63
|
+
router.register(r"position", viewsets.PositionModelViewSet, basename="position")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
employee_router = WBCoreRouter()
|
|
67
|
+
employee_router.register(
|
|
68
|
+
r"weeklyoffperiod",
|
|
69
|
+
viewsets.WeeklyOffPeriodEmployeeHumanResourceModelViewSet,
|
|
70
|
+
basename="employee-weeklyoffperiod",
|
|
71
|
+
)
|
|
72
|
+
employee_router.register(
|
|
73
|
+
r"absence",
|
|
74
|
+
viewsets.AbsenceTypeCountEmployeeModelViewSet,
|
|
75
|
+
basename="employee-absencecount",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
employee_router.register(
|
|
79
|
+
r"absencerequest",
|
|
80
|
+
viewsets.AbsenceRequestEmployeeHumanResourceModelViewset,
|
|
81
|
+
basename="employee-absencerequest",
|
|
82
|
+
)
|
|
83
|
+
employee_router.register(
|
|
84
|
+
r"employeeyearbalance",
|
|
85
|
+
viewsets.YearBalanceEmployeeHumanResourceModelViewset,
|
|
86
|
+
basename="employee-employeeyearbalance",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Absence routers
|
|
90
|
+
router.register(
|
|
91
|
+
r"absencerequesttyperepresentation",
|
|
92
|
+
viewsets.AbsenceRequestTypeRepresentationViewSet,
|
|
93
|
+
basename="absencerequesttyperepresentation",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
router.register(r"absenceplanner", viewsets.AbsenceRequestPlanner, basename="absenceplanner")
|
|
98
|
+
router.register(r"absencerequest", viewsets.AbsenceRequestModelViewSet)
|
|
99
|
+
router.register(r"absencerequesttype", viewsets.AbsenceRequestTypeModelViewSet, basename="absencerequesttype")
|
|
100
|
+
|
|
101
|
+
absence_request_router = WBCoreRouter()
|
|
102
|
+
absence_request_router.register(
|
|
103
|
+
r"periods",
|
|
104
|
+
viewsets.AbsenceRequestPeriodsAbsenceRequestModelViewSet,
|
|
105
|
+
basename="request-periods",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Review Routers
|
|
109
|
+
router.register(r"reviewrepresentation", viewsets.ReviewRepresentationViewSet, basename="reviewrepresentation")
|
|
110
|
+
router.register(
|
|
111
|
+
r"reviewgrouprepresentation", viewsets.ReviewGroupRepresentationViewSet, basename="reviewgrouprepresentation"
|
|
112
|
+
)
|
|
113
|
+
router.register(
|
|
114
|
+
r"reviewquestioncategoryrepresentation",
|
|
115
|
+
viewsets.ReviewQuestionCategoryRepresentationViewSet,
|
|
116
|
+
basename="reviewquestioncategoryrepresentation",
|
|
117
|
+
)
|
|
118
|
+
router.register(
|
|
119
|
+
r"reviewquestionrepresentation",
|
|
120
|
+
viewsets.ReviewQuestionRepresentationViewSet,
|
|
121
|
+
basename="reviewquestionrepresentation",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
router.register(r"reviewgroup", viewsets.ReviewGroupModelViewSet, basename="reviewgroup")
|
|
125
|
+
|
|
126
|
+
router.register(r"review", viewsets.ReviewModelViewSet, basename="review")
|
|
127
|
+
router.register(r"reviewtemplate", viewsets.ReviewTemplateModelViewSet, basename="reviewtemplate")
|
|
128
|
+
|
|
129
|
+
router.register(
|
|
130
|
+
r"reviewquestioncategory", viewsets.ReviewQuestionCategoryModelViewSet, basename="reviewquestioncategory"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
router.register(r"reviewquestion", viewsets.ReviewQuestionModelViewSet, basename="reviewquestion")
|
|
134
|
+
router.register(r"reviewanswer", viewsets.ReviewAnswerModelViewSet, basename="reviewanswer")
|
|
135
|
+
router.register(r"absencetable", viewsets.AbsenceTablePandasViewSet, basename="absencetable")
|
|
136
|
+
|
|
137
|
+
reviewgroup_router = WBCoreRouter()
|
|
138
|
+
reviewgroup_router.register(r"review", viewsets.ReviewReviewGroupModelViewSet, basename="reviewgroup-review")
|
|
139
|
+
|
|
140
|
+
review_router = WBCoreRouter()
|
|
141
|
+
review_router.register(r"reviewquestion", viewsets.ReviewQuestionReviewModelViewSet, basename="review-reviewquestion")
|
|
142
|
+
review_router.register(
|
|
143
|
+
r"reviewquestionnocategory",
|
|
144
|
+
viewsets.ReviewQuestionReviewNoCategoryModelViewSet,
|
|
145
|
+
basename="review-reviewquestionnocategory",
|
|
146
|
+
)
|
|
147
|
+
review_router.register(
|
|
148
|
+
r"reviewanswerquestionnocategory",
|
|
149
|
+
viewsets.ReviewAnswerReviewNoCategoryModelViewSet,
|
|
150
|
+
basename="review-reviewanswerquestionnocategory",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
review_category_router = WBCoreRouter()
|
|
155
|
+
review_category_router.register(
|
|
156
|
+
r"reviewquestioncategory",
|
|
157
|
+
viewsets.ReviewQuestionReviewCategoryModelViewSet,
|
|
158
|
+
basename="review-reviewquestioncategory",
|
|
159
|
+
)
|
|
160
|
+
review_category_router.register(
|
|
161
|
+
r"reviewanswerquestioncategory",
|
|
162
|
+
viewsets.ReviewAnswerReviewQuestionCategoryModelViewSet,
|
|
163
|
+
basename="review-reviewanswerquestioncategory",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
review_question_category_router = WBCoreRouter()
|
|
167
|
+
review_question_category_router.register(
|
|
168
|
+
r"reviewquestion",
|
|
169
|
+
viewsets.ReviewQuestionReviewQuestionCategoryModelViewSet,
|
|
170
|
+
basename="reviewquestioncategory-reviewquestion",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# KPI routers
|
|
174
|
+
router.register(r"kpi", viewsets.KPIModelViewSet, basename="kpi")
|
|
175
|
+
router.register(r"kpirepresentation", viewsets.KPIRepresentationViewSet, basename="kpirepresentation")
|
|
176
|
+
router.register(
|
|
177
|
+
r"evaluationrepresentation", viewsets.EvaluationRepresentationViewSet, basename="evaluationrepresentation"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
kpi_router = WBCoreRouter()
|
|
181
|
+
|
|
182
|
+
kpi_router.register(
|
|
183
|
+
r"evaluation",
|
|
184
|
+
viewsets.KPIEvaluationModelViewSet,
|
|
185
|
+
basename="kpi-evaluation",
|
|
186
|
+
)
|
|
187
|
+
kpi_router.register(r"evaluationgraph", viewsets.EvaluationGraphChartViewset, basename="kpi-evaluationgraph")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Review pandas views
|
|
191
|
+
router.register("reviewprogress", viewsets.ReviewProgressPandasViewSet, basename="reviewprogress")
|
|
192
|
+
router.register("kpievaluationpandas", viewsets.KPIEvaluationPandasViewSet, basename="kpievaluationpandas")
|
|
193
|
+
review_router.register(r"progress", viewsets.ReviewProgressReviewPandasViewSet, basename="review-progress")
|
|
194
|
+
review_router.register(
|
|
195
|
+
r"reviewanswerpandasview", viewsets.ReviewAnswerReviewPandasViewSet, basename="review-reviewanswerpandasview"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
urlpatterns = [
|
|
199
|
+
path("", include(router.urls)),
|
|
200
|
+
path(
|
|
201
|
+
"absencerequesttype/<int:absencerequesttype_id>/",
|
|
202
|
+
include(absencerequesttype_router.urls),
|
|
203
|
+
),
|
|
204
|
+
path(
|
|
205
|
+
"employee/<int:employee_id>/",
|
|
206
|
+
include(employee_router.urls),
|
|
207
|
+
),
|
|
208
|
+
path(
|
|
209
|
+
"request/<int:request_id>/",
|
|
210
|
+
include(absence_request_router.urls),
|
|
211
|
+
),
|
|
212
|
+
path(
|
|
213
|
+
"calendar/<int:calendar_id>/",
|
|
214
|
+
include(calendar_router.urls),
|
|
215
|
+
),
|
|
216
|
+
path("reviewgroup/<int:review_group_id>/", include(reviewgroup_router.urls)),
|
|
217
|
+
path("review/<int:review_id>/", include(review_router.urls)),
|
|
218
|
+
path("review/<int:review_id>/category/<int:category_id>/", include(review_category_router.urls)),
|
|
219
|
+
path("reviewquestioncategory/<int:category_id>/", include(review_question_category_router.urls)),
|
|
220
|
+
path("kpi/<int:kpi_id>/", include(kpi_router.urls)),
|
|
221
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from collections import OrderedDict, defaultdict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from dateutil import rrule
|
|
5
|
+
from django.utils import timezone
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_number_of_hours_between_dates(
|
|
9
|
+
d1, d2, list_employee_dayoffs, list_public_holidays=False, hours_range=None, granularity=12
|
|
10
|
+
):
|
|
11
|
+
if hours_range is None:
|
|
12
|
+
hours_range = range(0, 23)
|
|
13
|
+
|
|
14
|
+
def convert_days_from_hours(hours, granularity, hours_per_day):
|
|
15
|
+
return int(hours / granularity) * granularity / hours_per_day
|
|
16
|
+
|
|
17
|
+
rules = rrule.rruleset()
|
|
18
|
+
|
|
19
|
+
byweekday_list = [rrule.MO, rrule.TU, rrule.WE, rrule.TH, rrule.FR, rrule.SA, rrule.SU]
|
|
20
|
+
|
|
21
|
+
rules.rrule(
|
|
22
|
+
rrule.rrule(
|
|
23
|
+
freq=rrule.HOURLY,
|
|
24
|
+
byweekday=byweekday_list,
|
|
25
|
+
byhour=hours_range,
|
|
26
|
+
dtstart=d1,
|
|
27
|
+
until=d2,
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
if list_public_holidays:
|
|
31
|
+
list_employee_dayoffs.extend(list_public_holidays)
|
|
32
|
+
current_tz = timezone.get_current_timezone()
|
|
33
|
+
for holiday in sorted(list_employee_dayoffs):
|
|
34
|
+
s1 = datetime(holiday.year, holiday.month, holiday.day, 0, 0, 0, tzinfo=current_tz)
|
|
35
|
+
s2 = datetime(holiday.year, holiday.month, holiday.day, 23, 59, 59, tzinfo=current_tz)
|
|
36
|
+
rules.exrule(rrule.rrule(rrule.HOURLY, dtstart=s1, until=s2))
|
|
37
|
+
dates = defaultdict(int)
|
|
38
|
+
for r in list(rules):
|
|
39
|
+
dates[r.date()] += 1
|
|
40
|
+
final = OrderedDict()
|
|
41
|
+
for k, v in dates.items():
|
|
42
|
+
final[k] = convert_days_from_hours(v, granularity, len(hours_range))
|
|
43
|
+
return final
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from .absence import (
|
|
2
|
+
AbsenceRequestCrossBorderCountryModelViewSet,
|
|
3
|
+
AbsenceRequestEmployeeHumanResourceModelViewset,
|
|
4
|
+
AbsenceRequestModelViewSet,
|
|
5
|
+
AbsenceRequestPeriodsAbsenceRequestModelViewSet,
|
|
6
|
+
AbsenceRequestTypeModelViewSet,
|
|
7
|
+
AbsenceRequestTypeRepresentationViewSet,
|
|
8
|
+
AbsenceTypeCountEmployeeModelViewSet,
|
|
9
|
+
)
|
|
10
|
+
from .absence_charts import AbsenceRequestPlanner, AbsenceTablePandasViewSet
|
|
11
|
+
from .calendars import (
|
|
12
|
+
DayOffCalendarModelViewSet,
|
|
13
|
+
DayOffCalendarRepresentationViewSet,
|
|
14
|
+
DayOffDayOffCalendarModelViewSet,
|
|
15
|
+
DayOffModelViewSet,
|
|
16
|
+
DayOffRepresentationViewSet,
|
|
17
|
+
DefaultDailyPeriodDayOffCalendarModelViewSet,
|
|
18
|
+
DefaultDailyPeriodRepresentationViewSet,
|
|
19
|
+
EmployeeWeeklyOffPeriodsRepresentationViewSet,
|
|
20
|
+
)
|
|
21
|
+
from .employee import (
|
|
22
|
+
EmployeeBalanceModelViewSet,
|
|
23
|
+
EmployeeHumanResourceRepresentationViewSet,
|
|
24
|
+
EmployeeModelViewSet,
|
|
25
|
+
EmployeeYearBalanceRepresentationViewSet,
|
|
26
|
+
PositionModelViewSet,
|
|
27
|
+
PositionRepresentationViewSet,
|
|
28
|
+
WeeklyOffPeriodEmployeeHumanResourceModelViewSet,
|
|
29
|
+
YearBalanceEmployeeHumanResourceModelViewset,
|
|
30
|
+
)
|
|
31
|
+
from .kpi import (
|
|
32
|
+
EvaluationGraphChartViewset,
|
|
33
|
+
EvaluationRepresentationViewSet,
|
|
34
|
+
KPIEvaluationModelViewSet,
|
|
35
|
+
KPIEvaluationPandasViewSet,
|
|
36
|
+
KPIModelViewSet,
|
|
37
|
+
KPIRepresentationViewSet,
|
|
38
|
+
)
|
|
39
|
+
from .review import (
|
|
40
|
+
ReviewAnswerReviewPandasViewSet,
|
|
41
|
+
ReviewAnswerModelViewSet,
|
|
42
|
+
ReviewAnswerRepresentationViewSet,
|
|
43
|
+
ReviewAnswerReviewNoCategoryModelViewSet,
|
|
44
|
+
ReviewAnswerReviewQuestionCategoryModelViewSet,
|
|
45
|
+
ReviewGroupModelViewSet,
|
|
46
|
+
ReviewGroupRepresentationViewSet,
|
|
47
|
+
ReviewModelViewSet,
|
|
48
|
+
ReviewProgressPandasViewSet,
|
|
49
|
+
ReviewProgressReviewPandasViewSet,
|
|
50
|
+
ReviewQuestionCategoryModelViewSet,
|
|
51
|
+
ReviewQuestionCategoryRepresentationViewSet,
|
|
52
|
+
ReviewQuestionModelViewSet,
|
|
53
|
+
ReviewQuestionRepresentationViewSet,
|
|
54
|
+
ReviewQuestionReviewCategoryModelViewSet,
|
|
55
|
+
ReviewQuestionReviewModelViewSet,
|
|
56
|
+
ReviewQuestionReviewNoCategoryModelViewSet,
|
|
57
|
+
ReviewQuestionReviewQuestionCategoryModelViewSet,
|
|
58
|
+
ReviewRepresentationViewSet,
|
|
59
|
+
ReviewReviewGroupModelViewSet,
|
|
60
|
+
ReviewTemplateModelViewSet,
|
|
61
|
+
)
|