dolibarr 24.0.0-beta
navigation.class.php
1<?php
2/* Copyright (C) 2026 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2026 Nick Fragoulis
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18/* htdocs/ai/tools/navigation.php */
19
20require_once DOL_DOCUMENT_ROOT . '/core/lib/functions.lib.php';
21require_once DOL_DOCUMENT_ROOT . '/core/class/extrafields.class.php';
22
29{
35 public function getDefinitions(): array
36 {
37 return [
38 [
39 "name" => "navigate_to_page",
40 "description" => "Generates a valid Dolibarr URL. Handles generic names (e.g., 'invoice' maps to customer invoices) and directory structures automatically. Can filter lists by status.",
41 "inputSchema" => [
42 "type" => "object",
43 "properties" => [
44 "object_type" => [
45 "type" => "string",
46 "description" => "The object type. Examples: 'invoice', 'thirdparty', 'order', 'proposal', 'project', 'supplier_invoice'.",
47 ],
48 "view" => [
49 "type" => "string",
50 "description" => "The type of view needed: 'list', 'card', 'create'.",
51 "enum" => ["list", "card", "create"]
52 ],
53 "id" => [
54 "type" => "integer",
55 "description" => "The ID of the record (optional)."
56 ],
57 "ref" => [
58 "type" => "string",
59 "description" => "The Reference of the record (optional)."
60 ],
61 "status_filter" => [
62 "type" => "string",
63 "description" => "A human-readable status to filter the list. Only applies to 'list' view. Examples: 'draft', 'open', 'paid', 'shipped', 'closed', 'canceled'."
64 ],
65 "params" => [
66 "type" => "object",
67 "description" => "Additional URL parameters (e.g. {'search_thirdparty': 'MyCompany'}). These will be combined with the status filter."
68 ]
69 ],
70 "required" => ["object_type", "view"]
71 ]
72 ]
73 ];
74 }
75
82 public function getCategories(): array
83 {
84 return ['global'];
85 }
86
94 public function execute(string $name, array $args)
95 {
96 global $langs, $db;
97
98 // Load translation files
99 $langs->load("companies");
100 $langs->load("bills");
101 $langs->load("orders");
102 $langs->load("propal");
103 $langs->load("projects");
104 $langs->load("sendings");
105
106 if ($name !== 'navigate_to_page') {
107 return null;
108 }
109
110 if (empty($this->user->id)) {
111 return ["error" => "Permission Denied: User not logged in."];
112 }
113
114 $rawType = $args['object_type'] ?? '';
115 $view = $args['view'] ?? 'list';
116 $id = (int) ($args['id'] ?? 0);
117 $ref = $args['ref'] ?? '';
118 $statusFilter = $args['status_filter'] ?? '';
119 $params = $args['params'] ?? [];
120
121 // Resolve Logical Object to Physical Path (with Aliases)
122 $pathInfo = $this->resolvePath($rawType, $view);
123
124 if (empty($pathInfo)) {
125 return ["error" => "Unknown object type: '$rawType'. Try 'invoice', 'order', or 'thirdparty'."];
126 }
127
128 $relativePath = $pathInfo['path'];
129 $elementType = $pathInfo['type'];
130
131 // Check permissions
132 if (!$this->checkPermissions($elementType, $view, $id)) {
133 return ["error" => "Permission Denied: You don't have permission to access this resource."];
134 }
135
136 // Build Query Parameters
137 $queryParams = [];
138
139 // Handle Action/ID logic
140 if ($id > 0) {
141 $queryParams['id'] = $id;
142 } elseif (!empty($ref)) {
143 $queryParams['ref'] = $ref;
144 }
145
146 // Set action for create view
147 if ($view === 'create') {
148 $queryParams['action'] = 'create';
149 }
150
151 // Handle Status Filtering
152 if ($view === 'list' && !empty($statusFilter)) {
153 $statusParam = $this->mapStatusToFilter($elementType, $statusFilter);
154 if ($statusParam) {
155 $queryParams = array_merge($queryParams, $statusParam);
156 } else {
157 return ["error" => "Unknown status filter '$statusFilter' for object type '$rawType'."];
158 }
159 } elseif (!empty($statusFilter)) {
160 return ["error" => "The 'status_filter' parameter can only be used with the 'list' view."];
161 }
162
163 // Merge extra params
164 if (!empty($params) && is_array($params)) {
165 $queryParams = array_merge($queryParams, $params);
166 }
167
168 // Generate Native URL
169 $baseUrl = dol_buildpath($relativePath, 1);
170
171 $finalUrl = $baseUrl;
172 if (!empty($queryParams)) {
173 $finalUrl .= '?' . http_build_query($queryParams);
174 }
175
176 return [
177 "url" => $finalUrl,
178 "description" => $this->generateDescription($elementType, $view, $id, $statusFilter),
179 "meta" => [
180 "resolved_type" => $elementType,
181 "path" => $relativePath
182 ]
183 ];
184 }
185
194 private function mapStatusToFilter($elementType, $statusFilter)
195 {
196 $statusFilter = strtolower(trim($statusFilter));
197
198 // Master map of element types to their status filters
199 $statusMap = [
200 'invoice_customer' => [
201 'draft' => ['statut' => 0],
202 'unpaid' => ['statut' => 1], // Validated but not paid
203 'paid' => ['statut' => 2],
204 ],
205 'invoice_supplier' => [
206 'draft' => ['statut' => 0],
207 'unpaid' => ['statut' => 1],
208 'paid' => ['statut' => 2],
209 ],
210 'order' => [
211 'draft' => ['statut' => 0],
212 'validated' => ['statut' => 1],
213 'shipped' => ['statut' => 2], // Or partially shipped
214 'closed' => ['statut' => 3],
215 'canceled' => ['statut' => -1],
216 ],
217 'order_supplier' => [
218 'draft' => ['statut' => 0],
219 'validated' => ['statut' => 1],
220 'approved' => ['statut' => 2],
221 'received' => ['statut' => 3], // Or partially received
222 'canceled' => ['statut' => -1],
223 ],
224 'proposal' => [
225 'draft' => ['statut' => 0],
226 'open' => ['statut' => 1],
227 'signed' => ['statut' => 2],
228 'billed' => ['statut' => 3],
229 'refused' => ['statut' => 4],
230 'canceled' => ['statut' => 5],
231 ],
232 'project' => [
233 'draft' => ['status' => 0],
234 'open' => ['status' => 1],
235 'closed' => ['status' => 2],
236 ],
237 'expedition' => [ // Shipments
238 'draft' => ['status' => 0],
239 'validated' => ['status' => 1],
240 'shipped' => ['status' => 2],
241 'canceled' => ['status' => -1],
242 ],
243 'contract' => [
244 'draft' => ['statut' => 0],
245 'active' => ['statut' => 1],
246 'closed' => ['statut' => 2],
247 'resiliated' => ['statut' => 3], // Resiliated
248 ],
249 'fichinter' => [ // Interventions
250 'draft' => ['statut' => 0],
251 'validated' => ['statut' => 1],
252 'billed' => ['statut' => 2],
253 'closed' => ['statut' => 3],
254 ],
255 // Add other object types as needed
256 ];
257
258 return $statusMap[$elementType][$statusFilter] ?? null;
259 }
260
268 private function resolvePath($input, $view)
269 {
270 $input = strtolower(trim($input));
271
272 // Normalize Aliases (Make the tool robust to LLM guessing)
273 $aliases = [
274 // Invoices
275 'invoice' => 'invoice_customer',
276 'bill' => 'invoice_customer',
277 'facture' => 'invoice_customer',
278 'supplier_invoice' => 'invoice_supplier',
279 'vendor_bill' => 'invoice_supplier',
280 // Thirdparties
281 'company' => 'thirdparty',
282 'societe' => 'thirdparty',
283 'customer' => 'thirdparty',
284 'client' => 'thirdparty',
285 'supplier' => 'thirdparty',
286 'vendor' => 'thirdparty',
287 // Commercial
288 'propal' => 'proposal',
289 'quote' => 'proposal',
290 'command' => 'order',
291 'customer_order' => 'order',
292 'supplier_order' => 'order_supplier',
293 // Products/Services
294 'product' => 'product',
295 'service' => 'product',
296 // Projects
297 'project' => 'project',
298 'task' => 'project_task',
299 // Shipping
300 'shipment' => 'expedition',
301 'shipping' => 'expedition',
302 'delivery' => 'expedition',
303 // Payments
304 'payment' => 'payment',
305 'payment_customer' => 'payment',
306 'payment_supplier' => 'payment_supplier',
307 // Banking
308 'transaction' => 'bank',
309 'account' => 'bank',
310 'bank_account' => 'bank',
311 // Events
312 'event' => 'agenda',
313 'agenda' => 'agenda',
314 'appointment' => 'agenda',
315 // Contracts
316 'contract' => 'contract',
317 // Interventions
318 'intervention' => 'fichinter',
319 // Members
320 'member' => 'adherent',
321 'membership' => 'adherent',
322 // Categories
323 'category' => 'categories',
324 ];
325
326 $type = $aliases[$input] ?? $input;
327
328 // Map Normalized Types to Physical Paths
329 $map = [
330 'thirdparty' => '/societe/',
331 'contact' => '/contact/',
332 'product' => '/product/',
333 'project' => '/projet/',
334 'project_task' => '/projet/tasks/',
335 'invoice_customer' => '/compta/facture/',
336 'invoice_supplier' => '/fourn/facture/',
337 'order' => '/commande/',
338 'order_supplier' => '/fourn/commande/',
339 'proposal' => '/comm/propal/',
340 'expedition' => '/expedition/',
341 'payment' => '/compta/paiement.php', // Direct file, not directory
342 'payment_supplier' => '/fourn/paiement.php', // Direct file, not directory
343 'bank' => '/compta/bank/',
344 'agenda' => '/comm/action/',
345 'contract' => '/contrat/',
346 'fichinter' => '/fichinter/',
347 'adherent' => '/adherents/',
348 'categories' => '/categories/',
349 ];
350
351 if (!isset($map[$type])) {
352 return null;
353 }
354
355 $dir = $map[$type];
356
357 // Determine Script based on View
358 // Handle special cases where the path is already a file
359 if (strpos($dir, '.php') !== false) {
360 return [
361 'type' => $type,
362 'path' => $dir
363 ];
364 }
365
366 $script = 'list.php'; // Default
367
368 if ($view === 'card' || $view === 'create') {
369 $script = 'card.php';
370 }
371
372 return [
373 'type' => $type,
374 'path' => $dir . $script
375 ];
376 }
377
386 private function checkPermissions($elementType, $view, $id = 0)
387 {
388 // Default to false
389 $permitted = false;
390
391 // Check permissions based on element type
392 switch ($elementType) {
393 case 'thirdparty':
394 $permitted = $this->user->hasRight('societe', 'lire') ||
395 ($view === 'create' && $this->user->hasRight('societe', 'creer'));
396 break;
397
398 case 'contact':
399 $permitted = $this->user->hasRight('societe', 'contact->lire') ||
400 ($view === 'create' && $this->user->hasRight('societe', 'contact->creer'));
401 break;
402
403 case 'product':
404 $permitted = $this->user->hasRight('produit', 'lire') ||
405 ($view === 'create' && $this->user->hasRight('produit', 'creer'));
406 break;
407
408 case 'project':
409 $permitted = $this->user->hasRight('projet', 'lire') ||
410 ($view === 'create' && $this->user->hasRight('projet', 'creer'));
411 break;
412
413 case 'project_task':
414 $permitted = $this->user->hasRight('projet', 'lire');
415 break;
416
417 case 'invoice_customer':
418 $permitted = $this->user->hasRight('facture', 'lire') ||
419 ($view === 'create' && $this->user->hasRight('facture', 'creer'));
420 break;
421
422 case 'invoice_supplier':
423 $permitted = $this->user->hasRight('fournisseur', 'facture->lire') ||
424 ($view === 'create' && $this->user->hasRight('fournisseur', 'facture->creer'));
425 break;
426
427 case 'order':
428 $permitted = $this->user->hasRight('commande', 'lire') ||
429 ($view === 'create' && $this->user->hasRight('commande', 'creer'));
430 break;
431
432 case 'order_supplier':
433 $permitted = $this->user->hasRight('fournisseur', 'commande->lire') ||
434 ($view === 'create' && $this->user->hasRight('fournisseur', 'commande->creer'));
435 break;
436
437 case 'proposal':
438 $permitted = $this->user->hasRight('propal', 'lire') ||
439 ($view === 'create' && $this->user->hasRight('propal', 'creer'));
440 break;
441
442 case 'expedition':
443 $permitted = $this->user->hasRight('expedition', 'lire') ||
444 ($view === 'create' && $this->user->hasRight('expedition', 'creer'));
445 break;
446
447 case 'payment':
448 $permitted = $this->user->hasRight('facture', 'paiement');
449 break;
450
451 case 'payment_supplier':
452 $permitted = $this->user->hasRight('fournisseur', 'facture->paiement');
453 break;
454
455 case 'bank':
456 $permitted = $this->user->hasRight('banque', 'lire') ||
457 ($view === 'create' && $this->user->hasRight('banque', 'creer'));
458 break;
459
460 case 'agenda':
461 $permitted = $this->user->hasRight('agenda', 'myactions->read') ||
462 $this->user->hasRight('agenda', 'allactions->read');
463 break;
464
465 case 'contract':
466 $permitted = $this->user->hasRight('contrat', 'lire') ||
467 ($view === 'create' && $this->user->hasRight('contrat', 'creer'));
468 break;
469
470 case 'fichinter':
471 $permitted = $this->user->hasRight('ficheinter', 'lire') ||
472 ($view === 'create' && $this->user->hasRight('ficheinter', 'creer'));
473 break;
474
475 case 'adherent':
476 $permitted = $this->user->hasRight('adherent', 'lire') ||
477 ($view === 'create' && $this->user->hasRight('adherent', 'creer'));
478 break;
479
480 case 'categories':
481 $permitted = $this->user->hasRight('categorie', 'lire') ||
482 ($view === 'create' && $this->user->hasRight('categorie', 'creer'));
483 break;
484
485 default:
486 // If we don't have specific permission checks, default to read access
487 $permitted = true;
488 break;
489 }
490
491 // If accessing a specific record, check if user has access to that specific record
492 if ($permitted && $id > 0) {
493 $permitted = $this->checkSpecificRecordAccess($elementType, $id);
494 }
495
496 return $permitted;
497 }
498
506 private function checkSpecificRecordAccess($elementType, $id)
507 {
508 global $db, $conf;
509
510 // For thirdparties, check if user has access to this specific thirdparty
511 if ($elementType === 'thirdparty') {
512 require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
513 $soc = new Societe($db);
514 if ($soc->fetch($id) > 0) {
515 return $soc->isInEEC() || $soc->isCustomer() || $soc->isSupplier();
516 }
517 return false;
518 }
519
520 // For projects, check if user is assigned to the project
521 if ($elementType === 'project') {
522 require_once DOL_DOCUMENT_ROOT . '/projet/class/project.class.php';
523 $project = new Project($db);
524 if ($project->fetch($id) > 0) {
525 return $project->restrictedProjectArea($this->user) == 0;
526 }
527 return false;
528 }
529
530 // For other element types, we'll assume access if the user has general permission
531 // In a full implementation, you would check each object type specifically
532 return true;
533 }
534
544 private function generateDescription($type, $view, $id, $statusFilter = '')
545 {
546 global $langs;
547
548 // Load translations
549 $langs->load("companies");
550 $langs->load("bills");
551 $langs->load("orders");
552 $langs->load("propal");
553 $langs->load("projects");
554
555 // Get the label for the element type
556 $label = '';
557 switch ($type) {
558 case 'thirdparty':
559 $label = $langs->trans("ThirdParty");
560 break;
561 case 'contact':
562 $label = $langs->trans("Contact");
563 break;
564 case 'product':
565 $label = $langs->trans("ProductService");
566 break;
567 case 'project':
568 $label = $langs->trans("Project");
569 break;
570 case 'project_task':
571 $label = $langs->trans("Task");
572 break;
573 case 'invoice_customer':
574 $label = $langs->trans("CustomerInvoice");
575 break;
576 case 'invoice_supplier':
577 $label = $langs->trans("SupplierInvoice");
578 break;
579 case 'order':
580 $label = $langs->trans("CustomerOrder");
581 break;
582 case 'order_supplier':
583 $label = $langs->trans("SupplierOrder");
584 break;
585 case 'proposal':
586 $label = $langs->trans("Proposal");
587 break;
588 case 'expedition':
589 $label = $langs->trans("Shipment");
590 break;
591 case 'payment':
592 $label = $langs->trans("Payment");
593 break;
594 case 'payment_supplier':
595 $label = $langs->trans("SupplierPayment");
596 break;
597 case 'bank':
598 $label = $langs->trans("BankAccount");
599 break;
600 case 'agenda':
601 $label = $langs->trans("Event");
602 break;
603 case 'contract':
604 $label = $langs->trans("Contract");
605 break;
606 case 'fichinter':
607 $label = $langs->trans("Intervention");
608 break;
609 case 'adherent':
610 $label = $langs->trans("Member");
611 break;
612 case 'categories':
613 $label = $langs->trans("Category");
614 break;
615 default:
616 $label = ucfirst($type);
617 break;
618 }
619
620 // Generate description based on view and status
621 if ($view === 'list') {
622 $baseDesc = $langs->trans("ListOf") . " " . $label;
623 if (!empty($statusFilter)) {
624 return $baseDesc . " (" . ucfirst($statusFilter) . ")";
625 }
626 return $baseDesc;
627 } elseif ($view === 'create') {
628 return $langs->trans("New") . " " . $label;
629 } elseif ($id > 0) {
630 return $label . " #" . $id;
631 } else {
632 return $label;
633 }
634 }
635}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
Abstract base class for all MCP (Model Context Protocol) tools.
Class to manage projects.
Class to manage third parties objects (customers, suppliers, prospects...)
AI tool for generating navigation URLs in Dolibarr.
resolvePath($input, $view)
Maps user-friendly names to specific Dolibarr paths.
getDefinitions()
Returns an array of tool definitions, including name, description, and input schema.
checkPermissions($elementType, $view, $id=0)
Check if user has permissions for the requested resource using the modern hasRight() method.
generateDescription($type, $view, $id, $statusFilter='')
Generate a human-readable description for the URL.
checkSpecificRecordAccess($elementType, $id)
Check if user has access to a specific record.
execute(string $name, array $args)
Executes the requested tool function based on its name.
getCategories()
Return categories this tool belongs to.
mapStatusToFilter($elementType, $statusFilter)
Maps human-readable status terms to Dolibarr URL parameters for a given element type.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
$conf db user
Active Directory does not allow anonymous connections.
Definition repair.php:134