Files
ACORE/apps/locations/tests.py
T

954 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_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')