This commit is contained in:
0
backend/modules/invoicing/__init__.py
Normal file
0
backend/modules/invoicing/__init__.py
Normal file
43
backend/modules/invoicing/admin.py
Normal file
43
backend/modules/invoicing/admin.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# backend/modules/invoicing/admin.py
|
||||
|
||||
from django.contrib import admin
|
||||
from .models import InvoiceTransaction
|
||||
|
||||
@admin.register(InvoiceTransaction)
|
||||
class InvoiceTransactionEntryAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Konfiguracija prikaza financijskih zapisa u Admin panelu.
|
||||
"""
|
||||
# Prikaz stupaca u listi
|
||||
list_display = ('invoice_number', 'work_order_ref', 'amount', 'created_at', 'is_active')
|
||||
|
||||
# Omogućuje brzo filtriranje
|
||||
list_filter = ('created_at', 'is_active', 'work_order')
|
||||
list_select_related = ('work_order__vehicle',)
|
||||
|
||||
# Omogućuje pretragu
|
||||
search_fields = ('invoice_number', 'work_order__id', 'work_order__vehicle__registration_number')
|
||||
|
||||
# Polja koja se ne smiju mijenjati u adminu
|
||||
readonly_fields = ('id', 'created_at', 'updated_at')
|
||||
|
||||
# Grupiranje polja za lakši pregled
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('invoice_number', 'work_order', 'amount', 'payment_date', 'is_active')
|
||||
}),
|
||||
('Sustavni podaci', {
|
||||
'fields': ('id', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
@admin.display(description="Povezani putni nalog")
|
||||
def work_order_ref(self, obj):
|
||||
if not obj.work_order_id:
|
||||
return "—"
|
||||
short_id = str(obj.work_order_id).split('-')[0].upper()
|
||||
registration = getattr(obj.work_order.vehicle, 'registration_number', '')
|
||||
if registration:
|
||||
return f"WO-{short_id} ({registration})"
|
||||
return f"WO-{short_id}"
|
||||
8
backend/modules/invoicing/apps.py
Normal file
8
backend/modules/invoicing/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# /backend/modules/invoicing/apps.py
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
class FleetConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'modules.invoicing'
|
||||
verbose_name = "Invoicing"
|
||||
30
backend/modules/invoicing/migrations/0001_initial.py
Normal file
30
backend/modules/invoicing/migrations/0001_initial.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-29 13:34
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FinancialEntry',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unikatni identifikator entiteta (UUID).', primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Vrijeme kreiranja')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Vrijeme zadnje izmjene')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Aktivan zapis')),
|
||||
('invoice_number', models.CharField(max_length=50)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Financijski zapis',
|
||||
'verbose_name_plural': 'Financijski zapisi',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-30 05:55
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('crm', '0001_initial'),
|
||||
('invoicing', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Invoice',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unikatni identifikator entiteta (UUID).', primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Vrijeme kreiranja')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Vrijeme zadnje izmjene')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Aktivan zapis')),
|
||||
('invoice_number', models.CharField(max_length=50, unique=True, verbose_name='Broj fakture')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Iznos')),
|
||||
('currency', models.CharField(default='EUR', max_length=3)),
|
||||
('tax_rate', models.DecimalField(decimal_places=2, default=25.0, max_digits=4)),
|
||||
('status', models.CharField(choices=[('draft', 'Nacrt'), ('sent', 'Poslano'), ('paid', 'Plaćeno'), ('cancelled', 'Stornirano')], default='draft', max_length=20)),
|
||||
('due_date', models.DateField(verbose_name='Rok plaćanja')),
|
||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='crm.client', verbose_name='Klijent')),
|
||||
('creator', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='invoices', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Faktura',
|
||||
'verbose_name_plural': 'Fakture',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceItem',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unikatni identifikator entiteta (UUID).', primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Vrijeme kreiranja')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Vrijeme zadnje izmjene')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Aktivan zapis')),
|
||||
('description', models.CharField(max_length=255, verbose_name='Opis stavke')),
|
||||
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Količina')),
|
||||
('unit_price', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Cijena po jedinici')),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='invoicing.invoice', verbose_name='Faktura')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Stavka fakture',
|
||||
'verbose_name_plural': 'Stavke fakture',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-30 17:54
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('invoicing', '0002_invoice_invoiceitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InvoiceTransaction',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unikatni identifikator entiteta (UUID).', primary_key=True, serialize=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Vrijeme kreiranja')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Vrijeme zadnje izmjene')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Aktivan zapis')),
|
||||
('invoice_number', models.CharField(max_length=50)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transaction', to='invoicing.invoice', verbose_name='Transaction')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Financijski zapis',
|
||||
'verbose_name_plural': 'Financijski zapisi',
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='FinancialEntry',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-30 18:12
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('invoicing', '0003_invoicetransaction_delete_financialentry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoicetransaction',
|
||||
name='payment_date',
|
||||
field=models.DateField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoicetransaction',
|
||||
name='invoice',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to='invoicing.invoice', verbose_name='Faktura'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-30 18:13
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('invoicing', '0004_invoicetransaction_payment_date_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='invoicetransaction',
|
||||
name='payment_date',
|
||||
field=models.DateField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fleet', '0012_workorder_purpose_choices'),
|
||||
('invoicing', '0005_alter_invoicetransaction_payment_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoicetransaction',
|
||||
name='work_order',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='invoice_transactions',
|
||||
to='fleet.workorder',
|
||||
verbose_name='Putni nalog',
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
0
backend/modules/invoicing/migrations/__init__.py
Normal file
0
backend/modules/invoicing/migrations/__init__.py
Normal file
123
backend/modules/invoicing/models.py
Normal file
123
backend/modules/invoicing/models.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# backend/modules/invoicing/models.py
|
||||
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from core.base_models import BaseModel
|
||||
from django.db import models, transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from .utils import generate_invoice_number
|
||||
|
||||
class Invoice(BaseModel):
|
||||
# Identifikacija (UUID i created_at dolaze iz BaseModel)
|
||||
invoice_number = models.CharField(max_length=50, unique=True, verbose_name=_("Broj fakture"))
|
||||
|
||||
# Relacije
|
||||
creator = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="invoices"
|
||||
)
|
||||
client = models.ForeignKey(
|
||||
'crm.Client',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("Klijent")
|
||||
)
|
||||
|
||||
# Financijski detalji
|
||||
amount = models.DecimalField(max_digits=12, decimal_places=2, verbose_name=_("Iznos"))
|
||||
currency = models.CharField(max_length=3, default="EUR")
|
||||
tax_rate = models.DecimalField(max_digits=4, decimal_places=2, default=25.00)
|
||||
|
||||
# Statusi
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('draft', 'Nacrt'),
|
||||
('sent', 'Poslano'),
|
||||
('paid', 'Plaćeno'),
|
||||
('cancelled', 'Stornirano')
|
||||
],
|
||||
default='draft'
|
||||
)
|
||||
|
||||
due_date = models.DateField(verbose_name=_("Rok plaćanja"))
|
||||
|
||||
class Meta:
|
||||
# Nasljeđujemo ordering iz BaseModel, ali ga možemo override-ati
|
||||
verbose_name = _("Faktura")
|
||||
verbose_name_plural = _("Fakture")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.invoice_number} - {self.client}"
|
||||
|
||||
def get_total_amount(self):
|
||||
return self.items.aggregate(total=models.Sum(models.F('quantity') * models.F('unit_price')))['total'] or 0
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.invoice_number:
|
||||
self.invoice_number = generate_invoice_number()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class InvoiceItem(BaseModel):
|
||||
# Relacija prema fakturi
|
||||
invoice = models.ForeignKey(
|
||||
Invoice,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='items',
|
||||
verbose_name=_("Faktura")
|
||||
)
|
||||
|
||||
# Detalji stavke
|
||||
description = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Opis stavke")
|
||||
)
|
||||
quantity = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
default=1,
|
||||
verbose_name=_("Količina")
|
||||
)
|
||||
unit_price = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Cijena po jedinici")
|
||||
)
|
||||
|
||||
# Automatski izračun (property ne zauzima prostor u bazi)
|
||||
@property
|
||||
def total(self):
|
||||
return self.quantity * self.unit_price
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Stavka fakture")
|
||||
verbose_name_plural = _("Stavke fakture")
|
||||
|
||||
def __str__(self):
|
||||
# Koristimo prednost BaseModel __str__ metode
|
||||
return f"{self.description} ({self.quantity} x {self.unit_price})"
|
||||
|
||||
class InvoiceTransaction(BaseModel):
|
||||
# Polja specifična za fakturiranje
|
||||
invoice = models.ForeignKey(
|
||||
Invoice,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='transactions',
|
||||
verbose_name=_("Faktura")
|
||||
)
|
||||
work_order = models.ForeignKey(
|
||||
'fleet.WorkOrder',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='invoice_transactions',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Putni nalog"),
|
||||
)
|
||||
invoice_number = models.CharField(max_length=50)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
payment_date = models.DateField(default=timezone.now)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Financijski zapis"
|
||||
verbose_name_plural = "Financijski zapisi"
|
||||
57
backend/modules/invoicing/serializers.py
Normal file
57
backend/modules/invoicing/serializers.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from django.db import transaction
|
||||
from core.serializers import BaseSerializer
|
||||
from rest_framework import serializers
|
||||
from .models import Invoice, InvoiceItem, InvoiceTransaction
|
||||
from .services import InvoiceService #
|
||||
|
||||
class InvoiceItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = InvoiceItem
|
||||
fields = ['description', 'quantity', 'unit_price']
|
||||
|
||||
class InvoiceSerializer(serializers.ModelSerializer):
|
||||
items = InvoiceItemSerializer(many=True) # Nested lista stavki
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['invoice_number', 'client', 'due_date', 'status', 'items', 'amount']
|
||||
read_only_fields = ['invoice_number', 'creator', 'status', 'amount']
|
||||
extra_kwargs = {
|
||||
'invoice_number': {'required': False, 'allow_null': True},
|
||||
'creator': {'required': False, 'allow_null': True}
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Delegiramo stvaranje servisu i vraćamo instancu Invoice.
|
||||
"""
|
||||
items = validated_data.pop('items', [])
|
||||
# Servisu šaljemo items u validated_data kao što servis očekuje
|
||||
validated_data['items'] = items
|
||||
|
||||
# Pokušavamo dohvatiti korisnika iz contexta (ako postoji)
|
||||
request = self.context.get('request')
|
||||
user = getattr(request, 'user', None)
|
||||
|
||||
# Pozivamo servis unutar transaction.atomic (servis već radi atomic, ali dodatno osiguranje nije štetno)
|
||||
invoice = InvoiceService.create_full_invoice(user=user, **validated_data)
|
||||
return invoice
|
||||
|
||||
class InvoiceTransactionSerializer(BaseSerializer):
|
||||
invoice = serializers.PrimaryKeyRelatedField(queryset=Invoice.objects.all())
|
||||
|
||||
def validate_amount(self, value):
|
||||
request = self.context.get('request')
|
||||
# Ako request ne postoji (npr. unit test), tretiramo ga kao običnog korisnika (sigurnost prvo)
|
||||
user = getattr(request, 'user', None)
|
||||
|
||||
# Ako user ne postoji ili nije staff, i iznos je <= 0 -> greška
|
||||
if value <= 0 and (not user or not user.is_staff):
|
||||
raise serializers.ValidationError("Iznos računa mora biti veći od 0.")
|
||||
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = InvoiceTransaction
|
||||
fields = ['invoice', 'work_order', 'invoice_number', 'amount', 'payment_date', 'is_active']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
77
backend/modules/invoicing/services.py
Normal file
77
backend/modules/invoicing/services.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from django.utils import timezone
|
||||
from modules.invoicing.models import Invoice, InvoiceItem
|
||||
from modules.invoicing.utils import generate_invoice_number
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.exceptions import ValidationError
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from modules.invoicing import tasks # premještamo ovdje ako treba
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class InvoiceService:
|
||||
@staticmethod
|
||||
def create_full_invoice(user=None, **validated_data):
|
||||
# 1. Obavezno dohvati stavke
|
||||
items_data = validated_data.pop('items', [])
|
||||
|
||||
fields_to_remove = ['amount', 'creator', 'invoice_number']
|
||||
for field in fields_to_remove:
|
||||
validated_data.pop(field, None)
|
||||
|
||||
# 2. Sigurna provjera korisnika
|
||||
if not user:
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(username="paperless_bot")
|
||||
except User.DoesNotExist:
|
||||
raise ValidationError("Sistemski korisnik 'paperless_bot' ne postoji.")
|
||||
|
||||
with transaction.atomic():
|
||||
# 3. Računanje iznosa (sigurno rukovanje s items_data)
|
||||
total = sum(Decimal(item['quantity']) * Decimal(item['unit_price']) for item in items_data)
|
||||
|
||||
# 4. Kreiraj fakturu
|
||||
invoice = Invoice.objects.create(
|
||||
**validated_data,
|
||||
amount=total,
|
||||
creator=user,
|
||||
invoice_number=generate_invoice_number()
|
||||
)
|
||||
|
||||
# 5. Kreiraj stavke
|
||||
for item_data in items_data:
|
||||
InvoiceItem.objects.create(invoice=invoice, **item_data)
|
||||
|
||||
# 6. Pokreni task nakon commit-a
|
||||
transaction.on_commit(lambda: tasks.process_paperless_document_task.delay(invoice.id))
|
||||
|
||||
return invoice
|
||||
|
||||
@staticmethod
|
||||
def create_invoice_entry(data, user=None):
|
||||
"""
|
||||
Kreira financijski zapis (transaction) na temelju validated data.
|
||||
Očekuje se da data['invoice'] može biti instanca ili PK.
|
||||
"""
|
||||
from modules.invoicing.models import InvoiceTransaction, Invoice
|
||||
|
||||
invoice = data.get("invoice")
|
||||
if isinstance(invoice, int):
|
||||
invoice = Invoice.objects.get(pk=invoice)
|
||||
elif hasattr(invoice, "pk") is False:
|
||||
raise ValidationError("Nevažeći invoice.")
|
||||
|
||||
amount = data.get("amount")
|
||||
payment_date = data.get("payment_date", None)
|
||||
work_order = data.get("work_order")
|
||||
|
||||
tx = InvoiceTransaction.objects.create(
|
||||
invoice=invoice,
|
||||
work_order=work_order,
|
||||
invoice_number=invoice.invoice_number,
|
||||
amount=amount,
|
||||
payment_date=payment_date or timezone.now()
|
||||
)
|
||||
return tx
|
||||
81
backend/modules/invoicing/tasks.py
Normal file
81
backend/modules/invoicing/tasks.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import uuid
|
||||
from celery import shared_task
|
||||
from datetime import date, timedelta
|
||||
from infrastructure.paperless_client import PaperlessGateway
|
||||
from core.utils import format_currency, parse_iso_date
|
||||
from django.contrib.auth import get_user_model
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from modules.crm.models import Client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def process_paperless_document_task(self, document_id):
|
||||
"""
|
||||
Celery zadatak koji povezuje infrastrukturu i poslovnu logiku.
|
||||
"""
|
||||
gateway = PaperlessGateway() # rename so we don't overwrite 'client'
|
||||
|
||||
try:
|
||||
User = get_user_model()
|
||||
# 1. Dohvat iz infrastrukture
|
||||
raw_data = gateway.get_document_metadata(document_id)
|
||||
raw_created = parse_iso_date(raw_data.get("created"))
|
||||
correspondent_name = raw_data.get("correspondent")
|
||||
|
||||
# pokušaj dohvatiti crm klijenta
|
||||
try:
|
||||
crm_client = Client.objects.get(name=correspondent_name)
|
||||
except Client.DoesNotExist:
|
||||
logger.error(f"Klijent {correspondent_name} nije pronađen!")
|
||||
# Kreiramo fallback klijenta ali popunimo UNIQUE polja jedinstvenim vrijednostima
|
||||
unique_email = f"client+{uuid.uuid4()}@example.test"
|
||||
unique_tax_id = str(uuid.uuid4())
|
||||
crm_client, created = Client.objects.get_or_create(
|
||||
name=correspondent_name,
|
||||
defaults={
|
||||
"email": unique_email,
|
||||
"tax_id": unique_tax_id,
|
||||
}
|
||||
)
|
||||
if created:
|
||||
logger.info(f"Fallback klijent kreiran za '{correspondent_name}' (id={crm_client.id}).")
|
||||
|
||||
# 2. Transformacija i čišćenje podataka
|
||||
total_raw = raw_data.get("total_amount", "0")
|
||||
try:
|
||||
amount_decimal = Decimal(str(total_raw))
|
||||
except Exception:
|
||||
amount_decimal = Decimal("0")
|
||||
|
||||
processed_data = {
|
||||
"client": crm_client,
|
||||
"invoice_number": raw_data.get("invoice_number") or raw_data.get("correspondent"),
|
||||
"due_date": (raw_created or date.today()) + timedelta(days=30),
|
||||
"amount": amount_decimal,
|
||||
"created_at": raw_created,
|
||||
}
|
||||
|
||||
from modules.invoicing.services import InvoiceService
|
||||
|
||||
lookup_field = User.USERNAME_FIELD
|
||||
system_user = User.objects.filter(**{lookup_field: "bot@erp.hr"}).first()
|
||||
|
||||
if not system_user:
|
||||
system_user = User(**{lookup_field: "bot@erp.hr"})
|
||||
system_user.email = "bot@erp.hr"
|
||||
system_user.first_name = "Paperless"
|
||||
system_user.last_name = "Bot"
|
||||
system_user.set_unusable_password()
|
||||
system_user.save()
|
||||
|
||||
# Pozovi servis uvijek nakon što garantujemo da system_user postoji
|
||||
InvoiceService.create_full_invoice(user=system_user, **processed_data)
|
||||
|
||||
logger.info(f"Dokument {document_id} uspješno obrađen.")
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Greška pri obradi dokumenta {document_id}: {exc}")
|
||||
# Automatski retry ako padne
|
||||
raise self.retry(exc=exc, countdown=60)
|
||||
112
backend/modules/invoicing/tests/conftest.py
Normal file
112
backend/modules/invoicing/tests/conftest.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# backend/modules/invoicing/tests/conftest.py
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.invoicing.models import Invoice
|
||||
from unittest.mock import MagicMock
|
||||
from django.conf import settings
|
||||
from core.celery import app as celery_app
|
||||
import modules.invoicing.tasks as tasks
|
||||
import uuid
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
# Dodan username
|
||||
return User.objects.create_user(email="test@test.hr", password="password")
|
||||
|
||||
@pytest.fixture
|
||||
def api_client(user):
|
||||
from rest_framework.test import APIClient
|
||||
client = APIClient()
|
||||
client.force_authenticate(user=user)
|
||||
return client
|
||||
|
||||
@pytest.fixture
|
||||
def staff_user():
|
||||
return User.objects.create_superuser(email='admin@erp.hr', password='password', first_name='Admin', last_name='User')
|
||||
|
||||
@pytest.fixture
|
||||
def regular_user():
|
||||
return User.objects.create_user(email='user@erp.hr', password='password', first_name='User', last_name='User')
|
||||
|
||||
@pytest.fixture
|
||||
def serviser_a():
|
||||
return User.objects.create_user(email="a@test.hr", password="password", first_name='A', last_name='User')
|
||||
|
||||
@pytest.fixture
|
||||
def serviser_b():
|
||||
return User.objects.create_user(email="b@test.hr", password="password", first_name='B', last_name='User')
|
||||
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
from modules.crm.models import Client
|
||||
return Client.objects.create(name="Testni Klijent", tax_id="12345678901")
|
||||
|
||||
@pytest.fixture
|
||||
def invoice_a(serviser_a, test_client): # Dodali smo test_client
|
||||
from modules.invoicing.utils import generate_invoice_number
|
||||
return Invoice.objects.create(
|
||||
invoice_number=generate_invoice_number(),
|
||||
creator=serviser_a,
|
||||
client=test_client, # OVDJE JE BIO PROBLEM (bio je null)
|
||||
amount=100.00,
|
||||
due_date="2026-12-31"
|
||||
)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def paperless_settings(settings):
|
||||
# Postavi privremene vrijednosti za vrijeme trajanja testa
|
||||
settings.PAPERLESS_API_URL = "http://mock-paperless.local"
|
||||
settings.PAPERLESS_API_TOKEN = "mock-token"
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def force_celery_eager(settings):
|
||||
settings.CELERY_TASK_ALWAYS_EAGER = True
|
||||
settings.CELERY_TASK_EAGER_PROPAGATES = True
|
||||
|
||||
# Ovo je ključ: prisilno ažuriraj konfiguraciju Celery aplikacije
|
||||
celery_app.conf.update(
|
||||
task_always_eager=True,
|
||||
task_eager_propagates=True,
|
||||
)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_paperless_gateway(monkeypatch):
|
||||
"""
|
||||
Koristi monkeypatch za zamjenu PaperlessGateway klase.
|
||||
"""
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_document_metadata.return_value = {
|
||||
"correspondent": "INV-2026-TEST",
|
||||
"total_amount": "100.00",
|
||||
"created": "2026-07-01"
|
||||
}
|
||||
|
||||
# Kreiramo tvornicu koja vraća naš mock
|
||||
MockGateway = MagicMock(return_value=mock_client)
|
||||
|
||||
# Zamijeni klasu unutar tasks modula
|
||||
monkeypatch.setattr(tasks, "PaperlessGateway", MockGateway)
|
||||
|
||||
yield MockGateway
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(db):
|
||||
"""Kreira klijenta kojeg koristi PaperlessGateway u testovima."""
|
||||
from modules.crm.models import Client
|
||||
return Client.objects.create(name="INV-2026-TEST") # Ime mora odgovarati mocku iz conftest.py
|
||||
|
||||
@pytest.fixture
|
||||
def create_unique_client(db):
|
||||
"""Factory funkcija za kreiranje unikatnog klijenta."""
|
||||
from modules.crm.models import Client
|
||||
def _create_client(name):
|
||||
return Client.objects.create(
|
||||
name=name,
|
||||
tax_id=str(uuid.uuid4()), # Garantira unikatnost
|
||||
email=f"{uuid.uuid4()}@test.hr" # Garantira unikatnost
|
||||
)
|
||||
return _create_client
|
||||
|
||||
46
backend/modules/invoicing/tests/test_api.py
Normal file
46
backend/modules/invoicing/tests/test_api.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from django.contrib.auth import get_user_model
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
|
||||
from modules.crm.models import Client
|
||||
from modules.invoicing.models import Invoice
|
||||
|
||||
|
||||
class InvoiceAPITests(TestCase):
|
||||
def setUp(self):
|
||||
self.api_client = APIClient()
|
||||
User = get_user_model()
|
||||
# Kreiraj test korisnika (ako treba autentikacija)
|
||||
self.user = User.objects.create_user(
|
||||
username=f"user_{uuid.uuid4().hex[:8]}",
|
||||
email=f"user+{uuid.uuid4().hex[:8]}@example.test",
|
||||
password="pass"
|
||||
)
|
||||
# Kreiraj jedinstvenog klijenta (email s uuid kako bismo izbjegli duplicate key)
|
||||
self.client_obj = Client.objects.create(
|
||||
name="API Test Client",
|
||||
email=f"client+{uuid.uuid4()}@example.test"
|
||||
)
|
||||
# autentikacija ako je endpoint zaštićen
|
||||
self.api_client.force_authenticate(user=self.user)
|
||||
|
||||
def test_api_creates_invoice_with_id(self):
|
||||
url = reverse('invoice-list') # prilagodi ako je druga ruta
|
||||
payload = {
|
||||
"client": self.client_obj.id,
|
||||
"due_date": date.today().isoformat(),
|
||||
"items": [
|
||||
{"description": "Test item", "quantity": "1.00", "unit_price": "10.00"}
|
||||
]
|
||||
}
|
||||
|
||||
response = self.api_client.post(url, payload, format='json')
|
||||
assert response.status_code in (200, 201)
|
||||
# provjeri da se faktura stvorila i da vraća id/kod
|
||||
created = Invoice.objects.filter(client=self.client_obj).first()
|
||||
assert created is not None
|
||||
assert created.amount == Decimal("10.00")
|
||||
18
backend/modules/invoicing/tests/test_models.py
Normal file
18
backend/modules/invoicing/tests/test_models.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
from modules.invoicing.models import Invoice, InvoiceItem
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invoice_str_representation(invoice_a):
|
||||
assert str(invoice_a) == f"{invoice_a.invoice_number} - {invoice_a.client}"
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invoice_total_calculation(invoice_a):
|
||||
InvoiceItem.objects.create(invoice=invoice_a, description="Test", quantity=2, unit_price=50.00)
|
||||
InvoiceItem.objects.create(invoice=invoice_a, description="Test2", quantity=1, unit_price=25.00)
|
||||
assert invoice_a.get_total_amount() == 125.00
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invoice_number_autogenerate():
|
||||
# Testiramo da li se automatski generira ako nije zadan
|
||||
from modules.invoicing.models import Invoice
|
||||
# (Za ovo ti treba 'creator' i 'client' fixture-i)
|
||||
10
backend/modules/invoicing/tests/test_permissions.py
Normal file
10
backend/modules/invoicing/tests/test_permissions.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invoice_access_restriction(api_client, regular_user, invoice_a):
|
||||
# invoice_a pripada serviseru_a, a api_client je regular_user
|
||||
url = f'/api/invoicing/invoices/{invoice_a.id}/'
|
||||
response = api_client.get(url)
|
||||
# Trebao bi dobiti 404 ili 403 ovisno o tvojoj logici
|
||||
assert response.status_code in [status.HTTP_404_NOT_FOUND, status.HTTP_403_FORBIDDEN]
|
||||
22
backend/modules/invoicing/tests/test_serializers.py
Normal file
22
backend/modules/invoicing/tests/test_serializers.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
from modules.invoicing.serializers import InvoiceSerializer
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invoice_serializer_valid(regular_user, test_client):
|
||||
data = {
|
||||
"client": test_client.id,
|
||||
"due_date": "2026-12-31",
|
||||
"status": "draft",
|
||||
"items": [
|
||||
{"description": "Servis", "quantity": 1, "unit_price": 100.00}
|
||||
]
|
||||
}
|
||||
# Context je ključan za pristup useru
|
||||
request = MagicMock()
|
||||
request.user = regular_user
|
||||
serializer = InvoiceSerializer(data=data, context={'request': request})
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
invoice = serializer.save()
|
||||
assert invoice.amount == 100.00
|
||||
40
backend/modules/invoicing/tests/test_services.py
Normal file
40
backend/modules/invoicing/tests/test_services.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from modules.crm.models import Client
|
||||
from modules.invoicing.models import Invoice, InvoiceItem
|
||||
from modules.invoicing.services import InvoiceService
|
||||
|
||||
|
||||
class InvoiceServiceTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.creator = User.objects.create_user(username="creator", email="c@example.com", password="pass")
|
||||
self.client = Client.objects.create(name="Test Client")
|
||||
|
||||
def test_create_full_invoice_creates_invoice_and_items(self):
|
||||
items = [
|
||||
{"description": "Item A", "quantity": Decimal("2.00"), "unit_price": Decimal("10.00")},
|
||||
{"description": "Item B", "quantity": Decimal("1.00"), "unit_price": Decimal("5.50")},
|
||||
]
|
||||
validated_data = {
|
||||
"client": self.client,
|
||||
"due_date": date.today(),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
invoice = InvoiceService.create_full_invoice(user=self.creator, **validated_data)
|
||||
|
||||
# Basic assertions
|
||||
self.assertIsInstance(invoice, Invoice)
|
||||
self.assertEqual(invoice.creator, self.creator)
|
||||
self.assertEqual(invoice.client, self.client)
|
||||
self.assertEqual(invoice.items.count(), 2)
|
||||
|
||||
# Total should be 2*10 + 1*5.5 = 25.5
|
||||
self.assertEqual(invoice.amount, Decimal("25.50"))
|
||||
item_descriptions = [i.description for i in invoice.items.all()]
|
||||
self.assertIn("Item A", item_descriptions)
|
||||
self.assertIn("Item B", item_descriptions)
|
||||
40
backend/modules/invoicing/tests/test_tasks.py
Normal file
40
backend/modules/invoicing/tests/test_tasks.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch
|
||||
from datetime import datetime
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from modules.crm.models import Client
|
||||
from modules.invoicing import tasks
|
||||
|
||||
|
||||
class TasksTests(TestCase):
|
||||
def setUp(self):
|
||||
# Kreiramo klijenta koji vraća PaperlessGateway; email jedinstven radi izbjegavanja duplikata
|
||||
self.client = Client.objects.create(
|
||||
name="Paperless Corp",
|
||||
email=f"paperless+{uuid.uuid4()}@example.test"
|
||||
)
|
||||
User = get_user_model()
|
||||
# Kreiramo sistemskog korisnika koji već postoji (trebao bi se koristiti)
|
||||
self.system_user = User.objects.create_user(
|
||||
username="bot@erp.hr",
|
||||
email="bot@erp.hr",
|
||||
password="pass"
|
||||
)
|
||||
|
||||
@patch("modules.invoicing.tasks.PaperlessGateway.get_document_metadata")
|
||||
@patch("modules.invoicing.tasks.InvoiceService.create_full_invoice")
|
||||
def test_process_paperless_document_calls_service(self, mock_create_full_invoice, mock_get_metadata):
|
||||
# simuliramo odgovor infrastrukture
|
||||
mock_get_metadata.return_value = {
|
||||
"created": "2026-01-01T00:00:00Z",
|
||||
"correspondent": "Paperless Corp",
|
||||
"total_amount": "100.00"
|
||||
}
|
||||
|
||||
# Pozovemo task sinkrono pomoću .apply (ispravno mapira argumente)
|
||||
tasks.process_paperless_document_task.apply(args=(123,))
|
||||
|
||||
# Očekujemo da je pozvan servis za izradu fakture
|
||||
mock_create_full_invoice.assert_called()
|
||||
28
backend/modules/invoicing/tests/test_utils.py
Normal file
28
backend/modules/invoicing/tests/test_utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django.test import TestCase
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.invoicing.models import Invoice
|
||||
from modules.invoicing.utils import generate_invoice_number
|
||||
from modules.crm.models import Client
|
||||
|
||||
|
||||
class UtilsTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username="creator", email="c@example.com", password="pass")
|
||||
self.client = Client.objects.create(name="Client Utils")
|
||||
|
||||
def test_generate_invoice_number_increments(self):
|
||||
year = date.today().year
|
||||
Invoice.objects.create(
|
||||
invoice_number=f"INV-{year}-0001",
|
||||
creator=self.user,
|
||||
client=self.client,
|
||||
amount=Decimal("1.00"),
|
||||
due_date=date.today()
|
||||
)
|
||||
next_num = generate_invoice_number()
|
||||
# očekujemo INV-<year>-0002
|
||||
self.assertTrue(next_num.endswith("-0002"))
|
||||
46
backend/modules/invoicing/tests/test_views.py
Normal file
46
backend/modules/invoicing/tests/test_views.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.contrib.auth import get_user_model
|
||||
from unittest.mock import patch
|
||||
|
||||
from modules.invoicing.views import InvoiceTransactionEntryViewSet
|
||||
from modules.invoicing.serializers import InvoiceTransactionSerializer
|
||||
from modules.invoicing.models import InvoiceTransaction
|
||||
from modules.invoicing.models import Invoice
|
||||
from modules.crm.models import Client
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class ViewsTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username="u1", email="u1@example.com", password="pass")
|
||||
self.client = Client.objects.create(name="Client Views")
|
||||
self.invoice = Invoice.objects.create(
|
||||
invoice_number="INV-TEST-0001",
|
||||
creator=self.user,
|
||||
client=self.client,
|
||||
amount=Decimal("10.00"),
|
||||
due_date=date.today()
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
self.viewset = InvoiceTransactionEntryViewSet()
|
||||
|
||||
@patch("modules.invoicing.services.InvoiceService.create_invoice_entry")
|
||||
def test_perform_create_calls_service(self, mock_create_invoice_entry):
|
||||
# simuliramo serializer.validated_data i poziv perform_create
|
||||
serializer = InvoiceTransactionSerializer(data={
|
||||
"invoice": self.invoice.id,
|
||||
"invoice_number": self.invoice.invoice_number,
|
||||
"amount": "5.00",
|
||||
"is_active": True
|
||||
}, context={"request": self.factory.get("/")})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Poziv perform_create koji bi trebao delegirati na servis
|
||||
self.viewset.request = self.factory.post("/")
|
||||
self.viewset.request.user = self.user
|
||||
|
||||
# Ako kod poziva InvoiceService.create_invoice_entry, mock se aktivira.
|
||||
self.viewset.perform_create(serializer)
|
||||
mock_create_invoice_entry.assert_called()
|
||||
8
backend/modules/invoicing/urls.py
Normal file
8
backend/modules/invoicing/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import InvoiceViewSet, InvoiceTransactionEntryViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'invoices', InvoiceViewSet, basename='invoice')
|
||||
router.register(r'transaction', InvoiceTransactionEntryViewSet, basename='financial-transaction-entry')
|
||||
|
||||
urlpatterns = router.urls
|
||||
26
backend/modules/invoicing/utils.py
Normal file
26
backend/modules/invoicing/utils.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import datetime
|
||||
from django.apps import apps
|
||||
from django.db import transaction
|
||||
|
||||
def generate_invoice_number():
|
||||
"""
|
||||
Generira novi broj fakture u formatu INV-YYYY-XXXX.
|
||||
Koristi transakciju kako bi se spriječilo dupliciranje brojeva.
|
||||
"""
|
||||
with transaction.atomic():
|
||||
Invoice = apps.get_model('invoicing', 'Invoice')
|
||||
|
||||
current_year = datetime.date.today().year
|
||||
|
||||
# Zaključavamo tablicu za čitanje kako bismo izbjegli Race Condition
|
||||
last_invoice = Invoice.objects.filter(
|
||||
invoice_number__startswith=f"INV-{current_year}-"
|
||||
).select_for_update().order_by('-invoice_number').first()
|
||||
|
||||
if last_invoice:
|
||||
last_num = int(last_invoice.invoice_number.split('-')[-1])
|
||||
new_num = last_num + 1
|
||||
else:
|
||||
new_num = 1
|
||||
|
||||
return f"INV-{current_year}-{new_num:04d}"
|
||||
75
backend/modules/invoicing/views.py
Normal file
75
backend/modules/invoicing/views.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# backend/modules/invoicing/views.py
|
||||
|
||||
import logging
|
||||
from rest_framework import viewsets, permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from .models import InvoiceTransaction, Invoice
|
||||
from .serializers import InvoiceSerializer, InvoiceTransactionSerializer
|
||||
from .services import InvoiceService
|
||||
|
||||
# Postavljanje loggera za modul
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class InvoiceViewSet(viewsets.ModelViewSet):
|
||||
queryset = Invoice.objects.all().select_related('client', 'creator')
|
||||
serializer_class = InvoiceSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
validated_data = serializer.validated_data
|
||||
|
||||
# 2. Delegiraj SVE servisu
|
||||
from .services import InvoiceService
|
||||
invoice_instance = InvoiceService.create_full_invoice(
|
||||
user=self.request.user,
|
||||
**validated_data
|
||||
)
|
||||
|
||||
serializer.instance = invoice_instance
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Filtriraj samo aktivne zapise
|
||||
"""
|
||||
# Korisnik vidi samo fakture koje je on kreirao
|
||||
# (ili koje su povezane s njegovim profilom, ako si klijent)
|
||||
if self.request.user.is_staff:
|
||||
return Invoice.objects.all()
|
||||
return Invoice.objects.filter(creator=self.request.user, is_active=True)
|
||||
|
||||
class InvoiceTransactionEntryViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet za upravljanje financijskim zapisima.
|
||||
Korištenjem InvoiceService-a, ViewSet ostaje lagan i fokusiran samo na HTTP sloj.
|
||||
"""
|
||||
queryset = InvoiceTransaction.objects.filter(is_active=True).select_related('invoice', 'work_order').order_by('-created_at')
|
||||
serializer_class = InvoiceTransactionSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Presrećemo proces kreiranja kako bismo koristili InvoiceService.
|
||||
"""
|
||||
try:
|
||||
# Proslijeđujemo validirane podatke i korisnika u servis
|
||||
InvoiceService.create_invoice_entry(
|
||||
data=serializer.validated_data,
|
||||
user=self.request.user
|
||||
)
|
||||
except ValidationError as e:
|
||||
# Ponovno podižemo iznimku kako bi DRF ispravno vratio 400 Bad Request
|
||||
logger.warning(f"Validacijska greška pri kreiranju računa od strane {self.request.user}: {e}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Kritična greška pri kreiranju financijskog zapisa: {str(e)}", exc_info=True)
|
||||
raise ValidationError({"error": "Došlo je do greške prilikom spremanja zapisa."})
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""
|
||||
Umjesto fizičkog brisanja, radimo soft-delete ako je potrebno.
|
||||
"""
|
||||
instance.is_active = False
|
||||
instance.save()
|
||||
logger.info(f"Financijski zapis {instance.id} je deaktiviran od strane {self.request.user}")
|
||||
Reference in New Issue
Block a user