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 f83f789..9a750cb 100644 --- a/backend/modules/fleet/tests/test_work_order_images_endpoint.py +++ b/backend/modules/fleet/tests/test_work_order_images_endpoint.py @@ -9,7 +9,10 @@ import uuid from io import BytesIO from datetime import date, timedelta import zipfile +from unittest.mock import patch from PIL import Image +from pypdf import PdfReader +from kombu.exceptions import OperationalError as KombuOperationalError from modules.fleet.models import ( Vehicle, @@ -324,6 +327,48 @@ class WorkOrderImagesEndpointTests(TestCase): self.assertNotIn('MT250226', header_xml) self.assertNotIn('LTM-1350', header_xml) + def test_task_specific_service_records_pdf_uses_task_header_values(self): + vehicle_two = Vehicle.objects.create( + asset_type='crane', + registration_number='WO-IMG-003', + make='Tadano', + model='ATF 120', + crane_serial_number='SN-456', + superstructure_working_hours=777, + chassis_working_hours=888, + current_mileage=99911, + ) + work_order_two = WorkOrder.objects.create( + vehicle=vehicle_two, + creator=self.user, + display_code='MT170726', + purpose='kontrola', + ) + task_two = Task.objects.create( + title='PDF kontekst task', + assigned_to=self.user, + vehicle=vehicle_two, + work_order=work_order_two, + scheduled_date=date(2033, 12, 27), + ) + + response = self.client.get( + f"/api/fleet/work-orders/{work_order_two.pk}/service-records-pdf/?task_id={task_two.pk}" + ) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual(response['Content-Type'], 'application/pdf') + + reader = PdfReader(BytesIO(response.content)) + text = "\n".join((page.extract_text() or '') for page in reader.pages) + self.assertIn('Tadano', text) + self.assertIn('ATF 120', text) + self.assertIn('SN-456', text) + self.assertIn('777', text) + self.assertIn('888', text) + self.assertIn('99911', text) + self.assertIn('MT170726', text) + self.assertNotIn('MT150726', text) + def test_monthly_service_tasks_archive_returns_zip_with_task_docx(self): response = self.client.get('/api/fleet/reports/monthly-service-tasks-archive/?year=2033&month=12') self.assertEqual(response.status_code, 200, response.content) @@ -485,6 +530,33 @@ class WorkOrderImagesEndpointTests(TestCase): self.assertFalse(stale.is_active) self.assertEqual(stale.status, 'failed') + def test_monthly_service_tasks_archive_request_uses_async_local_fallback_when_broker_unavailable(self): + with ( + patch('modules.fleet.views.build_monthly_archive_cached_task.delay', side_effect=KombuOperationalError('broker-down')), + patch('modules.fleet.views._schedule_monthly_archive_local_fallback') as fallback_mock, + ): + response = self.client.post( + '/api/fleet/reports/monthly-service-tasks-archive-request/', + data={'year': 2033, 'month': 12}, + format='json', + ) + self.assertEqual(response.status_code, 202, response.content) + fallback_mock.assert_called_once() + + generated = ( + GeneratedFleetArchive.objects + .filter( + requested_by=self.user, + archive_type='service_tasks', + year=2033, + month=12, + ) + .order_by('-created_at') + .first() + ) + self.assertIsNotNone(generated) + self.assertEqual(generated.status, 'pending') + 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 de8ef67..6333f65 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -927,17 +927,6 @@ def _build_work_order_pdf(work_order): def _build_work_order_service_records_pdf(work_order, related_tasks=None): - vehicle = work_order.vehicle - client_name = getattr(vehicle.client, 'name', None) or '-' - manufacturer_str = str(vehicle.make or '-') - model_str = str(vehicle.model or '-') - serial_str = str(vehicle.crane_serial_number or '-') - upgrade_hours_str = str(getattr(vehicle, 'superstructure_working_hours', '-') or '-') - chassis_hours_str = str(getattr(vehicle, 'chassis_working_hours', '-') or '-') - mileage_str = str(vehicle.current_mileage or '-') - nalog_str = _work_order_display_code(work_order) - generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y') - HEADER_H = 86 FOOTER_H = 72 MARGIN = 28 @@ -946,6 +935,16 @@ def _build_work_order_service_records_pdf(work_order, related_tasks=None): related_tasks = list(_work_order_related_tasks_queryset(work_order)) else: related_tasks = list(related_tasks) + context = _build_service_report_context(work_order, related_tasks) + client_name = context['client_name'] + manufacturer_str = context['manufacturer'] + model_str = context['model_name'] + serial_str = context['serial'] + upgrade_hours_str = context['upgrade_hours'] + chassis_hours_str = context['chassis_hours'] + mileage_str = context['mileage'] + nalog_str = context['work_order_display_code'] + generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y') # Collect service records for all task vehicles (supports cross-crane work orders) task_vehicle_ids = list({task.vehicle_id for task in related_tasks if task.vehicle_id}) service_rows = list( @@ -1083,8 +1082,8 @@ def _build_work_order_service_records_pdf(work_order, related_tasks=None): y = content_top table1 = [ - ["Naziv Tvrtke:", client_name, "Lokacija intervencije:", work_order.location or '-'], - ["Serviser:", _user_display_name(work_order.creator) or '-', "Asistirao:", '-'], + ["Naziv Tvrtke:", client_name, "Lokacija intervencije:", context['location']], + ["Serviser:", context['servicer_name'], "Asistirao:", '-'], ] y = draw_table( y, @@ -1102,10 +1101,9 @@ def _build_work_order_service_records_pdf(work_order, related_tasks=None): ]), ) - completion_label = "Da ☒ Ne ☐" if work_order.status == 'closed' else "Da ☐ Ne ☒" table2 = [ ["Prijevozno sredstvo:", "Registracija:", "Broj narudžbe klijenta:", "Posao završen"], - [work_order.servicer_vehicle_make_model or '-', work_order.servicer_vehicle_registration or '-', "-", completion_label], + [context['servicer_vehicle_make_model'], context['servicer_vehicle_registration'], "-", context['completion_label']], ] y = draw_table( y, @@ -1839,7 +1837,7 @@ def _docx_remove_empty_page_break_paragraphs(document): body.remove(element) -def _build_service_report_docx_context(work_order, related_tasks): +def _build_service_report_context(work_order, related_tasks): primary_task = related_tasks[0] if len(related_tasks) == 1 else None context_work_order = getattr(primary_task, 'work_order', None) if primary_task else None if context_work_order is None: @@ -1849,10 +1847,9 @@ def _build_service_report_docx_context(work_order, related_tasks): if context_vehicle is None: context_vehicle = getattr(context_work_order, 'vehicle', None) or work_order.vehicle - model_parts = [ - str(getattr(context_vehicle, 'make', '') or '').strip(), - str(getattr(context_vehicle, 'model', '') or '').strip(), - ] + manufacturer = str(getattr(context_vehicle, 'make', '') or '').strip() + model_name = str(getattr(context_vehicle, 'model', '') or '').strip() + model_parts = [manufacturer, model_name] model_label = " ".join(part for part in model_parts if part) or '-' return { 'work_order': context_work_order, @@ -1863,6 +1860,8 @@ def _build_service_report_docx_context(work_order, related_tasks): 'servicer_vehicle_make_model': getattr(context_work_order, 'servicer_vehicle_make_model', None) or '-', 'servicer_vehicle_registration': getattr(context_work_order, 'servicer_vehicle_registration', None) or '-', 'completion_label': "Da ☒ Ne ☐" if getattr(context_work_order, 'status', None) == 'closed' else "Da ☐ Ne ☒", + 'manufacturer': manufacturer or '-', + 'model_name': model_name or '-', 'model': model_label, 'serial': str(getattr(context_vehicle, 'crane_serial_number', None) or '-'), 'upgrade_hours': str(getattr(context_vehicle, 'superstructure_working_hours', None) or '-'), @@ -1951,7 +1950,7 @@ def _build_work_order_service_records_docx_bytes(work_order, related_tasks=None) related_tasks = list(_work_order_related_tasks_queryset(work_order).select_related('work_hours_table')) else: related_tasks = list(related_tasks) - context = _build_service_report_docx_context(work_order, related_tasks) + context = _build_service_report_context(work_order, related_tasks) _populate_service_report_docx_header(doc, context) normalized_rows = [] for task in related_tasks: @@ -2986,6 +2985,17 @@ def _latest_monthly_archive_source_update(*, user, archive_type, year, month): return max(latest_values) +def _schedule_monthly_archive_local_fallback(generated_archive_id): + def _runner(): + try: + build_monthly_archive_cached_task.apply(args=[generated_archive_id]).get() + cleanup_expired_generated_archives_task.apply().get() + except Exception as exc: + logger.exception("Lokalni fallback za ZIP arhivu nije uspio: %s", exc) + + threading.Thread(target=_runner, daemon=True).start() + + def _request_monthly_archive_generation(*, request, archive_type): year, month = _parse_year_month_params(request) _cleanup_expired_generated_archive_records() @@ -3076,8 +3086,7 @@ def _request_monthly_archive_generation(*, request, archive_type): build_monthly_archive_cached_task.delay(str(generated_archive.pk)) cleanup_expired_generated_archives_task.delay() except KombuOperationalError: - build_monthly_archive_cached_task.apply(args=[str(generated_archive.pk)]).get() - cleanup_expired_generated_archives_task.apply().get() + _schedule_monthly_archive_local_fallback(str(generated_archive.pk)) return { 'status': 'processing',