feat: add async monthly ZIP generation with notifications
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

Implement background generation for monthly SN/PN ZIP archives and expose completion through in-app notifications.

- add GeneratedFleetArchive persistence model with expiry metadata
- add archive request/download endpoints and Celery background tasks
- emit notification stages for requested/completed/failed archive jobs
- update calendar bulk download actions to trigger async requests
- add notification modal actions to download generated ZIP files
- extend backend tests for async archive request and download flows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-08-06 10:59:19 +02:00
parent 594881f6cd
commit 456c9e4c3e
8 changed files with 693 additions and 100 deletions

View File

@@ -27,7 +27,7 @@ from pytesseract import TesseractNotFoundError
from reportlab.lib.pagesizes import A4
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas
from .models import VehicleNotification, GeneratedWorkOrderPdf
from .models import VehicleNotification, GeneratedWorkOrderPdf, GeneratedFleetArchive
from .pdf_layout import register_unicode_fonts, draw_standard_header_footer
from .email_utils import append_user_signature
@@ -855,6 +855,26 @@ def cleanup_expired_generated_pdfs_task():
return {"deleted": deleted}
@shared_task
def cleanup_expired_generated_archives_task():
now = timezone.now()
expired = GeneratedFleetArchive.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 = 'ZIP arhiva je istekla.'
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
@@ -934,4 +954,98 @@ def build_work_order_pdf_cached_task(generated_pdf_id):
},
)
logger.exception("Greška kod build_work_order_pdf_cached_task: %s", exc)
return {"status": "failed", "error": str(exc)}
return {"status": "failed", "error": str(exc)}
@shared_task
def build_monthly_archive_cached_task(generated_archive_id):
from .services import NotificationService
from .views import (
_build_monthly_service_tasks_archive_content,
_build_monthly_work_orders_archive_content,
_generated_archive_filename_for_user,
)
generated = (
GeneratedFleetArchive.objects
.select_related('requested_by')
.filter(pk=generated_archive_id, is_active=True)
.first()
)
if generated is None:
return {'error': 'Generated ZIP zapis nije pronađen.'}
requested_by = generated.requested_by
if requested_by is None:
generated.status = 'failed'
generated.error_message = 'Korisnik koji je zatražio ZIP arhivu nije dostupan.'
generated.save(update_fields=['status', 'error_message', 'updated_at'])
return {'status': 'failed', 'error': generated.error_message}
try:
if generated.archive_type == 'work_orders':
archive_bytes = _build_monthly_work_orders_archive_content(
user=requested_by,
year=generated.year,
month=generated.month,
)
else:
archive_bytes = _build_monthly_service_tasks_archive_content(
user=requested_by,
year=generated.year,
month=generated.month,
)
filename = generated.filename or _generated_archive_filename_for_user(
requested_by,
year=generated.year,
month=generated.month,
archive_type=generated.archive_type,
)
generated.file.save(filename, ContentFile(archive_bytes), save=False)
generated.status = 'ready'
generated.error_message = ''
generated.save(update_fields=['file', 'status', 'error_message', 'updated_at'])
NotificationService.create_notification(
recipient=requested_by,
title='ZIP arhiva spremna',
message=f"ZIP arhiva je spremna za preuzimanje ({generated.month:02d}.{generated.year}.).",
level='success',
send_email=False,
metadata={
'entity_type': 'fleet_archive',
'archive_type': generated.archive_type,
'stage': 'completed',
'year': generated.year,
'month': generated.month,
'generated_archive_id': str(generated.pk),
'download_url': f"fleet/reports/generated-archives/{generated.pk}/download/",
'filename': generated.filename or filename,
'expires_at': generated.expires_at.isoformat() if generated.expires_at else None,
'section': 'service-records',
},
)
return {'status': 'ready', 'generated_archive_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'])
NotificationService.create_notification(
recipient=requested_by,
title='Greška kod ZIP arhive',
message=f"Generiranje ZIP arhive nije uspjelo ({generated.month:02d}.{generated.year}.).",
level='warning',
send_email=False,
metadata={
'entity_type': 'fleet_archive',
'archive_type': generated.archive_type,
'stage': 'failed',
'year': generated.year,
'month': generated.month,
'generated_archive_id': str(generated.pk),
'section': 'service-records',
},
)
logger.exception("Greška kod build_monthly_archive_cached_task: %s", exc)
return {'status': 'failed', 'error': str(exc)}