fix: regenerate SN archive with task-specific DOCX headers
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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user