* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. */ /** * \file htdocs/custom/buchaltungswidget/core/boxes/box_rentabilitaet.php * \ingroup buchaltungswidget * \brief Widget showing profitability analysis - materials purchased vs invoiced */ include_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php'; /** * Class to manage the profitability analysis widget */ class box_rentabilitaet extends ModeleBoxes { public $boxcode = "rentabilitaet"; public $boximg = "accountancy"; public $boxlabel = "Rentabilitaet"; public $depends = array("facture", "fournisseur"); /** * Constructor */ public function __construct($db, $param = '') { global $user; $this->db = $db; $this->hidden = !($user->hasRight('facture', 'lire') || $user->hasRight('fournisseur', 'facture', 'lire')); } /** * Load data into info_box_contents array to show a widget */ public function loadBox($max = 5) { global $conf, $langs, $user; $langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta")); $this->info_box_head = array( 'text' => $langs->trans("Rentabilitaet"), 'sublink' => dol_buildpath('/buchaltungswidget/rentabilitaet_detail.php', 1), 'subpicto' => 'chart', 'subtext' => $langs->trans("ShowDetails"), 'limit' => 0, 'graph' => false, 'nbcol' => 4, ); if (!$user->hasRight('facture', 'lire') && !$user->hasRight('fournisseur', 'facture', 'lire')) { $this->info_box_contents[0][0] = array( 'td' => 'class="center"', 'text' => $langs->trans("ReadPermissionNotAllowed"), ); return; } $currentYear = date('Y'); $lastYear = $currentYear - 1; $nextYear = $currentYear + 1; $currentMonth = date('n'); // Get profitability data for all years $dataCurrentYear = $this->getProfitabilityByMonth($currentYear); $dataLastYear = $this->getProfitabilityByMonth($lastYear); // Calculate projection $projectionNextYear = $this->calculateProjection($dataCurrentYear, $dataLastYear); // Build the output $this->info_box_contents = array(); $line = 0; // Mini chart area - dual axis bar/line chart $chartId = 'rentabilitaet_chart_'.uniqid(); $chartData = $this->prepareChartData($dataCurrentYear, $dataLastYear, $currentYear, $lastYear); $this->info_box_contents[$line][] = array( 'td' => 'colspan="4" class="buchaltung-chart-container"', 'text' => ' ', 'asis' => 1, ); $line++; // Summary table header $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-header"', 'text' => ''); $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-header right"', 'text' => $lastYear); $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-header right buchaltung-current-quarter"', 'text' => $currentYear); $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-header right buchaltung-future"', 'text' => $nextYear.' *'); $line++; // Materials purchased (only for customers!) $purchasedLast = array_sum($dataLastYear['purchased']); $purchasedCurrent = array_sum(array_slice($dataCurrentYear['purchased'], 0, $currentMonth, true)); $purchasedProjection = $projectionNextYear['purchased']; $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label"', 'text' => $langs->trans("MaterialsPurchasedForCustomers")); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-lastyear"', 'text' => price($purchasedLast, 0, $langs, 1, 0, 0, $conf->currency)); $this->info_box_contents[$line][] = array('td' => 'class="right"', 'text' => price($purchasedCurrent, 0, $langs, 1, 0, 0, $conf->currency)); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-future"', 'text' => price($purchasedProjection, 0, $langs, 1, 0, 0, $conf->currency)); $line++; // Materials & Services invoiced $invoicedLast = array_sum($dataLastYear['invoiced']); $invoicedCurrent = array_sum(array_slice($dataCurrentYear['invoiced'], 0, $currentMonth, true)); $invoicedProjection = $projectionNextYear['invoiced']; $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label"', 'text' => $langs->trans("MaterialsServicesInvoiced")); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-lastyear"', 'text' => price($invoicedLast, 0, $langs, 1, 0, 0, $conf->currency)); $this->info_box_contents[$line][] = array('td' => 'class="right"', 'text' => price($invoicedCurrent, 0, $langs, 1, 0, 0, $conf->currency)); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-future"', 'text' => price($invoicedProjection, 0, $langs, 1, 0, 0, $conf->currency)); $line++; // Gross profit $grossProfitLast = $invoicedLast - $purchasedLast; $grossProfitCurrent = $invoicedCurrent - $purchasedCurrent; $grossProfitProjection = $invoicedProjection - $purchasedProjection; $colorLast = $grossProfitLast >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $colorCurrent = $grossProfitCurrent >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $colorProjection = $grossProfitProjection >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label"', 'text' => $langs->trans("GrossProfit")); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-lastyear '.$colorLast.'"', 'text' => price($grossProfitLast, 0, $langs, 1, 0, 0, $conf->currency)); $this->info_box_contents[$line][] = array('td' => 'class="right '.$colorCurrent.'"', 'text' => ''.price($grossProfitCurrent, 0, $langs, 1, 0, 0, $conf->currency).'', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-future '.$colorProjection.'"', 'text' => price($grossProfitProjection, 0, $langs, 1, 0, 0, $conf->currency)); $line++; // Profit margin percentage $marginLast = ($purchasedLast > 0) ? (($invoicedLast - $purchasedLast) / $purchasedLast) * 100 : 0; $marginCurrent = ($purchasedCurrent > 0) ? (($invoicedCurrent - $purchasedCurrent) / $purchasedCurrent) * 100 : 0; $marginProjection = ($purchasedProjection > 0) ? (($invoicedProjection - $purchasedProjection) / $purchasedProjection) * 100 : 0; $colorLast = $marginLast >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $colorCurrent = $marginCurrent >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $colorProjection = $marginProjection >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label buchaltung-profit-row"', 'text' => ''.$langs->trans("ProfitMargin").'', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-profit-row buchaltung-lastyear '.$colorLast.'"', 'text' => ''.number_format($marginLast, 1, ',', '.').' %', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-profit-row '.$colorCurrent.'"', 'text' => ''.number_format($marginCurrent, 1, ',', '.').' %', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-profit-row buchaltung-future '.$colorProjection.'"', 'text' => ''.number_format($marginProjection, 1, ',', '.').' %', 'asis' => 1); $line++; // Productivity rating $rating = $this->getProductivityRating($marginCurrent); $this->info_box_contents[$line][] = array( 'td' => 'colspan="4" class="buchaltung-rating"', 'text' => '
'.$langs->trans("ProductivityRating").': '.$rating['text'].' '.$rating['description'].'
', 'asis' => 1, ); $line++; // Footer note $this->info_box_contents[$line][] = array( 'td' => 'colspan="4" class="buchaltung-footnote"', 'text' => '* '.$langs->trans("StatisticalProjection").' | '.$langs->trans("OnlyCustomerMaterials").'', 'asis' => 1, ); } /** * Prepare chart data for monthly display */ private function prepareChartData($currentData, $lastData, $currentYear, $lastYear) { $labels = array('Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'); $currentMonth = date('n'); $purchased = array(); $invoiced = array(); $marginPercent = array(); $marginColors = array(); for ($m = 1; $m <= 12; $m++) { if ($m <= $currentMonth) { $p = isset($currentData['purchased'][$m]) ? $currentData['purchased'][$m] : 0; $i = isset($currentData['invoiced'][$m]) ? $currentData['invoiced'][$m] : 0; $purchased[] = round($p, 2); $invoiced[] = round($i, 2); $margin = ($p > 0) ? round((($i - $p) / $p) * 100, 1) : 0; $marginPercent[] = $margin; $marginColors[] = $margin >= 0 ? 'rgba(40, 167, 69, 1)' : 'rgba(220, 53, 69, 1)'; } else { $purchased[] = 0; $invoiced[] = 0; $marginPercent[] = null; $marginColors[] = 'rgba(200, 200, 200, 0.5)'; } } return array( 'labels' => $labels, 'purchased' => $purchased, 'invoiced' => $invoiced, 'marginPercent' => $marginPercent, 'marginColors' => $marginColors, ); } /** * Get profitability data by month * IMPORTANT: Only materials purchased FOR CUSTOMERS, not general company expenses */ private function getProfitabilityByMonth($year) { global $conf; $result = array( 'purchased' => array_fill(1, 12, 0), 'invoiced' => array_fill(1, 12, 0), ); // Materials purchased FOR CUSTOMERS only (products, not services) // This should be materials that are resold or used in customer projects $sql = "SELECT MONTH(f.datef) as month, SUM(fd.total_ht) as total"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn as f"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."facture_fourn_det as fd ON fd.fk_facture_fourn = f.rowid"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = fd.fk_product"; $sql .= " WHERE f.fk_statut > 0 AND f.entity = ".((int) $conf->entity); $sql .= " AND YEAR(f.datef) = ".((int) $year); // Only products (type 0), not services (type 1) // And only products that are meant for resale or customer projects $sql .= " AND p.fk_product_type = 0"; // Products only $sql .= " AND (p.tobuy = 1 OR p.tosell = 1)"; // Products that are bought/sold $sql .= " GROUP BY MONTH(f.datef)"; $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { $result['purchased'][$obj->month] = (float) $obj->total; } $this->db->free($resql); } // All materials and services invoiced to customers $sql = "SELECT MONTH(f.datef) as month, SUM(fd.total_ht) as total"; $sql .= " FROM ".MAIN_DB_PREFIX."facture as f"; $sql .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet as fd ON fd.fk_facture = f.rowid"; $sql .= " WHERE f.fk_statut > 0 AND f.entity = ".((int) $conf->entity); $sql .= " AND YEAR(f.datef) = ".((int) $year); $sql .= " GROUP BY MONTH(f.datef)"; $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { $result['invoiced'][$obj->month] = (float) $obj->total; } $this->db->free($resql); } return $result; } /** * Calculate statistical projection for next year */ private function calculateProjection($currentData, $lastData) { $currentMonth = date('n'); $avgPurchased = array_sum(array_slice($currentData['purchased'], 0, $currentMonth, true)) / max(1, $currentMonth); $avgInvoiced = array_sum(array_slice($currentData['invoiced'], 0, $currentMonth, true)) / max(1, $currentMonth); // Calculate trend $lastYearTotal = array_sum($lastData['invoiced']); $currentYearProjected = $avgInvoiced * 12; $growthRate = ($lastYearTotal > 0) ? (($currentYearProjected - $lastYearTotal) / $lastYearTotal) : 0; $growthRate = max(-0.3, min(0.3, $growthRate)); return array( 'purchased' => round($avgPurchased * 12 * (1 + $growthRate * 0.5), 2), 'invoiced' => round($avgInvoiced * 12 * (1 + $growthRate * 0.7), 2), ); } /** * Get productivity rating based on margin percentage */ private function getProductivityRating($marginPercent) { global $langs; if ($marginPercent >= 100) { return array( 'class' => 'rating-excellent', 'text' => $langs->trans("Excellent"), 'description' => $langs->trans("RatingExcellentDesc"), ); } elseif ($marginPercent >= 50) { return array( 'class' => 'rating-good', 'text' => $langs->trans("Good"), 'description' => $langs->trans("RatingGoodDesc"), ); } elseif ($marginPercent >= 20) { return array( 'class' => 'rating-average', 'text' => $langs->trans("Average"), 'description' => $langs->trans("RatingAverageDesc"), ); } elseif ($marginPercent >= 0) { return array( 'class' => 'rating-low', 'text' => $langs->trans("Low"), 'description' => $langs->trans("RatingLowDesc"), ); } else { return array( 'class' => 'rating-critical', 'text' => $langs->trans("Critical"), 'description' => $langs->trans("RatingCriticalDesc"), ); } } /** * Method to show the widget */ public function showBox($head = null, $contents = null, $nooutput = 0) { return parent::showBox($this->info_box_head, $this->info_box_contents, $nooutput); } }