* * 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_gewinn_verlust.php * \ingroup buchaltungswidget * \brief Widget showing Profit/Loss overview - only customer-related costs, no company overhead */ include_once DOL_DOCUMENT_ROOT.'/core/boxes/modules_boxes.php'; /** * Class to manage the Profit/Loss overview widget */ class box_gewinn_verlust extends ModeleBoxes { public $boxcode = "gewinn_verlust"; public $boximg = "accountancy"; public $boxlabel = "GewinnVerlust"; 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("GewinnVerlust"), 'sublink' => dol_buildpath('/buchaltungswidget/gewinn_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 data for all three years $dataCurrentYear = $this->getIncomeExpenseByMonth($currentYear); $dataLastYear = $this->getIncomeExpenseByMonth($lastYear); // Calculate statistical projection for next year $projectionNextYear = $this->calculateProjection($dataCurrentYear, $dataLastYear); // Build the output $this->info_box_contents = array(); $line = 0; // Mini chart area $chartId = 'gewinn_chart_'.uniqid(); $chartData = $this->prepareChartData($dataCurrentYear, $dataLastYear, $currentYear, $lastYear); // Determine line color based on current profit status $lastValue = $chartData['lastCurrentValue']; $lineColor = $lastValue >= 0 ? 'rgba(40, 167, 69, 1)' : 'rgba(220, 53, 69, 1)'; $fillColor = $lastValue >= 0 ? 'rgba(40, 167, 69, 0.2)' : 'rgba(220, 53, 69, 0.2)'; $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++; // Income row $incomeLast = array_sum($dataLastYear['income']); $incomeCurrent = array_sum(array_slice($dataCurrentYear['income'], 0, $currentMonth, true)); $incomeProjection = $projectionNextYear['income']; $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label"', 'text' => $langs->trans("Income")); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-lastyear"', 'text' => price($incomeLast, 0, $langs, 1, 0, 0, $conf->currency)); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-positive"', 'text' => ''.price($incomeCurrent, 0, $langs, 1, 0, 0, $conf->currency).'', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-future"', 'text' => price($incomeProjection, 0, $langs, 1, 0, 0, $conf->currency)); $line++; // Customer-related expenses only $expensesLast = array_sum($dataLastYear['customer_expenses']); $expensesCurrent = array_sum(array_slice($dataCurrentYear['customer_expenses'], 0, $currentMonth, true)); $expensesProjection = $projectionNextYear['expenses']; $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label"', 'text' => $langs->trans("CustomerRelatedCosts")); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-lastyear"', 'text' => price($expensesLast, 0, $langs, 1, 0, 0, $conf->currency)); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-negative"', 'text' => ''.price($expensesCurrent, 0, $langs, 1, 0, 0, $conf->currency).'', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-future"', 'text' => price($expensesProjection, 0, $langs, 1, 0, 0, $conf->currency)); $line++; // Profit/Loss row $profitLast = $incomeLast - $expensesLast; $profitCurrent = $incomeCurrent - $expensesCurrent; $profitProjection = $incomeProjection - $expensesProjection; $colorLast = $profitLast >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $colorCurrent = $profitCurrent >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $colorProjection = $profitProjection >= 0 ? 'buchaltung-positive' : 'buchaltung-negative'; $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label buchaltung-profit-row"', 'text' => ''.$langs->trans("ProfitLoss").'', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-profit-row '.$colorLast.'"', 'text' => ''.price($profitLast, 0, $langs, 1, 0, 0, $conf->currency).'', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-profit-row '.$colorCurrent.'"', 'text' => ''.price($profitCurrent, 0, $langs, 1, 0, 0, $conf->currency).'', 'asis' => 1); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-profit-row buchaltung-future '.$colorProjection.'"', 'text' => ''.price($profitProjection, 0, $langs, 1, 0, 0, $conf->currency).'', 'asis' => 1); $line++; // Estimated income tax if profit if ($profitCurrent > 0) { $estimatedTax = $this->calculateIncomeTax($profitCurrent); $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-label" colspan="2"', 'text' => $langs->trans("EstimatedIncomeTax")); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-negative" colspan="2"', 'text' => '~'.price($estimatedTax, 0, $langs, 1, 0, 0, $conf->currency).'', 'asis' => 1); $line++; } // Footer note $this->info_box_contents[$line][] = array( 'td' => 'colspan="4" class="buchaltung-footnote"', 'text' => '* '.$langs->trans("StatisticalProjection").'', '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'); $currentProfit = array(); $lastProfit = array(); $currentColors = array(); $currentBgColors = array(); $cumulativeCurrent = 0; $cumulativeLast = 0; for ($m = 1; $m <= 12; $m++) { // Last year cumulative $incomeLast = isset($lastData['income'][$m]) ? $lastData['income'][$m] : 0; $expensesLast = isset($lastData['customer_expenses'][$m]) ? $lastData['customer_expenses'][$m] : 0; $cumulativeLast += ($incomeLast - $expensesLast); $lastProfit[] = round($cumulativeLast, 2); // Current year cumulative (only up to current month) if ($m <= $currentMonth) { $incomeCurrent = isset($currentData['income'][$m]) ? $currentData['income'][$m] : 0; $expensesCurrent = isset($currentData['customer_expenses'][$m]) ? $currentData['customer_expenses'][$m] : 0; $cumulativeCurrent += ($incomeCurrent - $expensesCurrent); $currentProfit[] = round($cumulativeCurrent, 2); // Color based on profit/loss if ($cumulativeCurrent >= 0) { $currentColors[] = 'rgba(40, 167, 69, 1)'; // Green for profit $currentBgColors[] = 'rgba(40, 167, 69, 0.15)'; } else { $currentColors[] = 'rgba(220, 53, 69, 1)'; // Red for loss $currentBgColors[] = 'rgba(220, 53, 69, 0.15)'; } } else { $currentProfit[] = null; // No data for future months $currentColors[] = 'rgba(200, 200, 200, 0.5)'; $currentBgColors[] = 'rgba(200, 200, 200, 0.1)'; } } return array( 'labels' => $labels, 'currentProfit' => $currentProfit, 'lastProfit' => $lastProfit, 'currentColors' => $currentColors, 'currentBgColors' => $currentBgColors, 'lastCurrentValue' => $cumulativeCurrent, ); } /** * Get income and customer-related expenses by month * IMPORTANT: Only includes costs related to customer projects (materials for customers), * NOT company overhead costs */ private function getIncomeExpenseByMonth($year) { global $conf; $result = array( 'income' => array_fill(1, 12, 0), 'customer_expenses' => array_fill(1, 12, 0), ); // Income from customer invoices $sql = "SELECT MONTH(f.datef) as month, SUM(f.total_ht) as total"; $sql .= " FROM ".MAIN_DB_PREFIX."facture as f"; $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['income'][$obj->month] = (float) $obj->total; } $this->db->free($resql); } // Customer-related expenses only: // - Products (materials) for customers (product_type = 0) // - Services directly for customers // Exclude: Company overhead, rent, utilities, etc. $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 .= " LEFT 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 include products (materials) - product_type 0 = product, 1 = service // Also include invoice lines with product linked $sql .= " AND (fd.fk_product IS NOT NULL AND fd.fk_product > 0)"; $sql .= " AND (p.fk_product_type = 0 OR fd.product_type = 0)"; // Materials only $sql .= " GROUP BY MONTH(f.datef)"; $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { $result['customer_expenses'][$obj->month] = (float) $obj->total; } $this->db->free($resql); } return $result; } /** * Calculate statistical projection for next year based on trends */ private function calculateProjection($currentData, $lastData) { $currentMonth = date('n'); // Calculate average monthly values from current year (up to now) $avgIncome = array_sum(array_slice($currentData['income'], 0, $currentMonth, true)) / max(1, $currentMonth); $avgExpenses = array_sum(array_slice($currentData['customer_expenses'], 0, $currentMonth, true)) / max(1, $currentMonth); // Calculate year-over-year growth rate $lastYearIncome = array_sum($lastData['income']); $currentYearIncome = array_sum(array_slice($currentData['income'], 0, $currentMonth, true)); $projectedCurrentYear = ($currentMonth > 0) ? ($currentYearIncome / $currentMonth) * 12 : 0; $growthRate = ($lastYearIncome > 0) ? (($projectedCurrentYear - $lastYearIncome) / $lastYearIncome) : 0; $growthRate = max(-0.5, min(0.5, $growthRate)); // Cap growth rate at +/- 50% return array( 'income' => round($projectedCurrentYear * (1 + $growthRate * 0.5), 2), // Conservative projection 'expenses' => round(($avgExpenses * 12) * (1 + $growthRate * 0.3), 2), ); } /** * Calculate estimated income tax (simplified German model) */ private function calculateIncomeTax($profit) { // Simplified German income tax calculation // Grundfreibetrag 2024: ~11,604 EUR $taxableIncome = max(0, $profit - 11604); if ($taxableIncome <= 0) { return 0; } elseif ($taxableIncome <= 17005) { // Zone 2: ~14-24% return $taxableIncome * 0.18; } elseif ($taxableIncome <= 66760) { // Zone 3: ~24-42% return $taxableIncome * 0.30; } else { // Zone 4: 42%+ return $taxableIncome * 0.42; } } /** * 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); } }