Prebaci slanje emaila putnog naloga u pozadinski task bez blokiranja UI-a i dodaj fallback kad queue nije dostupna. Kod greške slanja spremi generirane PDF-ove u cache i pošalji notifikaciju s download linkovima. Dodaj EmailDispatchLog model i Django admin pregled za praćenje kome je poslano, što je poslano, status i detalje privitaka. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
294 lines
10 KiB
Python
294 lines
10 KiB
Python
from django.contrib import admin
|
|
from django.contrib.admin import ModelAdmin
|
|
from django.utils.html import format_html
|
|
from .models import (
|
|
Crane,
|
|
Vehicle,
|
|
WorkOrder,
|
|
WorkOrderPhoto,
|
|
WorkOrderInvoice,
|
|
WorkOrderAdditionalCostsTable,
|
|
ServiceContextNote,
|
|
GeneratedWorkOrderPdf,
|
|
EmailDispatchLog,
|
|
VehicleServiceRecord,
|
|
VehicleServicePhoto,
|
|
VehicleServiceAttachment,
|
|
)
|
|
|
|
|
|
class ServicePhotoInline(admin.TabularInline):
|
|
model = VehicleServicePhoto
|
|
extra = 0
|
|
fields = ('thumbnail_preview', 'image', 'description', 'uploaded_by', 'created_at')
|
|
readonly_fields = ('thumbnail_preview', 'created_at')
|
|
show_change_link = True
|
|
|
|
@admin.display(description="Pregled")
|
|
def thumbnail_preview(self, obj):
|
|
if obj.image:
|
|
return format_html(
|
|
'<a href="{}" target="_blank">'
|
|
'<img src="{}" style="height:64px;width:auto;border-radius:4px;object-fit:cover;" />'
|
|
'</a>',
|
|
obj.image.url,
|
|
obj.image.url,
|
|
)
|
|
return "—"
|
|
|
|
class ServiceAttachmentInline(admin.TabularInline):
|
|
model = VehicleServiceAttachment
|
|
extra = 0
|
|
fields = ('file', 'description', 'uploaded_by', 'created_at')
|
|
readonly_fields = ('created_at',)
|
|
show_change_link = True
|
|
|
|
|
|
class WorkOrderInline(admin.TabularInline):
|
|
model = WorkOrder
|
|
extra = 0
|
|
fields = ('display_code', 'creator', 'date', 'start_mileage', 'end_mileage', 'origin_location', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'status')
|
|
readonly_fields = ('date',)
|
|
|
|
|
|
class ServiceRecordInline(admin.TabularInline):
|
|
model = VehicleServiceRecord
|
|
extra = 0
|
|
fields = ('task', 'performed_by', 'service_date', 'mileage', 'next_service_due_at', 'cost')
|
|
readonly_fields = ('service_date', 'next_service_due_at')
|
|
|
|
|
|
class WorkOrderInvoiceInline(admin.TabularInline):
|
|
model = WorkOrderInvoice
|
|
extra = 0
|
|
fields = ('naziv_racuna', 'lokacija', 'datum', 'opis', 'image', 'created_by', 'created_at', 'is_active')
|
|
readonly_fields = ('created_at',)
|
|
|
|
|
|
class WorkOrderPhotoInline(admin.TabularInline):
|
|
model = WorkOrderPhoto
|
|
extra = 0
|
|
fields = ('thumbnail_preview', 'image', 'description', 'uploaded_by', 'created_at', 'is_active')
|
|
readonly_fields = ('thumbnail_preview', 'created_at')
|
|
show_change_link = True
|
|
|
|
@admin.display(description="Pregled")
|
|
def thumbnail_preview(self, obj):
|
|
if obj.image:
|
|
return format_html(
|
|
'<a href="{}" target="_blank">'
|
|
'<img src="{}" style="height:64px;width:auto;border-radius:4px;object-fit:cover;" />'
|
|
'</a>',
|
|
obj.image.url,
|
|
obj.image.url,
|
|
)
|
|
return "—"
|
|
|
|
|
|
@admin.register(Vehicle)
|
|
class VehicleAdmin(admin.ModelAdmin):
|
|
list_display = (
|
|
'registration_number',
|
|
'crane_serial_number',
|
|
'superstructure_working_hours',
|
|
'chassis_working_hours',
|
|
'make',
|
|
'model',
|
|
'year',
|
|
'current_mileage',
|
|
'service_interval_km',
|
|
'next_service_at_display',
|
|
'is_company_vehicle',
|
|
'client',
|
|
'assigned_servicer',
|
|
'is_active',
|
|
)
|
|
list_filter = ('is_active', 'is_company_vehicle', 'make', 'service_interval_km')
|
|
search_fields = ('registration_number', 'vin', 'make', 'model', 'assigned_servicer__username', 'client__name')
|
|
readonly_fields = ()
|
|
inlines = (WorkOrderInline, ServiceRecordInline)
|
|
ordering = ('registration_number',)
|
|
fieldsets = (
|
|
(None, {
|
|
'fields': ('asset_type', 'registration_number', 'vin', 'make', 'model', 'year'),
|
|
}),
|
|
('Vlasništvo', {
|
|
'fields': ('is_company_vehicle', 'client', 'assigned_servicer'),
|
|
}),
|
|
('Servis', {
|
|
'fields': ('current_mileage', 'service_interval_km', 'superstructure_working_hours', 'chassis_working_hours', 'is_active'),
|
|
}),
|
|
)
|
|
|
|
def next_service_at_display(self, obj):
|
|
try:
|
|
return obj.next_service_at()
|
|
except Exception:
|
|
return None
|
|
next_service_at_display.short_description = "Sljedeći servis @ (km)"
|
|
next_service_at_display.admin_order_field = 'current_mileage'
|
|
|
|
def get_queryset(self, request):
|
|
queryset = super().get_queryset(request)
|
|
return queryset.filter(asset_type='vehicle')
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
obj.asset_type = 'vehicle'
|
|
super().save_model(request, obj, form, change)
|
|
|
|
|
|
@admin.register(Crane)
|
|
class CraneAdmin(VehicleAdmin):
|
|
list_display = (
|
|
'registration_number',
|
|
'crane_serial_number',
|
|
'superstructure_working_hours',
|
|
'chassis_working_hours',
|
|
'make',
|
|
'model',
|
|
'year',
|
|
'current_mileage',
|
|
'service_interval_km',
|
|
'next_service_at_display',
|
|
'client',
|
|
'assigned_servicer',
|
|
'is_active',
|
|
)
|
|
list_filter = ('is_active', 'client', 'make', 'service_interval_km', 'crane_serial_number')
|
|
search_fields = ('registration_number', 'crane_serial_number', 'vin', 'make', 'model', 'assigned_servicer__username', 'client__name')
|
|
fieldsets = (
|
|
(None, {
|
|
'fields': ('asset_type', 'registration_number', 'crane_serial_number', 'vin', 'make', 'model', 'year'),
|
|
}),
|
|
('Povezivanje', {
|
|
'fields': ('client', 'assigned_servicer', 'is_company_vehicle'),
|
|
}),
|
|
('Servis', {
|
|
'fields': ('current_mileage', 'service_interval_km', 'superstructure_working_hours', 'chassis_working_hours', 'is_active'),
|
|
}),
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
queryset = ModelAdmin.get_queryset(self, request)
|
|
return queryset.filter(asset_type='crane')
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
obj.asset_type = 'crane'
|
|
ModelAdmin.save_model(self, request, obj, form, change)
|
|
|
|
|
|
@admin.register(WorkOrder)
|
|
class WorkOrderAdmin(admin.ModelAdmin):
|
|
list_display = ('display_code', 'id', 'vehicle', 'creator', 'date', 'origin_location', 'location', 'travel_start_at', 'travel_end_at', 'start_mileage', 'end_mileage', 'distance_display', 'status')
|
|
list_filter = ('status', 'date', 'vehicle')
|
|
search_fields = ('display_code', 'vehicle__registration_number', 'creator__username', 'purpose')
|
|
readonly_fields = ('date',)
|
|
ordering = ('-date',)
|
|
inlines = (WorkOrderInvoiceInline, WorkOrderPhotoInline)
|
|
|
|
def distance_display(self, obj):
|
|
return obj.distance
|
|
distance_display.short_description = "Prijeđeno (km)"
|
|
|
|
|
|
@admin.register(WorkOrderInvoice)
|
|
class WorkOrderInvoiceAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'work_order', 'naziv_racuna', 'lokacija', 'datum', 'created_by', 'is_active')
|
|
list_filter = ('datum', 'is_active', 'work_order')
|
|
search_fields = ('naziv_racuna', 'lokacija', 'opis', 'work_order__id')
|
|
ordering = ('-datum', '-created_at')
|
|
|
|
|
|
@admin.register(WorkOrderAdditionalCostsTable)
|
|
class WorkOrderAdditionalCostsTableAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'work_order', 'total_for_payout', 'updated_at')
|
|
search_fields = ('work_order__display_code', 'work_order__vehicle__registration_number')
|
|
readonly_fields = ('created_at', 'updated_at')
|
|
ordering = ('-updated_at',)
|
|
|
|
|
|
@admin.register(ServiceContextNote)
|
|
class ServiceContextNoteAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'recipient', 'created_by', 'audience_source', 'note_date', 'is_closed', 'created_at')
|
|
list_filter = ('audience_source', 'is_closed', 'note_date')
|
|
search_fields = ('note', 'recipient__email', 'recipient__first_name', 'recipient__last_name', 'created_by__email')
|
|
readonly_fields = ('created_at', 'updated_at', 'closed_at')
|
|
ordering = ('is_closed', 'note_date', '-created_at')
|
|
|
|
|
|
@admin.register(GeneratedWorkOrderPdf)
|
|
class GeneratedWorkOrderPdfAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'work_order', 'pdf_type', 'status', 'requested_by', 'expires_at', 'is_active', 'created_at')
|
|
list_filter = ('pdf_type', 'status', 'is_active', 'expires_at')
|
|
search_fields = ('work_order__id', 'requested_by__username', 'requested_by__email', 'filename')
|
|
readonly_fields = ('created_at', 'updated_at')
|
|
ordering = ('-created_at',)
|
|
|
|
@admin.register(VehicleServiceRecord)
|
|
class VehicleServiceRecordAdmin(admin.ModelAdmin):
|
|
list_display = ('id', 'service_title', 'task', 'vehicle', 'performed_by', 'service_date', 'mileage', 'next_service_due_at', 'cost', 'short_description')
|
|
list_filter = ('service_date', 'performed_by', 'vehicle')
|
|
search_fields = ('service_title', 'vehicle__registration_number', 'performed_by__username', 'description', 'parts')
|
|
readonly_fields = ('id', 'service_date', 'next_service_due_at')
|
|
ordering = ('-service_date',)
|
|
inlines = (ServicePhotoInline, ServiceAttachmentInline)
|
|
fieldsets = (
|
|
(None, {
|
|
'fields': ('id', 'service_title', 'task', 'vehicle', 'performed_by', 'service_date'),
|
|
}),
|
|
('Detalji servisa', {
|
|
'fields': ('description', 'parts', 'mileage', 'cost', 'next_service_due_at'),
|
|
}),
|
|
)
|
|
|
|
@admin.display(description="Opis")
|
|
def short_description(self, obj):
|
|
if obj.description:
|
|
return obj.description[:60] + ('…' if len(obj.description) > 60 else '')
|
|
return "—"
|
|
|
|
|
|
@admin.register(EmailDispatchLog)
|
|
class EmailDispatchLogAdmin(admin.ModelAdmin):
|
|
list_display = (
|
|
'id',
|
|
'dispatch_type',
|
|
'status',
|
|
'requested_by',
|
|
'work_order',
|
|
'service_record',
|
|
'recipients_count',
|
|
'sent_at',
|
|
'created_at',
|
|
)
|
|
list_filter = ('dispatch_type', 'status', 'created_at', 'sent_at')
|
|
search_fields = (
|
|
'subject',
|
|
'message',
|
|
'error_message',
|
|
'requested_by__email',
|
|
'requested_by__username',
|
|
'work_order__display_code',
|
|
)
|
|
readonly_fields = (
|
|
'dispatch_type',
|
|
'requested_by',
|
|
'work_order',
|
|
'service_record',
|
|
'recipients',
|
|
'subject',
|
|
'message',
|
|
'attachments',
|
|
'status',
|
|
'error_message',
|
|
'metadata',
|
|
'sent_at',
|
|
'created_at',
|
|
'updated_at',
|
|
)
|
|
ordering = ('-created_at',)
|
|
|
|
@admin.display(description='Primatelja')
|
|
def recipients_count(self, obj):
|
|
recipients = obj.recipients or []
|
|
return len(recipients) if isinstance(recipients, list) else 0 |