fix: use task-specific service report context
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

Ensure service report DOCX/PDF headers and monthly ZIP archives use task/work-order context, with async fallback when the broker is unavailable.
This commit is contained in:
mariomitte
2026-08-07 01:15:31 +02:00
parent 8b90916641
commit cb165df30c
2 changed files with 104 additions and 23 deletions

View File

@@ -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,

View File

@@ -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',