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>
This commit is contained in:
@@ -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')
|
list_display = ('email', 'first_name', 'last_name', 'occupation', 'work_position', 'residence', 'is_serviser', 'is_kupac', 'is_team_member', 'is_active', 'client_profile')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {'fields': ('email', 'password')}),
|
(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')}),
|
('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')}),
|
('Važni datumi', {'fields': ('last_login', 'date_joined')}),
|
||||||
)
|
)
|
||||||
@@ -18,7 +18,7 @@ class CustomUserAdmin(UserAdmin):
|
|||||||
add_fieldsets = (
|
add_fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
'fields': ('email', 'password', 'is_team_member'),
|
'fields': ('email', 'password', 'first_name', 'last_name', 'telefon', 'occupation', 'signature', 'is_team_member'),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
16
backend/core/users/migrations/0007_customuser_signature.py
Normal file
16
backend/core/users/migrations/0007_customuser_signature.py
Normal file
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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),
|
||||||
|
]
|
||||||
@@ -3,6 +3,17 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from .managers import CustomUserManager
|
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):
|
class CustomUser(AbstractUser):
|
||||||
# Email koristimo za login, pa mora biti jedinstven
|
# Email koristimo za login, pa mora biti jedinstven
|
||||||
username=None
|
username=None
|
||||||
@@ -19,6 +30,7 @@ class CustomUser(AbstractUser):
|
|||||||
occupation = models.CharField(max_length=120, blank=True, default='', verbose_name="Zanimanje")
|
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")
|
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")
|
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")
|
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")
|
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:
|
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."})
|
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):
|
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()
|
self.full_clean() # Prisiljava pokretanje clean() prije svakog save()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
39
backend/core/users/tests/test_email_signature.py
Normal file
39
backend/core/users/tests/test_email_signature.py
Normal file
@@ -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"
|
||||||
17
backend/modules/fleet/email_utils.py
Normal file
17
backend/modules/fleet/email_utils.py
Normal file
@@ -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}"
|
||||||
@@ -29,6 +29,7 @@ from reportlab.lib.utils import ImageReader
|
|||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
from .models import VehicleNotification, GeneratedWorkOrderPdf
|
from .models import VehicleNotification, GeneratedWorkOrderPdf
|
||||||
from .pdf_layout import register_unicode_fonts, draw_standard_header_footer
|
from .pdf_layout import register_unicode_fonts, draw_standard_header_footer
|
||||||
|
from .email_utils import append_user_signature
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
register_unicode_fonts()
|
register_unicode_fonts()
|
||||||
@@ -435,7 +436,7 @@ def send_work_order_email_bundle_task(
|
|||||||
|
|
||||||
email_message = EmailMessage(
|
email_message = EmailMessage(
|
||||||
subject=subject or f"Putni nalog {_work_order_display_code(work_order)}",
|
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,
|
from_email=from_email,
|
||||||
to=recipients or [],
|
to=recipients or [],
|
||||||
)
|
)
|
||||||
@@ -450,7 +451,8 @@ def send_work_order_email_bundle_task(
|
|||||||
}
|
}
|
||||||
for filename, content, content_type in attachments
|
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:
|
try:
|
||||||
email_message.send(fail_silently=False)
|
email_message.send(fail_silently=False)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core import mail
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from modules.fleet.models import GeneratedWorkOrderPdf, Vehicle, VehicleNotification, WorkOrder
|
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
|
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.assertTrue(isinstance(cached_pdfs, list) and len(cached_pdfs) > 0)
|
||||||
self.assertIn('download_url', 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)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from reportlab.lib.styles import getSampleStyleSheet
|
|||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
from reportlab.platypus import Table, TableStyle, Paragraph
|
from reportlab.platypus import Table, TableStyle, Paragraph
|
||||||
from .pdf_layout import register_unicode_fonts, draw_standard_header_footer
|
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 import viewsets, permissions, status, mixins
|
||||||
from rest_framework.decorators import action, api_view, permission_classes
|
from rest_framework.decorators import action, api_view, permission_classes
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -2203,10 +2204,11 @@ class VehicleServiceRecordViewSet(viewsets.ModelViewSet):
|
|||||||
body = _first_non_empty(request.data.get('message')) or (
|
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')}."
|
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(
|
_send_document_email(
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
body=body,
|
body=body_with_signature,
|
||||||
filename=f"{service_record.pk}.service-record.pdf",
|
filename=f"{service_record.pk}.service-record.pdf",
|
||||||
pdf_bytes=pdf_bytes,
|
pdf_bytes=pdf_bytes,
|
||||||
)
|
)
|
||||||
@@ -2216,7 +2218,7 @@ class VehicleServiceRecordViewSet(viewsets.ModelViewSet):
|
|||||||
service_record=service_record,
|
service_record=service_record,
|
||||||
recipients=[recipient],
|
recipients=[recipient],
|
||||||
subject=subject,
|
subject=subject,
|
||||||
message=body,
|
message=body_with_signature,
|
||||||
attachments=[{'type': 'service_record_pdf', 'filename': f"{service_record.pk}.service-record.pdf"}],
|
attachments=[{'type': 'service_record_pdf', 'filename': f"{service_record.pk}.service-record.pdf"}],
|
||||||
status='sent',
|
status='sent',
|
||||||
sent_at=timezone.now(),
|
sent_at=timezone.now(),
|
||||||
|
|||||||
Reference in New Issue
Block a user