Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
@@ -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",
)