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

ParameterTypeRequiredDescription
job_idInteger stringYesPositive active physical job ID.
quote_idInteger stringYesPositive active quote ID; no quote-status or matching-customer restriction.
copy_stockInteger stringNoDefault 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_sorInteger stringNoDefault 0. Nonzero appends quote costs.sor as named rate rows and regenerates legacy/enriched rate storage.
copy_miscInteger stringNoDefault 0. Nonzero appends costs.other to misc_used; fractional qty is preserved.
copy_verboseInteger stringNoDefault 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

ResponseCondition
{"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_unlink removes type-2 relations but does not undo copied costs.
  • Job_setStock edits stock with the dedicated inventory-aware workflow.
  • Job_updateCosts recalculates broader job totals.