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

@@ -65,6 +65,16 @@ def _generated_work_order_pdf_upload_to(instance, filename):
return f"fleet/{vehicle_reg}/generated_pdfs/{unique_name}"
def _generated_fleet_archive_upload_to(instance, filename):
requester = 'unknown'
try:
requester = str(instance.requested_by_id or 'unknown')
except Exception:
requester = 'unknown'
unique_name = f"{uuid.uuid4().hex}_{filename}"
return f"fleet/generated_archives/{requester}/{unique_name}"
class VehicleServicePhoto(BaseModel):
"""
Fotografije kvarova / radova povezane s VehicleServiceRecord.
@@ -480,6 +490,59 @@ class GeneratedWorkOrderPdf(BaseModel):
return f"{self.pdf_type}:{self.work_order_id}:{self.status}"
class GeneratedFleetArchive(BaseModel):
ARCHIVE_TYPE_CHOICES = [
('service_tasks', _("Pojedinačni servisni taskovi")),
('work_orders', _("Putni nalozi i računi")),
]
STATUS_CHOICES = [
('pending', _("U obradi")),
('ready', _("Spremno")),
('failed', _("Neuspješno")),
]
requested_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='requested_generated_fleet_archives',
verbose_name=_("Zatražio"),
)
archive_type = models.CharField(
max_length=32,
choices=ARCHIVE_TYPE_CHOICES,
db_index=True,
verbose_name=_("Tip ZIP arhive"),
)
year = models.PositiveSmallIntegerField(db_index=True, verbose_name=_("Godina"))
month = models.PositiveSmallIntegerField(db_index=True, verbose_name=_("Mjesec"))
status = models.CharField(
max_length=16,
choices=STATUS_CHOICES,
default='pending',
db_index=True,
verbose_name=_("Status"),
)
file = models.FileField(
upload_to=_generated_fleet_archive_upload_to,
null=True,
blank=True,
verbose_name=_("ZIP datoteka"),
)
filename = models.CharField(max_length=255, blank=True, verbose_name=_("Naziv datoteke"))
expires_at = models.DateTimeField(null=True, blank=True, db_index=True, verbose_name=_("Vrijedi do"))
error_message = models.TextField(blank=True, verbose_name=_("Poruka greške"))
class Meta:
verbose_name = _("Generirana ZIP arhiva")
verbose_name_plural = _("Generirane ZIP arhive")
ordering = ['-created_at']
def __str__(self):
return f"{self.archive_type}:{self.year}-{self.month:02d}:{self.status}"
class VehicleServiceRecord(BaseModel):
"""
Zapisi o servisima/popravcima na vozilu.