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:
+101
-33
@@ -23,8 +23,14 @@ import {
|
||||
import { Add, Save, TrashCan } from "@carbon/icons-react";
|
||||
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||
import { api } from "../lib/api";
|
||||
import { INDIAN_STATES, type BankAccount, type Settings, type TaxType } from "../lib/types";
|
||||
import { emailError, ifscError, panError, phoneError } from "../lib/validators";
|
||||
import {
|
||||
INDIAN_STATES,
|
||||
type BankAccount,
|
||||
type GstRegistration,
|
||||
type Settings,
|
||||
type TaxType,
|
||||
} from "../lib/types";
|
||||
import { emailError, ifscError, panError, phoneError, vendorGstinError } from "../lib/validators";
|
||||
import { ImagePicker } from "../components/ImagePicker";
|
||||
|
||||
const blankBank = (): BankAccount => ({
|
||||
@@ -70,7 +76,19 @@ export default function AppSettings({
|
||||
const setBank = <K extends keyof BankAccount>(key: K, value: BankAccount[K]) =>
|
||||
setEditing((prev) => (prev ? { ...prev, [key]: value } : prev));
|
||||
|
||||
const registered = form.gstRegistration !== "unregistered";
|
||||
const gstinProblem = vendorGstinError(
|
||||
form.gstRegistration,
|
||||
form.vendorGstin,
|
||||
form.vendorPan,
|
||||
form.vendorStateCode,
|
||||
);
|
||||
|
||||
const saveAll = async () => {
|
||||
if (gstinProblem) {
|
||||
setError(gstinProblem);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
@@ -140,6 +158,22 @@ export default function AppSettings({
|
||||
<section className="voiced-stack" style={{ marginBottom: "2.5rem" }}>
|
||||
<h3>Vendor details</h3>
|
||||
<Grid narrow>
|
||||
<Column sm={4} md={8} lg={12}>
|
||||
<RadioButtonGroup
|
||||
legendText="GST registration"
|
||||
name="s-gstReg"
|
||||
orientation="horizontal"
|
||||
valueSelected={form.gstRegistration}
|
||||
onChange={(value) => {
|
||||
set("gstRegistration", value as GstRegistration);
|
||||
if (value === "unregistered") set("defaultTaxType", "none");
|
||||
else if (form.defaultTaxType === "none") set("defaultTaxType", "cgst_sgst");
|
||||
}}
|
||||
>
|
||||
<RadioButton id="s-gr-un" labelText="Unregistered" value="unregistered" />
|
||||
<RadioButton id="s-gr-reg" labelText="Regular" value="regular" />
|
||||
</RadioButtonGroup>
|
||||
</Column>
|
||||
<Column sm={4} md={8} lg={12}>
|
||||
<TextInput
|
||||
id="s-vendorName"
|
||||
@@ -187,14 +221,18 @@ export default function AppSettings({
|
||||
invalidText={panError(form.vendorPan)}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={6}>
|
||||
<TextInput
|
||||
id="s-vendorGstin"
|
||||
labelText="GSTIN (blank if unregistered)"
|
||||
value={form.vendorGstin}
|
||||
onChange={(e) => set("vendorGstin", e.target.value.toUpperCase())}
|
||||
/>
|
||||
</Column>
|
||||
{registered ? (
|
||||
<Column sm={4} md={4} lg={6}>
|
||||
<TextInput
|
||||
id="s-vendorGstin"
|
||||
labelText="GSTIN"
|
||||
value={form.vendorGstin}
|
||||
onChange={(e) => set("vendorGstin", e.target.value.toUpperCase())}
|
||||
invalid={Boolean(gstinProblem)}
|
||||
invalidText={gstinProblem}
|
||||
/>
|
||||
</Column>
|
||||
) : null}
|
||||
<Column sm={4} md={8} lg={12}>
|
||||
<Select
|
||||
id="s-vendorState"
|
||||
@@ -207,6 +245,22 @@ export default function AppSettings({
|
||||
))}
|
||||
</Select>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={6}>
|
||||
<TextInput
|
||||
id="s-signatoryName"
|
||||
labelText="Authorised signatory name"
|
||||
value={form.signatoryName}
|
||||
onChange={(e) => set("signatoryName", e.target.value)}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={6}>
|
||||
<TextInput
|
||||
id="s-signatoryDesignation"
|
||||
labelText="Signatory designation"
|
||||
value={form.signatoryDesignation}
|
||||
onChange={(e) => set("signatoryDesignation", e.target.value)}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={8} lg={8}>
|
||||
<ImagePicker
|
||||
kind="logo"
|
||||
@@ -229,17 +283,29 @@ export default function AppSettings({
|
||||
<section className="voiced-stack" style={{ marginBottom: "2.5rem" }}>
|
||||
<h3>Defaults</h3>
|
||||
<Grid narrow>
|
||||
<Column sm={4} md={4} lg={5}>
|
||||
<NumberInput
|
||||
id="s-taxRate"
|
||||
label="Default tax rate (%)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.5}
|
||||
value={form.defaultTaxRate}
|
||||
onChange={(_e, { value }) => set("defaultTaxRate", Number(value) || 0)}
|
||||
/>
|
||||
</Column>
|
||||
{registered ? (
|
||||
<>
|
||||
<Column sm={4} md={4} lg={5}>
|
||||
<NumberInput
|
||||
id="s-taxRate"
|
||||
label="Default tax rate (%)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.5}
|
||||
value={form.defaultTaxRate}
|
||||
onChange={(_e, { value }) => set("defaultTaxRate", Number(value) || 0)}
|
||||
/>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={5}>
|
||||
<TextInput
|
||||
id="s-hsn"
|
||||
labelText="Default HSN/SAC"
|
||||
value={form.defaultHsnSac}
|
||||
onChange={(e) => set("defaultHsnSac", e.target.value)}
|
||||
/>
|
||||
</Column>
|
||||
</>
|
||||
) : null}
|
||||
<Column sm={4} md={4} lg={5}>
|
||||
<NumberInput
|
||||
id="s-terms"
|
||||
@@ -261,18 +327,20 @@ export default function AppSettings({
|
||||
<SelectItem value="g100" text="Carbon dark (g100)" />
|
||||
</Select>
|
||||
</Column>
|
||||
<Column sm={4} md={8} lg={16}>
|
||||
<RadioButtonGroup
|
||||
legendText="Default tax treatment"
|
||||
name="s-taxType"
|
||||
valueSelected={form.defaultTaxType}
|
||||
onChange={(value) => set("defaultTaxType", value as TaxType)}
|
||||
>
|
||||
<RadioButton id="s-tt-cgst" labelText="CGST + SGST (within state)" value="cgst_sgst" />
|
||||
<RadioButton id="s-tt-igst" labelText="IGST (inter-state)" value="igst" />
|
||||
<RadioButton id="s-tt-none" labelText="No tax" value="none" />
|
||||
</RadioButtonGroup>
|
||||
</Column>
|
||||
{registered ? (
|
||||
<Column sm={4} md={8} lg={16}>
|
||||
<RadioButtonGroup
|
||||
legendText="Default tax treatment"
|
||||
name="s-taxType"
|
||||
valueSelected={form.defaultTaxType}
|
||||
onChange={(value) => set("defaultTaxType", value as TaxType)}
|
||||
>
|
||||
<RadioButton id="s-tt-cgst" labelText="CGST + SGST (within state)" value="cgst_sgst" />
|
||||
<RadioButton id="s-tt-igst" labelText="IGST (inter-state)" value="igst" />
|
||||
<RadioButton id="s-tt-none" labelText="No tax" value="none" />
|
||||
</RadioButtonGroup>
|
||||
</Column>
|
||||
) : null}
|
||||
</Grid>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user