dolibarr 24.0.0-beta
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 *
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
26require_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
27require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
28require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
29
36{
42 public function __construct(DoliDB $db)
43 {
44 $this->db = $db;
45 }
46
47
56 private $map = [
57 // --- CUSTOMER OBJECTS ---
58 'proposal' => [
59 'class' => 'Propal',
60 'path' => '/comm/propal/class/propal.class.php',
61 'card' => '/comm/propal/card.php',
62 'date_field' => 'datep', // Propal uses 'datep'
63 'soc_field' => 'socid'
64 ],
65 'order' => [
66 'class' => 'Commande',
67 'path' => '/commande/class/commande.class.php',
68 'card' => '/commande/card.php',
69 'date_field' => 'date_commande', // Commande uses 'date_commande'
70 'soc_field' => 'socid'
71 ],
72 'invoice' => [
73 'class' => 'Facture',
74 'path' => '/compta/facture/class/facture.class.php',
75 'card' => '/compta/facture/card.php',
76 'date_field' => 'date',
77 'soc_field' => 'socid'
78 ],
79 // --- SUPPLIER OBJECTS ---
80 'supplier_proposal' => [
81 'class' => 'SupplierProposal',
82 'path' => '/supplier_proposal/class/supplier_proposal.class.php',
83 'card' => '/supplier_proposal/card.php',
84 'date_field' => 'date', // Uses standard 'date' property for doc date
85 'soc_field' => 'socid'
86 ],
87 'supplier_order' => [
88 'class' => 'CommandeFournisseur',
89 'path' => '/fourn/class/fournisseur.commande.class.php',
90 'card' => '/fourn/commande/card.php',
91 'date_field' => 'date_commande',
92 'soc_field' => 'socid'
93 ],
94 'supplier_invoice' => [
95 'class' => 'FactureFournisseur',
96 'path' => '/fourn/class/fournisseur.facture.class.php',
97 'card' => '/fourn/facture/card.php',
98 'date_field' => 'date',
99 'soc_field' => 'socid'
100 ],
101 // --- LOGISTICS ---
102 'shipment' => [
103 'class' => 'Expedition',
104 'path' => '/expedition/class/expedition.class.php',
105 'card' => '/expedition/card.php',
106 'date_field' => 'date_expedition',
107 'soc_field' => 'socid'
108 ],
109 'reception' => [
110 'class' => 'Reception',
111 'path' => '/reception/class/reception.class.php',
112 'card' => '/reception/card.php',
113 'date_field' => 'date_reception',
114 'soc_field' => 'socid'
115 ],
116 ];
117
124 private const PERM_MAP = [
125 'proposal' => ['propal', 'creer'],
126 'order' => ['commande', 'creer'],
127 'invoice' => ['facture', 'creer'],
128 'supplier_proposal' => ['supplier_proposal', 'creer'],
129 'supplier_order' => ['fournisseur', 'commande'],
130 'supplier_invoice' => ['fournisseur', 'facture'],
131 'shipment' => ['expedition', 'creer'],
132 'reception' => ['reception', 'creer'],
133 ];
134
140 public function getDefinitions(): array
141 {
142 return [
143 // Order tool
144 [
145 "name" => "create_sales_order",
146 "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:
147- 'create order for customer X'
148- 'new order for Y'
149- 'add order from customer Z'
150- 'add order for X with 5 items'
151If user says 'order' without any qualifier, they mean a SALES ORDER - use this tool.",
152 "inputSchema" => [
153 "type" => "object",
154 "properties" => [
155 "socid" => [
156 "type" => "integer",
157 "description" => "Customer ID (Thirdparty ID) - REQUIRED"
158 ],
159 "date_commande" => [
160 "type" => "string",
161 "description" => "Order date (YYYY-MM-DD format, optional, defaults to today)"
162 ],
163 "note" => [
164 "type" => "string",
165 "description" => "Order notes (optional)"
166 ],
167 "lines" => [
168 "type" => "array",
169 "description" => "Products being ordered by the customer",
170 "items" => [
171 "type" => "object",
172 "properties" => [
173 "product_id" => ["type" => "integer", "default" => 0, "description" => "Product ID (0 if not found)"],
174 "description" => ["type" => "string", "description" => "Product name or description"],
175 "quantity" => ["type" => "number", "default" => 1, "description" => "Quantity ordered"],
176 "unit_price" => ["type" => "number", "description" => "Selling price per unit (optional)"],
177 "vat_rate" => ["type" => "number", "description" => "VAT rate (optional, auto-calculated if not provided)"]
178 ],
179 "required" => ["quantity"]
180 ]
181 ]
182 ],
183 "required" => ["socid"]
184 ]
185 ],
186 // Invoice tool
187 [
188 "name" => "create_customer_invoice",
189 "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'",
190 "inputSchema" => [
191 "type" => "object",
192 "properties" => [
193 "object_type" => [
194 "type" => "string",
195 "enum" => ["invoice"],
196 "default" => "invoice"
197 ],
198 "header" => [
199 "type" => "object",
200 "description" => "Invoice header data. Must include 'socid' (Customer ID).",
201 "properties" => [
202 "socid" => ["type" => "integer", "description" => "Customer ID (Thirdparty ID)"],
203 "date" => ["type" => "string", "description" => "Invoice date (YYYY-MM-DD)"],
204 "note_public" => ["type" => "string", "description" => "Public note"],
205 "note_private" => ["type" => "string", "description" => "Private note"]
206 ],
207 "required" => ["socid"]
208 ],
209 "lines" => [
210 "type" => "array",
211 "description" => "Invoice line items.",
212 "items" => [
213 "type" => "object",
214 "properties" => [
215 "product_id" => ["type" => "integer", "default" => 0],
216 "description" => ["type" => "string"],
217 "quantity" => ["type" => "number", "default" => 1],
218 "unit_price" => ["type" => "number"],
219 "vat_rate" => ["type" => "number"],
220 "fk_unit" => ["type" => "integer"]
221 ],
222 "required" => ["quantity"]
223 ]
224 ]
225 ],
226 "required" => ["header"]
227 ]
228 ],
229 // Generic tool for other documents (excluding order and invoice)
230 [
231 "name" => "create_other_document",
232 "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.",
233 "inputSchema" => [
234 "type" => "object",
235 "properties" => [
236 "object_type" => [
237 "type" => "string",
238 "enum" => ['proposal', 'supplier_order', 'supplier_invoice', 'supplier_proposal'],
239 "description" => "Document type. Cannot be 'order' or 'invoice'."
240 ],
241 "header" => [
242 "type" => "object",
243 "description" => "Header data. Must include 'socid'.",
244 "properties" => [
245 "socid" => ["type" => "integer", "description" => "Thirdparty ID (Customer for proposal, Supplier for supplier_*)"],
246 "date" => ["type" => "string", "description" => "Document date (YYYY-MM-DD)"],
247 "duree_validite" => ["type" => "integer", "description" => "Validity in days (proposal only)"],
248 "note_public" => ["type" => "string", "description" => "Public note"],
249 "note_private" => ["type" => "string", "description" => "Private note"]
250 ],
251 "required" => ["socid"]
252 ],
253 "lines" => [
254 "type" => "array",
255 "description" => "Array of line items.",
256 "items" => [
257 "type" => "object",
258 "properties" => [
259 "product_id" => ["type" => "integer", "default" => 0],
260 "description" => ["type" => "string"],
261 "quantity" => ["type" => "number", "default" => 1],
262 "unit_price" => ["type" => "number"],
263 "vat_rate" => ["type" => "number"],
264 "fk_unit" => ["type" => "integer"]
265 ],
266 "required" => ["quantity"]
267 ]
268 ]
269 ],
270 "required" => ["object_type", "header"]
271 ]
272 ],
273 [
274 "name" => "add_line_item",
275 "description" => "Add a single line to an existing draft document.",
276 "inputSchema" => [
277 "type" => "object",
278 "properties" => [
279 "object_type" => ["type" => "string", "enum" => array_keys($this->map)],
280 "parent_id" => ["type" => "integer"],
281 "product_id" => ["type" => "integer", "default" => 0],
282 "description" => ["type" => "string"],
283 "quantity" => ["type" => "number", "default" => 1],
284 "unit_price" => ["type" => "number"],
285 "vat_rate" => ["type" => "number"]
286 ],
287 "required" => ["object_type", "parent_id", "quantity"]
288 ]
289 ],
290 [
291 "name" => "delete_object",
292 "description" => "Delete a Draft document.",
293 "inputSchema" => [
294 "type" => "object",
295 "properties" => [
296 "object_type" => ["type" => "string", "enum" => array_keys($this->map)],
297 "id" => ["type" => "integer"]
298 ],
299 "required" => ["object_type", "id"]
300 ]
301 ]
302 ];
303 }
304
311 public function getCategories(): array
312 {
313 return ['commercial', 'billing', 'thirdparty'];
314 }
315
323 public function execute(string $name, array $args)
324 {
325 global $user, $langs, $conf, $mysoc;
326
327 // Ensure $this->user is the authenticated global user
328 $this->user = $user;
329
330 if (!$user->id) {
331 return ["error" => "User not authenticated."];
332 }
333
334 if (!is_object($mysoc) || empty($mysoc->id)) {
335 $mysoc = new Societe($this->db);
336 $mysoc->setMysoc($conf);
337 }
338
339 $langs->loadLangs(["main", "bills", "companies", "orders", "propal", "products", "supplier_orders", "supplier_proposals", "sendings", "receptions"]);
340
341 try {
342 switch ($name) {
343 case 'create_sales_order':
344 // Direct mapping for order
345 $args['object_type'] = 'order';
346 // Reorganize args to match createDocument format
347 if (!isset($args['header']) && isset($args['socid'])) {
348 $args['header'] = [
349 'socid' => $args['socid'],
350 'date_commande' => $args['date_commande'] ?? null,
351 'note' => $args['note'] ?? null
352 ];
353 unset($args['socid'], $args['date_commande'], $args['note']);
354 }
355 return $this->createDocument($args);
356
357 case 'create_customer_invoice':
358 $args['object_type'] = 'invoice';
359 return $this->createDocument($args);
360
361 case 'create_other_document':
362 // object_type is already set in args
363 return $this->createDocument($args);
364
365 case 'add_line_item':
366 return $this->addLineItem($args);
367
368 case 'delete_object':
369 return $this->deleteObject($args);
370
371 default:
372 return ["error" => "Unknown tool: $name"];
373 }
374 } catch (Exception $e) {
375 return ["error" => "Exception: " . $e->getMessage()];
376 }
377 }
378
390 private function createDocument(array $args)
391 {
392 $type = (string) $args['object_type'];
393
394 // Validate type against map
395 if (! isset($this->map[$type])) {
396 return ["error" => "Configuration not found for object type: " . $type];
397 }
398
399 // Check permissions
400 $permError = $this->checkPermission($type);
401 if ($permError !== null) {
402 return $permError;
403 }
404
406 $confMap = $this->map[$type];
407
408 // Instantiate the specific Dolibarr class (Propal, Commande, etc.)
409 // We treat it as 'mixed' or generic object here to allow dynamic property assignment
410 $obj = $this->instantiate($type);
411
412 // Process Header with Field Mapping
413 foreach ($args['header'] as $k => $v) {
414 $key = (string) $k;
415
416 // Map 'date' to specific date field (e.g., date_commande)
417 if ($key === 'date' && isset($confMap['date_field'])) {
418 $key = $confMap['date_field'];
419 }
420 // Map 'socid' to specific soc field
421 if ($key === 'socid' && isset($confMap['soc_field'])) {
422 $key = $confMap['soc_field'];
423 $obj->fk_soc = $v; // Standard Dolibarr field for thirdparty linkage
424 }
425
426 // Convert date strings to timestamp if needed
427 if (strpos($key, 'date') !== false && ! is_numeric($v) && is_string($v)) {
428 $timestamp = strtotime($v);
429 if ($timestamp !== false) {
430 $v = $timestamp;
431 }
432 }
433
434 // Assign value dynamically
435 // PHPStan normally dislikes dynamic property access on objects, so we suppress it for this mapper logic
437 $obj->{$key} = $v;
438 }
439
440 // Set Defaults
441 $dateField = $confMap['date_field'] ?? 'date';
442 // Check if date field is empty (property might not exist or be null/0)
443 if (empty($obj->{$dateField})) {
445 $obj->{$dateField} = dol_now();
446 }
447
448 // Specific default for Proposals
449 if ($type === 'proposal' && empty($obj->duree_validite)) {
450 $obj->duree_validite = 15;
451 }
452
453 // Attempt Creation
454 $id = $obj->create($this->user);
455
456 if ($id <= 0) {
457 $err = (string) $obj->error;
458 if (! empty($obj->errors)) {
459 $err .= " " . json_encode($obj->errors);
460 }
461 return ["error" => "Creation failed ($type): " . $err];
462 }
463
464 // Process Lines (if provided)
465 $linesAdded = 0;
466 $lineErrors = [];
467
468 if (! empty($args['lines']) && is_array($args['lines'])) {
469 foreach ($args['lines'] as $line) {
470 $line['object_type'] = $type;
471 $line['parent_id'] = $id;
472
473 // Process line addition
474 // Assumes processAddLine returns array{success: bool, error?: string}
475 $res = $this->processAddLine($obj, $line);
476
477 if (! empty($res['success'])) {
478 $linesAdded++;
479 } else {
480 $lineErrors[] = isset($res['error']) ? (string) $res['error'] : 'Unknown line error';
481 }
482 }
483 }
484
485 return [
486 "success" => true,
487 "id" => (int) $id,
488 "ref" => (string) $obj->ref,
489 "lines_added" => $linesAdded,
490 "line_errors" => $lineErrors,
491 "url" => DOL_URL_ROOT . $confMap['card'] . "?id=" . $id
492 ];
493 }
494
513 private function processAddLine(CommonObject $object, array $args)
514 {
515 global $mysoc, $conf;
516 // Check status (Dolibarr objects usually use 'statut' property, 0 = Draft)
517 if (isset($object->statut) && $object->statut != 0) {
518 return ["success" => false, "error" => "Document is not in draft status"];
519 }
520
521 // Ensure Thirdparty is loaded
522 if (empty($object->thirdparty)) {
523 $object->fetch_thirdparty();
524 }
525
526 // Get company default VAT
527 $companyDefaultVAT = 0.0;
528 if (! empty($conf->global->MAIN_VAT_DEFAULT)) {
529 $companyDefaultVAT = (float) $conf->global->MAIN_VAT_DEFAULT;
530 }
531
532 // Normalize Inputs
533 $productIdentifier = isset($args['product']) ? (string) $args['product'] : (isset($args['description']) ? (string) $args['description'] : '');
534
535 $qtyInput = $args['qty'] ?? $args['quantity'] ?? 1;
536 $qty = (float) $qtyInput;
537
538 $priceInput = isset($args['price']) ? $args['price'] : ($args['unit_price'] ?? null);
539 $price = ($priceInput !== null) ? (float) $priceInput : null;
540
541 $vat = isset($args['vat_rate']) ? (float) $args['vat_rate'] : null;
542 $discount = isset($args['discount']) ? (float) $args['discount'] : 0.0;
543
544 if ($qty <= 0) {
545 return ["success" => false, "error" => "Quantity must be positive."];
546 }
547
548 // Find product
550 $prod = null;
551
552 if ($productIdentifier !== '') {
553 $findResult = $this->findProduct($productIdentifier);
554 if (is_array($findResult) && isset($findResult['error'])) {
555 // Only abort if the caller EXPLICITLY asked for a product (via the 'product'
556 // argument). If they only provided a free-text 'description', we silently
557 // fall through with $prod = null so Dolibarr creates a free-text line item,
558 // which is a perfectly valid Dolibarr feature.
559 // Previous behaviour aborted ALL line creations whose description didn't
560 // match an existing product reference, which broke AI-driven creation of
561 // invoices/orders/proposals from one-off line descriptions.
562 if (isset($args['product'])) {
563 return array_merge(['success' => false], $findResult);
564 }
565 // fall through: $prod stays null
566 }
567 if (is_object($findResult)) {
568 $prod = $findResult;
569 }
570 }
571
572 // Set values based on product or user input
573 if ($price === null) {
574 $price = ($prod && isset($prod->price)) ? (float) $prod->price : 0.0;
575 }
576
577 if ($vat === null) {
578 $vat = ($prod && isset($prod->tva_tx) && $prod->tva_tx !== '') ? (float) $prod->tva_tx : $companyDefaultVAT;
579 }
580
581 // Description
582 $userDesc = isset($args['description']) ? (string) $args['description'] : '';
583 $desc = '';
584
585 if ($userDesc !== '') {
586 if ($prod && ! empty($prod->label)) {
587 if (strtolower($userDesc) === strtolower($prod->label)) {
588 $desc = $prod->label;
589 } else {
590 $desc = $prod->label . ' - ' . $userDesc;
591 }
592 } else {
593 $desc = $userDesc;
594 }
595 } else {
596 if ($prod && ! empty($prod->label)) {
597 $desc = $prod->label;
598 }
599 }
600
601 // Product Unit handling
602 $fk_unit = 0;
603 if (! empty($conf->global->PRODUCT_USE_UNITS) && $prod && ! empty($prod->fk_unit)) {
604 $fk_unit = (int) $prod->fk_unit;
605 }
606
607 // Add the line
608 $res = 0;
609 $docType = (string) $args['object_type'];
610 $fkProduct = ($prod && isset($prod->id)) ? (int) $prod->id : 0;
611 $prodType = ($prod && isset($prod->type)) ? (int) $prod->type : 0;
612
613 if ($docType === 'invoice') {
615 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, '', '', 0, 0, '', 'HT', 0, $prodType, -1, 0, '', 0);
616 } elseif ($docType === 'order') {
618 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0, 'HT', 0, '', '', $prodType);
619 } elseif ($docType === 'proposal') {
621 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 'HT', 0, 0, $prodType);
622 } elseif ($docType === 'supplier_invoice') {
624 // IMPORTANT: FactureFournisseur::addline() does NOT share the same signature
625 // as Facture::addline(). Its parameter order is:
626 // ($desc, $pu, $txtva, $txlocaltax1, $txlocaltax2, $qty, $fk_product, $remise_percent, ...)
627 // i.e. $qty is in position 6, not 3 (unlike customer Facture / Commande / Propal).
628 // The previous call passed our $qty as $txtva (-> a 1% VAT rate) and our $vat
629 // as $txlocaltax1, and position 6 ended up being a hardcoded 0 -> a line was
630 // inserted with qty=0, which Dolibarr silently dropped from the visible totals.
631 $res = $object->addline($desc, $price, $vat, 0, 0, $qty, $fkProduct, $discount, '', '', 0, 0, 'HT', $prodType);
632 } elseif ($docType === 'supplier_order') {
634 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0, 'HT', 0, '', '', $prodType);
635 } elseif ($docType === 'supplier_proposal') {
637 $res = $object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0, 'HT', 0, '', '', $prodType);
638 } elseif ($docType === 'shipment') {
639 // Shipment Logic
640 if (! getDolGlobalString('SHIPMENT_STANDALONE')) {
641 return ["success" => false, "error" => "Shipment standalone mode required to add lines manually."];
642 }
643 // addlinefree(qty, type, fk_product, fk_unit, weight, desc, weight_units)
645 $res = $object->addlinefree($qty, 'shipping', $fkProduct, $fk_unit, 0, $desc, 0);
646 } elseif ($docType === 'reception') {
647 // Reception Logic
648 if (! getDolGlobalString('RECEPTION_STANDALONE')) {
649 return ["success" => false, "error" => "Reception standalone mode required to add lines manually."];
650 }
651 require_once DOL_DOCUMENT_ROOT . '/reception/class/receptionlinebatch.class.php';
652 // addlinefree(qty, type, fk_product, fk_unit, weight, desc, weight_units)
654 $res = $object->addlinefree($qty, 'reception', $fkProduct, $fk_unit, 0, $desc, 0);
655 } else {
656 return ["success" => false, "error" => "Type $docType not supported for lines"];
657 }
658
659 // Update unit if needed (Logic for standard docs, Shipment/Reception handle units in addlinefree)
660 // Only trigger updateLineUnit for the standard commercial documents
661 $commercialDocs = ['invoice', 'order', 'proposal', 'supplier_invoice', 'supplier_order', 'supplier_proposal'];
662 if (in_array($docType, $commercialDocs, true) && $res > 0 && $fk_unit > 0 && ! empty($conf->global->PRODUCT_USE_UNITS)) {
663 $this->updateLineUnit($docType, $res, $fk_unit);
664 }
665
666 if ($res > 0) {
667 return [
668 "success" => true,
669 "line_id" => (int) $res,
670 "debug" => [
671 "product_identifier" => $productIdentifier,
672 "final_description" => $desc
673 ]
674 ];
675 }
676
677 $errorMsg = isset($object->error) ? (string) $object->error : 'Unknown error adding line';
678 return ["success" => false, "error" => $errorMsg];
679 }
680
689 private function addLineItem(array $args)
690 {
691 $type = (string) $args['object_type'];
692
693 // Check Permissions
694 $permError = $this->checkPermission($type);
695 if ($permError !== null) {
696 return [
697 'success' => false,
698 'error' => $permError['error'] ?? 'Permission denied'
699 ];
700 }
701
702 $parentId = (int) $args['parent_id'];
703
704 // Instantiate and Fetch the Parent Document
705 try {
706 $obj = $this->instantiate($type);
707 } catch (Exception $e) {
708 return ["success" => false, "error" => $e->getMessage()];
709 }
710
711 if (! method_exists($obj, 'fetch')) {
712 return ["success" => false, "error" => "Object does not support fetching"];
713 }
714
715 $result = $obj->fetch($parentId);
716 if ($result <= 0) {
717 return ["success" => false, "error" => "Parent document not found with ID: " . $parentId];
718 }
719
720 // Map Schema arguments to Helper arguments
721 // The helper expects 'product' (which can be an ID or Ref), but schema sends 'product_id'
722 if (! empty($args['product_id'])) {
723 $args['product'] = (string) $args['product_id'];
724 }
725
726 // Call the helper logic
727 // processAddLine(CommonObject $object, array $args)
728 return $this->processAddLine($obj, $args);
729 }
730
738 private function findProduct($identifier)
739 {
740 $product = new Product($this->db);
741 // Cast strictly to string for string manipulation
742 $searchString = trim((string) $identifier);
743
744 // Regex to handle "id: 123" format
745 $matches = [];
746 if (preg_match('/^(?:id)[:\s]+(\d+)$/i', $searchString, $matches)) {
747 $searchString = $matches[1];
748 }
749
750 // Try to Fetch by ID
751 if (is_numeric($searchString)) {
752 if ($product->fetch((int) $searchString) > 0) {
753 return $product;
754 }
755 }
756
757 // Try to Fetch by Ref
758 if ($product->fetch(0, $searchString) > 0) {
759 return $product;
760 }
761
762 // Custom SQL Search (Barcode, Label, Ref - Exact Match)
763
764 $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
765 $sql .= " WHERE (barcode = '" . $this->db->escape($searchString) . "' OR label = '" . $this->db->escape($searchString) . "' OR ref = '" . $this->db->escape($searchString) . "')";
766 $sql .= " AND entity IN (" . getEntity('product') . ")";
767 $sql .= " LIMIT 1";
768
769 $res = $this->db->query($sql);
770 if ($res) {
771 $numRows = $this->db->num_rows($res);
772 if ($numRows > 0) {
773 $row = $this->db->fetch_object($res);
774 if ($row) {
775 $product->fetch((int) $row->rowid);
776 $this->db->free($res);
777 return $product;
778 }
779 }
780 $this->db->free($res);
781 }
782
783 // Loose match (LIKE) if exact match fails
784 $sql = "SELECT rowid, ref, label FROM " . MAIN_DB_PREFIX . "product";
785 $sql .= " WHERE (ref LIKE '%" . $this->db->escape($searchString) . "%' OR label LIKE '%" . $this->db->escape($searchString) . "%')";
786 $sql .= " AND entity IN (" . getEntity('product') . ")";
787 $sql .= " AND tosell = 1"; // Only fetch products available for sale
788 $sql .= " LIMIT 5";
789
790 $res = $this->db->query($sql);
791
792 if ($res) {
793 $numRows = $this->db->num_rows($res);
794 if ($numRows > 0) {
795 // If exactly one match found via loose search, use it
796 if ($numRows == 1) {
797 $row = $this->db->fetch_object($res);
798 if ($row) {
799 $product->fetch((int) $row->rowid);
800 $this->db->free($res);
801 return $product;
802 }
803 }
804
805 // If multiple matches, return list for ambiguity error
806 $matchList = [];
807 while ($row = $this->db->fetch_object($res)) {
808 $matchList[] = $row->ref . " - " . $row->label;
809 }
810 $this->db->free($res);
811
812 return [
813 "error" => "Multiple products found for '" . $searchString . "'",
814 "matches" => $matchList
815 ];
816 }
817 $this->db->free($res);
818 }
819
820 return ["error" => "Product '" . $searchString . "' not found."];
821 }
822
830 private function deleteObject(array $args): array
831 {
832 $type = (string) $args['object_type'];
833 $id = (int) $args['id'];
834
835 // Check permissions
836 $permError = $this->checkPermission($type);
837 if ($permError !== null) {
838 return $permError;
839 }
840
841 // Instantiate generic object based on type
842 $obj = $this->instantiate($type);
843
844 // Fetch object
845 if ($obj->fetch($id) <= 0) {
846 return ["error" => "Object not found with ID: " . $id];
847 }
848
849 // Check Status: Can only delete drafts (statut == 0)
850 // We use int cast because status might be string '0' in some DB configurations
851 $status = isset($obj->statut) ? (int) $obj->statut : -1;
852
853 if ($status !== 0) {
854 return ["error" => "Can only delete drafts (status 0). Current status: " . $status];
855 }
856
857 // Perform Deletion
858 if ($obj->delete($this->user) > 0) {
859 return ["success" => true];
860 }
861
862 // Capture error message
863 $errorMsg = ! empty($obj->error) ? (string) $obj->error : 'Unknown error';
864
865 return ["error" => "Delete failed: " . $errorMsg];
866 }
867
875 private function checkPermission(string $type): ?array
876 {
877 if (! isset(self::PERM_MAP[$type])) {
878 return ["error" => "Unknown type for permission check: " . $type];
879 }
880 [$module, $perm] = self::PERM_MAP[$type];
881 if (! $this->user->hasRight($module, $perm)) {
882 return ["error" => "Permission denied for action on " . $type];
883 }
884 return null;
885 }
886
895 private function instantiate(string $type): CommonObject
896 {
897 if (! isset($this->map[$type])) {
898 throw new Exception("Unknown type: " . $type);
899 }
900
901 $config = $this->map[$type];
902 $path = (string) $config['path'];
903 $className = (string) $config['class'];
904
905 // Include the base class and the specific class file
906 require_once DOL_DOCUMENT_ROOT . '/core/class/commonobject.class.php';
907 require_once DOL_DOCUMENT_ROOT . $path;
908
909 if (! class_exists($className)) {
910 throw new Exception("Class '$className' not found for type '$type'");
911 }
912
913 return new $className($this->db);
914 }
915
925 private function updateLineUnit(string $type, int $lineId, int $unitId): void
926 {
927 // Map document types to their specific detail tables
929 $tableMap = [
930 'invoice' => 'facturedet',
931 'order' => 'commandedet',
932 'proposal' => 'propaldet',
933 'supplier_invoice' => 'facture_fourn_det',
934 'supplier_order' => 'commande_fournisseurdet',
935 'supplier_proposal' => 'supplier_proposaldet'
936 ];
937
938 if (! isset($tableMap[$type])) {
939 return;
940 }
941
942 $table = $tableMap[$type];
943
944 $sql = "UPDATE " . MAIN_DB_PREFIX . $this->db->escape($table);
945 $sql .= " SET fk_unit = " . (int) $unitId;
946 $sql .= " WHERE rowid = " . (int) $lineId;
947
948 $resql = $this->db->query($sql);
949
950 if (! $resql) {
951 dol_syslog("Error updating unit for line $lineId: " . $this->db->lasterror(), LOG_ERR);
952 }
953 }
954}
$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)
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.
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