feat: add async monthly ZIP generation with notifications
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:
@@ -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)}
|
||||
Reference in New Issue
Block a user