Version 2.1.0: Rechnungsübernahme erweitert

- Neuer Button "Ohne Produktgruppen" für flache Übernahme ohne Sections/Zwischensummen
- Mehraufwand-Produktgruppe über Modul-Einstellung konfigurierbar
- Standard-Bankkonto wird automatisch aus Einstellung gesetzt
- "Ihr Zeichen" (ref_client) vom Auftrag übernommen
- Extrafelder Angebotsnummer/Auftragsnummer vom Auftrag kopiert
- Verwaiste Zwischensummen werden nach Rechnungserstellung bereinigt
- Zwei neue Modul-Einstellungen: Bankkonto und Mehraufwand-Sektion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-02 18:39:37 +01:00
parent 8ea1180041
commit dabcdbde13
5 changed files with 325 additions and 68 deletions

View file

@ -1,6 +1,6 @@
# Stundenzettel Modul für Dolibarr
**Version:** 1.8.0
**Version:** 2.1.0
**Autor:** Data IT Solution
**Kompatibilität:** Dolibarr 16.0+
**Lizenz:** GPL v3
@ -56,6 +56,8 @@ Die Moduleinstellungen finden Sie unter **Einstellungen > Module > Stundenzettel
| **Standard-Filter** | Welcher Filter in der Produktliste standardmäßig angezeigt wird |
| **Standard-Datum** | Aktuelles Datum oder Datum des letzten offenen Stundenzettels |
| **Stunden-Übernahme** | Gesamtstunden auf einer Zeile oder pro Tag eine Zeile |
| **Standard-Bankkonto** | Welches Bankkonto bei der Rechnungsübernahme automatisch gesetzt wird |
| **Mehraufwand als Produktgruppe** | Ob der Mehraufwand bei Übernahme mit Produktgruppen als eigene Sektion übernommen wird |
### Stundenübernahme-Modus (pro Auftrag)
@ -117,6 +119,20 @@ Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung
## Changelog
### Version 2.1.0
- **Rechnungsübernahme ohne Produktgruppen**: Neuer Button "Ohne Produktgruppen" überträgt Produkte und Leistungen als flache Liste ohne Section-Titel und Zwischensummen
- **Mehraufwand-Produktgruppe konfigurierbar**: Neue Modul-Einstellung ob Mehraufwand bei Übernahme mit Produktgruppen als eigene Sektion übernommen wird
- **Standard-Bankkonto**: Bankkonto wird bei der Rechnungsübernahme automatisch aus Modul-Einstellung gesetzt
- **Ihr Zeichen (ref_client)**: Wird automatisch vom Auftrag in die Rechnung übernommen
- **Extrafelder-Übernahme**: Angebotsnummer und Auftragsnummer werden automatisch vom Auftrag in die Rechnung kopiert
- **Verwaiste Zwischensummen bereinigen**: Nach Rechnungserstellung werden verwaiste Section-Titel und Zwischensummen aus facturedet und facture_lines_manager entfernt
### Version 2.0.0
- **PWA Mobile App**: Installierbare Progressive Web App für Stundenzettel-Verwaltung unterwegs
- **4-Panel-Navigation**: Alle STZ, Stundenzettel, Produktliste und Lieferauflistung per Swipe
- **Merkzettel auf Produktliste**: Merkzettel-Box über der Produktliste mit Abhaken und Hinzufügen
- **Mehraufwand-Transfer**: MA-Produkte aus Auftrag in Stundenzettel übernehmen
### Version 1.8.0
- **Rücknahmen in Rechnungsübernahme**: Zurückgenommene Produkte (origin='returned') werden nicht mehr in die Rechnung übertragen
- Mehraufwand: Rücknahmen werden von der Rechnungsmenge abgezogen (Matching über fk_product oder description)

View file

@ -83,6 +83,28 @@ if ($action == 'setDEFAULT_SECTIONS') {
exit;
}
if ($action == 'setINVOICE_BANK_ACCOUNT') {
$value = GETPOST('bank_account_value', 'int');
if (dolibarr_set_const($db, 'STUNDENZETTEL_INVOICE_BANK_ACCOUNT', $value, 'chaine', 0, '', $conf->entity) > 0) {
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
} else {
setEventMessages($langs->trans("Error"), null, 'errors');
}
header("Location: ".$_SERVER["PHP_SELF"]);
exit;
}
if ($action == 'setINVOICE_MEHRAUFWAND_SECTION') {
$value = GETPOST('mehraufwand_section_value', 'alpha');
if (dolibarr_set_const($db, 'STUNDENZETTEL_INVOICE_MEHRAUFWAND_SECTION', $value, 'chaine', 0, '', $conf->entity) > 0) {
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
} else {
setEventMessages($langs->trans("Error"), null, 'errors');
}
header("Location: ".$_SERVER["PHP_SELF"]);
exit;
}
/*
* View
*/
@ -212,6 +234,62 @@ print '</tr>';
print '</table>';
// Rechnungsübernahme-Einstellungen
print '<br>';
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th class="titlefield">'.$langs->trans("InvoiceTransferSettings").'</th>';
print '<th style="width: 300px;">'.$langs->trans("Value").'</th>';
print '<th class="right" style="width: 100px;"></th>';
print '</tr>';
// Bankkonto für Rechnungen
$currentBankAccount = getDolGlobalString('STUNDENZETTEL_INVOICE_BANK_ACCOUNT', '0');
$sqlBankAccounts = "SELECT rowid, ref, label FROM ".MAIN_DB_PREFIX."bank_account WHERE clos = 0 ORDER BY label";
$resqlBank = $db->query($sqlBankAccounts);
print '<tr class="oddeven">';
print '<td>'.$langs->trans("InvoiceBankAccount").'<br><small class="opacitymedium">'.$langs->trans("InvoiceBankAccountDesc").'</small></td>';
print '<td>';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="setINVOICE_BANK_ACCOUNT">';
print '<select name="bank_account_value" class="flat minwidth200">';
print '<option value="0">'.$langs->trans("None").'</option>';
if ($resqlBank) {
while ($objBank = $db->fetch_object($resqlBank)) {
$selected = ($currentBankAccount == $objBank->rowid) ? ' selected' : '';
print '<option value="'.$objBank->rowid.'"'.$selected.'>'.dol_escape_htmltag($objBank->label.' ('.$objBank->ref.')').'</option>';
}
}
print '</select>';
print '</td>';
print '<td class="right">';
print '<input type="submit" class="button small" value="'.$langs->trans("Modify").'">';
print '</form>';
print '</td>';
print '</tr>';
// Mehraufwand als Produktgruppe
$currentMehraufwandSection = getDolGlobalString('STUNDENZETTEL_INVOICE_MEHRAUFWAND_SECTION', '1');
print '<tr class="oddeven">';
print '<td>'.$langs->trans("InvoiceMehraufwandSection").'<br><small class="opacitymedium">'.$langs->trans("InvoiceMehraufwandSectionDesc").'</small></td>';
print '<td>';
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="setINVOICE_MEHRAUFWAND_SECTION">';
print '<select name="mehraufwand_section_value" class="flat minwidth200">';
print '<option value="1"'.($currentMehraufwandSection == '1' ? ' selected' : '').'>'.$langs->trans("Yes").'</option>';
print '<option value="0"'.($currentMehraufwandSection == '0' ? ' selected' : '').'>'.$langs->trans("No").'</option>';
print '</select>';
print '</td>';
print '<td class="right">';
print '<input type="submit" class="button small" value="'.$langs->trans("Modify").'">';
print '</form>';
print '</td>';
print '</tr>';
print '</table>';
// PWA Mobile App Bereich
print '<br>';
print '<table class="noborder centpercent">';

View file

@ -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.";
// Version
$this->version = '1.8.0';
$this->version = '2.1.0';
// Autor
$this->editor_name = 'Data IT Solution';

View file

@ -283,6 +283,19 @@ incl = inkl.
NettoSTZ = Netto STZ
NettoSTZHelp = Netto-Wert aller freigegebenen Stundenzettel (Produkte + Arbeitsstunden)
# Rechnungsübernahme Einstellungen
InvoiceTransferSettings = Rechnungsübernahme
InvoiceBankAccount = Standard-Bankkonto
InvoiceBankAccountDesc = Welches Bankkonto soll bei der Rechnungsübernahme automatisch gesetzt werden?
InvoiceMehraufwandSection = Mehraufwand als Produktgruppe
InvoiceMehraufwandSectionDesc = Soll der Mehraufwand bei der Übernahme mit Produktgruppen als eigene Sektion übernommen werden?
TransferWithSections = Mit Produktgruppen
TransferWithoutSections = Ohne Produktgruppen
ConfirmTransferInvoiceChoice = Wie sollen die Produkte in die Rechnung übertragen werden?
ConfirmTransferInvoiceWithSections = Die Produkte werden mit Produktgruppen (Sektionen und Zwischensummen) in die Rechnung übertragen.
ConfirmTransferInvoiceWithoutSections = Die Produkte werden ohne Produktgruppen als flache Liste in die Rechnung übertragen.
None = Keins
# PWA Mobile App
PWAMobileApp = PWA Mobile App
PWALink = Stundenzettel PWA öffnen

View file

@ -359,10 +359,13 @@ if ($action == 'reset_stundenzettel' && $canReset) {
exit;
}
// In Rechnung übernehmen
if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes' && $user->hasRight('facture', 'creer')) {
// In Rechnung übernehmen (mit oder ohne Produktgruppen)
if (in_array($action, array('confirm_transfer_invoice', 'confirm_transfer_invoice_flat')) && $user->hasRight('facture', 'creer')) {
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
// Steuerung: Mit oder ohne Produktgruppen
$withSections = ($action == 'confirm_transfer_invoice');
$db->begin();
$error = 0;
@ -377,9 +380,36 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
$facture->note_private = $langs->trans('CreatedFromStundenzettel').' - '.$order->ref;
$facture->linked_objects['commande'] = $order->id;
// "Ihr Zeichen" vom Auftrag übernehmen
if (!empty($order->ref_client)) {
$facture->ref_customer = $order->ref_client;
}
// Bankkonto aus Modul-Einstellung
$bankAccount = getDolGlobalString('STUNDENZETTEL_INVOICE_BANK_ACCOUNT', 0);
if ($bankAccount > 0) {
$facture->fk_account = (int)$bankAccount;
}
$facture_id = $facture->create($user);
if ($facture_id > 0) {
// Extrafelder vom Auftrag übernehmen (Angebotsnummer, Auftragsnummer)
$order->fetch_optionals();
$facture->fetch_optionals();
$extrafieldsUpdated = false;
if (!empty($order->array_options['options_angebotsnummer'])) {
$facture->array_options['options_angebotsnummer'] = $order->array_options['options_angebotsnummer'];
$extrafieldsUpdated = true;
}
if (!empty($order->array_options['options_auftragsnummer'])) {
$facture->array_options['options_auftragsnummer'] = $order->array_options['options_auftragsnummer'];
$extrafieldsUpdated = true;
}
if ($extrafieldsUpdated) {
$facture->insertExtraFields();
}
// Einkaufspreise aller Produkte vorladen
// Priorität: 1. cost_price (manuell gesetzt), 2. bester Lieferanten-Stückpreis, 3. PMP
$buyPrices = array(); // fk_product => buy_price
@ -562,31 +592,35 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
// Nur Sections mit tatsächlich verwendeten Produkten hinzufügen
if ($sectionHasInvoicedProducts) {
// Section-Titel hinzufügen (special_code = 100)
$result = $facture->addline(
$section->title,
0, // subprice
0, // qty
0, // tva_tx
0, 0, // localtax
0, // fk_product
0, // remise_percent
'', '', // dates
0, 0, '', 'HT', 0,
9, // product_type (9 = Title)
$rang++,
100, // special_code = 100 für Section
0, '', 0, 0
);
$currentSectionDetId = 0;
if ($result > 0) {
$invoiceManagerLines[] = array(
'type' => 'section',
'fk_facturedet' => $result,
'title' => $section->title,
'parent' => null
// Section-Titel hinzufügen (nur bei Übernahme mit Produktgruppen)
if ($withSections) {
$result = $facture->addline(
$section->title,
0, // subprice
0, // qty
0, // tva_tx
0, 0, // localtax
0, // fk_product
0, // remise_percent
'', '', // dates
0, 0, '', 'HT', 0,
9, // product_type (9 = Title)
$rang++,
100, // special_code = 100 für Section
0, '', 0, 0
);
$currentSectionDetId = $result;
if ($result > 0) {
$invoiceManagerLines[] = array(
'type' => 'section',
'fk_facturedet' => $result,
'title' => $section->title,
'parent' => null
);
$currentSectionDetId = $result;
}
}
// Produkte der Section hinzufügen
@ -627,12 +661,14 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
);
if ($result > 0) {
$invoiceManagerLines[] = array(
'type' => 'product',
'fk_facturedet' => $result,
'title' => null,
'parent' => $currentSectionDetId
);
if ($withSections && $currentSectionDetId > 0) {
$invoiceManagerLines[] = array(
'type' => 'product',
'fk_facturedet' => $result,
'title' => null,
'parent' => $currentSectionDetId
);
}
$sectionSubtotal += $prod->subprice * $qtyToInvoice;
} elseif ($result < 0) {
$error++;
@ -641,8 +677,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
}
}
// Zwischensumme hinzufügen wenn Section show_subtotal hat
if ($section->show_subtotal && $sectionSubtotal > 0) {
// Zwischensumme hinzufügen wenn Section show_subtotal hat (nur mit Produktgruppen)
if ($withSections && $section->show_subtotal && $sectionSubtotal > 0) {
$subtotalLabel = 'Zwischensumme: '.$section->title;
$result = $facture->addline(
$subtotalLabel,
@ -689,8 +725,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
if ($hasOrphansWithQty) {
$sonstigeSectionResult = 0;
// Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind
if ($orderHasSections) {
// Nur Section erstellen wenn mit Produktgruppen und im Auftrag Sections vorhanden
if ($withSections && $orderHasSections) {
$sonstigeSectionResult = $facture->addline(
$langs->trans('OtherProducts'),
0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
@ -745,7 +781,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
$prod->fk_unit // 28: fk_unit
);
if ($result > 0 && $orderHasSections && $sonstigeSectionResult > 0) {
if ($result > 0 && $withSections && $orderHasSections && $sonstigeSectionResult > 0) {
$invoiceManagerLines[] = array(
'type' => 'product',
'fk_facturedet' => $result,
@ -759,8 +795,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
}
}
// Zwischensumme für Sonstige Produkte
if ($sonstigeSectionResult > 0) {
// Zwischensumme für Sonstige Produkte (nur mit Produktgruppen)
if ($withSections && $sonstigeSectionResult > 0) {
$subtotalLabel = 'Zwischensumme: '.$langs->trans('OtherProducts');
$subtotalResult = $facture->addline(
$subtotalLabel,
@ -779,12 +815,13 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
}
// Mehraufwand hinzufügen (am Ende)
// Nur als Section wenn im Auftrag auch Sections vorhanden sind
// Section nur wenn: mit Produktgruppen UND Sections im Auftrag UND Einstellung aktiv
$mehraufwandAsSection = getDolGlobalString('STUNDENZETTEL_INVOICE_MEHRAUFWAND_SECTION', '1') == '1';
if (count($additionalProducts) > 0) {
$mehraufwandSectionResult = 0;
// Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind
if ($orderHasSections) {
// Section nur erstellen wenn mit Produktgruppen, Sections vorhanden und Einstellung aktiv
if ($withSections && $orderHasSections && $mehraufwandAsSection) {
$mehraufwandSectionResult = $facture->addline(
$langs->trans('Mehraufwand'),
0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
@ -880,7 +917,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
);
}
if ($productResult > 0 && $orderHasSections && $mehraufwandSectionResult > 0) {
if ($productResult > 0 && $withSections && $orderHasSections && $mehraufwandAsSection && $mehraufwandSectionResult > 0) {
$invoiceManagerLines[] = array(
'type' => 'product',
'fk_facturedet' => $productResult,
@ -893,8 +930,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
}
}
// Zwischensumme für Mehraufwand
if ($mehraufwandSectionResult > 0) {
// Zwischensumme für Mehraufwand (nur mit Produktgruppen und Einstellung aktiv)
if ($withSections && $mehraufwandAsSection && $mehraufwandSectionResult > 0) {
$subtotalLabel = 'Zwischensumme: '.$langs->trans('Mehraufwand');
$subtotalResult = $facture->addline(
$subtotalLabel,
@ -968,7 +1005,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
if ($hasWorkHours) {
// Arbeitszeit-Section erstellen (nur wenn Sections im Auftrag)
$arbeitszeitSectionResult = 0;
if ($orderHasSections) {
if ($withSections && $orderHasSections) {
$arbeitszeitSectionResult = $facture->addline(
$langs->trans('Leistungen'),
0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
@ -1049,7 +1086,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
);
if ($hoursResult > 0) {
if ($orderHasSections && $arbeitszeitSectionResult > 0) {
if ($withSections && $orderHasSections && $arbeitszeitSectionResult > 0) {
$invoiceManagerLines[] = array(
'type' => 'product',
'fk_facturedet' => $hoursResult,
@ -1064,8 +1101,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
}
}
// Zwischensumme für Arbeitszeit
if ($arbeitszeitSectionResult > 0 && $arbeitszeitSubtotal > 0) {
// Zwischensumme für Arbeitszeit (nur mit Produktgruppen)
if ($withSections && $arbeitszeitSectionResult > 0 && $arbeitszeitSubtotal > 0) {
$subtotalLabel = 'Zwischensumme: '.$langs->trans('Leistungen');
$subtotalResult = $facture->addline(
$subtotalLabel,
@ -1083,8 +1120,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
}
}
// llx_facture_lines_manager für Rechnung erstellen
if (!$error && count($invoiceManagerLines) > 0) {
// llx_facture_lines_manager für Rechnung erstellen (nur mit Produktgruppen)
if ($withSections && !$error && count($invoiceManagerLines) > 0) {
$lineOrder = 1;
$sectionMap = array(); // old_facturedet_id => new_manager_rowid
@ -1122,14 +1159,112 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
if (!$error) {
// ============================================
// Rang-Werte in facturedet korrigieren
// Rang-Werte in facturedet korrigieren (nur mit Produktgruppen)
// Synchronisiert rang mit line_order aus facture_lines_manager
// ============================================
$sqlFixRang = "UPDATE ".MAIN_DB_PREFIX."facturedet fd
INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager flm ON flm.fk_facturedet = fd.rowid
SET fd.rang = flm.line_order
WHERE flm.fk_facture = ".((int)$facture_id);
$db->query($sqlFixRang);
if ($withSections) {
$sqlFixRang = "UPDATE ".MAIN_DB_PREFIX."facturedet fd
INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager flm ON flm.fk_facturedet = fd.rowid
SET fd.rang = flm.line_order
WHERE flm.fk_facture = ".((int)$facture_id);
$db->query($sqlFixRang);
}
// ============================================
// Verwaiste Zwischensummen/Sections bereinigen
// SubtotalTitle syncManagerTable() kann beim Seitenaufruf
// manager-Einträge ohne gültigen fk_facturedet erstellen.
// Außerdem können addline()-Fehler zu Orphans führen.
// ============================================
// 1. Verwaiste facture_lines_manager-Einträge löschen (fk_facturedet=NULL oder ungültig)
$sqlCleanManager = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sqlCleanManager .= " WHERE fk_facture = ".((int)$facture_id);
$sqlCleanManager .= " AND (fk_facturedet IS NULL OR fk_facturedet NOT IN (";
$sqlCleanManager .= " SELECT rowid FROM ".MAIN_DB_PREFIX."facturedet WHERE fk_facture = ".((int)$facture_id);
$sqlCleanManager .= " ))";
$db->query($sqlCleanManager);
// 2. Verwaiste Section-Titel in facturedet löschen (product_type=9 ohne zugehörige Produkte)
// Ein Section-Titel (special_code=100) ist verwaist wenn keine Produkte zwischen ihm
// und dem nächsten Section-Titel/Ende existieren
$sqlOrphanSections = "SELECT fd.rowid, fd.rang, fd.special_code, fd.description";
$sqlOrphanSections .= " FROM ".MAIN_DB_PREFIX."facturedet fd";
$sqlOrphanSections .= " WHERE fd.fk_facture = ".((int)$facture_id);
$sqlOrphanSections .= " AND fd.product_type = 9";
$sqlOrphanSections .= " ORDER BY fd.rang ASC";
$resqlOrphan = $db->query($sqlOrphanSections);
if ($resqlOrphan) {
$titleLines = array();
while ($obj = $db->fetch_object($resqlOrphan)) {
$titleLines[] = $obj;
}
// Alle regulären Produkt-/Dienstleistungszeilen laden
$sqlProducts = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX."facturedet";
$sqlProducts .= " WHERE fk_facture = ".((int)$facture_id);
$sqlProducts .= " AND product_type < 9";
$sqlProducts .= " ORDER BY rang ASC";
$resqlProducts = $db->query($sqlProducts);
$productRangs = array();
if ($resqlProducts) {
while ($objP = $db->fetch_object($resqlProducts)) {
$productRangs[] = (int)$objP->rang;
}
}
// Section-Titel und Zwischensummen prüfen
$orphanIds = array();
for ($i = 0; $i < count($titleLines); $i++) {
$line = $titleLines[$i];
// Zwischensummen (special_code=102) ohne zugehörigen Section-Titel
if ($line->special_code == 102) {
// Prüfe ob es einen vorherigen Section-Titel gibt
$hasSection = false;
for ($j = $i - 1; $j >= 0; $j--) {
if ($titleLines[$j]->special_code == 100 && !in_array($titleLines[$j]->rowid, $orphanIds)) {
$hasSection = true;
break;
}
}
if (!$hasSection) {
$orphanIds[] = (int)$line->rowid;
}
continue;
}
// Section-Titel (special_code=100): verwaist wenn keine Produkte zwischen
// diesem Titel und dem nächsten Titel/Zwischensumme
if ($line->special_code == 100) {
$currentRang = (int)$line->rang;
// Nächsten Titel/Zwischensumme finden
$nextTitleRang = PHP_INT_MAX;
for ($j = $i + 1; $j < count($titleLines); $j++) {
$nextTitleRang = (int)$titleLines[$j]->rang;
break;
}
// Prüfen ob Produkte zwischen currentRang und nextTitleRang existieren
$hasProducts = false;
foreach ($productRangs as $pRang) {
if ($pRang > $currentRang && $pRang < $nextTitleRang) {
$hasProducts = true;
break;
}
}
if (!$hasProducts) {
$orphanIds[] = (int)$line->rowid;
}
}
}
// Verwaiste Zeilen aus facturedet und facture_lines_manager löschen
if (!empty($orphanIds)) {
$orphanIdList = implode(',', $orphanIds);
$db->query("DELETE FROM ".MAIN_DB_PREFIX."facturedet WHERE rowid IN (".$orphanIdList.")");
$db->query("DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE fk_facture = ".((int)$facture_id)." AND fk_facturedet IN (".$orphanIdList.")");
}
}
// Status auf "in Rechnung übertragen" setzen (direkt per SQL)
$sqlUpdate = "UPDATE ".MAIN_DB_PREFIX."commande_extrafields SET stundenzettel_status = 2 WHERE fk_object = ".((int)$order->id);
@ -1269,17 +1404,32 @@ if ($action == 'confirm_release_warning') {
);
}
// Bestätigung für Rechnungsübertragung
// Bestätigung für Rechnungsübertragung (mit Auswahl: mit/ohne Produktgruppen)
if ($action == 'transfer_invoice') {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1',
$langs->trans('ConfirmTransferInvoiceTitle'),
$langs->trans('ConfirmTransferInvoice'),
'confirm_transfer_invoice',
array(),
0,
1
);
$baseUrl = $_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1';
print '<div class="confirmmessage">';
print '<div class="confirmmessagebox" style="max-width:600px; margin:10px auto; padding:20px; border:1px solid #c0c0c0; border-radius:5px; background:#f8f8f8;">';
print '<h3 style="margin-top:0;">'.img_picto('', 'bill', 'class="pictofixedwidth"').$langs->trans('ConfirmTransferInvoiceTitle').'</h3>';
print '<p>'.$langs->trans('ConfirmTransferInvoiceChoice').'</p>';
print '<div style="display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:15px;">';
// Button: Mit Produktgruppen
print '<a class="butAction" href="'.$baseUrl.'&action=confirm_transfer_invoice">';
print img_picto('', 'object_list', 'class="pictofixedwidth"').$langs->trans('TransferWithSections');
print '</a>';
// Button: Ohne Produktgruppen
print '<a class="butAction" href="'.$baseUrl.'&action=confirm_transfer_invoice_flat">';
print img_picto('', 'object_line', 'class="pictofixedwidth"').$langs->trans('TransferWithoutSections');
print '</a>';
print '</div>';
print '<div style="display:flex; gap:10px; justify-content:center; margin-top:10px;">';
print '<a class="butActionDelete" href="'.$baseUrl.'">'.$langs->trans('Cancel').'</a>';
print '</div>';
print '<div style="margin-top:12px; font-size:0.9em; color:#666;">';
print '<p style="margin:3px 0;"><strong>'.$langs->trans('TransferWithSections').':</strong> '.$langs->trans('ConfirmTransferInvoiceWithSections').'</p>';
print '<p style="margin:3px 0;"><strong>'.$langs->trans('TransferWithoutSections').':</strong> '.$langs->trans('ConfirmTransferInvoiceWithoutSections').'</p>';
print '</div>';
print '</div>';
print '</div>';
}
// Info banner - Produktliste zeigt immer den Auftrag, andere Tabs den Stundenzettel