from core.test_utils import grant_all_locations from datetime import date from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.exceptions import ValidationError from django.db import IntegrityError, transaction from django.test import TestCase from core.models import Directory, DirectoryType, PortalAccess from apps.employees.models import EmployeePermissionOverride from apps.locations.models import Pharmacy from apps.rro.models import CashRegister Employee = get_user_model() def grant_office_access(user): user.groups.add(Group.objects.get(name='Офисный сотрудник')) for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'): if hasattr(user, attr): delattr(user, attr) def grant_rro_access(user): from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False) perm = Permission.objects.get(content_type=ct, codename='access_rro') EmployeePermissionOverride.objects.get_or_create( employee=user, permission=perm, defaults={'override_type': 'grant'}, ) for attr in ('_perm_cache', '_user_perm_cache', '_group_perm_cache'): if hasattr(user, attr): delattr(user, attr) class CashRegisterModelTests(TestCase): @classmethod def setUpTestData(cls): org_type = DirectoryType.objects.create(code='organization', name='Организация') region_type = DirectoryType.objects.create(code='region', name='Регион') status_type, _ = DirectoryType.objects.get_or_create( code='rro_status', defaults={'name': 'Статус РРО/ПРРО', 'hierarchy_type': 'strict'}, ) cls.status_active, _ = Directory.objects.get_or_create( directory_type=status_type, code='active', defaults={'name': 'Активный', 'is_active': True}, ) cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса') cls.org = Directory.objects.create(directory_type=org_type, code='akc', name='АКС') cls.pharmacy = Pharmacy.objects.create( organization=cls.org, location_region=cls.region, pharmacy_number=42, location_type='pharmacy', street='Тестовая', house_number='1', ) cls.pharmacy2 = Pharmacy.objects.create( organization=cls.org, location_region=cls.region, pharmacy_number=43, location_type='pharmacy', street='Другая', house_number='2', ) def _reg(self, **kw): defaults = { 'pharmacy': self.pharmacy, 'kind': CashRegister.Kind.PRRO, 'fiscal_number': kw.pop('fiscal_number', f'FN-{CashRegister.objects.count()+1}'), 'status': self.status_active, } defaults.update(kw) return CashRegister(**defaults) def test_seed_statuses_exist(self): codes = set( Directory.objects.filter(directory_type__code='rro_status') .values_list('code', flat=True) ) self.assertTrue({'active', 'reserve', 'repair', 'decommissioned'} <= codes) def test_unique_fiscal_number(self): self._reg(fiscal_number='FN-UNIQUE').save() with self.assertRaises(IntegrityError): with transaction.atomic(): self._reg(fiscal_number='FN-UNIQUE', pharmacy=self.pharmacy2).save() def test_only_one_primary_per_pharmacy(self): a = self._reg(fiscal_number='FN-P1', is_primary=True) a.full_clean() a.save() b = self._reg(fiscal_number='FN-P2', is_primary=True) with self.assertRaises(ValidationError): b.full_clean() def test_primary_ok_on_different_pharmacies(self): a = self._reg(fiscal_number='FN-A', is_primary=True) a.full_clean() a.save() b = self._reg(fiscal_number='FN-B', pharmacy=self.pharmacy2, is_primary=True) b.full_clean() b.save() class CashRegisterPortalTests(TestCase): @classmethod def setUpTestData(cls): org_type = DirectoryType.objects.create(code='organization', name='Организация') region_type = DirectoryType.objects.create(code='region', name='Регион') status_type, _ = DirectoryType.objects.get_or_create( code='rro_status', defaults={'name': 'Статус РРО/ПРРО', 'hierarchy_type': 'strict'}, ) cls.status_active, _ = Directory.objects.get_or_create( directory_type=status_type, code='active', defaults={'name': 'Активный', 'is_active': True}, ) region = Directory.objects.create(directory_type=region_type, code='kv', name='Киев') org = Directory.objects.create(directory_type=org_type, code='org', name='ТестОрг') cls.pharmacy = Pharmacy.objects.create( organization=org, location_region=region, pharmacy_number=7, location_type='pharmacy', street='Главная', house_number='7', ) CashRegister.objects.create( pharmacy=cls.pharmacy, kind=CashRegister.Kind.RRO, fiscal_number='FN-PORTAL-1', status=cls.status_active, model_name='Datecs FP-700', is_primary=True, ) cls.user = Employee.objects.create( username='rro.staff', email='rro.staff@company.local', first_name='РРО', last_name='Тест', hire_date=date(2026, 1, 1), user_location_type='office', is_staff=True, mattermost_sync_enabled=False, ad_sync_enabled=False, totp_enabled=True, ) grant_all_locations(cls.user) grant_rro_access(cls.user) def _login(self): self.client.force_login(self.user) session = self.client.session session['2fa_verified'] = True session.save() def test_rro_list_page(self): self._login() resp = self.client.get('/rro/', HTTP_HX_REQUEST='true') self.assertEqual(resp.status_code, 200) self.assertContains(resp, 'FN-PORTAL-1') self.assertContains(resp, 'id="rro-form-panel"') self.assertContains(resp, 'role="dialog"') self.assertNotContains(resp, 'id="rro-form-overlay"') self.assertNotContains(resp, 'translateX(100%)') def test_pharmacy_detail_rro_tab(self): self._login() resp = self.client.get(f'/pharmacies/{self.pharmacy.pk}/', HTTP_HX_REQUEST='true') self.assertEqual(resp.status_code, 200) self.assertContains(resp, 'FN-PORTAL-1') self.assertContains(resp, 'ph-panel-rro') def test_pharmacy_detail_hides_rro_for_other_pharmacy_user(self): other = Pharmacy.objects.create( organization=self.pharmacy.organization, location_region=self.pharmacy.location_region, pharmacy_number=99, location_type='pharmacy', street='Чужа', house_number='9', ) stranger = Employee.objects.create( username='rro.stranger', email='rro.stranger@company.local', first_name='Інша', last_name='Аптека', hire_date=date(2026, 1, 1), user_location_type='pharmacy', user_pharmacy=other, is_staff=False, mattermost_sync_enabled=False, ad_sync_enabled=False, ) self.client.force_login(stranger) session = self.client.session session['2fa_verified'] = True session.save() # Чужая аптека вне области доступа: карточка недоступна целиком (404), # операционные данные (РРО и т.п.) не утекают. resp = self.client.get(f'/pharmacies/{self.pharmacy.pk}/', HTTP_HX_REQUEST='true') self.assertEqual(resp.status_code, 404) self.assertNotContains(resp, 'FN-PORTAL-1', status_code=404) self.assertNotContains(resp, 'ph-panel-rro', status_code=404) class ManagerRroScopeTests(TestCase): @classmethod def setUpTestData(cls): org_type = DirectoryType.objects.create(code='organization', name='Организация') region_type = DirectoryType.objects.create(code='region', name='Регион') status_type, _ = DirectoryType.objects.get_or_create( code='rro_status', defaults={'name': 'Статус РРО/ПРРО', 'hierarchy_type': 'strict'}, ) cls.status_active, _ = Directory.objects.get_or_create( directory_type=status_type, code='active', defaults={'name': 'Активный', 'is_active': True}, ) region = Directory.objects.create(directory_type=region_type, code='kv2', name='Киев') org = Directory.objects.create(directory_type=org_type, code='orgm', name='ТестОрг') cls.pharmacy_a = Pharmacy.objects.create( organization=org, location_region=region, pharmacy_number=1, location_type='pharmacy', street='A', house_number='1', ) cls.pharmacy_b = Pharmacy.objects.create( organization=org, location_region=region, pharmacy_number=2, location_type='pharmacy', street='B', house_number='2', ) CashRegister.objects.create( pharmacy=cls.pharmacy_a, kind=CashRegister.Kind.RRO, fiscal_number='FN-MINE', status=cls.status_active, ) CashRegister.objects.create( pharmacy=cls.pharmacy_b, kind=CashRegister.Kind.RRO, fiscal_number='FN-OTHER', status=cls.status_active, ) cls.manager = Employee.objects.create( username='rro.manager', email='rro.manager@company.local', first_name='Зав', last_name='Аптекой', hire_date=date(2026, 1, 1), user_location_type='pharmacy', user_pharmacy=cls.pharmacy_a, is_staff=False, mattermost_sync_enabled=False, ad_sync_enabled=False, ) cls.manager.groups.add(Group.objects.get(name='Заведующий аптекой')) def _login(self): self.client.force_login(self.manager) session = self.client.session session['2fa_verified'] = True session.save() def test_manager_sees_only_own_pharmacy(self): self._login() resp = self.client.get('/rro/', HTTP_HX_REQUEST='true') self.assertEqual(resp.status_code, 200) self.assertContains(resp, 'FN-MINE') self.assertNotContains(resp, 'FN-OTHER') self.assertTrue(resp.context['rro_pharmacy_scoped']) def test_manager_cannot_bypass_with_query_param(self): self._login() resp = self.client.get( f'/rro/?pharmacy={self.pharmacy_b.pk}', HTTP_HX_REQUEST='true', ) self.assertEqual(resp.status_code, 200) self.assertNotContains(resp, 'FN-OTHER') class CashRegisterCrudTests(TestCase): @classmethod def setUpTestData(cls): org_type = DirectoryType.objects.create(code='organization', name='Организация') region_type = DirectoryType.objects.create(code='region', name='Регион') status_type, _ = DirectoryType.objects.get_or_create( code='rro_status', defaults={'name': 'Статус РРО/ПРРО', 'hierarchy_type': 'strict'}, ) cls.status_active, _ = Directory.objects.get_or_create( directory_type=status_type, code='active', defaults={'name': 'Активный', 'is_active': True}, ) cls.status_repair, _ = Directory.objects.get_or_create( directory_type=status_type, code='repair', defaults={'name': 'В ремонте', 'is_active': True}, ) region = Directory.objects.create(directory_type=region_type, code='od2', name='Одесса2') org = Directory.objects.create(directory_type=org_type, code='org2', name='Орг2') cls.pharmacy = Pharmacy.objects.create( organization=org, location_region=region, pharmacy_number=11, location_type='pharmacy', street='CRUD', house_number='1', ) cls.staff = Employee.objects.create( username='rro.crud.staff', email='rro.crud.staff@company.local', first_name='CRUD', last_name='Staff', hire_date=date(2026, 1, 1), user_location_type='office', is_staff=True, mattermost_sync_enabled=False, ad_sync_enabled=False, totp_enabled=True, ) cls.regular = Employee.objects.create( username='rro.crud.user', email='rro.crud.user@company.local', first_name='CRUD', last_name='User', hire_date=date(2026, 1, 1), user_location_type='pharmacy', user_pharmacy=cls.pharmacy, is_staff=False, mattermost_sync_enabled=False, ad_sync_enabled=False, ) grant_all_locations(cls.staff) grant_rro_access(cls.staff) def _login(self, user): self.client.force_login(user) session = self.client.session session['2fa_verified'] = True session.save() def _payload(self, **extra): data = { 'pharmacy': self.pharmacy.pk, 'kind': CashRegister.Kind.PRRO, 'fiscal_number': 'FN-CRUD-NEW', 'status': self.status_active.pk, 'model_name': 'Checkbox', 'provider': 'checkbox', } data.update(extra) return data def test_staff_create(self): self._login(self.staff) resp = self.client.post('/rro/create/', self._payload(), HTTP_HX_REQUEST='true') self.assertEqual(resp.status_code, 200) self.assertTrue(CashRegister.objects.filter(fiscal_number='FN-CRUD-NEW').exists()) self.assertContains(resp, 'Кассовый аппарат добавлен') def test_non_staff_create_forbidden(self): # Есть просмотр РРО, но без staff — create запрещён (не редирект раздела). grant_rro_access(self.regular) self._login(self.regular) resp = self.client.post('/rro/create/', self._payload(fiscal_number='FN-FORBIDDEN')) self.assertEqual(resp.status_code, 403) self.assertFalse(CashRegister.objects.filter(fiscal_number='FN-FORBIDDEN').exists()) def test_staff_update(self): reg = CashRegister.objects.create( pharmacy=self.pharmacy, kind=CashRegister.Kind.RRO, fiscal_number='FN-UPD', status=self.status_active, ) self._login(self.staff) resp = self.client.post( f'/rro/{reg.pk}/update/', self._payload( fiscal_number='FN-UPD-2', kind=CashRegister.Kind.RRO, status=self.status_repair.pk, provider='', ), HTTP_HX_REQUEST='true', ) self.assertEqual(resp.status_code, 200) reg.refresh_from_db() self.assertEqual(reg.fiscal_number, 'FN-UPD-2') self.assertEqual(reg.status_id, self.status_repair.pk) def test_duplicate_fiscal_shows_form_error(self): CashRegister.objects.create( pharmacy=self.pharmacy, kind=CashRegister.Kind.PRRO, fiscal_number='FN-DUP', status=self.status_active, ) self._login(self.staff) resp = self.client.post( '/rro/create/', self._payload(fiscal_number='FN-DUP'), HTTP_HX_REQUEST='true', ) self.assertEqual(resp.status_code, 200) self.assertEqual(CashRegister.objects.filter(fiscal_number='FN-DUP').count(), 1) self.assertTrue(resp.context['rro_form_errors']) def test_primary_conflict_shows_form_error(self): CashRegister.objects.create( pharmacy=self.pharmacy, kind=CashRegister.Kind.PRRO, fiscal_number='FN-PRIM-1', status=self.status_active, is_primary=True, ) self._login(self.staff) resp = self.client.post( '/rro/create/', self._payload(fiscal_number='FN-PRIM-2', is_primary='1'), HTTP_HX_REQUEST='true', ) self.assertEqual(resp.status_code, 200) self.assertFalse(CashRegister.objects.filter(fiscal_number='FN-PRIM-2').exists()) self.assertIn('is_primary', resp.context['rro_form_errors']) def test_new_query_opens_create_panel(self): self._login(self.staff) resp = self.client.get( f'/rro/?pharmacy={self.pharmacy.pk}&new=1', HTTP_HX_REQUEST='true', ) self.assertEqual(resp.status_code, 200) self.assertTrue(resp.context['show_create_panel']) self.assertEqual(str(resp.context['prefill_pharmacy']), str(self.pharmacy.pk))