diff --git a/backend/modules/fleet/tests/test_work_order_images_endpoint.py b/backend/modules/fleet/tests/test_work_order_images_endpoint.py index ef8ea87..f83f789 100644 --- a/backend/modules/fleet/tests/test_work_order_images_endpoint.py +++ b/backend/modules/fleet/tests/test_work_order_images_endpoint.py @@ -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, diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 9d2c693..de8ef67 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -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,21 +2997,39 @@ def _request_monthly_archive_generation(*, request, archive_type): month=month, ) if cached: - _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, - } + 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, + 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 = ( GeneratedFleetArchive.objects