This commit is contained in:
0
backend/modules/crm/__init__.py
Normal file
0
backend/modules/crm/__init__.py
Normal file
86
backend/modules/crm/admin.py
Normal file
86
backend/modules/crm/admin.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from modules.fleet.models import Crane
|
||||
from .models import Client
|
||||
|
||||
|
||||
class ClientAdminForm(forms.ModelForm):
|
||||
customer_cranes = forms.ModelMultipleChoiceField(
|
||||
label=_("Vozila kupca"),
|
||||
queryset=Crane.objects.filter(is_active=True, asset_type='crane').order_by('registration_number'),
|
||||
required=False,
|
||||
widget=FilteredSelectMultiple(_("Dizalice"), is_stacked=False),
|
||||
help_text=_("Odaberite postojeće dizalice koje pripadaju kupcu."),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = '__all__'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.pk:
|
||||
self.fields['customer_cranes'].initial = Crane.objects.filter(client=self.instance, is_active=True, asset_type='crane')
|
||||
|
||||
|
||||
@admin.register(Client)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
form = ClientAdminForm
|
||||
# Polja koja se prikazuju u listi
|
||||
list_display = ('name', 'tax_id', 'city', 'client_type', 'vehicle_count', 'is_active', 'created_at')
|
||||
|
||||
# Polja koja omogućuju klik na objekt
|
||||
list_display_links = ('name', 'tax_id')
|
||||
|
||||
# Filteri sa strane (vrlo korisno za ERP)
|
||||
list_filter = ('client_type', 'is_active', 'created_at')
|
||||
|
||||
# Polja po kojima admin može pretraživati
|
||||
search_fields = ('name', 'tax_id', 'email')
|
||||
|
||||
readonly_fields = ('created_at', 'updated_at', 'linked_cranes_preview')
|
||||
|
||||
# Grupiranje polja u formi za ljepši pregled
|
||||
fieldsets = (
|
||||
(_('Osnovni podaci'), {
|
||||
'fields': ('name', 'client_type', 'tax_id')
|
||||
}),
|
||||
(_('Kontakt podaci'), {
|
||||
'fields': ('email', 'phone', 'address', 'city', 'postal_code')
|
||||
}),
|
||||
(_('Vozila kupca'), {
|
||||
'fields': ('customer_cranes', 'linked_cranes_preview')
|
||||
}),
|
||||
(_('Sustavni podaci'), {
|
||||
'fields': ('is_active', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
list_per_page = 50
|
||||
|
||||
@admin.display(description="Dizalice")
|
||||
def vehicle_count(self, obj):
|
||||
return obj.vehicles.filter(asset_type='crane').count()
|
||||
|
||||
@admin.display(description="Povezane dizalice")
|
||||
def linked_cranes_preview(self, obj):
|
||||
if not obj.pk:
|
||||
return _("Spremite klijenta za prikaz povezanih dizalica.")
|
||||
cranes = Crane.objects.filter(client=obj, is_active=True, asset_type='crane').order_by('registration_number')
|
||||
if not cranes.exists():
|
||||
return _("Nema povezanih dizalica.")
|
||||
return ", ".join(str(crane) for crane in cranes)
|
||||
|
||||
def save_related(self, request, form, formsets, change):
|
||||
super().save_related(request, form, formsets, change)
|
||||
selected_cranes = form.cleaned_data.get('customer_cranes')
|
||||
if selected_cranes is None:
|
||||
return
|
||||
|
||||
selected_ids = list(selected_cranes.values_list('id', flat=True))
|
||||
Crane.objects.filter(client=form.instance, asset_type='crane').exclude(pk__in=selected_ids).update(client=None)
|
||||
if selected_ids:
|
||||
Crane.objects.filter(pk__in=selected_ids).update(client=form.instance, is_company_vehicle=False, asset_type='crane')
|
||||
8
backend/modules/crm/apps.py
Normal file
8
backend/modules/crm/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# /backend/modules/crm/apps.py
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
class FleetConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'modules.crm'
|
||||
verbose_name = "Customer Relationship Management"
|
||||
37
backend/modules/crm/migrations/0001_initial.py
Normal file
37
backend/modules/crm/migrations/0001_initial.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.2.15 on 2026-06-29 20:13
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Client',
|
||||
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')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Naziv/Ime')),
|
||||
('client_type', models.CharField(choices=[('legal', 'Pravna osoba'), ('individual', 'Fizička osoba')], default='legal', max_length=20)),
|
||||
('tax_id', models.CharField(max_length=50, unique=True, verbose_name='OIB/VAT broj')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
|
||||
('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='Telefon')),
|
||||
('address', models.TextField(verbose_name='Adresa')),
|
||||
('city', models.CharField(max_length=100, verbose_name='Grad')),
|
||||
('postal_code', models.CharField(max_length=20, verbose_name='Poštanski broj')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Klijent',
|
||||
'verbose_name_plural': 'Klijenti',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/modules/crm/migrations/__init__.py
Normal file
0
backend/modules/crm/migrations/__init__.py
Normal file
31
backend/modules/crm/models.py
Normal file
31
backend/modules/crm/models.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from core.base_models import BaseModel
|
||||
|
||||
class Client(BaseModel):
|
||||
# Tip klijenta: OIB/VAT broj je ključan za fakturiranje
|
||||
CLIENT_TYPES = [
|
||||
('legal', _('Pravna osoba')),
|
||||
('individual', _('Fizička osoba')),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=255, verbose_name=_("Naziv/Ime"))
|
||||
client_type = models.CharField(max_length=20, choices=CLIENT_TYPES, default='legal')
|
||||
|
||||
# Porezni podaci (za fakturiranje)
|
||||
tax_id = models.CharField(max_length=50, unique=True, verbose_name=_("OIB/VAT broj"))
|
||||
|
||||
# Kontakt podaci
|
||||
email = models.EmailField(unique=True, verbose_name=_("Email"))
|
||||
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("Telefon"))
|
||||
address = models.TextField(verbose_name=_("Adresa"))
|
||||
city = models.CharField(max_length=100, verbose_name=_("Grad"))
|
||||
postal_code = models.CharField(max_length=20, verbose_name=_("Poštanski broj"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Klijent")
|
||||
verbose_name_plural = _("Klijenti")
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.tax_id})"
|
||||
50
backend/modules/crm/serializers.py
Normal file
50
backend/modules/crm/serializers.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from .models import Client
|
||||
from .services import ClientService
|
||||
|
||||
|
||||
class ClientSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = [
|
||||
'id', 'name', 'client_type', 'tax_id',
|
||||
'email', 'phone', 'address', 'city', 'postal_code',
|
||||
'created_at', 'is_active'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'is_active']
|
||||
|
||||
def _get_request_user(self):
|
||||
"""
|
||||
Sigurno dohvaćanje korisnika iz contexta serializer-a.
|
||||
Request iz RequestFactory može biti WSGIRequest bez atributa 'user',
|
||||
zato koristimo getattr.
|
||||
"""
|
||||
request = self.context.get('request') if self.context else None
|
||||
return getattr(request, 'user', None)
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Delegate creation to service layer.
|
||||
"""
|
||||
try:
|
||||
user = self._get_request_user()
|
||||
return ClientService.create_client(validated_data, user=user)
|
||||
except DRFValidationError:
|
||||
# re-raise as-is
|
||||
raise
|
||||
except Exception as exc:
|
||||
# wrap non-DRF exceptions
|
||||
raise DRFValidationError({"detail": str(exc)})
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
Delegate update to service layer.
|
||||
"""
|
||||
try:
|
||||
user = self._get_request_user()
|
||||
return ClientService.update_client(instance, validated_data, user=user)
|
||||
except DRFValidationError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise DRFValidationError({"detail": str(exc)})
|
||||
65
backend/modules/crm/services.py
Normal file
65
backend/modules/crm/services.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from django.db import transaction, IntegrityError
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from .models import Client
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientService:
|
||||
@staticmethod
|
||||
def create_client(data: dict, user=None) -> Client:
|
||||
"""
|
||||
Create a Client instance with model validation and handle IntegrityError.
|
||||
"""
|
||||
# Instantiate model and run model-level validation
|
||||
client = Client(**data)
|
||||
try:
|
||||
client.full_clean()
|
||||
except Exception as e:
|
||||
logger.error(f"Client validation failed: {e}")
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
client.save()
|
||||
except IntegrityError as exc:
|
||||
logger.error(f"Integrity error creating client: {exc}")
|
||||
# Convert DB integrity errors into DRF ValidationError for the API layer
|
||||
raise ValidationError({"detail": str(exc)})
|
||||
|
||||
return client
|
||||
|
||||
@staticmethod
|
||||
def update_client(instance: Client, data: dict, user=None) -> Client:
|
||||
"""
|
||||
Update the given Client instance. Validate and save.
|
||||
"""
|
||||
for attr, value in data.items():
|
||||
setattr(instance, attr, value)
|
||||
|
||||
try:
|
||||
instance.full_clean()
|
||||
except Exception as e:
|
||||
logger.error(f"Client validation failed on update: {e}")
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
instance.save()
|
||||
except IntegrityError as exc:
|
||||
logger.error(f"Integrity error updating client: {exc}")
|
||||
raise ValidationError({"detail": str(exc)})
|
||||
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def delete_client(instance: Client, user=None) -> Client:
|
||||
"""
|
||||
Soft-delete the client (if you want soft delete).
|
||||
If hard delete is desired, call instance.delete() instead.
|
||||
"""
|
||||
instance.is_active = False
|
||||
instance.save()
|
||||
return instance
|
||||
75
backend/modules/crm/tests/test_models.py
Normal file
75
backend/modules/crm/tests/test_models.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError
|
||||
|
||||
from modules.crm.models import Client
|
||||
|
||||
|
||||
class ClientModelTests(TestCase):
|
||||
def setUp(self):
|
||||
# osnovni validni podaci koje ćemo koristiti u testovima
|
||||
self.base_data = {
|
||||
"name": "Test Client",
|
||||
"tax_id": str(uuid.uuid4()), # jedinstveno
|
||||
"email": f"client+{uuid.uuid4()}@example.test",
|
||||
"address": "Ulica 1",
|
||||
"city": "Grad",
|
||||
"postal_code": "10000",
|
||||
}
|
||||
|
||||
def test_create_client_success(self):
|
||||
client = Client.objects.create(
|
||||
client_type="legal",
|
||||
**self.base_data
|
||||
)
|
||||
self.assertIsNotNone(client.pk)
|
||||
self.assertEqual(client.name, "Test Client")
|
||||
self.assertEqual(client.client_type, "legal")
|
||||
# default ordering ili drugi atributi mogu se dodatno provjeriti
|
||||
self.assertTrue(str(client).startswith("Test Client"))
|
||||
self.assertIn(client.tax_id, str(client))
|
||||
|
||||
def test_default_client_type_is_legal(self):
|
||||
# Ako eksplicitno ne pošaljemo client_type, default je 'legal'
|
||||
data = dict(self.base_data)
|
||||
data["tax_id"] = str(uuid.uuid4())
|
||||
data["email"] = f"client+{uuid.uuid4()}@example.test"
|
||||
client = Client.objects.create(**data)
|
||||
self.assertEqual(client.client_type, "legal")
|
||||
|
||||
def test_str_representation_contains_name_and_taxid(self):
|
||||
client = Client.objects.create(client_type="individual", **self.base_data)
|
||||
expected = f"{client.name} ({client.tax_id})"
|
||||
self.assertEqual(str(client), expected)
|
||||
|
||||
def test_unique_tax_id_constraint(self):
|
||||
# Prva kreacija prođe
|
||||
Client.objects.create(client_type="legal", **self.base_data)
|
||||
# Pokušaj dupliciranja tax_id-a treba podići IntegrityError
|
||||
dup = dict(self.base_data)
|
||||
dup["email"] = f"client+{uuid.uuid4()}@example.test" # unique email drugi
|
||||
with self.assertRaises(IntegrityError):
|
||||
Client.objects.create(client_type="legal", **dup)
|
||||
|
||||
def test_unique_email_constraint(self):
|
||||
Client.objects.create(client_type="legal", **self.base_data)
|
||||
dup = dict(self.base_data)
|
||||
dup["tax_id"] = str(uuid.uuid4()) # unique tax_id drugi
|
||||
with self.assertRaises(IntegrityError):
|
||||
Client.objects.create(client_type="legal", **dup)
|
||||
|
||||
def test_full_clean_validates_email_format(self):
|
||||
data = dict(self.base_data)
|
||||
data["email"] = "not-an-email"
|
||||
client = Client(**data)
|
||||
with self.assertRaises(ValidationError):
|
||||
client.full_clean() # EmailField validation je pokrenuta
|
||||
|
||||
def test_full_clean_requires_required_fields(self):
|
||||
# Uklonimo obavezno polje name i očekujemo ValidationError
|
||||
data = dict(self.base_data)
|
||||
data.pop("name")
|
||||
client = Client(**data)
|
||||
with self.assertRaises(ValidationError):
|
||||
client.full_clean()
|
||||
43
backend/modules/crm/tests/test_serializers.py
Normal file
43
backend/modules/crm/tests/test_serializers.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import uuid
|
||||
from django.test import TestCase, RequestFactory
|
||||
from unittest.mock import patch
|
||||
from modules.crm.serializers import ClientSerializer
|
||||
from modules.crm.models import Client
|
||||
|
||||
|
||||
class ClientSerializerTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.base_data = {
|
||||
"name": "Ser Client",
|
||||
"client_type": "legal",
|
||||
"tax_id": str(uuid.uuid4()),
|
||||
"email": f"client+{uuid.uuid4()}@example.test",
|
||||
"address": "Street 1",
|
||||
"city": "City",
|
||||
"postal_code": "00000",
|
||||
}
|
||||
|
||||
@patch("modules.crm.serializers.ClientService.create_client")
|
||||
def test_create_calls_service(self, mock_create):
|
||||
# Prepare mock return value
|
||||
mock_instance = Client(**self.base_data)
|
||||
mock_instance.pk = 1
|
||||
mock_create.return_value = mock_instance
|
||||
|
||||
serializer = ClientSerializer(data=self.base_data, context={"request": self.factory.post("/")})
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
result = serializer.save()
|
||||
mock_create.assert_called_once()
|
||||
self.assertEqual(result.pk, 1)
|
||||
|
||||
@patch("modules.crm.serializers.ClientService.update_client")
|
||||
def test_update_calls_service(self, mock_update):
|
||||
client = Client.objects.create(**self.base_data)
|
||||
mock_update.return_value = client
|
||||
|
||||
serializer = ClientSerializer(instance=client, data={"city": "UpdatedCity"}, partial=True, context={"request": self.factory.put("/")})
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
result = serializer.save()
|
||||
mock_update.assert_called_once()
|
||||
self.assertEqual(result, client)
|
||||
36
backend/modules/crm/tests/test_services.py
Normal file
36
backend/modules/crm/tests/test_services.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from modules.crm.services import ClientService
|
||||
from modules.crm.models import Client
|
||||
|
||||
|
||||
class ClientServiceTests(TestCase):
|
||||
def setUp(self):
|
||||
self.base_data = {
|
||||
"name": "Service Client",
|
||||
"client_type": "legal",
|
||||
"tax_id": str(uuid.uuid4()),
|
||||
"email": f"client+{uuid.uuid4()}@example.test",
|
||||
"address": "Street 1",
|
||||
"city": "City",
|
||||
"postal_code": "00000",
|
||||
}
|
||||
|
||||
def test_create_client_success(self):
|
||||
client = ClientService.create_client(self.base_data)
|
||||
self.assertIsNotNone(client.pk)
|
||||
self.assertEqual(client.name, "Service Client")
|
||||
|
||||
def test_create_client_duplicate_tax_id_raises(self):
|
||||
# create first
|
||||
Client.objects.create(**self.base_data)
|
||||
dup = dict(self.base_data)
|
||||
dup["email"] = f"client+{uuid.uuid4()}@example.test"
|
||||
with self.assertRaises(ValidationError):
|
||||
ClientService.create_client(dup)
|
||||
|
||||
def test_update_client_success(self):
|
||||
client = Client.objects.create(**self.base_data)
|
||||
updated = ClientService.update_client(client, {"city": "NewCity"})
|
||||
self.assertEqual(updated.city, "NewCity")
|
||||
62
backend/modules/crm/tests/test_views.py
Normal file
62
backend/modules/crm/tests/test_views.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import uuid
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.contrib.auth import get_user_model
|
||||
from unittest.mock import patch
|
||||
from modules.crm.serializers import ClientSerializer
|
||||
from modules.crm.views import ClientViewSet
|
||||
from modules.crm.models import Client
|
||||
|
||||
|
||||
class ClientViewSetTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username=f"user_{uuid.uuid4().hex[:6]}", email=f"user+{uuid.uuid4()}@example.test", password="pass")
|
||||
self.factory = RequestFactory()
|
||||
self.view = ClientViewSet()
|
||||
self.view.request = self.factory.post("/")
|
||||
self.view.request.user = self.user
|
||||
|
||||
self.base_data = {
|
||||
"name": "View Client",
|
||||
"client_type": "legal",
|
||||
"tax_id": str(uuid.uuid4()),
|
||||
"email": f"client+{uuid.uuid4()}@example.test",
|
||||
"address": "Street 1",
|
||||
"city": "City",
|
||||
"postal_code": "00000",
|
||||
}
|
||||
|
||||
@patch("modules.crm.views.ClientService.create_client")
|
||||
def test_perform_create_delegates_to_service(self, mock_create):
|
||||
# make serializer valid
|
||||
serializer = ClientSerializer(data=self.base_data, context={"request": self.view.request})
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
mock_inst = Client(**self.base_data)
|
||||
mock_inst.pk = 1
|
||||
mock_create.return_value = mock_inst
|
||||
|
||||
self.view.request = self.factory.post("/")
|
||||
self.view.request.user = self.user
|
||||
|
||||
# perform_create should call serializer.save() which delegates to service
|
||||
self.view.perform_create(serializer)
|
||||
mock_create.assert_called_once()
|
||||
|
||||
@patch("modules.crm.views.ClientService.update_client")
|
||||
def test_perform_update_delegates_to_service(self, mock_update):
|
||||
client = Client.objects.create(**self.base_data)
|
||||
serializer = ClientSerializer(instance=client, data={"city": "UpdatedCity"}, partial=True, context={"request": self.view.request})
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
mock_update.return_value = client
|
||||
|
||||
self.view.request = self.factory.put("/")
|
||||
self.view.request.user = self.user
|
||||
serializer.instance = client
|
||||
|
||||
self.view.perform_update(serializer)
|
||||
mock_update.assert_called_once()
|
||||
client.refresh_from_db()
|
||||
# serializer.perform_update causes serializer.save() which we delegated; check instance still exists
|
||||
self.assertIsNotNone(client.pk)
|
||||
7
backend/modules/crm/urls.py
Normal file
7
backend/modules/crm/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import ClientViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'clients', ClientViewSet, basename='client')
|
||||
|
||||
urlpatterns = router.urls
|
||||
41
backend/modules/crm/views.py
Normal file
41
backend/modules/crm/views.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import logging
|
||||
from rest_framework import viewsets, permissions
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from .models import Client
|
||||
from .serializers import ClientSerializer
|
||||
from .services import ClientService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientViewSet(viewsets.ModelViewSet):
|
||||
queryset = Client.objects.filter(is_active=True)
|
||||
serializer_class = ClientSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
try:
|
||||
instance = serializer.save()
|
||||
serializer.instance = instance
|
||||
except ValidationError as e:
|
||||
logger.warning(f"Validation error creating client by {self.request.user}: {e}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error creating client: {e}", exc_info=True)
|
||||
raise ValidationError({"detail": "Došlo je do greške pri stvaranju klijenta."})
|
||||
|
||||
def perform_update(self, serializer):
|
||||
try:
|
||||
instance = serializer.save()
|
||||
serializer.instance = instance
|
||||
except ValidationError as e:
|
||||
logger.warning(f"Validation error updating client by {self.request.user}: {e}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error updating client: {e}", exc_info=True)
|
||||
raise ValidationError({"detail": "Došlo je do greške pri ažuriranju klijenta."})
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# Delegate deletion (soft-delete) to service layer
|
||||
ClientService.delete_client(instance, user=self.request.user)
|
||||
logger.info(f"Client {instance.id} soft-deleted by {self.request.user}")
|
||||
Reference in New Issue
Block a user