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/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 getRequiredRights(string $toolName)
83 {
84 return self::RIGHTS_ENFORCED_DOWNSTREAM;
85 }
86
93 public function getCategories(): array
94 {
95 return ['global'];
96 }
97
105 public function execute(string $name, array $args)
106 {
107 global $langs, $db;
108
109 // Load translation files
110 $langs->load("companies");
111 $langs->load("bills");
112 $langs->load("orders");
113 $langs->load("propal");
114 $langs->load("projects");
115 $langs->load("sendings");
116
117 if ($name !== 'navigate_to_page') {
118 return null;
119 }
120
121 if (empty($this->user->id)) {
122 return ["error" => "Permission Denied: User not logged in."];
123 }
124
125 $rawType = $args['object_type'] ?? '';
126 $view = $args['view'] ?? 'list';
127 $id = (int) ($args['id'] ?? 0);
128 $ref = $args['ref'] ?? '';
129 $statusFilter = $args['status_filter'] ?? '';
130 $params = $args['params'] ?? [];
131
132 // Resolve Logical Object to Physical Path (with Aliases)
133 $pathInfo = $this->resolvePath($rawType, $view);
134
135 if (empty($pathInfo)) {
136 return ["error" => "Unknown object type: '$rawType'. Try 'invoice', 'order', or 'thirdparty'."];
137 }
138
139 $relativePath = $pathInfo['path'];
140 $elementType = $pathInfo['type'];
141
142 // Check permissions
143 if (!$this->checkPermissions($elementType, $view, $id)) {
144 return ["error" => "Permission Denied: You don't have permission to access this resource."];
145 }
146
147 // Build Query Parameters
148 $getQueryParams = [];
149
150 // Handle Action/ID logic
151 if ($id > 0) {
152 $getQueryParams['id'] = $id;
153 } elseif (!empty($ref)) {
154 $getQueryParams['ref'] = $ref;
155 }
156
157 // Set action for create view
158 if ($view === 'create') {
159 $getQueryParams['action'] = 'create';
160 }
161
162 // Handle Status Filtering
163 if ($view === 'list' && !empty($statusFilter)) {
164 $statusParam = $this->mapStatusToFilter($elementType, $statusFilter);
165 if ($statusParam) {
166 $getQueryParams = array_merge($getQueryParams, $statusParam);
167 } else {
168 return ["error" => "Unknown status filter '$statusFilter' for object type '$rawType'."];
169 }
170 } elseif (!empty($statusFilter)) {
171 return ["error" => "The 'status_filter' parameter can only be used with the 'list' view."];
172 }
173
174 // Merge extra params
175 if (!empty($params) && is_array($params)) {
176 $getQueryParams = array_merge($getQueryParams, $params);
177 }
178
179 // Generate Native URL
180 $baseUrl = dol_buildpath($relativePath, 1);
181
182 $finalUrl = $baseUrl;
183 if (!empty($getQueryParams)) {
184 $finalUrl .= '?' . http_build_query($getQueryParams);
185 }
186
187 return [
188 "url" => $finalUrl,
189 "description" => $this->generateDescription($elementType, $view, $id, $statusFilter),
190 "meta" => [
191 "resolved_type" => $elementType,
192 "path" => $relativePath
193 ]
194 ];
195 }
196
205 private function mapStatusToFilter($elementType, $statusFilter)
206 {
207 $statusFilter = strtolower(trim($statusFilter));
208
209 // Master map of element types to their status filters
210 $statusMap = [
211 'invoice_customer' => [
212 'draft' => ['statut' => 0],
213 'unpaid' => ['statut' => 1], // Validated but not paid
214 'paid' => ['statut' => 2],
215 ],
216 'invoice_supplier' => [
217 'draft' => ['statut' => 0],
218 'unpaid' => ['statut' => 1],
219 'paid' => ['statut' => 2],
220 ],
221 'order' => [
222 'draft' => ['statut' => 0],
223 'validated' => ['statut' => 1],
224 'shipped' => ['statut' => 2], // Or partially shipped
225 'closed' => ['statut' => 3],
226 'canceled' => ['statut' => -1],
227 ],
228 'order_supplier' => [
229 'draft' => ['statut' => 0],
230 'validated' => ['statut' => 1],
231 'approved' => ['statut' => 2],
232 'received' => ['statut' => 3], // Or partially received
233 'canceled' => ['statut' => -1],
234 ],
235 'proposal' => [
236 'draft' => ['statut' => 0],
237 'open' => ['statut' => 1],
238 'signed' => ['statut' => 2],
239 'billed' => ['statut' => 3],
240 'refused' => ['statut' => 4],
241 'canceled' => ['statut' => 5],
242 ],
243 'project' => [
244 'draft' => ['status' => 0],
245 'open' => ['status' => 1],
246 'closed' => ['status' => 2],
247 ],
248 'expedition' => [ // Shipments
249 'draft' => ['status' => 0],
250 'validated' => ['status' => 1],
251 'shipped' => ['status' => 2],
252 'canceled' => ['status' => -1],
253 ],
254 'contract' => [
255 'draft' => ['statut' => 0],
256 'active' => ['statut' => 1],
257 'closed' => ['statut' => 2],
258 'resiliated' => ['statut' => 3], // Resiliated
259 ],
260 'fichinter' => [ // Interventions
261 'draft' => ['statut' => 0],
262 'validated' => ['statut' => 1],
263 'billed' => ['statut' => 2],
264 'closed' => ['statut' => 3],
265 ],
266 // Add other object types as needed
267 ];
268
269 return $statusMap[$elementType][$statusFilter] ?? null;
270 }
271
279 private function resolvePath($input, $view)
280 {
281 $input = strtolower(trim($input));
282
283 // Normalize Aliases (Make the tool robust to LLM guessing)
284 $aliases = [
285 // Invoices
286 'invoice' => 'invoice_customer',
287 'bill' => 'invoice_customer',
288 'facture' => 'invoice_customer',
289 'supplier_invoice' => 'invoice_supplier',
290 'vendor_bill' => 'invoice_supplier',
291 // Thirdparties
292 'company' => 'thirdparty',
293 'societe' => 'thirdparty',
294 'customer' => 'thirdparty',
295 'client' => 'thirdparty',
296 'supplier' => 'thirdparty',
297 'vendor' => 'thirdparty',
298 // Commercial
299 'propal' => 'proposal',
300 'quote' => 'proposal',
301 'command' => 'order',
302 'customer_order' => 'order',
303 'supplier_order' => 'order_supplier',
304 // Products/Services
305 'product' => 'product',
306 'service' => 'product',
307 // Projects
308 'project' => 'project',
309 'task' => 'project_task',
310 // Shipping
311 'shipment' => 'expedition',
312 'shipping' => 'expedition',
313 'delivery' => 'expedition',
314 // Payments
315 'payment' => 'payment',
316 'payment_customer' => 'payment',
317 'payment_supplier' => 'payment_supplier',
318 // Banking
319 'transaction' => 'bank',
320 'account' => 'bank',
321 'bank_account' => 'bank',
322 // Events
323 'event' => 'agenda',
324 'agenda' => 'agenda',
325 'appointment' => 'agenda',
326 // Contracts
327 'contract' => 'contract',
328 // Interventions
329 'intervention' => 'fichinter',
330 // Members
331 'member' => 'adherent',
332 'membership' => 'adherent',
333 // Categories
334 'category' => 'categories',
335 ];
336
337 $type = $aliases[$input] ?? $input;
338
339 // Map Normalized Types to Physical Paths
340 $map = [
341 'thirdparty' => '/societe/',
342 'contact' => '/contact/',
343 'product' => '/product/',
344 'project' => '/projet/',
345 'project_task' => '/projet/tasks/',
346 'invoice_customer' => '/compta/facture/',
347 'invoice_supplier' => '/fourn/facture/',
348 'order' => '/commande/',
349 'order_supplier' => '/fourn/commande/',
350 'proposal' => '/comm/propal/',
351 'expedition' => '/expedition/',
352 'payment' => '/compta/paiement.php', // Direct file, not directory
353 'payment_supplier' => '/fourn/paiement.php', // Direct file, not directory
354 'bank' => '/compta/bank/',
355 'agenda' => '/comm/action/',
356 'contract' => '/contrat/',
357 'fichinter' => '/fichinter/',
358 'adherent' => '/adherents/',
359 'categories' => '/categories/',
360 ];
361
362 if (!isset($map[$type])) {
363 return null;
364 }
365
366 $dir = $map[$type];
367
368 // Determine Script based on View
369 // Handle special cases where the path is already a file
370 if (strpos($dir, '.php') !== false) {
371 return [
372 'type' => $type,
373 'path' => $dir
374 ];
375 }
376
377 $script = 'list.php'; // Default
378
379 if ($view === 'card' || $view === 'create') {
380 $script = 'card.php';
381 }
382
383 return [
384 'type' => $type,
385 'path' => $dir . $script
386 ];
387 }
388
397 private function checkPermissions($elementType, $view, $id = 0)
398 {
399 // Default to false
400 $permitted = false;
401
402 // Check permissions based on element type
403 switch ($elementType) {
404 case 'thirdparty':
405 $permitted = $this->user->hasRight('societe', 'lire') ||
406 ($view === 'create' && $this->user->hasRight('societe', 'creer'));
407 break;
408
409 case 'contact':
410 $permitted = $this->user->hasRight('societe', 'contact->lire') ||
411 ($view === 'create' && $this->user->hasRight('societe', 'contact->creer'));
412 break;
413
414 case 'product':
415 $permitted = $this->user->hasRight('produit', 'lire') ||
416 ($view === 'create' && $this->user->hasRight('produit', 'creer'));
417 break;
418
419 case 'project':
420 $permitted = $this->user->hasRight('projet', 'lire') ||
421 ($view === 'create' && $this->user->hasRight('projet', 'creer'));
422 break;
423
424 case 'project_task':
425 $permitted = $this->user->hasRight('projet', 'lire');
426 break;
427
428 case 'invoice_customer':
429 $permitted = $this->user->hasRight('facture', 'lire') ||
430 ($view === 'create' && $this->user->hasRight('facture', 'creer'));
431 break;
432
433 case 'invoice_supplier':
434 $permitted = $this->user->hasRight('fournisseur', 'facture->lire') ||
435 ($view === 'create' && $this->user->hasRight('fournisseur', 'facture->creer'));
436 break;
437
438 case 'order':
439 $permitted = $this->user->hasRight('commande', 'lire') ||
440 ($view === 'create' && $this->user->hasRight('commande', 'creer'));
441 break;
442
443 case 'order_supplier':
444 $permitted = $this->user->hasRight('fournisseur', 'commande->lire') ||
445 ($view === 'create' && $this->user->hasRight('fournisseur', 'commande->creer'));
446 break;
447
448 case 'proposal':
449 $permitted = $this->user->hasRight('propal', 'lire') ||
450 ($view === 'create' && $this->user->hasRight('propal', 'creer'));
451 break;
452
453 case 'expedition':
454 $permitted = $this->user->hasRight('expedition', 'lire') ||
455 ($view === 'create' && $this->user->hasRight('expedition', 'creer'));
456 break;
457
458 case 'payment':
459 $permitted = $this->user->hasRight('facture', 'paiement');
460 break;
461
462 case 'payment_supplier':
463 $permitted = $this->user->hasRight('fournisseur', 'facture->paiement');
464 break;
465
466 case 'bank':
467 $permitted = $this->user->hasRight('banque', 'lire') ||
468 ($view === 'create' && $this->user->hasRight('banque', 'creer'));
469 break;
470
471 case 'agenda':
472 $permitted = $this->user->hasRight('agenda', 'myactions->read') ||
473 $this->user->hasRight('agenda', 'allactions->read');
474 break;
475
476 case 'contract':
477 $permitted = $this->user->hasRight('contrat', 'lire') ||
478 ($view === 'create' && $this->user->hasRight('contrat', 'creer'));
479 break;
480
481 case 'fichinter':
482 $permitted = $this->user->hasRight('ficheinter', 'lire') ||
483 ($view === 'create' && $this->user->hasRight('ficheinter', 'creer'));
484 break;
485
486 case 'adherent':
487 $permitted = $this->user->hasRight('adherent', 'lire') ||
488 ($view === 'create' && $this->user->hasRight('adherent', 'creer'));
489 break;
490
491 case 'categories':
492 $permitted = $this->user->hasRight('categorie', 'lire') ||
493 ($view === 'create' && $this->user->hasRight('categorie', 'creer'));
494 break;
495
496 default:
497 // If we don't have specific permission checks, default to read access
498 $permitted = true;
499 break;
500 }
501
502 // If accessing a specific record, check if user has access to that specific record
503 if ($permitted && $id > 0) {
504 $permitted = $this->checkSpecificRecordAccess($elementType, $id);
505 }
506
507 return $permitted;
508 }
509
517 private function checkSpecificRecordAccess($elementType, $id)
518 {
519 global $db, $conf;
520
521 // For thirdparties, check if user has access to this specific thirdparty
522 if ($elementType === 'thirdparty') {
523 require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
524 $soc = new Societe($db);
525 if ($soc->fetch($id) > 0) {
526 return $soc->isInEEC() || $soc->isCustomer() || $soc->isSupplier();
527 }
528 return false;
529 }
530
531 // For projects, check if user is assigned to the project
532 if ($elementType === 'project') {
533 require_once DOL_DOCUMENT_ROOT . '/projet/class/project.class.php';
534 $project = new Project($db);
535 if ($project->fetch($id) > 0) {
536 return $project->restrictedProjectArea($this->user) == 0;
537 }
538 return false;
539 }
540
541 // For other element types, we'll assume access if the user has general permission
542 // In a full implementation, you would check each object type specifically
543 return true;
544 }
545
555 private function generateDescription($type, $view, $id, $statusFilter = '')
556 {
557 global $langs;
558
559 // Load translations
560 $langs->load("companies");
561 $langs->load("bills");
562 $langs->load("orders");
563 $langs->load("propal");
564 $langs->load("projects");
565
566 // Get the label for the element type
567 $label = '';
568 switch ($type) {
569 case 'thirdparty':
570 $label = $langs->trans("ThirdParty");
571 break;
572 case 'contact':
573 $label = $langs->trans("Contact");
574 break;
575 case 'product':
576 $label = $langs->trans("ProductService");
577 break;
578 case 'project':
579 $label = $langs->trans("Project");
580 break;
581 case 'project_task':
582 $label = $langs->trans("Task");
583 break;
584 case 'invoice_customer':
585 $label = $langs->trans("CustomerInvoice");
586 break;
587 case 'invoice_supplier':
588 $label = $langs->trans("SupplierInvoice");
589 break;
590 case 'order':
591 $label = $langs->trans("CustomerOrder");
592 break;
593 case 'order_supplier':
594 $label = $langs->trans("SupplierOrder");
595 break;
596 case 'proposal':
597 $label = $langs->trans("Proposal");
598 break;
599 case 'expedition':
600 $label = $langs->trans("Shipment");
601 break;
602 case 'payment':
603 $label = $langs->trans("Payment");
604 break;
605 case 'payment_supplier':
606 $label = $langs->trans("SupplierPayment");
607 break;
608 case 'bank':
609 $label = $langs->trans("BankAccount");
610 break;
611 case 'agenda':
612 $label = $langs->trans("Event");
613 break;
614 case 'contract':
615 $label = $langs->trans("Contract");
616 break;
617 case 'fichinter':
618 $label = $langs->trans("Intervention");
619 break;
620 case 'adherent':
621 $label = $langs->trans("Member");
622 break;
623 case 'categories':
624 $label = $langs->trans("Category");
625 break;
626 default:
627 $label = ucfirst($type);
628 break;
629 }
630
631 // Generate description based on view and status
632 if ($view === 'list') {
633 $baseDesc = $langs->trans("ListOf") . " " . $label;
634 if (!empty($statusFilter)) {
635 return $baseDesc . " (" . ucfirst($statusFilter) . ")";
636 }
637 return $baseDesc;
638 } elseif ($view === 'create') {
639 return $langs->trans("New") . " " . $label;
640 } elseif ($id > 0) {
641 return $label . " #" . $id;
642 } else {
643 return $label;
644 }
645 }
646}
$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.
getRequiredRights(string $toolName)
This class already checks the rights of every page it can navigate to.
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.
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:141