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:
@@ -3,6 +3,7 @@ import {
|
||||
Button,
|
||||
InlineLoading,
|
||||
InlineNotification,
|
||||
Modal,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -10,9 +11,10 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Tag,
|
||||
TextInput,
|
||||
} from "@carbon/react";
|
||||
import { Download, TrashCan } from "@carbon/icons-react";
|
||||
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||
import { Download, Close } from "@carbon/icons-react";
|
||||
import { api } from "../lib/api";
|
||||
import { formatAmount, formatDate } from "../lib/format";
|
||||
import { exportInvoicePdf } from "../lib/pdf";
|
||||
@@ -23,6 +25,8 @@ export default function InvoiceHistory({ settings }: { settings: Settings }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<number | null>(null);
|
||||
const [cancelTarget, setCancelTarget] = useState<InvoiceSummary | null>(null);
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
@@ -52,16 +56,15 @@ export default function InvoiceHistory({ settings }: { settings: Settings }) {
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (row: InvoiceSummary) => {
|
||||
const ok = await confirm(`Delete invoice ${row.number}? This cannot be undone.`, {
|
||||
title: "Delete invoice",
|
||||
kind: "warning",
|
||||
});
|
||||
if (!ok) return;
|
||||
const onCancel = async () => {
|
||||
if (!cancelTarget) return;
|
||||
try {
|
||||
await api.deleteInvoice(row.id);
|
||||
await api.cancelInvoice(cancelTarget.id, reason);
|
||||
setCancelTarget(null);
|
||||
setReason("");
|
||||
await load();
|
||||
} catch (e) {
|
||||
setCancelTarget(null);
|
||||
setError(String(e));
|
||||
}
|
||||
};
|
||||
@@ -99,7 +102,15 @@ export default function InvoiceHistory({ settings }: { settings: Settings }) {
|
||||
<TableCell>{formatDate(row.invoiceDate)}</TableCell>
|
||||
<TableCell>{row.clientName || "—"}</TableCell>
|
||||
<TableCell>₹{formatAmount(row.total)}</TableCell>
|
||||
<TableCell>{row.status}</TableCell>
|
||||
<TableCell>
|
||||
{row.status === "cancelled" ? (
|
||||
<Tag type="red" size="sm">
|
||||
Cancelled
|
||||
</Tag>
|
||||
) : (
|
||||
row.status
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="voiced-header-action">
|
||||
<Button
|
||||
@@ -111,14 +122,19 @@ export default function InvoiceHistory({ settings }: { settings: Settings }) {
|
||||
>
|
||||
{busyId === row.id ? "Exporting…" : "PDF"}
|
||||
</Button>
|
||||
<Button
|
||||
kind="ghost"
|
||||
size="sm"
|
||||
renderIcon={TrashCan}
|
||||
onClick={() => onDelete(row)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{row.status === "issued" ? (
|
||||
<Button
|
||||
kind="ghost"
|
||||
size="sm"
|
||||
renderIcon={Close}
|
||||
onClick={() => {
|
||||
setReason("");
|
||||
setCancelTarget(row);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -127,6 +143,26 @@ export default function InvoiceHistory({ settings }: { settings: Settings }) {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={cancelTarget !== null}
|
||||
danger
|
||||
modalHeading={`Cancel invoice ${cancelTarget?.number ?? ""}?`}
|
||||
primaryButtonText="Cancel invoice"
|
||||
secondaryButtonText="Keep invoice"
|
||||
onRequestClose={() => setCancelTarget(null)}
|
||||
onRequestSubmit={onCancel}
|
||||
>
|
||||
<p style={{ marginBottom: "1rem" }}>
|
||||
The invoice stays in the list as cancelled and its number is not reused.
|
||||
</p>
|
||||
<TextInput
|
||||
id="cancelReason"
|
||||
labelText="Reason (optional)"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user