from __future__ import annotations import html import re from dataclasses import dataclass from datetime import date, datetime from decimal import Decimal from typing import Any @dataclass(slots=True) class EmailAttachment: """In-memory attachment used by the SMTP email service. This avoids writing temporary invoice/receipt files to disk and keeps Phase 7S.1D independent of any PDF engine. The attachment is currently generated as an HTML snapshot, which users can open/print/save as PDF from the mail client. A later PDF-rendering phase can reuse the same hook. """ filename: str content: bytes content_type: str = "application/octet-stream" def _safe_text(value: Any) -> str: if value is None: return "" if isinstance(value, (date, datetime)): return value.isoformat() return str(value) def _safe_filename(value: str, fallback: str) -> str: cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "_", (value or "").strip()).strip("_") return cleaned or fallback def _money(value: Any) -> str: try: amount = Decimal(str(value or "0")) return f"₹ {amount:,.2f}" except Exception: return _safe_text(value) def _client_name(client: Any) -> str: if not client: return "Client" return ( getattr(client, "client_name", None) or getattr(client, "trade_name", None) or getattr(client, "name", None) or "Client" ) def _invoice_number(invoice: Any) -> str: return _safe_text( getattr(invoice, "invoice_no", None) or getattr(invoice, "invoice_number", None) or getattr(invoice, "number", None) or getattr(invoice, "id", "invoice") ) def _invoice_items(invoice: Any) -> list[Any]: for attr in ("items", "line_items", "invoice_items"): rows = getattr(invoice, attr, None) if rows: try: return list(rows) except Exception: return [] return [] def build_invoice_html(invoice: Any, *, firm_name: str = "") -> str: client = getattr(invoice, "client", None) invoice_no = _invoice_number(invoice) rows = [] for index, item in enumerate(_invoice_items(invoice), start=1): desc = getattr(item, "description", None) or getattr(item, "item_description", None) or getattr(item, "service_name", None) or "Professional Fees" sac = getattr(item, "sac_code", None) or getattr(item, "hsn_sac", None) or "" taxable = getattr(item, "taxable_value", None) or getattr(item, "amount", None) or getattr(item, "line_total", None) gst_rate = getattr(item, "gst_rate", None) or getattr(item, "tax_rate", None) or "" total = getattr(item, "total_amount", None) or getattr(item, "gross_amount", None) or taxable rows.append( f"
{html.escape(firm_name or _safe_text(getattr(invoice, 'firm_name', '') or 'Audit Firm'))}
Invoice No: {html.escape(invoice_no)}
Invoice Date: {html.escape(_safe_text(getattr(invoice, 'invoice_date', '')))}
Due Date: {html.escape(_safe_text(getattr(invoice, 'due_date', '')))}
{html.escape(_safe_text(_client_name(client)))}
| # | Description | SAC | Taxable | GST % | Total |
|---|
| Taxable Value | {html.escape(_money(getattr(invoice, 'taxable_value', None) or getattr(invoice, 'subtotal', None)))} |
|---|---|
| CGST | {html.escape(_money(getattr(invoice, 'cgst_amount', None) or getattr(invoice, 'cgst', None)))} |
| SGST | {html.escape(_money(getattr(invoice, 'sgst_amount', None) or getattr(invoice, 'sgst', None)))} |
| IGST | {html.escape(_money(getattr(invoice, 'igst_amount', None) or getattr(invoice, 'igst', None)))} |
| Total | {html.escape(_money(getattr(invoice, 'total_amount', None)))} |
| Outstanding | {html.escape(_money(getattr(invoice, 'balance_amount', None)))} |
This is an ERP-generated invoice attachment. For payment, please use the client portal payment link provided in the email.
{html.escape(firm_name or 'Audit Firm')}
| Receipt No | {html.escape(receipt_no)} |
|---|---|
| Receipt Date | {html.escape(_safe_text(getattr(payment, 'payment_date', None) or getattr(payment, 'receipt_date', None)))} |
| Client | {html.escape(_safe_text(_client_name(client)))} |
| Invoice No | {html.escape(_invoice_number(invoice) if invoice else '')} |
| Amount Received | {html.escape(_money(getattr(payment, 'amount_received', None) or getattr(payment, 'amount', None)))} |
| TDS Deducted | {html.escape(_money(getattr(payment, 'tds_amount', None) or getattr(payment, 'tds_deducted', None)))} |
| Bank Charges | {html.escape(_money(getattr(payment, 'bank_charges', None)))} |
| Payment Mode | {html.escape(_safe_text(getattr(payment, 'mode', None) or getattr(payment, 'payment_mode', None)))} |
| Reference | {html.escape(_safe_text(getattr(payment, 'reference_no', None) or getattr(payment, 'reference_number', None) or getattr(payment, 'utr_no', None)))} |
This is an ERP-generated receipt attachment.