dolibarr 24.0.0-beta
reports.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
29require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
30require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
31
37class ToolReports 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" => "get_thirdparty_transactions",
60 "description" => "Generate a list of raw transactions (Invoices, Orders) for a specific thirdparty.",
61 "inputSchema" => [
62 "type" => "object",
63 "properties" => [
64 "thirdparty_id" => ["type" => "integer", "description" => "The unique ID of the thirdparty."],
65 "thirdparty_name" => ["type" => "string", "description" => "The name of the thirdparty."],
66 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD)."],
67 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD)."],
68 "transaction_type" => [
69 "type" => "string",
70 "enum" => ["all", "invoices", "orders", "proposals"],
71 "description" => "Filter by type.",
72 "default" => "all"
73 ]
74 ],
75 "oneOf" => [
76 ["required" => ["thirdparty_id"]],
77 ["required" => ["thirdparty_name"]]
78 ]
79 ]
80 ],
81 [
82 "name" => "get_sales_report",
83 "description" => "Generate a sales/revenue report. If a Thirdparty is provided, it returns a detailed breakdown for that customer. Otherwise, it returns a global summary.",
84 "inputSchema" => [
85 "type" => "object",
86 "properties" => [
87 "thirdparty_id" => [
88 "type" => "integer",
89 "description" => "Optional: The ID of the customer."
90 ],
91 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD)."],
92 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD)."],
93 "group_by" => [
94 "type" => "string",
95 "enum" => ["thirdparty", "product", "month"],
96 "description" => "Only used if no Thirdparty is specified. Groups global results.",
97 "default" => "thirdparty"
98 ]
99 ],
100 "required" => ["date_start", "date_end"]
101 ]
102 ],
103 [
104 "name" => "get_purchase_report",
105 "description" => "Generate a purchase/expense report. If a Supplier is provided, it returns a detailed breakdown. Otherwise, it returns a global summary.",
106 "inputSchema" => [
107 "type" => "object",
108 "properties" => [
109 "thirdparty_id" => [
110 "type" => "integer",
111 "description" => "Optional: The ID of the supplier."
112 ],
113 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD)."],
114 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD)."],
115 "group_by" => [
116 "type" => "string",
117 "enum" => ["supplier", "product", "month"],
118 "description" => "Only used if no Supplier is specified. Groups global results.",
119 "default" => "supplier"
120 ]
121 ],
122 "required" => ["date_start", "date_end"]
123 ]
124 ],
125 [
126 "name" => "get_inventory_report",
127 "description" => "Generate an inventory report showing current stock levels and valuation.",
128 "inputSchema" => [
129 "type" => "object",
130 "properties" => [
131 "category_id" => ["type" => "integer", "description" => "Filter by category ID."],
132 "warehouse_id" => ["type" => "integer", "description" => "Filter by warehouse ID."],
133 "include_zero_stock" => ["type" => "boolean", "default" => false]
134 ]
135 ]
136 ],
137 [
138 "name" => "get_financial_report",
139 "description" => "Generate a summary financial report (Income vs Expense) for a period.",
140 "inputSchema" => [
141 "type" => "object",
142 "properties" => [
143 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD)."],
144 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD)."]
145 ],
146 "required" => ["date_start", "date_end"]
147 ]
148 ],
149 ];
150 }
151
158 public function getCategories(): array
159 {
160 return ['reporting', 'commercial', 'billing', 'stock'];
161 }
162
170 public function execute(string $name, array $args)
171 {
172 switch ($name) {
173 case 'get_thirdparty_transactions':
174 return $this->getThirdpartyTransactions($args);
175 case 'get_sales_report':
176 return $this->getSalesReport($args);
177 case 'get_purchase_report':
178 return $this->getPurchaseReport($args);
179 case 'get_inventory_report':
180 return $this->getInventoryReport($args);
181 case 'get_financial_report':
182 return $this->getFinancialReport($args);
183 default:
184 return ["error" => "Tool function '$name' not found."];
185 }
186 }
187
199 private function resolveThirdparty($args)
200 {
201
202 if (!empty($args['thirdparty_id'])) {
203 return (int) $args['thirdparty_id'];
204 }
205
206 if (!empty($args['thirdparty_name'])) {
207 $name = $this->db->escape($args['thirdparty_name']);
208
209 $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe
210 WHERE nom LIKE '%" . $name . "%'
211 AND entity IN (" . getEntity('societe') . ")
212 LIMIT 1";
213
214 $resql = $this->db->query($sql);
215 if ($resql && $obj = $this->db->fetch_object($resql)) {
216 return $obj->rowid;
217 }
218 }
219
220 return null;
221 }
222
229 private function getSalesReport(array $args): array
230 {
231 global $langs;
232
233 $langs->loadLangs(array("main", "bills", "companies", "products"));
234
235 $limit = isset($args['limit']) ? (int) $args['limit'] : 50;
236 $dateStart = dol_stringtotime($args['date_start']);
237 $dateEnd = dol_stringtotime($args['date_end']);
238 $socid = $this->resolveThirdparty($args);
239 $groupBy = isset($args['group_by']) ? (string) $args['group_by'] : 'thirdparty';
240
241 $list = [];
242 $totalSum = 0.0;
243 // Status Filter: Valid (1) and Paid (2). Exclude Draft (0) and Abandoned (3).
244 $dateRange = " AND f.datef >= '" . $this->db->idate($dateStart)
245 . "' AND f.datef <= '" . $this->db->idate($dateEnd)
246 . "' AND f.fk_statut IN (1, 2)";
247
248 // CASE 1 -- Detailed list for a specific thirdparty.
249 if ($socid) {
250 $sql = "SELECT f.rowid, f.ref, f.total_ttc, f.fk_statut, f.paye, f.datef, s.nom FROM "
251 . MAIN_DB_PREFIX . "facture as f LEFT JOIN "
252 . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid WHERE f.entity IN ("
253 . getEntity('facture') . ")"
254 . $dateRange
255 . " AND f.fk_soc = " . (int) $socid
256 . " ORDER BY f.datef DESC LIMIT " . ((int) $limit);
257
258 $resql = $this->db->query($sql);
259 if ($resql) {
260 while ($r = $this->db->fetch_object($resql)) {
261 $totalSum += (float) $r->total_ttc;
262
263 $statusLabel = $langs->transnoentitiesnoconv("Unknown");
264 if ($r->fk_statut == 1 && $r->paye == 0) {
265 $statusLabel = $langs->transnoentitiesnoconv("BillStatusNotPaid");
266 } elseif ($r->fk_statut == 1 && $r->paye == 1) {
267 $statusLabel = $langs->transnoentitiesnoconv("BillStatusStarted");
268 } elseif ($r->fk_statut == 2) {
269 $statusLabel = $langs->transnoentitiesnoconv("BillStatusPaid");
270 }
271
272 $url = DOL_URL_ROOT . "/compta/facture/card.php?id=" . $r->rowid;
273 $refHtml = '<a href="' . $url . '">' . $r->ref . '</a>';
274
275 $list[] = [
276 $langs->transnoentitiesnoconv("Ref") => $refHtml,
277 $langs->transnoentitiesnoconv("Date") => dol_print_date($this->db->jdate($r->datef), 'day'),
278 $langs->transnoentitiesnoconv("Customer") => $r->nom,
279 $langs->transnoentitiesnoconv("Amount") => price($r->total_ttc),
280 $langs->transnoentitiesnoconv("Status") => $statusLabel
281 ];
282 }
283 $this->db->free($resql);
284 }
285 } else {
286 // CASE 2 -- Global grouped report.
287 // Mirrors the pattern already used by getPurchaseReport(); previous implementation
288 // of getSalesReport() ignored $groupBy entirely and always returned a flat list.
289 $sanitizedSqlGroup = '';
290 $colName = '';
291 $sqlJoin = " LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid";
292
293 if ($groupBy === 'month') {
294 $sanitizedSqlGroup = "DATE_FORMAT(f.datef, '%Y-%m')";
295 $colName = $langs->transnoentitiesnoconv("Month");
296 } elseif ($groupBy === 'product') {
297 // Aggregate on product line items. Lines without product_id fall back to their description.
298 $sanitizedSqlGroup = "COALESCE(p.ref, fd.description, '?')";
299 $colName = $langs->transnoentitiesnoconv("Product");
300 $sqlJoin .= " INNER JOIN " . MAIN_DB_PREFIX . "facturedet as fd ON fd.fk_facture = f.rowid LEFT JOIN "
301 . MAIN_DB_PREFIX . "product as p ON fd.fk_product = p.rowid";
302 } else {
303 // Default: group by customer
304 $sanitizedSqlGroup = "s.nom";
305 $colName = $langs->transnoentitiesnoconv("Customer");
306 }
307
308 // For product grouping we sum line totals (more accurate per-product);
309 // otherwise we sum the invoice total_ttc.
310 $amountExpr = ($groupBy === 'product') ? "SUM(fd.total_ttc)" : "SUM(f.total_ttc)";
311 $countExpr = ($groupBy === 'product') ? "COUNT(DISTINCT f.rowid)" : "COUNT(f.rowid)";
312
313 $sql = "SELECT " . $sanitizedSqlGroup . " as group_key, "
314 . $amountExpr . " as total_amount, "
315 . $countExpr . " as count_inv FROM "
316 . MAIN_DB_PREFIX . "facture as f"
317 . $sqlJoin
318 . " WHERE f.entity IN (" . getEntity('facture') . ")"
319 . $dateRange
320 . " GROUP BY group_key ORDER BY total_amount DESC LIMIT "
321 . ((int) max(1, $limit));
322
323 $resql = $this->db->query($sql);
324 if ($resql) {
325 while ($r = $this->db->fetch_object($resql)) {
326 $totalSum += (float) $r->total_amount;
327 $list[] = [
328 $colName => $r->group_key ? $r->group_key : $langs->transnoentitiesnoconv('Unknown'),
329 $langs->transnoentitiesnoconv("Number") => (int) $r->count_inv,
330 $langs->transnoentitiesnoconv("Amount") => price($r->total_amount)
331 ];
332 }
333 $this->db->free($resql);
334 }
335 }
336
337 if (empty($list)) {
338 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
339 }
340
341 // Append Total Row (shape depends on detailed-vs-grouped path)
342 if ($socid) {
343 $list[] = [
344 $langs->transnoentitiesnoconv("Ref") => $langs->transnoentitiesnoconv("Total"),
345 $langs->transnoentitiesnoconv("Date") => "",
346 $langs->transnoentitiesnoconv("Customer") => "",
347 $langs->transnoentitiesnoconv("Amount") => price($totalSum),
348 $langs->transnoentitiesnoconv("Status") => ""
349 ];
350 } else {
351 $list[] = [
352 $langs->transnoentitiesnoconv("Total") => $langs->transnoentitiesnoconv("Total"),
353 $langs->transnoentitiesnoconv("Amount") => price($totalSum)
354 ];
355 }
356
357 return $list;
358 }
359
366 private function getThirdpartyTransactions(array $args): array
367 {
368 global $langs;
369
370 $langs->loadLangs(array("main", "bills", "orders", "propal"));
371
372 $dateStart = dol_stringtotime($args['date_start']);
373 $dateEnd = dol_stringtotime($args['date_end']);
374 $type = isset($args['transaction_type']) ? (string) $args['transaction_type'] : 'all';
375
376 $socid = $this->resolveThirdparty($args);
377 if (!$socid) {
378 $langs->load("errors");
379 return [[$langs->transnoentitiesnoconv("Error") => $langs->transnoentitiesnoconv("ErrorThirdPartyNotFound")]];
380 }
381
382 $queries = [];
383
384 // Invoices
385 if ($type == 'all' || $type == 'invoices') {
386 $queries[] = "SELECT 'Invoice' as source_type, rowid, ref, total_ttc as amount, datef as date_entry, fk_statut
387 FROM " . MAIN_DB_PREFIX . "facture
388 WHERE fk_soc = " . (int) $socid . " AND entity IN (" . getEntity('facture') . ")
389 AND fk_statut IN (1, 2)";
390 }
391
392 // Orders
393 if ($type == 'all' || $type == 'orders') {
394 $queries[] = "SELECT 'Order' as source_type, rowid, ref, total_ttc as amount, date_commande as date_entry, fk_statut
395 FROM " . MAIN_DB_PREFIX . "commande
396 WHERE fk_soc = " . (int) $socid . " AND entity IN (" . getEntity('commande') . ")
397 AND fk_statut > 0";
398 }
399
400 // Proposals
401 if ($type == 'all' || $type == 'proposals') {
402 $queries[] = "SELECT 'Proposal' as source_type, rowid, ref, total_ttc as amount, datep as date_entry, fk_statut
403 FROM " . MAIN_DB_PREFIX . "propal
404 WHERE fk_soc = " . (int) $socid . " AND entity IN (" . getEntity('propal') . ")
405 AND fk_statut > 0";
406 }
407
408 if (empty($queries)) {
409 return [[$langs->transnoentitiesnoconv("Error") => "Invalid transaction type"]];
410 }
411
412 $sql = "SELECT * FROM (";
413 $sql .= implode(" UNION ", $queries);
414 $sql .= ") as combined_transactions ";
415 $whereParts = [];
416 if ($dateStart > 0) {
417 $whereParts[] = "date_entry >= '" . $this->db->idate($dateStart) . "'";
418 }
419 if ($dateEnd > 0) {
420 $whereParts[] = "date_entry <= '" . $this->db->idate($dateEnd) . "'";
421 }
422
423 if (!empty($whereParts)) {
424 $sql .= " WHERE " . implode(" AND ", $whereParts);
425 }
426
427 $sql .= " ORDER BY date_entry DESC";
428
429 $resql = $this->db->query($sql);
430 $list = [];
431 $totalAmt = 0.0;
432
433 if ($resql) {
434 while ($r = $this->db->fetch_object($resql)) {
435 $totalAmt += (float) $r->amount;
436
437 $statusTxt = "";
438 $urlPath = "";
439
440 if ($r->source_type === 'Invoice') {
441 $urlPath = "/compta/facture/card.php?id=" . $r->rowid;
442 if ($r->fk_statut == 2) {
443 $statusTxt = $langs->transnoentitiesnoconv("BillStatusPaid");
444 } elseif ($r->fk_statut == 1) {
445 $statusTxt = $langs->transnoentitiesnoconv("BillStatusNotPaid");
446 }
447 } elseif ($r->source_type === 'Order') {
448 $urlPath = "/commande/card.php?id=" . $r->rowid;
449 if ($r->fk_statut == 1) {
450 $statusTxt = $langs->transnoentitiesnoconv("StatusOrderValidated");
451 } elseif ($r->fk_statut == 2) {
452 $statusTxt = $langs->transnoentitiesnoconv("StatusOrderOnProcess");
453 } elseif ($r->fk_statut == 3) {
454 $statusTxt = $langs->transnoentitiesnoconv("StatusOrderDelivered");
455 }
456 } elseif ($r->source_type === 'Proposal') {
457 $urlPath = "/comm/propal/card.php?id=" . $r->rowid;
458 if ($r->fk_statut == 1) {
459 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusValidated");
460 } elseif ($r->fk_statut == 2) {
461 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusSigned");
462 } elseif ($r->fk_statut == 3) {
463 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusNotSigned");
464 } elseif ($r->fk_statut == 4) {
465 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusBilled");
466 }
467 }
468
469 $fullUrl = $urlPath ? DOL_URL_ROOT . $urlPath : "";
470 $refHtml = $fullUrl ? '<a href="' . $fullUrl . '">' . $r->ref . '</a>' : $r->ref;
471
472 $list[] = [
473 $langs->transnoentitiesnoconv("Type") => $langs->transnoentitiesnoconv($r->source_type),
474 $langs->transnoentitiesnoconv("Ref") => $refHtml,
475 $langs->transnoentitiesnoconv("Date") => dol_print_date($this->db->jdate($r->date_entry), 'day'),
476 $langs->transnoentitiesnoconv("Amount") => price($r->amount),
477 $langs->transnoentitiesnoconv("Status") => $statusTxt
478 ];
479 }
480 $this->db->free($resql);
481 }
482
483 if (empty($list)) {
484 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
485 }
486
487 // Summary
488 $list[] = [
489 $langs->transnoentitiesnoconv("Type") => $langs->transnoentitiesnoconv("Total"),
490 $langs->transnoentitiesnoconv("Ref") => "",
491 $langs->transnoentitiesnoconv("Date") => "",
492 $langs->transnoentitiesnoconv("Amount") => price($totalAmt),
493 $langs->transnoentitiesnoconv("Status") => ""
494 ];
495
496 return $list;
497 }
498
505 private function getPurchaseReport(array $args): array
506 {
507 global $langs;
508
509 $langs->loadLangs(array("main", "bills", "companies"));
510
511 $socid = $this->resolveThirdparty($args);
512 $groupBy = isset($args['group_by']) ? (string) $args['group_by'] : 'supplier';
513 $dateStart = dol_stringtotime($args['date_start']);
514 $dateEnd = dol_stringtotime($args['date_end']);
515 $list = [];
516 $totalSum = 0.0;
517
518 // Detailed report for a specific Supplier
519 if ($socid) {
520 $sql = "SELECT f.rowid, f.ref, f.total_ttc, f.datef, s.nom
521 FROM " . MAIN_DB_PREFIX . "facture_fourn as f
522 LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid
523 WHERE f.entity IN (" . getEntity('facture_fourn') . ")
524 AND f.fk_soc = " . (int) $socid . "
525 AND f.datef >= '" . $this->db->idate($dateStart) . "'
526 AND f.datef <= '" . $this->db->idate($dateEnd) . "'
527 AND f.fk_statut > 0
528 ORDER BY f.datef DESC";
529
530 $resql = $this->db->query($sql);
531 if ($resql) {
532 while ($r = $this->db->fetch_object($resql)) {
533 $totalSum += (float) $r->total_ttc;
534
535 $url = DOL_URL_ROOT . "/fourn/facture/card.php?id=" . $r->rowid;
536 $refHtml = '<a href="' . $url . '">' . $r->ref . '</a>';
537
538 $list[] = [
539 $langs->transnoentitiesnoconv("Ref") => $refHtml,
540 $langs->transnoentitiesnoconv("Date") => dol_print_date($this->db->jdate($r->datef), 'day'),
541 $langs->transnoentitiesnoconv("Supplier") => $r->nom,
542 $langs->transnoentitiesnoconv("Amount") => price($r->total_ttc)
543 ];
544 }
545 $this->db->free($resql);
546 }
547 } else { // Global Grouped Report
548 $sanitizedSqlGroup = "";
549 $colName = "";
550
551 if ($groupBy === 'month') {
552 $sanitizedSqlGroup = "DATE_FORMAT(f.datef, '%Y-%m')";
553 $colName = $langs->transnoentitiesnoconv("Month");
554 } else {
555 $sanitizedSqlGroup = "s.nom";
556 $colName = $langs->transnoentitiesnoconv("Supplier");
557 }
558
559 $sql = "SELECT " . $sanitizedSqlGroup . " as group_key, SUM(f.total_ttc) as total_amount, COUNT(f.rowid) as count_inv
560 FROM " . MAIN_DB_PREFIX . "facture_fourn as f
561 LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid
562 WHERE f.entity IN (" . getEntity('facture_fourn') . ")
563 AND f.datef >= '" . $this->db->idate($dateStart) . "'
564 AND f.datef <= '" . $this->db->idate($dateEnd) . "'
565 AND f.fk_statut > 0
566 GROUP BY group_key
567 ORDER BY total_amount DESC";
568
569 $resql = $this->db->query($sql);
570 if ($resql) {
571 while ($r = $this->db->fetch_object($resql)) {
572 $totalSum += (float) $r->total_amount;
573 $list[] = [
574 $colName => $r->group_key ? $r->group_key : $langs->transnoentitiesnoconv('Unknown'),
575 $langs->transnoentitiesnoconv("Number") => $r->count_inv,
576 $langs->transnoentitiesnoconv("Amount") => price($r->total_amount)
577 ];
578 }
579 $this->db->free($resql);
580 }
581 }
582
583 if (empty($list)) {
584 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
585 }
586
587 // Summary Row
588 $summary = [
589 $langs->transnoentitiesnoconv("Amount") => price($totalSum)
590 ];
591
592 if ($socid) {
593 $summary[$langs->transnoentitiesnoconv("Ref")] = $langs->transnoentitiesnoconv("Total");
594 $summary[$langs->transnoentitiesnoconv("Date")] = "";
595 $summary[$langs->transnoentitiesnoconv("Supplier")] = "";
596 } else {
597 $summary[($groupBy === 'month' ? $langs->transnoentitiesnoconv("Month") : $langs->transnoentitiesnoconv("Supplier"))] = $langs->transnoentitiesnoconv("Total");
598 $summary[$langs->transnoentitiesnoconv("Number")] = "";
599 }
600 $list[] = $summary;
601
602 return $list;
603 }
604
611 private function getInventoryReport(array $args): array
612 {
613 global $langs;
614
615 $langs->loadLangs(array("products", "stocks"));
616
617 $catId = isset($args['category_id']) ? (int) $args['category_id'] : 0;
618 $warehouseId = isset($args['warehouse_id']) ? (int) $args['warehouse_id'] : 0;
619 $includeZero = isset($args['include_zero_stock']) ? (bool) $args['include_zero_stock'] : false;
620
621 $sql = "SELECT p.rowid, p.ref, p.label, p.pmp, ";
622
623 if ($warehouseId > 0) {
624 $sql .= " ps.reel as stock_level ";
625 $sql .= " FROM " . MAIN_DB_PREFIX . "product as p ";
626 $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product_stock as ps ON p.rowid = ps.fk_product ";
627 $sql .= " WHERE ps.fk_entrepot = " . (int) $warehouseId;
628 } else {
629 $sql .= " p.stock as stock_level ";
630 $sql .= " FROM " . MAIN_DB_PREFIX . "product as p ";
631 $sql .= " WHERE 1=1 ";
632 }
633
634 $sql .= " AND p.entity IN (" . getEntity('product') . ")";
635
636 if ($catId > 0) {
637 $sql .= " AND p.rowid IN (SELECT fk_product FROM " . MAIN_DB_PREFIX . "categorie_product WHERE fk_categorie = " . (int) $catId . ")";
638 }
639
640 if (!$includeZero) {
641 if ($warehouseId > 0) {
642 $sql .= " AND ps.reel > 0";
643 } else {
644 $sql .= " AND p.stock > 0";
645 }
646 }
647
648 $sql .= " ORDER BY p.ref ASC LIMIT 200";
649
650 $resql = $this->db->query($sql);
651 $list = [];
652 $totalValuation = 0.0;
653 $totalItems = 0;
654
655 if ($resql) {
656 while ($r = $this->db->fetch_object($resql)) {
657 $stockVal = $r->stock_level * $r->pmp;
658 $totalValuation += $stockVal;
659 $totalItems += (int) $r->stock_level;
660
661 $url = DOL_URL_ROOT . "/product/card.php?id=" . $r->rowid;
662 $refHtml = '<a href="' . $url . '">' . $r->ref . '</a>';
663
664 $list[] = [
665 $langs->transnoentitiesnoconv("Ref") => $refHtml,
666 $langs->transnoentitiesnoconv("Label") => $r->label,
667 $langs->transnoentitiesnoconv("Stock") => $r->stock_level,
668 $langs->transnoentitiesnoconv("PMPValue") => price($r->pmp),
669 $langs->transnoentitiesnoconv("TotalValue") => price($stockVal)
670 ];
671 }
672 $this->db->free($resql);
673 }
674
675 if (empty($list)) {
676 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
677 }
678
679 $list[] = [
680 $langs->transnoentitiesnoconv("Ref") => $langs->transnoentitiesnoconv("Total"),
681 $langs->transnoentitiesnoconv("Label") => "",
682 $langs->transnoentitiesnoconv("Stock") => $totalItems,
683 $langs->transnoentitiesnoconv("PMPValue") => "",
684 $langs->transnoentitiesnoconv("TotalValue") => price($totalValuation)
685 ];
686
687 return $list;
688 }
689
696 private function getFinancialReport(array $args): array
697 {
698 global $langs;
699
700 $langs->loadLangs(array("compta", "bills"));
701 $dateStart = dol_stringtotime($args['date_start']);
702 $dateEnd = dol_stringtotime($args['date_end']);
703
704 // Income (Customer Invoices - Validated/Paid, no Drafts)
705 $sqlIncome = "SELECT SUM(total_ttc) as total FROM " . MAIN_DB_PREFIX . "facture
706 WHERE entity IN (" . getEntity('facture') . ")
707 AND datef >= '" . $this->db->idate($dateStart) . "'
708 AND datef <= '" . $this->db->idate($dateEnd) . "'
709 AND fk_statut IN (1, 2)";
710
711 $resIncome = $this->db->query($sqlIncome);
712 $objIncome = $this->db->fetch_object($resIncome);
713 $income = $objIncome && $objIncome->total ? (float) $objIncome->total : 0.0;
714
715 // Expenses (Supplier Invoices - Validated, no Drafts)
716 $sqlExpense = "SELECT SUM(total_ttc) as total FROM " . MAIN_DB_PREFIX . "facture_fourn
717 WHERE entity IN (" . getEntity('facture_fourn') . ")
718 AND datef >= '" . $this->db->idate($dateStart) . "'
719 AND datef <= '" . $this->db->idate($dateEnd) . "'
720 AND fk_statut > 0";
721
722 $resExpense = $this->db->query($sqlExpense);
723 $objExpense = $this->db->fetch_object($resExpense);
724 $expense = $objExpense && $objExpense->total ? (float) $objExpense->total : 0.0;
725
726 $net = $income - $expense;
727
728 $list = [
729 [
730 $langs->transnoentitiesnoconv("Category") => $langs->transnoentitiesnoconv("Income"),
731 $langs->transnoentitiesnoconv("Description") => $langs->transnoentitiesnoconv("BillsCustomers"),
732 $langs->transnoentitiesnoconv("Amount") => price($income)
733 ],
734 [
735 $langs->transnoentitiesnoconv("Category") => $langs->transnoentitiesnoconv("Expenses"),
736 $langs->transnoentitiesnoconv("Description") => $langs->transnoentitiesnoconv("BillsSuppliers"),
737 $langs->transnoentitiesnoconv("Amount") => price($expense)
738 ],
739 [
740 $langs->transnoentitiesnoconv("Category") => $langs->transnoentitiesnoconv("Total"),
741 $langs->transnoentitiesnoconv("Description") => $langs->transnoentitiesnoconv("Profit"),
742 $langs->transnoentitiesnoconv("Amount") => price($net)
743 ]
744 ];
745
746 return $list;
747 }
748}
Class to manage Dolibarr database access.
Abstract base class for all MCP (Model Context Protocol) tools.
Class ToolReports.
getCategories()
Return categories this tool belongs to.
execute(string $name, array $args)
Executes the requested tool function based on its name.
__construct(DoliDB $db)
Constructor.
getThirdpartyTransactions(array $args)
Generate a list of raw transactions (Invoices, Orders, Proposals).
getDefinitions()
Returns an array of tool definitions.
getPurchaseReport(array $args)
Generate a purchase/expense report.
getFinancialReport(array $args)
Generate a summary financial report (Income vs Expense).
getSalesReport(array $args)
Generate a sales/revenue report.
resolveThirdparty($args)
Resolves a Thirdparty ID from either an ID or a name.
getInventoryReport(array $args)
Generate an inventory report.
dol_stringtotime($string, $gm=1)
Convert a string date into a GM Timestamps date Warning: YYYY-MM-DDTHH:MM:SS+02:00 (RFC3339) is not s...
Definition date.lib.php:435
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
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).
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