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>
This commit is contained in:
@@ -7,9 +7,11 @@ from rest_framework.test import APIClient
|
|||||||
import uuid
|
import uuid
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import zipfile
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from modules.fleet.models import Vehicle, WorkOrder, WorkOrderInvoice, VehicleServiceRecord, VehicleServicePhoto, GeneratedWorkOrderPdf
|
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'):
|
def create_test_image(filename='test.jpg', size=(40, 40), color='red'):
|
||||||
@@ -55,9 +57,16 @@ class WorkOrderImagesEndpointTests(TestCase):
|
|||||||
creator=self.user,
|
creator=self.user,
|
||||||
purpose='kontrola',
|
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(
|
self.service_record = VehicleServiceRecord.objects.create(
|
||||||
vehicle=self.vehicle,
|
vehicle=self.vehicle,
|
||||||
|
task=self.task,
|
||||||
performed_by=self.user,
|
performed_by=self.user,
|
||||||
description='Servisni zapis za test izolacije fotografija.',
|
description='Servisni zapis za test izolacije fotografija.',
|
||||||
service_title='Test servis',
|
service_title='Test servis',
|
||||||
@@ -240,3 +249,22 @@ class WorkOrderImagesEndpointTests(TestCase):
|
|||||||
cached.refresh_from_db()
|
cached.refresh_from_db()
|
||||||
self.assertFalse(cached.is_active)
|
self.assertFalse(cached.is_active)
|
||||||
self.assertEqual(cached.status, 'failed')
|
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())
|
||||||
|
|||||||
@@ -236,6 +236,31 @@ def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
|
|||||||
pass
|
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):
|
def _get_cached_pdf(work_order, pdf_type):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
expected_filename = _pdf_filename(work_order, pdf_type)
|
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):
|
def _build_work_order_service_records_docx_bytes(work_order):
|
||||||
|
from docx.shared import Cm
|
||||||
|
|
||||||
doc = _create_docx_document(SERVICE_REPORT_DOCX_TEMPLATE_NAME)
|
doc = _create_docx_document(SERVICE_REPORT_DOCX_TEMPLATE_NAME)
|
||||||
vehicle = work_order.vehicle
|
vehicle = work_order.vehicle
|
||||||
client_name = getattr(getattr(vehicle, 'client', None), 'name', None) or '-'
|
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,
|
vehicle_id=work_order.vehicle_id,
|
||||||
)
|
)
|
||||||
.select_related('performed_by', 'task')
|
.select_related('performed_by', 'task')
|
||||||
|
.prefetch_related('photos')
|
||||||
.order_by('service_date', 'created_at')
|
.order_by('service_date', 'created_at')
|
||||||
)
|
)
|
||||||
records_by_task = {
|
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"Korišteni dijelovi: {record.parts or '-'}")
|
||||||
doc.add_paragraph(f"Trošak: {record.cost or '-'} EUR")
|
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 '-'}")
|
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('')
|
doc.add_paragraph('')
|
||||||
if not has_records:
|
if not has_records:
|
||||||
doc.add_paragraph('Nema povezanih servisnih zapisa za ovaj putni nalog.')
|
doc.add_paragraph('Nema povezanih servisnih zapisa za ovaj putni nalog.')
|
||||||
|
|||||||
Reference in New Issue
Block a user