dolibarr 25.0.0-alpha
reports.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 * Copyright (C) 2026 MDW <mdeweerd@users.noreply.github.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
31require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
32require_once DOL_DOCUMENT_ROOT . '/core/lib/date.lib.php';
33
39class ToolReports extends McpTool
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
64 public function getDefinitions(): array
65 {
66 return [
67 [
68 "name" => "get_thirdparty_transactions",
69 "description" => "Generate a list of raw transactions (Invoices, Orders) for a specific thirdparty.",
70 "inputSchema" => [
71 "type" => "object",
72 "properties" => [
73 "thirdparty_id" => ["type" => "integer", "description" => "The unique ID of the thirdparty."],
74 "thirdparty_name" => ["type" => "string", "description" => "The name of the thirdparty."],
75 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD). Compute it from the current date when the user says a relative period like this month."],
76 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD). Compute it from the current date when the user says a relative period."],
77 "transaction_type" => [
78 "type" => "string",
79 "enum" => ["all", "invoices", "orders", "proposals"],
80 "description" => "Filter by type.",
81 "default" => "all"
82 ]
83 ],
84 "oneOf" => [
85 ["required" => ["thirdparty_id"]],
86 ["required" => ["thirdparty_name"]]
87 ]
88 ]
89 ],
90 [
91 "name" => "get_sales_report",
92 "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.",
93 "inputSchema" => [
94 "type" => "object",
95 "properties" => [
96 "thirdparty_id" => [
97 "type" => "integer",
98 "description" => "Optional: The ID of the customer."
99 ],
100 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD). Compute it from the current date when the user says a relative period like this month."],
101 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD). Compute it from the current date when the user says a relative period."],
102 "group_by" => [
103 "type" => "string",
104 "enum" => ["thirdparty", "product", "month"],
105 "description" => "Only used if no Thirdparty is specified. Groups global results.",
106 "default" => "thirdparty"
107 ]
108 ],
109 "required" => ["date_start", "date_end"]
110 ]
111 ],
112 [
113 "name" => "get_purchase_report",
114 "description" => "Generate a purchase/expense report. If a Supplier is provided, it returns a detailed breakdown. Otherwise, it returns a global summary.",
115 "inputSchema" => [
116 "type" => "object",
117 "properties" => [
118 "thirdparty_id" => [
119 "type" => "integer",
120 "description" => "Optional: The ID of the supplier."
121 ],
122 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD). Compute it from the current date when the user says a relative period like this month."],
123 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD). Compute it from the current date when the user says a relative period."],
124 "group_by" => [
125 "type" => "string",
126 "enum" => ["supplier", "product", "month"],
127 "description" => "Only used if no Supplier is specified. Groups global results.",
128 "default" => "supplier"
129 ]
130 ],
131 "required" => ["date_start", "date_end"]
132 ]
133 ],
134 [
135 "name" => "get_inventory_report",
136 "description" => "Generate an inventory report showing current stock levels and valuation.",
137 "inputSchema" => [
138 "type" => "object",
139 "properties" => [
140 "category_id" => ["type" => "integer", "description" => "Filter by category ID."],
141 "warehouse_id" => ["type" => "integer", "description" => "Filter by warehouse ID."],
142 "include_zero_stock" => ["type" => "boolean", "default" => false]
143 ]
144 ]
145 ],
146 [
147 "name" => "get_financial_report",
148 "description" => "Generate a summary financial report (Income vs Expense) for a period.",
149 "inputSchema" => [
150 "type" => "object",
151 "properties" => [
152 "date_start" => ["type" => "string", "description" => "Start date (YYYY-MM-DD). Compute it from the current date when the user says a relative period like this month."],
153 "date_end" => ["type" => "string", "description" => "End date (YYYY-MM-DD). Compute it from the current date when the user says a relative period."]
154 ],
155 "required" => ["date_start", "date_end"]
156 ]
157 ],
158 ];
159 }
160
167 public function getRequiredRights(string $toolName)
168 {
169 $map = array(
170 'get_sales_report' => array(array('facture', 'lire')),
171 'get_purchase_report' => array(array('fournisseur', 'facture', 'lire')),
172 'get_inventory_report' => array(array('produit', 'lire'), array('stock', 'lire')),
173 'get_financial_report' => array(array('facture', 'lire'), array('fournisseur', 'facture', 'lire')),
174 'get_thirdparty_transactions' => array(array('societe', 'lire'), array('facture', 'lire'))
175 );
176
177 return isset($map[$toolName]) ? $map[$toolName] : self::RIGHTS_UNDECLARED;
178 }
179
186 public function getCategories(): array
187 {
188 return ['reporting', 'commercial', 'billing', 'stock'];
189 }
190
198 public function execute(string $name, array $args)
199 {
200 switch ($name) {
201 case 'get_thirdparty_transactions':
202 return $this->getThirdpartyTransactions($args);
203 case 'get_sales_report':
204 return $this->getSalesReport($args);
205 case 'get_purchase_report':
206 return $this->getPurchaseReport($args);
207 case 'get_inventory_report':
208 return $this->getInventoryReport($args);
209 case 'get_financial_report':
210 return $this->getFinancialReport($args);
211 default:
212 return ["error" => "Tool function '$name' not found."];
213 }
214 }
215
227 private function resolveThirdparty($args)
228 {
229
230 if (!empty($args['thirdparty_id'])) {
231 return (int) $args['thirdparty_id'];
232 }
233
234 if (!empty($args['thirdparty_name'])) {
235 $sqlName = $this->db->escape($args['thirdparty_name']);
236
237 $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "societe
238 WHERE nom LIKE '%" . $sqlName . "%'
239 AND entity IN (" . getEntity('societe') . ")
240 LIMIT 1";
241
242 $resql = $this->db->query($sql);
243 if ($resql && $obj = $this->db->fetch_object($resql)) {
244 return $obj->rowid;
245 }
246 }
247
248 return null;
249 }
250
257 private function getSalesReport(array $args): array
258 {
259 global $langs;
260
261 $langs->loadLangs(array("main", "bills", "companies", "products"));
262
263 $limit = isset($args['limit']) ? (int) $args['limit'] : 50;
264 $dateStart = dol_stringtotime($args['date_start']);
265 $dateEnd = dol_stringtotime($args['date_end']);
266 $socid = $this->resolveThirdparty($args);
267 $groupBy = isset($args['group_by']) ? (string) $args['group_by'] : 'thirdparty';
268
269 $list = [];
270 $totalSum = 0.0;
271 // Status Filter: Valid (1) and Paid (2). Exclude Draft (0) and Abandoned (3).
272 $dateRange = " AND f.datef >= '" . $this->db->idate($dateStart)
273 . "' AND f.datef <= '" . $this->db->idate($dateEnd)
274 . "' AND f.fk_statut IN (1, 2)";
275
276 // CASE 1 -- Detailed list for a specific thirdparty.
277 if ($socid) {
278 $sql = "SELECT f.rowid, f.ref, f.total_ttc, f.fk_statut, f.paye, f.datef, s.nom FROM "
279 . MAIN_DB_PREFIX . "facture as f LEFT JOIN "
280 . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid WHERE f.entity IN ("
281 . getEntity('facture') . ")"
282 . $dateRange
283 . " AND f.fk_soc = " . (int) $socid
284 . " ORDER BY f.datef DESC LIMIT " . ((int) $limit);
285
286 $resql = $this->db->query($sql);
287 if ($resql) {
288 while ($r = $this->db->fetch_object($resql)) {
289 $totalSum += (float) $r->total_ttc;
290
291 $statusLabel = $langs->transnoentitiesnoconv("Unknown");
292 if ($r->fk_statut == 1 && $r->paye == 0) {
293 $statusLabel = $langs->transnoentitiesnoconv("BillStatusNotPaid");
294 } elseif ($r->fk_statut == 1 && $r->paye == 1) {
295 $statusLabel = $langs->transnoentitiesnoconv("BillStatusStarted");
296 } elseif ($r->fk_statut == 2) {
297 $statusLabel = $langs->transnoentitiesnoconv("BillStatusPaid");
298 }
299
300 $url = DOL_URL_ROOT . "/compta/facture/card.php?id=" . $r->rowid;
301 $refHtml = '<a href="' . $url . '">' . $r->ref . '</a>';
302
303 $list[] = [
304 $langs->transnoentitiesnoconv("Ref") => $refHtml,
305 $langs->transnoentitiesnoconv("Date") => dol_print_date($this->db->jdate($r->datef), 'day'),
306 $langs->transnoentitiesnoconv("Customer") => $r->nom,
307 $langs->transnoentitiesnoconv("Amount") => price($r->total_ttc),
308 $langs->transnoentitiesnoconv("Status") => $statusLabel
309 ];
310 }
311 $this->db->free($resql);
312 }
313 } else {
314 // CASE 2 -- Global grouped report.
315 // Mirrors the pattern already used by getPurchaseReport(); previous implementation
316 // of getSalesReport() ignored $groupBy entirely and always returned a flat list.
317 $sanitizedSqlGroup = '';
318 $colName = '';
319 $sqlJoin = " LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid";
320
321 if ($groupBy === 'month') {
322 $sanitizedSqlGroup = "DATE_FORMAT(f.datef, '%Y-%m')";
323 $colName = $langs->transnoentitiesnoconv("Month");
324 } elseif ($groupBy === 'product') {
325 // Aggregate on product line items. Lines without product_id fall back to their description.
326 $sanitizedSqlGroup = "COALESCE(p.ref, fd.description, '?')";
327 $colName = $langs->transnoentitiesnoconv("Product");
328 $sqlJoin .= " INNER JOIN " . MAIN_DB_PREFIX . "facturedet as fd ON fd.fk_facture = f.rowid LEFT JOIN "
329 . MAIN_DB_PREFIX . "product as p ON fd.fk_product = p.rowid";
330 } else {
331 // Default: group by customer
332 $sanitizedSqlGroup = "s.nom";
333 $colName = $langs->transnoentitiesnoconv("Customer");
334 }
335
336 // For product grouping we sum line totals (more accurate per-product);
337 // otherwise we sum the invoice total_ttc.
338 $amountExpr = ($groupBy === 'product') ? "SUM(fd.total_ttc)" : "SUM(f.total_ttc)";
339 $countExpr = ($groupBy === 'product') ? "COUNT(DISTINCT f.rowid)" : "COUNT(f.rowid)";
340
341 $sql = "SELECT " . $sanitizedSqlGroup . " as group_key, "
342 . $amountExpr . " as total_amount, "
343 . $countExpr . " as count_inv FROM "
344 . MAIN_DB_PREFIX . "facture as f"
345 . $sqlJoin
346 . " WHERE f.entity IN (" . getEntity('facture') . ")"
347 . $dateRange
348 . " GROUP BY group_key ORDER BY total_amount DESC LIMIT "
349 . ((int) max(1, $limit));
350
351 $resql = $this->db->query($sql);
352 if ($resql) {
353 while ($r = $this->db->fetch_object($resql)) {
354 $totalSum += (float) $r->total_amount;
355 $list[] = [
356 $colName => $r->group_key ? $r->group_key : $langs->transnoentitiesnoconv('Unknown'),
357 $langs->transnoentitiesnoconv("Number") => (int) $r->count_inv,
358 $langs->transnoentitiesnoconv("Amount") => price($r->total_amount)
359 ];
360 }
361 $this->db->free($resql);
362 }
363 }
364
365 if (empty($list)) {
366 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
367 }
368
369 // Append Total Row (shape depends on detailed-vs-grouped path)
370 if ($socid) {
371 $list[] = [
372 $langs->transnoentitiesnoconv("Ref") => $langs->transnoentitiesnoconv("Total"),
373 $langs->transnoentitiesnoconv("Date") => "",
374 $langs->transnoentitiesnoconv("Customer") => "",
375 $langs->transnoentitiesnoconv("Amount") => price($totalSum),
376 $langs->transnoentitiesnoconv("Status") => ""
377 ];
378 } else {
379 $list[] = [
380 $langs->transnoentitiesnoconv("Total") => $langs->transnoentitiesnoconv("Total"),
381 $langs->transnoentitiesnoconv("Amount") => price($totalSum)
382 ];
383 }
384
385 return $list;
386 }
387
394 private function getThirdpartyTransactions(array $args): array
395 {
396 global $langs;
397
398 $langs->loadLangs(array("main", "bills", "orders", "propal"));
399
400 $dateStart = dol_stringtotime($args['date_start']);
401 $dateEnd = dol_stringtotime($args['date_end']);
402 $type = isset($args['transaction_type']) ? (string) $args['transaction_type'] : 'all';
403
404 $socid = $this->resolveThirdparty($args);
405 if (!$socid) {
406 $langs->load("errors");
407 return [[$langs->transnoentitiesnoconv("Error") => $langs->transnoentitiesnoconv("ErrorThirdPartyNotFound")]];
408 }
409
410 $sqlQueries = [];
411
412 // Invoices
413 if ($type == 'all' || $type == 'invoices') {
414 $sqlQueries[] = "SELECT 'Invoice' as source_type, rowid, ref, total_ttc as amount, datef as date_entry, fk_statut
415 FROM " . MAIN_DB_PREFIX . "facture
416 WHERE fk_soc = " . (int) $socid . " AND entity IN (" . getEntity('facture') . ")
417 AND fk_statut IN (1, 2)";
418 }
419
420 // Orders
421 if ($type == 'all' || $type == 'orders') {
422 $sqlQueries[] = "SELECT 'Order' as source_type, rowid, ref, total_ttc as amount, date_commande as date_entry, fk_statut
423 FROM " . MAIN_DB_PREFIX . "commande
424 WHERE fk_soc = " . (int) $socid . " AND entity IN (" . getEntity('commande') . ")
425 AND fk_statut > 0";
426 }
427
428 // Proposals
429 if ($type == 'all' || $type == 'proposals') {
430 $sqlQueries[] = "SELECT 'Proposal' as source_type, rowid, ref, total_ttc as amount, datep as date_entry, fk_statut
431 FROM " . MAIN_DB_PREFIX . "propal
432 WHERE fk_soc = " . (int) $socid . " AND entity IN (" . getEntity('propal') . ")
433 AND fk_statut > 0";
434 }
435
436 if (empty($sqlQueries)) {
437 return [[$langs->transnoentitiesnoconv("Error") => "Invalid transaction type"]];
438 }
439
440 $sql = "SELECT * FROM (";
441 $sql .= implode(" UNION ", $sqlQueries);
442 $sql .= ") as combined_transactions ";
443 $whereParts = [];
444 if ($dateStart > 0) {
445 $whereParts[] = "date_entry >= '" . $this->db->idate($dateStart) . "'";
446 }
447 if ($dateEnd > 0) {
448 $whereParts[] = "date_entry <= '" . $this->db->idate($dateEnd) . "'";
449 }
450
451 if (!empty($whereParts)) {
452 $sql .= " WHERE " . implode(" AND ", $whereParts);
453 }
454
455 $sql .= " ORDER BY date_entry DESC";
456
457 $resql = $this->db->query($sql);
458 $list = [];
459 $totalAmt = 0.0;
460
461 if ($resql) {
462 while ($r = $this->db->fetch_object($resql)) {
463 $totalAmt += (float) $r->amount;
464
465 $statusTxt = "";
466 $urlPath = "";
467
468 if ($r->source_type === 'Invoice') {
469 $urlPath = "/compta/facture/card.php?id=" . $r->rowid;
470 if ($r->fk_statut == 2) {
471 $statusTxt = $langs->transnoentitiesnoconv("BillStatusPaid");
472 } elseif ($r->fk_statut == 1) {
473 $statusTxt = $langs->transnoentitiesnoconv("BillStatusNotPaid");
474 }
475 } elseif ($r->source_type === 'Order') {
476 $urlPath = "/commande/card.php?id=" . $r->rowid;
477 if ($r->fk_statut == 1) {
478 $statusTxt = $langs->transnoentitiesnoconv("StatusOrderValidated");
479 } elseif ($r->fk_statut == 2) {
480 $statusTxt = $langs->transnoentitiesnoconv("StatusOrderOnProcess");
481 } elseif ($r->fk_statut == 3) {
482 $statusTxt = $langs->transnoentitiesnoconv("StatusOrderDelivered");
483 }
484 } elseif ($r->source_type === 'Proposal') {
485 $urlPath = "/comm/propal/card.php?id=" . $r->rowid;
486 if ($r->fk_statut == 1) {
487 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusValidated");
488 } elseif ($r->fk_statut == 2) {
489 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusSigned");
490 } elseif ($r->fk_statut == 3) {
491 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusNotSigned");
492 } elseif ($r->fk_statut == 4) {
493 $statusTxt = $langs->transnoentitiesnoconv("PropalStatusBilled");
494 }
495 }
496
497 $fullUrl = $urlPath ? DOL_URL_ROOT . $urlPath : "";
498 $refHtml = $fullUrl ? '<a href="' . $fullUrl . '">' . $r->ref . '</a>' : $r->ref;
499
500 $list[] = [
501 $langs->transnoentitiesnoconv("Type") => $langs->transnoentitiesnoconv($r->source_type),
502 $langs->transnoentitiesnoconv("Ref") => $refHtml,
503 $langs->transnoentitiesnoconv("Date") => dol_print_date($this->db->jdate($r->date_entry), 'day'),
504 $langs->transnoentitiesnoconv("Amount") => price($r->amount),
505 $langs->transnoentitiesnoconv("Status") => $statusTxt
506 ];
507 }
508 $this->db->free($resql);
509 }
510
511 if (empty($list)) {
512 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
513 }
514
515 // Summary
516 $list[] = [
517 $langs->transnoentitiesnoconv("Type") => $langs->transnoentitiesnoconv("Total"),
518 $langs->transnoentitiesnoconv("Ref") => "",
519 $langs->transnoentitiesnoconv("Date") => "",
520 $langs->transnoentitiesnoconv("Amount") => price($totalAmt),
521 $langs->transnoentitiesnoconv("Status") => ""
522 ];
523
524 return $list;
525 }
526
533 private function getPurchaseReport(array $args): array
534 {
535 global $langs;
536
537 $langs->loadLangs(array("main", "bills", "companies"));
538
539 $socid = $this->resolveThirdparty($args);
540 $groupBy = isset($args['group_by']) ? (string) $args['group_by'] : 'supplier';
541 $dateStart = dol_stringtotime($args['date_start']);
542 $dateEnd = dol_stringtotime($args['date_end']);
543 $list = [];
544 $totalSum = 0.0;
545
546 // Detailed report for a specific Supplier
547 if ($socid) {
548 $sql = "SELECT f.rowid, f.ref, f.total_ttc, f.datef, s.nom
549 FROM " . MAIN_DB_PREFIX . "facture_fourn as f
550 LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid
551 WHERE f.entity IN (" . getEntity('facture_fourn') . ")
552 AND f.fk_soc = " . (int) $socid . "
553 AND f.datef >= '" . $this->db->idate($dateStart) . "'
554 AND f.datef <= '" . $this->db->idate($dateEnd) . "'
555 AND f.fk_statut > 0
556 ORDER BY f.datef DESC";
557
558 $resql = $this->db->query($sql);
559 if ($resql) {
560 while ($r = $this->db->fetch_object($resql)) {
561 $totalSum += (float) $r->total_ttc;
562
563 $url = DOL_URL_ROOT . "/fourn/facture/card.php?id=" . $r->rowid;
564 $refHtml = '<a href="' . $url . '">' . $r->ref . '</a>';
565
566 $list[] = [
567 $langs->transnoentitiesnoconv("Ref") => $refHtml,
568 $langs->transnoentitiesnoconv("Date") => dol_print_date($this->db->jdate($r->datef), 'day'),
569 $langs->transnoentitiesnoconv("Supplier") => $r->nom,
570 $langs->transnoentitiesnoconv("Amount") => price($r->total_ttc)
571 ];
572 }
573 $this->db->free($resql);
574 }
575 } else { // Global Grouped Report
576 $sanitizedSqlGroup = "";
577 $colName = "";
578
579 if ($groupBy === 'month') {
580 $sanitizedSqlGroup = "DATE_FORMAT(f.datef, '%Y-%m')";
581 $colName = $langs->transnoentitiesnoconv("Month");
582 } else {
583 $sanitizedSqlGroup = "s.nom";
584 $colName = $langs->transnoentitiesnoconv("Supplier");
585 }
586
587 $sql = "SELECT " . $sanitizedSqlGroup . " as group_key, SUM(f.total_ttc) as total_amount, COUNT(f.rowid) as count_inv
588 FROM " . MAIN_DB_PREFIX . "facture_fourn as f
589 LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON f.fk_soc = s.rowid
590 WHERE f.entity IN (" . getEntity('facture_fourn') . ")
591 AND f.datef >= '" . $this->db->idate($dateStart) . "'
592 AND f.datef <= '" . $this->db->idate($dateEnd) . "'
593 AND f.fk_statut > 0
594 GROUP BY group_key
595 ORDER BY total_amount DESC";
596
597 $resql = $this->db->query($sql);
598 if ($resql) {
599 while ($r = $this->db->fetch_object($resql)) {
600 $totalSum += (float) $r->total_amount;
601 $list[] = [
602 $colName => $r->group_key ? $r->group_key : $langs->transnoentitiesnoconv('Unknown'),
603 $langs->transnoentitiesnoconv("Number") => $r->count_inv,
604 $langs->transnoentitiesnoconv("Amount") => price($r->total_amount)
605 ];
606 }
607 $this->db->free($resql);
608 }
609 }
610
611 if (empty($list)) {
612 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
613 }
614
615 // Summary Row
616 $summary = [
617 $langs->transnoentitiesnoconv("Amount") => price($totalSum)
618 ];
619
620 if ($socid) {
621 $summary[$langs->transnoentitiesnoconv("Ref")] = $langs->transnoentitiesnoconv("Total");
622 $summary[$langs->transnoentitiesnoconv("Date")] = "";
623 $summary[$langs->transnoentitiesnoconv("Supplier")] = "";
624 } else {
625 $summary[($groupBy === 'month' ? $langs->transnoentitiesnoconv("Month") : $langs->transnoentitiesnoconv("Supplier"))] = $langs->transnoentitiesnoconv("Total");
626 $summary[$langs->transnoentitiesnoconv("Number")] = "";
627 }
628 $list[] = $summary;
629
630 return $list;
631 }
632
639 private function getInventoryReport(array $args): array
640 {
641 global $langs;
642
643 $langs->loadLangs(array("products", "stocks"));
644
645 $catId = isset($args['category_id']) ? (int) $args['category_id'] : 0;
646 $warehouseId = isset($args['warehouse_id']) ? (int) $args['warehouse_id'] : 0;
647 $includeZero = isset($args['include_zero_stock']) ? (bool) $args['include_zero_stock'] : false;
648
649 $sql = "SELECT p.rowid, p.ref, p.label, p.pmp, ";
650
651 if ($warehouseId > 0) {
652 $sql .= " ps.reel as stock_level ";
653 $sql .= " FROM " . MAIN_DB_PREFIX . "product as p ";
654 $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "product_stock as ps ON p.rowid = ps.fk_product ";
655 $sql .= " WHERE ps.fk_entrepot = " . (int) $warehouseId;
656 } else {
657 $sql .= " p.stock as stock_level ";
658 $sql .= " FROM " . MAIN_DB_PREFIX . "product as p ";
659 $sql .= " WHERE 1=1 ";
660 }
661
662 $sql .= " AND p.entity IN (" . getEntity('product') . ")";
663
664 if ($catId > 0) {
665 $sql .= " AND p.rowid IN (SELECT fk_product FROM " . MAIN_DB_PREFIX . "categorie_product WHERE fk_categorie = " . (int) $catId . ")";
666 }
667
668 if (!$includeZero) {
669 if ($warehouseId > 0) {
670 $sql .= " AND ps.reel > 0";
671 } else {
672 $sql .= " AND p.stock > 0";
673 }
674 }
675
676 $sql .= " ORDER BY p.ref ASC LIMIT 200";
677
678 $resql = $this->db->query($sql);
679 $list = [];
680 $totalValuation = 0.0;
681 $totalItems = 0;
682
683 if ($resql) {
684 while ($r = $this->db->fetch_object($resql)) {
685 $stockVal = $r->stock_level * $r->pmp;
686 $totalValuation += $stockVal;
687 $totalItems += (int) $r->stock_level;
688
689 $url = DOL_URL_ROOT . "/product/card.php?id=" . $r->rowid;
690 $refHtml = '<a href="' . $url . '">' . $r->ref . '</a>';
691
692 $list[] = [
693 $langs->transnoentitiesnoconv("Ref") => $refHtml,
694 $langs->transnoentitiesnoconv("Label") => $r->label,
695 $langs->transnoentitiesnoconv("Stock") => $r->stock_level,
696 $langs->transnoentitiesnoconv("PMPValue") => price($r->pmp),
697 $langs->transnoentitiesnoconv("TotalValue") => price($stockVal)
698 ];
699 }
700 $this->db->free($resql);
701 }
702
703 if (empty($list)) {
704 return [[$langs->transnoentitiesnoconv("Info") => $langs->transnoentitiesnoconv("NoRecordFound")]];
705 }
706
707 $list[] = [
708 $langs->transnoentitiesnoconv("Ref") => $langs->transnoentitiesnoconv("Total"),
709 $langs->transnoentitiesnoconv("Label") => "",
710 $langs->transnoentitiesnoconv("Stock") => $totalItems,
711 $langs->transnoentitiesnoconv("PMPValue") => "",
712 $langs->transnoentitiesnoconv("TotalValue") => price($totalValuation)
713 ];
714
715 return $list;
716 }
717
724 private function getFinancialReport(array $args): array
725 {
726 global $langs;
727
728 $langs->loadLangs(array("compta", "bills"));
729 $dateStart = dol_stringtotime($args['date_start']);
730 $dateEnd = dol_stringtotime($args['date_end']);
731
732 // Income (Customer Invoices - Validated/Paid, no Drafts)
733 $sqlIncome = "SELECT SUM(total_ttc) as total FROM " . MAIN_DB_PREFIX . "facture
734 WHERE entity IN (" . getEntity('facture') . ")
735 AND datef >= '" . $this->db->idate($dateStart) . "'
736 AND datef <= '" . $this->db->idate($dateEnd) . "'
737 AND fk_statut IN (1, 2)";
738
739 $resIncome = $this->db->query($sqlIncome);
740 $objIncome = $this->db->fetch_object($resIncome);
741 $income = $objIncome && $objIncome->total ? (float) $objIncome->total : 0.0;
742
743 // Expenses (Supplier Invoices - Validated, no Drafts)
744 $sqlExpense = "SELECT SUM(total_ttc) as total FROM " . MAIN_DB_PREFIX . "facture_fourn
745 WHERE entity IN (" . getEntity('facture_fourn') . ")
746 AND datef >= '" . $this->db->idate($dateStart) . "'
747 AND datef <= '" . $this->db->idate($dateEnd) . "'
748 AND fk_statut > 0";
749
750 $resExpense = $this->db->query($sqlExpense);
751 $objExpense = $this->db->fetch_object($resExpense);
752 $expense = $objExpense && $objExpense->total ? (float) $objExpense->total : 0.0;
753
754 $net = $income - $expense;
755
756 $list = [
757 [
758 $langs->transnoentitiesnoconv("Category") => $langs->transnoentitiesnoconv("Income"),
759 $langs->transnoentitiesnoconv("Description") => $langs->transnoentitiesnoconv("BillsCustomers"),
760 $langs->transnoentitiesnoconv("Amount") => price($income)
761 ],
762 [
763 $langs->transnoentitiesnoconv("Category") => $langs->transnoentitiesnoconv("Expenses"),
764 $langs->transnoentitiesnoconv("Description") => $langs->transnoentitiesnoconv("BillsSuppliers"),
765 $langs->transnoentitiesnoconv("Amount") => price($expense)
766 ],
767 [
768 $langs->transnoentitiesnoconv("Category") => $langs->transnoentitiesnoconv("Total"),
769 $langs->transnoentitiesnoconv("Description") => $langs->transnoentitiesnoconv("Profit"),
770 $langs->transnoentitiesnoconv("Amount") => price($net)
771 ]
772 ];
773
774 return $list;
775 }
776}
Class to manage Dolibarr database access.
Abstract base class for all MCP (Model Context Protocol) tools.
const RIGHTS_UNDECLARED
Default of getRequiredRights(): the class never declared anything, deny.
Class ToolReports.
getCategories()
Return categories this tool belongs to.
execute(string $name, array $args)
Executes the requested tool function based on its name.
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.
getRequiredRights(string $toolName)
Aggregated reports: the widest data reach of any tool class.
__construct(DoliDB $db, $user=null, $conf=null)
Constructor.
getInventoryReport(array $args)
Generate an inventory report.
dol_stringtotime($string, $gm=1, $processnotimeasnoon=0)
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:442
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.
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