Initial commit — ACORE pharmacy network management system
Django project: employees, locations, visit_manager, it_manager, mattermost_connector and stub connectors (ad, mail, portal).
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from unfold.admin import ModelAdmin, TabularInline
|
||||
|
||||
from .models import (
|
||||
Equipment, EquipmentAssignment,
|
||||
Ticket, TicketComment,
|
||||
InternetProvider, InternetContract,
|
||||
WorkLog, ServiceCall,
|
||||
Consumable,
|
||||
)
|
||||
|
||||
|
||||
class LocationChoiceField(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
type_label = obj.get_location_type_display()
|
||||
addr = obj.get_full_address() or obj.address or ''
|
||||
if obj.location_type == 'pharmacy':
|
||||
try:
|
||||
num = obj.pharmacy.pharmacy_number
|
||||
return f'Аптека №{num} — {addr}'
|
||||
except Exception:
|
||||
pass
|
||||
return f'{type_label} — {addr}' if addr else type_label
|
||||
|
||||
|
||||
class InternetContractForm(forms.ModelForm):
|
||||
location = LocationChoiceField(
|
||||
queryset=None,
|
||||
label=_('Локация'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
from apps.locations.models import Location
|
||||
self.fields['location'].queryset = (
|
||||
Location.objects
|
||||
.filter(is_active=True)
|
||||
.select_related('location_region')
|
||||
.prefetch_related('pharmacy')
|
||||
.order_by('location_type', 'address')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InternetContract
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Equipment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EquipmentAssignmentInline(TabularInline):
|
||||
model = EquipmentAssignment
|
||||
extra = 0
|
||||
fields = ('assigned_to', 'location', 'assigned_by', 'assigned_at', 'returned_at', 'notes')
|
||||
readonly_fields = ('assigned_at',)
|
||||
ordering = ('-assigned_at',)
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
|
||||
class ConsumableInline(TabularInline):
|
||||
model = Consumable
|
||||
extra = 0
|
||||
fields = ('consumable_type', 'brand', 'model_number', 'color', 'status',
|
||||
'page_yield', 'installed_date', 'replaced_date')
|
||||
ordering = ('color',)
|
||||
verbose_name = 'Расходник'
|
||||
verbose_name_plural = 'Расходники (картриджи, тонеры)'
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request)
|
||||
|
||||
|
||||
@admin.register(Equipment)
|
||||
class EquipmentAdmin(ModelAdmin):
|
||||
list_display = [
|
||||
'type_display', 'brand', 'model', 'serial_number',
|
||||
'status_display', 'assigned_to', 'location',
|
||||
'warranty_display', 'updated_at',
|
||||
]
|
||||
list_filter = ['equipment_type', 'status', 'location__location_type']
|
||||
search_fields = ['brand', 'model', 'serial_number', 'inventory_number', 'notes']
|
||||
list_select_related = ['assigned_to', 'location']
|
||||
ordering = ['equipment_type', 'brand', 'model']
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'warranty_display']
|
||||
|
||||
fieldsets = (
|
||||
(_('Оборудование'), {
|
||||
'fields': ('equipment_type', 'brand', 'model', 'serial_number', 'inventory_number'),
|
||||
}),
|
||||
(_('Статус и размещение'), {
|
||||
'fields': ('status', 'assigned_to', 'location'),
|
||||
}),
|
||||
(_('Гарантия и закупка'), {
|
||||
'fields': ('purchase_date', 'warranty_until', 'warranty_display'),
|
||||
}),
|
||||
(_('Примечания'), {
|
||||
'fields': ('notes',),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
(_('Служебное'), {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
inlines = [EquipmentAssignmentInline, ConsumableInline]
|
||||
|
||||
def get_inlines(self, request, obj):
|
||||
inlines = [EquipmentAssignmentInline]
|
||||
if obj and obj.equipment_type in ('printer', 'scanner'):
|
||||
inlines.append(ConsumableInline)
|
||||
return inlines
|
||||
|
||||
EQUIPMENT_ICONS = {
|
||||
'laptop': '💻', 'desktop': '🖥️', 'monitor': '🖥️',
|
||||
'printer': '🖨️', 'phone': '📱', 'tablet': '📟',
|
||||
'router': '📡', 'ups': '🔋', 'scanner': '📠',
|
||||
'other': '📦',
|
||||
}
|
||||
|
||||
STATUS_COLORS = {
|
||||
'stock': '#6b7280',
|
||||
'assigned': '#16a34a',
|
||||
'repair': '#ca8a04',
|
||||
'written_off': '#dc2626',
|
||||
}
|
||||
|
||||
def type_display(self, obj):
|
||||
icon = self.EQUIPMENT_ICONS.get(obj.equipment_type, '📦')
|
||||
return format_html('{} {}', icon, obj.get_equipment_type_display())
|
||||
type_display.short_description = _('Тип')
|
||||
type_display.admin_order_field = 'equipment_type'
|
||||
|
||||
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 warranty_display(self, obj):
|
||||
status = obj.warranty_status
|
||||
if status == 'Действительна':
|
||||
return format_html('<span style="color:#16a34a;">{}</span>', status)
|
||||
if 'Истекает' in status:
|
||||
return format_html('<span style="color:#ca8a04;">{}</span>', status)
|
||||
if status == 'Истекла':
|
||||
return format_html('<span style="color:#dc2626;">{}</span>', status)
|
||||
return status
|
||||
warranty_display.short_description = _('Гарантия')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Consumables
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@admin.register(Consumable)
|
||||
class ConsumableAdmin(ModelAdmin):
|
||||
list_display = [
|
||||
'consumable_type_display', 'brand', 'model_number',
|
||||
'color_display', 'status_display', 'equipment', 'location',
|
||||
'page_yield', 'installed_date',
|
||||
]
|
||||
list_filter = ['consumable_type', 'status', 'color']
|
||||
search_fields = ['brand', 'model_number', 'notes',
|
||||
'equipment__brand', 'equipment__model']
|
||||
list_select_related = ['equipment', 'location']
|
||||
ordering = ['consumable_type', 'color', 'brand']
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
(_('Расходник'), {
|
||||
'fields': ('consumable_type', 'brand', 'model_number', 'color'),
|
||||
}),
|
||||
(_('Статус и размещение'), {
|
||||
'fields': ('status', 'equipment', 'location'),
|
||||
}),
|
||||
(_('Эксплуатация'), {
|
||||
'fields': ('page_yield', 'purchase_date', 'installed_date', 'replaced_date'),
|
||||
}),
|
||||
(_('Примечания'), {
|
||||
'fields': ('notes',),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
(_('Служебное'), {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
COLOR_CIRCLES = {
|
||||
'black': '#1f2937',
|
||||
'cyan': '#0891b2',
|
||||
'magenta': '#db2777',
|
||||
'yellow': '#ca8a04',
|
||||
'color': 'linear-gradient(135deg,#0891b2,#db2777,#ca8a04)',
|
||||
'na': '#9ca3af',
|
||||
}
|
||||
|
||||
STATUS_COLORS = {
|
||||
'in_stock': '#16a34a',
|
||||
'installed': '#2563eb',
|
||||
'empty': '#dc2626',
|
||||
'ordered': '#ca8a04',
|
||||
'disposed': '#9ca3af',
|
||||
}
|
||||
|
||||
def consumable_type_display(self, obj):
|
||||
return obj.get_consumable_type_display()
|
||||
consumable_type_display.short_description = _('Тип')
|
||||
consumable_type_display.admin_order_field = 'consumable_type'
|
||||
|
||||
def color_display(self, obj):
|
||||
bg = self.COLOR_CIRCLES.get(obj.color, '#9ca3af')
|
||||
style = f'background:{bg}' if 'gradient' not in bg else f'background-image:{bg}'
|
||||
return format_html(
|
||||
'<span style="display:inline-flex;align-items:center;gap:6px;">'
|
||||
'<span style="display:inline-block;width:12px;height:12px;'
|
||||
'border-radius:50%;{};border:1px solid #e5e7eb;"></span>{}'
|
||||
'</span>',
|
||||
style, obj.get_color_display(),
|
||||
)
|
||||
color_display.short_description = _('Цвет')
|
||||
color_display.admin_order_field = 'color'
|
||||
|
||||
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'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tickets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TicketCommentInline(TabularInline):
|
||||
model = TicketComment
|
||||
extra = 1
|
||||
fields = ('author', 'text', 'is_internal', 'created_at')
|
||||
readonly_fields = ('created_at',)
|
||||
ordering = ('created_at',)
|
||||
|
||||
|
||||
@admin.register(Ticket)
|
||||
class TicketAdmin(ModelAdmin):
|
||||
list_display = [
|
||||
'number', 'title', 'ticket_type_display', 'priority_display',
|
||||
'status_display', 'requester', 'assignee', 'created_at', 'overdue_display',
|
||||
]
|
||||
list_filter = ['status', 'priority', 'ticket_type', 'assignee']
|
||||
search_fields = ['number', 'title', 'description', 'requester__last_name',
|
||||
'requester__first_name']
|
||||
list_select_related = ['requester', 'assignee', 'equipment', 'location']
|
||||
ordering = ['-created_at']
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
readonly_fields = ['number', 'created_at', 'updated_at', 'resolved_at', 'closed_at']
|
||||
|
||||
fieldsets = (
|
||||
(_('Заявка'), {
|
||||
'fields': ('number', 'title', 'ticket_type', 'priority', 'status'),
|
||||
}),
|
||||
(_('Участники'), {
|
||||
'fields': ('requester', 'assignee'),
|
||||
}),
|
||||
(_('Связи'), {
|
||||
'fields': ('equipment', 'location'),
|
||||
}),
|
||||
(_('Описание и решение'), {
|
||||
'fields': ('description', 'resolution'),
|
||||
}),
|
||||
(_('Даты'), {
|
||||
'fields': ('created_at', 'updated_at', 'resolved_at', 'closed_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
inlines = [TicketCommentInline]
|
||||
|
||||
actions = ['mark_in_progress', 'mark_resolved', 'mark_closed']
|
||||
|
||||
PRIORITY_COLORS = {
|
||||
'low': '#6b7280',
|
||||
'medium': '#2563eb',
|
||||
'high': '#ca8a04',
|
||||
'critical': '#dc2626',
|
||||
}
|
||||
|
||||
STATUS_COLORS = {
|
||||
'new': '#2563eb',
|
||||
'in_progress': '#ca8a04',
|
||||
'waiting': '#9333ea',
|
||||
'resolved': '#16a34a',
|
||||
'closed': '#6b7280',
|
||||
}
|
||||
|
||||
def ticket_type_display(self, obj):
|
||||
return obj.get_ticket_type_display()
|
||||
ticket_type_display.short_description = _('Тип')
|
||||
ticket_type_display.admin_order_field = 'ticket_type'
|
||||
|
||||
def priority_display(self, obj):
|
||||
color = self.PRIORITY_COLORS.get(obj.priority, '#6b7280')
|
||||
return format_html(
|
||||
'<span style="color:{};font-weight:bold;">{}</span>',
|
||||
color, obj.get_priority_display()
|
||||
)
|
||||
priority_display.short_description = _('Приоритет')
|
||||
priority_display.admin_order_field = 'priority'
|
||||
|
||||
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 overdue_display(self, obj):
|
||||
if obj.is_overdue:
|
||||
return format_html('<span style="color:#dc2626;font-weight:bold;">⚠ Просрочена</span>')
|
||||
return ''
|
||||
overdue_display.short_description = _('SLA')
|
||||
|
||||
@admin.action(description=_('Взять в работу'))
|
||||
def mark_in_progress(self, request, queryset):
|
||||
updated = queryset.filter(status='new').update(
|
||||
status='in_progress', assignee=request.user
|
||||
)
|
||||
self.message_user(request, f'Взято в работу: {updated}')
|
||||
|
||||
@admin.action(description=_('Отметить решёнными'))
|
||||
def mark_resolved(self, request, queryset):
|
||||
from django.utils import timezone
|
||||
for ticket in queryset.exclude(status__in=['resolved', 'closed']):
|
||||
ticket.status = 'resolved'
|
||||
ticket.save()
|
||||
self.message_user(request, f'Отмечено решёнными: {queryset.count()}')
|
||||
|
||||
@admin.action(description=_('Закрыть'))
|
||||
def mark_closed(self, request, queryset):
|
||||
for ticket in queryset.exclude(status='closed'):
|
||||
ticket.status = 'closed'
|
||||
ticket.save()
|
||||
self.message_user(request, f'Закрыто: {queryset.count()}')
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
obj.requester = obj.requester or request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internet Providers & Contracts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class InternetContractInline(TabularInline):
|
||||
model = InternetContract
|
||||
extra = 0
|
||||
fields = ('location', 'contract_number', 'service_type', 'status',
|
||||
'bandwidth_down', 'bandwidth_up', 'monthly_cost', 'end_date')
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(InternetProvider)
|
||||
class InternetProviderAdmin(ModelAdmin):
|
||||
list_display = ['name', 'phone', 'support_phone', 'email',
|
||||
'active_contracts_count', 'is_active']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'phone', 'email', 'support_phone']
|
||||
ordering = ['name']
|
||||
|
||||
fieldsets = (
|
||||
(_('Провайдер'), {
|
||||
'fields': ('name', 'is_active'),
|
||||
}),
|
||||
(_('Контакты'), {
|
||||
'fields': ('phone', 'email', 'website'),
|
||||
}),
|
||||
(_('Техподдержка'), {
|
||||
'fields': ('support_phone', 'support_email'),
|
||||
}),
|
||||
(_('Примечания'), {
|
||||
'fields': ('notes',),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
inlines = [InternetContractInline]
|
||||
|
||||
def active_contracts_count(self, obj):
|
||||
count = obj.active_contracts_count
|
||||
if count:
|
||||
return format_html('<span style="color:#16a34a;font-weight:bold;">{}</span>', count)
|
||||
return format_html('<span style="color:#9ca3af;">0</span>')
|
||||
active_contracts_count.short_description = _('Активных договоров')
|
||||
|
||||
|
||||
@admin.register(InternetContract)
|
||||
class InternetContractAdmin(ModelAdmin):
|
||||
form = InternetContractForm
|
||||
list_display = [
|
||||
'provider', 'location', 'contract_number', 'service_type_display',
|
||||
'connection_type_display', 'status_display', 'bandwidth_display',
|
||||
'monthly_cost', 'end_date', 'expiry_display',
|
||||
]
|
||||
list_filter = ['status', 'service_type', 'connection_type', 'provider', 'location__location_type']
|
||||
search_fields = ['contract_number', 'account_number', 'static_ips',
|
||||
'provider__name', 'location__address']
|
||||
list_select_related = ['provider', 'location']
|
||||
ordering = ['location', 'provider']
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'ip_list_display']
|
||||
|
||||
fieldsets = (
|
||||
(_('Договор'), {
|
||||
'fields': ('provider', 'contract_number', 'account_number',
|
||||
'service_type', 'connection_type', 'status'),
|
||||
}),
|
||||
(_('Локация'), {
|
||||
'fields': ('location',),
|
||||
}),
|
||||
(_('Параметры'), {
|
||||
'fields': ('bandwidth_down', 'bandwidth_up', 'monthly_cost'),
|
||||
}),
|
||||
(_('Сроки'), {
|
||||
'fields': ('start_date', 'end_date'),
|
||||
}),
|
||||
(_('IP-адреса'), {
|
||||
'fields': ('static_ips', 'ip_list_display'),
|
||||
}),
|
||||
(_('Примечания'), {
|
||||
'fields': ('notes',),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
(_('Служебное'), {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
STATUS_COLORS = {
|
||||
'active': '#16a34a',
|
||||
'suspended': '#ca8a04',
|
||||
'terminated': '#dc2626',
|
||||
}
|
||||
|
||||
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 service_type_display(self, obj):
|
||||
return obj.get_service_type_display()
|
||||
service_type_display.short_description = _('Услуга')
|
||||
service_type_display.admin_order_field = 'service_type'
|
||||
|
||||
def connection_type_display(self, obj):
|
||||
return obj.get_connection_type_display() if obj.connection_type else '—'
|
||||
connection_type_display.short_description = _('Технология')
|
||||
connection_type_display.admin_order_field = 'connection_type'
|
||||
|
||||
def expiry_display(self, obj):
|
||||
if not obj.end_date:
|
||||
return '—'
|
||||
if obj.status != 'active':
|
||||
return str(obj.end_date)
|
||||
if obj.is_expiring_soon:
|
||||
days = (obj.end_date - timezone.now().date()).days
|
||||
return format_html('<span style="color:#dc2626;font-weight:bold;">⚠ {} дн.</span>', days)
|
||||
return str(obj.end_date)
|
||||
expiry_display.short_description = _('Окончание')
|
||||
expiry_display.admin_order_field = 'end_date'
|
||||
|
||||
def ip_list_display(self, obj):
|
||||
ips = obj.static_ip_list
|
||||
if not ips:
|
||||
return '—'
|
||||
return mark_safe('<br>'.join(
|
||||
format_html(
|
||||
'<code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;">{}</code>',
|
||||
ip
|
||||
)
|
||||
for ip in ips
|
||||
))
|
||||
ip_list_display.short_description = _('Статические IP')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Work Logs & Service Calls
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LocationChoiceFieldSimple(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
type_label = obj.get_location_type_display()
|
||||
addr = obj.get_full_address() or obj.address or ''
|
||||
if obj.location_type == 'pharmacy':
|
||||
try:
|
||||
num = obj.pharmacy.pharmacy_number
|
||||
return f'Аптека №{num} — {addr}'
|
||||
except Exception:
|
||||
pass
|
||||
return f'{type_label} — {addr}' if addr else type_label
|
||||
|
||||
|
||||
def _location_queryset():
|
||||
from apps.locations.models import Location
|
||||
return (
|
||||
Location.objects
|
||||
.filter(is_active=True)
|
||||
.select_related('location_region')
|
||||
.prefetch_related('pharmacy')
|
||||
.order_by('location_type', 'address')
|
||||
)
|
||||
|
||||
|
||||
class WorkLogForm(forms.ModelForm):
|
||||
location = LocationChoiceFieldSimple(
|
||||
queryset=None, required=False, label=_('Локация'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['location'].queryset = _location_queryset()
|
||||
|
||||
class Meta:
|
||||
model = WorkLog
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ServiceCallForm(forms.ModelForm):
|
||||
location = LocationChoiceFieldSimple(
|
||||
queryset=None, required=False, label=_('Локация'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['location'].queryset = _location_queryset()
|
||||
|
||||
class Meta:
|
||||
model = ServiceCall
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
@admin.register(WorkLog)
|
||||
class WorkLogAdmin(ModelAdmin):
|
||||
form = WorkLogForm
|
||||
list_display = ['date', 'technician', 'work_type_display', 'location',
|
||||
'hours_spent', 'ticket', 'created_at']
|
||||
list_filter = ['work_type', 'technician', 'date']
|
||||
search_fields = ['description', 'result', 'technician__last_name',
|
||||
'technician__first_name']
|
||||
list_select_related = ['technician', 'location', 'ticket', 'equipment']
|
||||
ordering = ['-date', '-created_at']
|
||||
date_hierarchy = 'date'
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
(_('Работа'), {
|
||||
'fields': ('date', 'technician', 'work_type', 'location'),
|
||||
}),
|
||||
(_('Описание'), {
|
||||
'fields': ('description', 'result', 'hours_spent'),
|
||||
}),
|
||||
(_('Связи'), {
|
||||
'fields': ('equipment', 'ticket'),
|
||||
}),
|
||||
(_('Служебное'), {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
WORK_TYPE_COLORS = {
|
||||
'maintenance': '#2563eb',
|
||||
'installation': '#16a34a',
|
||||
'repair': '#ca8a04',
|
||||
'network': '#9333ea',
|
||||
'software': '#0891b2',
|
||||
'backup': '#6b7280',
|
||||
'other': '#6b7280',
|
||||
}
|
||||
|
||||
def work_type_display(self, obj):
|
||||
color = self.WORK_TYPE_COLORS.get(obj.work_type, '#6b7280')
|
||||
return format_html(
|
||||
'<span style="color:{};font-weight:bold;">{}</span>',
|
||||
color, obj.get_work_type_display()
|
||||
)
|
||||
work_type_display.short_description = _('Вид работ')
|
||||
work_type_display.admin_order_field = 'work_type'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change and not obj.technician_id:
|
||||
obj.technician = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(ServiceCall)
|
||||
class ServiceCallAdmin(ModelAdmin):
|
||||
form = ServiceCallForm
|
||||
list_display = ['date_time', 'caller', 'handler', 'channel_display',
|
||||
'location', 'duration_display', 'ticket']
|
||||
list_filter = ['channel', 'handler', 'date_time']
|
||||
search_fields = ['description', 'resolution',
|
||||
'caller__last_name', 'caller__first_name',
|
||||
'handler__last_name', 'handler__first_name']
|
||||
list_select_related = ['caller', 'handler', 'location', 'ticket']
|
||||
ordering = ['-date_time']
|
||||
date_hierarchy = 'date_time'
|
||||
|
||||
readonly_fields = ['created_at', 'duration_display']
|
||||
|
||||
fieldsets = (
|
||||
(_('Обращение'), {
|
||||
'fields': ('date_time', 'channel', 'caller', 'handler', 'location'),
|
||||
}),
|
||||
(_('Содержание'), {
|
||||
'fields': ('description', 'resolution', 'minutes_spent', 'duration_display'),
|
||||
}),
|
||||
(_('Связи'), {
|
||||
'fields': ('ticket',),
|
||||
}),
|
||||
(_('Служебное'), {
|
||||
'fields': ('created_at',),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
)
|
||||
|
||||
CHANNEL_ICONS = {
|
||||
'phone': '📞',
|
||||
'chat': '💬',
|
||||
'email': '📧',
|
||||
'personal': '🚶',
|
||||
'other': '📋',
|
||||
}
|
||||
|
||||
def channel_display(self, obj):
|
||||
icon = self.CHANNEL_ICONS.get(obj.channel, '📋')
|
||||
return format_html('{} {}', icon, obj.get_channel_display())
|
||||
channel_display.short_description = _('Канал')
|
||||
channel_display.admin_order_field = 'channel'
|
||||
|
||||
def duration_display(self, obj):
|
||||
return obj.duration_display
|
||||
duration_display.short_description = _('Длительность')
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change and not obj.handler_id:
|
||||
obj.handler = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
Reference in New Issue
Block a user