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.
67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
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,
|
||
};
|
||
}
|