fix: ispravi prikaz računa u admin/frontendu i smanji PDF slike
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

- 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>
This commit is contained in:
mariomitte
2026-07-31 11:54:34 +02:00
parent ae6ae08fb1
commit 525152b817
5 changed files with 207 additions and 30 deletions

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.utils.html import format_html
@@ -61,8 +62,27 @@ class ServiceRecordInline(admin.TabularInline):
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',)
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):
@@ -193,10 +213,30 @@ class WorkOrderAdmin(admin.ModelAdmin):
@admin.register(WorkOrderInvoice)
class WorkOrderInvoiceAdmin(admin.ModelAdmin):
list_display = ('id', 'work_order', 'naziv_racuna', 'lokacija', 'datum', 'created_by', 'is_active')
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)

View File

@@ -373,9 +373,13 @@ class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
return None
if request is None:
return None
return request.build_absolute_uri(
base = request.build_absolute_uri(
reverse('work-order-invoice-image', kwargs={'pk': obj.pk})
)
suffix = Path(obj.image.name or '').suffix.lower()
if suffix == '.pdf':
return base
return f"{base}?w=1280&q=80&fmt=jpeg"
def get_image_content_type(self, obj):
if not obj.image:

View File

@@ -645,10 +645,19 @@ def _build_work_order_invoices_pdf(work_order):
y = content_top
max_height = y - content_bottom
# Kompresiraj u JPEG u memoriji radi manje veličine PDF-a
compress_w = min(image.width, 1280)
if image.width > compress_w:
ratio_c = compress_w / float(image.width)
image = image.resize((compress_w, max(1, int(image.height * ratio_c))), Image.LANCZOS)
buf = BytesIO()
image.save(buf, format='JPEG', quality=75, optimize=True)
buf.seek(0)
ratio = min(max_width / float(image.width), max_height / float(image.height), 1.0)
draw_width = max(1, int(image.width * ratio))
draw_height = max(1, int(image.height * ratio))
image_reader = ImageReader(image)
image_reader = ImageReader(buf)
pdf.drawImage(
image_reader,
margin,

View File

@@ -208,6 +208,32 @@ def _docx_filename(work_order, doc_type):
return f"{display_code}.work-order.docx"
def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
"""
Otvori image_field (Django FileField), kompresiraj na max_width JPEG u memoriji,
vrati ImageReader spreman za reportlab. Vraća None ako slika nije dostupna.
"""
try:
image_field.open('rb')
with Image.open(image_field) as src:
img = src.convert('RGB')
if img.width > max_width:
ratio = max_width / float(img.width)
new_h = max(1, int(img.height * ratio))
img = img.resize((max_width, new_h), Image.LANCZOS)
buf = BytesIO()
img.save(buf, format='JPEG', quality=quality, optimize=True)
buf.seek(0)
return ImageReader(buf)
except Exception:
return None
finally:
try:
image_field.close()
except Exception:
pass
def _get_cached_pdf(work_order, pdf_type):
now = timezone.now()
expected_filename = _pdf_filename(work_order, pdf_type)
@@ -1166,7 +1192,9 @@ def _build_work_order_service_records_pdf(work_order):
prepared = []
for photo in row_photos:
try:
image_reader = ImageReader(photo.image)
image_reader = _compress_image_for_pdf(photo.image)
if image_reader is None:
continue
source_w, source_h = image_reader.getSize()
if not source_w or not source_h:
continue
@@ -1390,7 +1418,9 @@ def _build_service_record_pdf(service_record):
prepared = []
for photo in row_photos:
try:
image_reader = ImageReader(photo.image)
image_reader = _compress_image_for_pdf(photo.image)
if image_reader is None:
continue
source_w, source_h = image_reader.getSize()
if not source_w or not source_h:
continue