feat: dodaj display_code polje za putne naloge (MT070626 format)
- Dodano polje 'display_code' na WorkOrder model (format: 2 inicijala + ddmmyy) - Migracije za kreiranje polja i backfill postojećih naloga - Ažuriran WorkOrderSerializer za validaciju i serijalizaciju display_code - Backend tasks za auto-generiranje display_code pri kreiranju - Frontend komponente ažurirane za prikaz display_code umjesto UUID: * FleetDashboardShell tablica putnih naloga * WorkOrderDetailModal * TaskCreateModal * Sve PDF stranice - displayIds.js utility za formatiranjeDisplay ID-eva - Admin interface prikazuje display_code
This commit is contained in:
@@ -44,7 +44,7 @@ class ServiceAttachmentInline(admin.TabularInline):
|
||||
class WorkOrderInline(admin.TabularInline):
|
||||
model = WorkOrder
|
||||
extra = 0
|
||||
fields = ('creator', 'date', 'start_mileage', 'end_mileage', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'status')
|
||||
fields = ('display_code', 'creator', 'date', 'start_mileage', 'end_mileage', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'status')
|
||||
readonly_fields = ('date',)
|
||||
|
||||
|
||||
@@ -176,9 +176,9 @@ class CraneAdmin(VehicleAdmin):
|
||||
|
||||
@admin.register(WorkOrder)
|
||||
class WorkOrderAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'vehicle', 'creator', 'date', 'location', 'travel_start_at', 'travel_end_at', 'start_mileage', 'end_mileage', 'distance_display', 'status')
|
||||
list_display = ('display_code', 'id', 'vehicle', 'creator', 'date', 'location', 'travel_start_at', 'travel_end_at', 'start_mileage', 'end_mileage', 'distance_display', 'status')
|
||||
list_filter = ('status', 'date', 'vehicle')
|
||||
search_fields = ('vehicle__registration_number', 'creator__username', 'purpose')
|
||||
search_fields = ('display_code', 'vehicle__registration_number', 'creator__username', 'purpose')
|
||||
readonly_fields = ('date',)
|
||||
ordering = ('-date',)
|
||||
inlines = (WorkOrderInvoiceInline, WorkOrderPhotoInline)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-17 21:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fleet', '0024_alter_workorderphoto_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='workorder',
|
||||
name='display_code',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text='Format: MT070626 (2 slova + 6 znamenki).',
|
||||
max_length=8,
|
||||
verbose_name='Prikaz putnog naloga',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def backfill_workorder_display_code(apps, schema_editor):
|
||||
WorkOrder = apps.get_model('fleet', 'WorkOrder')
|
||||
for work_order in WorkOrder.objects.select_related('creator').all():
|
||||
current_code = str(getattr(work_order, 'display_code', '') or '').strip().upper()
|
||||
if current_code:
|
||||
continue
|
||||
creator = getattr(work_order, 'creator', None)
|
||||
first_name = str(getattr(creator, 'first_name', '') or '').strip()
|
||||
last_name = str(getattr(creator, 'last_name', '') or '').strip()
|
||||
if not first_name or not last_name:
|
||||
continue
|
||||
order_date = getattr(work_order, 'date', None)
|
||||
if not order_date:
|
||||
continue
|
||||
display_code = f"{first_name[0].upper()}{last_name[0].upper()}{order_date.strftime('%d%m%y')}"
|
||||
WorkOrder.objects.filter(pk=work_order.pk).update(display_code=display_code)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fleet', '0025_workorder_display_code'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_workorder_display_code, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -287,6 +287,13 @@ class WorkOrder(BaseModel):
|
||||
location = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija odredišta"))
|
||||
travel_start_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Početak puta"))
|
||||
travel_end_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Kraj puta"))
|
||||
display_code = models.CharField(
|
||||
max_length=8,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
verbose_name=_("Prikaz putnog naloga"),
|
||||
help_text=_("Format: MT070626 (2 slova + 6 znamenki)."),
|
||||
)
|
||||
purpose = models.CharField(max_length=50, choices=PURPOSE_CHOICES, blank=True, verbose_name=_("Svrha"))
|
||||
notes = models.TextField(blank=True, verbose_name=_("Bilješke"))
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open', verbose_name=_("Status"))
|
||||
@@ -322,6 +329,12 @@ class WorkOrder(BaseModel):
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.display_code:
|
||||
self.display_code = str(self.display_code).strip().upper()
|
||||
if not re.fullmatch(r"[A-Z]{2}[0-9]{6}", self.display_code):
|
||||
raise ValidationError({
|
||||
"display_code": _("Prikaz putnog naloga mora biti u formatu MT070626 (2 slova + 6 znamenki).")
|
||||
})
|
||||
if self.travel_start_at and self.travel_end_at and self.travel_end_at < self.travel_start_at:
|
||||
raise ValidationError({"travel_end_at": _("Kraj puta ne može biti prije početka puta.")})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
import re
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from .models import (
|
||||
Crane,
|
||||
Vehicle,
|
||||
@@ -98,7 +99,7 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = WorkOrder
|
||||
fields = [
|
||||
'id', 'vehicle', 'crane', 'crane_label', 'creator', 'date',
|
||||
'id', 'display_code', 'vehicle', 'crane', 'crane_label', 'creator', 'date',
|
||||
'start_mileage', 'end_mileage',
|
||||
'servicer_vehicle_registration', 'servicer_vehicle_make_model',
|
||||
'servicer_vehicle_start_mileage', 'servicer_vehicle_end_mileage',
|
||||
@@ -112,6 +113,7 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
||||
'creator': {'required': False},
|
||||
'start_mileage': {'required': False, 'allow_null': True},
|
||||
'end_mileage': {'required': False, 'allow_null': True},
|
||||
'display_code': {'required': False, 'allow_blank': False},
|
||||
'servicer_vehicle_registration': {'required': False, 'allow_blank': True},
|
||||
'servicer_vehicle_make_model': {'required': False, 'allow_blank': True},
|
||||
'servicer_vehicle_start_mileage': {'required': False, 'allow_null': True},
|
||||
@@ -146,6 +148,14 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
||||
|
||||
if user and 'creator' not in validated_data:
|
||||
validated_data['creator'] = user
|
||||
if not validated_data.get('display_code'):
|
||||
creator = validated_data.get('creator')
|
||||
generated_display_code = self._generate_display_code(creator, timezone.localdate())
|
||||
if not generated_display_code:
|
||||
raise serializers.ValidationError({
|
||||
'display_code': 'Serviser mora imati ime i prezime za automatsko generiranje oznake. Unesite oznaku ručno u formatu MT070626.'
|
||||
})
|
||||
validated_data['display_code'] = generated_display_code
|
||||
|
||||
return services.VehicleService.create_work_order(validated_data)
|
||||
|
||||
@@ -182,6 +192,22 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
||||
raise serializers.ValidationError({'servicer_vehicle_fuel_cost': 'Trošak goriva ne može biti negativan.'})
|
||||
return attrs
|
||||
|
||||
def validate_display_code(self, value):
|
||||
normalized = str(value or '').strip().upper()
|
||||
if not re.fullmatch(r"[A-Z]{2}[0-9]{6}", normalized):
|
||||
raise serializers.ValidationError('Prikaz putnog naloga mora biti u formatu MT070626 (2 slova + 6 znamenki).')
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def _generate_display_code(user, target_date):
|
||||
if user is None:
|
||||
return None
|
||||
first_name = str(getattr(user, 'first_name', '') or '').strip()
|
||||
last_name = str(getattr(user, 'last_name', '') or '').strip()
|
||||
if not first_name or not last_name:
|
||||
return None
|
||||
return f"{first_name[0].upper()}{last_name[0].upper()}{target_date.strftime('%d%m%y')}"
|
||||
|
||||
|
||||
class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
|
||||
image_url = serializers.SerializerMethodField()
|
||||
|
||||
@@ -141,7 +141,7 @@ class VehicleService:
|
||||
Task.objects.filter(
|
||||
is_active=True,
|
||||
work_order=instance,
|
||||
status='servis',
|
||||
status__in=('aktivan', 'servis'),
|
||||
).update(status='spreman_za_zavrsetak')
|
||||
|
||||
# Ako je nalog upravo zatvoren, obavijestimo kreatora/servisera
|
||||
|
||||
@@ -18,6 +18,13 @@ logger = logging.getLogger(__name__)
|
||||
register_unicode_fonts()
|
||||
|
||||
|
||||
def _work_order_display_code(work_order):
|
||||
normalized = str(getattr(work_order, 'display_code', '') or '').strip().upper()
|
||||
if normalized:
|
||||
return normalized
|
||||
return 'NALOG'
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
|
||||
def send_notification_email(self, notification_id):
|
||||
"""
|
||||
@@ -95,6 +102,7 @@ def _build_work_order_invoices_pdf(work_order):
|
||||
chassis_hours = str(getattr(work_order.vehicle, "chassis_working_hours", "-") or "-")
|
||||
mileage = str(getattr(work_order.vehicle, "current_mileage", "-") or "-")
|
||||
generated_date = timezone.localtime(timezone.now()).strftime("%d.%m.%Y")
|
||||
display_code = _work_order_display_code(work_order)
|
||||
|
||||
def draw_header_footer(page_num):
|
||||
draw_standard_header_footer(
|
||||
@@ -107,7 +115,7 @@ def _build_work_order_invoices_pdf(work_order):
|
||||
upgrade_hours=upgrade_hours,
|
||||
chassis_hours=chassis_hours,
|
||||
mileage=mileage,
|
||||
work_order_number=str(work_order.pk),
|
||||
work_order_number=display_code,
|
||||
generated_date=generated_date,
|
||||
report_title="Računi putnog naloga",
|
||||
page_size=A4,
|
||||
@@ -122,7 +130,7 @@ def _build_work_order_invoices_pdf(work_order):
|
||||
y -= 8
|
||||
|
||||
pdf.setFont("Vera-Bold", 13)
|
||||
pdf.drawString(margin, y, f"Računi putnog naloga WO-{work_order.pk}")
|
||||
pdf.drawString(margin, y, f"Računi putnog naloga {display_code}")
|
||||
y -= 20
|
||||
pdf.setFont("Vera", 10)
|
||||
pdf.drawString(margin, y, f"Datum naloga: {work_order.date}")
|
||||
@@ -202,7 +210,7 @@ def build_work_order_invoices_pdf_task(work_order_id):
|
||||
|
||||
pdf_bytes = _build_work_order_invoices_pdf(work_order)
|
||||
pdf_b64 = base64.b64encode(pdf_bytes).decode('ascii')
|
||||
filename = f"{work_order.pk}.work-order-invoices.pdf"
|
||||
filename = f"{_work_order_display_code(work_order)}.work-order-invoices.pdf"
|
||||
return {
|
||||
"filename": filename,
|
||||
"pdf_base64": pdf_b64,
|
||||
@@ -259,7 +267,7 @@ def build_work_order_pdf_cached_task(generated_pdf_id):
|
||||
else:
|
||||
pdf_bytes = _build_work_order_pdf(work_order)
|
||||
|
||||
filename = generated.filename or f"{work_order.pk}.{generated.pdf_type}.pdf"
|
||||
filename = generated.filename or f"{_work_order_display_code(work_order)}.{generated.pdf_type}.pdf"
|
||||
generated.file.save(filename, ContentFile(pdf_bytes), save=False)
|
||||
generated.status = 'ready'
|
||||
generated.error_message = ''
|
||||
@@ -269,7 +277,7 @@ def build_work_order_pdf_cached_task(generated_pdf_id):
|
||||
NotificationService.create_notification(
|
||||
recipient=generated.requested_by,
|
||||
title="PDF spreman",
|
||||
message=f"PDF dokument je uspješno generiran za WO-{work_order.pk}.",
|
||||
message=f"PDF dokument je uspješno generiran za putni nalog {_work_order_display_code(work_order)}.",
|
||||
level="success",
|
||||
send_email=False,
|
||||
metadata={
|
||||
@@ -294,7 +302,7 @@ def build_work_order_pdf_cached_task(generated_pdf_id):
|
||||
NotificationService.create_notification(
|
||||
recipient=generated.requested_by,
|
||||
title="Greška kod PDF-a",
|
||||
message=f"Generiranje PDF dokumenta nije uspjelo za WO-{work_order.pk}.",
|
||||
message=f"Generiranje PDF dokumenta nije uspjelo za putni nalog {_work_order_display_code(work_order)}.",
|
||||
level="warning",
|
||||
send_email=False,
|
||||
metadata={
|
||||
|
||||
@@ -3,7 +3,9 @@ from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.fleet.services import VehicleService
|
||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord
|
||||
from modules.fleet.serializers import WorkOrderSerializer
|
||||
from modules.task_management.models import Task
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
|
||||
class FleetServiceTests(TestCase):
|
||||
@@ -72,4 +74,58 @@ class FleetServiceTests(TestCase):
|
||||
|
||||
VehicleService.update_work_order(work_order, {"status": "closed"})
|
||||
task.refresh_from_db()
|
||||
self.assertEqual(task.status, "spreman_za_zavrsetak")
|
||||
self.assertEqual(task.status, "spreman_za_zavrsetak")
|
||||
|
||||
def test_closing_work_order_moves_active_task_to_ready_for_completion(self):
|
||||
work_order = WorkOrder.objects.create(
|
||||
vehicle=self.vehicle,
|
||||
creator=self.user,
|
||||
purpose="kontrola",
|
||||
status="open",
|
||||
start_mileage=5000,
|
||||
end_mileage=5200,
|
||||
)
|
||||
task = Task.objects.create(
|
||||
title="Preventiva",
|
||||
description="",
|
||||
status="aktivan",
|
||||
assigned_to=self.user,
|
||||
vehicle=self.vehicle,
|
||||
work_order=work_order,
|
||||
)
|
||||
|
||||
VehicleService.update_work_order(work_order, {"status": "closed"})
|
||||
task.refresh_from_db()
|
||||
self.assertEqual(task.status, "spreman_za_zavrsetak")
|
||||
|
||||
def test_work_order_serializer_autogenerates_display_code(self):
|
||||
self.user.first_name = "Mario"
|
||||
self.user.last_name = "Tkalac"
|
||||
self.user.save(update_fields=["first_name", "last_name"])
|
||||
|
||||
request = APIRequestFactory().post('/api/fleet/work-orders/', {})
|
||||
request.user = self.user
|
||||
serializer = WorkOrderSerializer(
|
||||
data={
|
||||
"vehicle": str(self.vehicle.id),
|
||||
"purpose": "kontrola",
|
||||
},
|
||||
context={"request": request},
|
||||
)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
work_order = serializer.save()
|
||||
self.assertRegex(work_order.display_code, r'^MT[0-9]{6}$')
|
||||
|
||||
def test_work_order_serializer_rejects_invalid_display_code(self):
|
||||
request = APIRequestFactory().post('/api/fleet/work-orders/', {})
|
||||
request.user = self.user
|
||||
serializer = WorkOrderSerializer(
|
||||
data={
|
||||
"vehicle": str(self.vehicle.id),
|
||||
"purpose": "kontrola",
|
||||
"display_code": "BAD-CODE",
|
||||
},
|
||||
context={"request": request},
|
||||
)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("display_code", serializer.errors)
|
||||
@@ -142,17 +142,26 @@ def _parse_format(value):
|
||||
GENERATED_PDF_TTL_HOURS = 24
|
||||
|
||||
|
||||
def _work_order_display_code(work_order):
|
||||
normalized = str(getattr(work_order, 'display_code', '') or '').strip().upper()
|
||||
if normalized:
|
||||
return normalized
|
||||
return 'NALOG'
|
||||
|
||||
|
||||
def _pdf_filename(work_order, pdf_type):
|
||||
display_code = _work_order_display_code(work_order)
|
||||
if pdf_type == 'invoices':
|
||||
return f"{work_order.pk}.work-order-invoices.pdf"
|
||||
return f"{display_code}.work-order-invoices.pdf"
|
||||
if pdf_type == 'service_records':
|
||||
return f"{work_order.pk}.work-order-service-records.pdf"
|
||||
return f"{work_order.pk}.work-order.pdf"
|
||||
return f"{display_code}.work-order-service-records.pdf"
|
||||
return f"{display_code}.work-order.pdf"
|
||||
|
||||
|
||||
def _get_cached_pdf(work_order, pdf_type):
|
||||
now = timezone.now()
|
||||
return (
|
||||
expected_filename = _pdf_filename(work_order, pdf_type)
|
||||
candidates = list(
|
||||
GeneratedWorkOrderPdf.objects
|
||||
.filter(
|
||||
is_active=True,
|
||||
@@ -164,8 +173,11 @@ def _get_cached_pdf(work_order, pdf_type):
|
||||
.exclude(file='')
|
||||
.exclude(file__isnull=True)
|
||||
.order_by('-created_at')
|
||||
.first()
|
||||
)
|
||||
for candidate in candidates:
|
||||
if str(candidate.filename or '').strip() == expected_filename:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _cleanup_expired_generated_pdfs():
|
||||
@@ -203,17 +215,18 @@ def _notify_pdf_request(*, user, work_order, doc_type, stage, generated_pdf=None
|
||||
else:
|
||||
doc_label = "računa putnog naloga"
|
||||
|
||||
display_code = _work_order_display_code(work_order)
|
||||
if stage == 'requested':
|
||||
title = f"Zahtjev za PDF {doc_label}"
|
||||
message = f"Zaprimljen je zahtjev za generiranje PDF dokumenta za WO-{work_order.pk}."
|
||||
message = f"Zaprimljen je zahtjev za generiranje PDF dokumenta za putni nalog {display_code}."
|
||||
level = "info"
|
||||
elif stage == 'failed':
|
||||
title = f"Greška kod PDF-a ({doc_label})"
|
||||
message = f"Generiranje PDF dokumenta nije uspjelo za WO-{work_order.pk}."
|
||||
message = f"Generiranje PDF dokumenta nije uspjelo za putni nalog {display_code}."
|
||||
level = "warning"
|
||||
else:
|
||||
title = f"PDF spreman ({doc_label})"
|
||||
message = f"PDF dokument je uspješno generiran za WO-{work_order.pk}."
|
||||
message = f"PDF dokument je uspješno generiran za putni nalog {display_code}."
|
||||
level = "success"
|
||||
|
||||
metadata = {
|
||||
@@ -337,6 +350,7 @@ def _build_work_order_pdf(work_order):
|
||||
invoices = list(work_order.invoices.filter(is_active=True).order_by('-datum', '-created_at'))
|
||||
now_local = timezone.localtime(timezone.now())
|
||||
tz = timezone.get_current_timezone()
|
||||
display_code = _work_order_display_code(work_order)
|
||||
|
||||
def _fmt_date(value):
|
||||
if not value:
|
||||
@@ -437,7 +451,7 @@ def _build_work_order_pdf(work_order):
|
||||
_paragraph("N A L O G<br/>ZA SLUŽBENO PUTOVANJE", bold=True, size=11, align=1),
|
||||
_paragraph(
|
||||
f"Mjesto i datum:<br/><b>{place_label} {_fmt_date(now_local.date())}</b><br/>"
|
||||
f"Broj naloga:<br/><b>{work_order.pk}</b>",
|
||||
f"Broj naloga:<br/><b>{display_code}</b>",
|
||||
size=8.8,
|
||||
),
|
||||
]]
|
||||
@@ -686,7 +700,7 @@ def _build_work_order_service_records_pdf(work_order):
|
||||
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 = str(work_order.pk)
|
||||
nalog_str = _work_order_display_code(work_order)
|
||||
generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y')
|
||||
|
||||
HEADER_H = 86
|
||||
@@ -1064,7 +1078,7 @@ def _build_service_record_pdf(service_record):
|
||||
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 = str(getattr(work_order, 'pk', '-') or '-')
|
||||
nalog_str = _work_order_display_code(work_order) if work_order else '-'
|
||||
generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y')
|
||||
|
||||
HEADER_H = 86
|
||||
@@ -1487,7 +1501,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
||||
pdf_bytes = _build_work_order_pdf(work_order)
|
||||
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='work_order', stage='completed')
|
||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="{work_order.pk}.work-order.pdf"'
|
||||
response['Content-Disposition'] = f'attachment; filename="{_pdf_filename(work_order, "work_order")}"'
|
||||
return response
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='pdf-request')
|
||||
@@ -1508,7 +1522,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
||||
pdf_bytes = _build_work_order_service_records_pdf(work_order)
|
||||
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='service_records', stage='completed')
|
||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="{work_order.pk}.work-order-service-records.pdf"'
|
||||
response['Content-Disposition'] = f'attachment; filename="{_pdf_filename(work_order, "service_records")}"'
|
||||
return response
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='service-records-pdf-request')
|
||||
@@ -1545,7 +1559,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
||||
except (ValueError, TypeError):
|
||||
raise DRFValidationError({"detail": "Neispravan PDF sadržaj računa."})
|
||||
|
||||
filename = payload.get('filename') or f"{work_order.pk}.work-order-invoices.pdf"
|
||||
filename = payload.get('filename') or _pdf_filename(work_order, 'invoices')
|
||||
_notify_pdf_request(user=request.user, work_order=work_order, doc_type='invoices', stage='completed')
|
||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
@@ -1600,7 +1614,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Računi putnog naloga WO-{work_order.pk}</title>
|
||||
<title>Računi putnog naloga {_work_order_display_code(work_order)}</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 24px; color: #111827; background: #f8fafc; }}
|
||||
.card {{ background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; margin-bottom: 16px; }}
|
||||
@@ -1614,7 +1628,7 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Putni nalog WO-{work_order.pk} — Računi</h1>
|
||||
<h1>Putni nalog {_work_order_display_code(work_order)} — Računi</h1>
|
||||
<p class="muted">Dizalica: {escape(work_order.vehicle.registration_number)} | Datum naloga: {escape(str(work_order.date))}</p>
|
||||
<div class="actions">
|
||||
<a href="{escape(pdf_url)}">Preuzmi PDF putnog naloga</a>
|
||||
@@ -1693,13 +1707,13 @@ class WorkOrderViewSet(viewsets.ModelViewSet):
|
||||
raise DRFValidationError({"recipient": "Nije pronađena email adresa za slanje."})
|
||||
|
||||
pdf_bytes = _build_work_order_pdf(work_order)
|
||||
subject = _first_non_empty(request.data.get('subject')) or f"Putni nalog WO-{work_order.pk}"
|
||||
subject = _first_non_empty(request.data.get('subject')) or f"Putni nalog {_work_order_display_code(work_order)}"
|
||||
body = _first_non_empty(request.data.get('message')) or "U prilogu je PDF putnog naloga."
|
||||
_send_document_email(
|
||||
recipient=recipient,
|
||||
subject=subject,
|
||||
body=body,
|
||||
filename=f"{work_order.pk}.work-order.pdf",
|
||||
filename=_pdf_filename(work_order, 'work_order'),
|
||||
pdf_bytes=pdf_bytes,
|
||||
)
|
||||
return Response({"sent": True, "recipient": recipient}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -155,7 +155,10 @@ class TaskSerializer(serializers.ModelSerializer):
|
||||
def get_work_order_label(self, obj):
|
||||
if not obj.work_order_id:
|
||||
return None
|
||||
return f"PN-{str(obj.work_order_id).split('-')[0].upper()}"
|
||||
display_code = str(getattr(obj.work_order, 'display_code', '') or '').strip().upper()
|
||||
if display_code:
|
||||
return display_code
|
||||
return None
|
||||
|
||||
def get_assigned_to_name(self, obj):
|
||||
if not obj.assigned_to_id:
|
||||
|
||||
Reference in New Issue
Block a user