patch oko teksta PDFa i UI tablica
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

This commit is contained in:
mariomitte
2026-07-12 03:25:03 +02:00
parent 3b54499b17
commit 4e3bf6dab0
40 changed files with 2149 additions and 331 deletions

View File

@@ -11,21 +11,13 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.html import escape
from django.db.models import Q
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.utils import ImageReader
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfgen import canvas
from reportlab.platypus import Table, TableStyle, Paragraph
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# Register Unicode-capable fonts (Vera supports HR characters: č, ć, đ, š, ž)
_RL_FONTS_DIR = os.path.join(os.path.dirname(__import__('reportlab').__file__), 'fonts')
pdfmetrics.registerFont(TTFont('Vera', os.path.join(_RL_FONTS_DIR, 'Vera.ttf')))
pdfmetrics.registerFont(TTFont('Vera-Bold', os.path.join(_RL_FONTS_DIR, 'VeraBd.ttf')))
pdfmetrics.registerFont(TTFont('Vera-Italic', os.path.join(_RL_FONTS_DIR, 'VeraIt.ttf')))
from .pdf_layout import register_unicode_fonts, draw_standard_header_footer
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.response import Response
@@ -61,6 +53,8 @@ from .services import (
)
from .tasks import build_work_order_invoices_pdf_task
register_unicode_fonts()
def _fleet_assets_queryset_for_user(user, model, *, asset_type=None):
queryset = model.objects.select_related('client', 'assigned_servicer').filter(is_active=True)
if asset_type:
@@ -226,6 +220,30 @@ def _build_work_order_pdf(work_order):
place_label = (work_order.location or 'Zagreb').split(',')[0].strip() or 'Zagreb'
invoice_names = [inv.naziv_racuna for inv in invoices[:5] if inv.naziv_racuna]
attachments_text = ', '.join(invoice_names) if invoice_names else '-'
assigned_servicer_vehicle = (
creator.assigned_vehicles
.filter(asset_type='vehicle', is_active=True)
.order_by('registration_number')
.first()
)
assigned_servicer_vehicle_label = "-"
assigned_servicer_vehicle_registration = "-"
if assigned_servicer_vehicle:
assigned_servicer_vehicle_label = " ".join(
part for part in [assigned_servicer_vehicle.make, assigned_servicer_vehicle.model] if part
).strip() or assigned_servicer_vehicle.registration_number
assigned_servicer_vehicle_registration = assigned_servicer_vehicle.registration_number or "-"
servicer_vehicle_label = (
assigned_servicer_vehicle_label
if assigned_servicer_vehicle
else (work_order.servicer_vehicle_make_model or '-')
)
servicer_vehicle_registration = (
assigned_servicer_vehicle_registration
if assigned_servicer_vehicle
else (work_order.servicer_vehicle_registration or '-')
)
buffer = BytesIO()
pdf = canvas.Canvas(buffer, pagesize=A4)
@@ -331,7 +349,7 @@ def _build_work_order_pdf(work_order):
[
"1 dana" if travel_start else "-",
_fmt_date(travel_start.date() if travel_start else work_order.date),
f"{work_order.servicer_vehicle_make_model or '-'}, {work_order.servicer_vehicle_registration or '-'}",
f"{servicer_vehicle_label}, {servicer_vehicle_registration}",
],
["Troškovi putovanja terete:", "Posebni dodaci (predujam):", "Početno stanje kilometara:"],
[
@@ -397,7 +415,7 @@ def _build_work_order_pdf(work_order):
y,
[
["RELACIJA od", "RELACIJA do", "Vrsta prijevoznog sredstva", "Razred [km]", "Iznos za prijevoz", "Ukupan iznos"],
[place_label or '-', work_order.location or '-', work_order.servicer_vehicle_make_model or '-', str(work_order.distance or 0), _fmt_eur(transport_total), _fmt_eur(transport_total)],
[place_label or '-', work_order.location or '-', f"{servicer_vehicle_label}, {servicer_vehicle_registration}", str(work_order.distance or 0), _fmt_eur(transport_total), _fmt_eur(transport_total)],
["", "", "", "", "", ""],
["", "", "", "", "", ""],
],
@@ -493,7 +511,8 @@ def _build_work_order_pdf(work_order):
def _build_work_order_service_records_pdf(work_order):
vehicle = work_order.vehicle
client_name = getattr(vehicle.client, 'name', None) or '-'
model_str = ' '.join(filter(None, [vehicle.make, vehicle.model])) 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 '-')
@@ -563,86 +582,24 @@ def _build_work_order_service_records_pdf(work_order):
super().save()
def _draw_header_footer(self):
w, h = A4
page_num = self._current_page
self.setStrokeColor(colors.black)
self.setLineWidth(0.7)
self.rect(MARGIN, h - HEADER_H, w - 2 * MARGIN, HEADER_H - 4, stroke=1, fill=0)
self.setFont("Vera-Bold", 11)
self.drawString(MARGIN + 6, h - 22, client_name[:44])
self.drawCentredString(w / 2, h - 22, "Izvještaj servisera")
self.setFont("Vera", 9)
self.drawRightString(w - MARGIN - 6, h - 22, f"Stranica {page_num}")
self.setLineWidth(0.5)
self.line(MARGIN, h - 36, w - MARGIN, h - 36)
labels = [
"Model:",
"Serijski broj:",
"Radni sati nadogradnje:",
"Radni sati podvozja:",
"Km:",
"Broj naloga:",
]
values = [
model_str,
serial_str,
upgrade_hours_str,
chassis_hours_str,
mileage_str,
nalog_str,
]
col_w = (w - 2 * MARGIN) / len(labels)
self.setFont("Vera", 7.2)
for i, (lbl, val) in enumerate(zip(labels, values)):
x = MARGIN + i * col_w + 3
self.drawString(x, h - 49, lbl)
value_text = str(val or '-')
value_font_size = 9
if i == len(labels) - 1:
value_font_size = 7
max_value_width = col_w - 8
while (
self.stringWidth(value_text, "Vera-Bold", value_font_size) > max_value_width
and value_font_size > 5.5
):
value_font_size -= 0.3
if self.stringWidth(value_text, "Vera-Bold", value_font_size) > max_value_width:
trimmed = value_text
while (
len(trimmed) > 3
and self.stringWidth(f"{trimmed}...", "Vera-Bold", value_font_size) > max_value_width
):
trimmed = trimmed[:-1]
value_text = f"{trimmed}..."
else:
value_text = value_text[:24]
self.setFont("Vera-Bold", value_font_size)
self.drawString(x, h - 61, value_text)
self.setFont("Vera", 7.2)
if i > 0:
self.line(MARGIN + i * col_w, h - 36, MARGIN + i * col_w, h - HEADER_H + 4)
footer_y = FOOTER_H
self.setLineWidth(0.5)
self.rect(MARGIN, footer_y + 42, w - 2 * MARGIN, 18, stroke=1, fill=0)
sig_labels = [f"Datum: {generated_date_str}", "Potpis servisera", "Pečat i potpis klijenta *1", "Pregledao"]
sig_w = (w - 2 * MARGIN) / 4
self.setFont("Vera", 7.2)
for i, lbl in enumerate(sig_labels):
x = MARGIN + i * sig_w
self.drawString(x + 4, footer_y + 45, lbl)
if i > 0:
self.line(x, footer_y + 42, x, footer_y + 60)
self.setFont("Vera", 6.5)
self.drawString(MARGIN, footer_y + 26, "*1- potpisom klijent potvrđuje da je suglasan s podacima u radnom nalogu")
self.drawCentredString(
w / 2,
footer_y + 14,
"Ovlašteni servis LIEBHERR Werk-Ehingen GmbH, LIEBHERR-Werk Nenzing GmbH, Liebherr-MCCTech Rostock GmbH",
draw_standard_header_footer(
self,
page_num=page_num,
client_name=client_name,
manufacturer=manufacturer_str,
model=model_str,
serial=serial_str,
upgrade_hours=upgrade_hours_str,
chassis_hours=chassis_hours_str,
mileage=mileage_str,
work_order_number=nalog_str,
generated_date=generated_date_str,
report_title="Izvještaj servisera",
page_size=A4,
margin=MARGIN,
header_h=HEADER_H,
footer_h=FOOTER_H,
)
pdf = PageCanvas(buffer, pagesize=A4)
@@ -799,7 +756,7 @@ def _build_work_order_service_records_pdf(work_order):
if not related_tasks:
pdf.setFont("Vera", 9)
pdf.drawString(MARGIN, y, "Nema povezanih taskova za ovaj putni nalog.")
pdf.drawString(MARGIN, y, "Nema povezanih servisnih zadataka za ovaj putni nalog.")
else:
for task in related_tasks:
task_records = [
@@ -812,7 +769,7 @@ def _build_work_order_service_records_pdf(work_order):
if y < content_bottom + 60:
y = new_page()
pdf.setFont("Vera-Bold", 10)
pdf.drawString(MARGIN, y, (task.title or f"Task #{task.id}")[:95])
pdf.drawString(MARGIN, y, (task.title or f"Servisni zadatak #{task.id}")[:95])
y -= 14
for record in task_records:
@@ -912,6 +869,247 @@ def _build_work_order_service_records_pdf(work_order):
pdf.save()
return buffer.getvalue()
def _build_service_record_pdf(service_record):
vehicle = service_record.vehicle
task = getattr(service_record, 'task', None)
work_order = getattr(task, 'work_order', None) if task else None
client_name = getattr(getattr(vehicle, 'client', None), '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 = str(getattr(work_order, 'pk', '-') or '-')
generated_date_str = timezone.localtime(timezone.now()).strftime('%d.%m.%Y')
HEADER_H = 86
FOOTER_H = 72
MARGIN = 28
def _fmt_date(value):
if not value:
return '-'
return value.strftime('%d.%m.%Y') if hasattr(value, 'strftime') else str(value)
buffer = BytesIO()
class PageCanvas(canvas.Canvas):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._current_page = 0
def showPage(self):
self._current_page += 1
self._draw_header_footer()
super().showPage()
def save(self):
self._current_page += 1
self._draw_header_footer()
super().save()
def _draw_header_footer(self):
page_num = self._current_page
draw_standard_header_footer(
self,
page_num=page_num,
client_name=client_name,
manufacturer=manufacturer_str,
model=model_str,
serial=serial_str,
upgrade_hours=upgrade_hours_str,
chassis_hours=chassis_hours_str,
mileage=mileage_str,
work_order_number=nalog_str,
generated_date=generated_date_str,
report_title="Izvještaj servisera",
page_size=A4,
margin=MARGIN,
header_h=HEADER_H,
footer_h=FOOTER_H,
)
pdf = PageCanvas(buffer, pagesize=A4)
page_w, page_h = A4
content_top = page_h - HEADER_H - 10
content_bottom = FOOTER_H + 52
content_width = page_w - 2 * MARGIN
def new_page():
pdf.showPage()
return content_top
def draw_table(y, data, col_widths, style, row_heights=None):
table = Table(data, colWidths=col_widths, rowHeights=row_heights)
table.setStyle(style)
_, h = table.wrap(content_width, page_h)
if y - h < content_bottom:
y = new_page()
table.drawOn(pdf, MARGIN, y - h)
return y - h - 10
paragraph_styles = getSampleStyleSheet()
description_style = paragraph_styles['BodyText'].clone('service-record-description')
description_style.fontName = 'Vera'
description_style.fontSize = 8.2
description_style.leading = 10.4
y = content_top - 6
y -= 6
info_rows = [
["ID", str(service_record.pk), "Datum", _fmt_date(service_record.service_date)],
["Naziv", service_record.service_title or '-', "Servisni zadatak", getattr(task, 'title', '-') or '-'],
["Dizalica", getattr(vehicle, 'registration_number', '-') or '-', "SN", getattr(vehicle, 'crane_serial_number', '-') or '-'],
["Serviser", _user_display_name(service_record.performed_by) or '-', "KM", str(service_record.mileage or '-')],
["Trošak", str(service_record.cost), "Sljedeći servis", str(service_record.next_service_due_at or '-')],
]
y = draw_table(
y,
info_rows,
[78, content_width * 0.37, 88, content_width - 166 - (content_width * 0.37)],
TableStyle([
('FONTNAME', (0, 0), (-1, -1), 'Vera'),
('FONTNAME', (0, 0), (0, -1), 'Vera-Bold'),
('FONTNAME', (2, 0), (2, -1), 'Vera-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 8.5),
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 4),
('RIGHTPADDING', (0, 0), (-1, -1), 4),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
]),
)
y = draw_table(
y,
[["Servisni opis"], [Paragraph(service_record.description or '-', description_style)]],
[content_width],
TableStyle([
('FONTNAME', (0, 0), (-1, 0), 'Vera-Bold'),
('FONTNAME', (0, 1), (-1, -1), 'Vera'),
('FONTSIZE', (0, 0), (-1, -1), 8.2),
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 4),
('RIGHTPADDING', (0, 0), (-1, -1), 4),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
]),
)
y = draw_table(
y,
[["Korišteni dijelovi"], [Paragraph(service_record.parts or '-', description_style)]],
[content_width],
TableStyle([
('FONTNAME', (0, 0), (-1, 0), 'Vera-Bold'),
('FONTNAME', (0, 1), (-1, -1), 'Vera'),
('FONTSIZE', (0, 0), (-1, -1), 8.2),
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 4),
('RIGHTPADDING', (0, 0), (-1, -1), 4),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
]),
)
attachments = list(service_record.attachments.filter(is_active=True).all())
attachment_names = [os.path.basename(item.file.name) for item in attachments if item.file]
y = draw_table(
y,
[["Prilozi"], [Paragraph(', '.join(attachment_names) if attachment_names else '-', description_style)]],
[content_width],
TableStyle([
('FONTNAME', (0, 0), (-1, 0), 'Vera-Bold'),
('FONTNAME', (0, 1), (-1, -1), 'Vera'),
('FONTSIZE', (0, 0), (-1, -1), 8.2),
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 4),
('RIGHTPADDING', (0, 0), (-1, -1), 4),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
]),
)
photos = [photo for photo in service_record.photos.filter(is_active=True).all() if photo.image]
if photos:
if y < content_bottom + 30:
y = new_page()
y -= 8
pdf.setFont("Vera", 8)
pdf.drawString(MARGIN, y, "Fotografije:")
y -= 10
image_gap = 10
max_image_height = 110
image_width = (content_width - image_gap) / 2
for index in range(0, len(photos), 2):
row_photos = photos[index:index + 2]
prepared = []
for photo in row_photos:
try:
image_reader = ImageReader(photo.image)
source_w, source_h = image_reader.getSize()
if not source_w or not source_h:
continue
scaled_h = min(max_image_height, image_width * (float(source_h) / float(source_w)))
prepared.append((photo, image_reader, scaled_h))
except (OSError, ValueError, TypeError, UnidentifiedImageError):
continue
if not prepared:
continue
caption_height = 12
row_height = max(item[2] for item in prepared) + caption_height
if y - row_height < content_bottom:
y = new_page()
base_y = y - caption_height
for photo_index, (photo, image_reader, scaled_h) in enumerate(prepared):
image_x = MARGIN + photo_index * (image_width + image_gap)
image_y = base_y - scaled_h
pdf.drawImage(
image_reader,
image_x,
image_y,
width=image_width,
height=scaled_h,
preserveAspectRatio=True,
anchor='c',
mask='auto',
)
pdf.setFont("Vera", 7)
caption = (photo.description or '').strip() or f"Slika {index + photo_index + 1}"
pdf.drawString(image_x, image_y - 10, caption[:52])
y = base_y - max(item[2] for item in prepared) - 14
else:
y = draw_table(
y,
[["Fotografije"], ["-"]],
[content_width],
TableStyle([
('FONTNAME', (0, 0), (-1, 0), 'Vera-Bold'),
('FONTNAME', (0, 1), (-1, -1), 'Vera'),
('FONTSIZE', (0, 0), (-1, -1), 8.2),
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 4),
('RIGHTPADDING', (0, 0), (-1, -1), 4),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
]),
)
pdf.save()
return buffer.getvalue()
def _first_non_empty(*values):
for value in values:
if isinstance(value, str) and value.strip():
@@ -1294,18 +1492,7 @@ class VehicleServiceRecordViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['get'], url_path='pdf')
def pdf(self, request, pk=None):
service_record = self.get_object()
rows = [
("ID", str(service_record.pk)),
("Datum", str(service_record.service_date)),
("Dizalica", service_record.vehicle.registration_number),
("Serviser", service_record.performed_by.get_full_name() or service_record.performed_by.email),
("Kilometraza", str(service_record.mileage)),
("Sljedeci servis", str(service_record.next_service_due_at or '-')),
("Trosak", str(service_record.cost)),
("Opis", service_record.description or '-'),
("Dijelovi", service_record.parts or '-'),
]
pdf_bytes = _build_simple_pdf(f"Servisni zapis {service_record.pk}", rows)
pdf_bytes = _build_service_record_pdf(service_record)
response = HttpResponse(pdf_bytes, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="{service_record.pk}.service-record.pdf"'
return response
@@ -1321,16 +1508,7 @@ class VehicleServiceRecordViewSet(viewsets.ModelViewSet):
if not recipient:
raise DRFValidationError({"recipient": "Nije pronađena email adresa za slanje."})
rows = [
("ID", str(service_record.pk)),
("Datum", str(service_record.service_date)),
("Dizalica", service_record.vehicle.registration_number),
("Kilometraza", str(service_record.mileage)),
("Trosak", str(service_record.cost)),
("Opis", service_record.description or '-'),
("Dijelovi", service_record.parts or '-'),
]
pdf_bytes = _build_simple_pdf(f"Servisni zapis {service_record.pk}", rows)
pdf_bytes = _build_service_record_pdf(service_record)
subject = _first_non_empty(request.data.get('subject')) or f"Servisni zapis {service_record.pk}"
body = _first_non_empty(request.data.get('message')) or (
f"U prilogu je PDF servisnog zapisa. Poslano {timezone.now().strftime('%d.%m.%Y %H:%M')}."