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)
|
||||
|
||||
**Version:** 1.2
|
||||
**Version:** 1.3
|
||||
**Compatibility:** Dolibarr 19.0+
|
||||
**Author:** Eduard Wisch - Data IT Solution
|
||||
**License:** GPL v3+
|
||||
|
|
@ -21,23 +21,33 @@ Das Buchhaltungs-Widget Modul erweitert Dolibarr um drei leistungsstarke Dashboa
|
|||
- Aktuelles Quartal hervorgehoben
|
||||
- Farbcodierung: Rot = Zahllast, Gruen = Erstattung
|
||||
- Detailseite mit monatlicher/quartalsweiser Ansicht
|
||||
- Hilfe-Icon mit Erklaerung der Berechnung
|
||||
|
||||
#### 2. Gewinn/Verlust
|
||||
#### 2. Gewinn/Verlust (Rohertrag)
|
||||
- Kumulierter Gewinn/Verlust im Jahresverlauf
|
||||
- Beruecksichtigt nur kundenbezogene Materialkosten
|
||||
- Keine Betriebskosten (Miete, Nebenkosten etc.)
|
||||
- Berechnung ueber Buchungskonten (wenn vorhanden):
|
||||
- Einnahmen: Kontenklasse 8xxx (Erloese)
|
||||
- Materialkosten: Kontenklasse 3xxx (Wareneinsatz)
|
||||
- Keine Betriebskosten (4xxx) - nur Rohertrag
|
||||
- Fallback auf Rechnungsdaten wenn keine Buchungen vorhanden
|
||||
- Schaetzung der Einkommensteuer
|
||||
- Farbige Linie: Gruen = Gewinn, Rot = Verlust
|
||||
- Hilfe-Icon mit Erklaerung der Berechnung
|
||||
|
||||
#### 3. Rentabilitaet
|
||||
- Vergleich: Materialeinkauf vs. Rechnungsstellung
|
||||
#### 3. Rentabilitaet (Echte Rentabilitaet)
|
||||
- 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
|
||||
- Produktivitaetsbewertung mit 5 Stufen:
|
||||
- Ausgezeichnet (>50%)
|
||||
- Gut (30-50%)
|
||||
- Durchschnittlich (15-30%)
|
||||
- Niedrig (0-15%)
|
||||
- Ausgezeichnet (>100%)
|
||||
- Gut (50-100%)
|
||||
- Durchschnittlich (20-50%)
|
||||
- Niedrig (0-20%)
|
||||
- Kritisch (<0%)
|
||||
- Hilfe-Icon mit Erklaerung der Berechnung
|
||||
|
||||
### Zahlungsstatistik (Kundenkarte)
|
||||
|
||||
|
|
@ -95,23 +105,33 @@ The Accounting Widgets module extends Dolibarr with three powerful dashboard wid
|
|||
- Current quarter highlighted
|
||||
- Color coding: Red = to pay, Green = refund
|
||||
- Detail page with monthly/quarterly view
|
||||
- Help icon explaining calculation
|
||||
|
||||
#### 2. Profit/Loss
|
||||
#### 2. Profit/Loss (Gross Margin)
|
||||
- Cumulative profit/loss throughout the year
|
||||
- Only considers customer-related material costs
|
||||
- Excludes operating costs (rent, utilities, etc.)
|
||||
- Calculation via accounting accounts (if available):
|
||||
- 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
|
||||
- Colored line: Green = profit, Red = loss
|
||||
- Help icon explaining calculation
|
||||
|
||||
#### 3. Profitability
|
||||
- Comparison: Material purchases vs. invoiced amounts
|
||||
#### 3. Profitability (Real Profitability)
|
||||
- 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
|
||||
- Productivity rating with 5 levels:
|
||||
- Excellent (>50%)
|
||||
- Good (30-50%)
|
||||
- Average (15-30%)
|
||||
- Low (0-15%)
|
||||
- Excellent (>100%)
|
||||
- Good (50-100%)
|
||||
- Average (20-50%)
|
||||
- Low (0-20%)
|
||||
- Critical (<0%)
|
||||
- Help icon explaining calculation
|
||||
|
||||
### Payment Statistics (Customer Card)
|
||||
|
||||
|
|
@ -157,6 +177,15 @@ The following options can be configured in the admin area:
|
|||
|
||||
## 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
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ class box_gewinn_verlust extends ModeleBoxes
|
|||
|
||||
$langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta"));
|
||||
|
||||
$helpText = $langs->trans("GewinnVerlustHelpText");
|
||||
$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),
|
||||
'subpicto' => 'chart',
|
||||
'subtext' => $langs->trans("ShowDetails"),
|
||||
|
|
@ -91,12 +92,13 @@ class box_gewinn_verlust extends ModeleBoxes
|
|||
'td' => 'colspan="4" class="buchaltung-chart-container"',
|
||||
'text' => '<canvas id="'.$chartId.'" height="140"></canvas>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof Chart !== "undefined") {
|
||||
var ctx = document.getElementById("'.$chartId.'").getContext("2d");
|
||||
(function() {
|
||||
function initGewinnChart() {
|
||||
var ctx = document.getElementById("'.$chartId.'");
|
||||
if (!ctx) return;
|
||||
var profitData = '.json_encode($chartData['currentProfit']).';
|
||||
|
||||
new Chart(ctx, {
|
||||
new Chart(ctx.getContext("2d"), {
|
||||
type: "line",
|
||||
data: {
|
||||
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>',
|
||||
'asis' => 1,
|
||||
);
|
||||
|
|
@ -274,8 +291,10 @@ class box_gewinn_verlust extends ModeleBoxes
|
|||
|
||||
/**
|
||||
* Get income and customer-related expenses by month
|
||||
* IMPORTANT: Only includes costs related to customer projects (materials for customers),
|
||||
* NOT company overhead costs
|
||||
* Uses accounting accounts (Buchungskonten) for precise filtering:
|
||||
* - Income: Account 8xxx (Erlöse)
|
||||
* - Material costs: Account 3xxx (Wareneinsatz/Materialkosten)
|
||||
* Excludes: Operating costs (4xxx), overhead, tools, etc.
|
||||
*/
|
||||
private function getIncomeExpenseByMonth($year)
|
||||
{
|
||||
|
|
@ -286,48 +305,102 @@ class box_gewinn_verlust extends ModeleBoxes
|
|||
'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 IN (1, 2) AND f.entity = ".((int) $conf->entity);
|
||||
$sql .= " AND YEAR(f.datef) = ".((int) $year);
|
||||
$sql .= " GROUP BY MONTH(f.datef)";
|
||||
// Check if accounting module is active and has data
|
||||
$useAccounting = $this->hasAccountingData($year);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$result['income'][$obj->month] = (float) $obj->total;
|
||||
if ($useAccounting) {
|
||||
// Income from accounting: Account 8xxx (Erlöse)
|
||||
$sql = "SELECT MONTH(b.doc_date) as month, ABS(SUM(CASE WHEN b.sens = 'C' THEN b.montant ELSE -b.montant END)) as 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:
|
||||
// - 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 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)";
|
||||
// Material costs from accounting: Account 3xxx (Wareneinsatz)
|
||||
$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."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 '3%'"; // Wareneinsatz/Materialkosten
|
||||
$sql .= " GROUP BY MONTH(b.doc_date)";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$result['customer_expenses'][$obj->month] = (float) $obj->total;
|
||||
$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);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ class box_rentabilitaet extends ModeleBoxes
|
|||
|
||||
$langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta"));
|
||||
|
||||
$helpText = $langs->trans("RentabilitaetHelpText");
|
||||
$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),
|
||||
'subpicto' => 'chart',
|
||||
'subtext' => $langs->trans("ShowDetails"),
|
||||
|
|
@ -86,10 +87,11 @@ class box_rentabilitaet extends ModeleBoxes
|
|||
'td' => 'colspan="4" class="buchaltung-chart-container"',
|
||||
'text' => '<canvas id="'.$chartId.'" height="140"></canvas>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof Chart !== "undefined") {
|
||||
var ctx = document.getElementById("'.$chartId.'").getContext("2d");
|
||||
new Chart(ctx, {
|
||||
(function() {
|
||||
function initRentabilitaetChart() {
|
||||
var ctx = document.getElementById("'.$chartId.'");
|
||||
if (!ctx) return;
|
||||
new Chart(ctx.getContext("2d"), {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: '.json_encode($chartData['labels']).',
|
||||
|
|
@ -104,12 +106,12 @@ class box_rentabilitaet extends ModeleBoxes
|
|||
pointRadius: 4,
|
||||
pointBackgroundColor: '.json_encode($chartData['marginColors']).'
|
||||
}, {
|
||||
label: "'.$langs->trans("MaterialsPurchased").'",
|
||||
label: "'.$langs->trans("AllExpenses").'",
|
||||
data: '.json_encode($chartData['purchased']).',
|
||||
backgroundColor: "rgba(220, 53, 69, 0.6)",
|
||||
yAxisID: "y"
|
||||
}, {
|
||||
label: "'.$langs->trans("MaterialsServicesInvoiced").'",
|
||||
label: "'.$langs->trans("Income").'",
|
||||
data: '.json_encode($chartData['invoiced']).',
|
||||
backgroundColor: "rgba(0, 123, 255, 0.6)",
|
||||
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>',
|
||||
'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.' *');
|
||||
$line++;
|
||||
|
||||
// Materials purchased (only for customers!)
|
||||
// All expenses (materials + operating costs)
|
||||
$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="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"', '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));
|
||||
|
|
@ -238,7 +255,7 @@ class box_rentabilitaet extends ModeleBoxes
|
|||
// Footer note
|
||||
$this->info_box_contents[$line][] = array(
|
||||
'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,
|
||||
);
|
||||
}
|
||||
|
|
@ -285,7 +302,10 @@ class box_rentabilitaet extends ModeleBoxes
|
|||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
|
|
@ -296,47 +316,98 @@ class box_rentabilitaet extends ModeleBoxes
|
|||
'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 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)";
|
||||
// Check if accounting module is active and has data
|
||||
$useAccounting = $this->hasAccountingData($year);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$result['purchased'][$obj->month] = (float) $obj->total;
|
||||
if ($useAccounting) {
|
||||
// Income from accounting: Account 8xxx (Erlöse)
|
||||
$sql = "SELECT MONTH(b.doc_date) as month, ABS(SUM(CASE WHEN b.sens = 'C' THEN b.montant ELSE -b.montant END)) as 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
|
||||
$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 IN (1, 2) AND f.entity = ".((int) $conf->entity);
|
||||
$sql .= " AND YEAR(f.datef) = ".((int) $year);
|
||||
$sql .= " GROUP BY MONTH(f.datef)";
|
||||
// ALL expenses from accounting: Account 3xxx + 4xxx
|
||||
$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."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 '3%' OR b.numero_compte LIKE '4%')"; // Wareneinsatz + Betriebskosten
|
||||
$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;
|
||||
$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);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -50,8 +50,9 @@ class box_ust_uebersicht extends ModeleBoxes
|
|||
|
||||
$langs->loadLangs(array("buchaltungswidget@buchaltungswidget", "bills", "compta"));
|
||||
|
||||
$helpText = $langs->trans("UStUebersichtHelpText");
|
||||
$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),
|
||||
'subpicto' => 'chart',
|
||||
'subtext' => $langs->trans("ShowDetails"),
|
||||
|
|
@ -88,10 +89,11 @@ class box_ust_uebersicht extends ModeleBoxes
|
|||
'td' => 'colspan="6" class="buchaltung-chart-container"',
|
||||
'text' => '<canvas id="'.$chartId.'" height="120"></canvas>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (typeof Chart !== "undefined") {
|
||||
var ctx = document.getElementById("'.$chartId.'").getContext("2d");
|
||||
new Chart(ctx, {
|
||||
(function() {
|
||||
function initUstChart() {
|
||||
var ctx = document.getElementById("'.$chartId.'");
|
||||
if (!ctx) return;
|
||||
new Chart(ctx.getContext("2d"), {
|
||||
type: "bar",
|
||||
data: {
|
||||
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>',
|
||||
'asis' => 1,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ MyWidgetDescription = Meine Widget Beschreibung
|
|||
# USt (VAT) Widget
|
||||
#
|
||||
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
|
||||
VATCollected = Eingenommene USt
|
||||
VATPaid = Gezahlte VSt (Vorsteuer)
|
||||
|
|
@ -62,6 +63,7 @@ VATRefundLegend = Gruen = Erstattung
|
|||
# Gewinn/Verlust Widget
|
||||
#
|
||||
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
|
||||
Income = Einnahmen
|
||||
Expenses = Ausgaben
|
||||
|
|
@ -80,10 +82,13 @@ Note = Hinweis
|
|||
# Rentabilitaet Widget
|
||||
#
|
||||
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
|
||||
MaterialsPurchased = Eingekaufte Materialien
|
||||
MaterialsPurchasedForCustomers = Eingekaufte Materialien (fuer Kunden)
|
||||
MaterialsServicesInvoiced = In Rechnung gestellt (Leistungen & Material)
|
||||
AllExpenses = Alle Ausgaben
|
||||
AllCostsIncluded = Alle Kosten inkl. Betriebsausgaben
|
||||
ProfitMargin = Gewinnmarge
|
||||
GrossProfit = Rohertrag
|
||||
ProfitMarginTrend = Gewinnmarge-Trend
|
||||
|
|
|
|||
Loading…
Reference in a new issue