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

@@ -8,12 +8,20 @@ import uuid
from io import BytesIO
from datetime import date, timedelta
import zipfile
from pathlib import Path
from django.conf import settings
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,
GeneratedFleetArchive,
VehicleNotification,
)
from modules.task_management.models import Task
from modules.fleet.tasks import build_monthly_archive_cached_task
def create_test_image(filename='test.jpg', size=(40, 40), color='red'):
@@ -310,8 +318,6 @@ class WorkOrderImagesEndpointTests(TestCase):
image=create_test_pdf('racun-prosinac.pdf'),
created_by=self.user,
)
before_files = set((Path(settings.MEDIA_ROOT) / 'fleet' / 'generated_archives').glob('*.zip'))
response = self.client.get('/api/fleet/reports/monthly-work-orders-archive/?year=2033&month=12')
self.assertEqual(response.status_code, 200, response.content)
self.assertEqual(response['Content-Type'], 'application/zip')
@@ -321,7 +327,83 @@ class WorkOrderImagesEndpointTests(TestCase):
self.assertIn('MT150726.work-order.pdf', names)
self.assertTrue(any(name.startswith('Racuni/MT150726/') for name in names), names)
generated_archives_dir = Path(settings.MEDIA_ROOT) / 'fleet' / 'generated_archives'
self.assertTrue(generated_archives_dir.exists())
after_files = set(generated_archives_dir.glob('*.zip'))
self.assertTrue(after_files - before_files)
def test_monthly_service_tasks_archive_request_creates_ready_download_with_notification(self):
response = self.client.post(
'/api/fleet/reports/monthly-service-tasks-archive-request/',
data={'year': 2033, 'month': 12},
format='json',
)
self.assertIn(response.status_code, [200, 202], response.content)
payload = response.json()
self.assertIn('generated_archive_id', payload)
generated = GeneratedFleetArchive.objects.filter(
pk=payload['generated_archive_id'],
requested_by=self.user,
archive_type='service_tasks',
).first()
self.assertIsNotNone(generated)
if generated.status != 'ready':
build_monthly_archive_cached_task.apply(args=[str(generated.pk)]).get()
generated.refresh_from_db()
self.assertEqual(generated.status, 'ready')
download_response = self.client.get(f"/api/fleet/reports/generated-archives/{generated.pk}/download/")
self.assertEqual(download_response.status_code, 200)
self.assertEqual(download_response['Content-Type'], 'application/zip')
archive_bytes = b''.join(download_response.streaming_content)
archive = zipfile.ZipFile(BytesIO(archive_bytes))
self.assertIn('MT150726.SN-Test_servisni_zadatak.docx', archive.namelist())
notifications = VehicleNotification.objects.filter(recipient=self.user).order_by('created_at')
self.assertTrue(notifications.filter(metadata__entity_type='fleet_archive', metadata__stage='requested').exists())
self.assertTrue(notifications.filter(metadata__entity_type='fleet_archive', metadata__stage='completed').exists())
def test_monthly_work_orders_archive_request_creates_ready_download_with_notification(self):
WorkOrderInvoice.objects.create(
work_order=self.work_order,
naziv_racuna='Prosinac račun',
datum='2033-12-10',
image=create_test_pdf('racun-prosinac.pdf'),
created_by=self.user,
)
response = self.client.post(
'/api/fleet/reports/monthly-work-orders-archive-request/',
data={'year': 2033, 'month': 12},
format='json',
)
self.assertIn(response.status_code, [200, 202], response.content)
payload = response.json()
self.assertIn('generated_archive_id', payload)
generated = GeneratedFleetArchive.objects.filter(
pk=payload['generated_archive_id'],
requested_by=self.user,
archive_type='work_orders',
).first()
self.assertIsNotNone(generated)
if generated.status != 'ready':
build_monthly_archive_cached_task.apply(args=[str(generated.pk)]).get()
generated.refresh_from_db()
self.assertEqual(generated.status, 'ready')
download_response = self.client.get(f"/api/fleet/reports/generated-archives/{generated.pk}/download/")
self.assertEqual(download_response.status_code, 200)
self.assertEqual(download_response['Content-Type'], 'application/zip')
archive_bytes = b''.join(download_response.streaming_content)
archive = zipfile.ZipFile(BytesIO(archive_bytes))
names = archive.namelist()
self.assertIn('MT150726.work-order.pdf', names)
self.assertTrue(any(name.startswith('Racuni/MT150726/') for name in names), names)
self.assertTrue(
VehicleNotification.objects.filter(
recipient=self.user,
metadata__entity_type='fleet_archive',
metadata__archive_type='work_orders',
metadata__stage='completed',
).exists()
)