fix: regenerate SN archive with task-specific DOCX headers
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

Invalidate cached monthly service-task ZIP archives so exports are rebuilt with the latest task/work-order/vehicle header data. Keep stale detection for work-order archives and add regression tests for per-task header isolation and stale cache replacement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-08-06 20:58:57 +02:00
parent 8ad1f40601
commit 8b90916641
2 changed files with 144 additions and 16 deletions

View File

@@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.urls import reverse
@@ -352,6 +353,58 @@ class WorkOrderImagesEndpointTests(TestCase):
self.assertNotIn('MT250226', header_xml)
self.assertNotIn('LTM-1350', header_xml)
def test_monthly_service_tasks_archive_uses_task_specific_headers_for_each_docx(self):
vehicle_two = Vehicle.objects.create(
asset_type='crane',
registration_number='WO-IMG-002',
make='Tadano',
model='ATF 90',
crane_serial_number='SN-999',
superstructure_working_hours=444,
chassis_working_hours=555,
current_mileage=66677,
)
work_order_two = WorkOrder.objects.create(
vehicle=vehicle_two,
creator=self.user,
display_code='MT160726',
purpose='kontrola',
)
Task.objects.create(
title='Drugi zadatak',
assigned_to=self.user,
vehicle=vehicle_two,
work_order=work_order_two,
scheduled_date=date(2033, 12, 26),
service_report_note='Napomena drugog taska',
)
response = self.client.get('/api/fleet/reports/monthly-service-tasks-archive/?year=2033&month=12')
self.assertEqual(response.status_code, 200, response.content)
archive = zipfile.ZipFile(BytesIO(response.content))
first_docx = 'MT150726.SN-Test_servisni_zadatak.docx'
second_docx = 'MT160726.SN-Drugi_zadatak.docx'
self.assertIn(first_docx, archive.namelist())
self.assertIn(second_docx, archive.namelist())
first_header = zipfile.ZipFile(BytesIO(archive.read(first_docx))).read('word/header1.xml').decode('utf-8', errors='ignore')
second_header = zipfile.ZipFile(BytesIO(archive.read(second_docx))).read('word/header1.xml').decode('utf-8', errors='ignore')
self.assertIn('Liebherr LTM 1090', first_header)
self.assertIn('MT150726', first_header)
self.assertNotIn('Tadano ATF 90', first_header)
self.assertNotIn('MT160726', first_header)
self.assertIn('Tadano ATF 90', second_header)
self.assertIn('SN-999', second_header)
self.assertIn('444', second_header)
self.assertIn('555', second_header)
self.assertIn('66677', second_header)
self.assertIn('MT160726', second_header)
self.assertNotIn('Liebherr LTM 1090', second_header)
self.assertNotIn('MT150726', second_header)
def test_monthly_work_orders_archive_contains_work_orders_and_invoices_folder(self):
WorkOrderInvoice.objects.create(
work_order=self.work_order,
@@ -402,6 +455,36 @@ class WorkOrderImagesEndpointTests(TestCase):
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_service_tasks_archive_request_ignores_stale_cached_archive(self):
stale = GeneratedFleetArchive.objects.create(
requested_by=self.user,
archive_type='service_tasks',
year=2033,
month=12,
status='ready',
filename='stale-archive.zip',
expires_at=timezone.now() + timedelta(days=2),
)
stale.file.save('stale-archive.zip', ContentFile(b'OLD-ARCHIVE'), save=True)
stale_updated_at = timezone.now() - timedelta(days=1)
GeneratedFleetArchive.objects.filter(pk=stale.pk).update(updated_at=stale_updated_at)
self.task.service_report_note = 'Nova napomena nakon cache-a'
self.task.save()
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)
self.assertNotEqual(str(stale.pk), str(payload['generated_archive_id']))
stale.refresh_from_db()
self.assertFalse(stale.is_active)
self.assertEqual(stale.status, 'failed')
def test_monthly_work_orders_archive_request_creates_ready_download_with_notification(self):
WorkOrderInvoice.objects.create(
work_order=self.work_order,

View File

@@ -23,7 +23,7 @@ from django.http import HttpResponse, FileResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.html import escape
from django.db.models import Q
from django.db.models import Q, Max
from django.db import transaction
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
@@ -2959,6 +2959,33 @@ def _get_cached_generated_archive(*, user, archive_type, year, month):
)
def _latest_monthly_archive_source_update(*, user, archive_type, year, month):
from modules.task_management.models import Task
base_tasks = Task.objects.filter(
assigned_to=user,
is_active=True,
scheduled_date__year=year,
scheduled_date__month=month,
work_order__isnull=False,
work_order__is_active=True,
)
latest_candidates = [
base_tasks.aggregate(value=Max('updated_at')).get('value'),
base_tasks.aggregate(value=Max('work_order__updated_at')).get('value'),
base_tasks.aggregate(value=Max('work_order__vehicle__updated_at')).get('value'),
base_tasks.aggregate(value=Max('work_hours_table__updated_at')).get('value'),
]
if archive_type == 'work_orders':
latest_candidates.append(
base_tasks.aggregate(value=Max('work_order__invoices__updated_at')).get('value')
)
latest_values = [value for value in latest_candidates if value is not None]
if not latest_values:
return None
return max(latest_values)
def _request_monthly_archive_generation(*, request, archive_type):
year, month = _parse_year_month_params(request)
_cleanup_expired_generated_archive_records()
@@ -2970,6 +2997,24 @@ def _request_monthly_archive_generation(*, request, archive_type):
month=month,
)
if cached:
if archive_type == 'service_tasks':
cached.is_active = False
cached.status = 'failed'
cached.error_message = 'ZIP arhiva servisnih taskova se regenerira za najnoviji kontekst.'
cached.save(update_fields=['is_active', 'status', 'error_message', 'updated_at'])
else:
latest_source_update = _latest_monthly_archive_source_update(
user=request.user,
archive_type=archive_type,
year=year,
month=month,
)
if latest_source_update and cached.updated_at and cached.updated_at < latest_source_update:
cached.is_active = False
cached.status = 'failed'
cached.error_message = 'ZIP arhiva zastarjela zbog novijih izmjena izvora.'
cached.save(update_fields=['is_active', 'status', 'error_message', 'updated_at'])
else:
_notify_monthly_archive_request(
user=request.user,
archive_type=archive_type,