4e1d5f221e
Co-authored-by: Cursor <cursoragent@cursor.com>
967 lines
42 KiB
Python
967 lines
42 KiB
Python
from core.test_utils import grant_all_locations
|
||
from unittest import mock
|
||
|
||
from django.core.exceptions import ValidationError
|
||
from django.db import IntegrityError, connection, transaction
|
||
from django.test import TestCase
|
||
|
||
from core.models import Directory, DirectoryType
|
||
from apps.locations.models import Pharmacy
|
||
|
||
|
||
class CarbonioEmailTests(TestCase):
|
||
"""Генерация адреса Carbonio (<префикс организации><номер>@домен),
|
||
ручной override и проверка уникальности."""
|
||
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
org_type = DirectoryType.objects.create(code='organization', name='Организация')
|
||
region_type = DirectoryType.objects.create(code='region', name='Регион')
|
||
cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
|
||
cls.aks = Directory.objects.create(
|
||
directory_type=org_type, code='akc', name='АКС', extra_data={'mail_prefix': 'a'})
|
||
cls.tira = Directory.objects.create(
|
||
directory_type=org_type, code='tira', name='Тира', extra_data={'mail_prefix': 't'})
|
||
cls.noprefix = Directory.objects.create(
|
||
directory_type=org_type, code='xx', name='Без префикса', extra_data={})
|
||
|
||
def _pharmacy(self, org, number, **kw):
|
||
return Pharmacy.objects.create(
|
||
organization=org, location_region=self.region,
|
||
pharmacy_number=number, location_type='pharmacy', **kw)
|
||
|
||
def test_build_email_format(self):
|
||
self.assertEqual(self._pharmacy(self.aks, 5).build_carbonio_email(), 'a5@aks-apteka.com.ua')
|
||
|
||
def test_build_email_uses_org_prefix(self):
|
||
self.assertEqual(self._pharmacy(self.tira, 5).build_carbonio_email(), 't5@aks-apteka.com.ua')
|
||
|
||
def test_build_email_empty_without_prefix(self):
|
||
self.assertEqual(self._pharmacy(self.noprefix, 5).build_carbonio_email(), '')
|
||
|
||
def test_autogenerate_on_save_when_enabled(self):
|
||
p = self._pharmacy(self.aks, 7, carbonio_sync_enabled=True)
|
||
self.assertEqual(p.carbonio_email, 'a7@aks-apteka.com.ua')
|
||
|
||
def test_no_autogenerate_when_disabled(self):
|
||
self.assertEqual(self._pharmacy(self.aks, 8).carbonio_email, '')
|
||
|
||
def test_manual_override_respected(self):
|
||
p = self._pharmacy(self.aks, 9, carbonio_sync_enabled=True,
|
||
carbonio_email='custom@aks-apteka.com.ua')
|
||
self.assertEqual(p.carbonio_email, 'custom@aks-apteka.com.ua')
|
||
|
||
def test_uniqueness_rejected_in_clean(self):
|
||
self._pharmacy(self.aks, 10, carbonio_sync_enabled=True) # a10@
|
||
p2 = self._pharmacy(self.tira, 11)
|
||
p2.carbonio_email = 'a10@aks-apteka.com.ua'
|
||
with self.assertRaises(ValidationError):
|
||
p2.clean()
|
||
|
||
def test_uniqueness_enforced_by_db(self):
|
||
self._pharmacy(self.aks, 12, carbonio_sync_enabled=True) # a12@
|
||
with self.assertRaises(IntegrityError):
|
||
with transaction.atomic():
|
||
self._pharmacy(self.tira, 13, carbonio_email='a12@aks-apteka.com.ua')
|
||
|
||
def test_autogenerate_skips_on_collision(self):
|
||
# ручной a15@ занят другой аптекой -> автоген для АКС №15 оставляет пусто
|
||
self._pharmacy(self.tira, 99, carbonio_sync_enabled=True,
|
||
carbonio_email='a15@aks-apteka.com.ua')
|
||
p = self._pharmacy(self.aks, 15, carbonio_sync_enabled=True)
|
||
self.assertEqual(p.carbonio_email, '')
|
||
|
||
def test_blank_emails_do_not_clash(self):
|
||
# несколько аптек без адреса — частичный уникальный индекс их не блокирует
|
||
self._pharmacy(self.aks, 16)
|
||
self._pharmacy(self.tira, 17)
|
||
self.assertEqual(Pharmacy.objects.filter(carbonio_email='').count(), 2)
|
||
|
||
|
||
class CarbonioPasswordSyncTests(TestCase):
|
||
"""sync_pharmacy_to_carbonio с замоканным CarbonioService: пароль ящика
|
||
сохраняется у нас зашифрованным и совпадает с отданным в Carbonio."""
|
||
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
org_type = DirectoryType.objects.create(code='organization', name='Организация')
|
||
region_type = DirectoryType.objects.create(code='region', name='Регион')
|
||
cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
|
||
cls.aks = Directory.objects.create(
|
||
directory_type=org_type, code='akc', name='АКС', extra_data={'mail_prefix': 'a'})
|
||
|
||
def _pharmacy(self, number, **kw):
|
||
return Pharmacy.objects.create(
|
||
organization=self.aks, location_region=self.region,
|
||
pharmacy_number=number, location_type='pharmacy', **kw)
|
||
|
||
def test_create_stores_encrypted_password(self):
|
||
from apps.mail_connector.services.sync_service import sync_pharmacy_to_carbonio
|
||
p = self._pharmacy(20) # синхронизация выключена, адрес пуст
|
||
fake = mock.Mock()
|
||
fake.get_account.return_value = None # аккаунта ещё нет → create
|
||
fake.create_account.return_value = 'acc-123'
|
||
with mock.patch(
|
||
'apps.mail_connector.services.sync_service.CarbonioService',
|
||
return_value=fake):
|
||
self.assertTrue(sync_pharmacy_to_carbonio(p))
|
||
|
||
p.refresh_from_db()
|
||
self.assertEqual(p.carbonio_email, 'a20@aks-apteka.com.ua')
|
||
self.assertEqual(p.carbonio_account_id, 'acc-123')
|
||
self.assertEqual(p.carbonio_sync_status, 'synced')
|
||
self.assertTrue(p.carbonio_password)
|
||
# тот же пароль ушёл в Carbonio, что и сохранён у нас
|
||
self.assertEqual(fake.create_account.call_args.args[1], p.carbonio_password)
|
||
|
||
# в БД пароль лежит зашифрованным
|
||
table, pk = Pharmacy._meta.db_table, Pharmacy._meta.pk.column
|
||
with connection.cursor() as cur:
|
||
cur.execute(f'SELECT carbonio_password FROM {table} WHERE {pk}=%s', [p.pk])
|
||
raw = cur.fetchone()[0]
|
||
self.assertTrue(raw.startswith('gAAAAA'))
|
||
|
||
def test_update_path_does_not_touch_password(self):
|
||
from apps.mail_connector.services.sync_service import sync_pharmacy_to_carbonio
|
||
p = self._pharmacy(21, carbonio_email='a21@aks-apteka.com.ua')
|
||
fake = mock.Mock()
|
||
fake.get_account.return_value = {'id': 'acc-existing'} # аккаунт уже есть → update
|
||
with mock.patch(
|
||
'apps.mail_connector.services.sync_service.CarbonioService',
|
||
return_value=fake):
|
||
self.assertTrue(sync_pharmacy_to_carbonio(p))
|
||
|
||
p.refresh_from_db()
|
||
self.assertEqual(p.carbonio_account_id, 'acc-existing')
|
||
self.assertEqual(p.carbonio_password, '') # на update пароль не трогаем
|
||
fake.create_account.assert_not_called()
|
||
fake.set_account_status.assert_called_once_with('acc-existing', 'active')
|
||
|
||
|
||
class DislocationColumnsTests(TestCase):
|
||
"""Whitelist, резолв пресета default и сохранение preference пользователя."""
|
||
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
from datetime import date
|
||
from django.contrib.auth import get_user_model
|
||
from apps.locations.dislocation_columns import HR_COLUMNS, OPS_COLUMNS, STANDARD_COLUMNS
|
||
from apps.locations.models import DislocationColumnPreset
|
||
|
||
Employee = get_user_model()
|
||
org_type = DirectoryType.objects.create(code='organization', name='Организация')
|
||
region_type = DirectoryType.objects.create(code='region', name='Регион')
|
||
cls.region = Directory.objects.create(directory_type=region_type, code='kv', name='Киев')
|
||
cls.org = Directory.objects.create(directory_type=org_type, code='org', name='ТестОрг')
|
||
cls.user = Employee.objects.create(
|
||
username='dislo.cols',
|
||
email='dislo.cols@company.local',
|
||
first_name='Коля',
|
||
last_name='Колонки',
|
||
hire_date=date(2026, 1, 1),
|
||
user_location_type='office',
|
||
mattermost_sync_enabled=False,
|
||
ad_sync_enabled=False,
|
||
)
|
||
# Любая группа = доступ к открытым разделам (в т.ч. /dislocation/).
|
||
from django.contrib.auth.models import Group
|
||
cls.user.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
|
||
DislocationColumnPreset.objects.update_or_create(
|
||
code='standard',
|
||
defaults={
|
||
'name': 'Стандарт',
|
||
'columns': list(STANDARD_COLUMNS),
|
||
'is_default': True,
|
||
},
|
||
)
|
||
DislocationColumnPreset.objects.update_or_create(
|
||
code='hr',
|
||
defaults={'name': 'HR', 'columns': list(HR_COLUMNS), 'is_default': False},
|
||
)
|
||
DislocationColumnPreset.objects.update_or_create(
|
||
code='ops',
|
||
defaults={'name': 'Ops', 'columns': list(OPS_COLUMNS), 'is_default': False},
|
||
)
|
||
Pharmacy.objects.create(
|
||
organization=cls.org, location_region=cls.region,
|
||
pharmacy_number=101, location_type='pharmacy', street='Тестовая', house_number='1',
|
||
)
|
||
|
||
def test_normalize_whitelist_and_locked_pharmacy(self):
|
||
from apps.locations.dislocation_columns import normalize_columns
|
||
|
||
self.assertEqual(
|
||
normalize_columns(['email', 'pharmacy', 'unknown', 'email', 'region']),
|
||
['email', 'pharmacy', 'region'],
|
||
)
|
||
self.assertEqual(
|
||
normalize_columns(['organization', 'pharmacy', 'phone']),
|
||
['organization', 'pharmacy', 'phone'],
|
||
)
|
||
self.assertEqual(normalize_columns([]), ['pharmacy'])
|
||
self.assertEqual(normalize_columns(['secret', 'password']), ['pharmacy'])
|
||
# pharmacy обязательна, но если её нет в списке — добавляется первой
|
||
self.assertEqual(
|
||
normalize_columns(['region', 'phone']),
|
||
['pharmacy', 'region', 'phone'],
|
||
)
|
||
def test_resolve_uses_default_preset(self):
|
||
from apps.locations.dislocation_columns import STANDARD_COLUMNS, resolve_columns_for_user
|
||
|
||
self.user.dislocation_columns = None
|
||
self.user.save(update_fields=['dislocation_columns'])
|
||
self.assertEqual(resolve_columns_for_user(self.user), list(STANDARD_COLUMNS))
|
||
|
||
def test_resolve_uses_custom_preference(self):
|
||
from apps.locations.dislocation_columns import resolve_columns_for_user
|
||
|
||
self.user.dislocation_columns = ['region', 'phone', 'bogus']
|
||
self.user.save(update_fields=['dislocation_columns'])
|
||
self.assertEqual(
|
||
resolve_columns_for_user(self.user),
|
||
['pharmacy', 'region', 'phone'],
|
||
)
|
||
|
||
def test_post_saves_columns_preference(self):
|
||
self.client.force_login(self.user)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
|
||
resp = self.client.post(
|
||
'/dislocation/columns/',
|
||
{'columns': ['organization', 'pharmacy', 'staff', 'region', 'email', 'evil']},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.user.refresh_from_db()
|
||
self.assertEqual(
|
||
self.user.dislocation_columns,
|
||
['organization', 'pharmacy', 'staff', 'region', 'email'],
|
||
)
|
||
self.assertContains(resp, "disloSortBy('organization'")
|
||
self.assertContains(resp, "disloSortBy('region'")
|
||
self.assertContains(resp, "disloSortBy('email'")
|
||
self.assertNotContains(resp, "disloSortBy('phone'")
|
||
|
||
def test_post_preset_code_applies_hr(self):
|
||
from apps.locations.dislocation_columns import HR_COLUMNS
|
||
|
||
self.client.force_login(self.user)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
|
||
resp = self.client.post(
|
||
'/dislocation/columns/',
|
||
{'preset_code': 'hr'},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.user.refresh_from_db()
|
||
self.assertEqual(self.user.dislocation_columns, list(HR_COLUMNS))
|
||
self.assertContains(resp, "disloSortBy('curator'")
|
||
self.assertContains(resp, "disloSortBy('manager'")
|
||
self.assertNotContains(resp, "disloSortBy('phone'")
|
||
|
||
def test_staff_can_create_preset(self):
|
||
from apps.locations.models import DislocationColumnPreset
|
||
from core.test_utils import login_2fa
|
||
|
||
self.user.is_staff = True
|
||
self.user.save(update_fields=['is_staff'])
|
||
login_2fa(self.client, self.user)
|
||
|
||
resp = self.client.post(
|
||
'/dislocation/column-presets/',
|
||
{
|
||
'action': 'create',
|
||
'name': 'IT набор',
|
||
'code': 'it',
|
||
'columns': ['pharmacy', 'mikrotik', 'provider', 'gpon'],
|
||
'is_default': '',
|
||
},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 200)
|
||
preset = DislocationColumnPreset.objects.get(code='it')
|
||
self.assertEqual(preset.name, 'IT набор')
|
||
self.assertEqual(preset.columns, ['pharmacy', 'mikrotik', 'provider', 'gpon'])
|
||
|
||
def test_non_staff_cannot_create_preset(self):
|
||
self.client.force_login(self.user)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
|
||
resp = self.client.post(
|
||
'/dislocation/column-presets/',
|
||
{
|
||
'action': 'create',
|
||
'name': 'Hack',
|
||
'code': 'hack',
|
||
'columns': ['pharmacy', 'phone'],
|
||
},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 403)
|
||
|
||
|
||
class PharmacyCardVisibilityTests(TestCase):
|
||
"""Матрица видимости вкладок карточки аптеки (синглтон в админке)."""
|
||
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
from datetime import date
|
||
|
||
from django.contrib.auth.models import Group
|
||
|
||
from apps.employees.models import Employee
|
||
from apps.locations.models import PharmacyCardVisibility
|
||
|
||
org_type = DirectoryType.objects.create(code='organization', name='Организация')
|
||
region_type = DirectoryType.objects.create(code='region', name='Регион')
|
||
region = Directory.objects.create(directory_type=region_type, code='vis', name='Вид')
|
||
org = Directory.objects.create(directory_type=org_type, code='visorg', name='ВизОрг')
|
||
cls.pharmacy = Pharmacy.objects.create(
|
||
organization=org, location_region=region,
|
||
pharmacy_number=42, location_type='pharmacy',
|
||
street='Вид', house_number='1',
|
||
)
|
||
other = Pharmacy.objects.create(
|
||
organization=org, location_region=region,
|
||
pharmacy_number=43, location_type='pharmacy',
|
||
street='Інша', house_number='2',
|
||
)
|
||
common = dict(
|
||
hire_date=date(2024, 1, 1),
|
||
mattermost_sync_enabled=False,
|
||
ad_sync_enabled=False,
|
||
)
|
||
cls.own = Employee.objects.create_user(
|
||
username='vis.own', user_location_type='pharmacy',
|
||
user_pharmacy=cls.pharmacy, **common,
|
||
)
|
||
cls.stranger = Employee.objects.create_user(
|
||
username='vis.stranger', user_location_type='pharmacy',
|
||
user_pharmacy=other, **common,
|
||
)
|
||
cls.staff = Employee.objects.create_user(
|
||
username='vis.staff', is_staff=True, totp_enabled=True, **common,
|
||
)
|
||
cls.superuser = Employee.objects.create_user(
|
||
username='vis.super', is_staff=True, is_superuser=True,
|
||
totp_enabled=True, **common,
|
||
)
|
||
cls.it_user = Employee.objects.create_user(username='vis.it', **common)
|
||
it_group, _ = Group.objects.get_or_create(name='IT')
|
||
cls.it_user.groups.add(it_group)
|
||
grant_all_locations(cls.it_user)
|
||
grant_all_locations(cls.staff)
|
||
PharmacyCardVisibility.load()
|
||
|
||
def _set_level(self, section, level):
|
||
from apps.locations.models import PharmacyCardVisibility
|
||
|
||
policy = PharmacyCardVisibility.load()
|
||
attr = f'show_{section}' if section != 'edit' else 'edit_pharmacy'
|
||
setattr(policy, attr, level)
|
||
policy.save()
|
||
|
||
def test_own_or_staff_default(self):
|
||
from apps.locations.models import (
|
||
PharmacyCardVisibility, user_can_view_pharmacy_section,
|
||
)
|
||
|
||
level = PharmacyCardVisibility.Level.OWN_OR_STAFF
|
||
self.assertTrue(user_can_view_pharmacy_section(self.own, self.pharmacy, level))
|
||
self.assertFalse(user_can_view_pharmacy_section(self.stranger, self.pharmacy, level))
|
||
self.assertTrue(user_can_view_pharmacy_section(self.staff, self.pharmacy, level))
|
||
|
||
def test_edit_pharmacy_default_staff_only(self):
|
||
from apps.locations.models import pharmacy_card_flags, user_can_edit_pharmacy
|
||
|
||
self.assertFalse(user_can_edit_pharmacy(self.own, self.pharmacy))
|
||
self.assertTrue(user_can_edit_pharmacy(self.staff, self.pharmacy))
|
||
flags = pharmacy_card_flags(self.own, self.pharmacy)
|
||
self.assertFalse(flags['can_edit'])
|
||
flags_staff = pharmacy_card_flags(self.staff, self.pharmacy)
|
||
self.assertTrue(flags_staff['can_edit'])
|
||
|
||
def test_edit_pharmacy_own_or_staff_allows_own(self):
|
||
from apps.locations.models import PharmacyCardVisibility, user_can_edit_pharmacy
|
||
|
||
self._set_level('edit', PharmacyCardVisibility.Level.OWN_OR_STAFF)
|
||
self.assertTrue(user_can_edit_pharmacy(self.own, self.pharmacy))
|
||
self.assertFalse(user_can_edit_pharmacy(self.stranger, self.pharmacy))
|
||
self.assertTrue(user_can_edit_pharmacy(self.staff, self.pharmacy))
|
||
|
||
def test_portal_edit_forbidden_without_permission(self):
|
||
self.client.force_login(self.own)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
resp = self.client.post(
|
||
f'/pharmacies/{self.pharmacy.pk}/edit/',
|
||
{'working_hours': '09:00-21:00'},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 403)
|
||
|
||
def test_portal_edit_saves_when_allowed(self):
|
||
from apps.locations.models import PharmacyCardVisibility
|
||
|
||
self._set_level('edit', PharmacyCardVisibility.Level.OWN_OR_STAFF)
|
||
self.client.force_login(self.own)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
resp = self.client.post(
|
||
f'/pharmacies/{self.pharmacy.pk}/edit/',
|
||
{
|
||
'organization': self.pharmacy.organization_id,
|
||
'pharmacy_number': self.pharmacy.pharmacy_number,
|
||
'status': 'open',
|
||
'category': 'A',
|
||
'vat_category': '',
|
||
'working_hours': '09:00-21:00',
|
||
'location_region': self.pharmacy.location_region_id,
|
||
'street': 'Нова',
|
||
'house_number': '10',
|
||
'building': '',
|
||
'apartment': '',
|
||
'location': '46.4825,30.7233',
|
||
'phone': '',
|
||
'license_number': '',
|
||
'license_expiry_date': '',
|
||
'head_pharmacist': '',
|
||
'responsible_manager': '',
|
||
'curator': '',
|
||
'pharmacists_count': 2,
|
||
'comments': 'Оновлено з порталу',
|
||
},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.pharmacy.refresh_from_db()
|
||
self.assertEqual(self.pharmacy.working_hours, '09:00-21:00')
|
||
self.assertEqual(self.pharmacy.street, 'Нова')
|
||
self.assertEqual(self.pharmacy.comments, 'Оновлено з порталу')
|
||
self.assertContains(resp, 'Изменения сохранены')
|
||
|
||
def _grant_pharmacies_section(self, user):
|
||
from django.contrib.auth.models import Permission
|
||
user.user_permissions.add(Permission.objects.get(
|
||
content_type__app_label='core', codename='access_pharmacies',
|
||
))
|
||
|
||
def test_list_modal_contains_edit_form_for_staff(self):
|
||
self._grant_pharmacies_section(self.staff)
|
||
self.client.force_login(self.staff)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
resp = self.client.get(
|
||
f'/pharmacies/{self.pharmacy.pk}/modal/',
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.assertContains(resp, 'id="pharmacy-edit-modal"')
|
||
self.assertContains(resp, 'name="source"')
|
||
self.assertContains(resp, 'Сохранить')
|
||
self.assertContains(resp, 'data-tab="address"')
|
||
self.assertContains(resp, 'id="pharmacy-picker-map"')
|
||
self.assertContains(resp, 'pharmacyModalTab')
|
||
|
||
def test_list_modal_save_returns_list(self):
|
||
from apps.locations.models import PharmacyCardVisibility
|
||
|
||
self._set_level('edit', PharmacyCardVisibility.Level.OWN_OR_STAFF)
|
||
self._grant_pharmacies_section(self.own)
|
||
self.client.force_login(self.own)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
resp = self.client.post(
|
||
f'/pharmacies/{self.pharmacy.pk}/edit/',
|
||
{
|
||
'source': 'list',
|
||
'organization': self.pharmacy.organization_id,
|
||
'pharmacy_number': self.pharmacy.pharmacy_number,
|
||
'status': 'open',
|
||
'category': 'A',
|
||
'vat_category': '',
|
||
'working_hours': '10:00-22:00',
|
||
'location_region': self.pharmacy.location_region_id,
|
||
'street': 'Модальна',
|
||
'house_number': '3',
|
||
'building': '',
|
||
'apartment': '',
|
||
'location': '46.4825,30.7233',
|
||
'phone': '',
|
||
'license_number': '',
|
||
'license_expiry_date': '',
|
||
'head_pharmacist': '',
|
||
'responsible_manager': '',
|
||
'curator': '',
|
||
'pharmacists_count': 2,
|
||
'comments': '',
|
||
},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.pharmacy.refresh_from_db()
|
||
self.assertEqual(self.pharmacy.working_hours, '10:00-22:00')
|
||
self.assertEqual(self.pharmacy.street, 'Модальна')
|
||
self.assertContains(resp, 'id="pharmacy-modal-root"')
|
||
|
||
def test_list_modal_save_map_coordinates(self):
|
||
from apps.locations.models import PharmacyCardVisibility
|
||
|
||
self._set_level('edit', PharmacyCardVisibility.Level.OWN_OR_STAFF)
|
||
self._grant_pharmacies_section(self.own)
|
||
self.client.force_login(self.own)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
resp = self.client.post(
|
||
f'/pharmacies/{self.pharmacy.pk}/edit/',
|
||
{
|
||
'source': 'list',
|
||
'organization': self.pharmacy.organization_id,
|
||
'pharmacy_number': self.pharmacy.pharmacy_number,
|
||
'status': 'open',
|
||
'category': 'A',
|
||
'vat_category': '',
|
||
'working_hours': self.pharmacy.working_hours or '08:00-20:00',
|
||
'location_region': self.pharmacy.location_region_id,
|
||
'street': self.pharmacy.street or 'Тестова',
|
||
'house_number': self.pharmacy.house_number or '1',
|
||
'building': '',
|
||
'apartment': '',
|
||
'location': '46.581955,30.7891',
|
||
'phone': '',
|
||
'license_number': '',
|
||
'license_expiry_date': '',
|
||
'head_pharmacist': '',
|
||
'responsible_manager': '',
|
||
'curator': '',
|
||
'pharmacists_count': 2,
|
||
'comments': '',
|
||
},
|
||
HTTP_HX_REQUEST='true',
|
||
)
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.pharmacy.refresh_from_db()
|
||
self.assertEqual(self.pharmacy.location, '46.581955,30.7891')
|
||
self.assertTrue(self.pharmacy.has_coordinates)
|
||
|
||
def test_pharmacies_excel_export(self):
|
||
self._grant_pharmacies_section(self.staff)
|
||
self.client.force_login(self.staff)
|
||
session = self.client.session
|
||
session['2fa_verified'] = True
|
||
session.save()
|
||
resp = self.client.get('/pharmacies/export.xlsx')
|
||
self.assertEqual(resp.status_code, 200)
|
||
self.assertIn('spreadsheetml', resp['Content-Type'])
|
||
self.assertTrue(resp.content.startswith(b'PK'))
|
||
|
||
def test_detail_shows_edit_button_for_staff(self):
|
||
self.client.force_login(self.staff)
|
||
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.assertTrue(resp.context['can_edit'])
|
||
self.assertContains(resp, 'edit=1')
|
||
self.assertNotContains(resp, 'Редактировать в админке')
|
||
|
||
def test_everyone_and_hidden(self):
|
||
from apps.locations.models import (
|
||
PharmacyCardVisibility, user_can_view_pharmacy_section,
|
||
)
|
||
|
||
everyone = PharmacyCardVisibility.Level.EVERYONE
|
||
hidden = PharmacyCardVisibility.Level.HIDDEN
|
||
self.assertFalse(user_can_view_pharmacy_section(self.stranger, self.pharmacy, everyone))
|
||
self.assertFalse(user_can_view_pharmacy_section(self.staff, self.pharmacy, hidden))
|
||
self.assertTrue(user_can_view_pharmacy_section(self.superuser, self.pharmacy, hidden))
|
||
|
||
def test_staff_and_it_levels(self):
|
||
from apps.locations.models import (
|
||
PharmacyCardVisibility, user_can_view_pharmacy_section,
|
||
)
|
||
|
||
staff_lvl = PharmacyCardVisibility.Level.STAFF
|
||
it_lvl = PharmacyCardVisibility.Level.IT
|
||
self.assertFalse(user_can_view_pharmacy_section(self.own, self.pharmacy, staff_lvl))
|
||
self.assertTrue(user_can_view_pharmacy_section(self.staff, self.pharmacy, staff_lvl))
|
||
self.assertTrue(user_can_view_pharmacy_section(self.it_user, self.pharmacy, it_lvl))
|
||
self.assertFalse(user_can_view_pharmacy_section(self.own, self.pharmacy, it_lvl))
|
||
self.assertFalse(user_can_view_pharmacy_section(self.staff, self.pharmacy, it_lvl))
|
||
|
||
def test_pharmacy_card_flags_respect_per_section(self):
|
||
from apps.locations.models import (
|
||
PharmacyCardVisibility, pharmacy_card_flags,
|
||
)
|
||
|
||
self._set_level('rro', PharmacyCardVisibility.Level.EVERYONE)
|
||
self._set_level('equipment', PharmacyCardVisibility.Level.HIDDEN)
|
||
flags = pharmacy_card_flags(self.stranger, self.pharmacy)
|
||
self.assertFalse(flags['can_view_rro'])
|
||
self.assertFalse(flags['can_view_equipment'])
|
||
|
||
def test_detail_view_hides_tab_when_section_staff_only(self):
|
||
from apps.locations.models import PharmacyCardVisibility
|
||
from apps.rro.models import CashRegister
|
||
from core.models import Directory, DirectoryType
|
||
|
||
status_type, _ = DirectoryType.objects.get_or_create(
|
||
code='rro_status', defaults={'name': 'Статус РРО', 'hierarchy_type': 'strict'},
|
||
)
|
||
status, _ = Directory.objects.get_or_create(
|
||
directory_type=status_type, code='active',
|
||
defaults={'name': 'Активный', 'is_active': True},
|
||
)
|
||
CashRegister.objects.create(
|
||
pharmacy=self.pharmacy, kind='prro', fiscal_number='FN-VIS-1',
|
||
status=status, is_primary=True,
|
||
)
|
||
self._set_level('rro', PharmacyCardVisibility.Level.STAFF)
|
||
|
||
self.client.force_login(self.own)
|
||
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.assertFalse(resp.context['can_view_rro'])
|
||
self.assertNotContains(resp, 'FN-VIS-1')
|
||
self.assertNotContains(resp, 'ph-panel-rro')
|
||
|
||
self.client.force_login(self.staff)
|
||
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.assertTrue(resp.context.get('can_view_rro'))
|
||
self.assertContains(resp, 'FN-VIS-1')
|
||
self.assertContains(resp, 'ph-panel-rro')
|
||
|
||
|
||
class PharmacyHistoryTests(TestCase):
|
||
"""PharmacyHistory заполняется при изменении аптеки, а не при создании."""
|
||
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
org_type = DirectoryType.objects.create(code='organization', name='Организация')
|
||
region_type = DirectoryType.objects.create(code='region', name='Регион')
|
||
cls.region = Directory.objects.create(directory_type=region_type, code='od', name='Одесса')
|
||
cls.aks = Directory.objects.create(
|
||
directory_type=org_type, code='akc', name='АКС', extra_data={'mail_prefix': 'a'})
|
||
|
||
def _pharmacy(self, number=1, **kw):
|
||
return Pharmacy.objects.create(
|
||
organization=self.aks, location_region=self.region,
|
||
pharmacy_number=number, location_type='pharmacy', **kw)
|
||
|
||
def test_create_does_not_write_history(self):
|
||
from apps.locations.models import PharmacyHistory
|
||
p = self._pharmacy(30)
|
||
self.assertEqual(PharmacyHistory.objects.filter(pharmacy=p).count(), 0)
|
||
|
||
def test_field_change_writes_history(self):
|
||
from apps.locations.models import PharmacyHistory
|
||
p = self._pharmacy(31, phone='+380501111111')
|
||
p.phone = '+380502222222'
|
||
p.save()
|
||
rows = list(PharmacyHistory.objects.filter(pharmacy=p))
|
||
self.assertEqual(len(rows), 1)
|
||
self.assertEqual(rows[0].changed_field, 'phone')
|
||
self.assertEqual(rows[0].old_value, '+380501111111')
|
||
self.assertEqual(rows[0].new_value, '+380502222222')
|
||
self.assertIsNone(rows[0].changed_by)
|
||
|
||
def test_unchanged_save_writes_nothing(self):
|
||
from apps.locations.models import PharmacyHistory
|
||
p = self._pharmacy(32)
|
||
p.save()
|
||
self.assertEqual(PharmacyHistory.objects.filter(pharmacy=p).count(), 0)
|
||
|
||
def test_history_user_is_recorded(self):
|
||
from apps.locations.models import PharmacyHistory
|
||
from apps.employees.tests import make_employee
|
||
user = make_employee('hist-user')
|
||
p = self._pharmacy(33, working_hours='08:00-20:00')
|
||
p.working_hours = '09:00-21:00'
|
||
p._history_user = user
|
||
p.save()
|
||
row = PharmacyHistory.objects.get(pharmacy=p, changed_field='working_hours')
|
||
self.assertEqual(row.changed_by, user)
|
||
|
||
def test_untracked_update_fields_skip_history(self):
|
||
from apps.locations.models import PharmacyHistory
|
||
p = self._pharmacy(34, carbonio_sync_status='pending')
|
||
p.carbonio_sync_status = 'synced'
|
||
p.save(update_fields=['carbonio_sync_status'])
|
||
self.assertEqual(PharmacyHistory.objects.filter(pharmacy=p).count(), 0)
|
||
|
||
|
||
class LocationSortKeyTests(TestCase):
|
||
"""Селекты IT: офис сверху, аптеки по коду А1…А70, не по адресу."""
|
||
|
||
def test_pharmacies_sorted_by_object_code(self):
|
||
from apps.locations.models.base import Location, location_sort_key
|
||
|
||
org_type, _ = DirectoryType.objects.get_or_create(
|
||
code='organization', defaults={'name': 'Организация'},
|
||
)
|
||
region_type, _ = DirectoryType.objects.get_or_create(
|
||
code='region', defaults={'name': 'Регион'},
|
||
)
|
||
region = Directory.objects.create(directory_type=region_type, code='od-sort', name='Одесса-sort')
|
||
org_a = Directory.objects.create(
|
||
directory_type=org_type, code='aks-sort', name='АКС',
|
||
extra_data={'mail_prefix': 'a'},
|
||
)
|
||
org_p = Directory.objects.create(
|
||
directory_type=org_type, code='pent-sort', name='Пентальфа',
|
||
extra_data={'mail_prefix': 'p'},
|
||
)
|
||
office = Location.objects.create(
|
||
location_type='office', location_region=region,
|
||
street='Канатная', house_number='78', is_active=True,
|
||
)
|
||
p68 = Pharmacy.objects.create(
|
||
organization=org_a, location_region=region,
|
||
pharmacy_number=68, location_type='pharmacy',
|
||
street='Танцюри', house_number='16', is_active=True,
|
||
)
|
||
p3 = Pharmacy.objects.create(
|
||
organization=org_a, location_region=region,
|
||
pharmacy_number=3, location_type='pharmacy',
|
||
street='Ясная', house_number='1', is_active=True,
|
||
)
|
||
p1 = Pharmacy.objects.create(
|
||
organization=org_p, location_region=region,
|
||
pharmacy_number=1, location_type='pharmacy',
|
||
street='Академическая', house_number='2', is_active=True,
|
||
)
|
||
ordered = sorted([p68, office, p1, p3], key=location_sort_key)
|
||
self.assertEqual(ordered[0].pk, office.pk)
|
||
self.assertEqual(
|
||
[location_sort_key(x)[1:3] for x in ordered[1:]],
|
||
[(0, 3), (0, 68), (1, 1)], # А3, А68, П1
|
||
)
|
||
|
||
def test_closed_pharmacy_label_and_it_locations(self):
|
||
from apps.it_manager.views import _it_locations_sorted
|
||
from apps.locations.models.base import Location
|
||
from core.test_utils import grant_all_locations
|
||
from apps.employees.tests import make_employee
|
||
|
||
org_type, _ = DirectoryType.objects.get_or_create(
|
||
code='organization', defaults={'name': 'Организация'},
|
||
)
|
||
region_type, _ = DirectoryType.objects.get_or_create(
|
||
code='region', defaults={'name': 'Регион'},
|
||
)
|
||
region = Directory.objects.create(
|
||
directory_type=region_type, code='od-closed', name='Одесса-closed',
|
||
)
|
||
org = Directory.objects.create(
|
||
directory_type=org_type, code='aks-closed', name='АКС',
|
||
extra_data={'mail_prefix': 'a'},
|
||
)
|
||
closed = Pharmacy.objects.create(
|
||
organization=org, location_region=region,
|
||
pharmacy_number=99, location_type='pharmacy',
|
||
street='Закрытая', house_number='1',
|
||
status='closed',
|
||
)
|
||
self.assertFalse(closed.is_active)
|
||
# В селектах IT — Location.__str__ (не Pharmacy.__str__)
|
||
label = str(Location.objects.get(pk=closed.pk))
|
||
self.assertIn('А99', label)
|
||
self.assertIn('Закрыта', label)
|
||
|
||
user = make_employee('it-loc-closed')
|
||
grant_all_locations(user)
|
||
ids = {loc.pk for loc in _it_locations_sorted(user)}
|
||
self.assertIn(closed.pk, ids)
|
||
# Неактивный офис не попадает
|
||
dead_office = Location.objects.create(
|
||
location_type='office', location_region=region,
|
||
street='Старый', house_number='2', is_active=False,
|
||
)
|
||
self.assertNotIn(dead_office.pk, ids)
|
||
|
||
def test_vendor_location_in_select_and_report_code(self):
|
||
from apps.it_manager.views import _it_locations_sorted
|
||
from apps.locations.models.base import Location
|
||
from apps.reports.it_monthly.builder import location_object_code
|
||
from core.test_utils import grant_all_locations
|
||
from apps.employees.tests import make_employee
|
||
|
||
region_type, _ = DirectoryType.objects.get_or_create(
|
||
code='region', defaults={'name': 'Регион'},
|
||
)
|
||
region = Directory.objects.create(
|
||
directory_type=region_type, code='od-vendor', name='Одесса-vendor',
|
||
)
|
||
vendor = Location.objects.create(
|
||
location_type='vendor',
|
||
name='Вайтер Юг',
|
||
location_region=region,
|
||
is_active=True,
|
||
)
|
||
self.assertEqual(str(vendor), 'Вайтер Юг')
|
||
self.assertEqual(location_object_code(vendor), 'Вайтер Юг')
|
||
|
||
user = make_employee('it-loc-vendor')
|
||
grant_all_locations(user)
|
||
ids = {loc.pk for loc in _it_locations_sorted(user)}
|
||
self.assertIn(vendor.pk, ids)
|
||
|
||
|
||
class DislocationVendorsTabTests(TestCase):
|
||
"""Вкладка «Сервисы / поставщики» на /dislocation/."""
|
||
|
||
@classmethod
|
||
def setUpTestData(cls):
|
||
from django.contrib.auth.models import Group
|
||
from apps.employees.tests import make_employee
|
||
from apps.locations.models.base import Location
|
||
|
||
region_type, _ = DirectoryType.objects.get_or_create(
|
||
code='region', defaults={'name': 'Регион'},
|
||
)
|
||
cls.region = Directory.objects.create(
|
||
directory_type=region_type, code='od-dislo-v', name='Одесса-dislo-v',
|
||
)
|
||
cls.vendor = Location.objects.create(
|
||
location_type='vendor',
|
||
name='ТестСервис',
|
||
location_region=cls.region,
|
||
is_active=True,
|
||
)
|
||
cls.viewer = make_employee('dislo-vend-viewer', user_location_type='office')
|
||
dept_type, _ = DirectoryType.objects.get_or_create(
|
||
code='department', defaults={'name': 'Отдел'},
|
||
)
|
||
dept = Directory.objects.create(directory_type=dept_type, code='it-dv', name='IT-dv')
|
||
cls.viewer.department = dept
|
||
cls.viewer.save(update_fields=['department'])
|
||
cls.viewer.groups.add(Group.objects.get_or_create(name='Сотрудники (тест)')[0])
|
||
|
||
cls.it_user = make_employee('dislo-vend-it', user_location_type='office')
|
||
cls.it_user.department = dept
|
||
cls.it_user.save(update_fields=['department'])
|
||
cls.it_user.groups.add(Group.objects.get_or_create(name='IT')[0])
|
||
|
||
def test_tab_lists_vendors(self):
|
||
from django.test import Client
|
||
from core.test_utils import login_2fa
|
||
|
||
client = Client()
|
||
login_2fa(client, self.viewer)
|
||
r = client.get('/dislocation/', {'tab': 'vendors'}, HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r.status_code, 200)
|
||
self.assertContains(r, 'Сервисы / поставщики')
|
||
self.assertContains(r, 'ТестСервис')
|
||
self.assertNotContains(r, 'Добавить')
|
||
|
||
def test_vendor_form_is_popup(self):
|
||
from django.test import Client
|
||
from core.test_utils import login_2fa
|
||
|
||
client = Client()
|
||
login_2fa(client, self.it_user)
|
||
r = client.get('/dislocation/', {'tab': 'vendors'}, HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r.status_code, 200)
|
||
self.assertContains(r, 'id="dislo-vendor-panel"')
|
||
self.assertContains(r, 'role="dialog"')
|
||
self.assertNotContains(r, 'id="dislo-vendor-panel-overlay"')
|
||
self.assertNotContains(r, 'translateX(100%)')
|
||
|
||
def test_it_can_create_vendor(self):
|
||
from django.test import Client
|
||
from apps.locations.models.base import Location
|
||
from core.test_utils import login_2fa
|
||
|
||
client = Client()
|
||
login_2fa(client, self.it_user)
|
||
r = client.post('/dislocation/vendors/create/', {
|
||
'name': 'Новый Поставщик',
|
||
'location_region': str(self.region.pk),
|
||
'is_active': 'on',
|
||
}, HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r.status_code, 200)
|
||
self.assertTrue(
|
||
Location.objects.filter(location_type='vendor', name='Новый Поставщик').exists()
|
||
)
|
||
self.assertContains(r, 'Новый Поставщик')
|
||
|
||
def test_viewer_cannot_create_vendor(self):
|
||
from django.test import Client
|
||
from core.test_utils import login_2fa
|
||
|
||
client = Client()
|
||
login_2fa(client, self.viewer)
|
||
r = client.post('/dislocation/vendors/create/', {
|
||
'name': 'Запрещённый',
|
||
'location_region': str(self.region.pk),
|
||
}, HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r.status_code, 403)
|
||
|
||
def test_it_can_update_vendor(self):
|
||
from django.test import Client
|
||
from core.test_utils import login_2fa
|
||
|
||
client = Client()
|
||
login_2fa(client, self.it_user)
|
||
r = client.post(f'/dislocation/vendors/{self.vendor.pk}/update/', {
|
||
'name': 'ТестСервис Обновлён',
|
||
'location_region': str(self.region.pk),
|
||
'comments': 'ремонт техники',
|
||
'is_active': 'on',
|
||
}, HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r.status_code, 200)
|
||
self.vendor.refresh_from_db()
|
||
self.assertEqual(self.vendor.name, 'ТестСервис Обновлён')
|
||
self.assertEqual(self.vendor.comments, 'ремонт техники')
|
||
|
||
def test_vendor_detail_card_and_map(self):
|
||
from django.test import Client
|
||
from core.test_utils import login_2fa
|
||
|
||
client = Client()
|
||
login_2fa(client, self.viewer)
|
||
r = client.get(f'/dislocation/vendors/{self.vendor.pk}/', HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r.status_code, 200)
|
||
self.assertContains(r, 'ТестСервис')
|
||
self.assertContains(r, 'Координаты не указаны')
|
||
|
||
login_2fa(client, self.it_user)
|
||
r2 = client.post(f'/dislocation/vendors/{self.vendor.pk}/update/', {
|
||
'name': 'ТестСервис',
|
||
'location_region': str(self.region.pk),
|
||
'location': '46.482500,30.723300',
|
||
'is_active': 'on',
|
||
'from_card': '1',
|
||
}, HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r2.status_code, 200)
|
||
self.vendor.refresh_from_db()
|
||
# Дефолт поля location — как раз Одесса; has_coordinates False для дефолта.
|
||
# Ставим недефолтные координаты.
|
||
self.vendor.location = '46.500000,30.750000'
|
||
self.vendor.save(update_fields=['location', 'address'])
|
||
r3 = client.get(f'/dislocation/vendors/{self.vendor.pk}/', HTTP_HX_REQUEST='true')
|
||
self.assertEqual(r3.status_code, 200)
|
||
self.assertContains(r3, 'vendor-map')
|
||
self.assertContains(r3, '46')
|
||
self.assertContains(r3, '30,75000')
|