Доменные модули: сотрудники, локации, найм, счётчики, посещения
- 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:
+55
-29
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user