Jobs: reports, email and links
Link a quote and optionally copy its costs
/a/Job_quoteLink
Link an active quote to a job and optionally append selected quote costs to the job.
When to use this endpoint
Use after choosing an existing quote. Copy flags are independent of the relation: leave them at 0 to link only; enable them only when quote costs really need appending to saved job resources.
Authentication and permissions
Signed API-key authentication or a logged-in CRM session is required. The handler uses the authenticated tenant and has no additional role/page check.
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 |
|---|---|---|---|
job_id | Integer string | Yes | Positive active physical job ID. |
quote_id | Integer string | Yes | Positive active quote ID; no quote-status or matching-customer restriction. |
copy_stock | Integer string | No | Default 0 skips stock. 2 copies current named rows (id or sid, type, amount, source, cost/price, discounts and custom text); other nonzero values use the legacy sid/amt/source mapping. Existing usage is appended to, not replaced. |
copy_sor | Integer string | No | Default 0. Nonzero appends quote costs.sor as named rate rows and regenerates legacy/enriched rate storage. |
copy_misc | Integer string | No | Default 0. Nonzero appends costs.other to misc_used; fractional qty is preserved. |
copy_verbose | Integer string | No | Default 0. Nonzero appends costs.verbose to misc_used: verbose_descr becomes description, net_price/net_total become price/price_total and fractional amount becomes qty. |
Example request
$result = FM_api('Job_quoteLink', [
'job_id' => '1042',
'quote_id' => '602',
'copy_stock' => '2',
'copy_sor' => '1',
'copy_misc' => '0',
'copy_verbose' => '0',
]);
Example response
{"ok":1}
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":"missing or invalid parameter: job_id"} | Missing/nonpositive job ID; quote_id has analogous guard. |
{"error":"quote does not exist"} | Quote missing/deleted. |
{"error":"job does not exist"} | Job missing/deleted. |
{"error":"this job is already linked to a quote"} | Any type-2 relation exists for this job, including stale/deleted quote links. |
{"error":"Invalid stored job costs; review job costs before linking"} | Any copy flag is nonzero and an existing stock/rate/misc field has nonblank, non-null invalid JSON or decodes to a non-array. |
{"error":"Unable to save copied quote costs"} | Primary job-cost save failed; link is not inserted. |
{"error":"Unable to link quote"} | Link insert failed with no copied job-cost write. |
{"ok":1,"warning":"Costs were copied, but the quote link could not be saved. Review job costs before retrying."} | Job-cost save completed but relation insert failed; retry can duplicate costs and stock deductions. |
Legacy application errors may be returned with HTTP 200. Check the decoded response for error, not only the HTTP status.
Behaviour and edge cases
- A job may have only one type-2 relation under this guard; multiple jobs may link the same quote. This is a query guard, not serialized uniqueness enforcement.
- Quote costs come from meta.costs.stock/sor/other/verbose. An enabled flag validates all four existing job cost stores, even if only one group is copied; blank/null storage is treated as [].
- Stock copy mode 2 uses id then sid, type=1 custom-text rows with id=0, source fallback -1_0, amt/cost/price and discountValue/discountPercentage; generates linkID values. Legacy mode uses sid/amt/source only.
- Rate copy takes sid, floating amt/price and notes, merges existing parsed rates, enriches rows and writes both schedule_of_rates and schedule_of_rates2. Misc/verbose append to misc_used; verbose maps description, net_price/net_total and amount to price/price_total and floating qty.
- After a copied-cost save, deducts appended warehouse-source stock rows whose source is exactly 0_0. Other source handling is not a general reservation/allocation workflow. Refreshes stored misc_cost and sor_cost, not the full Job_updateCosts/price pipeline.
- Once linked, Quote_countLinkedJobs(update=1) recounts relations and refreshes quote has_jobs. That follow-up return is not checked.
- No extra finance, completion, customer-match or quote-status check, pagination, per-group limit or duplicate-row suppression is implemented.
Side effects
- Appends selected costs, recalculates stored misc/rate subtotals, advances job last_edited/edited_uuid=Job_quoteLink and invalidates its cache when costs change.
- Deducts qualifying warehouse quantities after the job write: the stock helper sums amounts per positive catalog ID, updates catalog in_stock/edit timestamps/edited_uuid, invalidates the stock cache and fires Stock_edit. The endpoint then inserts a type-2 relation with edit time and refreshes the quote’s linked-job count. Does not send mail or explicitly log a job/quote link history row.
See it in Fieldmotion
CRM1
Choose jobs and Link to Quote. jobsLinkToQuote.js sends job_id/quote_id and stock/rate flags; the Jobs area on quote screens also links jobs. Filter Job_quoteLink.
CRM2
Use the Job-to-Quote widget. widgetJobToQuoteLink.js sends stock mode 2 and separate rate/misc/verbose switches; bulk/link-table flows also call this route. Inspect flags before reproducing a cost copy.
Related endpoints
Job_unlinkremoves type-2 relations but does not undo copied costs.Job_setStockedits stock with the dedicated inventory-aware workflow.Job_updateCostsrecalculates broader job totals.