From 777de2adea41aa0caf6fbacd5b75a7630702ed08 Mon Sep 17 00:00:00 2001 From: lokses Date: Wed, 7 Oct 2026 15:39:56 +0300 Subject: [PATCH] =?UTF-8?q?it=5Fmanager:=20=D1=81=D0=BE=D1=80=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BB=D0=BE=D0=BA=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=BF=D0=BE=20=D0=BA=D0=BE=D0=B4=D1=83=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=BE=D0=BA=20=D0=B2=20=D1=81=D0=B5=D0=BB=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Аптеки А1…А70/П… вместо сортировки по адресу; убрать лимит 50 в searchable-select. Co-authored-by: Cursor --- apps/it_manager/views.py | 10 ++++-- apps/locations/models/base.py | 31 +++++++++++++++++ apps/locations/tests.py | 48 +++++++++++++++++++++++++++ static/assets/css/input.css | 2 +- static/assets/css/main.css | 2 +- static/assets/js/searchable-select.js | 4 ++- 6 files changed, 92 insertions(+), 5 deletions(-) diff --git a/apps/it_manager/views.py b/apps/it_manager/views.py index e5b9fc7..122646b 100644 --- a/apps/it_manager/views.py +++ b/apps/it_manager/views.py @@ -88,10 +88,16 @@ def _it_locations_qs(user): queryset=Pharmacy.objects.select_related('organization'), ), ) - .order_by('location_type', 'address') ) +def _it_locations_sorted(user) -> list: + """Офис/склад, затем аптеки по коду объекта (А1…А70, П1…), не по адресу.""" + from apps.locations.models.base import location_sort_key + + return sorted(_it_locations_qs(user), key=location_sort_key) + + def _split_it_locations(locations): pharmacies, offices = [], [] for loc in locations: @@ -301,7 +307,7 @@ def _it_manager_context(request): .annotate(units_total=models.Count('units', filter=Q(units__in=scoped_queryset(request.user, Equipment.objects.all())))) .order_by('equipment_type__name', 'brand__name', 'name') ) - locations_all = list(_it_locations_qs(request.user)) + locations_all = _it_locations_sorted(request.user) it_pharmacies, it_offices = _split_it_locations(locations_all) tickets_qs = ( diff --git a/apps/locations/models/base.py b/apps/locations/models/base.py index 13dcff6..6da5e8c 100644 --- a/apps/locations/models/base.py +++ b/apps/locations/models/base.py @@ -71,6 +71,37 @@ def pharmacy_search_text(pharmacy) -> str: return ' '.join(parts) +# Порядок юрлиц в селектах IT: А…, П…, Т…, Г…, Л…, прочие +_OBJECT_LETTER_ORDER = {'а': 0, 'п': 1, 'т': 2, 'г': 3, 'л': 4} +_OBJECT_CODE_RE = re.compile(r'^([АAПPТTГGЛL])(\d+)$', re.I) + + +def location_sort_key(location) -> tuple: + """Сортировка локаций: офис/склад, затем аптеки по коду А1…А70, П1…""" + loc_type = getattr(location, 'location_type', '') or '' + if loc_type == 'office': + return (0, 0, 0, (getattr(location, 'address', None) or '').casefold()) + if loc_type == 'warehouse': + return (0, 1, 0, (getattr(location, 'address', None) or '').casefold()) + + pharmacy = None + try: + pharmacy = location.pharmacy + except Exception: + pharmacy = None + code = pharmacy_object_code(pharmacy) if pharmacy else '' + m = _OBJECT_CODE_RE.match(code) + if m: + letter = m.group(1).upper().translate(_LAT_TO_CYR).lower() + return (1, _OBJECT_LETTER_ORDER.get(letter, 9), int(m.group(2)), code) + if code.startswith('№'): + try: + return (1, 8, int(code[1:]), code) + except ValueError: + pass + return (1, 9, 0, (getattr(location, 'address', None) or code or '').casefold()) + + class LocationManager(models.Manager): def active(self): diff --git a/apps/locations/tests.py b/apps/locations/tests.py index ee91614..69beeff 100644 --- a/apps/locations/tests.py +++ b/apps/locations/tests.py @@ -590,3 +590,51 @@ class PharmacyHistoryTests(TestCase): 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 + ) diff --git a/static/assets/css/input.css b/static/assets/css/input.css index f1c0be1..f23658f 100644 --- a/static/assets/css/input.css +++ b/static/assets/css/input.css @@ -478,7 +478,7 @@ left: 0; right: 0; width: 100%; - max-height: 18rem; + max-height: min(70vh, 28rem); overflow-y: auto; background: var(--color-card); border: 1px solid var(--color-border); diff --git a/static/assets/css/main.css b/static/assets/css/main.css index e39bc4f..b9918b4 100644 --- a/static/assets/css/main.css +++ b/static/assets/css/main.css @@ -1128,7 +1128,7 @@ left: 0; right: 0; width: 100%; - max-height: 18rem; + max-height: min(70vh, 28rem); overflow-y: auto; background: var(--color-card); border: 1px solid var(--color-border); diff --git a/static/assets/js/searchable-select.js b/static/assets/js/searchable-select.js index 527a6c6..c0724be 100644 --- a/static/assets/js/searchable-select.js +++ b/static/assets/js/searchable-select.js @@ -58,12 +58,14 @@ var q = foldLookalikes(query.trim()); // також без пробілів: «а 3» → «а3» var qCompact = q.replace(/\s+/g, ''); + // Без лимита: иначе при пустом запросе видны только первые ~50 опций + // (аптеки «ниже по списку» пропадали). Скролл в dropdown справляется. var matches = options(select).filter(function (o) { if (!q) return true; var hay = optionHaystack(o); var hayCompact = hay.replace(/\s+/g, ''); return hay.indexOf(q) !== -1 || (qCompact && hayCompact.indexOf(qCompact) !== -1); - }).slice(0, 50); + }); results.innerHTML = ''; matches.forEach(function (opt) {