Files
ACORE/apps/employees/models/base.py
T
lokses 6ff01c4119 Доменные модули: сотрудники, локации, найм, счётчики, посещения
- employees: last_seen, internal_number, default_page_size; управление-команды
- locations: категория/куратор/ответственный аптеки, seed должностей;
  Location.__str__ показывает номер аптеки (не только адрес)
- hiring: HiringRequest (заявки на найм) + тесты (7)
- utilities: UtilityReading (показания счётчиков по аптекам)
- visit_manager: правки

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:33:16 +03:00

312 lines
14 KiB
Python

from django.db import models
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from core.models import BaseModel
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from core.utils.validators import validate_ukrainian_phone, validate_birth_date, validate_ukrainian_tax_number
from core.utils.pagination import PAGE_SIZE_CHOICES, DEFAULT_PAGE_SIZE
class EmployeeManager(BaseUserManager):
def create_user(self, email=None, password=None, **extra_fields):
if not email:
username = extra_fields.get('username')
if username:
email = f"{username}@local.domain"
else:
raise ValueError('Необходимо указать email или username')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active', True)
return self.create_user(email, password, **extra_fields)
class Employee(AbstractBaseUser, PermissionsMixin, BaseModel):
username = models.CharField(
_('Логин'), max_length=150, unique=True,
help_text=_('Уникальный логин для входа в систему')
)
email = models.EmailField(
_('Email адрес'), unique=True, blank=True, null=True,
help_text=_('Рабочий email. Если не указан — генерируется из username@local.domain')
)
last_name = models.CharField(_('Фамилия'), max_length=150)
first_name = models.CharField(_('Имя'), max_length=150)
middle_name = models.CharField(_('Отчество'), max_length=150, blank=True)
is_staff = models.BooleanField(_('Доступ к панели администратора'), default=False,
help_text=_('Разрешает вход в Django admin. Только для IT-администраторов системы.'))
is_active = models.BooleanField(_('Активный'), default=True,
help_text=_('Работает ли сотрудник сейчас'))
date_joined = models.DateTimeField(_('Дата создания аккаунта'), default=timezone.now)
birth_date = models.DateField(
_('Дата рождения'), null=True, blank=True,
validators=[validate_birth_date]
)
inn_code = models.CharField(
_('ИНН'), max_length=10, blank=True, null=True,
help_text=_('Идентификационный номер налогоплательщика'),
validators=[validate_ukrainian_tax_number]
)
phone = models.CharField(
_('Рабочий телефон'), max_length=20, blank=True,
validators=[validate_ukrainian_phone]
)
internal_number = models.CharField(
_('Внутрішній номер'), max_length=10, blank=True,
help_text=_('Короткий внутрішній (додатковий) номер АТС')
)
personal_phone = models.CharField(
_('Личный телефон'), max_length=20, blank=True,
validators=[validate_ukrainian_phone]
)
personal_email = models.EmailField(_('Личный email'), blank=True)
emergency_contact_name = models.CharField(_('Контакт в экстренных случаях'), max_length=200, blank=True)
emergency_contact_phone = models.CharField(_('Телефон экстренного контакта'), max_length=20, blank=True,
validators=[validate_ukrainian_phone])
emergency_contact_relation = models.CharField(_('Степень родства'), max_length=100, blank=True)
LOCATION_TYPES = [
('pharmacy', _('Аптека')),
('warehouse', _('Склад')),
('office', _('Офис')),
]
user_location_type = models.CharField(
_('Тип локации'), max_length=20, choices=LOCATION_TYPES,
help_text=_('Где работает: аптека / склад / офис'),
null=True, blank=True
)
user_organization = models.ForeignKey(
'core.Directory', on_delete=models.SET_NULL,
null=True, blank=True,
limit_choices_to={'directory_type__code': 'organization', 'is_active': True},
related_name='organization_employees', verbose_name=_('Организация'),
help_text=_('Юрлицо: АКС, АКС Одесса, Тира, Пентальфа...')
)
user_pharmacy = models.ForeignKey(
'locations.Pharmacy', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='pharmacy_employees', verbose_name=_('Аптека'),
help_text=_('Заполняется только для location_type=pharmacy')
)
user_location = models.ForeignKey(
'locations.Location', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='location_employees', verbose_name=_('Локация (офис/склад)'),
help_text=_('Заполняется для location_type=office или warehouse')
)
department = models.ForeignKey(
'core.Directory', on_delete=models.SET_NULL,
limit_choices_to={'directory_type__code': 'department', 'is_active': True},
related_name='department_employees', verbose_name=_('Отдел'),
null=True, blank=True,
help_text=_('Заполняется для office / warehouse')
)
post = models.ForeignKey(
'core.Directory', on_delete=models.SET_NULL,
limit_choices_to={'directory_type__code': 'post', 'is_active': True},
related_name='post_employees', verbose_name=_('Должность'),
null=True, blank=True
)
manager = models.ForeignKey(
'self', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='subordinates', verbose_name=_('Руководитель')
)
hire_date = models.DateField(_('Дата приёма на работу'))
termination_date = models.DateField(_('Дата увольнения'), null=True, blank=True)
probation_end_date = models.DateField(_('Конец испытательного срока'), null=True, blank=True)
photo = models.ImageField(
_('Фото'), upload_to='employees/photos/%Y/%m/',
blank=True, null=True
)
mattermost_user_id = models.CharField(_('ID Mattermost'), max_length=50, blank=True, null=True)
mattermost_username = models.CharField(_('Логин Mattermost'), max_length=50, blank=True, null=True)
mattermost_sync_status = models.CharField(
_('Статус синхронизации Mattermost'), max_length=20,
choices=[
('pending', _('Ожидает')),
('synced', _('Синхронизирован')),
('error', _('Ошибка')),
('disabled', _('Отключена')),
],
default='pending'
)
mattermost_last_sync = models.DateTimeField(_('Последняя синхронизация Mattermost'), null=True, blank=True)
mattermost_sync_enabled = models.BooleanField(_('Синхронизация Mattermost'), default=True)
ad_user_dn = models.CharField('AD User DN', max_length=500, blank=True,
help_text='CN=Ivanov Ivan,OU=Users,DC=company,DC=local')
ad_sync_status = models.CharField(
_('Статус синхронизации AD'), max_length=20,
choices=[
('not_synced', _('Не синхронизирован')),
('synced', _('Синхронизирован')),
('error', _('Ошибка')),
('disabled', _('Отключена')),
],
default='not_synced'
)
ad_sync_error = models.TextField(_('Ошибка синхронизации AD'), blank=True)
ad_last_sync = models.DateTimeField(_('Последняя синхронизация AD'), null=True, blank=True)
ad_sync_enabled = models.BooleanField(_('Синхронизация AD'), default=False,
help_text=_('Только для офисных сотрудников'))
default_page_size = models.PositiveSmallIntegerField(
_('Записей на странице'), choices=[(n, str(n)) for n in PAGE_SIZE_CHOICES],
default=DEFAULT_PAGE_SIZE,
help_text=_('Сколько строк показывать в таблицах портала по умолчанию'),
)
last_seen = models.DateTimeField(
_('Был онлайн'), null=True, blank=True,
help_text=_('Обновляется автоматически при активности в портале (не в AD/Mattermost)'),
)
objects = EmployeeManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['first_name', 'last_name', 'hire_date']
class Meta:
verbose_name = _('Сотрудник')
verbose_name_plural = _('Сотрудники')
indexes = [
models.Index(fields=['last_name', 'first_name', 'middle_name']),
models.Index(fields=['email']),
models.Index(fields=['is_active']),
]
def __str__(self):
if self.middle_name:
return f"{self.last_name} {self.first_name} {self.middle_name}"
return f"{self.last_name} {self.first_name}"
def save(self, *args, **kwargs):
if not self.email and self.username:
self.email = f"{self.username}@local.domain"
super().save(*args, **kwargs)
def clean(self):
super().clean()
if self.user_location_type:
loc = self.user_location_type
if loc == 'pharmacy':
if not self.user_pharmacy_id:
raise ValidationError({'user_pharmacy': 'Для фармацевта обязательно укажите аптеку'})
if self.department_id:
raise ValidationError({'department': 'Для фармацевта отдел не указывается'})
if self.user_location_id:
raise ValidationError({'user_location': 'Локация (офис/склад) — только для office/warehouse'})
elif loc in ['office', 'warehouse']:
if not self.department_id:
raise ValidationError({'department': f'Для сотрудника {loc} обязателен отдел'})
if not self.user_location_id:
raise ValidationError({'user_location': 'Укажите конкретную локацию (офис или склад)'})
if self.user_location_id and self.user_location.location_type != loc:
raise ValidationError({'user_location': f'Локация должна быть типа «{self.get_user_location_type_display()}»'})
if self.user_pharmacy_id:
raise ValidationError({'user_pharmacy': 'Аптека только для фармацевтов'})
if self.post_id and self.user_location_type:
if self.post.location_types.exists():
if not self.post.location_types.filter(code=self.user_location_type).exists():
names = ', '.join(lt.name for lt in self.post.location_types.all())
raise ValidationError({'post': f'Должность доступна только для: {names}'})
if self.termination_date and self.hire_date:
if self.termination_date < self.hire_date:
raise ValidationError({'termination_date': 'Не может быть раньше даты приёма'})
if self.manager_id and self.manager_id == self.pk:
raise ValidationError({'manager': 'Сотрудник не может быть своим руководителем'})
@property
def short_name(self):
s = self.last_name
if self.first_name:
s += f" {self.first_name[0]}."
if self.middle_name:
s += f"{self.middle_name[0]}."
return s
@property
def age(self):
if not self.birth_date:
return None
today = timezone.now().date()
age = today.year - self.birth_date.year
if today < self.birth_date.replace(year=today.year):
age -= 1
return age
@property
def work_experience(self):
if not self.hire_date:
return 0
end = self.termination_date or timezone.now().date()
exp = end.year - self.hire_date.year
if end < self.hire_date.replace(year=end.year):
exp -= 1
return max(0, exp)
@property
def is_on_probation(self):
if not self.probation_end_date:
return False
return timezone.now().date() <= self.probation_end_date
@property
def is_manager(self):
return self.subordinates.exists()
@property
def subordinate_count(self):
return self.subordinates.filter(is_active=True).count()
def get_manager_chain(self):
managers = []
current = self.manager
while current and len(managers) < 10:
managers.append(current)
current = current.manager
return managers
def get_full_name(self):
return self.__str__()
def get_short_name(self):
return self.short_name
def has_perm(self, perm, obj=None):
today = timezone.now().date()
codename = perm.split('.')[-1]
override = self.permission_overrides.filter(
permission__codename=codename,
is_active=True,
).filter(
models.Q(valid_from__isnull=True) | models.Q(valid_from__lte=today)
).filter(
models.Q(valid_until__isnull=True) | models.Q(valid_until__gte=today)
).values_list('override_type', flat=True).first()
if override == 'revoke':
return False
if override == 'grant':
return True
return super().has_perm(perm, obj)