hiring: автоназначение ролей по типу локации

При утверждении найма сотрудник получает роли (группы) по умолчанию для
своего типа локации (Аптека/Офис/Склад). Наборы настраиваются в
EmployeeDefaults (админка «Настройки сотрудников по умолчанию»).

- EmployeeDefaults: 3 M2M default_groups_* + apply_default_groups();
- approve_hiring_request вызывает apply_default_groups после создания Employee;
- админка: блок «Роли по умолчанию при найме» (filter_horizontal);
- миграция 0023; тесты найма.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 16:16:22 +03:00
parent cfafe2f0b3
commit 3ec5fb58c4
5 changed files with 101 additions and 0 deletions
+12
View File
@@ -414,6 +414,9 @@ class EmployeeAdmin(ModelAdmin):
class EmployeeDefaultsAdmin(ModelAdmin):
"""Единственная запись настроек по умолчанию для сотрудников."""
filter_horizontal = (
'default_groups_office', 'default_groups_pharmacy', 'default_groups_warehouse',
)
fieldsets = (
(_('Офисные сотрудники'), {
'fields': ('office_email_domain',),
@@ -422,6 +425,15 @@ class EmployeeDefaultsAdmin(ModelAdmin):
'назначается рабочий email: логин@домен.'
),
}),
(_('Роли по умолчанию при найме'), {
'fields': (
'default_groups_office', 'default_groups_pharmacy', 'default_groups_warehouse',
),
'description': _(
'Эти роли (группы) автоматически выдаются новому сотруднику '
'при утверждении найма — по типу его локации.'
),
}),
)
def has_add_permission(self, request):
@@ -0,0 +1,29 @@
# Generated by Django 6.0.3 on 2026-09-21 13:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('employees', '0022_employee_it_responsibility_zone'),
]
operations = [
migrations.AddField(
model_name='employeedefaults',
name='default_groups_office',
field=models.ManyToManyField(blank=True, related_name='+', to='auth.group', verbose_name='Роли по умолчанию — Офис'),
),
migrations.AddField(
model_name='employeedefaults',
name='default_groups_pharmacy',
field=models.ManyToManyField(blank=True, related_name='+', to='auth.group', verbose_name='Роли по умолчанию — Аптека'),
),
migrations.AddField(
model_name='employeedefaults',
name='default_groups_warehouse',
field=models.ManyToManyField(blank=True, related_name='+', to='auth.group', verbose_name='Роли по умолчанию — Склад'),
),
]
+36
View File
@@ -19,6 +19,20 @@ class EmployeeDefaults(models.Model):
),
)
# Роли (группы), выдаваемые новому сотруднику по типу его локации.
default_groups_office = models.ManyToManyField(
'auth.Group', blank=True, related_name='+',
verbose_name=_('Роли по умолчанию — Офис'),
)
default_groups_pharmacy = models.ManyToManyField(
'auth.Group', blank=True, related_name='+',
verbose_name=_('Роли по умолчанию — Аптека'),
)
default_groups_warehouse = models.ManyToManyField(
'auth.Group', blank=True, related_name='+',
verbose_name=_('Роли по умолчанию — Склад'),
)
class Meta:
verbose_name = _('Настройки сотрудников по умолчанию')
verbose_name_plural = _('Настройки сотрудников по умолчанию')
@@ -41,3 +55,25 @@ class EmployeeDefaults(models.Model):
@property
def normalized_office_domain(self) -> str:
return (self.office_email_domain or 'aks-apteka.com.ua').strip().lstrip('@').lower()
def groups_for(self, location_type: str):
"""M2M-менеджер ролей по умолчанию для типа локации (или None)."""
return {
'office': self.default_groups_office,
'pharmacy': self.default_groups_pharmacy,
'warehouse': self.default_groups_warehouse,
}.get(location_type)
def apply_default_groups(self, employee) -> list:
"""Добавить сотруднику роли по умолчанию для его типа локации.
add() (не set) — не затирает уже назначенные роли. Возвращает
список добавленных групп (для лога/сообщений).
"""
mgr = self.groups_for(employee.user_location_type)
if mgr is None:
return []
groups = list(mgr.all())
if groups:
employee.groups.add(*groups)
return groups
+4
View File
@@ -249,6 +249,10 @@ def approve_hiring_request(hiring_request, decided_by, note=''):
employee.full_clean()
employee.save()
# Роли по умолчанию по типу локации (Аптека/Офис/Склад).
from apps.employees.models import EmployeeDefaults
EmployeeDefaults.load().apply_default_groups(employee)
from apps.employees.services.password_invite import create_password_invite
invite = create_password_invite(employee, created_by=decided_by)
+20
View File
@@ -142,6 +142,26 @@ class HiringServiceTests(TestCase):
create_hiring_request(**self._office_kwargs(user_location_id=None))
self.assertIn('user_location', cm.exception.message_dict)
def test_approve_assigns_default_groups_by_location(self):
from django.contrib.auth.models import Group
from apps.employees.models import EmployeeDefaults
role = Group.objects.create(name='Аптека-базовая')
EmployeeDefaults.load().default_groups_pharmacy.add(role)
hr = create_hiring_request(**self._pharmacy_kwargs())
approved = approve_hiring_request(hr, decided_by=self.hr_user)
self.assertIn(role, approved.employee.groups.all())
def test_approve_does_not_assign_other_location_groups(self):
from django.contrib.auth.models import Group
from apps.employees.models import EmployeeDefaults
pharm_role = Group.objects.create(name='Аптека-роль')
EmployeeDefaults.load().default_groups_pharmacy.add(pharm_role)
hr = create_hiring_request(**self._office_kwargs())
approved = approve_hiring_request(hr, decided_by=self.hr_user)
self.assertNotIn(pharm_role, approved.employee.groups.all())
def test_is_overdue_when_trial_period_passed(self):
hr = create_hiring_request(**self._pharmacy_kwargs(
last_name='Прострочений', first_name='Тест',