Доменные модули: сотрудники, локации, найм, счётчики, посещения

- employees: last_seen, internal_number, default_page_size; управление-команды
- locations: категория/куратор/ответственный аптеки, seed должностей;
  Location.__str__ показывает номер аптеки (не только адрес)
- hiring: HiringRequest (заявки на найм) + тесты (7)
- utilities: UtilityReading (показания счётчиков по аптекам)
- visit_manager: правки

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 17:33:16 +03:00
parent 81dee5cba2
commit 6ff01c4119
39 changed files with 1984 additions and 40 deletions
+55 -29
View File
@@ -1,6 +1,8 @@
from django import forms
from django.contrib import admin
from django.db import models
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from unfold.admin import ModelAdmin, TabularInline
@@ -32,6 +34,27 @@ class HierarchicalRegionChoiceField(forms.ModelChoiceField):
return f'{indent}{obj.name}'
class HasCoordinatesFilter(admin.SimpleListFilter):
"""Записи, які так і лишились на дефолтних координатах поля (центр Одеси) —
тобто піну ніхто не ставив і геокодування по адресі не спрацювало."""
title = _('Координаты на карте')
parameter_name = 'has_coordinates'
def lookups(self, request, model_admin):
return [
('no', _('Без координат')),
('yes', _('С координатами')),
]
def queryset(self, request, queryset):
default = queryset.model._meta.get_field('location').default
if self.value() == 'no':
return queryset.filter(models.Q(location=default) | models.Q(location=''))
if self.value() == 'yes':
return queryset.exclude(location=default).exclude(location='')
return queryset
# ---------------------------------------------------------------------------
# Location
# ---------------------------------------------------------------------------
@@ -56,15 +79,18 @@ class LocationAdmin(ModelAdmin, ImportExportModelAdmin):
export_form_class = ExportForm
class Media:
css = {'all': ('locations/css/location_admin.css',)}
js = ('assets/js/admin_leaflet_resize_fix.js',)
list_display = ['address_display', 'location_type', 'location_region',
'coordinates_display', 'is_active', 'updated_at']
list_filter = ['location_type', 'is_active']
list_filter = ['location_type', 'is_active', HasCoordinatesFilter]
search_fields = ['address', 'street', 'house_number', 'comments']
list_select_related = ['location_region']
ordering = ['location_region', 'address']
readonly_fields = ['created_at', 'updated_at']
# address тепер лише генерується в Location.save() з street/house_number/building/
# apartment — ручний ввід сюди все одно перезатреться при збереженні
readonly_fields = ['address', 'created_at', 'updated_at']
fieldsets = (
(_('Тип и регион'), {
@@ -109,7 +135,7 @@ class LocationAdmin(ModelAdmin, ImportExportModelAdmin):
def coordinates_display(self, obj):
if obj.has_coordinates and obj.latitude is not None and obj.longitude is not None:
return format_html('📍 {}', f'{float(obj.latitude):.5f}, {float(obj.longitude):.5f}')
return '—'
return mark_safe('<span style="color:#dc2626;">⚠ немає координат</span>')
coordinates_display.short_description = _('Координаты')
coordinates_display.admin_order_field = 'location'
@@ -199,7 +225,7 @@ class PharmacyAdmin(ModelAdmin, ImportExportModelAdmin):
class Media:
css = {'all': ('locations/css/location_admin.css',)}
js = ('assets/js/admin_phone_format.js',)
js = ('assets/js/admin_phone_format.js', 'assets/js/admin_leaflet_resize_fix.js')
# ------------------------------------------------------------------
# List view
@@ -208,36 +234,45 @@ class PharmacyAdmin(ModelAdmin, ImportExportModelAdmin):
list_display = [
'pharmacy_display',
'organization',
'category',
'vat_category',
'location_region',
'coordinates_display',
'phone_numbers_display',
'status_display',
'status',
'license_status_display',
'head_pharmacist',
'responsible_manager',
'curator',
'employees_count_display',
'working_hours',
'is_active',
]
list_editable = ['working_hours', 'is_active']
list_filter = ['organization', 'status', 'is_active', LicenseStatusFilter]
list_editable = ['category', 'vat_category', 'status', 'working_hours']
list_filter = ['organization', 'category', 'vat_category', 'status', 'is_active', LicenseStatusFilter, HasCoordinatesFilter]
search_fields = ['pharmacy_number', 'address', 'phone', 'license_number',
'head_pharmacist__last_name']
list_select_related = ['organization', 'location_region', 'head_pharmacist']
'head_pharmacist__last_name', 'responsible_manager__last_name', 'curator__last_name']
list_select_related = ['organization', 'location_region', 'head_pharmacist', 'responsible_manager', 'curator']
ordering = ['organization', 'pharmacy_number']
# ------------------------------------------------------------------
# Detail view — tabs
# ------------------------------------------------------------------
# address тепер лише генерується в Location.save() з street/house_number/building/
# apartment — ручний ввід сюди все одно перезатреться при збереженні
# is_active тепер похідне поле від status (Pharmacy.save()) — ручне
# редагування нічого не дасть, тому лише readonly-показ.
readonly_fields = [
'employees_count_display', 'license_status_display',
'created_at', 'updated_at',
'address', 'employees_count_display', 'license_status_display',
'is_active', 'created_at', 'updated_at',
]
fieldsets = (
(_('Основное'), {
'classes': ('tab',),
'fields': (
'organization', 'pharmacy_number',
'organization', 'pharmacy_number', 'category', 'vat_category',
'status', 'is_active', 'working_hours',
'location_type',
),
@@ -265,8 +300,8 @@ class PharmacyAdmin(ModelAdmin, ImportExportModelAdmin):
(_('Персонал'), {
'classes': ('tab',),
'fields': (
'head_pharmacist', 'pharmacists_count',
'employees_count_display',
'head_pharmacist', 'responsible_manager', 'curator',
'pharmacists_count', 'employees_count_display',
),
}),
(_('Примечания'), {
@@ -300,21 +335,12 @@ class PharmacyAdmin(ModelAdmin, ImportExportModelAdmin):
pharmacy_display.short_description = _('Аптека')
pharmacy_display.admin_order_field = 'pharmacy_number'
STATUS_COLORS = {
'open': '#16a34a',
'closed': '#dc2626',
'suspended': '#ca8a04',
'preparing': '#2563eb',
}
def status_display(self, obj):
color = self.STATUS_COLORS.get(obj.status, '#6b7280')
return format_html(
'<span style="color:{};font-weight:bold;">{}</span>',
color, obj.get_status_display(),
)
status_display.short_description = _('Статус')
status_display.admin_order_field = 'status'
def coordinates_display(self, obj):
if obj.has_coordinates and obj.latitude is not None and obj.longitude is not None:
return format_html('📍 {}', f'{float(obj.latitude):.5f}, {float(obj.longitude):.5f}')
return mark_safe('<span style="color:#dc2626;">⚠ немає координат</span>')
coordinates_display.short_description = _('Координаты')
coordinates_display.admin_order_field = 'location'
def license_status_display(self, obj):
status = obj.license_status