//! HTML rendering for the printable invoice. //! //! Renders a self-contained, print-friendly HTML document. The //! frontend uses this for the in-browser invoice view, and the same //! HTML is what would be saved as a PDF. use crate::{Invoice, InvoiceTotals, LineItem}; use chrono::{DateTime, Utc}; pub fn render_html( invoice: &Invoice, lines: &[LineItem], totals: &InvoiceTotals, ) -> String { let mut out = String::with_capacity(4096); out.push_str(r#" Invoice "#); out.push_str(&escape_html(&invoice.invoice_number)); out.push_str(r#"

Tax Invoice

"#); out.push_str(&escape_html(&invoice.invoice_number)); out.push_str("

\n
\n
\n "); out.push_str(&escape_html(&invoice.status)); out.push_str(r#"

Issued "#); out.push_str(&invoice.issued_at.format("%d %b %Y").to_string()); out.push_str("

\n
\n
\n\n
\n
\n

From

\n

"); out.push_str(&escape_html(&invoice.seller_name)); out.push_str("

\n

"); out.push_str(&escape_html(&invoice.seller_address)); out.push_str("

\n

"); if let Some(gstin) = &invoice.seller_gstin { out.push_str(&format!("GSTIN: {}
", escape_html(gstin))); } if let Some(pan) = &invoice.seller_pan { out.push_str(&format!("PAN: {}", escape_html(pan))); } out.push_str("

\n
\n
\n

Bill To

\n

"); out.push_str(&escape_html(invoice.customer_name.as_deref().unwrap_or("N/A"))); out.push_str("

\n

"); out.push_str(&escape_html( invoice.customer_billing_address.as_deref().unwrap_or("N/A"), )); out.push_str("

\n

"); if let Some(gstin) = &invoice.customer_gstin { out.push_str(&format!("GSTIN: {}
", escape_html(gstin))); } if let Some(state) = &invoice.place_of_supply_state { out.push_str(&format!("Place of Supply: {}", escape_html(state))); } out.push_str("

\n
\n
\n\n \n \n \n \n \n \n \n \n \n \n \n \n"); for line in lines { out.push_str(&format!( " \n \n \n \n \n \n \n \n", line.line_number, escape_html(&line.description), escape_html(line.hsn_sac_code.as_deref().unwrap_or("")), format_qty(line.quantity), format_inr(line.unit_price_paise), format_inr(line.subtotal_paise()), )); } out.push_str(" \n
#DescriptionHSN/SACQtyRateAmount
{}{}{}{}{}{}
\n\n
\n
Subtotal"); out.push_str(&format_inr(totals.subtotal)); out.push_str("
\n"); if totals.discount > 0 { out.push_str(&format!( "
Discount-{}
\n", format_inr(totals.discount), )); } out.push_str(&format!( "
Taxable value{}
\n", format_inr(totals.taxable_value), )); if totals.cgst > 0 { out.push_str(&format!( "
CGST{}
\n", format_inr(totals.cgst), )); out.push_str(&format!( "
SGST{}
\n", format_inr(totals.sgst), )); } if totals.igst > 0 { out.push_str(&format!( "
IGST{}
\n", format_inr(totals.igst), )); } out.push_str(&format!( "
Total ({}){}
\n", invoice.currency, format_inr(totals.total), )); out.push_str("
\n\n

\n This is a computer-generated invoice. No signature is required.\n

\n
\n\n\n"); out } fn escape_html(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } fn format_inr(paise: i64) -> String { let sign = if paise < 0 { "-" } else { "" }; let abs = paise.unsigned_abs(); let rupees = abs / 100; let paise = abs % 100; let rupees_str = { let s = rupees.to_string(); let mut out = String::new(); for (i, c) in s.chars().rev().enumerate() { if i != 0 && i % 3 == 0 { out.insert(0, ','); } out.insert(0, c); } out }; format!("{}₹{}.{:02}", sign, rupees_str, paise) } fn format_qty(q: f64) -> String { if (q.fract() - 0.0).abs() < 0.001 { format!("{}", q.trunc() as i64) } else { format!("{:.3}", q) } } #[allow(dead_code)] fn _today_anchor() -> DateTime { Utc::now() }