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:
parent
00c4792c17
commit
927ed2ec07
5 changed files with 303 additions and 108 deletions
67
README.md
67
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue