From 666ddb56c9f659dc05899d16d86120558c8bddda Mon Sep 17 00:00:00 2001 From: mariomitte Date: Tue, 21 Jul 2026 15:58:50 +0200 Subject: [PATCH] feat: dodaj prefill email potpisa i body logging Uvodi signature polje i default predlozak potpisa za nove korisnike, te data migraciju koja popunjava postojece prazne potpise. Email flow sada dosljedno dodaje korisnicki potpis i sprema finalni body poruke u dispatch log radi audita.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/core/users/admin.py | 4 +- .../migrations/0007_customuser_signature.py | 16 ++++++++ .../0008_prefill_empty_signatures.py | 39 +++++++++++++++++++ backend/core/users/models.py | 35 ++++++++++++++++- .../core/users/tests/test_email_signature.py | 39 +++++++++++++++++++ backend/modules/fleet/email_utils.py | 17 ++++++++ backend/modules/fleet/tasks.py | 6 ++- .../test_work_order_email_task_cached_pdfs.py | 23 +++++++++++ backend/modules/fleet/views.py | 6 ++- 9 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 backend/core/users/migrations/0007_customuser_signature.py create mode 100644 backend/core/users/migrations/0008_prefill_empty_signatures.py create mode 100644 backend/core/users/tests/test_email_signature.py create mode 100644 backend/modules/fleet/email_utils.py diff --git a/backend/core/users/admin.py b/backend/core/users/admin.py index 20d2880..0e4d33c 100644 --- a/backend/core/users/admin.py +++ b/backend/core/users/admin.py @@ -9,7 +9,7 @@ class CustomUserAdmin(UserAdmin): list_display = ('email', 'first_name', 'last_name', 'occupation', 'work_position', 'residence', 'is_serviser', 'is_kupac', 'is_team_member', 'is_active', 'client_profile') fieldsets = ( (None, {'fields': ('email', 'password')}), - ('Osobni podaci', {'fields': ('first_name', 'last_name', 'telefon', 'oib', 'occupation', 'work_position', 'residence')}), + ('Osobni podaci', {'fields': ('first_name', 'last_name', 'telefon', 'oib', 'occupation', 'work_position', 'residence', 'signature')}), ('ERP Status', {'fields': ('is_active', 'is_staff', 'is_superuser', 'is_serviser', 'is_kupac', 'is_team_member', 'is_verified', 'client_profile')}), ('Važni datumi', {'fields': ('last_login', 'date_joined')}), ) @@ -18,7 +18,7 @@ class CustomUserAdmin(UserAdmin): add_fieldsets = ( (None, { 'classes': ('wide',), - 'fields': ('email', 'password', 'is_team_member'), + 'fields': ('email', 'password', 'first_name', 'last_name', 'telefon', 'occupation', 'signature', 'is_team_member'), }), ) diff --git a/backend/core/users/migrations/0007_customuser_signature.py b/backend/core/users/migrations/0007_customuser_signature.py new file mode 100644 index 0000000..6af153f --- /dev/null +++ b/backend/core/users/migrations/0007_customuser_signature.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_customuser_work_position'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='signature', + field=models.TextField(blank=True, default='', verbose_name='Email potpis'), + ), + ] diff --git a/backend/core/users/migrations/0008_prefill_empty_signatures.py b/backend/core/users/migrations/0008_prefill_empty_signatures.py new file mode 100644 index 0000000..3041bdc --- /dev/null +++ b/backend/core/users/migrations/0008_prefill_empty_signatures.py @@ -0,0 +1,39 @@ +from django.db import migrations + + +def prefill_empty_signatures(apps, schema_editor): + CustomUser = apps.get_model('users', 'CustomUser') + for user in CustomUser.objects.all().iterator(): + signature_value = user.signature if isinstance(user.signature, str) else '' + if signature_value.strip(): + continue + first_name = (user.first_name or '').strip() + last_name = (user.last_name or '').strip() + full_name = f"{first_name} {last_name}".strip() + fallback_name = (user.email or '').strip() or 'Ime i prezime' + display_name = full_name or fallback_name + occupation = (user.occupation or '').strip() + name_line = f"{occupation} {display_name}".strip() if occupation else display_name + mobile = (user.telefon or '').strip() or "+385 91 636 3466" + signature = ( + "Poslano iz aplikacije\n" + "S poštovanjem / Freundliche Grüße / Kind regards\n\n" + f"{name_line}\n\n" + "KNEZ LJUBO d.o.o.\n" + "Ivlje 44, 10040 Zagreb\n\n" + "Office: Donje Svetice 40, 10000 Zagreb\n\n" + f"Mobile: {mobile}\n\n" + "www.knezljubo.hr" + ) + user.signature = signature + user.save(update_fields=['signature']) + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0007_customuser_signature'), + ] + + operations = [ + migrations.RunPython(prefill_empty_signatures, migrations.RunPython.noop), + ] diff --git a/backend/core/users/models.py b/backend/core/users/models.py index abd6880..967135e 100644 --- a/backend/core/users/models.py +++ b/backend/core/users/models.py @@ -3,6 +3,17 @@ from django.core.exceptions import ValidationError from django.db import models from .managers import CustomUserManager +DEFAULT_EMAIL_SIGNATURE_TEMPLATE = ( + "Poslano iz aplikacije\n" + "S poštovanjem / Freundliche Grüße / Kind regards\n\n" + "{name_line}\n\n" + "KNEZ LJUBO d.o.o.\n" + "Ivlje 44, 10040 Zagreb\n\n" + "Office: Donje Svetice 40, 10000 Zagreb\n\n" + "Mobile: {mobile}\n\n" + "www.knezljubo.hr" +) + class CustomUser(AbstractUser): # Email koristimo za login, pa mora biti jedinstven username=None @@ -19,6 +30,7 @@ class CustomUser(AbstractUser): occupation = models.CharField(max_length=120, blank=True, default='', verbose_name="Zanimanje") residence = models.CharField(max_length=255, blank=True, default='', verbose_name="Prebivalište") work_position = models.CharField(max_length=120, blank=True, default='', verbose_name="Na radnom mjestu") + signature = models.TextField(blank=True, default='', verbose_name="Email potpis") licence_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="Broj licence") is_verified = models.BooleanField(default=False, verbose_name="Verificiran profil") @@ -47,6 +59,27 @@ class CustomUser(AbstractUser): if self.is_kupac and not self.client_profile: raise ValidationError({'client_profile': "Korisnik koji je označen kao kupac mora imati povezan klijentski profil."}) + def build_default_signature(self): + full_name = self.get_full_name().strip() + fallback_name = self.email.strip() if isinstance(self.email, str) and self.email.strip() else 'Ime i prezime' + display_name = full_name or fallback_name + occupation = self.occupation.strip() if isinstance(self.occupation, str) else '' + name_line = f"{occupation} {display_name}".strip() if occupation else display_name + mobile = self.telefon.strip() if isinstance(self.telefon, str) and self.telefon.strip() else "+385 91 636 3466" + return DEFAULT_EMAIL_SIGNATURE_TEMPLATE.format( + name_line=name_line, + mobile=mobile, + ) + + def get_email_signature(self): + stored_signature = self.signature.strip() if isinstance(self.signature, str) else '' + if stored_signature: + return stored_signature + return self.build_default_signature() + def save(self, *args, **kwargs): + signature = self.signature.strip() if isinstance(self.signature, str) else '' + if not signature: + self.signature = self.build_default_signature() self.full_clean() # Prisiljava pokretanje clean() prije svakog save() - super().save(*args, **kwargs) \ No newline at end of file + super().save(*args, **kwargs) diff --git a/backend/core/users/tests/test_email_signature.py b/backend/core/users/tests/test_email_signature.py new file mode 100644 index 0000000..dfedc66 --- /dev/null +++ b/backend/core/users/tests/test_email_signature.py @@ -0,0 +1,39 @@ +import uuid + +import pytest +from django.contrib.auth import get_user_model + + +@pytest.mark.django_db +def test_new_user_gets_prefilled_signature(): + User = get_user_model() + suffix = uuid.uuid4().hex[:8] + user = User.objects.create_user( + email=f"signature-{suffix}@example.test", + password="test1234", + first_name="Mario", + last_name="Tkalac", + telefon="+385916363466", + ) + + signature = user.signature + assert signature.startswith("Poslano iz aplikacije\n") + assert "S poštovanjem / Freundliche Grüße / Kind regards" in signature + assert "Mario Tkalac" in signature + assert "KNEZ LJUBO d.o.o." in signature + assert "Office: Donje Svetice 40, 10000 Zagreb" in signature + assert "Mobile: +385916363466" in signature + assert "www.knezljubo.hr" in signature + + +@pytest.mark.django_db +def test_custom_signature_is_preserved(): + User = get_user_model() + user = User.objects.create_user( + email=f"custom-signature-{uuid.uuid4().hex[:8]}@example.test", + password="test1234", + signature="Custom email signature", + ) + + assert user.signature == "Custom email signature" + assert user.get_email_signature() == "Custom email signature" diff --git a/backend/modules/fleet/email_utils.py b/backend/modules/fleet/email_utils.py new file mode 100644 index 0000000..4cc50b3 --- /dev/null +++ b/backend/modules/fleet/email_utils.py @@ -0,0 +1,17 @@ +def append_user_signature(body, user): + body_text = str(body or '').strip() + if user is None: + return body_text + + signature = '' + get_signature = getattr(user, 'get_email_signature', None) + if callable(get_signature): + signature = str(get_signature() or '').strip() + else: + signature = str(getattr(user, 'signature', '') or '').strip() + + if not signature: + return body_text + if not body_text: + return signature + return f"{body_text}\n\n{signature}" diff --git a/backend/modules/fleet/tasks.py b/backend/modules/fleet/tasks.py index 8f0a0cb..88f0cba 100644 --- a/backend/modules/fleet/tasks.py +++ b/backend/modules/fleet/tasks.py @@ -29,6 +29,7 @@ from reportlab.lib.utils import ImageReader from reportlab.pdfgen import canvas from .models import VehicleNotification, GeneratedWorkOrderPdf from .pdf_layout import register_unicode_fonts, draw_standard_header_footer +from .email_utils import append_user_signature logger = logging.getLogger(__name__) register_unicode_fonts() @@ -435,7 +436,7 @@ def send_work_order_email_bundle_task( email_message = EmailMessage( subject=subject or f"Putni nalog {_work_order_display_code(work_order)}", - body="\n".join(body_lines).strip(), + body=append_user_signature("\n".join(body_lines).strip(), requested_by), from_email=from_email, to=recipients or [], ) @@ -450,7 +451,8 @@ def send_work_order_email_bundle_task( } for filename, content, content_type in attachments ] - dispatch_log.save(update_fields=['attachments', 'updated_at']) + dispatch_log.message = email_message.body + dispatch_log.save(update_fields=['attachments', 'message', 'updated_at']) try: email_message.send(fail_silently=False) diff --git a/backend/modules/fleet/tests/test_work_order_email_task_cached_pdfs.py b/backend/modules/fleet/tests/test_work_order_email_task_cached_pdfs.py index 74d2721..5e9c89e 100644 --- a/backend/modules/fleet/tests/test_work_order_email_task_cached_pdfs.py +++ b/backend/modules/fleet/tests/test_work_order_email_task_cached_pdfs.py @@ -1,6 +1,7 @@ from unittest.mock import patch from django.contrib.auth import get_user_model +from django.core import mail from django.test import TestCase, override_settings from modules.fleet.models import GeneratedWorkOrderPdf, Vehicle, VehicleNotification, WorkOrder @@ -58,3 +59,25 @@ class WorkOrderEmailTaskCachedPdfTests(TestCase): cached_pdfs = notif.metadata.get('cached_pdfs') if isinstance(notif.metadata, dict) else None self.assertTrue(isinstance(cached_pdfs, list) and len(cached_pdfs) > 0) self.assertIn('download_url', cached_pdfs[0]) + + def test_success_email_appends_requester_signature(self): + mail.outbox.clear() + + result = send_work_order_email_bundle_task( + work_order_id=str(self.work_order.pk), + requested_by_id=str(self.user.pk), + recipients=['client@example.test'], + message='Test poruka bez potpisa.', + include_work_order_pdf=True, + include_service_records_pdf=False, + include_invoices_pdf=False, + include_images=False, + include_monthly_tasks=False, + ) + + self.assertEqual(result.get('status'), 'ok') + self.assertEqual(len(mail.outbox), 1) + sent_body = mail.outbox[0].body + self.assertIn('Test poruka bez potpisa.', sent_body) + self.assertIn('S poštovanjem / Freundliche Grüße / Kind regards', sent_body) + self.assertIn(self.user.email, sent_body) diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 656503d..446f77d 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -30,6 +30,7 @@ from reportlab.lib.styles import getSampleStyleSheet from reportlab.pdfgen import canvas from reportlab.platypus import Table, TableStyle, Paragraph from .pdf_layout import register_unicode_fonts, draw_standard_header_footer +from .email_utils import append_user_signature from rest_framework import viewsets, permissions, status, mixins from rest_framework.decorators import action, api_view, permission_classes from rest_framework.response import Response @@ -2203,10 +2204,11 @@ class VehicleServiceRecordViewSet(viewsets.ModelViewSet): 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')}." ) + body_with_signature = append_user_signature(body, request.user) _send_document_email( recipient=recipient, subject=subject, - body=body, + body=body_with_signature, filename=f"{service_record.pk}.service-record.pdf", pdf_bytes=pdf_bytes, ) @@ -2216,7 +2218,7 @@ class VehicleServiceRecordViewSet(viewsets.ModelViewSet): service_record=service_record, recipients=[recipient], subject=subject, - message=body, + message=body_with_signature, attachments=[{'type': 'service_record_pdf', 'filename': f"{service_record.pk}.service-record.pdf"}], status='sent', sent_at=timezone.now(),