From 7a149dd84d9406b8efea1a4fdc497c9fc5ae1769 Mon Sep 17 00:00:00 2001 From: lokses Date: Tue, 6 Oct 2026 17:22:41 +0300 Subject: [PATCH] =?UTF-8?q?portal:=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=D1=8B?= =?UTF-8?q?=20PNMS/PVMS-=D0=BF=D1=80=D0=B0=D0=B2=20=D0=B2=20=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D1=8F=D1=85=20=D0=B8=20seed-=D0=BC=D0=B8=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вынесены can_* для кассы и посещаемости, синхронизация в builders/signals, seed групп в core.0023–0024. Co-authored-by: Cursor --- apps/employees/admin.py | 66 ++++++++- apps/portal_connector/builders.py | 20 +-- apps/portal_connector/pos_permissions.py | 65 +++++++++ apps/portal_connector/pvms_builders.py | 20 +-- apps/portal_connector/pvms_permissions.py | 69 +++++++++ apps/portal_connector/signals.py | 26 +++- apps/portal_connector/tests.py | 56 ++++++++ .../0023_seed_pnms_pos_permissions.py | 132 ++++++++++++++++++ .../0024_seed_pvms_visit_permissions.py | 105 ++++++++++++++ 9 files changed, 522 insertions(+), 37 deletions(-) create mode 100644 apps/portal_connector/pos_permissions.py create mode 100644 apps/portal_connector/pvms_permissions.py create mode 100644 core/migrations/0023_seed_pnms_pos_permissions.py create mode 100644 core/migrations/0024_seed_pvms_visit_permissions.py diff --git a/apps/employees/admin.py b/apps/employees/admin.py index f9efa08..53cf998 100644 --- a/apps/employees/admin.py +++ b/apps/employees/admin.py @@ -29,6 +29,31 @@ def _portal_permissions_qs(): return (access | extra).order_by('content_type__app_label', 'codename') +def _pnms_permissions_qs(): + from apps.portal_connector.pos_permissions import ( + PNMS_ACCESS_APP_LABEL, + PNMS_ACCESS_MODEL, + POS_PERMISSION_CODENAMES, + ) + return Permission.objects.filter( + content_type__app_label=PNMS_ACCESS_APP_LABEL, + content_type__model=PNMS_ACCESS_MODEL, + codename__in=POS_PERMISSION_CODENAMES, + ).order_by('codename') + + +def _pvms_permissions_qs(): + from apps.portal_connector.pvms_permissions import ( + PVMS_ACCESS_APP_LABEL, + PVMS_ACCESS_MODEL, + VISIT_PERMISSION_CODENAMES, + ) + return Permission.objects.filter( + content_type__app_label=PVMS_ACCESS_APP_LABEL, + content_type__model=PVMS_ACCESS_MODEL, + codename__in=VISIT_PERMISSION_CODENAMES, + ).order_by('codename') + class HierarchicalDepartmentChoiceField(forms.ModelChoiceField): """Поле выбора отдела с отступами по иерархии в выпадающем списке.""" @@ -608,7 +633,7 @@ class EmployeeDefaultsAdmin(ModelAdmin): class GroupAdminForm(forms.ModelForm): - """Единая форма роли: имя + портальный доступ + прочие права + профиль.""" + """Единая форма роли: портал + PNMS/касса + PVMS + прочие права + профиль.""" description = forms.CharField( label=_('Описание'), required=False, widget=forms.Textarea(attrs={'rows': 2}), @@ -619,7 +644,21 @@ class GroupAdminForm(forms.ModelForm): queryset=Permission.objects.none(), required=False, widget=admin.widgets.FilteredSelectMultiple(_('разделы'), is_stacked=False), - help_text=_('Доступ к разделам портала. Прочие права (кассовые и т.п.) — в списке «Права».'), + help_text=_('Доступ к разделам портала ACORE.'), + ) + pnms_permissions = forms.ModelMultipleChoiceField( + label=_('PNMS / касса (PPOS)'), + queryset=Permission.objects.none(), + required=False, + widget=admin.widgets.FilteredSelectMultiple(_('права кассы'), is_stacked=False), + help_text=_('POS can_* — синхронизируются в PNMS → PPOS как pos_permissions.'), + ) + pvms_permissions = forms.ModelMultipleChoiceField( + label=_('PVMS / посещаемость'), + queryset=Permission.objects.none(), + required=False, + widget=admin.widgets.FilteredSelectMultiple(_('права посещаемости'), is_stacked=False), + help_text=_('Visit can_* — синхронизируются в PVMS как visit_permissions.'), ) class Meta: @@ -629,11 +668,18 @@ class GroupAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) portal_qs = _portal_permissions_qs() + pnms_qs = _pnms_permissions_qs() + pvms_qs = _pvms_permissions_qs() self._portal_ids = set(portal_qs.values_list('pk', flat=True)) + self._pnms_ids = set(pnms_qs.values_list('pk', flat=True)) + self._pvms_ids = set(pvms_qs.values_list('pk', flat=True)) self.fields['portal_permissions'].queryset = portal_qs - # Портальные права не дублируем в общем списке — ими управляет виджет выше. + self.fields['pnms_permissions'].queryset = pnms_qs + self.fields['pvms_permissions'].queryset = pvms_qs + # Портал / PNMS / PVMS не дублируем в общем списке — отдельные виджеты. + managed_ids = self._portal_ids | self._pnms_ids | self._pvms_ids self.fields['permissions'].queryset = ( - self.fields['permissions'].queryset.exclude(pk__in=self._portal_ids) + self.fields['permissions'].queryset.exclude(pk__in=managed_ids) ) if self.instance and self.instance.pk: profile = getattr(self.instance, 'profile', None) @@ -643,11 +689,19 @@ class GroupAdminForm(forms.ModelForm): self.fields['portal_permissions'].initial = list( self.instance.permissions.filter(pk__in=self._portal_ids) ) + self.fields['pnms_permissions'].initial = list( + self.instance.permissions.filter(pk__in=self._pnms_ids) + ) + self.fields['pvms_permissions'].initial = list( + self.instance.permissions.filter(pk__in=self._pvms_ids) + ) def _save_m2m(self): - # set() для permissions снимет портальные (их нет в queryset) — вернём по виджету. + # set() для permissions снимет managed-права (их нет в queryset) — вернём по виджетам. super()._save_m2m() self.instance.permissions.add(*(self.cleaned_data.get('portal_permissions') or [])) + self.instance.permissions.add(*(self.cleaned_data.get('pnms_permissions') or [])) + self.instance.permissions.add(*(self.cleaned_data.get('pvms_permissions') or [])) PermissionGroup.objects.update_or_create( group=self.instance, defaults={ @@ -704,6 +758,8 @@ class GroupAdmin(ModelAdmin): fieldsets = ( (None, {'fields': ('name', 'description', 'is_active')}), (_('Доступ к разделам портала'), {'fields': ('portal_permissions',)}), + (_('PNMS / касса (PPOS)'), {'fields': ('pnms_permissions',)}), + (_('PVMS / посещаемость'), {'fields': ('pvms_permissions',)}), (_('Прочие права'), {'fields': ('permissions',), 'classes': ('collapse',)}), ) diff --git a/apps/portal_connector/builders.py b/apps/portal_connector/builders.py index 28e7b6c..d286980 100644 --- a/apps/portal_connector/builders.py +++ b/apps/portal_connector/builders.py @@ -6,24 +6,12 @@ from typing import Any from django.core.exceptions import ObjectDoesNotExist +from apps.portal_connector.pos_permissions import POS_PERMISSION_CODENAMES + logger = logging.getLogger(__name__) -# Must match PNMS users.models.PERMISSION_CODES / ACORE POS can_* -POS_PERMISSION_CODENAMES = frozenset({ - 'can_deactivate_receipt', - 'can_create_return', - 'can_apply_manual_discount', - 'can_change_retail_price', - 'can_view_purchase_price', - 'can_close_session', - 'can_manage_transfers', - 'can_do_inventory', - 'can_receive_delivery', - 'can_ship_transfer', - 'can_view_reports', - 'can_view_all_receipts', - 'can_manage_employees', -}) +# Re-export for callers/tests +__all__ = ['POS_PERMISSION_CODENAMES', 'build_pharmacy_payload', 'build_employee_payload'] def _region_code(pharmacy) -> tuple[str, str, str]: diff --git a/apps/portal_connector/pos_permissions.py b/apps/portal_connector/pos_permissions.py new file mode 100644 index 0000000..b606851 --- /dev/null +++ b/apps/portal_connector/pos_permissions.py @@ -0,0 +1,65 @@ +"""POS / PNMS permission codes — shared by builders, admin, and seed migrations. + +Must stay in sync with PNMS users.models.PERMISSION_CHOICES. +""" +from __future__ import annotations + +PNMS_ACCESS_APP_LABEL = 'core' +PNMS_ACCESS_MODEL = 'pnmsaccess' + +# (codename, human name) — Russian labels for Django admin (LANGUAGE_CODE=ru) +PNMS_PERMISSION_CHOICES: list[tuple[str, str]] = [ + ('can_deactivate_receipt', 'Анулювання чека'), + ('can_create_return', 'Повернення по чеку'), + ('can_apply_manual_discount', 'Ручна знижка на касі'), + ('can_change_retail_price', 'Зміна роздрібної ціни'), + ('can_view_purchase_price', 'Перегляд закупівельної ціни'), + ('can_close_session', 'Закриття касової зміни'), + ('can_manage_transfers', 'Робота з накладними (приймання товару)'), + ('can_do_inventory', 'Проведення інвентаризації'), + ('can_receive_delivery', 'Приёмка от поставщика'), + ('can_ship_transfer', 'Отгрузка накладной со склада'), + ('can_view_reports', 'Перегляд звітів касової зміни'), + ('can_view_all_receipts', 'Перегляд усіх чеків зміни (не тільки своїх)'), + ('can_manage_employees', 'Управління співробітниками аптеки'), +] + +POS_PERMISSION_CODENAMES = frozenset(code for code, _ in PNMS_PERMISSION_CHOICES) + +# Default grants by ACORE auth.Group name (canonical) +GROUP_PNMS_PERMS: dict[str, list[str]] = { + 'Администратор сети': [c for c, _ in PNMS_PERMISSION_CHOICES], + 'Ревизор': [ + 'can_deactivate_receipt', + 'can_create_return', + 'can_do_inventory', + 'can_view_reports', + 'can_view_all_receipts', + 'can_view_purchase_price', + ], + 'Заведующий аптекой': [ + 'can_close_session', + 'can_manage_transfers', + 'can_do_inventory', + 'can_view_reports', + 'can_view_all_receipts', + 'can_manage_employees', + 'can_apply_manual_discount', + 'can_change_retail_price', + 'can_view_purchase_price', + 'can_create_return', + ], + 'Фармацевт': [ + 'can_close_session', + 'can_view_reports', + ], +} + +CANONICAL_ROLE_GROUPS = ( + 'Администратор сети', + 'Ревизор', + 'Фармацевт', + 'Заведующий аптекой', + 'Офисный сотрудник', + 'Сотрудник склада', +) diff --git a/apps/portal_connector/pvms_builders.py b/apps/portal_connector/pvms_builders.py index 75312a2..eada8cd 100644 --- a/apps/portal_connector/pvms_builders.py +++ b/apps/portal_connector/pvms_builders.py @@ -7,13 +7,12 @@ from typing import Any from django.core.exceptions import ObjectDoesNotExist -logger = logging.getLogger(__name__) +from apps.portal_connector.pvms_permissions import ( + VISIT_PERMISSION_CODENAMES, + role_from_acore_groups, +) -VISIT_PERMISSION_CODENAMES = frozenset({ - 'can_view_attendance', - 'can_manage_schedules', - 'can_register_others', -}) +logger = logging.getLogger(__name__) def _visit_permissions(employee) -> list[str]: @@ -43,9 +42,11 @@ def _visit_permissions(employee) -> list[str]: def _employee_role(employee) -> str: - if employee.is_superuser or employee.is_staff: - return 'admin' - return 'employee' + group_names = list(employee.groups.values_list('name', flat=True)) + return role_from_acore_groups( + group_names, + is_staff=bool(employee.is_superuser or employee.is_staff), + ) def build_pvms_employee_payload(employee, *, include_password: bool = False) -> dict[str, Any]: @@ -56,6 +57,7 @@ def build_pvms_employee_payload(employee, *, include_password: bool = False) -> 'last_name': employee.last_name, 'middle_name': employee.middle_name or '', 'role': _employee_role(employee), + 'groups': list(employee.groups.values_list('name', flat=True)), 'is_active': bool(employee.is_active), 'is_staff': bool(employee.is_staff or employee.is_superuser), 'visit_permissions': _visit_permissions(employee), diff --git a/apps/portal_connector/pvms_permissions.py b/apps/portal_connector/pvms_permissions.py new file mode 100644 index 0000000..0d8540c --- /dev/null +++ b/apps/portal_connector/pvms_permissions.py @@ -0,0 +1,69 @@ +"""PVMS visit permission codes — shared by builders, admin, and seed migrations. + +Must stay in sync with PVMS apps.users / sync ingest allowlist. +""" +from __future__ import annotations + +PVMS_ACCESS_APP_LABEL = 'core' +PVMS_ACCESS_MODEL = 'pvmsaccess' + +# (codename, human name) — Russian labels for Django admin (LANGUAGE_CODE=ru) +PVMS_PERMISSION_CHOICES: list[tuple[str, str]] = [ + ('can_view_attendance', 'Перегляд звітів відвідуваності'), + ('can_manage_schedules', 'Управління графіками'), + ('can_register_others', 'Реєстрація візитів інших співробітників'), +] + +VISIT_PERMISSION_CODENAMES = frozenset(code for code, _ in PVMS_PERMISSION_CHOICES) + +# Default grants by ACORE auth.Group name (canonical) +GROUP_PVMS_PERMS: dict[str, list[str]] = { + 'Администратор сети': [c for c, _ in PVMS_PERMISSION_CHOICES], + 'Заведующий аптекой': [c for c, _ in PVMS_PERMISSION_CHOICES], + 'Ревизор': [ + 'can_view_attendance', + ], + 'Офисный сотрудник': [ + 'can_view_attendance', + ], + # Фармацевт / склад — только self-register, без visit can_* + 'Фармацевт': [], + 'Сотрудник склада': [], +} + +# ACORE auth.Group.name → PVMS LocalEmployee.role +ACORE_GROUP_TO_PVMS_ROLE: dict[str, str] = { + 'Администратор сети': 'admin', + 'Администратор портала': 'admin', + 'Администратор портала': 'admin', + 'Заведующий аптекой': 'manager', + 'Ревизор': 'employee', + 'Фармацевт': 'employee', + 'Офисный сотрудник': 'employee', + 'Сотрудник склада': 'employee', + 'IT': 'employee', +} + +CANONICAL_ROLE_GROUPS = ( + 'Администратор сети', + 'Ревизор', + 'Фармацевт', + 'Заведующий аптекой', + 'Офисный сотрудник', + 'Сотрудник склада', +) + + +def role_from_acore_groups(group_names: list[str], *, is_staff: bool = False) -> str: + """Map ACORE groups → PVMS role; staff/superuser still elevates to admin.""" + if is_staff: + return 'admin' + for name in group_names or []: + if name in ACORE_GROUP_TO_PVMS_ROLE: + role = ACORE_GROUP_TO_PVMS_ROLE[name] + if role == 'admin': + return 'admin' + for name in group_names or []: + if ACORE_GROUP_TO_PVMS_ROLE.get(name) == 'manager': + return 'manager' + return 'employee' diff --git a/apps/portal_connector/signals.py b/apps/portal_connector/signals.py index 77a10c5..75cf24c 100644 --- a/apps/portal_connector/signals.py +++ b/apps/portal_connector/signals.py @@ -100,12 +100,24 @@ def _pharmacy_push_pnms(sender, instance, created, **kwargs): def employee_groups_changed(sender, instance, action, **kwargs): + """Смена auth.Group → пересчитать role/pos_permissions на PNMS/PVMS/MSORTER.""" if action not in ('post_add', 'post_remove', 'post_clear'): return - if not _msorter_enabled(): - return - try: - from apps.portal_connector.tasks import push_employee_to_msorter - push_employee_to_msorter.delay(instance.pk, include_password=False) - except Exception: - logger.exception('Failed to enqueue employee groups→MSORTER id=%s', instance.pk) + if _pnms_enabled(): + try: + from apps.portal_connector.tasks import push_employee_to_pnms + push_employee_to_pnms.delay(instance.pk, include_password=False) + except Exception: + logger.exception('Failed to enqueue employee groups→PNMS id=%s', instance.pk) + if _pvms_enabled(): + try: + from apps.portal_connector.tasks import push_employee_to_pvms + push_employee_to_pvms.delay(instance.pk, include_password=False) + except Exception: + logger.exception('Failed to enqueue employee groups→PVMS id=%s', instance.pk) + if _msorter_enabled(): + try: + from apps.portal_connector.tasks import push_employee_to_msorter + push_employee_to_msorter.delay(instance.pk, include_password=False) + except Exception: + logger.exception('Failed to enqueue employee groups→MSORTER id=%s', instance.pk) diff --git a/apps/portal_connector/tests.py b/apps/portal_connector/tests.py index 4d5e427..f7bd746 100644 --- a/apps/portal_connector/tests.py +++ b/apps/portal_connector/tests.py @@ -153,3 +153,59 @@ class PushAllDefaultsTests(SimpleTestCase): params = inspect.signature(push_all_identity_to_pnms).parameters self.assertEqual(params['include_password'].default, False) + + +class PosPermissionsSeedTests(TestCase): + def test_pnms_can_permissions_exist_and_revisor_seeded(self): + from django.contrib.auth.models import Permission + from apps.portal_connector.pos_permissions import ( + PNMS_ACCESS_APP_LABEL, + PNMS_ACCESS_MODEL, + POS_PERMISSION_CODENAMES, + ) + + qs = Permission.objects.filter( + content_type__app_label=PNMS_ACCESS_APP_LABEL, + content_type__model=PNMS_ACCESS_MODEL, + ) + self.assertEqual(qs.count(), len(POS_PERMISSION_CODENAMES)) + revisor = Group.objects.get(name='Ревизор') + codes = set(revisor.permissions.filter( + content_type__model=PNMS_ACCESS_MODEL, + ).values_list('codename', flat=True)) + self.assertIn('can_deactivate_receipt', codes) + self.assertIn('can_create_return', codes) + self.assertIn('can_do_inventory', codes) + + def test_build_employee_payload_includes_group_pos_perms(self): + from apps.portal_connector.builders import build_employee_payload + + emp = Employee.objects.create_user( + username='pos-revisor', + password='x', + first_name='R', + last_name='V', + hire_date=date(2020, 1, 1), + ) + emp.groups.add(Group.objects.get(name='Ревизор')) + payload = build_employee_payload(emp) + self.assertIn('can_deactivate_receipt', payload['pos_permissions']) + self.assertIn('can_do_inventory', payload['pos_permissions']) + + +class GroupChangePushTests(TestCase): + @override_settings(PNMS_SYNC_ENABLED=True, PVMS_SYNC_ENABLED=False, MSORTER_SYNC_ENABLED=False) + def test_group_m2m_enqueues_pnms_push(self): + from unittest.mock import patch + + emp = Employee.objects.create_user( + username='grp-sync', + password='x', + first_name='G', + last_name='S', + hire_date=date(2020, 1, 1), + ) + group = Group.objects.get(name='Фармацевт') + with patch('apps.portal_connector.tasks.push_employee_to_pnms.delay') as pnms: + emp.groups.add(group) + pnms.assert_called_once_with(emp.pk, include_password=False) diff --git a/core/migrations/0023_seed_pnms_pos_permissions.py b/core/migrations/0023_seed_pnms_pos_permissions.py new file mode 100644 index 0000000..49b8746 --- /dev/null +++ b/core/migrations/0023_seed_pnms_pos_permissions.py @@ -0,0 +1,132 @@ +"""Create PNMS/POS can_* Django permissions and seed role groups.""" +from django.db import migrations + +PNMS_ACCESS_APP_LABEL = 'core' +PNMS_ACCESS_MODEL = 'pnmsaccess' + +PNMS_PERMISSION_CHOICES = [ + ('can_deactivate_receipt', 'Анулювання чека'), + ('can_create_return', 'Повернення по чеку'), + ('can_apply_manual_discount', 'Ручна знижка на касі'), + ('can_change_retail_price', 'Зміна роздрібної ціни'), + ('can_view_purchase_price', 'Перегляд закупівельної ціни'), + ('can_close_session', 'Закриття касової зміни'), + ('can_manage_transfers', 'Робота з накладними (приймання товару)'), + ('can_do_inventory', 'Проведення інвентаризації'), + ('can_receive_delivery', 'Приёмка от поставщика'), + ('can_ship_transfer', 'Отгрузка накладной со склада'), + ('can_view_reports', 'Перегляд звітів касової зміни'), + ('can_view_all_receipts', 'Перегляд усіх чеків зміни (не тільки своїх)'), + ('can_manage_employees', 'Управління співробітниками аптеки'), +] + +CANONICAL_ROLE_GROUPS = ( + 'Администратор сети', + 'Ревизор', + 'Фармацевт', + 'Заведующий аптекой', + 'Офисный сотрудник', + 'Сотрудник склада', +) + +GROUP_PNMS_PERMS = { + 'Администратор сети': [c for c, _ in PNMS_PERMISSION_CHOICES], + 'Ревизор': [ + 'can_deactivate_receipt', + 'can_create_return', + 'can_do_inventory', + 'can_view_reports', + 'can_view_all_receipts', + 'can_view_purchase_price', + ], + 'Заведующий аптекой': [ + 'can_close_session', + 'can_manage_transfers', + 'can_do_inventory', + 'can_view_reports', + 'can_view_all_receipts', + 'can_manage_employees', + 'can_apply_manual_discount', + 'can_change_retail_price', + 'can_view_purchase_price', + 'can_create_return', + ], + 'Фармацевт': [ + 'can_close_session', + 'can_view_reports', + ], +} + + +def forwards(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + PermissionGroup = apps.get_model('employees', 'PermissionGroup') + + ct, _ = ContentType.objects.get_or_create( + app_label=PNMS_ACCESS_APP_LABEL, + model=PNMS_ACCESS_MODEL, + ) + perms_by_code = {} + for codename, name in PNMS_PERMISSION_CHOICES: + perm, _ = Permission.objects.get_or_create( + content_type=ct, + codename=codename, + defaults={'name': name}, + ) + if perm.name != name: + perm.name = name + perm.save(update_fields=['name']) + perms_by_code[codename] = perm + + for group_name in CANONICAL_ROLE_GROUPS: + group, _ = Group.objects.get_or_create(name=group_name) + PermissionGroup.objects.get_or_create( + group=group, + defaults={ + 'description': f'Группа для роли: {group_name}', + 'is_active': True, + }, + ) + + for group_name, codes in GROUP_PNMS_PERMS.items(): + group = Group.objects.filter(name=group_name).first() + if group is None: + continue + to_add = [perms_by_code[c] for c in codes if c in perms_by_code] + if to_add: + group.permissions.add(*to_add) + + +def backwards(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + + ct = ContentType.objects.filter( + app_label=PNMS_ACCESS_APP_LABEL, + model=PNMS_ACCESS_MODEL, + ).first() + if ct is None: + return + pnms_perms = list(Permission.objects.filter(content_type=ct)) + if not pnms_perms: + return + for group in Group.objects.filter(name__in=GROUP_PNMS_PERMS.keys()): + group.permissions.remove(*pnms_perms) + Permission.objects.filter(content_type=ct).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + ('employees', '0018_seed_group_section_access'), + ('core', '0022_dashboardlayout_widget_widths'), + ] + + operations = [ + migrations.RunPython(forwards, backwards), + ] diff --git a/core/migrations/0024_seed_pvms_visit_permissions.py b/core/migrations/0024_seed_pvms_visit_permissions.py new file mode 100644 index 0000000..94c727c --- /dev/null +++ b/core/migrations/0024_seed_pvms_visit_permissions.py @@ -0,0 +1,105 @@ +"""Create PVMS visit can_* Django permissions and seed role groups.""" +from django.db import migrations + +PVMS_ACCESS_APP_LABEL = 'core' +PVMS_ACCESS_MODEL = 'pvmsaccess' + +PVMS_PERMISSION_CHOICES = [ + ('can_view_attendance', 'Перегляд звітів відвідуваності'), + ('can_manage_schedules', 'Управління графіками'), + ('can_register_others', 'Реєстрація візитів інших співробітників'), +] + +CANONICAL_ROLE_GROUPS = ( + 'Администратор сети', + 'Ревизор', + 'Фармацевт', + 'Заведующий аптекой', + 'Офисный сотрудник', + 'Сотрудник склада', +) + +GROUP_PVMS_PERMS = { + 'Администратор сети': [c for c, _ in PVMS_PERMISSION_CHOICES], + 'Заведующий аптекой': [c for c, _ in PVMS_PERMISSION_CHOICES], + 'Ревизор': [ + 'can_view_attendance', + ], + 'Офисный сотрудник': [ + 'can_view_attendance', + ], +} + + +def forwards(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + PermissionGroup = apps.get_model('employees', 'PermissionGroup') + + ct, _ = ContentType.objects.get_or_create( + app_label=PVMS_ACCESS_APP_LABEL, + model=PVMS_ACCESS_MODEL, + ) + perms_by_code = {} + for codename, name in PVMS_PERMISSION_CHOICES: + perm, _ = Permission.objects.get_or_create( + content_type=ct, + codename=codename, + defaults={'name': name}, + ) + if perm.name != name: + perm.name = name + perm.save(update_fields=['name']) + perms_by_code[codename] = perm + + for group_name in CANONICAL_ROLE_GROUPS: + group, _ = Group.objects.get_or_create(name=group_name) + PermissionGroup.objects.get_or_create( + group=group, + defaults={ + 'description': f'Группа для роли: {group_name}', + 'is_active': True, + }, + ) + + for group_name, codes in GROUP_PVMS_PERMS.items(): + group = Group.objects.filter(name=group_name).first() + if group is None: + continue + to_add = [perms_by_code[c] for c in codes if c in perms_by_code] + if to_add: + group.permissions.add(*to_add) + + +def backwards(apps, schema_editor): + Group = apps.get_model('auth', 'Group') + Permission = apps.get_model('auth', 'Permission') + ContentType = apps.get_model('contenttypes', 'ContentType') + + ct = ContentType.objects.filter( + app_label=PVMS_ACCESS_APP_LABEL, + model=PVMS_ACCESS_MODEL, + ).first() + if ct is None: + return + pvms_perms = list(Permission.objects.filter(content_type=ct)) + if not pvms_perms: + return + for group in Group.objects.filter(name__in=GROUP_PVMS_PERMS.keys()): + group.permissions.remove(*pvms_perms) + Permission.objects.filter(content_type=ct).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + ('employees', '0018_seed_group_section_access'), + ('core', '0023_seed_pnms_pos_permissions'), + ] + + operations = [ + migrations.RunPython(forwards, backwards), + ]