Quantities can now be time, count, distance, area, weight or volume, or a custom unit, instead of only
second/minute/hour/session/unit.
- Registry in src/lib/units.ts (mirrored in src-tauri/src/units.rs): grouped built-in units plus a custom unit of 1-12
characters. A rate line's unit is validated when an invoice or a preset is saved; migration M12 drops the old unit
allow-list from item_presets and keeps every row.
- Time is typed as h:mm: "4:30" hours counts as 4.5, "4:20" as 4.333... (kept exact, so rate x quantity rounds once, the
same in TypeScript and Rust). Other units take plain decimals up to 3 places. A bad quantity blocks issuing.
- The PDF quantity column shows the unit ("4.5 hr", "12.75 km"); the rate keeps "per hour" / "/hr". RenderItem carries
priceText, perText and rateShort, so no template parses the rate text with a regex over five hard-coded units.
- Quantity columns are sized from the widest word once a quantity with its unit is wider than 72 pt, so one long
custom unit cannot squeeze the description (the serenity and citrus-split tables overflowed in the new fixture).
Classic's quantity column is now content-sized.
- ERPNext: the default UOM map comes from the registry, with fractional-capable UOMs for time, distance, area, weight
and volume. A custom unit is sent as Nos. A quantity that is not exact to 3 decimals (4:20 hours) is still refused for
push, as before.
- Goldens and template thumbnails regenerated; the template harness has a "units" fixture with the longest texts.
Claude-Session: https://claude.ai/code/session_01PZypiWDfMkDTeEPeXjRhW5
155 lines
4.2 KiB
TypeScript
155 lines
4.2 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 id (see lib/units.ts) 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;
|
|
}
|