Version 1.3.0: Netto STZ Spalte in Auftragsliste
- Neue Spalte "Netto STZ" zeigt Netto-Wert aller freigegebenen Stundenzettel - Berechnung bei Freigabe/Wiedereröffnung von einzelnen oder allen Stundenzetteln - Arbeitsstunden verwenden Preis der gewählten Leistungsposition (nicht mehr Kunden-Standard) - Unterstützt kundenspezifische Preise für Produkte und Leistungen - Extrafeld stundenzettel_netto wird bei Modulaktivierung erstellt - Debug-Script debug_netto.php für Fehleranalyse - Deutsche Übersetzungen für Meldungen ergänzt - Formular-Verbesserung: Enter-Taste und Save-Button bei Produktmengen Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
192cdad8e0
commit
41973f0231
7 changed files with 385 additions and 4 deletions
13
README.md
13
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Stundenzettel Modul für Dolibarr
|
# Stundenzettel Modul für Dolibarr
|
||||||
|
|
||||||
**Version:** 1.2.0
|
**Version:** 1.3.0
|
||||||
**Autor:** Data IT Solution
|
**Autor:** Data IT Solution
|
||||||
**Kompatibilität:** Dolibarr 16.0+
|
**Kompatibilität:** Dolibarr 16.0+
|
||||||
**Lizenz:** GPL v3
|
**Lizenz:** GPL v3
|
||||||
|
|
@ -76,6 +76,7 @@ Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung
|
||||||
|------|---------|--------------|
|
|------|---------|--------------|
|
||||||
| `auftragsbeschreibung` | Auftrag | Zusätzliche Beschreibung für den Auftrag |
|
| `auftragsbeschreibung` | Auftrag | Zusätzliche Beschreibung für den Auftrag |
|
||||||
| `stundenzettel_status` | Auftrag | Status der Stundenzettel (0=Offen, 1=Freigegeben, 2=Abgerechnet) |
|
| `stundenzettel_status` | Auftrag | Status der Stundenzettel (0=Offen, 1=Freigegeben, 2=Abgerechnet) |
|
||||||
|
| `stundenzettel_netto` | Auftrag | Berechneter Netto-Wert aller freigegebenen Stundenzettel |
|
||||||
| `stundenzettel_default_service` | Kunde | Standard-Dienstleistung für Stundenzettel |
|
| `stundenzettel_default_service` | Kunde | Standard-Dienstleistung für Stundenzettel |
|
||||||
|
|
||||||
## Berechtigungen
|
## Berechtigungen
|
||||||
|
|
@ -99,6 +100,16 @@ Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### Version 1.3.0
|
||||||
|
- **Netto STZ Spalte in Auftragsliste**: Neue Spalte zeigt den Netto-Wert aller freigegebenen Stundenzettel eines Auftrags
|
||||||
|
- Automatische Berechnung bei Freigabe/Wiedereröffnung von Stundenzetteln
|
||||||
|
- Berücksichtigt Produkte (mit Auftrags- oder Katalogpreisen)
|
||||||
|
- Berücksichtigt Arbeitsstunden (mit der gewählten Leistungsposition pro Zeile)
|
||||||
|
- Unterstützt kundenspezifische Preise
|
||||||
|
- **Verbesserte Preisberechnung**: Jede Arbeitszeit-Zeile verwendet den Preis ihrer eigenen Leistungsposition (nicht mehr Standard-Leistung des Kunden)
|
||||||
|
- **Extrafeld `stundenzettel_netto`**: Wird automatisch bei Modulaktivierung erstellt
|
||||||
|
- **Debug-Script**: `debug_netto.php` für Fehleranalyse der Netto-Berechnung
|
||||||
|
|
||||||
### Version 1.2.0
|
### Version 1.2.0
|
||||||
- **Leistungsposition pro Arbeitszeit**: Jede Arbeitszeit kann einer eigenen Leistungsposition (Dienstleistung) zugeordnet werden
|
- **Leistungsposition pro Arbeitszeit**: Jede Arbeitszeit kann einer eigenen Leistungsposition (Dienstleistung) zugeordnet werden
|
||||||
- **Mobile-optimierte Ansicht**: Responsive CSS für Touch-Geräte (Smartphones/Tablets)
|
- **Mobile-optimierte Ansicht**: Responsive CSS für Touch-Geräte (Smartphones/Tablets)
|
||||||
|
|
|
||||||
12
card.php
12
card.php
|
|
@ -187,6 +187,8 @@ if ($action == 'confirm_validate' && $confirm == 'yes' && $permissiontovalidate)
|
||||||
$result = $object->validate($user);
|
$result = $object->validate($user);
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
setEventMessages($langs->trans('StundenzettelValidated'), null, 'mesgs');
|
setEventMessages($langs->trans('StundenzettelValidated'), null, 'mesgs');
|
||||||
|
// Netto-Wert aller Stundenzettel des Auftrags neu berechnen
|
||||||
|
updateOrderNettoSTZ($db, $object->fk_commande);
|
||||||
} else {
|
} else {
|
||||||
setEventMessages($object->error, $object->errors, 'errors');
|
setEventMessages($object->error, $object->errors, 'errors');
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +199,8 @@ if ($action == 'confirm_setdraft' && $confirm == 'yes' && $permissiontoadd) {
|
||||||
$result = $object->setDraft($user);
|
$result = $object->setDraft($user);
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
||||||
|
// Netto-Wert neu berechnen (da dieser Stundenzettel nicht mehr freigegeben ist)
|
||||||
|
updateOrderNettoSTZ($db, $object->fk_commande);
|
||||||
} else {
|
} else {
|
||||||
setEventMessages($object->error, $object->errors, 'errors');
|
setEventMessages($object->error, $object->errors, 'errors');
|
||||||
}
|
}
|
||||||
|
|
@ -208,6 +212,10 @@ if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) {
|
||||||
$result = $object->delete($user);
|
$result = $object->delete($user);
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
setEventMessages($langs->trans('StundenzettelDeleted'), null, 'mesgs');
|
setEventMessages($langs->trans('StundenzettelDeleted'), null, 'mesgs');
|
||||||
|
// Netto-Wert neu berechnen
|
||||||
|
if ($fk_commande > 0) {
|
||||||
|
updateOrderNettoSTZ($db, $fk_commande);
|
||||||
|
}
|
||||||
// Weiterleitung zur Stundenzettel-Liste des Auftrags
|
// Weiterleitung zur Stundenzettel-Liste des Auftrags
|
||||||
if ($fk_commande > 0) {
|
if ($fk_commande > 0) {
|
||||||
header('Location: '.dol_buildpath('/stundenzettel/stundenzettel_commande.php?id='.$fk_commande.'&tab=stundenzettel&noredirect=1', 1));
|
header('Location: '.dol_buildpath('/stundenzettel/stundenzettel_commande.php?id='.$fk_commande.'&tab=stundenzettel&noredirect=1', 1));
|
||||||
|
|
@ -1233,7 +1241,7 @@ elseif ($object->id > 0) {
|
||||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||||
print '<input type="hidden" name="action" value="update_qty">';
|
print '<input type="hidden" name="action" value="update_qty">';
|
||||||
print '<input type="hidden" name="line_id" value="'.$prod->rowid.'">';
|
print '<input type="hidden" name="line_id" value="'.$prod->rowid.'">';
|
||||||
print '<input type="number" name="qty_done" id="qty_'.$prod->rowid.'" value="'.$qtyDoneFormatted.'" class="flat" style="width:70px; text-align:center;" min="0" step="1">';
|
print '<input type="number" name="qty_done" id="qty_'.$prod->rowid.'" value="'.$qtyDoneFormatted.'" class="flat" style="width:70px; text-align:center;" min="0" step="1" onkeypress="if(event.keyCode==13){this.form.submit();return false;}">';
|
||||||
print '</form>';
|
print '</form>';
|
||||||
} else {
|
} else {
|
||||||
print formatQty($prod->qty_done);
|
print formatQty($prod->qty_done);
|
||||||
|
|
@ -1259,7 +1267,7 @@ elseif ($object->id > 0) {
|
||||||
|
|
||||||
// Save Button
|
// Save Button
|
||||||
print '<td class="center" style="width:40px;">';
|
print '<td class="center" style="width:40px;">';
|
||||||
print '<a class="reposition" href="javascript:document.getElementById(\'form_qty_'.$prod->rowid.'\').submit();" title="'.$langs->trans("Save").'">';
|
print '<a class="reposition" href="#" onclick="document.getElementById(\'form_qty_'.$prod->rowid.'\').submit(); return false;" title="'.$langs->trans("Save").'">';
|
||||||
print '<span class="fas fa-save" style="color: #007bff;"></span>';
|
print '<span class="fas fa-save" style="color: #007bff;"></span>';
|
||||||
print '</a>';
|
print '</a>';
|
||||||
print '</td>';
|
print '</td>';
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ class modStundenzettel extends DolibarrModules
|
||||||
$this->descriptionlong = "Verwaltet Stundenzettel für Kundenaufträge. Ermöglicht die Dokumentation von Arbeitszeiten, verbrauchten Materialien und Notizen. Integration mit SubtotalTitle für Produktgruppen-Unterstützung.";
|
$this->descriptionlong = "Verwaltet Stundenzettel für Kundenaufträge. Ermöglicht die Dokumentation von Arbeitszeiten, verbrauchten Materialien und Notizen. Integration mit SubtotalTitle für Produktgruppen-Unterstützung.";
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
$this->version = '1.2.0';
|
$this->version = '1.3.0';
|
||||||
|
|
||||||
// Autor
|
// Autor
|
||||||
$this->editor_name = 'Data IT Solution';
|
$this->editor_name = 'Data IT Solution';
|
||||||
|
|
@ -323,6 +323,9 @@ class modStundenzettel extends DolibarrModules
|
||||||
// Stundenpreis-Felder hinzufügen (Update 1.2.0)
|
// Stundenpreis-Felder hinzufügen (Update 1.2.0)
|
||||||
$this->addHourlyRateFields();
|
$this->addHourlyRateFields();
|
||||||
|
|
||||||
|
// Extrafeld "Netto Stundenzettel" für Aufträge anlegen
|
||||||
|
$this->createExtraFieldNettoSTZ();
|
||||||
|
|
||||||
$sql = array();
|
$sql = array();
|
||||||
|
|
||||||
return $this->_init($sql, $options);
|
return $this->_init($sql, $options);
|
||||||
|
|
@ -520,6 +523,60 @@ class modStundenzettel extends DolibarrModules
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt das Extrafeld "Netto Stundenzettel" für Aufträge (commande)
|
||||||
|
* Zeigt den berechneten Netto-Wert aller Stundenzettel eines Auftrags
|
||||||
|
*
|
||||||
|
* @return int 1 if created or exists, -1 if error
|
||||||
|
*/
|
||||||
|
private function createExtraFieldNettoSTZ()
|
||||||
|
{
|
||||||
|
global $langs;
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
|
||||||
|
|
||||||
|
$extrafields = new ExtraFields($this->db);
|
||||||
|
|
||||||
|
// Prüfen ob Extrafeld bereits existiert
|
||||||
|
$extrafields->fetch_name_optionals_label('commande');
|
||||||
|
|
||||||
|
if (!isset($extrafields->attributes['commande']['label']['stundenzettel_netto'])) {
|
||||||
|
// Extrafeld anlegen: Double für Netto-Betrag
|
||||||
|
$result = $extrafields->addExtraField(
|
||||||
|
'stundenzettel_netto', // attrname - Feldname
|
||||||
|
'Netto STZ', // label - Anzeigename (kurz für Spalte)
|
||||||
|
'price', // type - Feldtyp (price = Geldbetrag)
|
||||||
|
110, // pos - Position (nach Auftragsbeschreibung)
|
||||||
|
'', // size - Größe
|
||||||
|
'commande', // elementtype - Objekttyp
|
||||||
|
0, // unique
|
||||||
|
0, // required
|
||||||
|
'', // default_value
|
||||||
|
array('options' => array()), // param
|
||||||
|
0, // alwayseditable (nicht editierbar - wird berechnet)
|
||||||
|
'', // perms
|
||||||
|
1, // list - In Liste anzeigen
|
||||||
|
'', // ishidden
|
||||||
|
0, // computed
|
||||||
|
'', // entity
|
||||||
|
'', // langfile
|
||||||
|
'$conf->stundenzettel->enabled', // enabled - nur wenn Modul aktiv
|
||||||
|
0, // totalizable
|
||||||
|
0, // printable
|
||||||
|
array('css' => '', 'cssview' => '', 'csslist' => 'right nowraponall') // moreparams - rechts ausrichten
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result < 0) {
|
||||||
|
dol_syslog("modStundenzettel::createExtraFieldNettoSTZ Error creating extrafield: ".$extrafields->error, LOG_ERR);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dol_syslog("modStundenzettel::createExtraFieldNettoSTZ Extrafield 'stundenzettel_netto' created successfully", LOG_DEBUG);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Funktion beim Deaktivieren des Moduls
|
* Funktion beim Deaktivieren des Moduls
|
||||||
*
|
*
|
||||||
|
|
|
||||||
147
debug_netto.php
Normal file
147
debug_netto.php
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Debug-Script für Netto STZ Berechnung
|
||||||
|
* Aufruf: debug_netto.php?order_id=21
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load Dolibarr environment
|
||||||
|
$res = 0;
|
||||||
|
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
|
||||||
|
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||||
|
if (!$res) die("Include of main fails");
|
||||||
|
|
||||||
|
dol_include_once('/stundenzettel/lib/stundenzettel.lib.php');
|
||||||
|
|
||||||
|
$order_id = GETPOST('order_id', 'int');
|
||||||
|
if (empty($order_id)) {
|
||||||
|
die("Bitte order_id angeben: debug_netto.php?order_id=XXX");
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "<h2>Debug Netto STZ für Auftrag #".$order_id."</h2>";
|
||||||
|
|
||||||
|
// 1. Auftrag und Kunde prüfen
|
||||||
|
$sqlOrder = "SELECT c.rowid, c.ref, c.fk_soc, s.nom as customer_name FROM ".MAIN_DB_PREFIX."commande c";
|
||||||
|
$sqlOrder .= " LEFT JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = c.fk_soc";
|
||||||
|
$sqlOrder .= " WHERE c.rowid = ".((int)$order_id);
|
||||||
|
$resqlOrder = $db->query($sqlOrder);
|
||||||
|
$order = $db->fetch_object($resqlOrder);
|
||||||
|
|
||||||
|
echo "<h3>Auftrag</h3>";
|
||||||
|
echo "<pre>Ref: ".$order->ref."\nKunde: ".$order->customer_name." (ID: ".$order->fk_soc.")</pre>";
|
||||||
|
|
||||||
|
// 2. Standard-Leistung des Kunden
|
||||||
|
$sqlService = "SELECT stundenzettel_default_service FROM ".MAIN_DB_PREFIX."societe_extrafields WHERE fk_object = ".((int)$order->fk_soc);
|
||||||
|
$resqlService = $db->query($sqlService);
|
||||||
|
$serviceObj = $db->fetch_object($resqlService);
|
||||||
|
$defaultServiceId = $serviceObj ? (int)$serviceObj->stundenzettel_default_service : 0;
|
||||||
|
|
||||||
|
echo "<h3>Standard-Leistung beim Kunden</h3>";
|
||||||
|
if ($defaultServiceId > 0) {
|
||||||
|
$sqlProd = "SELECT rowid, ref, label, price FROM ".MAIN_DB_PREFIX."product WHERE rowid = ".((int)$defaultServiceId);
|
||||||
|
$resqlProd = $db->query($sqlProd);
|
||||||
|
$prod = $db->fetch_object($resqlProd);
|
||||||
|
echo "<pre>Service ID: ".$defaultServiceId."\nRef: ".$prod->ref."\nLabel: ".$prod->label."\nPreis: ".price($prod->price)." EUR</pre>";
|
||||||
|
|
||||||
|
// Kundenspezifischer Preis?
|
||||||
|
$priceInfo = getCustomerPrice($db, $defaultServiceId, $order->fk_soc);
|
||||||
|
echo "<pre>Effektiver Preis (mit Kundenpreis): ".price($priceInfo['price'])." EUR";
|
||||||
|
echo " (".($priceInfo['is_customer_price'] ? "Kundenpreis" : "Standardpreis").")</pre>";
|
||||||
|
} else {
|
||||||
|
echo "<pre style='color:red'>KEINE Standard-Leistung beim Kunden hinterlegt!</pre>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Stundenzettel des Auftrags
|
||||||
|
echo "<h3>Stundenzettel des Auftrags</h3>";
|
||||||
|
$sqlStz = "SELECT s.rowid, s.ref, s.status, s.hourly_rate, s.hourly_rate_is_custom,";
|
||||||
|
$sqlStz .= " (SELECT SUM(duration) FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = s.rowid) as total_minutes";
|
||||||
|
$sqlStz .= " FROM ".MAIN_DB_PREFIX."stundenzettel s";
|
||||||
|
$sqlStz .= " WHERE s.fk_commande = ".((int)$order_id);
|
||||||
|
$resqlStz = $db->query($sqlStz);
|
||||||
|
|
||||||
|
echo "<table border='1' cellpadding='5'>";
|
||||||
|
echo "<tr><th>ID</th><th>Ref</th><th>Status</th><th>Leistungen</th></tr>";
|
||||||
|
|
||||||
|
$totalNetto = 0;
|
||||||
|
while ($stz = $db->fetch_object($resqlStz)) {
|
||||||
|
$statusText = ($stz->status == 0) ? '<span style="color:orange">Entwurf</span>' : '<span style="color:green">Freigegeben</span>';
|
||||||
|
|
||||||
|
// Leistungen dieses Stundenzettels laden
|
||||||
|
$sqlLeist = "SELECT l.duration, l.fk_product, l.description, p.ref as product_ref, p.label as product_label, p.price as product_price";
|
||||||
|
$sqlLeist .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l";
|
||||||
|
$sqlLeist .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product";
|
||||||
|
$sqlLeist .= " WHERE l.fk_stundenzettel = ".((int)$stz->rowid);
|
||||||
|
$resqlLeist = $db->query($sqlLeist);
|
||||||
|
|
||||||
|
$leistungenHtml = "<table border='1' style='font-size:12px'>";
|
||||||
|
$leistungenHtml .= "<tr><th>Dauer</th><th>Leistung</th><th>Preis</th><th>Berechnung</th></tr>";
|
||||||
|
|
||||||
|
$stzSubtotal = 0;
|
||||||
|
while ($leist = $db->fetch_object($resqlLeist)) {
|
||||||
|
$hours = $leist->duration / 60;
|
||||||
|
$hourlyRate = 0;
|
||||||
|
$rateSource = "<span style='color:red'>KEINER!</span>";
|
||||||
|
|
||||||
|
if ($leist->fk_product > 0) {
|
||||||
|
$priceInfo = getCustomerPrice($db, $leist->fk_product, $order->fk_soc);
|
||||||
|
$hourlyRate = $priceInfo['price'];
|
||||||
|
$rateSource = $leist->product_ref." - ".$leist->product_label;
|
||||||
|
if ($priceInfo['is_customer_price']) {
|
||||||
|
$rateSource .= " <span style='color:green'>(Kundenpreis)</span>";
|
||||||
|
}
|
||||||
|
} elseif ($stz->hourly_rate > 0) {
|
||||||
|
$hourlyRate = (float)$stz->hourly_rate;
|
||||||
|
$rateSource = "Stundenzettel-Rate";
|
||||||
|
} elseif ($defaultServiceId > 0) {
|
||||||
|
$priceInfo = getCustomerPrice($db, $defaultServiceId, $order->fk_soc);
|
||||||
|
$hourlyRate = $priceInfo['price'];
|
||||||
|
$rateSource = "Kunden-Standard";
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtotal = ($stz->status >= 1) ? ($hourlyRate * $hours) : 0;
|
||||||
|
if ($stz->status >= 1) {
|
||||||
|
$stzSubtotal += $subtotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
$calcText = ($stz->status >= 1)
|
||||||
|
? price($hourlyRate)." x ".$hours."h = <b>".price($subtotal)."</b>"
|
||||||
|
: "<i>Nicht freigegeben</i>";
|
||||||
|
|
||||||
|
$leistungenHtml .= "<tr>";
|
||||||
|
$leistungenHtml .= "<td>".$hours."h</td>";
|
||||||
|
$leistungenHtml .= "<td>".$rateSource."</td>";
|
||||||
|
$leistungenHtml .= "<td>".price($hourlyRate)." EUR</td>";
|
||||||
|
$leistungenHtml .= "<td>".$calcText."</td>";
|
||||||
|
$leistungenHtml .= "</tr>";
|
||||||
|
}
|
||||||
|
$leistungenHtml .= "<tr><td colspan='3'><b>Summe:</b></td><td><b>".price($stzSubtotal)." EUR</b></td></tr>";
|
||||||
|
$leistungenHtml .= "</table>";
|
||||||
|
|
||||||
|
$totalNetto += $stzSubtotal;
|
||||||
|
|
||||||
|
echo "<tr>";
|
||||||
|
echo "<td>".$stz->rowid."</td>";
|
||||||
|
echo "<td>".$stz->ref."</td>";
|
||||||
|
echo "<td>".$statusText."</td>";
|
||||||
|
echo "<td>".$leistungenHtml."</td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
echo "</table>";
|
||||||
|
|
||||||
|
echo "<h3>Ergebnis</h3>";
|
||||||
|
echo "<pre style='font-size:18px; color:blue'>Berechneter Netto STZ: <b>".price($totalNetto)." EUR</b></pre>";
|
||||||
|
|
||||||
|
// 4. Aktueller Wert in der Datenbank
|
||||||
|
$sqlCurrent = "SELECT stundenzettel_netto FROM ".MAIN_DB_PREFIX."commande_extrafields WHERE fk_object = ".((int)$order_id);
|
||||||
|
$resqlCurrent = $db->query($sqlCurrent);
|
||||||
|
$current = $db->fetch_object($resqlCurrent);
|
||||||
|
$currentValue = $current ? $current->stundenzettel_netto : "NICHT GESETZT";
|
||||||
|
|
||||||
|
echo "<pre>Aktueller Wert in DB: ".($currentValue !== "NICHT GESETZT" ? price($currentValue)." EUR" : $currentValue)."</pre>";
|
||||||
|
|
||||||
|
echo "<hr><p><a href='debug_netto.php?order_id=".$order_id."&update=1'>Jetzt neu berechnen und speichern</a></p>";
|
||||||
|
|
||||||
|
if (GETPOST('update', 'int')) {
|
||||||
|
$result = updateOrderNettoSTZ($db, $order_id);
|
||||||
|
echo "<p style='color:green; font-weight:bold'>Berechnung durchgeführt! Ergebnis: ".price($result)." EUR</p>";
|
||||||
|
echo "<p><a href='debug_netto.php?order_id=".$order_id."'>Seite neu laden</a></p>";
|
||||||
|
}
|
||||||
|
|
@ -104,7 +104,13 @@ MarkAsOpen = Wieder öffnen
|
||||||
ConfirmValidate = Stundenzettel wirklich freigeben?
|
ConfirmValidate = Stundenzettel wirklich freigeben?
|
||||||
ConfirmDelete = Stundenzettel wirklich löschen?
|
ConfirmDelete = Stundenzettel wirklich löschen?
|
||||||
ConfirmDeleteLeistung = Diese Leistung wirklich löschen?
|
ConfirmDeleteLeistung = Diese Leistung wirklich löschen?
|
||||||
|
RecordSaved = Eintrag gespeichert
|
||||||
|
RecordModified = Eintrag aktualisiert
|
||||||
RecordDeleted = Eintrag gelöscht
|
RecordDeleted = Eintrag gelöscht
|
||||||
|
SetupSaved = Einstellungen gespeichert
|
||||||
|
ProductsTransferred = Produkte übernommen
|
||||||
|
NoProductsSelected = Keine Produkte ausgewählt
|
||||||
|
ErrorCreatingStundenzettel = Fehler beim Erstellen des Stundenzettels
|
||||||
ConfirmTransfer = Alle Produkte in Rechnung übertragen?
|
ConfirmTransfer = Alle Produkte in Rechnung übertragen?
|
||||||
AllProductsDocumented = Alle Produkte dokumentiert
|
AllProductsDocumented = Alle Produkte dokumentiert
|
||||||
NotAllProductsDocumented = Nicht alle Produkte dokumentiert
|
NotAllProductsDocumented = Nicht alle Produkte dokumentiert
|
||||||
|
|
@ -249,3 +255,7 @@ SelectInvoiceHoursMode = Wie sollen die Arbeitsstunden übernommen werden?
|
||||||
perStundenzettel = pro Stundenzettel
|
perStundenzettel = pro Stundenzettel
|
||||||
Entries = Einträge
|
Entries = Einträge
|
||||||
incl = inkl.
|
incl = inkl.
|
||||||
|
|
||||||
|
# Extrafields Aufträge
|
||||||
|
NettoSTZ = Netto STZ
|
||||||
|
NettoSTZHelp = Netto-Wert aller freigegebenen Stundenzettel (Produkte + Arbeitsstunden)
|
||||||
|
|
|
||||||
|
|
@ -266,3 +266,143 @@ function getEffectiveHourlyRate($db, $stundenzettel, $defaultServiceId) {
|
||||||
'source' => 'none'
|
'source' => 'none'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet den Netto-Wert aller Stundenzettel eines Auftrags
|
||||||
|
* und aktualisiert das Extrafield stundenzettel_netto
|
||||||
|
*
|
||||||
|
* @param DoliDB $db Datenbankverbindung
|
||||||
|
* @param int $fk_commande Auftrags-ID
|
||||||
|
* @return float Der berechnete Netto-Wert
|
||||||
|
*/
|
||||||
|
function updateOrderNettoSTZ($db, $fk_commande) {
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
if (empty($fk_commande)) {
|
||||||
|
dol_syslog("updateOrderNettoSTZ: fk_commande is empty", LOG_WARNING);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalNetto = 0;
|
||||||
|
|
||||||
|
// Hole Kunden-ID und Standard-Leistung ZUERST (vor der Stundenzettel-Schleife)
|
||||||
|
$defaultServiceId = 0;
|
||||||
|
$socid = 0;
|
||||||
|
|
||||||
|
// Hole Kunden-ID vom Auftrag
|
||||||
|
$sqlOrder = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."commande WHERE rowid = ".((int)$fk_commande);
|
||||||
|
$resqlOrder = $db->query($sqlOrder);
|
||||||
|
if ($resqlOrder && ($objOrder = $db->fetch_object($resqlOrder))) {
|
||||||
|
$socid = (int)$objOrder->fk_soc;
|
||||||
|
|
||||||
|
// Hole Standard-Leistung vom Kunden (Extrafield)
|
||||||
|
$sqlService = "SELECT stundenzettel_default_service FROM ".MAIN_DB_PREFIX."societe_extrafields";
|
||||||
|
$sqlService .= " WHERE fk_object = ".((int)$socid);
|
||||||
|
$resqlService = $db->query($sqlService);
|
||||||
|
if ($resqlService && ($objService = $db->fetch_object($resqlService))) {
|
||||||
|
$defaultServiceId = (int)$objService->stundenzettel_default_service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dol_syslog("updateOrderNettoSTZ: commande=".$fk_commande.", socid=".$socid.", defaultServiceId=".$defaultServiceId, LOG_DEBUG);
|
||||||
|
|
||||||
|
// 1. Alle freigegebenen Stundenzettel des Auftrags laden (status >= 1 = validiert)
|
||||||
|
$sqlStz = "SELECT s.rowid, s.fk_soc, s.hourly_rate, s.hourly_rate_is_custom";
|
||||||
|
$sqlStz .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s";
|
||||||
|
$sqlStz .= " WHERE s.fk_commande = ".((int)$fk_commande);
|
||||||
|
$sqlStz .= " AND s.status >= 1"; // Nur validierte/freigegebene Stundenzettel
|
||||||
|
|
||||||
|
$resqlStz = $db->query($sqlStz);
|
||||||
|
if (!$resqlStz) {
|
||||||
|
dol_syslog("updateOrderNettoSTZ: SQL error: ".$db->lasterror(), LOG_ERR);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$numStz = $db->num_rows($resqlStz);
|
||||||
|
dol_syslog("updateOrderNettoSTZ: Found ".$numStz." validated Stundenzettel", LOG_DEBUG);
|
||||||
|
|
||||||
|
while ($stz = $db->fetch_object($resqlStz)) {
|
||||||
|
// 2. Produkte dieses Stundenzettels summieren
|
||||||
|
$sqlProd = "SELECT sp.fk_product, sp.fk_commandedet, sp.qty_done, sp.origin,";
|
||||||
|
$sqlProd .= " cd.subprice as order_price, cd.tva_tx";
|
||||||
|
$sqlProd .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp";
|
||||||
|
$sqlProd .= " LEFT JOIN ".MAIN_DB_PREFIX."commandedet as cd ON cd.rowid = sp.fk_commandedet";
|
||||||
|
$sqlProd .= " WHERE sp.fk_stundenzettel = ".((int)$stz->rowid);
|
||||||
|
$sqlProd .= " AND sp.origin IN ('order', 'added', 'extra')"; // Nicht 'omitted' (entfällt)
|
||||||
|
|
||||||
|
$resqlProd = $db->query($sqlProd);
|
||||||
|
if ($resqlProd) {
|
||||||
|
while ($prod = $db->fetch_object($resqlProd)) {
|
||||||
|
$price = 0;
|
||||||
|
$qty = (float)$prod->qty_done;
|
||||||
|
|
||||||
|
if ($prod->fk_commandedet > 0 && $prod->order_price > 0) {
|
||||||
|
// Preis aus Auftragszeile
|
||||||
|
$price = (float)$prod->order_price;
|
||||||
|
} elseif ($prod->fk_product > 0) {
|
||||||
|
// Kundenspezifischer oder Standard-Preis
|
||||||
|
$priceInfo = getCustomerPrice($db, $prod->fk_product, $socid);
|
||||||
|
$price = $priceInfo['price'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalNetto += $price * $qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Leistungen (Arbeitsstunden) - JEDE Zeile einzeln mit ihrer gewählten Leistungsposition
|
||||||
|
$sqlHours = "SELECT l.duration, l.fk_product FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l";
|
||||||
|
$sqlHours .= " WHERE l.fk_stundenzettel = ".((int)$stz->rowid);
|
||||||
|
|
||||||
|
$resqlHours = $db->query($sqlHours);
|
||||||
|
if ($resqlHours) {
|
||||||
|
while ($leistung = $db->fetch_object($resqlHours)) {
|
||||||
|
$minutes = (float)$leistung->duration;
|
||||||
|
$hoursWorked = $minutes / 60;
|
||||||
|
|
||||||
|
if ($hoursWorked > 0) {
|
||||||
|
$hourlyRate = 0;
|
||||||
|
|
||||||
|
// Priorität: 1. Leistungsposition der Zeile, 2. Stundenzettel-Rate, 3. Kunden-Standard
|
||||||
|
if ($leistung->fk_product > 0) {
|
||||||
|
// Preis der gewählten Leistungsposition (mit Kundenpreis falls vorhanden)
|
||||||
|
$priceInfo = getCustomerPrice($db, $leistung->fk_product, $socid);
|
||||||
|
$hourlyRate = $priceInfo['price'];
|
||||||
|
dol_syslog("updateOrderNettoSTZ: Leistung fk_product=".$leistung->fk_product." price=".$hourlyRate, LOG_DEBUG);
|
||||||
|
} elseif ($stz->hourly_rate > 0) {
|
||||||
|
// Fallback: Manueller Preis im Stundenzettel
|
||||||
|
$hourlyRate = (float)$stz->hourly_rate;
|
||||||
|
dol_syslog("updateOrderNettoSTZ: Using STZ hourly_rate=".$hourlyRate, LOG_DEBUG);
|
||||||
|
} elseif ($defaultServiceId > 0) {
|
||||||
|
// Fallback: Standard-Leistung des Kunden
|
||||||
|
$priceInfo = getCustomerPrice($db, $defaultServiceId, $socid);
|
||||||
|
$hourlyRate = $priceInfo['price'];
|
||||||
|
dol_syslog("updateOrderNettoSTZ: Using defaultService price=".$hourlyRate, LOG_DEBUG);
|
||||||
|
} else {
|
||||||
|
dol_syslog("updateOrderNettoSTZ: No price for leistung! fk_product=".$leistung->fk_product, LOG_WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtotal = $hourlyRate * $hoursWorked;
|
||||||
|
$totalNetto += $subtotal;
|
||||||
|
dol_syslog("updateOrderNettoSTZ: subtotal=".$subtotal." (rate=".$hourlyRate." x hours=".$hoursWorked.")", LOG_DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dol_syslog("updateOrderNettoSTZ: TOTAL=".$totalNetto, LOG_DEBUG);
|
||||||
|
|
||||||
|
// 4. Extrafield aktualisieren
|
||||||
|
$sqlUpdate = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields";
|
||||||
|
$sqlUpdate .= " SET stundenzettel_netto = ".((float)$totalNetto);
|
||||||
|
$sqlUpdate .= " WHERE fk_object = ".((int)$fk_commande);
|
||||||
|
|
||||||
|
$resqlUpdate = $db->query($sqlUpdate);
|
||||||
|
if (!$resqlUpdate || $db->affected_rows($resqlUpdate) == 0) {
|
||||||
|
// Zeile existiert noch nicht - INSERT
|
||||||
|
$sqlInsert = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_netto)";
|
||||||
|
$sqlInsert .= " VALUES (".((int)$fk_commande).", ".((float)$totalNetto).")";
|
||||||
|
$db->query($sqlInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $totalNetto;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,10 @@ if ($doRelease) {
|
||||||
$sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 1)";
|
$sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 1)";
|
||||||
$db->query($sql2);
|
$db->query($sql2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Netto-Wert aller Stundenzettel berechnen und speichern
|
||||||
|
updateOrderNettoSTZ($db, $order->id);
|
||||||
|
|
||||||
setEventMessages($langs->trans('StundenzettelReleased'), null, 'mesgs');
|
setEventMessages($langs->trans('StundenzettelReleased'), null, 'mesgs');
|
||||||
} else {
|
} else {
|
||||||
setEventMessages($db->lasterror(), null, 'errors');
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
|
@ -322,6 +326,10 @@ if ($action == 'reopen_stundenzettel' && $canReopen) {
|
||||||
$sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 0)";
|
$sql2 = "INSERT INTO ".MAIN_DB_PREFIX."commande_extrafields (fk_object, stundenzettel_status) VALUES (".((int)$order->id).", 0)";
|
||||||
$db->query($sql2);
|
$db->query($sql2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Netto-Wert neu berechnen (wird 0 wenn keine freigegebenen Stundenzettel mehr)
|
||||||
|
updateOrderNettoSTZ($db, $order->id);
|
||||||
|
|
||||||
setEventMessages($langs->trans('StundenzettelReopened'), null, 'mesgs');
|
setEventMessages($langs->trans('StundenzettelReopened'), null, 'mesgs');
|
||||||
} else {
|
} else {
|
||||||
setEventMessages($db->lasterror(), null, 'errors');
|
setEventMessages($db->lasterror(), null, 'errors');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue