Files
Voiced/src/lib/invoice.ts
T
xavierk a00c816635 Add issue/cancel lifecycle, GST derivation and integer-paise totals
issue_invoice allocates the number, freezes vendor and bank details and
stores server-computed totals in one transaction. Issued invoices are
cancelled, not deleted. Tax heads derive from supplier state and place of
supply (a disagreeing choice is rejected); unregistered suppliers issue a
plain Invoice. Adds GSTIN checksum validation, canonical India Compliance
state list, server-side drafts, series validation, relative and
magic-byte-checked asset paths, and GST settings. PDF re-exports use the
frozen vendor snapshot.
2026-10-04 04:15:58 +05:30

67 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { InvoiceItem, TaxType } from "./types";
// The preview must show exactly what Rust stores (src-tauri/src/gst.rs), so money is
// computed in integer paise with round-half-up, the same way.
const toPaise = (rupees: number) => Math.round((Number.isFinite(rupees) ? rupees : 0) * 100);
function linePaise(item: InvoiceItem): number {
if (item.mode === "rate") {
return Math.round(toPaise(item.rate || 0) * (item.quantity || 0));
}
return toPaise(item.amount || 0);
}
/** Round-half-up division for non-negative integers. */
const divRound = (num: number, den: number) => Math.floor((num + den / 2) / den);
/** Amount for a single line: a fixed total, or rate × quantity. */
export function computeLineAmount(item: InvoiceItem): number {
return linePaise(item) / 100;
}
export interface Totals {
subtotal: number;
discount: number;
taxable: number;
cgst: number;
sgst: number;
igst: number;
taxTotal: number;
total: number;
}
export function computeTotals(
items: InvoiceItem[],
discount: number,
taxType: TaxType,
taxRate: number,
): Totals {
const subtotal = items.reduce((sum, item) => sum + linePaise(item), 0);
const discountPaise = Math.min(Math.max(toPaise(discount), 0), Math.max(subtotal, 0));
const taxable = subtotal - discountPaise;
const rateBp = Math.round((Number.isFinite(taxRate) ? taxRate : 0) * 100);
let cgst = 0;
let sgst = 0;
let igst = 0;
if (taxType === "cgst_sgst") {
cgst = divRound(taxable * rateBp, 20_000);
sgst = cgst;
} else if (taxType === "igst") {
igst = divRound(taxable * rateBp, 10_000);
}
const taxTotal = cgst + sgst + igst;
return {
subtotal: subtotal / 100,
discount: discountPaise / 100,
taxable: taxable / 100,
cgst: cgst / 100,
sgst: sgst / 100,
igst: igst / 100,
taxTotal: taxTotal / 100,
total: (taxable + taxTotal) / 100,
};
}