373 lines
12 KiB
PHP
373 lines
12 KiB
PHP
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* 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/gewinn_detail.php
|
|
* \ingroup buchaltungswidget
|
|
* \brief Detail page for Profit/Loss with full charts and year selection
|
|
*/
|
|
|
|
// Load Dolibarr environment
|
|
$res = 0;
|
|
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
|
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
|
}
|
|
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
|
$tmp2 = realpath(__FILE__);
|
|
$i = strlen($tmp) - 1;
|
|
$j = strlen($tmp2) - 1;
|
|
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
|
$i--;
|
|
$j--;
|
|
}
|
|
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
|
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
|
}
|
|
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
|
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../main.inc.php")) {
|
|
$res = @include "../main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../../main.inc.php")) {
|
|
$res = @include "../../main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../../../main.inc.php")) {
|
|
$res = @include "../../../main.inc.php";
|
|
}
|
|
if (!$res) {
|
|
die("Include of main fails");
|
|
}
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
|
|
|
// Load translation files
|
|
$langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta"));
|
|
|
|
// Access control
|
|
if (!$user->hasRight('facture', 'lire') && !$user->hasRight('fournisseur', 'facture', 'lire')) {
|
|
accessforbidden();
|
|
}
|
|
|
|
// Get parameters
|
|
$selectedYear = GETPOST('year', 'int');
|
|
if (empty($selectedYear)) {
|
|
$selectedYear = date('Y');
|
|
}
|
|
|
|
$currentYear = date('Y');
|
|
$years = range($currentYear - 5, $currentYear + 1);
|
|
|
|
/*
|
|
* View
|
|
*/
|
|
|
|
$title = $langs->trans("GewinnVerlust");
|
|
llxHeader('', $title, '', '', 0, 0, array('/includes/nnnick/chartjs/dist/Chart.min.js'), array('/buchaltungswidget/css/buchaltungswidget.css'));
|
|
|
|
print load_fiche_titre($title, '', 'accountancy');
|
|
|
|
// Year selector
|
|
print '<div class="fichecenter">';
|
|
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
|
|
print '<div class="buchaltung-filter-bar">';
|
|
print '<label for="year">'.$langs->trans("Year").':</label>';
|
|
print '<select name="year" id="year" class="flat" onchange="this.form.submit()">';
|
|
foreach ($years as $y) {
|
|
$selected = ($y == $selectedYear) ? ' selected' : '';
|
|
print '<option value="'.$y.'"'.$selected.'>'.$y.'</option>';
|
|
}
|
|
print '</select>';
|
|
print '</div>';
|
|
print '</form>';
|
|
print '</div>';
|
|
|
|
// Get data
|
|
$currentData = getIncomeExpenseByMonth($db, $selectedYear);
|
|
$lastYearData = getIncomeExpenseByMonth($db, $selectedYear - 1);
|
|
|
|
// Main chart - Cumulative profit/loss
|
|
print '<div class="fichecenter">';
|
|
print '<div class="fichethirdleft">';
|
|
print '<div class="buchaltung-detail-chart" style="height: 300px;">';
|
|
print '<h4>'.$langs->trans("CumulativeProfitLoss").'</h4>';
|
|
print '<div style="height: 240px; position: relative;">';
|
|
print '<canvas id="cumulativeChart"></canvas>';
|
|
print '</div>';
|
|
print '</div>';
|
|
print '</div>';
|
|
|
|
print '<div class="fichethirdright">';
|
|
print '<div class="buchaltung-detail-chart" style="height: 300px;">';
|
|
print '<h4>'.$langs->trans("MonthlyIncomeExpenses").'</h4>';
|
|
print '<div style="height: 240px; position: relative;">';
|
|
print '<canvas id="monthlyChart"></canvas>';
|
|
print '</div>';
|
|
print '</div>';
|
|
print '</div>';
|
|
print '</div>';
|
|
|
|
print '<div class="clearboth" style="margin-bottom: 20px;"></div>';
|
|
|
|
// Data table
|
|
print '<div class="div-table-responsive">';
|
|
print '<table class="noborder centpercent">';
|
|
|
|
// Header
|
|
print '<tr class="liste_titre">';
|
|
print '<th>'.$langs->trans("Month").'</th>';
|
|
print '<th class="right">'.$langs->trans("Income").'</th>';
|
|
print '<th class="right">'.$langs->trans("CustomerRelatedCosts").'</th>';
|
|
print '<th class="right">'.$langs->trans("ProfitLoss").'</th>';
|
|
print '<th class="right">'.$langs->trans("CumulativeProfit").'</th>';
|
|
print '<th class="right">'.($selectedYear - 1).' '.$langs->trans("CumulativeProfit").'</th>';
|
|
print '</tr>';
|
|
|
|
$months = array(1 => 'Januar', 2 => 'Februar', 3 => 'Maerz', 4 => 'April', 5 => 'Mai', 6 => 'Juni',
|
|
7 => 'Juli', 8 => 'August', 9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Dezember');
|
|
$monthsShort = array(1 => 'Jan', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mai', 6 => 'Jun',
|
|
7 => 'Jul', 8 => 'Aug', 9 => 'Sep', 10 => 'Okt', 11 => 'Nov', 12 => 'Dez');
|
|
|
|
$totalIncome = 0;
|
|
$totalExpenses = 0;
|
|
$cumulative = 0;
|
|
$cumulativeLast = 0;
|
|
|
|
$labels = array();
|
|
$incomeData = array();
|
|
$expensesData = array();
|
|
$cumulativeData = array();
|
|
$cumulativeLastData = array();
|
|
$monthlyProfit = array();
|
|
|
|
foreach ($months as $m => $name) {
|
|
$income = isset($currentData['income'][$m]) ? $currentData['income'][$m] : 0;
|
|
$expenses = isset($currentData['customer_expenses'][$m]) ? $currentData['customer_expenses'][$m] : 0;
|
|
$profit = $income - $expenses;
|
|
$cumulative += $profit;
|
|
|
|
$incomeLast = isset($lastYearData['income'][$m]) ? $lastYearData['income'][$m] : 0;
|
|
$expensesLast = isset($lastYearData['customer_expenses'][$m]) ? $lastYearData['customer_expenses'][$m] : 0;
|
|
$cumulativeLast += ($incomeLast - $expensesLast);
|
|
|
|
$totalIncome += $income;
|
|
$totalExpenses += $expenses;
|
|
|
|
$colorClass = $profit >= 0 ? 'buchaltung-positive' : 'buchaltung-negative';
|
|
$colorCumulative = $cumulative >= 0 ? 'buchaltung-positive' : 'buchaltung-negative';
|
|
$colorCumulativeLast = $cumulativeLast >= 0 ? 'buchaltung-positive' : 'buchaltung-negative';
|
|
|
|
print '<tr class="oddeven">';
|
|
print '<td>'.$name.'</td>';
|
|
print '<td class="right buchaltung-positive">'.price($income, 0, $langs, 1, 2, 2, $conf->currency).'</td>';
|
|
print '<td class="right buchaltung-negative">'.price($expenses, 0, $langs, 1, 2, 2, $conf->currency).'</td>';
|
|
print '<td class="right '.$colorClass.'"><strong>'.price($profit, 0, $langs, 1, 2, 2, $conf->currency).'</strong></td>';
|
|
print '<td class="right '.$colorCumulative.'"><strong>'.price($cumulative, 0, $langs, 1, 2, 2, $conf->currency).'</strong></td>';
|
|
print '<td class="right buchaltung-lastyear '.$colorCumulativeLast.'">'.price($cumulativeLast, 0, $langs, 1, 2, 2, $conf->currency).'</td>';
|
|
print '</tr>';
|
|
|
|
// Chart data
|
|
$labels[] = $monthsShort[$m];
|
|
$incomeData[] = round($income, 2);
|
|
$expensesData[] = round($expenses, 2);
|
|
$cumulativeData[] = round($cumulative, 2);
|
|
$cumulativeLastData[] = round($cumulativeLast, 2);
|
|
$monthlyProfit[] = round($profit, 2);
|
|
}
|
|
|
|
// Total row
|
|
$totalProfit = $totalIncome - $totalExpenses;
|
|
$colorClass = $totalProfit >= 0 ? 'buchaltung-positive' : 'buchaltung-negative';
|
|
|
|
print '<tr class="liste_total">';
|
|
print '<td><strong>'.$langs->trans("Total").'</strong></td>';
|
|
print '<td class="right buchaltung-positive"><strong>'.price($totalIncome, 0, $langs, 1, 2, 2, $conf->currency).'</strong></td>';
|
|
print '<td class="right buchaltung-negative"><strong>'.price($totalExpenses, 0, $langs, 1, 2, 2, $conf->currency).'</strong></td>';
|
|
print '<td class="right '.$colorClass.'"><strong>'.price($totalProfit, 0, $langs, 1, 2, 2, $conf->currency).'</strong></td>';
|
|
print '<td colspan="2"></td>';
|
|
print '</tr>';
|
|
|
|
print '</table>';
|
|
print '</div>';
|
|
|
|
// Estimated income tax
|
|
if ($totalProfit > 0) {
|
|
$estimatedTax = calculateIncomeTax($totalProfit);
|
|
print '<div class="buchaltung-tax-box">';
|
|
print '<strong>'.$langs->trans("EstimatedIncomeTax").':</strong> ';
|
|
print '<span class="buchaltung-negative">~'.price($estimatedTax, 0, $langs, 1, 2, 2, $conf->currency).'</span>';
|
|
print ' ('.$langs->trans("NetAfterTax").': <span class="buchaltung-positive">'.price($totalProfit - $estimatedTax, 0, $langs, 1, 2, 2, $conf->currency).'</span>)';
|
|
print '</div>';
|
|
}
|
|
|
|
// Info box
|
|
print '<div class="buchaltung-info-box">';
|
|
print '<strong>'.$langs->trans("Note").':</strong> '.$langs->trans("CustomerRelatedCostsNote");
|
|
print '</div>';
|
|
|
|
// Charts JavaScript
|
|
print '<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
// Cumulative chart
|
|
var ctx1 = document.getElementById("cumulativeChart").getContext("2d");
|
|
new Chart(ctx1, {
|
|
type: "line",
|
|
data: {
|
|
labels: '.json_encode($labels).',
|
|
datasets: [{
|
|
label: "'.$selectedYear.' '.$langs->trans("CumulativeProfit").'",
|
|
data: '.json_encode($cumulativeData).',
|
|
borderColor: "rgba(0, 123, 255, 1)",
|
|
backgroundColor: "rgba(0, 123, 255, 0.1)",
|
|
fill: true,
|
|
tension: 0.3,
|
|
borderWidth: 3
|
|
}, {
|
|
label: "'.($selectedYear-1).' '.$langs->trans("CumulativeProfit").'",
|
|
data: '.json_encode($cumulativeLastData).',
|
|
borderColor: "rgba(108, 117, 125, 0.7)",
|
|
backgroundColor: "transparent",
|
|
borderDash: [5, 5],
|
|
fill: false,
|
|
tension: 0.3,
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: true, position: "bottom" },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ": " + new Intl.NumberFormat("de-DE", {style: "currency", currency: "EUR"}).format(context.raw);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: function(context) {
|
|
return context.tick.value === 0 ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0.1)";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Monthly chart
|
|
var ctx2 = document.getElementById("monthlyChart").getContext("2d");
|
|
new Chart(ctx2, {
|
|
type: "bar",
|
|
data: {
|
|
labels: '.json_encode($labels).',
|
|
datasets: [{
|
|
label: "'.$langs->trans("Income").'",
|
|
data: '.json_encode($incomeData).',
|
|
backgroundColor: "rgba(40, 167, 69, 0.6)"
|
|
}, {
|
|
label: "'.$langs->trans("CustomerRelatedCosts").'",
|
|
data: '.json_encode($expensesData).',
|
|
backgroundColor: "rgba(220, 53, 69, 0.6)"
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: true, position: "bottom" },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ": " + new Intl.NumberFormat("de-DE", {style: "currency", currency: "EUR"}).format(context.raw);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true }
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>';
|
|
|
|
llxFooter();
|
|
$db->close();
|
|
|
|
/**
|
|
* Get income and customer-related expenses by month
|
|
*/
|
|
function getIncomeExpenseByMonth($db, $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 = $db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $db->fetch_object($resql)) {
|
|
$result['income'][$obj->month] = (float) $obj->total;
|
|
}
|
|
$db->free($resql);
|
|
}
|
|
|
|
// Customer-related expenses only (materials for customers)
|
|
$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);
|
|
$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)";
|
|
$sql .= " GROUP BY MONTH(f.datef)";
|
|
|
|
$resql = $db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $db->fetch_object($resql)) {
|
|
$result['customer_expenses'][$obj->month] = (float) $obj->total;
|
|
}
|
|
$db->free($resql);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Calculate estimated income tax
|
|
*/
|
|
function calculateIncomeTax($profit)
|
|
{
|
|
$taxableIncome = max(0, $profit - 11604);
|
|
|
|
if ($taxableIncome <= 0) {
|
|
return 0;
|
|
} elseif ($taxableIncome <= 17005) {
|
|
return $taxableIncome * 0.18;
|
|
} elseif ($taxableIncome <= 66760) {
|
|
return $taxableIncome * 0.30;
|
|
} else {
|
|
return $taxableIncome * 0.42;
|
|
}
|
|
}
|