Corp/backend/tests/employees/test_personnel_state.py

268 lines
8.3 KiB
Python

from datetime import date, time
import pytest
from django.utils import timezone
from employees.constants import (
ACTION_DISMISSED,
ACTION_HIRED,
ACTION_ON_DAY_OFF,
ACTION_ON_LEAVE,
ACTION_ON_SICK_LEAVE,
ACTION_REHIRED,
ACTION_REMOTE,
ACTION_RETURNED_FROM_LEAVE,
ACTION_WORKING,
)
from employees.models import EmployeeAction
from employees.services.personnel_state import (
PERSONNEL_STATUS_NORMAL,
calculate_employee_tenure_days,
resolve_employee_personnel_state,
)
from requests_app.enums import RequestStatus, RequestType
from requests_app.models import Request
pytestmark = pytest.mark.django_db
def _aware_datetime(year: int, month: int, day: int):
return timezone.make_aware(timezone.datetime(year, month, day, 12))
def test_permanent_working_state_expects_attendance(user_factory):
employee = user_factory()
working = EmployeeAction.objects.create(
employee=employee,
action=ACTION_WORKING,
date=_aware_datetime(2026, 4, 10),
)
state = resolve_employee_personnel_state(employee, date(2026, 4, 20))
assert state.status == ACTION_WORKING
assert state.action_id == working.id
assert state.expects_attendance is True
def test_no_actions_falls_back_to_normal_state(user_factory):
employee = user_factory()
EmployeeAction.objects.filter(employee=employee).delete()
state = resolve_employee_personnel_state(employee, date(2026, 4, 20))
assert state.status == PERSONNEL_STATUS_NORMAL
assert state.expects_attendance is True
def test_temporary_interval_is_active_until_date_to(user_factory):
employee = user_factory()
leave = EmployeeAction.objects.create(
employee=employee,
action=ACTION_ON_LEAVE,
date=_aware_datetime(2026, 4, 20),
date_to=date(2026, 4, 22),
)
state = resolve_employee_personnel_state(employee, date(2026, 4, 21))
assert state.status == ACTION_ON_LEAVE
assert state.action_id == leave.id
assert state.date_to == date(2026, 4, 22)
assert state.expects_attendance is False
def test_temporary_interval_ends_after_date_to(user_factory):
employee = user_factory()
EmployeeAction.objects.create(
employee=employee,
action=ACTION_ON_LEAVE,
date=_aware_datetime(2026, 4, 20),
date_to=date(2026, 4, 22),
)
state = resolve_employee_personnel_state(employee, date(2026, 4, 23))
assert state.status == ACTION_WORKING
assert state.expects_attendance is True
def test_temporary_without_date_to_is_one_day_legacy_fallback(user_factory):
employee = user_factory()
EmployeeAction.objects.create(
employee=employee,
action=ACTION_ON_LEAVE,
date=_aware_datetime(2026, 4, 20),
)
EmployeeAction.objects.create(
employee=employee,
action=ACTION_RETURNED_FROM_LEAVE,
date=_aware_datetime(2026, 4, 23),
)
active = resolve_employee_personnel_state(employee, date(2026, 4, 20))
ended = resolve_employee_personnel_state(employee, date(2026, 4, 21))
returned = resolve_employee_personnel_state(employee, date(2026, 4, 23))
assert active.status == ACTION_ON_LEAVE
assert active.date_to == date(2026, 4, 20)
assert ended.status == ACTION_WORKING
assert returned.status == ACTION_RETURNED_FROM_LEAVE
assert returned.expects_attendance is True
def test_marker_action_is_one_day_status(user_factory):
employee = user_factory()
hired = EmployeeAction.objects.create(
employee=employee,
action=ACTION_HIRED,
date=_aware_datetime(2026, 4, 20),
)
marker = resolve_employee_personnel_state(employee, date(2026, 4, 20))
fallback = resolve_employee_personnel_state(employee, date(2026, 4, 21))
assert marker.status == ACTION_HIRED
assert marker.action_id == hired.id
assert marker.expects_attendance is True
assert fallback.status == ACTION_WORKING
def test_rehire_restores_previous_permanent_state_after_dismissal(user_factory):
employee = user_factory()
remote = EmployeeAction.objects.create(
employee=employee,
action=ACTION_REMOTE,
date=_aware_datetime(2026, 4, 10),
)
dismissed = EmployeeAction.objects.create(
employee=employee,
action=ACTION_DISMISSED,
date=_aware_datetime(2026, 4, 12),
)
rehire = EmployeeAction.objects.create(
employee=employee,
action=ACTION_REHIRED,
date=_aware_datetime(2026, 4, 15),
)
dismissed_state = resolve_employee_personnel_state(employee, date(2026, 4, 13))
rehire_state = resolve_employee_personnel_state(employee, date(2026, 4, 15))
restored_state = resolve_employee_personnel_state(employee, date(2026, 4, 16))
assert dismissed_state.status == ACTION_DISMISSED
assert dismissed_state.action_id == dismissed.id
assert rehire_state.status == ACTION_REHIRED
assert rehire_state.action_id == rehire.id
assert rehire_state.expects_attendance is True
assert restored_state.status == ACTION_REMOTE
assert restored_state.action_id == remote.id
def test_tenure_sums_employee_action_periods(user_factory):
employee = user_factory()
EmployeeAction.objects.create(
employee=employee,
action=ACTION_HIRED,
date=_aware_datetime(2020, 1, 1),
)
EmployeeAction.objects.create(
employee=employee,
action=ACTION_DISMISSED,
date=_aware_datetime(2022, 1, 1),
)
EmployeeAction.objects.create(
employee=employee,
action=ACTION_REHIRED,
date=_aware_datetime(2024, 1, 1),
)
EmployeeAction.objects.create(
employee=employee,
action=ACTION_REMOTE,
date=_aware_datetime(2025, 1, 1),
)
tenure_days = calculate_employee_tenure_days(employee, date(2026, 7, 12))
assert tenure_days == (
(date(2022, 1, 1) - date(2020, 1, 1)).days
+ (date(2026, 7, 12) - date(2024, 1, 1)).days
)
def test_overlapping_temporary_intervals_use_priority(user_factory):
employee = user_factory()
EmployeeAction.objects.create(
employee=employee,
action=ACTION_ON_LEAVE,
date=_aware_datetime(2026, 4, 20),
date_to=date(2026, 4, 25),
)
sick = EmployeeAction.objects.create(
employee=employee,
action=ACTION_ON_SICK_LEAVE,
date=_aware_datetime(2026, 4, 21),
date_to=date(2026, 4, 22),
)
EmployeeAction.objects.create(
employee=employee,
action=ACTION_ON_DAY_OFF,
date=_aware_datetime(2026, 4, 21),
date_to=date(2026, 4, 21),
)
state = resolve_employee_personnel_state(employee, date(2026, 4, 21))
assert state.status == ACTION_ON_SICK_LEAVE
assert state.action_id == sick.id
def test_short_day_off_keeps_workday_and_exposes_approved_interval(user_factory):
employee = user_factory()
request = Request.objects.create(
employee=employee,
approver=employee,
type=RequestType.SHORT_DAY_OFF,
status=RequestStatus.APPROVED,
date_from=date(2026, 4, 21),
date_to=date(2026, 4, 21),
time_from=time(14, 0),
time_to=time(16, 30),
)
EmployeeAction.objects.create(
employee=employee,
source_request=request,
action=ACTION_ON_DAY_OFF,
date=timezone.make_aware(timezone.datetime(2026, 4, 21, 14, 0)),
date_to=date(2026, 4, 21),
extra={"partial_day": True},
)
state = resolve_employee_personnel_state(employee, date(2026, 4, 21))
assert state.expects_attendance is True
assert state.status != ACTION_ON_DAY_OFF
assert state.approved_absence_intervals == ((time(14, 0), time(16, 30)),)
assert state.approved_absence_minutes == 150
def test_dismissal_overrides_temporary_interval(user_factory):
employee = user_factory()
EmployeeAction.objects.create(
employee=employee,
action=ACTION_ON_LEAVE,
date=_aware_datetime(2026, 4, 20),
date_to=date(2026, 4, 25),
)
dismissed = EmployeeAction.objects.create(
employee=employee,
action=ACTION_DISMISSED,
date=_aware_datetime(2026, 4, 21),
)
state = resolve_employee_personnel_state(employee, date(2026, 4, 22))
assert state.status == ACTION_DISMISSED
assert state.action_id == dismissed.id
assert state.expects_attendance is False