Files
ACORE/apps/hiring/models.py
T
lokses 5a274f7f90 Найм: авто-задача IT на доступи з чек-листом + сповіщення постановника
При створенні HR-заявки з обраними базами/доступами тепер одразу ставиться
задача IT-відділу (замість тікета на затвердженні):

- TaskChecklistItem: чек-лист задачі, виконавець відмічає зроблене
  (task-detail: чекбокси, лічильник виконаного, хто/коли відмітив)
- create_hiring_request → _create_access_task: задача з чек-листом по базах,
  creator = HR (постановник), co_executors = IT-група (тригерить сповіщення
  ботом Ричард), priority=high, due_date рахується за робочим часом
- add_working_hours (visit_manager/services): дедлайн за робочим вікном
  09:00–18:00 з урахуванням вихідних і свят (Holiday)
- Постановнику (HR) бот пише, що заявку взято в роботу, строк — 24 год
  робочого часу (до розрахованої дати)
- approve більше не створює дублюючий IT-тікет (доступи вже задачею)
- HiringRequest.access_task; чек-лист в адмінці задач (inline)
- Тести: hiring (задача+чек-лист, approve без тікета), робочі години
  (вихідні/свята), відмітка чек-листа через в'ю

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 01:01:30 +03:00

120 lines
4.9 KiB
Python

from datetime import timedelta
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import BaseModel
from core.utils.validators import validate_ukrainian_phone
class HiringRequest(BaseModel):
"""Заявка HR на найм кандидата: створення тимчасового (стажерського)
облікового запису та подальше затвердження/відхилення."""
STATUS_CHOICES = [
('trial', _('На випробуванні')),
('approved', _('Затверджено')),
('rejected', _('Відхилено')),
]
TRIAL_DAYS_CHOICES = [(i, _('%(n)d дн.') % {'n': i}) for i in range(1, 6)]
last_name = models.CharField(_('Прізвище'), max_length=150)
first_name = models.CharField(_('Ім\'я'), max_length=150)
middle_name = models.CharField(_('По батькові'), max_length=150, blank=True)
candidate_email = models.EmailField(_('Особистий email кандидата'), blank=True)
candidate_phone = models.CharField(
_('Телефон кандидата'), max_length=20, blank=True,
validators=[validate_ukrainian_phone],
)
LOCATION_TYPES = [
('pharmacy', _('Аптека')),
('warehouse', _('Склад')),
('office', _('Офіс')),
]
user_location_type = models.CharField(
_('Тип локації'), max_length=20, choices=LOCATION_TYPES,
)
department = models.ForeignKey(
'core.Directory', on_delete=models.SET_NULL,
null=True, blank=True,
limit_choices_to={'directory_type__code': 'department', 'is_active': True},
related_name='hiring_requests_department', verbose_name=_('Відділ'),
)
user_pharmacy = models.ForeignKey(
'locations.Pharmacy', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='hiring_requests', verbose_name=_('Аптека'),
)
post = models.ForeignKey(
'core.Directory', on_delete=models.SET_NULL,
null=True, blank=True,
limit_choices_to={'directory_type__code': 'post', 'is_active': True},
related_name='hiring_requests_post', verbose_name=_('Посада'),
)
hire_date = models.DateField(_('Дата виходу'), default=timezone.localdate)
trial_days = models.PositiveSmallIntegerField(
_('Термін випробування, днів'), choices=TRIAL_DAYS_CHOICES, default=3,
validators=[MinValueValidator(1), MaxValueValidator(5)],
)
requested_access = models.ManyToManyField(
'core.Directory', blank=True,
limit_choices_to={'directory_type__code': 'access_system', 'is_active': True},
related_name='hiring_requests_access', verbose_name=_('Потрібні доступи'),
)
access_note = models.TextField(_('Примітка щодо доступів'), blank=True)
status = models.CharField(_('Статус'), max_length=10, choices=STATUS_CHOICES, default='trial')
requested_by = models.ForeignKey(
'employees.Employee', on_delete=models.PROTECT,
related_name='hiring_requests_submitted', verbose_name=_('Подав HR'),
)
employee = models.OneToOneField(
'employees.Employee', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='hiring_request', verbose_name=_('Обліковий запис'),
)
decision_by = models.ForeignKey(
'employees.Employee', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='hiring_requests_decided', verbose_name=_('Рішення прийняв'),
)
decision_at = models.DateTimeField(_('Дата рішення'), null=True, blank=True)
decision_note = models.TextField(_('Коментар до рішення'), blank=True)
granted_ticket = models.ForeignKey(
'it_manager.Ticket', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='hiring_requests', verbose_name=_('Заявка в IT'),
)
access_task = models.ForeignKey(
'tasks.Task', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='+', verbose_name=_('Задача на видачу доступів'),
)
class Meta:
verbose_name = _('Заявка на найм')
verbose_name_plural = _('Заявки на найм')
ordering = ['-created_at']
indexes = [
models.Index(fields=['status']),
]
def __str__(self):
return f'{self.last_name} {self.first_name} ({self.get_status_display()})'
@property
def trial_end_date(self):
return self.hire_date + timedelta(days=self.trial_days)
@property
def is_overdue(self):
return self.status == 'trial' and self.trial_end_date < timezone.localdate()