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