dolibarr 25.0.0-alpha
crud_objects.class.php
Go to the documentation of this file.
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 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
27require_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
28require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
29require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
30
37{
50 public function __construct(DoliDB $db, $user = null, $conf = null)
51 {
52 $this->db = $db;
53 $this->user = $user;
54 if ($conf !== null) {
55 $this->conf = $conf;
56 }
57 }
58
59
68 private $map = [
69 // --- CUSTOMER OBJECTS ---
70 'proposal' => [
71 'class' => 'Propal',
72 'path' => '/comm/propal/class/propal.class.php',
73 'card' => '/comm/propal/card.php',
74 'date_field' => 'datep', // Propal uses 'datep'
75 'soc_field' => 'socid'
76 ],
77 'order' => [
78 'class' => 'Commande',
79 'path' => '/commande/class/commande.class.php',
80 'card' => '/commande/card.php',
81 'date_field' => 'date_commande', // Commande uses 'date_commande'
82 'soc_field' => 'socid'
83 ],
84 'invoice' => [
85 'class' => 'Facture',
86 'path' => '/compta/facture/class/facture.class.php',
87 'card' => '/compta/facture/card.php',
88 'date_field' => 'date',
89 'soc_field' => 'socid'
90 ],
91 // --- SUPPLIER OBJECTS ---
92 'supplier_proposal' => [
93 'class' => 'SupplierProposal',
94 'path' => '/supplier_proposal/class/supplier_proposal.class.php',
95 'card' => '/supplier_proposal/card.php',
96 'date_field' => 'date', // Uses standard 'date' property for doc date
97 'soc_field' => 'socid'
98 ],
99 'supplier_order' => [
100 'class' => 'CommandeFournisseur',
101 'path' => '/fourn/class/fournisseur.commande.class.php',
102 'card' => '/fourn/commande/card.php',
103 'date_field' => 'date_commande',
104 'soc_field' => 'socid'
105 ],
106 'supplier_invoice' => [
107 'class' => 'FactureFournisseur',
108 'path' => '/fourn/class/fournisseur.facture.class.php',
109 'card' => '/fourn/facture/card.php',
110 'date_field' => 'date',
111 'soc_field' => 'socid'
112 ],
113 // --- LOGISTICS ---
114 'shipment' => [
115 'class' => 'Expedition',
116 'path' => '/expedition/class/expedition.class.php',
117 'card' => '/expedition/card.php',
118 'date_field' => 'date_expedition',
119 'soc_field' => 'socid'
120 ],
121 'reception' => [
122 'class' => 'Reception',
123 'path' => '/reception/class/reception.class.php',
124 'card' => '/reception/card.php',
125 'date_field' => 'date_reception',
126 'soc_field' => 'socid'
127 ],
128 ];
129
136 private const PERM_MAP = [
137 'proposal' => ['propal', 'creer'],
138 'order' => ['commande', 'creer'],
139 'invoice' => ['facture', 'creer'],
140 'supplier_proposal' => ['supplier_proposal', 'creer'],
141 'supplier_order' => ['fournisseur', 'commande'],
142 'supplier_invoice' => ['fournisseur', 'facture'],
143 'shipment' => ['expedition', 'creer'],
144 'reception' => ['reception', 'creer'],
145 ];
146
152 public function getDefinitions(): array
153 {
154 return [
155 // Order tool
156 [
157 "name" => "create_sales_order",
158 "description" => "Create a CUSTOMER SALES ORDER. This is specifically for creating ORDERS that customers place with you. USE THIS TOOL whenever user mentions: 'create', 'new' or 'add' with 'order', 'customer order' or 'sales order'. This is NOT for invoices or supplier orders. Examples of when to use this tool:
159- 'create order for customer X'
160- 'new order for Y'
161- 'add order from customer Z'
162- 'add order for X with 5 items'
163If user says 'order' without any qualifier, they mean a SALES ORDER - use this tool.",
164 "inputSchema" => [
165 "type" => "object",
166 "properties" => [
167 "socid" => [
168 "type" => "integer",
169 "description" => "Customer ID (Thirdparty ID) - REQUIRED"
170 ],
171 "date_commande" => [
172 "type" => "string",
173 "description" => "Order date (YYYY-MM-DD format, optional, defaults to today)"
174 ],
175 "note" => [
176 "type" => "string",
177 "description" => "Order notes (optional)"
178 ],
179 "lines" => [
180 "type" => "array",
181 "description" => "Products being ordered by the customer",
182 "items" => [
183 "type" => "object",
184 "properties" => [
185 "product_id" => ["type" => "integer", "default" => 0, "description" => "Product ID (0 if not found)"],
186 "description" => ["type" => "string", "description" => "Product name or description"],
187 "quantity" => ["type" => "number", "default" => 1, "description" => "Quantity ordered"],
188 "unit_price" => ["type" => "number", "description" => "Selling price per unit (optional)"],
189 "vat_rate" => ["type" => "number", "description" => "VAT rate (optional, auto-calculated if not provided)"]
190 ],
191 "required" => ["quantity"]
192 ]
193 ]
194 ],
195 "required" => ["socid"]
196 ]
197 ],
198 // Invoice tool
199 [
200 "name" => "create_customer_invoice",
201 "description" => "Create a customer invoice (bill). Do NOT use this for orders - use create_sales_order instead. Do NOT use this for payments - use pay_invoice instead. Examples: 'create invoice for customer X', 'new bill customer Y'",
202 "inputSchema" => [
203 "type" => "object",
204 "properties" => [
205 "object_type" => [
206 "type" => "string",
207 "enum" => ["invoice"],
208 "default" => "invoice"
209 ],
210 "header" => [
211 "type" => "object",
212 "description" => "Invoice header data. Must include 'socid' (Customer ID).",
213 "properties" => [
214 "socid" => ["type" => "integer", "description" => "Customer ID (Thirdparty ID)"],
215 "date" => ["type" => "string", "description" => "Invoice date (YYYY-MM-DD)"],
216 "note_public" => ["type" => "string", "description" => "Public note"],
217 "note_private" => ["type" => "string", "description" => "Private note"]
218 ],
219 "required" => ["socid"]
220 ],
221 "lines" => [
222 "type" => "array",
223 "description" => "Invoice line items.",
224 "items" => [
225 "type" => "object",
226 "properties" => [
227 "product_id" => ["type" => "integer", "default" => 0],
228 "description" => ["type" => "string"],
229 "quantity" => ["type" => "number", "default" => 1],
230 "unit_price" => ["type" => "number"],
231 "vat_rate" => ["type" => "number"],
232 "fk_unit" => ["type" => "integer"]
233 ],
234 "required" => ["quantity"]
235 ]
236 ]
237 ],
238 "required" => ["header"]
239 ]
240 ],
241 // Generic tool for other documents (excluding order and invoice)
242 [
243 "name" => "create_other_document",
244 "description" => "Create documents other than orders and invoices. Use this for: 'proposal', 'supplier_order', 'supplier_invoice', 'supplier_proposal'. DO NOT use for 'order' or 'invoice' - they have dedicated tools.",
245 "inputSchema" => [
246 "type" => "object",
247 "properties" => [
248 "object_type" => [
249 "type" => "string",
250 "enum" => ['proposal', 'supplier_order', 'supplier_invoice', 'supplier_proposal'],
251 "description" => "Document type. Cannot be 'order' or 'invoice'."
252 ],
253 "header" => [
254 "type" => "object",
255 "description" => "Header data. Must include 'socid'.",
256 "properties" => [
257 "socid" => ["type" => "integer", "description" => "Thirdparty ID (Customer for proposal, Supplier for supplier_*)"],
258 "date" => ["type" => "string", "description" => "Document date (YYYY-MM-DD)"],
259 "duree_validite" => ["type" => "integer", "description" => "Validity in days (proposal only)"],
260 "note_public" => ["type" => "string", "description" => "Public note"],
261 "note_private" => ["type" => "string", "description" => "Private note"]
262 ],
263 "required" => ["socid"]
264 ],
265 "lines" => [
266 "type" => "array",
267 "description" => "Array of line items.",
268 "items" => [
269 "type" => "object",
270 "properties" => [
271 "product_id" => ["type" => "integer", "default" => 0],
272 "description" => ["type" => "string"],
273 "quantity" => ["type" => "number", "default" => 1],
274 "unit_price" => ["type" => "number"],
275 "vat_rate" => ["type" => "number"],
276 "fk_unit" => ["type" => "integer"]
277 ],
278 "required" => ["quantity"]
279 ]
280 ]
281 ],
282 "required" => ["object_type", "header"]
283 ]
284 ],
285 [
286 "name" => "add_line_item",
287 "description" => "Add a single line to an existing draft document.",
288 "inputSchema" => [
289 "type" => "object",
290 "properties" => [
291 "object_type" => ["type" => "string", "enum" => array_keys($this->map)],
292 "parent_id" => ["type" => "integer"],
293 "product_id" => ["type" => "integer", "default" => 0],
294 "description" => ["type" => "string"],
295 "quantity" => ["type" => "number", "default" => 1],
296 "unit_price" => ["type" => "number"],
297 "vat_rate" => ["type" => "number"]
298 ],
299 "required" => ["object_type", "parent_id", "quantity"]
300 ]
301 ],
302 [
303 "name" => "delete_object",
304 "description" => "Delete a Draft document.",
305 "inputSchema" => [
306 "type" => "object",
307 "properties" => [
308 "object_type" => ["type" => "string", "enum" => array_keys($this->map)],
309 "id" => ["type" => "integer"]
310 ],
311 "required" => ["object_type", "id"]
312 ]
313 ]
314 ];
315 }
316
323 public function getCategories(): array
324 {
325 return ['commercial', 'billing', 'thirdparty'];
326 }
327
335 public function execute(string $name, array $args)
336 {
337 global $langs, $conf, $mysoc;
338
339 // Use the user injected via constructor (McpHandler). Fall back to global $user
340 // when running in a web session context (e.g. AI Assistant) where DI is not used.
341 // is_object() is used instead of empty() because the parent McpTool declares $user
342 // with a non-nullable type hint, which makes PHPStan flag empty($this->user) as
343 // unreachable code.
344 if (!is_object($this->user) || empty($this->user->id)) {
345 global $user;
346 if (is_object($user) && !empty($user->id)) {
347 $this->user = $user;
348 } else {
349 return ["error" => "User not authenticated."];
350 }
351 }
352
353 if (!is_object($mysoc) || empty($mysoc->id)) {
354 $mysoc = new Societe($this->db);
355 $mysoc->setMysoc($conf);
356 }
357
358 $langs->loadLangs(["main", "bills", "companies", "orders", "propal", "products", "supplier_orders", "supplier_proposals", "sendings", "receptions"]);
359
360 try {
361 switch ($name) {
362 case 'create_sales_order':
363 // Direct mapping for order
364 $args['object_type'] = 'order';
365 // Reorganize args to match createDocument format
366 if (!isset($args['header']) && isset($args['socid'])) {
367 $args['header'] = [
368 'socid' => $args['socid'],
369 'date_commande' => $args['date_commande'] ?? null,
370 'note' => $args['note'] ?? null
371 ];
372 unset($args['socid'], $args['date_commande'], $args['note']);
373 }
374 return $this->createDocument($args);
375
376 case 'create_customer_invoice':
377 $args['object_type'] = 'invoice';
378 return $this->createDocument($args);
379
380 case 'create_other_document':
381 // object_type is already set in args
382 return $this->createDocument($args);
383
384 case 'add_line_item':
385 return $this->addLineItem($args);
386
387 case 'delete_object':
388 return $this->deleteObject($args);
389
390 default:
391 return ["error" => "Unknown tool: $name"];
392 }
393 } catch (Exception $e) {
394 return ["error" => "Exception: " . $e->getMessage()];
395 }
396 }
397
409 private function createDocument(array $args)
410 {
411 $type = (string) $args['object_type'];
412
413 // Validate type against map
414 if (! isset($this->map[$type])) {
415 return ["error" => "Configuration not found for object type: " . $type];
416 }
417
418 // Check permissions
419 $permError = $this->checkPermission($type);
420 if ($permError !== null) {
421 return $permError;
422 }
423
425 $confMap = $this->map[$type];
426
427 // Instantiate the specific Dolibarr class (Propal, Commande, etc.)
428 // We treat it as 'mixed' or generic object here to allow dynamic property assignment
429 $obj = $this->instantiate($type);
430
431 // Process Header with Field Mapping
432 foreach ($args['header'] as $k => $v) {
433 $key = (string) $k;
434
435 // Map 'date' to specific date field (e.g., date_commande)
436 if ($key === 'date' && isset($confMap['date_field'])) {
437 $key = $confMap['date_field'];
438 }
439 // Map 'socid' to specific soc field
440 if ($key === 'socid' && isset($confMap['soc_field'])) {
441 $key = $confMap['soc_field'];
442 $obj->fk_soc = $v; // Standard Dolibarr field for thirdparty linkage
443 }
444
445 // Convert date strings to timestamp if needed
446 if (strpos($key, 'date') !== false && ! is_numeric($v) && is_string($v)) {
447 $timestamp = strtotime($v);
448 if ($timestamp !== false) {
449 $v = $timestamp;
450 }
451 }
452
453 // Assign value dynamically
454 // PHPStan normally dislikes dynamic property access on objects, so we suppress it for this mapper logic
456 $obj->{$key} = $v;
457 }
458
459 // Set Defaults
460 $dateField = $confMap['date_field'] ?? 'date';
461 // Check if date field is empty (property might not exist or be null/0)
462 if (empty($obj->{$dateField})) {
464 $obj->{$dateField} = dol_now();
465 }
466
467 // Specific default for Proposals
468 if ($type === 'proposal' && empty($obj->duree_validite)) {
469 $obj->duree_validite = 15;
470 }
471
472 // Attempt Creation
473 $id = $obj->create($this->user);
474
475 if ($id <= 0) {
476 $err = (string) $obj->error;
477 if (! empty($obj->errors)) {
478 $err .= " " . json_encode($obj->errors);
479 }
480 return ["error" => "Creation failed ($type): " . $err];
481 }
482
483 // Process Lines (if provided)
484 $linesAdded = 0;
485 $lineErrors = [];
486
487 if (! empty($args['lines']) && is_array($args['lines'])) {
488 foreach ($args['lines'] as $line) {
489 $line['object_type'] = $type;
490 $line['parent_id'] = $id;
491
492 // Process line addition
493 // Assumes processAddLine returns array{success: bool, error?: string}
494 $res = $this->processAddLine($obj, $line);
495
496 if (! empty($res['success'])) {
497 $linesAdded++;
498 } else {
499 $lineErrors[] = isset($res['error']) ? (string) $res['error'] : 'Unknown line error';
500 }
501 }
502 }
503
504 return [
505 "success" => true,
506 "id" => (int) $id,
507 "ref" => (string) $obj->ref,
508 "lines_added" => $linesAdded,
509 "line_errors" => $lineErrors,
510 "url" => DOL_URL_ROOT . $confMap['card'] . "?id=" . $id
511 ];
512 }
513
532 private function processAddLine(CommonObject $object, array $args)
533 {
534 global $mysoc, $conf;
535 // Check status (Dolibarr objects usually use 'statut' property, 0 = Draft)
536 if (isset($object->statut) && $object->statut != 0) {
537 return ["success" => false, "error" => "Document is not in draft status"];
538 }
539
540 // Ensure Thirdparty is loaded
541 if (empty($object->thirdparty)) {
542 $object->fetch_thirdparty();
543 }
544
545 // Get company default VAT
546 $companyDefaultVAT = 0.0;
547 if (! empty($conf->global->MAIN_VAT_DEFAULT)) {
548 $companyDefaultVAT = (float) $conf->global->MAIN_VAT_DEFAULT;
549 }
550
551 // Normalize Inputs
552 $productIdentifier = isset($args['product']) ? (string) $args['product'] : (isset($args['description']) ? (string) $args['description'] : '');
553
554 $qtyInput = $args['qty'] ?? $args['quantity'] ?? 1;
555 $qty = (float) $qtyInput;
556
557 $priceInput = isset($args['price']) ? $args['price'] : ($args['unit_price'] ?? null);
558 $price = ($priceInput !== null) ? (float) $priceInput : null;
559
560 $vat = isset($args['vat_rate']) ? (float) $args['vat_rate'] : null;
561 $discount = isset($args['discount']) ? (float) $args['discount'] : 0.0;
562
563 if ($qty <= 0) {
564 return ["success" => false, "error" => "Quantity must be positive."];
565 }
566
567 // Find product
569 $prod = null;
570
571 if ($productIdentifier !== '') {
572 $findResult = $this->findProduct($productIdentifier);
573 if (is_array($findResult) && isset($findResult['error'])) {
574 // Only abort if the caller EXPLICITLY asked for a product (via the 'product'
575 // argument). If they only provided a free-text 'description', we silently
576 // fall through with $prod = null so Dolibarr creates a free-text line item,
577 // which is a perfectly valid Dolibarr feature.
578 // Previous behaviour aborted ALL line creations whose description didn't
579 // match an existing product reference, which broke AI-driven creation of
580 // invoices/orders/proposals from one-off line descriptions.
581 if (isset($args['product'])) {
582 return array_merge(['success' => false], $findResult);
583 }
584 // fall through: $prod stays null
585 }
586 if (is_object($findResult)) {
587 $prod = $findResult;
588 }
589 }
590
591 // Set values based on product or user input
592 if ($price === null) {
593 $price = ($prod && isset($prod->price)) ? (float) $prod->price : 0.0;
594 }
595
596 if ($vat === null) {
597 $vat = ($prod && isset($prod->tva_tx) && $prod->tva_tx !== '') ? (float) $prod->tva_tx : $companyDefaultVAT;
598 }
599
600 // Description
601 $userDesc = isset($args['description']) ? (string) $args['description'] : '';
602 $desc = '';
603
604 if ($userDesc !== '') {
605 if ($prod && ! empty($prod->label)) {
606 if (strtolower($userDesc) === strtolower($prod->label)) {
607 $desc = $prod->label;
608 } else {
609 $desc = $prod->label . ' - ' . $userDesc;
610 }
611 } else {
612 $desc = $userDesc;
613 }
614 } else {
615 if ($prod && ! empty($prod->label)) {
616 $desc = $prod->label;
617 }
618 }
619
620 // Product Unit handling
621 $fk_unit = 0;
622 if (! empty($conf->global->PRODUCT_USE_UNITS) && $prod && ! empty($prod->fk_unit)) {
623 $fk_unit = (int) $prod->fk_unit;
624 }
625
626 // Add the line
627 $res = 0;
628 $docType = (string) $args['object_type'];
629 $fkProduct = ($prod && isset($prod->id)) ? (int) $prod->id : 0;
630 $prodType = ($prod && isset($prod->type)) ? (int) $prod->type : 0;
631
632 if ($docType === 'invoice') {
634 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, '', '', 0, 0, '', 'HT', 0, $prodType, -1, 0, '', 0);
635 } elseif ($docType === 'order') {
637 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0, 'HT', 0, '', '', $prodType);
638 } elseif ($docType === 'proposal') {
640 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 'HT', 0, 0, $prodType);
641 } elseif ($docType === 'supplier_invoice') {
643 // IMPORTANT: FactureFournisseur::addline() does NOT share the same signature
644 // as Facture::addline(). Its parameter order is:
645 // ($desc, $pu, $txtva, $txlocaltax1, $txlocaltax2, $qty, $fk_product, $remise_percent, ...)
646 // i.e. $qty is in position 6, not 3 (unlike customer Facture / Commande / Propal).
647 // The previous call passed our $qty as $txtva (-> a 1% VAT rate) and our $vat
648 // as $txlocaltax1, and position 6 ended up being a hardcoded 0 -> a line was
649 // inserted with qty=0, which Dolibarr silently dropped from the visible totals.
650 $res = $object->addline($desc, $price, $vat, 0, 0, $qty, $fkProduct, $discount, '', '', 0, 0, 'HT', $prodType);
651 } elseif ($docType === 'supplier_order') {
653 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0, 'HT', 0, '', '', $prodType);
654 } elseif ($docType === 'supplier_proposal') {
656 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0, 'HT', 0, '', '', $prodType);
657 } elseif ($docType === 'shipment') {
658 // Shipment Logic
659 if (! getDolGlobalString('SHIPMENT_STANDALONE')) {
660 return ["success" => false, "error" => "Shipment standalone mode required to add lines manually."];
661 }
662 // addlinefree(qty, type, fk_product, fk_unit, weight, desc, weight_units)
664 $res = $object->addlinefree($qty, 'shipping', $fkProduct, $fk_unit, 0, $desc, 0);
665 } elseif ($docType === 'reception') {
666 // Reception Logic
667 if (! getDolGlobalString('RECEPTION_STANDALONE')) {
668 return ["success" => false, "error" => "Reception standalone mode required to add lines manually."];
669 }
670 require_once DOL_DOCUMENT_ROOT . '/reception/class/receptionlinebatch.class.php';
671 // addlinefree(qty, type, fk_product, fk_unit, weight, desc, weight_units)
673 $res = $object->addlinefree($qty, 'reception', $fkProduct, $fk_unit, 0, $desc, 0);
674 } else {
675 return ["success" => false, "error" => "Type $docType not supported for lines"];
676 }
677
678 // Update unit if needed (Logic for standard docs, Shipment/Reception handle units in addlinefree)
679 // Only trigger updateLineUnit for the standard commercial documents
680 $commercialDocs = ['invoice', 'order', 'proposal', 'supplier_invoice', 'supplier_order', 'supplier_proposal'];
681 if (in_array($docType, $commercialDocs, true) && $res > 0 && $fk_unit > 0 && ! empty($conf->global->PRODUCT_USE_UNITS)) {
682 $this->updateLineUnit($docType, $res, $fk_unit);
683 }
684
685 if ($res > 0) {
686 return [
687 "success" => true,
688 "line_id" => (int) $res,
689 "debug" => [
690 "product_identifier" => $productIdentifier,
691 "final_description" => $desc
692 ]
693 ];
694 }
695
696 $errorMsg = isset($object->error) ? (string) $object->error : 'Unknown error adding line';
697 return ["success" => false, "error" => $errorMsg];
698 }
699
708 private function addLineItem(array $args)
709 {
710 $type = (string) $args['object_type'];
711
712 // Check Permissions
713 $permError = $this->checkPermission($type);
714 if ($permError !== null) {
715 return [
716 'success' => false,
717 'error' => $permError['error'] ?? 'Permission denied'
718 ];
719 }
720
721 $parentId = (int) $args['parent_id'];
722
723 // Instantiate and Fetch the Parent Document
724 try {
725 $obj = $this->instantiate($type);
726 } catch (Exception $e) {
727 return ["success" => false, "error" => $e->getMessage()];
728 }
729
730 if (! method_exists($obj, 'fetch')) {
731 return ["success" => false, "error" => "Object does not support fetching"];
732 }
733
734 $result = $obj->fetch($parentId);
735 if ($result <= 0) {
736 return ["success" => false, "error" => "Parent document not found with ID: " . $parentId];
737 }
738
739 // Map Schema arguments to Helper arguments
740 // The helper expects 'product' (which can be an ID or Ref), but schema sends 'product_id'
741 if (! empty($args['product_id'])) {
742 $args['product'] = (string) $args['product_id'];
743 }
744
745 // Call the helper logic
746 // processAddLine(CommonObject $object, array $args)
747 return $this->processAddLine($obj, $args);
748 }
749
757 private function findProduct($identifier)
758 {
759 $product = new Product($this->db);
760 // Cast strictly to string for string manipulation
761 $searchString = trim((string) $identifier);
762
763 // Regex to handle "id: 123" format
764 $matches = [];
765 if (preg_match('/^(?:id)[:\s]+(\d+)$/i', $searchString, $matches)) {
766 $searchString = $matches[1];
767 }
768
769 // Try to Fetch by ID
770 if (is_numeric($searchString)) {
771 if ($product->fetch((int) $searchString) > 0) {
772 return $product;
773 }
774 }
775
776 // Try to Fetch by Ref
777 if ($product->fetch(0, $searchString) > 0) {
778 return $product;
779 }
780
781 // Custom SQL Search (Barcode, Label, Ref - Exact Match)
782
783 $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
784 $sql .= " WHERE (barcode = '" . $this->db->escape($searchString) . "' OR label = '" . $this->db->escape($searchString) . "' OR ref = '" . $this->db->escape($searchString) . "')";
785 $sql .= " AND entity IN (" . getEntity('product') . ")";
786 $sql .= " LIMIT 1";
787
788 $res = $this->db->query($sql);
789 if ($res) {
790 $numRows = $this->db->num_rows($res);
791 if ($numRows > 0) {
792 $row = $this->db->fetch_object($res);
793 if ($row) {
794 $product->fetch((int) $row->rowid);
795 $this->db->free($res);
796 return $product;
797 }
798 }
799 $this->db->free($res);
800 }
801
802 // Loose match (LIKE) if exact match fails
803 $sql = "SELECT rowid, ref, label FROM " . MAIN_DB_PREFIX . "product";
804 $sql .= " WHERE (ref LIKE '%" . $this->db->escape($searchString) . "%' OR label LIKE '%" . $this->db->escape($searchString) . "%')";
805 $sql .= " AND entity IN (" . getEntity('product') . ")";
806 $sql .= " AND tosell = 1"; // Only fetch products available for sale
807 $sql .= " LIMIT 5";
808
809 $res = $this->db->query($sql);
810
811 if ($res) {
812 $numRows = $this->db->num_rows($res);
813 if ($numRows > 0) {
814 // If exactly one match found via loose search, use it
815 if ($numRows == 1) {
816 $row = $this->db->fetch_object($res);
817 if ($row) {
818 $product->fetch((int) $row->rowid);
819 $this->db->free($res);
820 return $product;
821 }
822 }
823
824 // If multiple matches, return list for ambiguity error
825 $matchList = [];
826 while ($row = $this->db->fetch_object($res)) {
827 $matchList[] = $row->ref . " - " . $row->label;
828 }
829 $this->db->free($res);
830
831 return [
832 "error" => "Multiple products found for '" . $searchString . "'",
833 "matches" => $matchList
834 ];
835 }
836 $this->db->free($res);
837 }
838
839 return ["error" => "Product '" . $searchString . "' not found."];
840 }
841
849 private function deleteObject(array $args): array
850 {
851 $type = (string) $args['object_type'];
852 $id = (int) $args['id'];
853
854 // Check permissions
855 $permError = $this->checkPermission($type);
856 if ($permError !== null) {
857 return $permError;
858 }
859
860 // Instantiate generic object based on type
861 $obj = $this->instantiate($type);
862
863 // Fetch object
864 if ($obj->fetch($id) <= 0) {
865 return ["error" => "Object not found with ID: " . $id];
866 }
867
868 // Check Status: Can only delete drafts (statut == 0)
869 // We use int cast because status might be string '0' in some DB configurations
870 $status = isset($obj->statut) ? (int) $obj->statut : -1;
871
872 if ($status !== 0) {
873 return ["error" => "Can only delete drafts (status 0). Current status: " . $status];
874 }
875
876 // Perform Deletion
877 if ($obj->delete($this->user) > 0) {
878 return ["success" => true];
879 }
880
881 // Capture error message
882 $errorMsg = ! empty($obj->error) ? (string) $obj->error : 'Unknown error';
883
884 return ["error" => "Delete failed: " . $errorMsg];
885 }
886
894 private function checkPermission(string $type): ?array
895 {
896 if (! isset(self::PERM_MAP[$type])) {
897 return ["error" => "Unknown type for permission check: " . $type];
898 }
899 [$module, $perm] = self::PERM_MAP[$type];
900 if (! $this->user->hasRight($module, $perm)) {
901 return ["error" => "Permission denied for action on " . $type];
902 }
903 return null;
904 }
905
914 private function instantiate(string $type): CommonObject
915 {
916 if (! isset($this->map[$type])) {
917 throw new Exception("Unknown type: " . $type);
918 }
919
920 $config = $this->map[$type];
921 $path = (string) $config['path'];
922 $className = (string) $config['class'];
923
924 // Include the base class and the specific class file
925 require_once DOL_DOCUMENT_ROOT . '/core/class/commonobject.class.php';
926 require_once DOL_DOCUMENT_ROOT . $path;
927
928 if (! class_exists($className)) {
929 throw new Exception("Class '$className' not found for type '$type'");
930 }
931
932 return new $className($this->db);
933 }
934
944 private function updateLineUnit(string $type, int $lineId, int $unitId): void
945 {
946 // Map document types to their specific detail tables
948 $tableMap = [
949 'invoice' => 'facturedet',
950 'order' => 'commandedet',
951 'proposal' => 'propaldet',
952 'supplier_invoice' => 'facture_fourn_det',
953 'supplier_order' => 'commande_fournisseurdet',
954 'supplier_proposal' => 'supplier_proposaldet'
955 ];
956
957 if (! isset($tableMap[$type])) {
958 return;
959 }
960
961 $table = $tableMap[$type];
962
963 $sql = "UPDATE " . MAIN_DB_PREFIX . $this->db->sanitize($table);
964 $sql .= " SET fk_unit = " . (int) $unitId;
965 $sql .= " WHERE rowid = " . (int) $lineId;
966
967 $resql = $this->db->query($sql);
968
969 if (! $resql) {
970 dol_syslog("Error updating unit for line $lineId: " . $this->db->lasterror(), LOG_ERR);
971 }
972 }
973}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
if(! $sortfield) if(! $sortorder) $object
Definition account.php:100
Parent class of all other business classes (invoices, contracts, proposals, orders,...
Class to manage Dolibarr database access.
Abstract base class for all MCP (Model Context Protocol) tools.
Class to manage products or services.
Class to manage third parties objects (customers, suppliers, prospects...)
Tool class for CRUD operations on Dolibarr objects TODO Remove all tools in this file.
getCategories()
Return categories this tool belongs to.
addLineItem(array $args)
Entry point for the 'add_line_item' tool.
checkPermission(string $type)
Check if the current user has permission for the given object type.
instantiate(string $type)
Factory Helper to instantiate Dolibarr objects.
__construct(DoliDB $db, $user=null, $conf=null)
Constructor.
findProduct($identifier)
Find a product by various identifiers (ID, Ref, Barcode, Label).
getDefinitions()
Returns an array of tool definitions, including name, description, and input schema.
execute(string $name, array $args)
Executes the requested tool function based on its name.
deleteObject(array $args)
Delete a document object.
global $mysoc
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dol_now($mode='gmt')
Return date for now.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
conf($dolibarr_main_document_root, $realpathconf=null)
Load conf file (file must exists)
Definition inc.php:429
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
buildzip.php
$conf db user
Active Directory does not allow anonymous connections.
Definition repair.php:134