Coverage for core\email_service.py: 0%
26 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
« prev ^ index » next coverage.py v7.16.0, created at 2026-10-17 16:50 +0800
1import logging
2from html import escape
4from django.conf import settings
5from django.core.mail import EmailMultiAlternatives
7logger = logging.getLogger(__name__)
10def send_invoice_email(invoice, recipient_email: str | None = None) -> bool:
11 """Send an invoice email via the configured provider. Returns True if sent."""
12 if not recipient_email or not getattr(settings, 'EMAIL_ENABLED', False):
13 return False
15 from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'no-reply@fudo.local')
17 try:
18 invoice_number = escape(str(invoice.invoice_number))
19 customer_name = escape(str(invoice.customer_name))
20 currency = escape(str(invoice.currency))
21 line_rows = []
22 text_lines = []
23 for line in invoice.lines.all():
24 description = escape(str(line.description))
25 line_total = f"{line.total:.2f}"
26 line_rows.append(
27 f'<tr><td style="padding: 10px 0; border-bottom: 1px solid #e2e8f0;">{description}</td>'
28 f'<td style="padding: 10px 0; text-align: right; border-bottom: 1px solid #e2e8f0;">'
29 f'{line.quantity} x {line.unit_price:.2f} = {line_total} {currency}</td></tr>'
30 )
31 text_lines.append(
32 f"- {line.description}: {line.quantity} x {line.unit_price:.2f} = "
33 f"{line_total} {invoice.currency}"
34 )
36 subject = f"Invoice {invoice.invoice_number} - {invoice.customer_name}"
37 text_body = (
38 f"Hello {invoice.customer_name},\n\n"
39 f"Your invoice {invoice.invoice_number} has been issued.\n\n"
40 + "\n".join(text_lines)
41 + f"\n\nSubtotal: {invoice.subtotal:.2f} {invoice.currency}\n"
42 f"Tax: {invoice.tax_total:.2f} {invoice.currency}\n"
43 f"Total: {invoice.total_amount:.2f} {invoice.currency}\n"
44 f"Due date: {invoice.due_date}\n\n"
45 f"Thank you for your business.\nRegards,\nFUDO ERP"
46 )
47 html_body = f"""
48 <html><body style="font-family: Arial, sans-serif; color: #0f172a; line-height: 1.6;">
49 <div style="max-width: 640px; margin: 0 auto; padding: 24px; border: 1px solid #e2e8f0; border-radius: 12px;">
50 <h2 style="margin: 0 0 12px;">Invoice {invoice_number}</h2>
51 <p>Hello {customer_name},</p>
52 <p>Your invoice has been issued for <strong>{invoice.total_amount:.2f} {currency}</strong>.</p>
53 <table style="width: 100%; border-collapse: collapse; margin: 24px 0;">
54 <thead><tr><th style="text-align: left;">Description</th><th style="text-align: right;">Amount</th></tr></thead>
55 <tbody>{''.join(line_rows)}</tbody>
56 </table>
57 <p style="text-align: right;"><strong>Subtotal:</strong> {invoice.subtotal:.2f} {currency}<br />
58 <strong>Tax:</strong> {invoice.tax_total:.2f} {currency}<br />
59 <strong>Total:</strong> {invoice.total_amount:.2f} {currency}</p>
60 <p><strong>Due date:</strong> {invoice.due_date}<br /><strong>Status:</strong> {escape(str(invoice.status))}</p>
61 <p>Thank you for your business.</p><p>Regards,<br />FUDO ERP</p>
62 </div>
63 </body></html>
64 """
66 message = EmailMultiAlternatives(
67 subject=subject,
68 body=text_body,
69 from_email=from_email,
70 to=[recipient_email],
71 )
72 message.attach_alternative(html_body, 'text/html')
73 return message.send() > 0
74 except Exception as exc: # pragma: no cover - provider failures fail softly
75 logger.exception(
76 "Failed to send invoice email for invoice %s: %s",
77 getattr(invoice, 'id', 'unknown'),
78 exc,
79 )
80 return False