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';
59 "name" =>
"search_invoice",
60 "description" =>
"Search for invoices. By default, lists UNPAID invoices. Excludes drafts.",
66 "description" =>
"Optional: Customer name."
70 "enum" => [
"unpaid",
"paid",
"draft",
"all"],
71 "description" =>
"Filter by status. Default is 'unpaid'. 'all' shows history but excludes drafts.",
82 "name" =>
"get_invoice",
83 "description" =>
"Get details of a specific invoice by ID or Reference.",
87 "ref" => [
"type" =>
"string",
"description" =>
"Invoice Ref (e.g. FA2401-001)"],
88 "id" => [
"type" =>
"integer",
"description" =>
"Invoice ID"]
91 [
"required" => [
"ref"]],
92 [
"required" => [
"id"]]
97 "name" =>
"validate_invoice",
98 "description" =>
"Validate a draft invoice.",
102 "invoice" => [
"type" =>
"string",
"description" =>
"Invoice ID or Ref."]
104 "required" => [
"invoice"]
108 "name" =>
"pay_invoice",
109 "description" =>
"Register a payment for an invoice.",
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."]
118 "required" => [
"invoice"]
142 public function execute(
string $name, array $args)
145 case 'search_invoice':
146 case 'search_invoices':
152 case 'validate_invoice':
159 return [
"error" =>
"Tool function '$name' not found."];
172 $limit = isset($args[
'limit']) ? (int) $args[
'limit'] : 10;
173 $status = isset($args[
'status']) ? $args[
'status'] :
'unpaid';
180 dol_syslog(
"Search DB Error: Too many record requested", LOG_ERR);
181 return [
"error" =>
"DB Error"];
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') .
")";
190 if ($status ===
'draft') {
192 $sql .=
" AND f.fk_statut = 0";
193 } elseif ($status ===
'paid') {
195 $sql .=
" AND f.fk_statut = 2";
196 } elseif ($status ===
'all') {
198 $sql .=
" AND f.fk_statut IN (1, 2)";
202 $sql .=
" AND f.fk_statut = 1 AND f.paye = 0";
206 if (!empty($args[
'customer'])) {
208 if (!is_array($cust)) {
209 $sql .=
" AND f.fk_soc = " . ((int) $cust->id);
211 $sql .=
" AND s.nom LIKE '%" . $this->db->escape($args[
'customer']) .
"%'";
215 $sql .=
" ORDER BY f.datef DESC LIMIT " . ((int) $limit);
217 $resql = $this->db->query($sql);
221 while ($r = $this->db->fetch_object($resql)) {
223 if ($status !==
'draft' && $r->fk_statut == 0) {
227 $ref = ($r->fk_statut == 0 ?
"(PROV" . $r->rowid .
")" : $r->ref);
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";
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
250 $this->db->free($resql);
254 return [
"info" =>
"No " . $status .
" invoices found matching your criteria."];
269 $id = isset($args[
'ref']) ? $args[
'ref'] : (isset($args[
'id']) ? $args[
'id'] :
null);
272 if (is_array($invoice)) {
276 $invoice->fetch_thirdparty();
277 $invoice->fetch_lines();
280 foreach ($invoice->lines as $l) {
281 $prodRef = !empty($l->product_ref) ? $l->product_ref : (!empty($l->product_label) ? $l->product_label :
'');
284 "product" => $prodRef,
286 "qty" => (float) $l->qty,
287 "price" =>
price($l->subprice),
288 "total_line" =>
price($l->total_ht),
289 "vat" => $l->tva_tx .
"%"
294 "id" => $invoice->id,
295 "ref" => $invoice->ref,
297 "status" => $invoice->getLibStatut(1),
298 "customer" => $invoice->thirdparty->name,
299 "total_ht" =>
price($invoice->total_ht),
300 "total_ttc" =>
price($invoice->total_ttc),
302 "url" => DOL_URL_ROOT .
"/compta/facture/card.php?id=" . $invoice->id
317 if (is_array($invoice)) {
321 if ($invoice->statut != 0) {
322 return [
"error" =>
"Invoice is already validated."];
325 if ($invoice->validate($user) < 0) {
326 $error = $invoice->error;
327 if (!empty($invoice->errors)) {
328 $error .=
' ' . implode(
', ', $invoice->errors);
330 if (empty(trim($error))) {
331 $error =
'Unknown error (validate returned < 0 with no message)';
333 return [
"error" =>
"Validation failed: " . $error];
336 $invoice->fetch($invoice->id);
339 "new_ref" => $invoice->ref,
340 "status" =>
"Validated (Unpaid)",
341 "url" => DOL_URL_ROOT .
"/compta/facture/card.php?id=" . $invoice->id
356 if (is_array($invoice)) {
361 if ($invoice->statut == 0) {
362 return [
"error" =>
"Cannot pay a Draft invoice. Please validate it first."];
365 $bank = $this->
findBankAccount(isset($args[
'bank_account']) ? $args[
'bank_account'] :
'');
367 return [
"error" =>
"No active Bank account found to receive payment."];
370 $remaining = $invoice->total_ttc - $invoice->getSommePaiement();
371 if ($remaining <= 0) {
372 return [
"error" =>
"Invoice is already fully paid."];
375 $amount = isset($args[
'amount']) ? (float) $args[
'amount'] : $remaining;
376 if ($amount > $remaining) {
377 $amount = $remaining;
380 $code = isset($args[
'payment_mode']) ? $args[
'payment_mode'] :
'VIR';
385 $payment->datepaye =
dol_now();
386 $payment->amounts = [$invoice->id => $amount];
387 $payment->paiementid = $modeId;
388 $payment->paiementcode = $code;
390 $paymentId = $payment->create($user, 1);
391 if ($paymentId < 0) {
392 $this->db->rollback();
393 return [
"error" =>
"Payment creation failed: " . implode(
', ', $payment->errors)];
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."];
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
422 $customer =
new Societe($this->db);
423 $identifier = trim($identifier);
425 if (preg_match(
'/^(?:socid|id)[:\s]+(\d+)$/i', $identifier, $m)) {
427 } elseif (preg_match(
'/^(?:code|ref)[:\s]+(.+)$/i', $identifier, $m)) {
431 if (is_numeric($identifier)) {
432 if ($customer->fetch((
int) $identifier) > 0) {
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') .
")";
443 $resql = $this->db->query($sql);
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);
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') .
")
458 $resql = $this->db->query($sql);
461 $num = $this->db->num_rows($resql);
464 $obj = $this->db->fetch_object($resql);
465 $customer->fetch($obj->rowid);
466 $this->db->free($resql);
468 } elseif ($num > 1) {
470 while ($obj = $this->db->fetch_object($resql)) {
471 $matches[] = $obj->nom;
473 $this->db->free($resql);
474 return [
"error" =>
"Multiple customers found.",
"matches" => $matches];
478 return [
"error" =>
"Customer not found."];
489 $invoice =
new Facture($this->db);
490 $identifier = trim($identifier);
492 if (preg_match(
'/^\(?prov[-_]?(\d+)\)?$/i', $identifier, $matches)) {
493 if ($invoice->fetch((
int) $matches[1]) > 0) {
497 if (is_numeric($identifier)) {
498 if ($invoice->fetch((
int) $identifier) > 0) {
502 if ($invoice->fetch(0, $identifier) > 0) {
506 return [
"error" =>
"Invoice not found."];
519 $identifier = trim($identifier);
523 $sql =
"SELECT rowid, label FROM " . MAIN_DB_PREFIX .
"bank_account
524 WHERE entity IN (" .
getEntity(
'bank_account') .
") AND clos = 0";
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) .
"%')";
533 $resql = $this->db->query($sql);
535 if ($resql && $this->db->num_rows($resql) > 0) {
536 $obj = $this->db->fetch_object($resql);
537 $this->db->free($resql);
542 $sql =
"SELECT rowid, label FROM " . MAIN_DB_PREFIX .
"bank_account
543 WHERE entity IN (" .
getEntity(
'bank_account') .
") AND clos = 0
546 $resql = $this->db->query($sql);
548 if ($resql && $this->db->num_rows($resql) > 0) {
549 $obj = $this->db->fetch_object($resql);
550 $this->db->free($resql);
$id
Support class for third parties, contacts, members, users or resources.
Class to manage Dolibarr database access.
Class to manage invoices.
Class to manage payments of customer invoices.
Class to manage third parties objects (customers, suppliers, prospects...)
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.