Files
mariomitte 525152b817
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled
fix: ispravi prikaz računa u admin/frontendu i smanji PDF slike
- admin.py: thumbnail_preview u WorkOrderInvoiceInline i WorkOrderInvoiceAdmin
  prikazuje slike/PDF veze putem optimiziranog image endpointa
- serializers.py: image_url sada uključuje ?w=1280&q=80&fmt=jpeg za ne-PDF slike
- views.py: doda _compress_image_for_pdf() helper, zamijeni ImageReader(photo.image)
  s kompresiranom JPEG verzijom na oba mjesta u PDF generiranju
- tasks.py: kompresija invoice slike u JPEG u memoriji prije ImageReader poziva
- WorkOrderInvoicesPdfPage.jsx:
  * InvoiceImagePreview prikazuje 'Učitavanje...' dok se blob dohvaća (ne 401 fallback)
  * uklonjen resolveMediaUrl fallback koji uzrokuje 401 greške
  * svaka invoice kartica je klikabilna i otvara InvoiceDetailModal
  * InvoiceDetailModal prikazuje detalje i uvećanu sliku/PDF vezu

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-31 11:54:34 +02:00

334 lines
12 KiB
Python

from pathlib import Path
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 = ('thumbnail_preview', 'naziv_racuna', 'lokacija', 'datum', 'opis', 'image', 'created_by', 'created_at', 'is_active')
readonly_fields = ('thumbnail_preview', 'created_at')
@admin.display(description="Pregled")
def thumbnail_preview(self, obj):
if not obj.pk or not obj.image:
return ""
suffix = Path(obj.image.name or '').suffix.lower()
if suffix == '.pdf':
return format_html(
'<a href="/api/fleet/work-order-invoices/{}/image/" target="_blank">📄 PDF</a>',
obj.pk,
)
return format_html(
'<a href="/api/fleet/work-order-invoices/{}/image/?w=1600&q=90&fmt=jpeg" target="_blank">'
'<img src="/api/fleet/work-order-invoices/{}/image/?w=200&q=75&fmt=jpeg" '
'style="height:64px;width:auto;border-radius:4px;object-fit:cover;" />'
'</a>',
obj.pk,
obj.pk,
)
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', 'image_preview', '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')
readonly_fields = ('image_preview', 'created_at')
@admin.display(description="Slika")
def image_preview(self, obj):
if not obj.pk or not obj.image:
return ""
suffix = Path(obj.image.name or '').suffix.lower()
if suffix == '.pdf':
return format_html(
'<a href="/api/fleet/work-order-invoices/{}/image/" target="_blank">📄 PDF</a>',
obj.pk,
)
return format_html(
'<a href="/api/fleet/work-order-invoices/{}/image/?w=1600&q=90&fmt=jpeg" target="_blank">'
'<img src="/api/fleet/work-order-invoices/{}/image/?w=200&q=75&fmt=jpeg" '
'style="height:80px;width:auto;border-radius:4px;object-fit:cover;" />'
'</a>',
obj.pk,
obj.pk,
)
@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