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.
This commit is contained in:
+32
-20
@@ -1,12 +1,23 @@
|
||||
import { round2 } from "./format";
|
||||
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 {
|
||||
if (item.mode === "rate") {
|
||||
return round2((item.rate || 0) * (item.quantity || 0));
|
||||
}
|
||||
return round2(item.amount || 0);
|
||||
return linePaise(item) / 100;
|
||||
}
|
||||
|
||||
export interface Totals {
|
||||
@@ -26,29 +37,30 @@ export function computeTotals(
|
||||
taxType: TaxType,
|
||||
taxRate: number,
|
||||
): Totals {
|
||||
const subtotal = round2(items.reduce((sum, item) => sum + computeLineAmount(item), 0));
|
||||
const discountValue = round2(Math.min(Math.max(discount || 0, 0), subtotal));
|
||||
const taxable = round2(subtotal - discountValue);
|
||||
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 = round2((taxable * (taxRate / 2)) / 100);
|
||||
sgst = round2((taxable * (taxRate / 2)) / 100);
|
||||
cgst = divRound(taxable * rateBp, 20_000);
|
||||
sgst = cgst;
|
||||
} else if (taxType === "igst") {
|
||||
igst = round2((taxable * taxRate) / 100);
|
||||
igst = divRound(taxable * rateBp, 10_000);
|
||||
}
|
||||
const taxTotal = round2(cgst + sgst + igst);
|
||||
const taxTotal = cgst + sgst + igst;
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
discount: discountValue,
|
||||
taxable,
|
||||
cgst,
|
||||
sgst,
|
||||
igst,
|
||||
taxTotal,
|
||||
total: round2(taxable + taxTotal),
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user