feat: Präzise Berechnung über Buchungskonten (SKR03/SKR04)

- Gewinn/Verlust: Kontenklasse 8xxx (Erlöse) minus 3xxx (Wareneinsatz)
- Rentabilität: Kontenklasse 8xxx minus 3xxx + 4xxx (alle Kosten inkl. Betriebskosten)
- Automatischer Fallback auf Rechnungsdaten wenn keine Buchungen vorhanden
- Hilfe-Icons mit Tooltips bei allen Widgets
- Dynamisches Chart.js-Laden (Charts funktionieren jetzt auch auf Dashboard)
- README auf Version 1.3 aktualisiert

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-23 08:00:50 +01:00
parent 00c4792c17
commit 927ed2ec07
5 changed files with 303 additions and 108 deletions

View file

@ -1,6 +1,6 @@
# BUCHHALTUNGS-WIDGET / ACCOUNTING WIDGETS FOR [DOLIBARR ERP & CRM](https://www.dolibarr.org) # BUCHHALTUNGS-WIDGET / ACCOUNTING WIDGETS FOR [DOLIBARR ERP & CRM](https://www.dolibarr.org)
**Version:** 1.2 **Version:** 1.3
**Compatibility:** Dolibarr 19.0+ **Compatibility:** Dolibarr 19.0+
**Author:** Eduard Wisch - Data IT Solution **Author:** Eduard Wisch - Data IT Solution
**License:** GPL v3+ **License:** GPL v3+
@ -21,23 +21,33 @@ Das Buchhaltungs-Widget Modul erweitert Dolibarr um drei leistungsstarke Dashboa
- Aktuelles Quartal hervorgehoben - Aktuelles Quartal hervorgehoben
- Farbcodierung: Rot = Zahllast, Gruen = Erstattung - Farbcodierung: Rot = Zahllast, Gruen = Erstattung
- Detailseite mit monatlicher/quartalsweiser Ansicht - Detailseite mit monatlicher/quartalsweiser Ansicht
- Hilfe-Icon mit Erklaerung der Berechnung
#### 2. Gewinn/Verlust #### 2. Gewinn/Verlust (Rohertrag)
- Kumulierter Gewinn/Verlust im Jahresverlauf - Kumulierter Gewinn/Verlust im Jahresverlauf
- Beruecksichtigt nur kundenbezogene Materialkosten - Berechnung ueber Buchungskonten (wenn vorhanden):
- Keine Betriebskosten (Miete, Nebenkosten etc.) - Einnahmen: Kontenklasse 8xxx (Erloese)
- Materialkosten: Kontenklasse 3xxx (Wareneinsatz)
- Keine Betriebskosten (4xxx) - nur Rohertrag
- Fallback auf Rechnungsdaten wenn keine Buchungen vorhanden
- Schaetzung der Einkommensteuer - Schaetzung der Einkommensteuer
- Farbige Linie: Gruen = Gewinn, Rot = Verlust - Farbige Linie: Gruen = Gewinn, Rot = Verlust
- Hilfe-Icon mit Erklaerung der Berechnung
#### 3. Rentabilitaet #### 3. Rentabilitaet (Echte Rentabilitaet)
- Vergleich: Materialeinkauf vs. Rechnungsstellung - Zeigt echte Rentabilitaet inkl. ALLER Kosten
- Berechnung ueber Buchungskonten (wenn vorhanden):
- Einnahmen: Kontenklasse 8xxx (Erloese)
- Alle Ausgaben: Kontenklasse 3xxx + 4xxx (Wareneinsatz + Betriebskosten)
- Fallback auf Rechnungsdaten wenn keine Buchungen vorhanden
- Gewinnmarge in Prozent - Gewinnmarge in Prozent
- Produktivitaetsbewertung mit 5 Stufen: - Produktivitaetsbewertung mit 5 Stufen:
- Ausgezeichnet (>50%) - Ausgezeichnet (>100%)
- Gut (30-50%) - Gut (50-100%)
- Durchschnittlich (15-30%) - Durchschnittlich (20-50%)
- Niedrig (0-15%) - Niedrig (0-20%)
- Kritisch (<0%) - Kritisch (<0%)
- Hilfe-Icon mit Erklaerung der Berechnung
### Zahlungsstatistik (Kundenkarte) ### Zahlungsstatistik (Kundenkarte)
@ -95,23 +105,33 @@ The Accounting Widgets module extends Dolibarr with three powerful dashboard wid
- Current quarter highlighted - Current quarter highlighted
- Color coding: Red = to pay, Green = refund - Color coding: Red = to pay, Green = refund
- Detail page with monthly/quarterly view - Detail page with monthly/quarterly view
- Help icon explaining calculation
#### 2. Profit/Loss #### 2. Profit/Loss (Gross Margin)
- Cumulative profit/loss throughout the year - Cumulative profit/loss throughout the year
- Only considers customer-related material costs - Calculation via accounting accounts (if available):
- Excludes operating costs (rent, utilities, etc.) - Income: Account class 8xxx (Revenue)
- Material costs: Account class 3xxx (Cost of goods)
- No operating costs (4xxx) - gross margin only
- Fallback to invoice data if no bookings exist
- Income tax estimation - Income tax estimation
- Colored line: Green = profit, Red = loss - Colored line: Green = profit, Red = loss
- Help icon explaining calculation
#### 3. Profitability #### 3. Profitability (Real Profitability)
- Comparison: Material purchases vs. invoiced amounts - Shows real profitability including ALL costs
- Calculation via accounting accounts (if available):
- Income: Account class 8xxx (Revenue)
- All expenses: Account class 3xxx + 4xxx (Cost of goods + Operating costs)
- Fallback to invoice data if no bookings exist
- Profit margin percentage - Profit margin percentage
- Productivity rating with 5 levels: - Productivity rating with 5 levels:
- Excellent (>50%) - Excellent (>100%)
- Good (30-50%) - Good (50-100%)
- Average (15-30%) - Average (20-50%)
- Low (0-15%) - Low (0-20%)
- Critical (<0%) - Critical (<0%)
- Help icon explaining calculation
### Payment Statistics (Customer Card) ### Payment Statistics (Customer Card)
@ -157,6 +177,15 @@ The following options can be configured in the admin area:
## Changelog ## Changelog
### Version 1.3
- Neu: Praezise Berechnung ueber Buchungskonten (SKR03/SKR04)
- Gewinn/Verlust: Kontenklasse 8xxx (Erloese) minus 3xxx (Wareneinsatz)
- Rentabilitaet: Kontenklasse 8xxx minus 3xxx + 4xxx (alle Kosten)
- Neu: Automatischer Fallback auf Rechnungsdaten wenn keine Buchungen vorhanden
- Neu: Hilfe-Icons mit Tooltips bei allen Widgets
- Neu: Dynamisches Chart.js-Laden (Charts funktionieren jetzt auch auf Dashboard ohne vorheriges Laden)
- Fix: Charts wurden nicht angezeigt wenn Chart.js nicht geladen war
### Version 1.2 ### Version 1.2
- Fix: VAT widget showed paid VAT (input tax) always as 0 (wrong column name in supplier invoice detail table) - Fix: VAT widget showed paid VAT (input tax) always as 0 (wrong column name in supplier invoice detail table)
- Fix: Cancelled invoices (status 3) were included in all financial calculations - Fix: Cancelled invoices (status 3) were included in all financial calculations

View file

@ -44,8 +44,9 @@ class box_gewinn_verlust extends ModeleBoxes
$langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta")); $langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta"));
$helpText = $langs->trans("GewinnVerlustHelpText");
$this->info_box_head = array( $this->info_box_head = array(
'text' => $langs->trans("GewinnVerlust"), 'text' => $langs->trans("GewinnVerlust").' <span class="classfortooltip" title="'.dol_escape_htmltag($helpText).'" style="cursor:help;">'.img_picto($helpText, 'help', 'class="opacitymedium"').'</span>',
'sublink' => dol_buildpath('/buchaltungswidget/gewinn_detail.php', 1), 'sublink' => dol_buildpath('/buchaltungswidget/gewinn_detail.php', 1),
'subpicto' => 'chart', 'subpicto' => 'chart',
'subtext' => $langs->trans("ShowDetails"), 'subtext' => $langs->trans("ShowDetails"),
@ -91,12 +92,13 @@ class box_gewinn_verlust extends ModeleBoxes
'td' => 'colspan="4" class="buchaltung-chart-container"', 'td' => 'colspan="4" class="buchaltung-chart-container"',
'text' => '<canvas id="'.$chartId.'" height="140"></canvas> 'text' => '<canvas id="'.$chartId.'" height="140"></canvas>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { (function() {
if (typeof Chart !== "undefined") { function initGewinnChart() {
var ctx = document.getElementById("'.$chartId.'").getContext("2d"); var ctx = document.getElementById("'.$chartId.'");
if (!ctx) return;
var profitData = '.json_encode($chartData['currentProfit']).'; var profitData = '.json_encode($chartData['currentProfit']).';
new Chart(ctx, { new Chart(ctx.getContext("2d"), {
type: "line", type: "line",
data: { data: {
labels: '.json_encode($chartData['labels']).', labels: '.json_encode($chartData['labels']).',
@ -151,7 +153,22 @@ class box_gewinn_verlust extends ModeleBoxes
} }
}); });
} }
}); function loadChartAndInit() {
if (typeof Chart !== "undefined") {
initGewinnChart();
} else {
var script = document.createElement("script");
script.src = "'.DOL_URL_ROOT.'/includes/nnnick/chartjs/dist/chart.min.js";
script.onload = initGewinnChart;
document.head.appendChild(script);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadChartAndInit);
} else {
loadChartAndInit();
}
})();
</script>', </script>',
'asis' => 1, 'asis' => 1,
); );
@ -274,8 +291,10 @@ class box_gewinn_verlust extends ModeleBoxes
/** /**
* Get income and customer-related expenses by month * Get income and customer-related expenses by month
* IMPORTANT: Only includes costs related to customer projects (materials for customers), * Uses accounting accounts (Buchungskonten) for precise filtering:
* NOT company overhead costs * - Income: Account 8xxx (Erlöse)
* - Material costs: Account 3xxx (Wareneinsatz/Materialkosten)
* Excludes: Operating costs (4xxx), overhead, tools, etc.
*/ */
private function getIncomeExpenseByMonth($year) private function getIncomeExpenseByMonth($year)
{ {
@ -286,48 +305,102 @@ class box_gewinn_verlust extends ModeleBoxes
'customer_expenses' => array_fill(1, 12, 0), 'customer_expenses' => array_fill(1, 12, 0),
); );
// Income from customer invoices // Check if accounting module is active and has data
$sql = "SELECT MONTH(f.datef) as month, SUM(f.total_ht) as total"; $useAccounting = $this->hasAccountingData($year);
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
$sql .= " WHERE f.fk_statut IN (1, 2) 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 ($useAccounting) {
if ($resql) { // Income from accounting: Account 8xxx (Erlöse)
while ($obj = $this->db->fetch_object($resql)) { $sql = "SELECT MONTH(b.doc_date) as month, ABS(SUM(CASE WHEN b.sens = 'C' THEN b.montant ELSE -b.montant END)) as total";
$result['income'][$obj->month] = (float) $obj->total; $sql .= " FROM ".MAIN_DB_PREFIX."accounting_bookkeeping as b";
$sql .= " WHERE b.entity = ".((int) $conf->entity);
$sql .= " AND YEAR(b.doc_date) = ".((int) $year);
$sql .= " AND b.numero_compte LIKE '8%'"; // Erlöskonten
$sql .= " GROUP BY MONTH(b.doc_date)";
$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);
} }
$this->db->free($resql);
}
// Customer-related expenses only: // Material costs from accounting: Account 3xxx (Wareneinsatz)
// - Products (materials) for customers (product_type = 0) $sql = "SELECT MONTH(b.doc_date) as month, SUM(CASE WHEN b.sens = 'D' THEN b.montant ELSE -b.montant END) as total";
// - Services directly for customers $sql .= " FROM ".MAIN_DB_PREFIX."accounting_bookkeeping as b";
// Exclude: Company overhead, rent, utilities, etc. $sql .= " WHERE b.entity = ".((int) $conf->entity);
$sql = "SELECT MONTH(f.datef) as month, SUM(fd.total_ht) as total"; $sql .= " AND YEAR(b.doc_date) = ".((int) $year);
$sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn as f"; $sql .= " AND b.numero_compte LIKE '3%'"; // Wareneinsatz/Materialkosten
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."facture_fourn_det as fd ON fd.fk_facture_fourn = f.rowid"; $sql .= " GROUP BY MONTH(b.doc_date)";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = fd.fk_product";
$sql .= " WHERE f.fk_statut IN (1, 2) 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); $resql = $this->db->query($sql);
if ($resql) { if ($resql) {
while ($obj = $this->db->fetch_object($resql)) { while ($obj = $this->db->fetch_object($resql)) {
$result['customer_expenses'][$obj->month] = (float) $obj->total; $result['customer_expenses'][$obj->month] = (float) $obj->total;
}
$this->db->free($resql);
}
} else {
// Fallback: Use invoice data if no accounting entries exist
// 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 IN (1, 2) 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);
}
// Material costs from supplier invoices (products only)
$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 IN (1, 2) 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 = $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);
} }
$this->db->free($resql);
} }
return $result; return $result;
} }
/**
* Check if accounting data exists for the given year
*/
private function hasAccountingData($year)
{
global $conf;
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."accounting_bookkeeping";
$sql .= " WHERE entity = ".((int) $conf->entity);
$sql .= " AND YEAR(doc_date) = ".((int) $year);
$sql .= " LIMIT 1";
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
$this->db->free($resql);
return ($obj->cnt > 0);
}
return false;
}
/** /**
* Calculate statistical projection for next year based on trends * Calculate statistical projection for next year based on trends
*/ */

View file

@ -44,8 +44,9 @@ class box_rentabilitaet extends ModeleBoxes
$langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta")); $langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta"));
$helpText = $langs->trans("RentabilitaetHelpText");
$this->info_box_head = array( $this->info_box_head = array(
'text' => $langs->trans("Rentabilitaet"), 'text' => $langs->trans("Rentabilitaet").' <span class="classfortooltip" title="'.dol_escape_htmltag($helpText).'" style="cursor:help;">'.img_picto($helpText, 'help', 'class="opacitymedium"').'</span>',
'sublink' => dol_buildpath('/buchaltungswidget/rentabilitaet_detail.php', 1), 'sublink' => dol_buildpath('/buchaltungswidget/rentabilitaet_detail.php', 1),
'subpicto' => 'chart', 'subpicto' => 'chart',
'subtext' => $langs->trans("ShowDetails"), 'subtext' => $langs->trans("ShowDetails"),
@ -86,10 +87,11 @@ class box_rentabilitaet extends ModeleBoxes
'td' => 'colspan="4" class="buchaltung-chart-container"', 'td' => 'colspan="4" class="buchaltung-chart-container"',
'text' => '<canvas id="'.$chartId.'" height="140"></canvas> 'text' => '<canvas id="'.$chartId.'" height="140"></canvas>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { (function() {
if (typeof Chart !== "undefined") { function initRentabilitaetChart() {
var ctx = document.getElementById("'.$chartId.'").getContext("2d"); var ctx = document.getElementById("'.$chartId.'");
new Chart(ctx, { if (!ctx) return;
new Chart(ctx.getContext("2d"), {
type: "bar", type: "bar",
data: { data: {
labels: '.json_encode($chartData['labels']).', labels: '.json_encode($chartData['labels']).',
@ -104,12 +106,12 @@ class box_rentabilitaet extends ModeleBoxes
pointRadius: 4, pointRadius: 4,
pointBackgroundColor: '.json_encode($chartData['marginColors']).' pointBackgroundColor: '.json_encode($chartData['marginColors']).'
}, { }, {
label: "'.$langs->trans("MaterialsPurchased").'", label: "'.$langs->trans("AllExpenses").'",
data: '.json_encode($chartData['purchased']).', data: '.json_encode($chartData['purchased']).',
backgroundColor: "rgba(220, 53, 69, 0.6)", backgroundColor: "rgba(220, 53, 69, 0.6)",
yAxisID: "y" yAxisID: "y"
}, { }, {
label: "'.$langs->trans("MaterialsServicesInvoiced").'", label: "'.$langs->trans("Income").'",
data: '.json_encode($chartData['invoiced']).', data: '.json_encode($chartData['invoiced']).',
backgroundColor: "rgba(0, 123, 255, 0.6)", backgroundColor: "rgba(0, 123, 255, 0.6)",
yAxisID: "y" yAxisID: "y"
@ -157,7 +159,22 @@ class box_rentabilitaet extends ModeleBoxes
} }
}); });
} }
}); function loadChartAndInit() {
if (typeof Chart !== "undefined") {
initRentabilitaetChart();
} else {
var script = document.createElement("script");
script.src = "'.DOL_URL_ROOT.'/includes/nnnick/chartjs/dist/chart.min.js";
script.onload = initRentabilitaetChart;
document.head.appendChild(script);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadChartAndInit);
} else {
loadChartAndInit();
}
})();
</script>', </script>',
'asis' => 1, 'asis' => 1,
); );
@ -170,12 +187,12 @@ class box_rentabilitaet extends ModeleBoxes
$this->info_box_contents[$line][] = array('td' => 'class="buchaltung-header right buchaltung-future"', 'text' => $nextYear.' *'); $this->info_box_contents[$line][] = array('td' => 'class="buchaltung-header right buchaltung-future"', 'text' => $nextYear.' *');
$line++; $line++;
// Materials purchased (only for customers!) // All expenses (materials + operating costs)
$purchasedLast = array_sum($dataLastYear['purchased']); $purchasedLast = array_sum($dataLastYear['purchased']);
$purchasedCurrent = array_sum(array_slice($dataCurrentYear['purchased'], 0, $currentMonth, true)); $purchasedCurrent = array_sum(array_slice($dataCurrentYear['purchased'], 0, $currentMonth, true));
$purchasedProjection = $projectionNextYear['purchased']; $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="buchaltung-label"', 'text' => $langs->trans("AllExpenses"));
$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 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"', '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)); $this->info_box_contents[$line][] = array('td' => 'class="right buchaltung-future"', 'text' => price($purchasedProjection, 0, $langs, 1, 0, 0, $conf->currency));
@ -238,7 +255,7 @@ class box_rentabilitaet extends ModeleBoxes
// Footer note // Footer note
$this->info_box_contents[$line][] = array( $this->info_box_contents[$line][] = array(
'td' => 'colspan="4" class="buchaltung-footnote"', 'td' => 'colspan="4" class="buchaltung-footnote"',
'text' => '<small>* '.$langs->trans("StatisticalProjection").' | '.$langs->trans("OnlyCustomerMaterials").'</small>', 'text' => '<small>* '.$langs->trans("StatisticalProjection").' | '.$langs->trans("AllCostsIncluded").'</small>',
'asis' => 1, 'asis' => 1,
); );
} }
@ -285,7 +302,10 @@ class box_rentabilitaet extends ModeleBoxes
/** /**
* Get profitability data by month * Get profitability data by month
* IMPORTANT: Only materials purchased FOR CUSTOMERS, not general company expenses * Uses accounting accounts for precise filtering:
* - Income: Account 8xxx (Erlöse)
* - ALL expenses: Account 3xxx (Wareneinsatz) + 4xxx (Betriebskosten)
* This shows REAL profitability including all overhead costs
*/ */
private function getProfitabilityByMonth($year) private function getProfitabilityByMonth($year)
{ {
@ -296,47 +316,98 @@ class box_rentabilitaet extends ModeleBoxes
'invoiced' => array_fill(1, 12, 0), 'invoiced' => array_fill(1, 12, 0),
); );
// Materials purchased FOR CUSTOMERS only (products, not services) // Check if accounting module is active and has data
// This should be materials that are resold or used in customer projects $useAccounting = $this->hasAccountingData($year);
$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 IN (1, 2) 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 ($useAccounting) {
if ($resql) { // Income from accounting: Account 8xxx (Erlöse)
while ($obj = $this->db->fetch_object($resql)) { $sql = "SELECT MONTH(b.doc_date) as month, ABS(SUM(CASE WHEN b.sens = 'C' THEN b.montant ELSE -b.montant END)) as total";
$result['purchased'][$obj->month] = (float) $obj->total; $sql .= " FROM ".MAIN_DB_PREFIX."accounting_bookkeeping as b";
$sql .= " WHERE b.entity = ".((int) $conf->entity);
$sql .= " AND YEAR(b.doc_date) = ".((int) $year);
$sql .= " AND b.numero_compte LIKE '8%'"; // Erlöskonten
$sql .= " GROUP BY MONTH(b.doc_date)";
$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);
} }
$this->db->free($resql);
}
// All materials and services invoiced to customers // ALL expenses from accounting: Account 3xxx + 4xxx
$sql = "SELECT MONTH(f.datef) as month, SUM(fd.total_ht) as total"; $sql = "SELECT MONTH(b.doc_date) as month, SUM(CASE WHEN b.sens = 'D' THEN b.montant ELSE -b.montant END) as total";
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f"; $sql .= " FROM ".MAIN_DB_PREFIX."accounting_bookkeeping as b";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."facturedet as fd ON fd.fk_facture = f.rowid"; $sql .= " WHERE b.entity = ".((int) $conf->entity);
$sql .= " WHERE f.fk_statut IN (1, 2) AND f.entity = ".((int) $conf->entity); $sql .= " AND YEAR(b.doc_date) = ".((int) $year);
$sql .= " AND YEAR(f.datef) = ".((int) $year); $sql .= " AND (b.numero_compte LIKE '3%' OR b.numero_compte LIKE '4%')"; // Wareneinsatz + Betriebskosten
$sql .= " GROUP BY MONTH(f.datef)"; $sql .= " GROUP BY MONTH(b.doc_date)";
$resql = $this->db->query($sql); $resql = $this->db->query($sql);
if ($resql) { if ($resql) {
while ($obj = $this->db->fetch_object($resql)) { while ($obj = $this->db->fetch_object($resql)) {
$result['invoiced'][$obj->month] = (float) $obj->total; $result['purchased'][$obj->month] = (float) $obj->total;
}
$this->db->free($resql);
}
} else {
// Fallback: Use invoice data if no accounting entries exist
// ALL supplier invoices (all expenses)
$sql = "SELECT MONTH(f.datef) as month, SUM(f.total_ht) as total";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_fourn as f";
$sql .= " WHERE f.fk_statut IN (1, 2) 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['purchased'][$obj->month] = (float) $obj->total;
}
$this->db->free($resql);
}
// All customer invoices (income)
$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 IN (1, 2) 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);
} }
$this->db->free($resql);
} }
return $result; return $result;
} }
/**
* Check if accounting data exists for the given year
*/
private function hasAccountingData($year)
{
global $conf;
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."accounting_bookkeeping";
$sql .= " WHERE entity = ".((int) $conf->entity);
$sql .= " AND YEAR(doc_date) = ".((int) $year);
$sql .= " LIMIT 1";
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
$this->db->free($resql);
return ($obj->cnt > 0);
}
return false;
}
/** /**
* Calculate statistical projection for next year * Calculate statistical projection for next year
*/ */

View file

@ -50,8 +50,9 @@ class box_ust_uebersicht extends ModeleBoxes
$langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta")); $langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta"));
$helpText = $langs->trans("UStUebersichtHelpText");
$this->info_box_head = array( $this->info_box_head = array(
'text' => $langs->trans("UStUebersicht"), 'text' => $langs->trans("UStUebersicht").' <span class="classfortooltip" title="'.dol_escape_htmltag($helpText).'" style="cursor:help;">'.img_picto($helpText, 'help', 'class="opacitymedium"').'</span>',
'sublink' => dol_buildpath('/buchaltungswidget/ust_detail.php', 1), 'sublink' => dol_buildpath('/buchaltungswidget/ust_detail.php', 1),
'subpicto' => 'chart', 'subpicto' => 'chart',
'subtext' => $langs->trans("ShowDetails"), 'subtext' => $langs->trans("ShowDetails"),
@ -88,10 +89,11 @@ class box_ust_uebersicht extends ModeleBoxes
'td' => 'colspan="6" class="buchaltung-chart-container"', 'td' => 'colspan="6" class="buchaltung-chart-container"',
'text' => '<canvas id="'.$chartId.'" height="120"></canvas> 'text' => '<canvas id="'.$chartId.'" height="120"></canvas>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { (function() {
if (typeof Chart !== "undefined") { function initUstChart() {
var ctx = document.getElementById("'.$chartId.'").getContext("2d"); var ctx = document.getElementById("'.$chartId.'");
new Chart(ctx, { if (!ctx) return;
new Chart(ctx.getContext("2d"), {
type: "bar", type: "bar",
data: { data: {
labels: ["Q1", "Q2", "Q3", "Q4"], labels: ["Q1", "Q2", "Q3", "Q4"],
@ -127,7 +129,22 @@ class box_ust_uebersicht extends ModeleBoxes
} }
}); });
} }
}); function loadChartAndInit() {
if (typeof Chart !== "undefined") {
initUstChart();
} else {
var script = document.createElement("script");
script.src = "'.DOL_URL_ROOT.'/includes/nnnick/chartjs/dist/chart.min.js";
script.onload = initUstChart;
document.head.appendChild(script);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadChartAndInit);
} else {
loadChartAndInit();
}
})();
</script>', </script>',
'asis' => 1, 'asis' => 1,
); );

View file

@ -45,6 +45,7 @@ MyWidgetDescription = Meine Widget Beschreibung
# USt (VAT) Widget # USt (VAT) Widget
# #
UStUebersicht = Umsatzsteuer-Uebersicht UStUebersicht = Umsatzsteuer-Uebersicht
UStUebersichtHelpText = Zeigt die Umsatzsteuer-Zahllast pro Quartal. Berechnung: Eingenommene USt (Kundenrechnungen) minus gezahlte Vorsteuer (Lieferantenrechnungen). Rot = Zahllast ans Finanzamt, Gruen = Erstattung vom Finanzamt.
VATOverview = Umsatzsteuer-Uebersicht VATOverview = Umsatzsteuer-Uebersicht
VATCollected = Eingenommene USt VATCollected = Eingenommene USt
VATPaid = Gezahlte VSt (Vorsteuer) VATPaid = Gezahlte VSt (Vorsteuer)
@ -62,6 +63,7 @@ VATRefundLegend = Gruen = Erstattung
# Gewinn/Verlust Widget # Gewinn/Verlust Widget
# #
GewinnVerlust = Gewinn / Verlust GewinnVerlust = Gewinn / Verlust
GewinnVerlustHelpText = Zeigt den Rohertrag: Einnahmen (Erloeskonten 8xxx) minus Materialkosten (Wareneinsatz Konten 3xxx). Betriebskosten wie Werkzeuge, Fahrzeug, Buero sind NICHT enthalten. Fuer die echte Rentabilitaet inkl. aller Kosten siehe das Rentabilitaets-Widget.
IncomeExpenseOverview = Einnahmen / Ausgaben IncomeExpenseOverview = Einnahmen / Ausgaben
Income = Einnahmen Income = Einnahmen
Expenses = Ausgaben Expenses = Ausgaben
@ -80,10 +82,13 @@ Note = Hinweis
# Rentabilitaet Widget # Rentabilitaet Widget
# #
Rentabilitaet = Rentabilitaet Rentabilitaet = Rentabilitaet
RentabilitaetHelpText = Zeigt die echte Rentabilitaet inkl. ALLER Kosten: Einnahmen (8xxx) minus Wareneinsatz (3xxx) UND Betriebskosten (4xxx wie Werkzeuge, Fahrzeug, Buero, etc.). Die Gewinnmarge zeigt das Verhaeltnis von Gewinn zu Gesamtkosten in Prozent.
ProfitabilityAnalysis = Rentabilitaetsanalyse ProfitabilityAnalysis = Rentabilitaetsanalyse
MaterialsPurchased = Eingekaufte Materialien MaterialsPurchased = Eingekaufte Materialien
MaterialsPurchasedForCustomers = Eingekaufte Materialien (fuer Kunden) MaterialsPurchasedForCustomers = Eingekaufte Materialien (fuer Kunden)
MaterialsServicesInvoiced = In Rechnung gestellt (Leistungen & Material) MaterialsServicesInvoiced = In Rechnung gestellt (Leistungen & Material)
AllExpenses = Alle Ausgaben
AllCostsIncluded = Alle Kosten inkl. Betriebsausgaben
ProfitMargin = Gewinnmarge ProfitMargin = Gewinnmarge
GrossProfit = Rohertrag GrossProfit = Rohertrag
ProfitMarginTrend = Gewinnmarge-Trend ProfitMarginTrend = Gewinnmarge-Trend