# 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 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 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, }