5a274f7f90
При створенні 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>
120 lines
4.9 KiB
Python
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()
|