dolibarr 24.0.0-beta
invoices.class.php
1<?php
2/* Copyright (C) 2026 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2026 Nick Fragoulis
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY, without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
25require_once DOL_DOCUMENT_ROOT . '/compta/facture/class/facture.class.php';
26require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
27require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
28require_once DOL_DOCUMENT_ROOT . '/compta/paiement/class/paiement.class.php';
29require_once DOL_DOCUMENT_ROOT . '/compta/bank/class/account.class.php';
30require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
31
37class ToolInvoices extends McpTool
38{
39
45 public function __construct(DoliDB $db)
46 {
47 $this->db = $db;
48 }
49
55 public function getDefinitions(): array
56 {
57 return [
58 [
59 "name" => "search_invoice",
60 "description" => "Search for invoices. By default, lists UNPAID invoices. Excludes drafts.",
61 "inputSchema" => [
62 "type" => "object",
63 "properties" => [
64 "customer" => [
65 "type" => "string",
66 "description" => "Optional: Customer name."
67 ],
68 "status" => [
69 "type" => "string",
70 "enum" => ["unpaid", "paid", "draft", "all"],
71 "description" => "Filter by status. Default is 'unpaid'. 'all' shows history but excludes drafts.",
72 "default" => "unpaid"
73 ],
74 "limit" => [
75 "type" => "integer",
76 "default" => 10
77 ]
78 ]
79 ]
80 ],
81 [
82 "name" => "get_invoice",
83 "description" => "Get details of a specific invoice by ID or Reference.",
84 "inputSchema" => [
85 "type" => "object",
86 "properties" => [
87 "ref" => ["type" => "string", "description" => "Invoice Ref (e.g. FA2401-001)"],
88 "id" => ["type" => "integer", "description" => "Invoice ID"]
89 ],
90 "oneOf" => [
91 ["required" => ["ref"]],
92 ["required" => ["id"]]
93 ]
94 ]
95 ],
96 [
97 "name" => "validate_invoice",
98 "description" => "Validate a draft invoice.",
99 "inputSchema" => [
100 "type" => "object",
101 "properties" => [
102 "invoice" => ["type" => "string", "description" => "Invoice ID or Ref."]
103 ],
104 "required" => ["invoice"]
105 ]
106 ],
107 [
108 "name" => "pay_invoice",
109 "description" => "Register a payment for an invoice.",
110 "inputSchema" => [
111 "type" => "object",
112 "properties" => [
113 "invoice" => ["type" => "string", "description" => "Invoice ID or Reference."],
114 "amount" => ["type" => "number", "description" => "Amount to pay. Defaults to full remaining."],
115 "payment_mode" => ["type" => "string", "description" => "Code (VIR, CB, LIQ)."],
116 "bank_account" => ["type" => "string", "description" => "Bank Account Name/Ref."]
117 ],
118 "required" => ["invoice"]
119 ]
120 ]
121 ];
122 }
123
130 public function getCategories(): array
131 {
132 return ['billing'];
133 }
134
142 public function execute(string $name, array $args)
143 {
144 switch ($name) {
145 case 'search_invoice':
146 case 'search_invoices':
147 return $this->searchInvoices($args);
148
149 case 'get_invoice':
150 return $this->getInvoice($args);
151
152 case 'validate_invoice':
153 return $this->validateInvoice($args);
154
155 case 'pay_invoice':
156 return $this->payInvoice($args);
157
158 default:
159 return ["error" => "Tool function '$name' not found."];
160 }
161 }
162
170 private function searchInvoices($args)
171 {
172 $limit = isset($args['limit']) ? (int) $args['limit'] : 10;
173 $status = isset($args['status']) ? $args['status'] : 'unpaid';
174
175 // Safety fallback
176 if ($limit <= 0) {
177 $limit = 5;
178 }
179 if ($limit > 1000) {
180 dol_syslog("Search DB Error: Too many record requested", LOG_ERR);
181 return ["error" => "DB Error"];
182 }
183
184 $sql = "SELECT f.rowid, f.ref, f.total_ttc, f.fk_statut, f.paye, f.datef, s.nom
185 FROM " . MAIN_DB_PREFIX . "facture as f
186 LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid
187 WHERE f.entity IN (" . getEntity('facture') . ")";
188
189 // Status filtering
190 if ($status === 'draft') {
191 // Explicitly asking for drafts
192 $sql .= " AND f.fk_statut = 0";
193 } elseif ($status === 'paid') {
194 // Fully paid
195 $sql .= " AND f.fk_statut = 2";
196 } elseif ($status === 'all') {
197 // Valid invoices (Unpaid + Paid). EXCLUDES Drafts (0) and Abandoned (3)
198 $sql .= " AND f.fk_statut IN (1, 2)";
199 } else {
200 // Default: 'unpaid'
201 // In Dolibarr: fk_statut=1 means Validated but not fully paid.
202 $sql .= " AND f.fk_statut = 1 AND f.paye = 0";
203 }
204
205 // Customer Filter
206 if (!empty($args['customer'])) {
207 $cust = $this->findCustomer($args['customer']);
208 if (!is_array($cust)) {
209 $sql .= " AND f.fk_soc = " . ((int) $cust->id);
210 } else {
211 $sql .= " AND s.nom LIKE '%" . $this->db->escape($args['customer']) . "%'";
212 }
213 }
214
215 $sql .= " ORDER BY f.datef DESC LIMIT " . ((int) $limit);
216
217 $resql = $this->db->query($sql);
218 $list = [];
219
220 if ($resql) {
221 while ($r = $this->db->fetch_object($resql)) {
222 // Double check to ensure no PROV/Drafts slip through unless asked
223 if ($status !== 'draft' && $r->fk_statut == 0) {
224 continue;
225 }
226
227 $ref = ($r->fk_statut == 0 ? "(PROV" . $r->rowid . ")" : $r->ref);
228
229 // Calculate Status Label
230 $statusLabel = "Unknown";
231 if ($r->fk_statut == 0) {
232 $statusLabel = "Draft";
233 } elseif ($r->fk_statut == 1) {
234 $statusLabel = "Unpaid";
235 } elseif ($r->fk_statut == 2) {
236 $statusLabel = "Paid";
237 } elseif ($r->fk_statut == 3) {
238 $statusLabel = "Abandoned";
239 }
240
241 $list[] = [
242 "ref" => $ref,
243 "date" => dol_print_date($this->db->jdate($r->datef), 'day'),
244 "customer" => $r->nom,
245 "amount" => price($r->total_ttc),
246 "status" => $statusLabel,
247 "url" => DOL_URL_ROOT . "/compta/facture/card.php?id=" . $r->rowid
248 ];
249 }
250 $this->db->free($resql);
251 }
252
253 if (empty($list)) {
254 return ["info" => "No " . $status . " invoices found matching your criteria."];
255 }
256
257 return $list;
258 }
259
267 private function getInvoice($args)
268 {
269 $id = isset($args['ref']) ? $args['ref'] : (isset($args['id']) ? $args['id'] : null);
270
271 $invoice = $this->findInvoice($id);
272 if (is_array($invoice)) {
273 return $invoice;
274 }
275
276 $invoice->fetch_thirdparty();
277 $invoice->fetch_lines();
278
279 $lines = [];
280 foreach ($invoice->lines as $l) {
281 $prodRef = !empty($l->product_ref) ? $l->product_ref : (!empty($l->product_label) ? $l->product_label : '');
282
283 $lines[] = [
284 "product" => $prodRef,
285 "desc" => dol_html_entity_decode(strip_tags($l->desc), ENT_QUOTES),
286 "qty" => (float) $l->qty,
287 "price" => price($l->subprice),
288 "total_line" => price($l->total_ht),
289 "vat" => $l->tva_tx . "%"
290 ];
291 }
292
293 return [
294 "id" => $invoice->id,
295 "ref" => $invoice->ref,
296 "date" => dol_print_date($invoice->date, 'day'),
297 "status" => $invoice->getLibStatut(1),
298 "customer" => $invoice->thirdparty->name,
299 "total_ht" => price($invoice->total_ht),
300 "total_ttc" => price($invoice->total_ttc),
301 "lines" => $lines,
302 "url" => DOL_URL_ROOT . "/compta/facture/card.php?id=" . $invoice->id
303 ];
304 }
305
313 private function validateInvoice($args)
314 {
315 global $user;
316 $invoice = $this->findInvoice($args['invoice']);
317 if (is_array($invoice)) {
318 return $invoice;
319 }
320
321 if ($invoice->statut != 0) {
322 return ["error" => "Invoice is already validated."];
323 }
324
325 if ($invoice->validate($user) < 0) {
326 $error = $invoice->error;
327 if (!empty($invoice->errors)) {
328 $error .= ' ' . implode(', ', $invoice->errors);
329 }
330 if (empty(trim($error))) {
331 $error = 'Unknown error (validate returned < 0 with no message)';
332 }
333 return ["error" => "Validation failed: " . $error];
334 }
335
336 $invoice->fetch($invoice->id);
337 return [
338 "success" => true,
339 "new_ref" => $invoice->ref,
340 "status" => "Validated (Unpaid)",
341 "url" => DOL_URL_ROOT . "/compta/facture/card.php?id=" . $invoice->id
342 ];
343 }
344
352 private function payInvoice($args)
353 {
354 global $user;
355 $invoice = $this->findInvoice($args['invoice']);
356 if (is_array($invoice)) {
357 return $invoice;
358 }
359
360 // Cannot pay drafts
361 if ($invoice->statut == 0) {
362 return ["error" => "Cannot pay a Draft invoice. Please validate it first."];
363 }
364
365 $bank = $this->findBankAccount(isset($args['bank_account']) ? $args['bank_account'] : '');
366 if (!$bank) {
367 return ["error" => "No active Bank account found to receive payment."];
368 }
369
370 $remaining = $invoice->total_ttc - $invoice->getSommePaiement();
371 if ($remaining <= 0) {
372 return ["error" => "Invoice is already fully paid."];
373 }
374
375 $amount = isset($args['amount']) ? (float) $args['amount'] : $remaining;
376 if ($amount > $remaining) {
377 $amount = $remaining;
378 }
379
380 $code = isset($args['payment_mode']) ? $args['payment_mode'] : 'VIR';
381 $modeId = dol_getIdFromCode($this->db, $code, 'c_paiement', 'code', 'id');
382
383 $this->db->begin();
384 $payment = new Paiement($this->db);
385 $payment->datepaye = dol_now();
386 $payment->amounts = [$invoice->id => $amount];
387 $payment->paiementid = $modeId;
388 $payment->paiementcode = $code;
389
390 $paymentId = $payment->create($user, 1);
391 if ($paymentId < 0) {
392 $this->db->rollback();
393 return ["error" => "Payment creation failed: " . implode(', ', $payment->errors)];
394 }
395 $payment->fetch($paymentId);
396 if ($payment->addPaymentToBank($user, 'payment', '(Payment via AI)', $bank->rowid, '', '') < 0) {
397 $this->db->rollback();
398 return ["error" => "Failed to add payment to bank ledger."];
399 }
400
401 $this->db->commit();
402
403 return [
404 "success" => true,
405 "paid_amount" => price($amount),
406 "remaining_due" => price($remaining - $amount),
407 "status" => ($remaining - $amount <= 0) ? "Fully Paid" : "Partially Paid",
408 "payment_url" => DOL_URL_ROOT . "/compta/paiement/card.php?id=" . $paymentId
409 ];
410 }
411
418 private function findCustomer($identifier)
419 {
420 global $conf;
421
422 $customer = new Societe($this->db);
423 $identifier = trim($identifier);
424
425 if (preg_match('/^(?:socid|id)[:\s]+(\d+)$/i', $identifier, $m)) {
426 $identifier = $m[1];
427 } elseif (preg_match('/^(?:code|ref)[:\s]+(.+)$/i', $identifier, $m)) {
428 $identifier = $m[1];
429 }
430
431 if (is_numeric($identifier)) {
432 if ($customer->fetch((int) $identifier) > 0) {
433 return $customer;
434 }
435 }
436
437 // Exact
438 $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe
439 WHERE (nom = '" . $this->db->escape($identifier) . "'
440 OR code_client = '" . $this->db->escape($identifier) . "')
441 AND entity IN (" . getEntity('societe') . ")";
442
443 $resql = $this->db->query($sql);
444
445 if ($resql && $this->db->num_rows($resql) > 0) {
446 $obj = $this->db->fetch_object($resql);
447 $customer->fetch($obj->rowid);
448 $this->db->free($resql);
449 return $customer;
450 }
451
452 $sql = "SELECT rowid, nom FROM " . MAIN_DB_PREFIX . "societe
453 WHERE (nom LIKE '%" . $this->db->escape($identifier) . "%'
454 OR code_client LIKE '%" . $this->db->escape($identifier) . "%')
455 AND entity IN (" . getEntity('societe') . ")
456 LIMIT 5";
457
458 $resql = $this->db->query($sql);
459
460 if ($resql) {
461 $num = $this->db->num_rows($resql);
462
463 if ($num == 1) {
464 $obj = $this->db->fetch_object($resql);
465 $customer->fetch($obj->rowid);
466 $this->db->free($resql);
467 return $customer;
468 } elseif ($num > 1) {
469 $matches = [];
470 while ($obj = $this->db->fetch_object($resql)) {
471 $matches[] = $obj->nom;
472 }
473 $this->db->free($resql);
474 return ["error" => "Multiple customers found.", "matches" => $matches];
475 }
476 }
477
478 return ["error" => "Customer not found."];
479 }
480
487 private function findInvoice($identifier)
488 {
489 $invoice = new Facture($this->db);
490 $identifier = trim($identifier);
491
492 if (preg_match('/^\‍(?prov[-_]?(\d+)\‍)?$/i', $identifier, $matches)) {
493 if ($invoice->fetch((int) $matches[1]) > 0) {
494 return $invoice;
495 }
496 }
497 if (is_numeric($identifier)) {
498 if ($invoice->fetch((int) $identifier) > 0) {
499 return $invoice;
500 }
501 }
502 if ($invoice->fetch(0, $identifier) > 0) {
503 return $invoice;
504 }
505
506 return ["error" => "Invoice not found."];
507 }
508
515 private function findBankAccount($identifier)
516 {
517 global $conf;
518
519 $identifier = trim($identifier);
520 $params = [];
521
522
523 $sql = "SELECT rowid, label FROM " . MAIN_DB_PREFIX . "bank_account
524 WHERE entity IN (" . getEntity('bank_account') . ") AND clos = 0";
525
526 if (is_numeric($identifier)) {
527 $sql .= " AND rowid = " . ((int) $identifier);
528 } elseif (!empty($identifier)) {
529 $sql .= " AND (ref = '" . $this->db->escape($identifier) . "'
530 OR label LIKE '%" . $this->db->escape($identifier) . "%')";
531 }
532
533 $resql = $this->db->query($sql);
534
535 if ($resql && $this->db->num_rows($resql) > 0) {
536 $obj = $this->db->fetch_object($resql);
537 $this->db->free($resql);
538 return $obj;
539 }
540
541 // fallback
542 $sql = "SELECT rowid, label FROM " . MAIN_DB_PREFIX . "bank_account
543 WHERE entity IN (" . getEntity('bank_account') . ") AND clos = 0
544 LIMIT 1";
545
546 $resql = $this->db->query($sql);
547
548 if ($resql && $this->db->num_rows($resql) > 0) {
549 $obj = $this->db->fetch_object($resql);
550 $this->db->free($resql);
551 return $obj;
552 }
553
554 return null;
555 }
556}
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
Class to manage Dolibarr database access.
Class to manage invoices.
Abstract base class for all MCP (Model Context Protocol) tools.
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.
payInvoice($args)
Register a payment on an invoice.
__construct(DoliDB $db)
Constructor.
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.
getDefinitions()
Returns an array of tool definitions, including name, description, and input schema.
validateInvoice($args)
Validate a draft invoice.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
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.