fix: use task-specific service report context
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:
@@ -9,7 +9,10 @@ import uuid
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from unittest.mock import patch
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from pypdf import PdfReader
|
||||||
|
from kombu.exceptions import OperationalError as KombuOperationalError
|
||||||
|
|
||||||
from modules.fleet.models import (
|
from modules.fleet.models import (
|
||||||
Vehicle,
|
Vehicle,
|
||||||
@@ -324,6 +327,48 @@ 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_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):
|
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')
|
response = self.client.get('/api/fleet/reports/monthly-service-tasks-archive/?year=2033&month=12')
|
||||||
self.assertEqual(response.status_code, 200, response.content)
|
self.assertEqual(response.status_code, 200, response.content)
|
||||||
@@ -485,6 +530,33 @@ class WorkOrderImagesEndpointTests(TestCase):
|
|||||||
self.assertFalse(stale.is_active)
|
self.assertFalse(stale.is_active)
|
||||||
self.assertEqual(stale.status, 'failed')
|
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):
|
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,
|
||||||
|
|||||||
@@ -927,17 +927,6 @@ def _build_work_order_pdf(work_order):
|
|||||||
|
|
||||||
|
|
||||||
def _build_work_order_service_records_pdf(work_order, related_tasks=None):
|
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
|
HEADER_H = 86
|
||||||
FOOTER_H = 72
|
FOOTER_H = 72
|
||||||
MARGIN = 28
|
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))
|
related_tasks = list(_work_order_related_tasks_queryset(work_order))
|
||||||
else:
|
else:
|
||||||
related_tasks = list(related_tasks)
|
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)
|
# 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})
|
task_vehicle_ids = list({task.vehicle_id for task in related_tasks if task.vehicle_id})
|
||||||
service_rows = list(
|
service_rows = list(
|
||||||
@@ -1083,8 +1082,8 @@ def _build_work_order_service_records_pdf(work_order, related_tasks=None):
|
|||||||
y = content_top
|
y = content_top
|
||||||
|
|
||||||
table1 = [
|
table1 = [
|
||||||
["Naziv Tvrtke:", client_name, "Lokacija intervencije:", work_order.location or '-'],
|
["Naziv Tvrtke:", client_name, "Lokacija intervencije:", context['location']],
|
||||||
["Serviser:", _user_display_name(work_order.creator) or '-', "Asistirao:", '-'],
|
["Serviser:", context['servicer_name'], "Asistirao:", '-'],
|
||||||
]
|
]
|
||||||
y = draw_table(
|
y = draw_table(
|
||||||
y,
|
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 = [
|
table2 = [
|
||||||
["Prijevozno sredstvo:", "Registracija:", "Broj narudžbe klijenta:", "Posao završen"],
|
["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 = draw_table(
|
||||||
y,
|
y,
|
||||||
@@ -1839,7 +1837,7 @@ def _docx_remove_empty_page_break_paragraphs(document):
|
|||||||
body.remove(element)
|
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
|
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
|
context_work_order = getattr(primary_task, 'work_order', None) if primary_task else None
|
||||||
if context_work_order is 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:
|
if context_vehicle is None:
|
||||||
context_vehicle = getattr(context_work_order, 'vehicle', None) or work_order.vehicle
|
context_vehicle = getattr(context_work_order, 'vehicle', None) or work_order.vehicle
|
||||||
|
|
||||||
model_parts = [
|
manufacturer = str(getattr(context_vehicle, 'make', '') or '').strip()
|
||||||
str(getattr(context_vehicle, 'make', '') or '').strip(),
|
model_name = str(getattr(context_vehicle, 'model', '') or '').strip()
|
||||||
str(getattr(context_vehicle, 'model', '') or '').strip(),
|
model_parts = [manufacturer, model_name]
|
||||||
]
|
|
||||||
model_label = " ".join(part for part in model_parts if part) or '-'
|
model_label = " ".join(part for part in model_parts if part) or '-'
|
||||||
return {
|
return {
|
||||||
'work_order': context_work_order,
|
'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_make_model': getattr(context_work_order, 'servicer_vehicle_make_model', None) or '-',
|
||||||
'servicer_vehicle_registration': getattr(context_work_order, 'servicer_vehicle_registration', 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 ☒",
|
'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,
|
'model': model_label,
|
||||||
'serial': str(getattr(context_vehicle, 'crane_serial_number', None) or '-'),
|
'serial': str(getattr(context_vehicle, 'crane_serial_number', None) or '-'),
|
||||||
'upgrade_hours': str(getattr(context_vehicle, 'superstructure_working_hours', 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'))
|
related_tasks = list(_work_order_related_tasks_queryset(work_order).select_related('work_hours_table'))
|
||||||
else:
|
else:
|
||||||
related_tasks = list(related_tasks)
|
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)
|
_populate_service_report_docx_header(doc, context)
|
||||||
normalized_rows = []
|
normalized_rows = []
|
||||||
for task in related_tasks:
|
for task in related_tasks:
|
||||||
@@ -2986,6 +2985,17 @@ def _latest_monthly_archive_source_update(*, user, archive_type, year, month):
|
|||||||
return max(latest_values)
|
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):
|
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()
|
||||||
@@ -3076,8 +3086,7 @@ def _request_monthly_archive_generation(*, request, archive_type):
|
|||||||
build_monthly_archive_cached_task.delay(str(generated_archive.pk))
|
build_monthly_archive_cached_task.delay(str(generated_archive.pk))
|
||||||
cleanup_expired_generated_archives_task.delay()
|
cleanup_expired_generated_archives_task.delay()
|
||||||
except KombuOperationalError:
|
except KombuOperationalError:
|
||||||
build_monthly_archive_cached_task.apply(args=[str(generated_archive.pk)]).get()
|
_schedule_monthly_archive_local_fallback(str(generated_archive.pk))
|
||||||
cleanup_expired_generated_archives_task.apply().get()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': 'processing',
|
'status': 'processing',
|
||||||
|
|||||||
Reference in New Issue
Block a user