Files
Voiced/src/lib/erpnext.ts
T
xavierk ff29748f4e Add the ERPNext settings UI, History and detail controls, and voiced.invoice.v1 export (Phase F3)
- Settings gets an Integrations tab: connection fields with a write-only API secret, a CA PEM option, Test connection with versions and warnings, live mapping lists loaded after a test, naming mode, toggles and its own Save.
- Pushing for a GST-registered vendor is refused until a connection test result is stored. The UI shows the reason and disables Send.
- History shows an ERPNext status column from one bulk call, a row Send and Open in ERPNext, and a bulk Send selected with a draft-or-submit confirm step and per-row results that never abort the batch. The detail view gets an ERPNext panel and per-payment send. All of it is hidden or disabled when the integration is not configured.
- Optional auto-push after issue runs in the background and never blocks or fails the issue.
- Generic export: a deterministic voiced.invoice.v1 JSON (and CSV line items) for selected invoices, documented in docs/voiced-invoice-v1.md with a golden test. Money is integer paise plus a decimal string.

The new screens have not been run in a webview yet. The ERPNext Payment Entry deduction fields still need a live check.
2026-10-04 20:57:31 +05:30

155 lines
4.3 KiB
TypeScript

/** Types for the ERPNext commands (see src-tauri/src/commands/erpnext.rs); the UI helpers live in erpnextUi.ts. */
export type ErpnextNamingMode = "mirror" | "series";
/** What the backend returns. The API secret is never included: `apiSecretSet` says whether one is stored. */
export interface ErpnextConfig {
baseUrl: string;
apiKey: string;
apiSecretSet: boolean;
extraCaPem: string;
company: string;
companyAddress: string;
namingMode: ErpnextNamingMode;
namingSeries: string;
incomeAccount: string;
costCenter: string;
cgstAccount: string;
sgstAccount: string;
utgstAccount: string;
igstAccount: string;
taxTemplateIntra: string;
taxTemplateInter: string;
paymentBankAccount: string;
tdsAccount: string;
defaultItemCode: string;
/** Voiced unit (second, minute, hour, session, unit) to ERPNext UOM. */
uomMap: Record<string, string>;
customerGroup: string;
territory: string;
sellingPriceList: string;
submitOnPush: boolean;
attachPdf: boolean;
autoPushOnIssue: boolean;
createMissingCustomers: boolean;
/** The last connection test, or null if none was stored. */
lastDetectResult: ErpnextConnectionTest | null;
}
/**
* What the webview sends to save or test. A blank `apiSecret` keeps the stored secret, a value replaces it,
* and `clearSecret` removes it. `lastDetectResult` omitted keeps the stored one.
*/
export type ErpnextConfigInput = Omit<ErpnextConfig, "apiSecretSet" | "lastDetectResult"> & {
apiSecret: string;
clearSecret?: boolean;
lastDetectResult?: ErpnextConnectionTest | null;
};
export interface ErpnextWarning {
code: string;
message: string;
}
export interface ErpnextVersions {
frappe: string | null;
erpnext: string | null;
indiaCompliance: string | null;
}
export interface ErpnextConnectionTest {
user: string;
versions: ErpnextVersions;
features: { v2Naming: boolean; indiaCompliance: boolean };
indiaCompliance: boolean;
warnings: ErpnextWarning[];
}
export interface ErpnextOptionItem {
/** The value to store (the document name). */
name: string;
label: string;
detail: string;
}
export interface ErpnextOptions {
companies: ErpnextOptionItem[];
companyAddresses: ErpnextOptionItem[];
incomeAccounts: ErpnextOptionItem[];
taxAccounts: ErpnextOptionItem[];
taxTemplates: ErpnextOptionItem[];
namingSeries: ErpnextOptionItem[];
itemGroups: ErpnextOptionItem[];
uoms: ErpnextOptionItem[];
costCenters: ErpnextOptionItem[];
priceLists: ErpnextOptionItem[];
customerGroups: ErpnextOptionItem[];
territories: ErpnextOptionItem[];
/** Lists that failed to load; the others are still filled. */
errors: { list: string; message: string }[];
}
export type ErpnextErrorKind =
| "auth"
| "permission"
| "not_found"
| "duplicate"
| "validation"
| "rate_limit"
| "server"
| "timeout"
| "network"
| "config"
| "protocol"
| "conflict"
| "precondition"
| "other";
/** `refused`: nothing was sent and nothing was recorded (a draft or cancelled invoice). */
export type ErpnextPushStatus = "synced" | "error" | "conflict" | "refused";
export interface ErpnextPushResult {
invoiceId: number;
number: string;
ok: boolean;
status: ErpnextPushStatus;
remoteName: string;
/** 0 draft, 1 submitted. */
remoteDocstatus: number;
/** Created by this call, not found already there. */
created: boolean;
/** Already synced with the same payload; nothing was done. */
noOp: boolean;
/** The archived PDF is attached on the remote document. */
attached: boolean;
error: string | null;
errorKind: ErpnextErrorKind | null;
/** Non-fatal notes, e.g. sent without the PDF because the invoice is not archived. */
warnings: string[];
}
export interface ErpnextPaymentPushResult {
paymentId: number;
invoiceId: number;
ok: boolean;
entryName: string | null;
/** Sent earlier; nothing was posted. */
alreadySynced: boolean;
error: string | null;
errorKind: ErpnextErrorKind | null;
}
/** `none` until something was pushed. */
export type ErpnextSyncState = "none" | "pending" | "synced" | "error" | "conflict";
export interface ErpnextSyncStatus {
invoiceId: number;
status: ErpnextSyncState;
remoteName: string;
/** 0 draft, 1 submitted. */
remoteDocstatus: number;
lastError: string;
syncedAt: string | null;
attached: boolean;
}