Harden portal access and hiring, add RRO portal CRUD.

Remove the is_staff permission bridge, tighten intern credential handling with one-time PDFs and random seed passwords, and let staff manage cash registers from the portal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-10 08:54:41 +03:00
parent 56e2b1c8fe
commit 70e5a77b04
16 changed files with 829 additions and 116 deletions
+177
View File
@@ -132,3 +132,180 @@ class CashRegisterPortalTests(TestCase):
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()
resp = self.client.get(f'/pharmacies/{self.pharmacy.pk}/', HTTP_HX_REQUEST='true')
self.assertEqual(resp.status_code, 200)
self.assertNotContains(resp, 'FN-PORTAL-1')
self.assertNotContains(resp, 'ph-panel-rro')
self.assertFalse(resp.context['can_view_ops'])
self.assertEqual(resp.context['staff'], [])
self.assertEqual(resp.context['equipment'], [])
self.assertEqual(resp.context['internet_contracts'], [])
self.assertEqual(resp.context['utility_readings'], [])
self.assertNotContains(resp, 'ph-panel-equipment')
self.assertNotContains(resp, 'ph-panel-internet')
self.assertNotContains(resp, 'ph-panel-utilities')
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,
)
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):
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))