Files
ACORE/apps/employees/models/hr_request.py
T
lokses 3359a8db13 employees: HR-заявки, приглашения на пароль, профиль и 2FA
Модель/сервисы HR-заявок с заменами, приглашения на установку пароля
(инвайты), обновлённая страница профиля сотрудника и правки 2FA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 18:03:32 +03:00

179 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Кадровые заявки сотрудников: отпуск, больничный, справки."""
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import BaseModel
class LeaveEntitlement(BaseModel):
"""Норма дней отпуска/больничных на год (если нет записи — дефолты из сервиса)."""
employee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='leave_entitlements',
verbose_name=_('Сотрудник'),
)
year = models.PositiveIntegerField(_('Год'))
annual_days = models.PositiveSmallIntegerField(_('Основной отпуск, дн.'), default=24)
additional_days = models.PositiveSmallIntegerField(_('Доп. отпуск, дн.'), default=7)
sick_days = models.PositiveSmallIntegerField(_('Больничные, дн.'), default=30)
class Meta:
verbose_name = _('Норма отпуска')
verbose_name_plural = _('Нормы отпусков')
constraints = [
models.UniqueConstraint(fields=['employee', 'year'], name='unique_leave_entitlement_per_year'),
]
ordering = ['-year', 'employee__last_name']
def __str__(self):
return f'{self.employee} — {self.year}'
class EmployeeHrRequest(BaseModel):
REQUEST_TYPES = [
('vacation', _('Отпуск')),
('additional_vacation', _('Дополнительный отпуск')),
('sick_leave', _('Больничный')),
('day_off', _('Отгул')),
('certificate', _('Справка')),
('substitution', _('Подмена')),
]
STATUS_CHOICES = [
('pending', _('На рассмотрении')),
('approved', _('Утверждено')),
('rejected', _('Отклонено')),
('cancelled', _('Отменено')),
]
DATE_REQUIRED_TYPES = {
'vacation', 'additional_vacation', 'sick_leave', 'day_off', 'substitution',
}
employee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='hr_requests',
verbose_name=_('Сотрудник'),
)
request_type = models.CharField(_('Тип'), max_length=32, choices=REQUEST_TYPES)
date_from = models.DateField(_('Дата с'), null=True, blank=True)
date_to = models.DateField(_('Дата по'), null=True, blank=True)
note = models.TextField(_('Комментарий'), blank=True)
substitute = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='hr_substitution_requests',
verbose_name=_('Кто подменяет'),
help_text=_('Заполняется, когда сотрудник берёт открытый запрос на подмену'),
)
status = models.CharField(
_('Статус'), max_length=16, choices=STATUS_CHOICES, default='pending',
)
decided_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='hr_requests_decided',
verbose_name=_('Решение принял'),
)
decided_at = models.DateTimeField(_('Дата решения'), null=True, blank=True)
decision_note = models.TextField(_('Комментарий HR'), blank=True)
class Meta:
verbose_name = _('Кадровая заявка')
verbose_name_plural = _('Кадровые заявки')
ordering = ['-created_at']
indexes = [
models.Index(fields=['employee', 'status']),
models.Index(fields=['status', 'request_type']),
]
def __str__(self):
return f'{self.employee} — {self.get_request_type_display()} ({self.get_status_display()})'
@property
def day_count(self) -> int:
if not self.date_from or not self.date_to:
return 0
return (self.date_to - self.date_from).days + 1
@property
def summary(self) -> str:
label = self.get_request_type_display()
if self.request_type == 'substitution' and self.substitute_id:
who = self.substitute.get_full_name() or self.substitute.username
label = f'{label} ({who})'
if self.date_from and self.date_to:
if self.date_from == self.date_to:
return f'{label} {self.date_from.strftime("%d.%m.%Y")}'
return (
f'{label} {self.date_from.strftime("%d.%m.%Y")}'
f' – {self.date_to.strftime("%d.%m.%Y")}'
)
return label
@property
def pharmacy_label(self) -> str:
"""Краткое название аптеки заявителя (код/организация №)."""
pharmacy = getattr(self.employee, 'user_pharmacy', None) if self.employee_id else None
if pharmacy is None:
return ''
from apps.locations.models.base import pharmacy_object_code
code = pharmacy_object_code(pharmacy)
org = ''
if pharmacy.organization_id and pharmacy.organization:
org = pharmacy.organization.name or ''
if code and org:
return f'{code} ({org})'
if code:
return code
if pharmacy.pharmacy_number is not None:
head = f'Аптека №{pharmacy.pharmacy_number}'
return f'{org} {head}'.strip() if org else head
return str(pharmacy)
@property
def pharmacy_address(self) -> str:
pharmacy = getattr(self.employee, 'user_pharmacy', None) if self.employee_id else None
if pharmacy is None:
return ''
return (pharmacy.get_full_address() or pharmacy.address or '').strip()
@property
def pharmacy_place(self) -> str:
"""Аптека + адрес одной строкой для списков."""
label = self.pharmacy_label
addr = self.pharmacy_address
if label and addr:
return f'{label} — {addr}'
return label or addr
@property
def status_label(self) -> str:
if self.request_type == 'substitution' and self.status == 'pending':
if self.substitute_id:
return str(_('Ожидает подтверждения'))
return str(_('Ищет подмену'))
return self.get_status_display()
def clean(self):
super().clean()
if self.request_type in self.DATE_REQUIRED_TYPES:
if not self.date_from or not self.date_to:
raise ValidationError(_('Укажите даты начала и окончания.'))
if self.date_to < self.date_from:
raise ValidationError(_('Дата окончания не может быть раньше начала.'))
elif self.date_from and self.date_to and self.date_to < self.date_from:
raise ValidationError(_('Дата окончания не может быть раньше начала.'))
if self.request_type == 'substitution' and self.substitute_id:
if self.substitute_id == self.employee_id:
raise ValidationError({'substitute': _('Нельзя указать себя для подмены.')})