- 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.
90 lines
2.7 KiB
Rust
90 lines
2.7 KiB
Rust
//! Outbound integrations. `InvoiceSink` is the seam between Voiced and a system that receives issued
|
|
//! invoices (ERPNext now; a webhook, Zoho or a Tally import later). Everything target-specific lives in a
|
|
//! submodule; the rest of the app only talks to this trait.
|
|
|
|
// Parts of the sink API are only used by the later UI step.
|
|
#![allow(dead_code)]
|
|
|
|
pub mod erpnext;
|
|
|
|
use serde::Serialize;
|
|
use std::future::Future;
|
|
|
|
/// What a sink reports after accepting an invoice.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct PushedInvoice {
|
|
/// The document name on the remote system.
|
|
pub remote_name: String,
|
|
/// 0 draft, 1 submitted.
|
|
pub remote_docstatus: i64,
|
|
/// False when the remote already had this invoice (an idempotent re-push).
|
|
pub created: bool,
|
|
/// Non-fatal notes, e.g. "pushed without the PDF because the invoice is not archived".
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
pub struct PushRequest {
|
|
pub invoice_id: i64,
|
|
/// Submit on the remote system instead of leaving a draft for review; `None` follows the sink's setting.
|
|
pub submit: Option<bool>,
|
|
}
|
|
|
|
pub struct PaymentRequest {
|
|
pub payment_id: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct PushedPayment {
|
|
pub remote_name: String,
|
|
/// False when the payment had already been sent (an idempotent re-push).
|
|
pub created: bool,
|
|
}
|
|
|
|
/// The locally recorded sync state of one invoice. `status` is `none` until something was pushed, then
|
|
/// `synced`, `error` or `conflict`.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SyncStatus {
|
|
pub invoice_id: i64,
|
|
pub status: String,
|
|
pub remote_name: String,
|
|
/// 0 draft, 1 submitted.
|
|
pub remote_docstatus: i64,
|
|
pub last_error: String,
|
|
pub synced_at: Option<String>,
|
|
/// The archived PDF was uploaded to the remote document.
|
|
pub attached: bool,
|
|
}
|
|
|
|
impl SyncStatus {
|
|
pub fn none(invoice_id: i64) -> Self {
|
|
SyncStatus {
|
|
invoice_id,
|
|
status: "none".into(),
|
|
remote_name: String::new(),
|
|
remote_docstatus: 0,
|
|
last_error: String::new(),
|
|
synced_at: None,
|
|
attached: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait InvoiceSink: Send + Sync {
|
|
/// Stable identifier used in the sync table (`erpnext`, ...).
|
|
fn id(&self) -> &'static str;
|
|
|
|
fn push_invoice<'a>(
|
|
&'a self,
|
|
request: PushRequest,
|
|
) -> impl Future<Output = Result<PushedInvoice, String>> + Send + 'a;
|
|
|
|
fn push_payment<'a>(
|
|
&'a self,
|
|
request: PaymentRequest,
|
|
) -> impl Future<Output = Result<PushedPayment, String>> + Send + 'a;
|
|
|
|
/// The locally recorded state; never touches the network.
|
|
fn status(&self, invoice_id: i64) -> Result<SyncStatus, String>;
|
|
}
|