3359a8db13
Модель/сервисы HR-заявок с заменами, приглашения на установку пароля (инвайты), обновлённая страница профиля сотрудника и правки 2FA. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
293 lines
10 KiB
Python
293 lines
10 KiB
Python
import datetime
|
|
|
|
from django.contrib.auth.models import Group
|
|
from django.core.exceptions import ValidationError
|
|
from django.test import Client, TestCase
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
from apps.employees.models import Employee, EmployeeHrRequest
|
|
from apps.employees.services.hr_requests import (
|
|
build_leave_balance,
|
|
cancel_hr_request,
|
|
check_leave_balance,
|
|
create_hr_request,
|
|
decide_hr_request,
|
|
next_pending_vacation,
|
|
pending_hr_requests_count,
|
|
)
|
|
from core.test_utils import login_2fa
|
|
|
|
|
|
def make_employee(username, **kw):
|
|
kw.setdefault('first_name', 'Иван')
|
|
kw.setdefault('last_name', 'Иванов')
|
|
kw.setdefault('hire_date', datetime.date(2020, 1, 1))
|
|
kw.setdefault('mattermost_sync_enabled', False)
|
|
kw.setdefault('ad_sync_enabled', False)
|
|
return Employee.objects.create_user(username=username, **kw)
|
|
|
|
|
|
class HrRequestServiceTests(TestCase):
|
|
def test_create_vacation_requires_dates(self):
|
|
employee = make_employee('emp1')
|
|
with self.assertRaises(ValidationError):
|
|
create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=None,
|
|
date_to=None,
|
|
)
|
|
|
|
def test_create_certificate_without_dates(self):
|
|
employee = make_employee('emp2')
|
|
req = create_hr_request(
|
|
employee=employee,
|
|
request_type='certificate',
|
|
date_from=None,
|
|
date_to=None,
|
|
note='2-НДФЛ',
|
|
)
|
|
self.assertEqual(req.status, 'pending')
|
|
self.assertEqual(req.day_count, 0)
|
|
|
|
def test_cancel_own_pending(self):
|
|
employee = make_employee('emp3')
|
|
req = create_hr_request(
|
|
employee=employee,
|
|
request_type='day_off',
|
|
date_from=datetime.date(2026, 6, 1),
|
|
date_to=datetime.date(2026, 6, 1),
|
|
)
|
|
cancelled = cancel_hr_request(req, actor=employee)
|
|
self.assertEqual(cancelled.status, 'cancelled')
|
|
|
|
def test_cannot_cancel_other_user_request(self):
|
|
employee = make_employee('emp4')
|
|
other = make_employee('emp5')
|
|
req = create_hr_request(
|
|
employee=employee,
|
|
request_type='day_off',
|
|
date_from=datetime.date(2026, 6, 2),
|
|
date_to=datetime.date(2026, 6, 2),
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
cancel_hr_request(req, actor=other)
|
|
|
|
def test_decide_approve_and_reject(self):
|
|
employee = make_employee('emp6')
|
|
hr = make_employee('hr1')
|
|
hr.groups.add(Group.objects.get(name='HR'))
|
|
req = create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(2026, 7, 1),
|
|
date_to=datetime.date(2026, 7, 5),
|
|
)
|
|
approved = decide_hr_request(req, decided_by=hr, approve=True)
|
|
self.assertEqual(approved.status, 'approved')
|
|
self.assertEqual(approved.decided_by_id, hr.pk)
|
|
|
|
req2 = create_hr_request(
|
|
employee=employee,
|
|
request_type='sick_leave',
|
|
date_from=datetime.date(2026, 8, 1),
|
|
date_to=datetime.date(2026, 8, 3),
|
|
)
|
|
rejected = decide_hr_request(req2, decided_by=hr, approve=False, decision_note='Нет')
|
|
self.assertEqual(rejected.status, 'rejected')
|
|
self.assertEqual(rejected.decision_note, 'Нет')
|
|
|
|
def test_leave_balance_counts_approved_days(self):
|
|
employee = make_employee('emp7')
|
|
year = timezone.localdate().year
|
|
create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(year, 3, 1),
|
|
date_to=datetime.date(year, 3, 5),
|
|
)
|
|
hr = make_employee('hr2')
|
|
hr.groups.add(Group.objects.get(name='HR'))
|
|
pending = EmployeeHrRequest.objects.filter(employee=employee, status='pending').first()
|
|
decide_hr_request(pending, decided_by=hr, approve=True)
|
|
|
|
balance = build_leave_balance(employee, year=year)
|
|
annual = next(row for row in balance if row.key == 'annual')
|
|
self.assertEqual(annual.used, 5)
|
|
self.assertEqual(annual.remaining, 19)
|
|
|
|
def test_next_pending_vacation_future_only(self):
|
|
employee = make_employee('emp8')
|
|
today = timezone.localdate()
|
|
create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=today - datetime.timedelta(days=5),
|
|
date_to=today - datetime.timedelta(days=1),
|
|
)
|
|
future = create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=today + datetime.timedelta(days=10),
|
|
date_to=today + datetime.timedelta(days=15),
|
|
)
|
|
self.assertEqual(next_pending_vacation(employee).pk, future.pk)
|
|
|
|
def test_pending_count(self):
|
|
employee = make_employee('emp9')
|
|
self.assertEqual(pending_hr_requests_count(), 0)
|
|
create_hr_request(
|
|
employee=employee,
|
|
request_type='certificate',
|
|
date_from=None,
|
|
date_to=None,
|
|
)
|
|
self.assertEqual(pending_hr_requests_count(), 1)
|
|
|
|
def test_create_rejects_exceeding_annual_balance(self):
|
|
employee = make_employee('emp10')
|
|
year = timezone.localdate().year
|
|
create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(year, 1, 1),
|
|
date_to=datetime.date(year, 1, 24),
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(year, 2, 1),
|
|
date_to=datetime.date(year, 2, 5),
|
|
)
|
|
|
|
def test_check_balance_allows_within_limit(self):
|
|
employee = make_employee('emp11')
|
|
year = timezone.localdate().year
|
|
check_leave_balance(
|
|
employee, 'vacation',
|
|
datetime.date(year, 6, 1), datetime.date(year, 6, 5),
|
|
)
|
|
|
|
def test_approve_rejects_when_balance_exhausted(self):
|
|
employee = make_employee('emp12')
|
|
hr = make_employee('hr3')
|
|
hr.groups.add(Group.objects.get(name='HR'))
|
|
year = timezone.localdate().year
|
|
from apps.employees.services.hr_requests import get_entitlement
|
|
|
|
ent = get_entitlement(employee, year)
|
|
ent.annual_days = 5
|
|
ent.save(update_fields=['annual_days'])
|
|
|
|
req = create_hr_request(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(year, 1, 1),
|
|
date_to=datetime.date(year, 1, 5),
|
|
)
|
|
EmployeeHrRequest.objects.create(
|
|
employee=employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(year, 6, 1),
|
|
date_to=datetime.date(year, 6, 3),
|
|
status='approved',
|
|
)
|
|
with self.assertRaises(ValidationError):
|
|
decide_hr_request(req, decided_by=hr, approve=True)
|
|
|
|
|
|
class HrRequestViewTests(TestCase):
|
|
def setUp(self):
|
|
self.client = Client()
|
|
self.employee = make_employee('view_emp')
|
|
self.hr = make_employee('view_hr')
|
|
self.hr.groups.add(Group.objects.get(name='HR'))
|
|
|
|
def test_create_request_via_post(self):
|
|
login_2fa(self.client, self.employee)
|
|
year = timezone.localdate().year
|
|
resp = self.client.post(
|
|
reverse('hr_request_create'),
|
|
{
|
|
'request_type': 'vacation',
|
|
'date_from': f'{year}-09-01',
|
|
'date_to': f'{year}-09-05',
|
|
'note': 'Отпуск',
|
|
},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertTrue(
|
|
EmployeeHrRequest.objects.filter(
|
|
employee=self.employee, request_type='vacation', status='pending',
|
|
).exists()
|
|
)
|
|
|
|
def test_cancel_own_request(self):
|
|
req = create_hr_request(
|
|
employee=self.employee,
|
|
request_type='day_off',
|
|
date_from=datetime.date(2026, 10, 1),
|
|
date_to=datetime.date(2026, 10, 1),
|
|
)
|
|
login_2fa(self.client, self.employee)
|
|
resp = self.client.post(reverse('hr_request_cancel', args=[req.pk]), HTTP_HX_REQUEST='true')
|
|
self.assertEqual(resp.status_code, 200)
|
|
req.refresh_from_db()
|
|
self.assertEqual(req.status, 'cancelled')
|
|
|
|
def test_hr_approve_pending(self):
|
|
req = create_hr_request(
|
|
employee=self.employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(2026, 11, 1),
|
|
date_to=datetime.date(2026, 11, 3),
|
|
)
|
|
login_2fa(self.client, self.hr)
|
|
resp = self.client.post(reverse('hr_request_approve', args=[req.pk]), HTTP_HX_REQUEST='true')
|
|
self.assertEqual(resp.status_code, 200)
|
|
req.refresh_from_db()
|
|
self.assertEqual(req.status, 'approved')
|
|
|
|
def test_create_certificate_via_post_on_profile(self):
|
|
login_2fa(self.client, self.employee)
|
|
resp = self.client.post(
|
|
reverse('hr_request_create'),
|
|
{
|
|
'request_type': 'certificate',
|
|
'note': '2-НДФЛ',
|
|
'return_to': 'profile',
|
|
},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, 'Заявку подано')
|
|
self.assertContains(resp, 'emp-profile-hr-requests')
|
|
self.assertContains(resp, 'data-profile-active-tab="overview"')
|
|
|
|
def test_validation_error_shown_on_page(self):
|
|
login_2fa(self.client, self.employee)
|
|
resp = self.client.post(
|
|
reverse('hr_request_create'),
|
|
{'request_type': 'vacation'},
|
|
HTTP_HX_REQUEST='true',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, 'Не удалось подать заявку')
|
|
self.assertContains(resp, 'data-hr-request-form-open="1"')
|
|
|
|
def test_non_hr_cannot_approve(self):
|
|
req = create_hr_request(
|
|
employee=self.employee,
|
|
request_type='vacation',
|
|
date_from=datetime.date(2026, 12, 1),
|
|
date_to=datetime.date(2026, 12, 2),
|
|
)
|
|
login_2fa(self.client, self.employee)
|
|
resp = self.client.post(reverse('hr_request_approve', args=[req.pk]), HTTP_HX_REQUEST='true')
|
|
self.assertEqual(resp.status_code, 403)
|
|
req.refresh_from_db()
|
|
self.assertEqual(req.status, 'pending')
|