Jobs: reports, email and links
Edit the invoice selected through a job
/a/Job_invoiceUpdate
Edit the active invoice selected through a job, creating one first when no active invoice is found.
When to use this endpoint
Use for the legacy job invoice/table editor’s autosave. id is a job ID, not an invoice ID. When a job is linked to a shared invoice, header changes affect that invoice for every linked job.
Read the full invoice and preserve its other rows before replacing rows. This route is not a per-job append or row-level patch API.
Authentication and permissions
Requires signed API-key or CRM session and the authenticated tenant. crm_access bit 64 returns This user does not have access to certain CRM features. Check user permissions.
The helper selects an active invoice but the handler does not independently require an active job when an invoice is already linked. Normal dispatcher user/account/IP restrictions and session CSRF apply.
The dispatcher also applies user/account and IP restrictions. CRM sessions carrying a CSRF token must supply X-FMCSRF or _csrf; missing or incorrect tokens can be rejected before the handler runs.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | Integer string | Yes | Positive physical job ID used for invoice selection. |
pandp | Numeric string | No | Omitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected. |
discount | Numeric string | No | Omitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected. |
discount_vat | Numeric string | No | Omitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected. |
pandp_vat | Numeric string | No | Omitted preserves the current value. Present charge/discount values must be finite numeric scalars; blank and malformed values are rejected. |
rows | Native array or JSON array string | No | Omitted preserves loaded rows. A valid native/JSON list replaces the complete invoice rows, not only this job’s rows. [] explicitly clears; blank, malformed, non-list and non-row data are rejected before writes. See row format below. |
doc_no | String | No | Presence writes the supplied value, including blank; omitted preserves it. |
order_no | String | No | Same presence semantics as doc_no. |
invoice_number | String | No | Truthy input trimmed into num; blank or '0' does not clear it. No uniqueness validation here. |
notes | String | No | If present, trims and replaces notes; blank clears. |
cdate | Date string | No | Truthy value; DD-MM-YYYY pattern is rewritten to YYYY-MM-DD, other text passed through. No strict calendar validation. |
due_date | Date/time string | No | Truthy input passed to strtotime in PHP timezone; not an epoch-input contract. Blank cannot clear, and invalid truthy input is not safely rejected. |
from_date | Date string | No | Truthy input stored as meta.invoice_from after DD-MM-YYYY rewrite; blank does not clear. |
to_date | Date string | No | Same semantics, stored as meta.invoice_to. |
nominal_code | String | No | Nonblank trimmed accounting code. Resolves the corresponding local meta.nominalID; a valid conflicting nominalID/nominal_id is rejected. |
nominalID / nominal_id | Integer string | No | Local account ID; nominalID takes precedence if both aliases are supplied. May be supplied alone to resolve the accounting nominal_code. A conflicting valid code/ID pair is rejected. |
tax_code_id | Integer string | No | Presence integer-casts and replaces tax_code_id, including 0; no compatibility validation. |
form_id | Integer string | No | Default 0 preserves form. Nonzero replaces the form ID without validating its type, existence or archive state. |
Example request
$result = FM_api('Job_invoiceUpdate', [
'id' => '1042',
'order_no' => 'PO-1042',
'notes' => 'Inspection invoice',
]);
Example response
{"ok":1,"num":"INV-501","id":"501"}
Changed saves return the invoice number and ID. No-change returns {"ok":1,"msg":"no update needed"} without num/id. Client code must handle both shapes; IDs are typically database strings.
Errors and unusual responses
| Response | Condition |
|---|---|
{"error":"not logged in"} | No authenticated tenant context is available. Invalid signed credentials can be rejected by the dispatcher before the handler runs. |
{"error":"This user does not have access to certain CRM features. Check user permissions."} | User crm_access bit 64 is set. |
{"error":"missing or invalid parameter: id"} | Missing/nonpositive job ID. |
{"error":"failed to create invoice","api_error":{"error":"Job 1042 does not exist"}} | Auto-generation did not return an invoice id; api_error preserves its response. |
{"error":"cannot edit invoices that are marked Paid"} | Selected invoice status is exactly 2; other statuses are not blocked by this guard. |
{"error":"Unable to save job delivery changes"} | A checked database write, audit or transaction failed; local transactional writes roll back. Cross-tenant files/commits are not distributed-atomic. |
{"error":"invalid parameter: rows"} | Present rows are malformed, not a list or contain non-row/empty values; rejected before writes. |
Legacy application errors may be returned with HTTP 200. Check the decoded response for error, not only the HTTP status.
Behaviour and edge cases
- Selects the first active type-3 invoice without ordering, then an active legacy-linked invoice. With neither, calls Job_invoiceGenerate. No transaction encloses creation and subsequent edits.
- Rows normalize through Invoice_lineItems: named fields include description, price, qty, net, taxpc, gross, nominal and itemcode; legacy positional rows are [description, price, qty, net, taxpc, gross, nominal, itemcode, taxtype]. Shared row helpers normalize tax/nominal aliases and remove empty rows. JSON-encode nested rows before signing with FM_api.
- Row normalization fills missing net/netPrice from price times qty, resolves supplied tax IDs against tenant tax categories, and can replace taxpc and recalculate gross when that rate differs. Existing discountOverall participates in gross calculation; do not assume supplied gross is always retained. It also reconciles legacy nominal aliases to account ID/code fields.
- With rows or any charge/tax adjustment supplied, recalculates invoice total_net/total from normalized net/gross rows, adds pandp/pandp_vat and subtracts discount/discount_vat. Without these inputs, preserves current totals.
- All four charge/discount fields are independently compared, so offsetting changes that leave totals unchanged still persist.
- Header, replacement rows, this job’s subtotal and history commit together in a checked transaction. Row storage errors roll back the header. This legacy route remains a whole-invoice row replacement, not a per-job cost save.
- Changed saves recompute only this job’s row-owned price/price_net. Rows with explicit job_id/jobID count for that job; unowned rows count only if the invoice has one linked job. Header charges/discounts are not allocated into that job subtotal.
- A missing/deleted selected invoice returns an error. The endpoint does not delete unrelated relation rows or silently switch invoices during the locked update.
- No request version/last_edited conflict guard, list filtering or pagination applies. Explicit charge/discount values must be finite numbers; nominal_code and nominalID/nominal_id resolve to a canonical accounting-code/local-ID pair, with conflicting valid pairs rejected.
Side effects
- May create/link an invoice first. Changed saves update invoice fields/meta/last_edited and edited_uuid=Job_invoiceUpdate, persist rows when changed, fire Invoice_edit and log job invoice update.
- Updates the active job’s price/price_net and app-sync timestamps only when its calculated subtotal differs; invalidates the meetings cache. Other linked jobs are not recalculated here.
See it in Fieldmotion
CRM1
Edit the job invoice screen or invoice table widget. pageJobInvoice.js / widgetInvoiceTable.js debounce Job_invoiceUpdate saves; inspect whether rows is omitted (header edit) or sent in full.
CRM2
The legacy pageJobInvoice and widgetInvoiceTable/widgetTableJobToInvoiceLink still call Job_invoiceUpdate. Summary autosave can send only discount/discount_vat; current mapped invoice row editing is a separate path.
Related endpoints
Job_invoiceGenerateis the non-idempotent auto-create helper.Job_invoiceLinkselects an existing invoice before this editor is used.Job_unlinkremoves a stored job/invoice relation.