81 'thirdparties' => array(
'thirdparty',
'billing',
'commercial'),
82 'categories' => array(
'thirdparty',
'stock'),
83 'invoices' => array(
'billing',
'thirdparty'),
84 'proposals' => array(
'commercial',
'thirdparty'),
85 'orders' => array(
'commercial',
'thirdparty'),
86 'products' => array(
'stock',
'commercial'),
87 'stockmovements' => array(
'stock'),
88 'warehouses' => array(
'stock'),
89 'projects' => array(
'project'),
90 'tasks' => array(
'project'),
91 'agendaevents' => array(
'thirdparty',
'project'),
92 'interventions' => array(
'project',
'commercial'),
93 'contracts' => array(
'commercial',
'billing'),
94 'members' => array(
'thirdparty',
'billing'),
95 'subscriptions' => array(
'thirdparty',
'billing'),
96 'expensereports' => array(
'billing'),
97 'tickets' => array(
'thirdparty',
'project'),
98 'shipments' => array(
'stock',
'commercial',
'thirdparty'),
99 'receptions' => array(
'stock',
'thirdparty')
108 const BRIDGE_MAX_LIMIT = 100;
115 private $defs =
null;
122 private $routes = [];
130 private $endpoints = [];
150 private $enrichments = [
152 'label' =>
'third parties (customers, prospects, suppliers)',
155 'default_properties' =>
'id,name,code_client,code_fournisseur,email,town,client,fournisseur,status',
156 'description' =>
"Use 'mode' to restrict to a nature of third party instead of filtering on names. To find one company by name use sqlfilters on t.nom (e.g. \"(t.nom:like:'%acme%')\"); other useful fields: t.name_alias, t.code_client, t.code_fournisseur, t.email, t.town, t.zip, t.fk_pays (country rowid), t.status (1=open, 0=closed). Prefer a small 'limit' and 'properties' (e.g. 'id,name,code_client,code_fournisseur,email,town,client,fournisseur,status') to keep answers short.",
158 'mode' =>
"Nature filter: 0=all (default), 1=customers/prospects, 2=prospects only, 3=neither customer nor prospect, 4=suppliers.",
159 'category' =>
"Rowid of a third-party category (tag) to restrict the list to.",
160 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}} instead of a bare list; use it to know the total count."
164 'description' =>
"Returns the full record: address, contact channels, customer/supplier codes, VAT number, default payment terms/modes, outstanding limit. In the result 'client' is 1=customer, 2=prospect, 3=both; 'fournisseur' is 1 when supplier."
167 'suffix' =>
'get_by_email',
168 'description' =>
"Find one third party by its exact company email address (not contact emails).",
169 'params' => [
'email' =>
"Exact email address of the company."]
171 'getOutStandingInvoices' => [
172 'suffix' =>
'outstanding_invoices',
173 'description' =>
"Total amount still due on validated, unpaid invoices of one third party. Returns {opened: amount} in the company currency.",
174 'params' => [
'mode' =>
"'customer' (default) for customer invoices, 'supplier' for supplier invoices."]
176 'getOutStandingOrder' => [
177 'suffix' =>
'outstanding_orders',
178 'description' =>
"Total amount of open (not yet invoiced) orders of one third party. Returns {opened: amount}.",
179 'params' => [
'mode' =>
"'customer' (default) for sales orders, 'supplier' for purchase orders."]
181 'getOutStandingProposals' => [
182 'suffix' =>
'outstanding_proposals',
183 'description' =>
"Total amount of open commercial proposals of one third party. Returns {opened: amount}.",
184 'params' => [
'mode' =>
"'customer' (default) or 'supplier'."]
189 'label' =>
'categories / tags',
192 'description' =>
"Categories form a tree per type (fk_parent = parent rowid, 0 for root). Always pass 'type' to restrict to one kind of object. Search by name with sqlfilters on t.label.",
194 'type' =>
"Kind of object the category applies to: 'product', 'customer', 'supplier', 'contact', 'member', 'project', 'user', 'bank_account', 'warehouse', 'actioncomm', 'website_page', 'ticket', 'knowledgemanagement'."
198 'params' => [
'include_childs' =>
"Set to true to also return the sub-categories (children). The parameter name 'include_childs' is fixed by the REST API."]
201 'suffix' =>
'objects_list',
202 'description' =>
"Objects tagged with one category (e.g. all products in category 12, all customers tagged 'VIP').",
204 'id' =>
"Rowid of the category.",
205 'type' =>
"Object type to list: 'product', 'customer', 'supplier', 'contact', 'member', 'project', 'user', 'warehouse', 'actioncomm', 'ticket'.",
206 'onlyids' =>
"1 to return only rowids (faster, use it for counting), 0 (default) for full objects."
212 'label' =>
'customer invoices',
215 'default_properties' =>
'id,ref,socid,date,date_lim_reglement,total_ht,total_ttc,paye,remaintopay',
216 'description' =>
"Use 'status' for the usual questions (unpaid, paid, drafts). Oldest first: sortfield 't.datef' with sortorder 'ASC' (due-date order: 't.date_lim_reglement'). Set withLines=false for lists: lines are large and rarely needed. Amounts: total_ht (excl. tax), total_tva, total_ttc (incl. tax), paye (1=paid). Dates are unix timestamps: date (invoice date), date_lim_reglement (due date). Overdue unpaid invoices: status='unpaid' plus sqlfilters \"(t.date_lim_reglement:<:'YYYY-MM-DD')\". Useful sqlfilters fields: t.ref, t.datef, t.total_ttc, t.fk_soc, t.type (0=standard, 1=replacement, 2=credit note, 3=deposit, 4=proforma), t.fk_statut (0=draft, 1=validated, 2=paid, 3=abandoned).",
218 'thirdparty_ids' =>
"Comma-separated third-party rowids to restrict to (e.g. '1,5'). Look the rowid up with api_thirdparties_list first when only a name is known.",
219 'status' =>
"One of 'draft', 'unpaid' (validated and not fully paid), 'paid', 'cancelled'. Empty = all.",
220 'withLines' =>
"false to omit invoice lines from each record (recommended for lists).",
221 'loadlinkedobjects' =>
"1 to include linked objects (orders, proposals, shipments) — slower, default 0.",
222 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}; use it to know how many invoices match."
226 'description' =>
"One invoice with its lines (product, qty, unit price, VAT rate, line totals), status, remaining amount to pay and linked contacts. Look the rowid up with api_invoices_list (sqlfilters on t.ref) when only the reference is known.",
228 'contact_list' =>
"0 = no contacts, 1 (default) = contact rowids, 2 = full contact records.",
229 'withLines' =>
"false to omit the lines."
233 'suffix' =>
'get_by_ref',
234 'description' =>
"One invoice by its exact reference (e.g. 'FA2401-0001').",
235 'params' => [
'ref' =>
"Exact invoice reference.",
'contact_list' =>
"0 = no contacts, 1 (default) = contact rowids, 2 = full contact records."]
238 'suffix' =>
'payments_list',
239 'description' =>
"Payments already recorded on one invoice: amount, date, payment mode, bank reference.",
240 'params' => [
'id' =>
"Rowid of the invoice."]
245 'label' =>
'commercial proposals (quotes / devis)',
248 'default_properties' =>
'id,ref,socid,datep,fin_validite,total_ht,total_ttc,fk_statut',
249 'description' =>
"Statuses (t.fk_statut): 0=draft, 1=validated (open, awaiting answer), 2=signed/accepted, 3=not signed/refused, 4=billed. Dates are unix timestamps: datep (proposal date), fin_validite (validity end — expired open proposals: fk_statut=1 plus sqlfilters \"(t.fin_validite:<:'YYYY-MM-DD')\"). Useful sqlfilters fields: t.ref, t.datep, t.total_ht, t.total_ttc, t.fk_soc, t.fk_statut.",
251 'thirdparty_ids' =>
"Comma-separated third-party rowids to restrict to (e.g. '1,5'). Look the rowid up with api_thirdparties_list first when only a name is known.",
252 'loadlinkedobjects' =>
"1 to include linked objects (orders, invoices) — slower, default 0.",
253 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}; use it to know how many proposals match."
257 'description' =>
"One proposal with its lines (product, qty, unit price, discount, line totals), status and validity date.",
258 'params' => [
'contact_list' =>
"0 = no contacts, 1 (default) = contact rowids, 2 = full contact records."]
261 'suffix' =>
'get_by_ref',
262 'description' =>
"One proposal by its exact reference (e.g. 'PR2401-0001').",
263 'params' => [
'ref' =>
"Exact proposal reference.",
'contact_list' =>
"0 = no contacts, 1 (default) = contact rowids, 2 = full contact records."]
268 'label' =>
'support tickets',
271 'default_properties' =>
'id,ref,track_id,subject,fk_soc,fk_statut,severity_code,type_code,datec',
272 'description' =>
"Statuses (t.fk_statut): 0=not read, 1=read, 2=assigned, 3=in progress, 5=needs more info, 7=waiting, 8=closed, 9=canceled. Open tickets = fk_statut < 8. Severity in severity_code (LOW, NORMAL, HIGH, BLOCKING), nature in type_code (COM=commercial, ISSUE=incident, ...). Useful sqlfilters fields: t.subject, t.fk_statut, t.severity_code, t.type_code, t.datec (creation), t.fk_user_assign (assigned user rowid).",
274 'socid' =>
"Third-party rowid to restrict tickets to one customer (0 = all).",
275 'loadcontacts' =>
"1 to include linked contacts per ticket, 0 (default, faster) without.",
276 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
280 'description' =>
"One ticket with its subject, full message, status, severity, assigned user and linked third party. The public tracking id is in track_id."
283 'suffix' =>
'get_by_track_id',
284 'description' =>
"One ticket by its public tracking id (the hash customers receive by email).",
285 'params' => [
'track_id' =>
"Public tracking id of the ticket."]
288 'suffix' =>
'get_by_ref',
289 'description' =>
"One ticket by its exact internal reference (e.g. 'TS2401-0001').",
290 'params' => [
'ref' =>
"Exact ticket reference."]
295 'label' =>
'projects (including opportunities/leads)',
298 'default_properties' =>
'id,ref,title,fk_soc,public,date_start,date_end,fk_statut,opp_status,opp_amount',
299 'description' =>
"Statuses (t.fk_statut): 0=draft, 1=open, 2=closed. A project used as a sales opportunity carries opp_status (pipeline step rowid), opp_percent (probability) and opp_amount. Dates are unix timestamps: date_start (dateo), date_end (datee). Useful sqlfilters fields: t.ref, t.title, t.fk_soc, t.fk_statut, t.dateo, t.datee, t.public (1=visible to everyone). Tasks of a project: use api_tasks_list with sqlfilters \"(t.fk_projet:=:'ID')\".",
301 'thirdparty_ids' =>
"Comma-separated third-party rowids to restrict to (e.g. '1,5').",
302 'category' =>
"Rowid of a project category (tag) to restrict the list to.",
303 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
307 'description' =>
"One project with its dates, status, budget, opportunity data and linked third party."
310 'suffix' =>
'get_by_ref',
311 'description' =>
"One project by its exact reference (e.g. 'PJ2401-0001').",
312 'params' => [
'ref' =>
"Exact project reference."]
317 'label' =>
'project tasks',
320 'default_properties' =>
'id,ref,label,fk_projet,progress,planned_workload,duration_effective,date_start,date_end',
321 'description' =>
"Tasks belong to a project (fk_projet — filter one project with sqlfilters \"(t.fk_projet:=:'ID')\"). progress is a percentage (0-100); planned_workload and duration_effective (time already spent) are in SECONDS — divide by 3600 for hours. fk_task_parent > 0 for subtasks. Useful sqlfilters fields: t.label, t.fk_projet, t.progress, t.dateo (start), t.datee (end).",
323 'includetimespent' =>
"1 to also load the time-spent summary per task (slower).",
324 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
328 'description' =>
"One task with its project, planned workload, progress and dates (durations in seconds).",
329 'params' => [
'includetimespent' =>
"1 to include the detail of time spent records."]
332 'suffix' =>
'timespent_list',
333 'description' =>
"Time records logged on one task: date, duration in seconds, user and note. Sum task_duration for the total.",
334 'params' => [
'id' =>
"Rowid of the task."]
339 'label' =>
'agenda / calendar events (meetings, calls)',
342 'default_properties' =>
'id,ref,label,type_code,datep,datef,fulldayevent,percentage,fk_soc,userownerid',
343 'description' =>
"Default sortfield is t.id — pass sortfield 't.datep' with sortorder 'ASC' for chronological order. Nature in type_code (AC_RDV=meeting, AC_TEL=phone call, AC_EMAIL=email, AC_OTH=other, AC_OTH_AUTO=automatic log). percentage: -1 = plain event, 0-99 = to-do in progress, 100 = done. Dates are unix timestamps: datep (start), datef (end). Upcoming events: sqlfilters \"(t.datep:>=:'YYYY-MM-DD')\". Other useful fields: t.label, t.fk_soc, t.fk_element/t.elementtype (linked business object).",
345 'user_ids' =>
"Comma-separated user rowids to restrict to events owned by these users (e.g. '1,3').",
346 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
350 'description' =>
"One event with its type, start/end dates, owner, linked third party/contact and note."
355 'label' =>
'field service interventions',
358 'default_properties' =>
'id,ref,socid,fk_statut,description,datec,duration',
359 'description' =>
"Statuses (t.fk_statut): 0=draft, 1=validated, 2=billed, 3=done/closed. duration is in SECONDS (divide by 3600 for hours). Useful sqlfilters fields: t.ref, t.fk_soc, t.fk_statut, t.datec, t.description.",
361 'thirdparty_ids' =>
"Comma-separated third-party rowids to restrict to (e.g. '1,5').",
362 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
366 'description' =>
"One intervention with its lines (each line = one on-site work entry with date, duration in seconds and description)."
371 'label' =>
'contracts (recurring services)',
374 'default_properties' =>
'id,ref,socid,date_contrat,statut',
375 'description' =>
"Contract statuses (t.statut): 0=draft, 1=validated. What matters is usually the LINE status (each line is one service): 0=inactive/draft, 4=active/running, 5=closed. Set withLines=false for lists (lines are large), then read one contract with api_contracts_get or its lines with api_contracts_lines_list. Useful sqlfilters fields: t.ref, t.fk_soc, t.statut, t.date_contrat.",
377 'thirdparty_ids' =>
"Comma-separated third-party rowids to restrict to (e.g. '1,5').",
378 'withLines' =>
"false to omit the service lines from each record (recommended for lists).",
379 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
383 'description' =>
"One contract with its service lines: per line fk_product, description, date_start/date_end (unix timestamps) and statut (0=inactive, 4=active, 5=closed).",
384 'params' => [
'withLines' =>
"false to omit the lines."]
387 'suffix' =>
'lines_list',
388 'description' =>
"Service lines of one contract, with their own pagination and sqlfilters (fields prefixed 'd.', e.g. \"(d.statut:=:'4')\" for active services).",
389 'params' => [
'id' =>
"Rowid of the contract."]
394 'label' =>
'foundation/association members',
397 'default_properties' =>
'id,ref,firstname,lastname,societe,email,typeid,statut,datefin',
398 'description' =>
"Statuses (t.statut): -1=draft, 1=validated (member), 0=membership terminated (resiliated), -2=excluded. Whether the subscription is up to date is in datefin (unix timestamp of the paid-up end date): late members = statut 1 plus sqlfilters \"(t.datefin:<:'YYYY-MM-DD')\" (or datefin null). Useful sqlfilters fields: t.firstname, t.lastname, t.societe, t.email, t.statut, t.datefin.",
400 'typeid' =>
"Rowid of a member type to restrict the list to.",
401 'category' =>
"Rowid of a member category (tag) to restrict the list to.",
402 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
406 'description' =>
"One member with identity, member type, status, linked third party and paid-up end date (datefin)."
408 'getSubscriptions' => [
409 'suffix' =>
'subscriptions_list',
410 'description' =>
"Subscription (membership fee) history of one member: period start/end and amount per payment.",
411 'params' => [
'id' =>
"Rowid of the member."]
416 'label' =>
'member subscriptions',
419 'description' =>
"All membership fee payments across members: fk_adherent (member rowid), dateh (period start), datef (period end), amount. NB: the default sortfield here is 'dateadh' WITHOUT the 't.' prefix (API quirk). To get the fees of one member prefer api_members_subscriptions_list.",
421 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
425 'description' =>
"One subscription payment with its member, period and amount."
429 'stockmovements' => [
430 'label' =>
'stock movements (in/out/transfer history)',
433 'default_properties' =>
'id,product_id,warehouse_id,qty,date,type,label,inventorycode',
434 'description' =>
"History of physical stock changes; each movement carries product_id, warehouse_id, qty (SIGNED: positive=in, negative=out), date and label. Movements of one product: sqlfilters \"(t.fk_product:=:'ID')\"; of one warehouse: \"(t.fk_entrepot:=:'ID')\"; over a period: \"(t.datem:>=:'YYYY-MM-DD')\". The source document (reception, shipment, inventory...) is in origintype/fk_origin when set.",
436 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
443 'label' =>
'warehouses',
446 'description' =>
"Warehouses/locations: ref (name), lieu (short location), statut (1=open, 0=closed). Search by name with sqlfilters on t.ref. Per-product stock by warehouse is NOT here — use api_products_get with includestockdata=1.",
448 'category' =>
"Rowid of a warehouse category (tag) to restrict the list to.",
449 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
453 'description' =>
"One warehouse with its address, status and description."
458 'label' =>
'customer orders (commandes)',
461 'default_properties' =>
'id,ref,socid,date_commande,delivery_date,total_ht,total_ttc,statut',
462 'description' =>
"Statuses (t.fk_statut): -1=cancelled, 0=draft, 1=validated (open), 2=shipment in progress, 3=closed (delivered / billed). Dates are unix timestamps: date_commande (order date), delivery_date (planned delivery). Useful sqlfilters fields: t.ref, t.date_commande, t.total_ht, t.total_ttc, t.fk_soc, t.fk_statut.",
464 'thirdparty_ids' =>
"Comma-separated third-party rowids to restrict to (e.g. '1,5'). Look the rowid up with api_thirdparties_list first when only a name is known.",
465 'loadlinkedobjects' =>
"1 to include linked objects (proposals, shipments, invoices) — slower, default 0.",
466 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}; use it to know how many orders match."
470 'description' =>
"One order with its lines (product, qty, unit price, discount, line totals), status and dates.",
471 'params' => [
'contact_list' =>
"0 = no contacts, 1 (default) = contact rowids, 2 = full contact records."]
474 'suffix' =>
'get_by_ref',
475 'description' =>
"One order by its exact reference (e.g. 'CO2401-0001').",
476 'params' => [
'ref' =>
"Exact order reference.",
'contact_list' =>
"0 = no contacts, 1 (default) = contact rowids, 2 = full contact records."]
481 'label' =>
'customer shipments (expéditions, goods sent)',
484 'default_properties' =>
'id,ref,socid,ref_customer,date_delivery,date_shipping,statut',
485 'description' =>
"Statuses (t.fk_statut): 0=draft (reference '(PROVnn)' until validated), 1=validated (goods left), 2=closed (billed / processed). Dates are unix timestamps: date_shipping (sent), date_delivery (planned delivery). Useful sqlfilters fields: t.ref, t.ref_customer, t.fk_soc, t.fk_statut, t.date_delivery.",
487 'thirdparty_ids' =>
"Comma-separated third-party rowids to restrict to (e.g. '1,5'). Look the rowid up with api_thirdparties_list first when only a name is known.",
488 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}; use it to know how many shipments match."
492 'description' =>
"One shipment: customer, source order, dates, status, tracking number; the lines are in api_shipments_get_lines."
495 'suffix' =>
'get_lines',
496 'description' =>
"Lines of a shipment: product, quantity shipped, batch/lot when the product is tracked.",
497 'params' => [
'id' =>
"Rowid of the shipment."]
502 'label' =>
'supplier receptions (réceptions, goods received)',
505 'default_properties' =>
'id,ref,socid,ref_supplier,date_reception,date_delivery,statut',
506 'description' =>
"Statuses (t.fk_statut): 0=draft (reference '(PROVnn)' until validated), 1=validated (goods received into stock), 2=closed / processed. Dates are unix timestamps: date_reception (received), date_delivery (planned). Useful sqlfilters fields: t.ref, t.ref_supplier, t.fk_soc, t.fk_statut, t.date_delivery. A draft created by the chat keeps its '(PROVnn)' reference: search it by id or with sqlfilters on t.ref_supplier.",
508 'thirdparty_ids' =>
"Comma-separated supplier rowids to restrict to (e.g. '1,5'). Look the rowid up with api_thirdparties_list first when only a name is known.",
509 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}; use it to know how many receptions match."
513 'description' =>
"One reception: supplier, supplier reference, dates, status; the lines are in api_receptions_get_lines."
516 'suffix' =>
'get_lines',
517 'description' =>
"Lines of a reception: product, quantity received, batch/lot and warehouse when tracked.",
518 'params' => [
'id' =>
"Rowid of the reception."]
522 'expensereports' => [
523 'label' =>
'employee expense reports (notes de frais)',
526 'default_properties' =>
'id,ref,fk_user_author,date_debut,date_fin,total_ht,total_ttc,fk_statut',
527 'description' =>
"Statuses (t.fk_statut): 0=draft, 2=validated (waiting approval), 4=canceled, 5=approved, 6=paid, 99=refused. The employee is fk_user_author (a USER rowid, not a third party). Period: date_debut/date_fin (unix timestamps). Useful sqlfilters fields: t.ref, t.fk_user_author, t.fk_statut, t.date_debut, t.total_ttc.",
529 'user_ids' =>
"Comma-separated user rowids to restrict to the reports of these employees (e.g. '1,3').",
530 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
534 'description' =>
"One expense report with its lines: per line the date, expense type code (type_fees_code: TRA_TRIP=transport, TRA_MEAL=meal, ...), VAT and amounts."
539 'label' =>
'products and services catalog',
542 'default_properties' =>
'id,ref,label,type,price,price_ttc,tva_tx,status,status_buy',
543 'description' =>
"Search by name with sqlfilters on t.label, by reference on t.ref (e.g. \"(t.label:like:'%screw%')\"). Result fields: type (0=product, 1=service), price (sale price excl. tax), price_ttc, tva_tx (VAT rate), status (1=for sale), status_buy (1=for purchase), stock_reel (only with includestockdata=1). Prefer 'properties' (e.g. 'id,ref,label,type,price,price_ttc,tva_tx,status,status_buy') and a small 'limit'.",
545 'mode' =>
"0=all (default), 1=products only, 2=services only.",
546 'category' =>
"Rowid of a product category to restrict the list to.",
547 'variant_filter' =>
"0=all (default), 1=products without variants, 2=parents of variants only, 3=variants only.",
548 'ids_only' =>
"true to return only rowids (fast, use for counting).",
549 'includestockdata' =>
"1 to add stock_reel / stock_theorique per product (slower; requires the Stock module).",
550 'pagination_data' =>
"Set to true to get {data, pagination:{total,page,page_count,limit}}."
554 'description' =>
"Full product record: description, prices, VAT, barcode, weight/dimensions, accounting codes, optional stock and sub-products.",
556 'includestockdata' =>
"1 to load stock_reel, stock_theorique and per-warehouse stock (requires the Stock module).",
557 'includesubproducts' =>
"true to load the kit/BOM components (sub-products).",
558 'includeparentid' =>
"true to add fk_product_parent for a variant.",
559 'includetrans' =>
"true to load multilingual labels/descriptions."
563 'suffix' =>
'get_by_ref',
564 'description' =>
"One product by its exact reference (e.g. 'PROD-001'); same options as get.",
565 'params' => [
'ref' =>
"Exact product reference."]
568 'suffix' =>
'get_by_barcode',
569 'description' =>
"One product by its barcode (EAN/UPC); same options as get.",
570 'params' => [
'barcode' =>
"Barcode value as printed."]
572 'getPurchasePrices' => [
573 'suffix' =>
'purchase_prices_list',
574 'description' =>
"Supplier prices of one product: for each supplier, the supplier reference, minimum quantity, unit purchase price and VAT. Identify the product by id, ref or barcode.",
575 'params' => [
'id' =>
"Rowid of the product (use 0 when identifying by ref or barcode).",
'ref' =>
"Product reference, alternative to id.",
'barcode' =>
"Product barcode, alternative to id."]
578 'suffix' =>
'attributes_list',
579 'module' =>
'variants',
580 'description' =>
"Variant attributes (e.g. Size, Color) defined in the catalog."
583 'suffix' =>
'variants_list',
584 'module' =>
'variants',
585 'description' =>
"Variants of one parent product.",
586 'params' => [
'id' =>
'Rowid of the PARENT product.',
'includestock' =>
"1 to add stock data on each variant."]
602 private $commonParamDocs = [
603 'sortfield' =>
"Field to sort on, prefixed with 't.' (e.g. 't.rowid', 't.ref', 't.datec'). Use the SQL column names: creation date is 't.datec' (NEVER 'date_creation') and last modification 't.tms' (never 'date_modification').",
604 'sortorder' =>
"Sort direction: 'ASC' or 'DESC'.",
605 'limit' =>
"Maximum number of records to return.",
606 'page' =>
"Zero-based page index for pagination.",
607 'sqlfilters' =>
"Universal search filter. Syntax: (t.field:operator:'value'); operators: =, !=, <, <=, >, >=, like, is; combine clauses with 'and'/'or' and parentheses. Example: \"(t.ref:like:'PR%') and (t.datec:>=:'2026-01-01')\". 'like' is case-insensitive; the IN operator is NOT supported (use 'or'); dates as 'YYYY-MM-DD'.",
608 'properties' =>
"Comma-separated list of properties to include in the response, to reduce its size (e.g. 'id,ref,label').",
609 'id' =>
"Rowid (numeric technical id) of the record."
623 if ($conf !==
null) {
638 require_once DOL_DOCUMENT_ROOT .
'/core/lib/functions2.lib.php';
639 require_once DOL_DOCUMENT_ROOT .
'/includes/restler/framework/Luracast/Restler/AutoLoader.php';
640 $loader = Luracast\Restler\AutoLoader::instance();
641 spl_autoload_register($loader);
642 require_once DOL_DOCUMENT_ROOT .
'/api/class/api.class.php';
643 require_once DOL_DOCUMENT_ROOT .
'/api/class/api_access.class.php';
673 if (!is_resource($handle)) {
677 while (($file = readdir($handle)) !==
false) {
679 if (!is_readable($dir.$file) || !preg_match(
"/^mod(.*)\\.class\\.php$/i", $file, $regmod)) {
683 $module = strtolower($regmod[1]);
687 $modulenameforenabled =
$module;
689 $modulenameforenabled =
'propal';
690 } elseif (
$module ==
'supplierproposal') {
691 $modulenameforenabled =
'supplier_proposal';
692 } elseif (
$module ==
'ficheinter') {
693 $modulenameforenabled =
'intervention';
695 $modulenameforenabled =
'service';
704 if (!is_resource($handle_part)) {
708 while (($file_searched = readdir($handle_part)) !==
false) {
709 if (in_array($file_searched, [
'api_access.class.php',
'api_setup.class.php',
'api_documents.class.php',
'api_login.class.php',
'api_status.class.php'],
true)) {
713 if (!is_readable($dir_part.$file_searched) || !preg_match(
"/^api_(.*)\\.class\\.php$/i", $file_searched, $regapi)) {
717 $key = strtolower($regapi[1]);
718 if (isset($endpoints[$key])) {
723 'module' => $modulenameforenabled,
724 'path' => $dir_part.$file_searched,
725 'candidate' => str_replace(
'_',
'', ucwords($regapi[1])),
727 'label' => $this->enrichments[$key][
'label'] ?? $key,
730 closedir($handle_part);
751 if ($this->endpoints[$key][
'class'] !==
'') {
754 require_once $this->endpoints[$key][
'path'];
755 $candidate = $this->endpoints[$key][
'candidate'];
757 if (class_exists($candidate.
'Api')) {
758 $classname = $candidate.
'Api';
759 } elseif (class_exists($candidate)) {
760 $classname = $candidate;
762 if ($classname ===
'') {
766 $reflection =
new ReflectionClass($classname);
767 $this->endpoints[$key][
'class'] = $reflection->getName();
771 if ($this->endpoints[$key][
'label'] === $key) {
772 $classdoc = (string) $reflection->getDocComment();
773 if (preg_match(
'/\*\s+([^@\s\/*][^\n]*)/', $classdoc, $mlabel)) {
775 $this->endpoints[$key][
'label'] = trim(preg_replace(
'/^API class (for|of)\s+(the\s+)?/i',
'', trim($mlabel[1])));
794 return [
'index' => [],
'get' => []];
809 if ($configured ===
'') {
812 $allowed = array_map(
'trim', explode(
',', $configured));
813 return array_intersect_key($methods, array_flip($allowed));
826 if ($this->defs !==
null) {
838 if ($cachefile !==
'' && is_readable($cachefile) && (
dol_now() - (int) filemtime($cachefile)) < $cachettl) {
839 $payload = json_decode((
string) file_get_contents($cachefile),
true);
840 if (is_array($payload) && isset($payload[
'defs'], $payload[
'routes'], $payload[
'endpoints'])
841 && is_array($payload[
'defs']) && is_array($payload[
'routes']) && is_array($payload[
'endpoints'])) {
842 $this->defs = $payload[
'defs'];
843 $this->routes = $payload[
'routes'];
844 $this->endpoints = $payload[
'endpoints'];
856 foreach ($this->endpoints as $key => $ep) {
864 if (!isset($this->enrichments[$key])) {
874 $ep = $this->endpoints[$key];
876 foreach ($methods as $method => $meta) {
880 if (!empty($meta[
'module']) && !
isModEnabled($meta[
'module'])) {
883 if (!method_exists($ep[
'class'], $method)) {
886 $suffix = $meta[
'suffix'] ?? ($method ===
'index' ?
'list' : strtolower($method));
887 $toolname =
'api_' . $key .
'_' . $suffix;
890 $def[
'categories'] = self::ENDPOINT_CATEGORIES[$key] ?? array(
'billing',
'commercial',
'thirdparty',
'stock',
'project',
'reporting');
891 $this->defs[] = $def;
892 $this->routes[$toolname] = [$key, $method];
897 if ($cachefile !==
'') {
923 if (!empty(
$conf->ai->multidir_temp[
$conf->entity])) {
924 $dir =
$conf->ai->multidir_temp[
$conf->entity];
925 } elseif (!empty(
$conf->ai->dir_temp)) {
926 $dir =
$conf->ai->dir_temp;
928 if (empty($dir) ||
dol_mkdir($dir) < 0) {
932 $modules = array_map(
'strval', array_values((array)
$conf->modules));
934 $signature = implode(
',', $modules).
'|'.DOL_VERSION.
'|'.((int)
$conf->entity).
'|'.((int) @filemtime(__FILE__)).
'|'.DOL_DOCUMENT_ROOT;
939 $signature .=
'|'.getDolGlobalInt(
'AI_MCP_API_BRIDGE').
'|'.
getDolGlobalString(
'AI_MCP_API_BRIDGE_METHODS');
941 return rtrim($dir,
'/').
'/bridge_tooldefs_'.md5($signature).
'.json';
955 $payload = json_encode(array(
'defs' => $this->defs,
'routes' => $this->routes,
'endpoints' => $this->endpoints));
956 if (!is_string($payload)) {
959 foreach ((array) glob(dirname($cachefile).
'/bridge_tooldefs_*.json') as $old) {
960 if (is_string($old) && $old !== $cachefile) {
964 $tmpfile = $cachefile.
'.tmp.'.getmypid();
965 if (file_put_contents($tmpfile, $payload) !==
false) {
966 @rename($tmpfile, $cachefile);
983 private function buildToolDefinition(array $ep,
string $key,
string $method,
string $toolname, array $meta = [])
986 $rm =
new ReflectionMethod($ep[
'class'], $method);
987 }
catch (ReflectionException $e) {
991 $doc = (string) $rm->getDocComment();
996 if (preg_match(
'/\*\s+([^@\s\/*][^\n]*)/', $doc, $m)) {
997 $summary = trim($m[1]);
1002 if (preg_match_all(
'/@param\s+(\S+)\s+\$(\w+)\s+([^\n]*)/', $doc, $mm, PREG_SET_ORDER)) {
1003 foreach ($mm as $pm) {
1004 $paramDocs[$pm[2]] = [
'type' => $pm[1],
'desc' => trim($pm[3])];
1010 foreach ($rm->getParameters() as $p) {
1011 $pname = $p->getName();
1012 $ptype = isset($paramDocs[$pname]) ? $this->
docTypeToJson($paramDocs[$pname][
'type']) :
'string';
1020 if (isset($meta[
'params'][$pname])) {
1021 $pdesc = $meta[
'params'][$pname];
1022 } elseif (!empty($paramDocs[$pname][
'desc'])) {
1023 $pdesc = $paramDocs[$pname][
'desc'];
1024 if (in_array($pname, [
'sqlfilters',
'sortfield'],
true) && !empty($this->commonParamDocs[$pname])) {
1025 $pdesc = rtrim($pdesc,
'. ').
'. '.$this->commonParamDocs[$pname];
1028 $pdesc = $this->commonParamDocs[$pname] ??
'';
1040 'description' => $pdesc
1042 $prop += $constraints;
1043 if ($p->isOptional()) {
1045 $prop[
'default'] = ($pname ==
'limit') ? self::BRIDGE_DEFAULT_LIMIT : $p->getDefaultValue();
1046 }
catch (ReflectionException $e) {
1050 $required[] = $pname;
1052 $properties[$pname] = $prop;
1055 if ($method ===
'index') {
1056 $verb =
'List / search';
1057 } elseif ($method ===
'get') {
1058 $verb =
'Get one record of';
1060 $verb =
'Read from';
1062 $schema = [
'type' =>
'object',
'properties' => $properties];
1064 $schema[
'required'] = $required;
1067 $description = $verb .
' ' . $ep[
'label'] .
' through the Dolibarr REST API (auto-generated tool). ' . $summary;
1068 if (!empty($meta[
'description'])) {
1069 $description = rtrim($description) .
' ' . $meta[
'description'];
1073 'name' => $toolname,
1074 'description' => $description,
1075 'inputSchema' => $schema
1097 $desc = (string) $desc;
1099 if (strpos($desc,
'{@') ===
false) {
1104 if (preg_match_all(
'/\{@(\w[\w-]*)\s*([^}]*)\}/', $desc, $matches, PREG_SET_ORDER)) {
1105 foreach ($matches as $tag) {
1106 $name = strtolower($tag[1]);
1107 $value = trim($tag[2]);
1109 if ($name ===
'min' && is_numeric($value)) {
1111 } elseif ($name ===
'max' && is_numeric($value)) {
1113 } elseif ($name ===
'choice' && $value !==
'') {
1114 $choices = array_map(
'trim', explode(
',', $value));
1115 if ($ptype ===
'integer' || $ptype ===
'number') {
1116 foreach ($choices as $i => $choice) {
1117 if (is_numeric($choice)) {
1122 $constraints[
'enum'] = array_values($choices);
1123 } elseif ($name ===
'pattern' && $value !==
'') {
1125 if ($pattern !==
'') {
1126 $constraints[
'pattern'] = $pattern;
1134 $desc = preg_replace(
'/\s*\{@\w[\w-]*[^}]*\}/',
'', $desc);
1136 return trim(preg_replace(
'/\s{2,}/',
' ', (
string) $desc));
1150 return (strpos($value,
'.') ===
false) ? (int) $value : (float) $value;
1169 if (!preg_match(
'/^(.)(.*)\1([a-zA-Z]*)$/s', $value, $reg)) {
1173 $expression = $reg[2];
1176 if ($flags !==
'' && !($flags ===
'i' && !preg_match(
'/[a-zA-Z]/', $expression))) {
1191 $t = strtolower(trim(explode(
'|', $type)[0]));
1192 if (in_array($t, [
'int',
'integer'],
true)) {
1195 if (in_array($t, [
'float',
'double'],
true)) {
1198 if ($t ===
'bool' || $t ===
'boolean') {
1212 return self::RIGHTS_ENFORCED_DOWNSTREAM;
1226 foreach (array_keys($this->enrichments) as $key) {
1227 $all = array_merge($all, self::ENDPOINT_CATEGORIES[$key] ?? array());
1230 return array_values(array_unique($all));
1241 'thirdparties' =>
'societe',
1242 'contacts' =>
'socpeople',
1243 'invoices' =>
'facture',
1244 'supplierinvoices' =>
'facture_fourn',
1245 'orders' =>
'commande',
1246 'supplierorders' =>
'commande_fournisseur',
1247 'proposals' =>
'propal',
1248 'supplierproposals' =>
'supplier_proposal',
1249 'products' =>
'product',
1250 'contracts' =>
'contrat',
1251 'interventions' =>
'fichinter',
1252 'tickets' =>
'ticket',
1253 'projects' =>
'projet',
1254 'tasks' =>
'project_task',
1255 'members' =>
'adherent',
1256 'expensereports' =>
'expensereport',
1257 'shipments' =>
'expedition',
1258 'receptions' =>
'reception',
1259 'agendaevents' =>
'actioncomm',
1260 'warehouses' =>
'stock',
1261 'categories' =>
'categorie',
1262 'bankaccounts' =>
'bank_account'
1265 return $map[$key] ??
'';
1276 public function execute(
string $name, array $args)
1279 return [
"error" =>
"API bridge is disabled (AI_MCP_API_BRIDGE not set)."];
1283 if (empty($this->routes[$name])) {
1284 return [
"error" =>
"Tool function '$name' not found."];
1286 list($key, $method) = $this->routes[$name];
1287 $ep = $this->endpoints[$key];
1295 $methodmeta = isset($this->enrichments[$key][
'methods'][$method]) ? $this->enrichments[$key][
'methods'][$method] : array();
1296 if (!empty($methodmeta[
'default_properties']) && !array_key_exists(
'properties', $args)) {
1297 $args[
'properties'] = $methodmeta[
'default_properties'];
1313 $saveduserapi = DolibarrApiAccess::$user;
1314 $saveduserglobal = empty($GLOBALS[
'user']) ? null : $GLOBALS[
'user'];
1315 DolibarrApiAccess::$user = $this->user;
1316 $GLOBALS[
'user'] = $this->user;
1318 require_once $ep[
'path'];
1319 $api =
new $ep[
'class']();
1322 $rm =
new ReflectionMethod($ep[
'class'], $method);
1325 foreach ($rm->getParameters() as $p) {
1326 $pname = $p->getName();
1327 if (array_key_exists($pname, $args)) {
1329 $callArgs[] = ($pname ==
'limit') ? min((
int) $args[$pname], self::BRIDGE_MAX_LIMIT) : $args[$pname];
1330 } elseif ($p->isOptional()) {
1332 $callArgs[] = ($pname ==
'limit') ? self::BRIDGE_DEFAULT_LIMIT : $p->getDefaultValue();
1334 $output = [
"error" =>
"Missing required parameter '$pname'."];
1339 if ($output ===
null) {
1341 $result = call_user_func_array([$api, $method], $callArgs);
1343 $output = json_decode(json_encode($result),
true);
1344 }
catch (Throwable $e) {
1345 $code = (int) $e->getCode();
1346 $message = $e->getMessage();
1347 if ($message ===
'') {
1350 $message =
'Access denied or resource error (HTTP '.($code > 0 ? $code : 500).
').';
1353 "error" => $message,
1354 "http_status" => ($code > 0 ? $code : 500)
1361 if ($elementForExtrafields !==
'') {
1362 require_once DOL_DOCUMENT_ROOT.
'/ai/lib/ai.lib.php';
1367 DolibarrApiAccess::$user = $saveduserapi;
1368 if ($saveduserglobal !==
null) {
1369 $GLOBALS[
'user'] = $saveduserglobal;
aiStripPersonalExtrafields($db, $payload, $elementtype)
Remove extrafields flagged as personal data from an API-shaped payload.
if(! $sortfield) if(! $sortorder) $module
getModuleDirForApiClass($moduleobject)
Get name of directory where the api_...class.php file is stored.
dolGetModulesDirs($subdir='')
Return list of directories that contain modules.
dol_now($mode='gmt')
Return date for now.
dol_osencode($str)
Return a string encoded into OS filesystem encoding.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
dol_mkdir($dir, $dataroot='', $newmask='')
Creation of a directory (this can create recursive subdir)
conf($dolibarr_main_document_root, $realpathconf=null)
Load conf file (file must exists)
$conf db user
Active Directory does not allow anonymous connections.