Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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"<tr><td>{index}</td><td>{html.escape(_safe_text(desc))}</td><td>{html.escape(_safe_text(sac))}</td>"
|
||||
f"<td style='text-align:right'>{html.escape(_money(taxable))}</td>"
|
||||
f"<td style='text-align:right'>{html.escape(_safe_text(gst_rate))}</td>"
|
||||
f"<td style='text-align:right'>{html.escape(_money(total))}</td></tr>"
|
||||
)
|
||||
if not rows:
|
||||
rows.append("<tr><td>1</td><td>Professional Fees</td><td></td><td style='text-align:right'></td><td></td><td style='text-align:right'></td></tr>")
|
||||
|
||||
return f"""<!doctype html>
|
||||
<html><head><meta charset='utf-8'><title>Invoice {html.escape(invoice_no)}</title>
|
||||
<style>body{{font-family:Arial,sans-serif;color:#111827}}.box{{border:1px solid #d1d5db;border-radius:10px;padding:18px;max-width:900px;margin:0 auto}}table{{width:100%;border-collapse:collapse;margin-top:14px}}th,td{{border:1px solid #d1d5db;padding:8px;font-size:13px}}th{{background:#f3f4f6;text-align:left}}.right{{text-align:right}}.muted{{color:#6b7280}}</style></head>
|
||||
<body><div class='box'>
|
||||
<h2>Tax Invoice</h2>
|
||||
<p><strong>{html.escape(firm_name or _safe_text(getattr(invoice, 'firm_name', '') or 'Audit Firm'))}</strong></p>
|
||||
<p class='muted'>Invoice No: <strong>{html.escape(invoice_no)}</strong><br>Invoice Date: {html.escape(_safe_text(getattr(invoice, 'invoice_date', '')))}<br>Due Date: {html.escape(_safe_text(getattr(invoice, 'due_date', '')))}</p>
|
||||
<h3>Bill To</h3><p>{html.escape(_safe_text(_client_name(client)))}</p>
|
||||
<table><thead><tr><th>#</th><th>Description</th><th>SAC</th><th class='right'>Taxable</th><th class='right'>GST %</th><th class='right'>Total</th></tr></thead><tbody>{''.join(rows)}</tbody></table>
|
||||
<table><tbody>
|
||||
<tr><th>Taxable Value</th><td class='right'>{html.escape(_money(getattr(invoice, 'taxable_value', None) or getattr(invoice, 'subtotal', None)))}</td></tr>
|
||||
<tr><th>CGST</th><td class='right'>{html.escape(_money(getattr(invoice, 'cgst_amount', None) or getattr(invoice, 'cgst', None)))}</td></tr>
|
||||
<tr><th>SGST</th><td class='right'>{html.escape(_money(getattr(invoice, 'sgst_amount', None) or getattr(invoice, 'sgst', None)))}</td></tr>
|
||||
<tr><th>IGST</th><td class='right'>{html.escape(_money(getattr(invoice, 'igst_amount', None) or getattr(invoice, 'igst', None)))}</td></tr>
|
||||
<tr><th>Total</th><td class='right'><strong>{html.escape(_money(getattr(invoice, 'total_amount', None)))}</strong></td></tr>
|
||||
<tr><th>Outstanding</th><td class='right'>{html.escape(_money(getattr(invoice, 'balance_amount', None)))}</td></tr>
|
||||
</tbody></table>
|
||||
<p class='muted'>This is an ERP-generated invoice attachment. For payment, please use the client portal payment link provided in the email.</p>
|
||||
</div></body></html>"""
|
||||
|
||||
|
||||
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"""<!doctype html>
|
||||
<html><head><meta charset='utf-8'><title>Receipt {html.escape(receipt_no)}</title>
|
||||
<style>body{{font-family:Arial,sans-serif;color:#111827}}.box{{border:1px solid #d1d5db;border-radius:10px;padding:18px;max-width:760px;margin:0 auto}}table{{width:100%;border-collapse:collapse;margin-top:14px}}th,td{{border:1px solid #d1d5db;padding:8px;font-size:13px}}th{{background:#f3f4f6;text-align:left}}.right{{text-align:right}}.muted{{color:#6b7280}}</style></head>
|
||||
<body><div class='box'>
|
||||
<h2>Payment Receipt</h2>
|
||||
<p><strong>{html.escape(firm_name or 'Audit Firm')}</strong></p>
|
||||
<table><tbody>
|
||||
<tr><th>Receipt No</th><td>{html.escape(receipt_no)}</td></tr>
|
||||
<tr><th>Receipt Date</th><td>{html.escape(_safe_text(getattr(payment, 'payment_date', None) or getattr(payment, 'receipt_date', None)))}</td></tr>
|
||||
<tr><th>Client</th><td>{html.escape(_safe_text(_client_name(client)))}</td></tr>
|
||||
<tr><th>Invoice No</th><td>{html.escape(_invoice_number(invoice) if invoice else '')}</td></tr>
|
||||
<tr><th>Amount Received</th><td class='right'><strong>{html.escape(_money(getattr(payment, 'amount_received', None) or getattr(payment, 'amount', None)))}</strong></td></tr>
|
||||
<tr><th>TDS Deducted</th><td class='right'>{html.escape(_money(getattr(payment, 'tds_amount', None) or getattr(payment, 'tds_deducted', None)))}</td></tr>
|
||||
<tr><th>Bank Charges</th><td class='right'>{html.escape(_money(getattr(payment, 'bank_charges', None)))}</td></tr>
|
||||
<tr><th>Payment Mode</th><td>{html.escape(_safe_text(getattr(payment, 'mode', None) or getattr(payment, 'payment_mode', None)))}</td></tr>
|
||||
<tr><th>Reference</th><td>{html.escape(_safe_text(getattr(payment, 'reference_no', None) or getattr(payment, 'reference_number', None) or getattr(payment, 'utr_no', None)))}</td></tr>
|
||||
</tbody></table>
|
||||
<p class='muted'>This is an ERP-generated receipt attachment.</p>
|
||||
</div></body></html>"""
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
Reference in New Issue
Block a user