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"{index}{html.escape(_safe_text(desc))}{html.escape(_safe_text(sac))}" f"{html.escape(_money(taxable))}" f"{html.escape(_safe_text(gst_rate))}" f"{html.escape(_money(total))}" ) if not rows: rows.append("1Professional Fees") return f""" Invoice {html.escape(invoice_no)}

Tax Invoice

{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', '')))}

Bill To

{html.escape(_safe_text(_client_name(client)))}

{''.join(rows)}
#DescriptionSACTaxableGST %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.

""" def build_receipt_html(payment: Any, *, firm_name: str = "") -> str: invoice = getattr(payment, "invoice", None) client = getattr(payment, "client", None) or getattr(invoice, "client", None) receipt_no = _safe_text(getattr(payment, "receipt_no", None) or getattr(payment, "receipt_number", None) or getattr(payment, "id", "receipt")) return f""" Receipt {html.escape(receipt_no)}

Payment Receipt

{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.

""" def invoice_attachment(invoice: Any, *, firm_name: str = "") -> EmailAttachment: invoice_no = _safe_filename(_invoice_number(invoice), "invoice") return EmailAttachment( filename=f"Invoice_{invoice_no}.html", content=build_invoice_html(invoice, firm_name=firm_name).encode("utf-8"), content_type="text/html", ) def receipt_attachment(payment: Any, *, firm_name: str = "") -> EmailAttachment: receipt_no = _safe_filename(_safe_text(getattr(payment, "receipt_no", None) or getattr(payment, "receipt_number", None) or getattr(payment, "id", "receipt")), "receipt") return EmailAttachment( filename=f"Receipt_{receipt_no}.html", content=build_receipt_html(payment, firm_name=firm_name).encode("utf-8"), content_type="text/html", )