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

1import logging 

2from html import escape 

3 

4from django.conf import settings 

5from django.core.mail import EmailMultiAlternatives 

6 

7logger = logging.getLogger(__name__) 

8 

9 

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 

14 

15 from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'no-reply@fudo.local') 

16 

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 ) 

35 

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 """ 

65 

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