# backend/modules/fleet/tasks.py from celery import shared_task from django.core.mail import send_mail from django.conf import settings from django.utils import timezone from django.core.files.base import ContentFile import logging from io import BytesIO import base64 from PIL import Image, UnidentifiedImageError from reportlab.lib.pagesizes import A4 from reportlab.lib.utils import ImageReader from reportlab.pdfgen import canvas from .models import VehicleNotification, GeneratedWorkOrderPdf from .pdf_layout import register_unicode_fonts, draw_standard_header_footer logger = logging.getLogger(__name__) register_unicode_fonts() @shared_task(bind=True, max_retries=3, default_retry_delay=60) def send_notification_email(self, notification_id): """ Celery task koji pošalje e-mail za VehicleNotification i označi is_sent = True. Prihvaća notification_id (PK) i radi safe-lookup. U slučaju greške rekreira retry. """ try: # Lokalni import kako bismo izbjegli kružne importove from .models import VehicleNotification notif = VehicleNotification.objects.select_related('recipient').get(pk=notification_id) except Exception as exc: logger.warning(f"Notification {notification_id} ne postoji ili nije dostupna: {exc}") return recipient = notif.recipient recipient_email = getattr(recipient, 'email', None) if recipient else None if not recipient_email: logger.info(f"Notification {notification_id} nema email primatelja; preskačem slanje.") return subject = notif.title message = notif.message from_email = getattr(settings, "DEFAULT_FROM_EMAIL", None) try: send_mail( subject=subject, message=message, from_email=from_email, recipient_list=[recipient_email], fail_silently=False, ) notif.is_sent = True notif.save(update_fields=['is_sent']) logger.info(f"Notification email poslan za Notification(id={notification_id}) na {recipient_email}") except Exception as exc: logger.exception(f"Greška pri slanju emaila za Notification(id={notification_id}): {exc}") try: # Retry with exponential backoff handled by celery config; here max_retries = 3 raise self.retry(exc=exc) except self.MaxRetriesExceededError: logger.error(f"Max retries exceeded za Notification(id={notification_id})") @shared_task def send_email_task(recipient_email, subject, message, notif_id): try: send_mail( subject=subject, message=message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[recipient_email], fail_silently=False, ) VehicleNotification.objects.filter(id=notif_id).update(is_sent=True) except Exception as exc: logger.error(f"Async mail failed: {exc}") def _build_work_order_invoices_pdf(work_order): buffer = BytesIO() pdf = canvas.Canvas(buffer, pagesize=A4) width, height = A4 margin = 28 header_h = 86 footer_h = 72 content_top = height - header_h - 10 content_bottom = footer_h + 52 client_name = getattr(getattr(work_order.vehicle, "client", None), "name", None) or "-" manufacturer = str(work_order.vehicle.make or "-") model_line = str(work_order.vehicle.model or "-") serial_line = str(getattr(work_order.vehicle, "crane_serial_number", None) or "-") upgrade_hours = str(getattr(work_order.vehicle, "superstructure_working_hours", "-") or "-") chassis_hours = str(getattr(work_order.vehicle, "chassis_working_hours", "-") or "-") mileage = str(getattr(work_order.vehicle, "current_mileage", "-") or "-") generated_date = timezone.localtime(timezone.now()).strftime("%d.%m.%Y") def draw_header_footer(page_num): draw_standard_header_footer( pdf, page_num=page_num, client_name=client_name, manufacturer=manufacturer, model=model_line, serial=serial_line, upgrade_hours=upgrade_hours, chassis_hours=chassis_hours, mileage=mileage, work_order_number=str(work_order.pk), generated_date=generated_date, report_title="Računi putnog naloga", page_size=A4, margin=margin, header_h=header_h, footer_h=footer_h, ) page_num = 1 draw_header_footer(page_num) y = content_top y -= 8 pdf.setFont("Vera-Bold", 13) pdf.drawString(margin, y, f"Računi putnog naloga WO-{work_order.pk}") y -= 20 pdf.setFont("Vera", 10) pdf.drawString(margin, y, f"Datum naloga: {work_order.date}") invoices = work_order.invoices.filter(is_active=True).order_by('-datum', '-created_at') if not invoices.exists(): y -= 20 pdf.drawString(margin, y, "Nema računa za ovaj putni nalog.") pdf.save() return buffer.getvalue() for index, invoice in enumerate(invoices, start=1): pdf.showPage() page_num += 1 draw_header_footer(page_num) y = content_top y -= 8 pdf.setFont("Vera-Bold", 13) pdf.drawString(margin, y, f"Račun #{index}") y -= 22 pdf.setFont("Vera", 10) pdf.drawString(margin, y, f"naziv_racuna: {invoice.naziv_racuna or '-'}") y -= 16 pdf.drawString(margin, y, f"lokacija: {invoice.lokacija or '-'}") y -= 16 pdf.drawString(margin, y, f"datum: {invoice.datum.strftime('%d.%m.%Y') if invoice.datum else '-'}") y -= 16 pdf.drawString(margin, y, f"opis: {(invoice.opis or '-')[:140]}") y -= 20 if not invoice.image: pdf.drawString(margin, y, "slika: Nema slike.") continue try: invoice.image.open('rb') with Image.open(invoice.image) as source: image = source.convert('RGB') max_width = width - (2 * margin) max_height = y - content_bottom if max_height < 120: pdf.showPage() page_num += 1 draw_header_footer(page_num) y = content_top max_height = y - content_bottom 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) pdf.drawImage( image_reader, margin, y - draw_height, width=draw_width, height=draw_height, preserveAspectRatio=True, mask='auto', ) except (UnidentifiedImageError, OSError): pdf.drawString(margin, y, "slika: Slika nije dostupna ili je oštećena.") finally: invoice.image.close() pdf.save() return buffer.getvalue() @shared_task def build_work_order_invoices_pdf_task(work_order_id): from .models import WorkOrder work_order = WorkOrder.objects.select_related('vehicle').filter(pk=work_order_id).first() if work_order is None: return {"error": "Putni nalog nije pronađen."} pdf_bytes = _build_work_order_invoices_pdf(work_order) pdf_b64 = base64.b64encode(pdf_bytes).decode('ascii') filename = f"{work_order.pk}.work-order-invoices.pdf" return { "filename": filename, "pdf_base64": pdf_b64, } @shared_task def cleanup_expired_generated_pdfs_task(): now = timezone.now() expired = GeneratedWorkOrderPdf.objects.filter( is_active=True, expires_at__isnull=False, expires_at__lte=now, ) deleted = 0 for item in expired: if item.file: item.file.delete(save=False) item.is_active = False item.status = 'failed' item.error_message = 'PDF cache istekao.' item.save(update_fields=['is_active', 'status', 'error_message', 'updated_at']) deleted += 1 return {"deleted": deleted} @shared_task def build_work_order_pdf_cached_task(generated_pdf_id): from .views import _build_work_order_pdf, _build_work_order_service_records_pdf from .services import NotificationService generated = ( GeneratedWorkOrderPdf.objects .select_related('work_order', 'work_order__vehicle', 'requested_by') .filter(pk=generated_pdf_id, is_active=True) .first() ) if generated is None: return {"error": "Generated PDF zapis nije pronađen."} work_order = generated.work_order try: if generated.pdf_type == 'invoices': payload = build_work_order_invoices_pdf_task(str(work_order.pk)) if not isinstance(payload, dict) or payload.get('error'): raise ValueError(payload.get('error') if isinstance(payload, dict) else 'Neuspješno generiranje PDF računa.') pdf_b64 = payload.get('pdf_base64') if not pdf_b64: raise ValueError('PDF sadržaj nije dostupan.') pdf_bytes = base64.b64decode(pdf_b64) elif generated.pdf_type == 'service_records': pdf_bytes = _build_work_order_service_records_pdf(work_order) else: pdf_bytes = _build_work_order_pdf(work_order) filename = generated.filename or f"{work_order.pk}.{generated.pdf_type}.pdf" generated.file.save(filename, ContentFile(pdf_bytes), save=False) generated.status = 'ready' generated.error_message = '' generated.save(update_fields=['file', 'status', 'error_message', 'updated_at']) if generated.requested_by: NotificationService.create_notification( recipient=generated.requested_by, title="PDF spreman", message=f"PDF dokument je uspješno generiran za WO-{work_order.pk}.", level="success", send_email=False, metadata={ "entity_type": "work_order_pdf", "work_order_id": str(work_order.pk), "vehicle_id": str(work_order.vehicle_id), "section": "work-orders", "pdf_type": generated.pdf_type, "stage": "completed", "generated_pdf_id": str(generated.pk), "download_url": f"/api/fleet/work-orders/{work_order.pk}/generated-pdfs/{generated.pk}/download/", "filename": generated.filename or filename, "expires_at": generated.expires_at.isoformat() if generated.expires_at else None, }, ) return {"status": "ready", "generated_pdf_id": str(generated.pk)} except Exception as exc: generated.status = 'failed' generated.error_message = str(exc) generated.save(update_fields=['status', 'error_message', 'updated_at']) if generated.requested_by: NotificationService.create_notification( recipient=generated.requested_by, title="Greška kod PDF-a", message=f"Generiranje PDF dokumenta nije uspjelo za WO-{work_order.pk}.", level="warning", send_email=False, metadata={ "entity_type": "work_order_pdf", "work_order_id": str(work_order.pk), "vehicle_id": str(work_order.vehicle_id), "section": "work-orders", "pdf_type": generated.pdf_type, "stage": "failed", "generated_pdf_id": str(generated.pk), }, ) logger.exception("Greška kod build_work_order_pdf_cached_task: %s", exc) return {"status": "failed", "error": str(exc)}