From 16264cb8a939f2aa27424ef3a7d31fb5da26a865 Mon Sep 17 00:00:00 2001 From: mariomitte Date: Fri, 31 Jul 2026 18:09:16 +0200 Subject: [PATCH] fix: dodaj fotografije servisnih zapisa u DOCX DOCX izvjestaj servisnih zapisa sada ugradjuje povezane fotografije nakon kompresije slike za manju velicinu dokumenta. Dodan je regresijski test koji provjerava da generirani DOCX sadrzi embedded media datoteke. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/test_work_order_images_endpoint.py | 28 +++++++++++++ backend/modules/fleet/views.py | 42 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/backend/modules/fleet/tests/test_work_order_images_endpoint.py b/backend/modules/fleet/tests/test_work_order_images_endpoint.py index 57a809c..53982d5 100644 --- a/backend/modules/fleet/tests/test_work_order_images_endpoint.py +++ b/backend/modules/fleet/tests/test_work_order_images_endpoint.py @@ -7,9 +7,11 @@ from rest_framework.test import APIClient import uuid from io import BytesIO from datetime import timedelta +import zipfile from PIL import Image from modules.fleet.models import Vehicle, WorkOrder, WorkOrderInvoice, VehicleServiceRecord, VehicleServicePhoto, GeneratedWorkOrderPdf +from modules.task_management.models import Task def create_test_image(filename='test.jpg', size=(40, 40), color='red'): @@ -55,9 +57,16 @@ class WorkOrderImagesEndpointTests(TestCase): creator=self.user, purpose='kontrola', ) + self.task = Task.objects.create( + title='Test servisni zadatak', + assigned_to=self.user, + vehicle=self.vehicle, + work_order=self.work_order, + ) self.service_record = VehicleServiceRecord.objects.create( vehicle=self.vehicle, + task=self.task, performed_by=self.user, description='Servisni zapis za test izolacije fotografija.', service_title='Test servis', @@ -240,3 +249,22 @@ class WorkOrderImagesEndpointTests(TestCase): cached.refresh_from_db() self.assertFalse(cached.is_active) self.assertEqual(cached.status, 'failed') + + def test_service_records_docx_contains_embedded_service_photos(self): + VehicleServicePhoto.objects.create( + service_record=self.service_record, + image=create_test_image('service-record-docx.jpg', size=(320, 200), color='blue'), + description='DOCX photo', + uploaded_by=self.user, + ) + + response = self.client.get(f"/api/fleet/work-orders/{self.work_order.pk}/service-records-docx/") + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual( + response['Content-Type'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ) + + archive = zipfile.ZipFile(BytesIO(response.content)) + media_files = [name for name in archive.namelist() if name.startswith('word/media/')] + self.assertTrue(media_files, archive.namelist()) diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 31d1555..0074513 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -236,6 +236,31 @@ def _compress_image_for_pdf(image_field, max_width=1280, quality=75): pass +def _compress_image_for_docx(image_field, max_width=1600, quality=80): + """ + Pripremi sliku za python-docx kao JPEG stream razumne veličine. + """ + 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 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) @@ -1759,6 +1784,8 @@ def _build_work_order_docx_bytes(work_order): def _build_work_order_service_records_docx_bytes(work_order): + from docx.shared import Cm + doc = _create_docx_document(SERVICE_REPORT_DOCX_TEMPLATE_NAME) vehicle = work_order.vehicle client_name = getattr(getattr(vehicle, 'client', None), 'name', None) or '-' @@ -1805,6 +1832,7 @@ def _build_work_order_service_records_docx_bytes(work_order): vehicle_id=work_order.vehicle_id, ) .select_related('performed_by', 'task') + .prefetch_related('photos') .order_by('service_date', 'created_at') ) records_by_task = { @@ -1915,6 +1943,20 @@ def _build_work_order_service_records_docx_bytes(work_order): doc.add_paragraph(f"Korišteni dijelovi: {record.parts or '-'}") doc.add_paragraph(f"Trošak: {record.cost or '-'} EUR") doc.add_paragraph(f"Kilometraža: {record.mileage if record.mileage is not None else '-'}") + photos = [photo for photo in record.photos.filter(is_active=True).all() if photo.image] + if photos: + doc.add_paragraph('Fotografije:') + for photo in photos: + image_stream = _compress_image_for_docx(photo.image) + if image_stream is None: + continue + try: + doc.add_picture(image_stream, width=Cm(16)) + except Exception: + continue + photo_caption = str(photo.description or '').strip() + if photo_caption: + doc.add_paragraph(photo_caption) doc.add_paragraph('') if not has_records: doc.add_paragraph('Nema povezanih servisnih zapisa za ovaj putni nalog.')