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.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
@@ -352,6 +353,58 @@ class WorkOrderImagesEndpointTests(TestCase):
self.assertNotIn('MT250226', header_xml) self.assertNotIn('MT250226', header_xml)
self.assertNotIn('LTM-1350', 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): def test_monthly_work_orders_archive_contains_work_orders_and_invoices_folder(self):
WorkOrderInvoice.objects.create( WorkOrderInvoice.objects.create(
work_order=self.work_order, 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='requested').exists())
self.assertTrue(notifications.filter(metadata__entity_type='fleet_archive', metadata__stage='completed').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): def test_monthly_work_orders_archive_request_creates_ready_download_with_notification(self):
WorkOrderInvoice.objects.create( WorkOrderInvoice.objects.create(
work_order=self.work_order, work_order=self.work_order,

View File

@@ -23,7 +23,7 @@ from django.http import HttpResponse, FileResponse
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape 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 django.db import transaction
from reportlab.lib.pagesizes import A4 from reportlab.lib.pagesizes import A4
from reportlab.lib import colors 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): def _request_monthly_archive_generation(*, request, archive_type):
year, month = _parse_year_month_params(request) year, month = _parse_year_month_params(request)
_cleanup_expired_generated_archive_records() _cleanup_expired_generated_archive_records()
@@ -2970,21 +2997,39 @@ def _request_monthly_archive_generation(*, request, archive_type):
month=month, month=month,
) )
if cached: if cached:
_notify_monthly_archive_request( if archive_type == 'service_tasks':
user=request.user, cached.is_active = False
archive_type=archive_type, cached.status = 'failed'
stage='completed', cached.error_message = 'ZIP arhiva servisnih taskova se regenerira za najnoviji kontekst.'
year=year, cached.save(update_fields=['is_active', 'status', 'error_message', 'updated_at'])
month=month, else:
generated_archive=cached, latest_source_update = _latest_monthly_archive_source_update(
) user=request.user,
return { archive_type=archive_type,
'status': 'ready', year=year,
'generated_archive_id': str(cached.pk), month=month,
'download_url': f"fleet/reports/generated-archives/{cached.pk}/download/", )
'filename': cached.filename, if latest_source_update and cached.updated_at and cached.updated_at < latest_source_update:
'expires_at': cached.expires_at.isoformat() if cached.expires_at else None, 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,
stage='completed',
year=year,
month=month,
generated_archive=cached,
)
return {
'status': 'ready',
'generated_archive_id': str(cached.pk),
'download_url': f"fleet/reports/generated-archives/{cached.pk}/download/",
'filename': cached.filename,
'expires_at': cached.expires_at.isoformat() if cached.expires_at else None,
}
existing_pending = ( existing_pending = (
GeneratedFleetArchive.objects GeneratedFleetArchive.objects