From 3ec5fb58c4a3812ceb1981c1c773a0f520a3ffd1 Mon Sep 17 00:00:00 2001 From: lokses Date: Mon, 21 Sep 2026 16:16:22 +0300 Subject: [PATCH] =?UTF-8?q?hiring:=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=20=D0=BF=D0=BE=20=D1=82=D0=B8=D0=BF=D1=83=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При утверждении найма сотрудник получает роли (группы) по умолчанию для своего типа локации (Аптека/Офис/Склад). Наборы настраиваются в 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 --- apps/employees/admin.py | 12 +++++++ ...defaults_default_groups_office_and_more.py | 29 +++++++++++++++ apps/employees/models/defaults.py | 36 +++++++++++++++++++ apps/hiring/services.py | 4 +++ apps/hiring/tests.py | 20 +++++++++++ 5 files changed, 101 insertions(+) create mode 100644 apps/employees/migrations/0023_employeedefaults_default_groups_office_and_more.py diff --git a/apps/employees/admin.py b/apps/employees/admin.py index 0c0a0ab..0c7ec33 100644 --- a/apps/employees/admin.py +++ b/apps/employees/admin.py @@ -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): diff --git a/apps/employees/migrations/0023_employeedefaults_default_groups_office_and_more.py b/apps/employees/migrations/0023_employeedefaults_default_groups_office_and_more.py new file mode 100644 index 0000000..89858d9 --- /dev/null +++ b/apps/employees/migrations/0023_employeedefaults_default_groups_office_and_more.py @@ -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='Роли по умолчанию — Склад'), + ), + ] diff --git a/apps/employees/models/defaults.py b/apps/employees/models/defaults.py index 1c8aa6e..3d9a22d 100644 --- a/apps/employees/models/defaults.py +++ b/apps/employees/models/defaults.py @@ -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 diff --git a/apps/hiring/services.py b/apps/hiring/services.py index 82451a0..53dd7e1 100644 --- a/apps/hiring/services.py +++ b/apps/hiring/services.py @@ -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) diff --git a/apps/hiring/tests.py b/apps/hiring/tests.py index 731dcfd..f5aed1c 100644 --- a/apps/hiring/tests.py +++ b/apps/hiring/tests.py @@ -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='Тест',