feat: dodaj dodatne troškove s OCR sinkronizacijom

Dodana je tablica DODATNI TROŠKOVI za putni nalog (backend model + API +
frontend modal) s izračunom ukupnog iznosa za isplatu.

Upload računa sada odmah upisuje redak u dodatne troškove, a asinkroni OCR
(Celery) naknadno pokušava popuniti broj računa i iznos iz slike/PDF-a.

Uvedena je invalidacija PDF cache-a nakon promjena računa/dodatnih troškova
kako bi generirani PDF uvijek prikazivao najnovije podatke.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-07-19 21:23:29 +02:00
parent d35eaeccf4
commit ae35a6041d
14 changed files with 836 additions and 19 deletions

View File

@@ -1,6 +1,7 @@
from rest_framework import serializers
import re
from pathlib import Path
from decimal import Decimal, InvalidOperation
from django.urls import reverse
from django.utils import timezone
from .models import (
@@ -8,6 +9,7 @@ from .models import (
Vehicle,
VehicleNotification,
WorkOrder,
WorkOrderAdditionalCostsTable,
WorkOrderPhoto,
WorkOrderInvoice,
VehicleServiceRecord,
@@ -268,6 +270,61 @@ class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
return value
class WorkOrderAdditionalCostsTableSerializer(serializers.ModelSerializer):
class Meta:
model = WorkOrderAdditionalCostsTable
fields = ['id', 'work_order', 'data', 'total_for_payout', 'created_at', 'updated_at']
read_only_fields = ['id', 'total_for_payout', 'created_at', 'updated_at']
@staticmethod
def _parse_decimal(value):
if value in (None, ''):
return Decimal('0.00')
try:
normalized = str(value).strip().replace('', '').replace(' ', '').replace(',', '.')
return Decimal(normalized)
except (InvalidOperation, ValueError, TypeError):
return Decimal('0.00')
def _normalize_row(self, row):
return {
'naziv': str(row.get('naziv', '') or '').strip(),
'broj_racuna': str(row.get('broj_racuna', '') or '').strip(),
'ukupan_iznos': str(row.get('ukupan_iznos', '') or '').strip(),
'prilog': str(row.get('prilog', '') or '').strip(),
'source_invoice_id': str(row.get('source_invoice_id', '') or '').strip(),
}
def validate_data(self, value):
if value is None:
return {'rows': []}
if not isinstance(value, dict):
raise serializers.ValidationError("Podaci tablice moraju biti JSON objekt.")
rows = value.get('rows', [])
if not isinstance(rows, list):
raise serializers.ValidationError({"rows": "Polje 'rows' mora biti lista redaka."})
normalized_rows = []
for row in rows[:100]:
if not isinstance(row, dict):
raise serializers.ValidationError({"rows": "Svaki redak mora biti JSON objekt."})
normalized_rows.append(self._normalize_row(row))
return {'rows': normalized_rows}
def _calculate_total_for_payout(self, data):
rows = data.get('rows', []) if isinstance(data, dict) else []
total = sum((self._parse_decimal(row.get('ukupan_iznos')) for row in rows), Decimal('0.00'))
return total.quantize(Decimal('0.01'))
def create(self, validated_data):
validated_data['total_for_payout'] = self._calculate_total_for_payout(validated_data.get('data'))
return super().create(validated_data)
def update(self, instance, validated_data):
data = validated_data.get('data', instance.data or {'rows': []})
validated_data['total_for_payout'] = self._calculate_total_for_payout(data)
return super().update(instance, validated_data)
class WorkOrderPhotoSerializer(serializers.ModelSerializer):
class Meta:
model = WorkOrderPhoto