Add ERPNext push logic for invoices and payments (Phase F2)
- ErpnextSink implements InvoiceSink. Pushing an issued invoice ensures the Customer and Address (read-back of the returned name, local PIN/state/GSTIN checks), creates the Sales Invoice, optionally submits it in a second call, and attaches the archived PDF as a private file once. - Idempotent: a mirrored-name 409 is compared by grand total (same total records as synced, different total is a conflict and never overwrites); series mode looks up an existing document by its remarks before re-posting; an identical re-push sends nothing. - Create and submit are separate calls so a draft survives a failed submit. A failed push is recorded in erpnext_sync with a readable message. Bulk push never aborts on one failing row. - Payment push requires a submitted parent, maps TDS to Payment Entry deductions and is idempotent through the stored entry name. - Migration M11 rebuilds erpnext_sync to allow a conflict status and adds attachment_sha256. - Commands for single and bulk push, payment push, sync status and the open-in-ERPNext URL, with typed wrappers. No UI yet. Tested against a mock server only. The Payment Entry deductions fields, the series-mode submit route and the payment amount semantics need a live ERPNext check.
This commit is contained in:
@@ -88,3 +88,67 @@ export interface ErpnextOptions {
|
||||
/** 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user