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.
This commit is contained in:
@@ -323,6 +323,14 @@ fn load_item_codes(conn: &Connection, inv: &Invoice) -> Result<Vec<Option<String
|
||||
Ok(inv.items.iter().map(|i| by_desc.get(&i.description.trim().to_lowercase()).cloned()).collect())
|
||||
}
|
||||
|
||||
/// True when a connection test result is stored (blank, `null` and `{}` count as none).
|
||||
fn has_detect_result(raw: &str) -> bool {
|
||||
match serde_json::from_str::<Value>(raw) {
|
||||
Ok(Value::Object(m)) => !m.is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_for_push(db: &Db, local_dir: &Path, invoice_id: i64) -> Result<Loaded, (String, ErpError)> {
|
||||
let conn = db
|
||||
.lock()
|
||||
@@ -346,6 +354,11 @@ fn load_for_push(db: &Db, local_dir: &Path, invoice_id: i64) -> Result<Loaded, (
|
||||
let cfg = config::load(&conn).map_err(|e| fail(pre(e)))?;
|
||||
let vendor = Vendor::from_snapshot(&invoice.vendor_snapshot)
|
||||
.ok_or_else(|| fail(pre(format!("Invoice {number} has no supplier details recorded, so it cannot be sent."))))?;
|
||||
// A registered vendor's invoices go out with India Compliance fields, and those depend on what the
|
||||
// connection test detected, so refuse until the test has run once.
|
||||
if vendor.registered && !has_detect_result(&cfg.last_detect_result) {
|
||||
return Err(fail(pre("Run Test connection first (Settings, Integrations): Voiced has not yet checked this ERPNext site.")));
|
||||
}
|
||||
let client = load_client(&conn, &invoice).map_err(|e| fail(pre(e)))?;
|
||||
let item_codes = load_item_codes(&conn, &invoice).map_err(|e| fail(pre(e)))?;
|
||||
let sync = load_sync(&conn, invoice_id).map_err(|e| fail(pre(e)))?;
|
||||
|
||||
Reference in New Issue
Block a user