dolibarr 25.0.0-alpha
invoices.class.php
1<?php
2/* Copyright (C) 2026 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2026 Nick Fragoulis
4 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.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 . '/compta/facture/class/facture.class.php';
27require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
28require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
29require_once DOL_DOCUMENT_ROOT . '/compta/paiement/class/paiement.class.php';
30require_once DOL_DOCUMENT_ROOT . '/compta/bank/class/account.class.php';
31require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
32
38class ToolInvoices extends McpTool
39{
40
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
65 private function getUser()
66 {
67 // is_object() instead of empty() to avoid PHPStan flagging empty() on a
68 // non-nullable typed parent property as unreachable.
69 if (is_object($this->user) && !empty($this->user->id)) {
70 return $this->user;
71 }
72 global $user;
73 return is_object($user) && !empty($user->id) ? $user : null;
74 }
75
81 public function getDefinitions(): array
82 {
83 return [
84 [
85 "name" => "search_invoice",
86 "description" => "Search for invoices. By default, lists UNPAID invoices. Excludes drafts.",
87 "inputSchema" => [
88 "type" => "object",
89 "properties" => [
90 "customer" => [
91 "type" => "string",
92 "description" => "Optional: Customer name."
93 ],
94 "status" => [
95 "type" => "string",
96 "enum" => ["unpaid", "paid", "draft", "all"],
97 "description" => "Filter by status. Default is 'unpaid'. 'all' shows history but excludes drafts.",
98 "default" => "unpaid"
99 ],
100 "limit" => [
101 "type" => "integer",
102 "default" => 10
103 ],
104 "ids" => [
105 "type" => "array",
106 "items" => ["type" => "integer"],
107 "description" => "Restrict to these invoice ids (e.g. the rows the user selected on a list). The result then includes count and total sums."
108 ]
109 ]
110 ]
111 ],
112 [
113 "name" => "get_invoice",
114 "description" => "Get details of a specific invoice by ID or Reference.",
115 "inputSchema" => [
116 "type" => "object",
117 "properties" => [
118 "ref" => ["type" => "string", "description" => "Invoice Ref (e.g. FA2401-001)"],
119 "id" => ["type" => "integer", "description" => "Invoice ID"]
120 ],
121 "oneOf" => [
122 ["required" => ["ref"]],
123 ["required" => ["id"]]
124 ]
125 ]
126 ],
127 [
128 "name" => "search_supplier_invoice",
129 "description" => "Search SUPPLIER (vendor/purchase) invoices by supplier name, ref, or payment status. Use for supplier invoices only; for customer invoices use search_invoice.",
130 "inputSchema" => [
131 "type" => "object",
132 "properties" => [
133 "query" => ["type" => "string", "description" => "Supplier name, invoice ref or supplier's own ref (optional)"],
134 "status" => ["type" => "string", "enum" => ["draft", "unpaid", "paid"], "description" => "Filter by status (optional)"],
135 "limit" => ["type" => "integer", "default" => 10],
136 "ids" => [
137 "type" => "array",
138 "items" => ["type" => "integer"],
139 "description" => "Restrict to these supplier invoice ids (e.g. the rows the user selected on a list). The result then includes count and total sums."
140 ]
141 ]
142 ]
143 ],
144 [
145 "name" => "get_supplier_invoice",
146 "description" => "Get details of a specific SUPPLIER (vendor/purchase) invoice by ID or Reference. For customer invoices use get_invoice.",
147 "inputSchema" => [
148 "type" => "object",
149 "properties" => [
150 "ref" => ["type" => "string", "description" => "Supplier invoice Ref"],
151 "id" => ["type" => "integer", "description" => "Supplier invoice ID"]
152 ],
153 "oneOf" => [
154 ["required" => ["ref"]],
155 ["required" => ["id"]]
156 ]
157 ]
158 ],
159 [
160 "name" => "validate_invoice",
161 "description" => "Validate a draft invoice.",
162 "inputSchema" => [
163 "type" => "object",
164 "properties" => [
165 "invoice" => ["type" => "string", "description" => "Invoice ID or Ref."]
166 ],
167 "required" => ["invoice"]
168 ]
169 ],
170 [
171 "name" => "pay_invoice",
172 "description" => "Register a payment for an invoice.",
173 "inputSchema" => [
174 "type" => "object",
175 "properties" => [
176 "invoice" => ["type" => "string", "description" => "Invoice ID or Reference."],
177 "amount" => ["type" => "number", "description" => "Amount to pay. Defaults to full remaining."],
178 "payment_mode" => ["type" => "string", "description" => "Code (VIR, CB, LIQ)."],
179 "bank_account" => ["type" => "string", "description" => "Bank Account Name/Ref."]
180 ],
181 "required" => ["invoice"]
182 ]
183 ]
184 ];
185 }
186
193 public function getRequiredRights(string $toolName)
194 {
195 $map = array(
196 'search_invoice' => array(array('facture', 'lire')),
197 'search_invoices' => array(array('facture', 'lire')),
198 'get_invoice' => array(array('facture', 'lire')),
199 'validate_invoice' => array(array('facture', 'creer')),
200 'pay_invoice' => array(array('facture', 'paiement')),
201 'search_supplier_invoice' => array(array('fournisseur', 'facture', 'lire')),
202 'get_supplier_invoice' => array(array('fournisseur', 'facture', 'lire'))
203 );
204
205 return isset($map[$toolName]) ? $map[$toolName] : self::RIGHTS_UNDECLARED;
206 }
207
215 public function writeConfirmationPreview(string $toolName, array $args)
216 {
217 global $langs;
218
219 $langs->load("other");
220
221 switch ($toolName) {
222 case 'validate_invoice':
223 return $langs->trans("AIPreviewValidateInvoice", (string) ($args['ref'] ?? $args['id'] ?? ''));
224 case 'pay_invoice':
225 return $langs->trans("AIPreviewPayInvoice", price((float) ($args['amount'] ?? 0)), (string) ($args['ref'] ?? $args['id'] ?? ''));
226 default:
227 return McpTool::NO_WRITE;
228 }
229 }
230
237 public function getCategories(): array
238 {
239 return ['billing'];
240 }
241
249 public function execute(string $name, array $args)
250 {
251 switch ($name) {
252 case 'search_invoice':
253 case 'search_invoices':
254 return $this->searchInvoices($args);
255
256 case 'search_supplier_invoice':
257 return $this->searchSupplierInvoices($args);
258 case 'get_supplier_invoice':
259 return $this->getSupplierInvoice($args);
260 case 'get_invoice':
261 return $this->getInvoice($args);
262
263 case 'validate_invoice':
264 return $this->validateInvoice($args);
265
266 case 'pay_invoice':
267 return $this->payInvoice($args);
268
269 default:
270 return ["error" => "Tool function '$name' not found."];
271 }
272 }
273
281 private function searchInvoices($args)
282 {
283 $limit = isset($args['limit']) ? (int) $args['limit'] : 10;
284 $status = isset($args['status']) ? $args['status'] : 'unpaid';
285
286 // Safety fallback
287 if ($limit <= 0) {
288 $limit = 5;
289 }
290 if ($limit > 1000) {
291 dol_syslog("Search DB Error: Too many record requested", LOG_ERR);
292 return ["error" => "DB Error"];
293 }
294
295 $sql = "SELECT f.rowid, f.ref, f.total_ht, f.total_ttc, f.fk_statut, f.paye, f.datef, s.nom
296 FROM " . MAIN_DB_PREFIX . "facture as f
297 LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid
298 WHERE f.entity IN (" . getEntity('facture') . ")";
299
300 // Selection mode: restrict to explicit ids (e.g. the user's checked
301 // rows) and let the server do the arithmetic the single-shot LLM
302 // architecture cannot: the model never sees tool results, so sums
303 // must be computed here. Status filtering is bypassed - a selection
304 // means these exact records, drafts included.
305 $onlyIds = array();
306 if (!empty($args['ids']) && is_array($args['ids'])) {
307 foreach ($args['ids'] as $selId) {
308 if ((int) $selId > 0 && count($onlyIds) < 100) {
309 $onlyIds[] = (int) $selId;
310 }
311 }
312 if (!empty($onlyIds)) {
313 $sql .= " AND f.rowid IN (".$this->db->sanitize(implode(',', $onlyIds)).")";
314 $status = 'ids';
315 }
316 }
317
318 // Status filtering (bypassed entirely in selection mode: the ids ARE the filter)
319 if ($status === 'ids') {
320 // no status clause
321 } elseif ($status === 'draft') {
322 // Explicitly asking for drafts
323 $sql .= " AND f.fk_statut = 0";
324 } elseif ($status === 'paid') {
325 // Fully paid
326 $sql .= " AND f.fk_statut = 2";
327 } elseif ($status === 'all') {
328 // Valid invoices (Unpaid + Paid). EXCLUDES Drafts (0) and Abandoned (3)
329 $sql .= " AND f.fk_statut IN (1, 2)";
330 } else {
331 // Default: 'unpaid'
332 // In Dolibarr: fk_statut=1 means Validated but not fully paid.
333 $sql .= " AND f.fk_statut = 1 AND f.paye = 0";
334 }
335
336 // Customer Filter
337 if (!empty($args['customer'])) {
338 $cust = $this->findCustomer($args['customer']);
339 if (!is_array($cust)) {
340 $sql .= " AND f.fk_soc = " . ((int) $cust->id);
341 } else {
342 $sql .= " AND s.nom LIKE '%" . $this->db->escape($args['customer']) . "%'";
343 }
344 }
345
346 $sql .= " ORDER BY f.datef DESC LIMIT " . ((int) $limit);
347
348 $resql = $this->db->query($sql);
349 $list = [];
350 $sumHt = 0.0;
351 $sumTtc = 0.0;
352
353 if ($resql) {
354 while ($r = $this->db->fetch_object($resql)) {
355 // Double check to ensure no PROV/Drafts slip through unless asked
356 if (!in_array($status, array('draft', 'ids')) && $r->fk_statut == 0) {
357 continue;
358 }
359
360 $ref = ($r->fk_statut == 0 ? "(PROV" . $r->rowid . ")" : $r->ref);
361
362 // Calculate Status Label
363 $statusLabel = "Unknown";
364 if ($r->fk_statut == 0) {
365 $statusLabel = "Draft";
366 } elseif ($r->fk_statut == 1) {
367 $statusLabel = "Unpaid";
368 } elseif ($r->fk_statut == 2) {
369 $statusLabel = "Paid";
370 } elseif ($r->fk_statut == 3) {
371 $statusLabel = "Abandoned";
372 }
373
374 $sumHt += (float) $r->total_ht;
375 $sumTtc += (float) $r->total_ttc;
376 $list[] = [
377 "ref" => $ref,
378 "date" => dol_print_date($this->db->jdate($r->datef), 'day'),
379 "customer" => $r->nom,
380 "amount" => price($r->total_ttc),
381 "status" => $statusLabel,
382 "url" => DOL_URL_ROOT . "/compta/facture/card.php?id=" . $r->rowid
383 ];
384 }
385 $this->db->free($resql);
386 }
387
388 if (empty($list)) {
389 return ["info" => "No " . $status . " invoices found matching your criteria."];
390 }
391
392 if ($status === 'ids') {
393 // Selection mode: hand back the aggregate the user actually asked
394 // for, next to the rows themselves.
395 return [
396 "count" => count($list),
397 "total_ht" => price($sumHt),
398 "total_ttc" => price($sumTtc),
399 "invoices" => $list
400 ];
401 }
402
403 return $list;
404 }
405
413 private function getInvoice($args)
414 {
415 $id = isset($args['ref']) ? $args['ref'] : (isset($args['id']) ? $args['id'] : null);
416
417 $invoice = $this->findInvoice($id);
418 if (is_array($invoice)) {
419 return $invoice;
420 }
421
422 $invoice->fetch_thirdparty();
423 $invoice->fetch_lines();
424
425 $lines = [];
426 foreach ($invoice->lines as $l) {
427 $prodRef = !empty($l->product_ref) ? $l->product_ref : (!empty($l->product_label) ? $l->product_label : '');
428
429 $lines[] = [
430 "product" => $prodRef,
431 "desc" => dol_html_entity_decode(strip_tags($l->desc), ENT_QUOTES),
432 "qty" => (float) $l->qty,
433 "price" => price($l->subprice),
434 "total_line" => price($l->total_ht),
435 "vat" => $l->tva_tx . "%"
436 ];
437 }
438
439 return [
440 "id" => $invoice->id,
441 "ref" => $invoice->ref,
442 "date" => dol_print_date($invoice->date, 'day'),
443 "status" => $invoice->getLibStatut(1),
444 "customer" => $invoice->thirdparty->name,
445 "total_ht" => price($invoice->total_ht),
446 "total_ttc" => price($invoice->total_ttc),
447 "lines" => $lines,
448 "url" => DOL_URL_ROOT . "/compta/facture/card.php?id=" . $invoice->id
449 ];
450 }
451
459 private function validateInvoice($args)
460 {
461 $user = $this->getUser();
462 if ($user === null) {
463 return ["error" => "User not authenticated."];
464 }
465
466 // Checked here as well as centrally: validating is irreversible, so it
467 // must not depend on a single caller remembering to check.
468 if (!$user->hasRight('facture', 'creer')) {
469 return ["error" => "Permission denied: validating an invoice requires the right facture/creer."];
470 }
471 $invoice = $this->findInvoice($args['invoice']);
472 if (is_array($invoice)) {
473 return $invoice;
474 }
475
476 if ($invoice->statut != 0) {
477 return ["error" => "Invoice is already validated."];
478 }
479
480 if ($invoice->validate($user) < 0) {
481 $error = $invoice->error;
482 if (!empty($invoice->errors)) {
483 $error .= ' ' . implode(', ', $invoice->errors);
484 }
485 if (empty(trim($error))) {
486 $error = 'Unknown error (validate returned < 0 with no message)';
487 }
488 return ["error" => "Validation failed: " . $error];
489 }
490
491 $invoice->fetch($invoice->id);
492 return [
493 "success" => true,
494 "new_ref" => $invoice->ref,
495 "status" => "Validated (Unpaid)",
496 "url" => DOL_URL_ROOT . "/compta/facture/card.php?id=" . $invoice->id
497 ];
498 }
499
507 private function payInvoice($args)
508 {
509 $user = $this->getUser();
510 if ($user === null) {
511 return ["error" => "User not authenticated."];
512 }
513
514 // Checked here as well as centrally: a payment cannot be taken back.
515 if (!$user->hasRight('facture', 'paiement')) {
516 return ["error" => "Permission denied: recording a payment requires the right facture/paiement."];
517 }
518 $invoice = $this->findInvoice($args['invoice']);
519 if (is_array($invoice)) {
520 return $invoice;
521 }
522
523 // Cannot pay drafts
524 if ($invoice->statut == 0) {
525 return ["error" => "Cannot pay a Draft invoice. Please validate it first."];
526 }
527
528 $bank = $this->findBankAccount(isset($args['bank_account']) ? $args['bank_account'] : '');
529 if (!$bank) {
530 return ["error" => "No active Bank account found to receive payment."];
531 }
532
533 $remaining = $invoice->total_ttc - $invoice->getSommePaiement();
534 if ($remaining <= 0) {
535 return ["error" => "Invoice is already fully paid."];
536 }
537
538 $amount = isset($args['amount']) ? (float) $args['amount'] : $remaining;
539 if ($amount > $remaining) {
540 $amount = $remaining;
541 }
542
543 $code = isset($args['payment_mode']) ? $args['payment_mode'] : 'VIR';
544 $modeId = dol_getIdFromCode($this->db, $code, 'c_paiement', 'code', 'id');
545
546 $this->db->begin();
547 $payment = new Paiement($this->db);
548 $payment->datepaye = dol_now();
549 $payment->amounts = [$invoice->id => $amount];
550 $payment->paiementid = $modeId;
551 $payment->paiementcode = $code;
552
553 $paymentId = $payment->create($user, 1);
554 if ($paymentId < 0) {
555 $this->db->rollback();
556 return ["error" => "Payment creation failed: " . implode(', ', $payment->errors)];
557 }
558 $payment->fetch($paymentId);
559 if ($payment->addPaymentToBank($user, 'payment', '(Payment via AI)', $bank->rowid, '', '') < 0) {
560 $this->db->rollback();
561 return ["error" => "Failed to add payment to bank ledger."];
562 }
563
564 $this->db->commit();
565
566 return [
567 "success" => true,
568 "paid_amount" => price($amount),
569 "remaining_due" => price($remaining - $amount),
570 "status" => ($remaining - $amount <= 0) ? "Fully Paid" : "Partially Paid",
571 "payment_url" => DOL_URL_ROOT . "/compta/paiement/card.php?id=" . $paymentId
572 ];
573 }
574
581 private function searchSupplierInvoices($args)
582 {
583 if (!$this->user->hasRight('fournisseur', 'facture', 'lire')) {
584 return ["error" => "Permission denied: supplier invoice read right is missing."];
585 }
586
587 $limit = min(25, max(1, (int) ($args['limit'] ?? 10)));
588 $query = trim((string) ($args['query'] ?? ''));
589 $status = (string) ($args['status'] ?? '');
590
591 $sql = "SELECT f.rowid FROM ".MAIN_DB_PREFIX."facture_fourn as f";
592 $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = f.fk_soc";
593 $sql .= " WHERE f.entity IN (".getEntity('facture_fourn').")";
594 $onlyIds = array();
595 if (!empty($args['ids']) && is_array($args['ids'])) {
596 foreach ($args['ids'] as $selId) {
597 if ((int) $selId > 0 && count($onlyIds) < 100) {
598 $onlyIds[] = (int) $selId;
599 }
600 }
601 if (!empty($onlyIds)) {
602 $sql .= " AND f.rowid IN (".$this->db->sanitize(implode(',', $onlyIds)).")";
603 }
604 }
605 if ($query !== '') {
606 $q = $this->db->escape($this->db->escapeforlike($query));
607 $sql .= " AND (f.ref LIKE '%".$this->db->escape($q)."%' OR f.ref_supplier LIKE '%".$this->db->escape($q)."%' OR s.nom LIKE '%".$this->db->escape($q)."%')";
608 }
609 if ($status === 'draft') {
610 $sql .= " AND f.fk_statut = 0";
611 } elseif ($status === 'unpaid') {
612 $sql .= " AND f.fk_statut = 1 AND f.paye = 0";
613 } elseif ($status === 'paid') {
614 $sql .= " AND f.paye = 1";
615 }
616 $sql .= " ORDER BY f.datef DESC";
617 $sql .= " LIMIT ".((int) $limit);
618
619 $resql = $this->db->query($sql);
620 if (!$resql) {
621 dol_syslog("AiToolsInvoices::searchSupplierInvoices sql error", LOG_ERR);
622 return ["error" => "Search failed."];
623 }
624
625 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
626 $out = [];
627 while ($obj = $this->db->fetch_object($resql)) {
628 $inv = new FactureFournisseur($this->db);
629 if ($inv->fetch((int) $obj->rowid) > 0) {
630 $inv->fetch_thirdparty();
631 $out[] = [
632 "id" => $inv->id,
633 "ref" => $inv->ref,
634 "ref_supplier" => $inv->ref_supplier,
635 "supplier" => is_object($inv->thirdparty) ? $inv->thirdparty->name : '',
636 "date" => dol_print_date($inv->date, 'day'),
637 "status" => $inv->getLibStatut(1),
638 "total_ttc" => price($inv->total_ttc),
639 "_ht" => (float) $inv->total_ht,
640 "_ttc" => (float) $inv->total_ttc,
641 "url" => DOL_URL_ROOT."/fourn/facture/card.php?facid=".$inv->id
642 ];
643 }
644 }
645
646 if (!empty($out) && !empty($onlyIds)) {
647 $sumHt = 0.0;
648 $sumTtc = 0.0;
649 foreach ($out as $row) {
650 $sumHt += $row['_ht'];
651 $sumTtc += $row['_ttc'];
652 }
653 foreach ($out as $k => $row) {
654 unset($out[$k]['_ht'], $out[$k]['_ttc']);
655 }
656 return [
657 "count" => count($out),
658 "total_ht" => price($sumHt),
659 "total_ttc" => price($sumTtc),
660 "invoices" => $out
661 ];
662 }
663 foreach ($out as $k => $row) {
664 unset($out[$k]['_ht'], $out[$k]['_ttc']);
665 }
666
667 return empty($out) ? ["message" => "No supplier invoices found."] : $out;
668 }
669
676 private function getSupplierInvoice($args)
677 {
678 if (!$this->user->hasRight('fournisseur', 'facture', 'lire')) {
679 return ["error" => "Permission denied: supplier invoice read right is missing."];
680 }
681
682 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
683 $inv = new FactureFournisseur($this->db);
684 $res = 0;
685 if (!empty($args['id'])) {
686 $res = $inv->fetch((int) $args['id']);
687 } elseif (!empty($args['ref'])) {
688 $res = $inv->fetch(0, (string) $args['ref']);
689 }
690 if ($res <= 0 || empty($inv->id)) {
691 return ["error" => "Supplier invoice not found."];
692 }
693 if (!in_array((int) $inv->entity, explode(',', getEntity('facture_fourn')))) {
694 return ["error" => "Supplier invoice not found."];
695 }
696
697 $inv->fetch_thirdparty();
698 $inv->fetch_lines();
699
700 $lines = [];
701 foreach ($inv->lines as $l) {
702 $lvars = get_object_vars($l);
703 $lines[] = [
704 "product" => !empty($l->product_ref) ? $l->product_ref : '',
705 "desc" => dol_html_entity_decode(strip_tags((string) $l->description), ENT_QUOTES),
706 "qty" => (float) $l->qty,
707 "price" => price($lvars['pu_ht'] ?? $lvars['subprice'] ?? 0),
708 "total_line" => price($l->total_ht),
709 "vat" => $l->tva_tx."%"
710 ];
711 }
712
713 return [
714 "id" => $inv->id,
715 "ref" => $inv->ref,
716 "ref_supplier" => $inv->ref_supplier,
717 "date" => dol_print_date($inv->date, 'day'),
718 "status" => $inv->getLibStatut(1),
719 "supplier" => is_object($inv->thirdparty) ? $inv->thirdparty->name : '',
720 "total_ht" => price($inv->total_ht),
721 "total_ttc" => price($inv->total_ttc),
722 "lines" => $lines,
723 "url" => DOL_URL_ROOT."/fourn/facture/card.php?facid=".$inv->id
724 ];
725 }
726
733 private function findCustomer($identifier)
734 {
735 global $conf;
736
737 $customer = new Societe($this->db);
738 $identifier = trim($identifier);
739
740 if (preg_match('/^(?:socid|id)[:\s]+(\d+)$/i', $identifier, $m)) {
741 $identifier = $m[1];
742 } elseif (preg_match('/^(?:code|ref)[:\s]+(.+)$/i', $identifier, $m)) {
743 $identifier = $m[1];
744 }
745
746 if (is_numeric($identifier)) {
747 if ($customer->fetch((int) $identifier) > 0) {
748 return $customer;
749 }
750 }
751
752 // Exact
753 $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe
754 WHERE (nom = '" . $this->db->escape($identifier) . "'
755 OR code_client = '" . $this->db->escape($identifier) . "')
756 AND entity IN (" . getEntity('societe') . ")";
757
758 $resql = $this->db->query($sql);
759
760 if ($resql && $this->db->num_rows($resql) > 0) {
761 $obj = $this->db->fetch_object($resql);
762 $customer->fetch($obj->rowid);
763 $this->db->free($resql);
764 return $customer;
765 }
766
767 $sql = "SELECT rowid, nom FROM " . MAIN_DB_PREFIX . "societe
768 WHERE (nom LIKE '%" . $this->db->escape($identifier) . "%'
769 OR code_client LIKE '%" . $this->db->escape($identifier) . "%')
770 AND entity IN (" . getEntity('societe') . ")
771 LIMIT 5";
772
773 $resql = $this->db->query($sql);
774
775 if ($resql) {
776 $num = $this->db->num_rows($resql);
777
778 if ($num == 1) {
779 $obj = $this->db->fetch_object($resql);
780 $customer->fetch($obj->rowid);
781 $this->db->free($resql);
782 return $customer;
783 } elseif ($num > 1) {
784 $matches = [];
785 while ($obj = $this->db->fetch_object($resql)) {
786 $matches[] = $obj->nom;
787 }
788 $this->db->free($resql);
789 return ["error" => "Multiple customers found.", "matches" => $matches];
790 }
791 }
792
793 return ["error" => "Customer not found."];
794 }
795
802 private function findInvoice($identifier)
803 {
804 $invoice = new Facture($this->db);
805 $identifier = trim($identifier);
806
807 if (preg_match('/^\‍(?prov[-_]?(\d+)\‍)?$/i', $identifier, $matches)) {
808 if ($invoice->fetch((int) $matches[1]) > 0) {
809 return $invoice;
810 }
811 }
812 if (is_numeric($identifier)) {
813 if ($invoice->fetch((int) $identifier) > 0) {
814 return $invoice;
815 }
816 }
817 if ($invoice->fetch(0, $identifier) > 0) {
818 return $invoice;
819 }
820
821 return ["error" => "Invoice not found."];
822 }
823
830 private function findBankAccount($identifier)
831 {
832 global $conf;
833
834 $identifier = trim($identifier);
835 $params = [];
836
837
838 $sql = "SELECT rowid, label FROM " . MAIN_DB_PREFIX . "bank_account
839 WHERE entity IN (" . getEntity('bank_account') . ") AND clos = 0";
840
841 if (is_numeric($identifier)) {
842 $sql .= " AND rowid = " . ((int) $identifier);
843 } elseif (!empty($identifier)) {
844 $sql .= " AND (ref = '" . $this->db->escape($identifier) . "'
845 OR label LIKE '%" . $this->db->escape($identifier) . "%')";
846 }
847
848 $resql = $this->db->query($sql);
849
850 if ($resql && $this->db->num_rows($resql) > 0) {
851 $obj = $this->db->fetch_object($resql);
852 $this->db->free($resql);
853 return $obj;
854 }
855
856 // fallback
857 $sql = "SELECT rowid, label FROM " . MAIN_DB_PREFIX . "bank_account
858 WHERE entity IN (" . getEntity('bank_account') . ") AND clos = 0
859 LIMIT 1";
860
861 $resql = $this->db->query($sql);
862
863 if ($resql && $this->db->num_rows($resql) > 0) {
864 $obj = $this->db->fetch_object($resql);
865 $this->db->free($resql);
866 return $obj;
867 }
868
869 return null;
870 }
871}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
Class to manage Dolibarr database access.
Class to manage suppliers invoices.
Class to manage invoices.
Abstract base class for all MCP (Model Context Protocol) tools.
const RIGHTS_UNDECLARED
Default of getRequiredRights(): the class never declared anything, deny.
const NO_WRITE
Returned by writeConfirmationPreview() when the tool writes nothing.
Class to manage payments of customer invoices.
Class to manage third parties objects (customers, suppliers, prospects...)
Class ToolInvoices.
searchInvoices($args)
Search invoices based on filters.
findInvoice($identifier)
Find an invoice by identifier.
getInvoice($args)
Get full invoice details.
searchSupplierInvoices($args)
Search supplier invoices.
payInvoice($args)
Register a payment on an invoice.
writeConfirmationPreview(string $toolName, array $args)
Preview of the invoice action this call would perform.
getSupplierInvoice($args)
Get one supplier invoice with lines.
getUser()
Return the authenticated user, preferring DI ($this->user) and falling back to global $user.
findCustomer($identifier)
Find a customer by identifier.
execute(string $name, array $args)
Executes the requested tool function based on its name.
findBankAccount($identifier)
Find a bank account.
getCategories()
Return categories this tool belongs to.
getRequiredRights(string $toolName)
Customer and supplier invoice reads and lifecycle actions.
__construct(DoliDB $db, $user=null, $conf=null)
Constructor.
getDefinitions()
Returns an array of tool definitions, including name, description, and input schema.
validateInvoice($args)
Validate a draft invoice.
dol_html_entity_decode($a, $b, $c='UTF-8', $keepsomeentities=0)
Replace html_entity_decode functions to manage errors.
dol_now($mode='gmt')
Return date for now.
dol_getIdFromCode($db, $key, $tablename, $fieldkey='code', $fieldid='id', $entityfilter=0, $filters='', $useCache=true)
Return an id or code from a code or id.
price($amount, $form=0, $outlangs='', $trunc=1, $rounding=-1, $forcerounding=-1, $currency_code='')
Function to format a value into an amount for visual output Function used into PDF and HTML pages.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
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:431
$conf db user
Active Directory does not allow anonymous connections.
Definition repair.php:141