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:
parent
8ea1180041
commit
dabcdbde13
5 changed files with 325 additions and 68 deletions
18
README.md
18
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Stundenzettel Modul für Dolibarr
|
# Stundenzettel Modul für Dolibarr
|
||||||
|
|
||||||
**Version:** 1.8.0
|
**Version:** 2.1.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
|
||||||
|
|
@ -56,6 +56,8 @@ Die Moduleinstellungen finden Sie unter **Einstellungen > Module > Stundenzettel
|
||||||
| **Standard-Filter** | Welcher Filter in der Produktliste standardmäßig angezeigt wird |
|
| **Standard-Filter** | Welcher Filter in der Produktliste standardmäßig angezeigt wird |
|
||||||
| **Standard-Datum** | Aktuelles Datum oder Datum des letzten offenen Stundenzettels |
|
| **Standard-Datum** | Aktuelles Datum oder Datum des letzten offenen Stundenzettels |
|
||||||
| **Stunden-Übernahme** | Gesamtstunden auf einer Zeile oder pro Tag eine Zeile |
|
| **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)
|
### Stundenübernahme-Modus (pro Auftrag)
|
||||||
|
|
||||||
|
|
@ -117,6 +119,20 @@ Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung
|
||||||
|
|
||||||
## Changelog
|
## 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
|
### Version 1.8.0
|
||||||
- **Rücknahmen in Rechnungsübernahme**: Zurückgenommene Produkte (origin='returned') werden nicht mehr in die Rechnung übertragen
|
- **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)
|
- Mehraufwand: Rücknahmen werden von der Rechnungsmenge abgezogen (Matching über fk_product oder description)
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,28 @@ if ($action == 'setDEFAULT_SECTIONS') {
|
||||||
exit;
|
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
|
* View
|
||||||
*/
|
*/
|
||||||
|
|
@ -212,6 +234,62 @@ print '</tr>';
|
||||||
|
|
||||||
print '</table>';
|
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
|
// PWA Mobile App Bereich
|
||||||
print '<br>';
|
print '<br>';
|
||||||
print '<table class="noborder centpercent">';
|
print '<table class="noborder centpercent">';
|
||||||
|
|
|
||||||
|
|
@ -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.8.0';
|
$this->version = '2.1.0';
|
||||||
|
|
||||||
// Autor
|
// Autor
|
||||||
$this->editor_name = 'Data IT Solution';
|
$this->editor_name = 'Data IT Solution';
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,19 @@ incl = inkl.
|
||||||
NettoSTZ = Netto STZ
|
NettoSTZ = Netto STZ
|
||||||
NettoSTZHelp = Netto-Wert aller freigegebenen Stundenzettel (Produkte + Arbeitsstunden)
|
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
|
# PWA Mobile App
|
||||||
PWAMobileApp = PWA Mobile App
|
PWAMobileApp = PWA Mobile App
|
||||||
PWALink = Stundenzettel PWA öffnen
|
PWALink = Stundenzettel PWA öffnen
|
||||||
|
|
|
||||||
|
|
@ -359,10 +359,13 @@ if ($action == 'reset_stundenzettel' && $canReset) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In Rechnung übernehmen
|
// In Rechnung übernehmen (mit oder ohne Produktgruppen)
|
||||||
if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes' && $user->hasRight('facture', 'creer')) {
|
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';
|
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
||||||
|
|
||||||
|
// Steuerung: Mit oder ohne Produktgruppen
|
||||||
|
$withSections = ($action == 'confirm_transfer_invoice');
|
||||||
|
|
||||||
$db->begin();
|
$db->begin();
|
||||||
$error = 0;
|
$error = 0;
|
||||||
|
|
||||||
|
|
@ -377,9 +380,36 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
$facture->note_private = $langs->trans('CreatedFromStundenzettel').' - '.$order->ref;
|
$facture->note_private = $langs->trans('CreatedFromStundenzettel').' - '.$order->ref;
|
||||||
$facture->linked_objects['commande'] = $order->id;
|
$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);
|
$facture_id = $facture->create($user);
|
||||||
|
|
||||||
if ($facture_id > 0) {
|
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
|
// Einkaufspreise aller Produkte vorladen
|
||||||
// Priorität: 1. cost_price (manuell gesetzt), 2. bester Lieferanten-Stückpreis, 3. PMP
|
// Priorität: 1. cost_price (manuell gesetzt), 2. bester Lieferanten-Stückpreis, 3. PMP
|
||||||
$buyPrices = array(); // fk_product => buy_price
|
$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
|
// Nur Sections mit tatsächlich verwendeten Produkten hinzufügen
|
||||||
if ($sectionHasInvoicedProducts) {
|
if ($sectionHasInvoicedProducts) {
|
||||||
// Section-Titel hinzufügen (special_code = 100)
|
$currentSectionDetId = 0;
|
||||||
$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
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($result > 0) {
|
// Section-Titel hinzufügen (nur bei Übernahme mit Produktgruppen)
|
||||||
$invoiceManagerLines[] = array(
|
if ($withSections) {
|
||||||
'type' => 'section',
|
$result = $facture->addline(
|
||||||
'fk_facturedet' => $result,
|
$section->title,
|
||||||
'title' => $section->title,
|
0, // subprice
|
||||||
'parent' => null
|
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
|
// Produkte der Section hinzufügen
|
||||||
|
|
@ -627,12 +661,14 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($result > 0) {
|
if ($result > 0) {
|
||||||
$invoiceManagerLines[] = array(
|
if ($withSections && $currentSectionDetId > 0) {
|
||||||
'type' => 'product',
|
$invoiceManagerLines[] = array(
|
||||||
'fk_facturedet' => $result,
|
'type' => 'product',
|
||||||
'title' => null,
|
'fk_facturedet' => $result,
|
||||||
'parent' => $currentSectionDetId
|
'title' => null,
|
||||||
);
|
'parent' => $currentSectionDetId
|
||||||
|
);
|
||||||
|
}
|
||||||
$sectionSubtotal += $prod->subprice * $qtyToInvoice;
|
$sectionSubtotal += $prod->subprice * $qtyToInvoice;
|
||||||
} elseif ($result < 0) {
|
} elseif ($result < 0) {
|
||||||
$error++;
|
$error++;
|
||||||
|
|
@ -641,8 +677,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zwischensumme hinzufügen wenn Section show_subtotal hat
|
// Zwischensumme hinzufügen wenn Section show_subtotal hat (nur mit Produktgruppen)
|
||||||
if ($section->show_subtotal && $sectionSubtotal > 0) {
|
if ($withSections && $section->show_subtotal && $sectionSubtotal > 0) {
|
||||||
$subtotalLabel = 'Zwischensumme: '.$section->title;
|
$subtotalLabel = 'Zwischensumme: '.$section->title;
|
||||||
$result = $facture->addline(
|
$result = $facture->addline(
|
||||||
$subtotalLabel,
|
$subtotalLabel,
|
||||||
|
|
@ -689,8 +725,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
if ($hasOrphansWithQty) {
|
if ($hasOrphansWithQty) {
|
||||||
$sonstigeSectionResult = 0;
|
$sonstigeSectionResult = 0;
|
||||||
|
|
||||||
// Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind
|
// Nur Section erstellen wenn mit Produktgruppen und im Auftrag Sections vorhanden
|
||||||
if ($orderHasSections) {
|
if ($withSections && $orderHasSections) {
|
||||||
$sonstigeSectionResult = $facture->addline(
|
$sonstigeSectionResult = $facture->addline(
|
||||||
$langs->trans('OtherProducts'),
|
$langs->trans('OtherProducts'),
|
||||||
0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
|
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
|
$prod->fk_unit // 28: fk_unit
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($result > 0 && $orderHasSections && $sonstigeSectionResult > 0) {
|
if ($result > 0 && $withSections && $orderHasSections && $sonstigeSectionResult > 0) {
|
||||||
$invoiceManagerLines[] = array(
|
$invoiceManagerLines[] = array(
|
||||||
'type' => 'product',
|
'type' => 'product',
|
||||||
'fk_facturedet' => $result,
|
'fk_facturedet' => $result,
|
||||||
|
|
@ -759,8 +795,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zwischensumme für Sonstige Produkte
|
// Zwischensumme für Sonstige Produkte (nur mit Produktgruppen)
|
||||||
if ($sonstigeSectionResult > 0) {
|
if ($withSections && $sonstigeSectionResult > 0) {
|
||||||
$subtotalLabel = 'Zwischensumme: '.$langs->trans('OtherProducts');
|
$subtotalLabel = 'Zwischensumme: '.$langs->trans('OtherProducts');
|
||||||
$subtotalResult = $facture->addline(
|
$subtotalResult = $facture->addline(
|
||||||
$subtotalLabel,
|
$subtotalLabel,
|
||||||
|
|
@ -779,12 +815,13 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mehraufwand hinzufügen (am Ende)
|
// 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) {
|
if (count($additionalProducts) > 0) {
|
||||||
$mehraufwandSectionResult = 0;
|
$mehraufwandSectionResult = 0;
|
||||||
|
|
||||||
// Nur Section erstellen wenn im Auftrag auch Sections vorhanden sind
|
// Section nur erstellen wenn mit Produktgruppen, Sections vorhanden und Einstellung aktiv
|
||||||
if ($orderHasSections) {
|
if ($withSections && $orderHasSections && $mehraufwandAsSection) {
|
||||||
$mehraufwandSectionResult = $facture->addline(
|
$mehraufwandSectionResult = $facture->addline(
|
||||||
$langs->trans('Mehraufwand'),
|
$langs->trans('Mehraufwand'),
|
||||||
0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
|
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(
|
$invoiceManagerLines[] = array(
|
||||||
'type' => 'product',
|
'type' => 'product',
|
||||||
'fk_facturedet' => $productResult,
|
'fk_facturedet' => $productResult,
|
||||||
|
|
@ -893,8 +930,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zwischensumme für Mehraufwand
|
// Zwischensumme für Mehraufwand (nur mit Produktgruppen und Einstellung aktiv)
|
||||||
if ($mehraufwandSectionResult > 0) {
|
if ($withSections && $mehraufwandAsSection && $mehraufwandSectionResult > 0) {
|
||||||
$subtotalLabel = 'Zwischensumme: '.$langs->trans('Mehraufwand');
|
$subtotalLabel = 'Zwischensumme: '.$langs->trans('Mehraufwand');
|
||||||
$subtotalResult = $facture->addline(
|
$subtotalResult = $facture->addline(
|
||||||
$subtotalLabel,
|
$subtotalLabel,
|
||||||
|
|
@ -968,7 +1005,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
if ($hasWorkHours) {
|
if ($hasWorkHours) {
|
||||||
// Arbeitszeit-Section erstellen (nur wenn Sections im Auftrag)
|
// Arbeitszeit-Section erstellen (nur wenn Sections im Auftrag)
|
||||||
$arbeitszeitSectionResult = 0;
|
$arbeitszeitSectionResult = 0;
|
||||||
if ($orderHasSections) {
|
if ($withSections && $orderHasSections) {
|
||||||
$arbeitszeitSectionResult = $facture->addline(
|
$arbeitszeitSectionResult = $facture->addline(
|
||||||
$langs->trans('Leistungen'),
|
$langs->trans('Leistungen'),
|
||||||
0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
|
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 ($hoursResult > 0) {
|
||||||
if ($orderHasSections && $arbeitszeitSectionResult > 0) {
|
if ($withSections && $orderHasSections && $arbeitszeitSectionResult > 0) {
|
||||||
$invoiceManagerLines[] = array(
|
$invoiceManagerLines[] = array(
|
||||||
'type' => 'product',
|
'type' => 'product',
|
||||||
'fk_facturedet' => $hoursResult,
|
'fk_facturedet' => $hoursResult,
|
||||||
|
|
@ -1064,8 +1101,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zwischensumme für Arbeitszeit
|
// Zwischensumme für Arbeitszeit (nur mit Produktgruppen)
|
||||||
if ($arbeitszeitSectionResult > 0 && $arbeitszeitSubtotal > 0) {
|
if ($withSections && $arbeitszeitSectionResult > 0 && $arbeitszeitSubtotal > 0) {
|
||||||
$subtotalLabel = 'Zwischensumme: '.$langs->trans('Leistungen');
|
$subtotalLabel = 'Zwischensumme: '.$langs->trans('Leistungen');
|
||||||
$subtotalResult = $facture->addline(
|
$subtotalResult = $facture->addline(
|
||||||
$subtotalLabel,
|
$subtotalLabel,
|
||||||
|
|
@ -1083,8 +1120,8 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// llx_facture_lines_manager für Rechnung erstellen
|
// llx_facture_lines_manager für Rechnung erstellen (nur mit Produktgruppen)
|
||||||
if (!$error && count($invoiceManagerLines) > 0) {
|
if ($withSections && !$error && count($invoiceManagerLines) > 0) {
|
||||||
$lineOrder = 1;
|
$lineOrder = 1;
|
||||||
$sectionMap = array(); // old_facturedet_id => new_manager_rowid
|
$sectionMap = array(); // old_facturedet_id => new_manager_rowid
|
||||||
|
|
||||||
|
|
@ -1122,14 +1159,112 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
||||||
|
|
||||||
if (!$error) {
|
if (!$error) {
|
||||||
// ============================================
|
// ============================================
|
||||||
// Rang-Werte in facturedet korrigieren
|
// Rang-Werte in facturedet korrigieren (nur mit Produktgruppen)
|
||||||
// Synchronisiert rang mit line_order aus facture_lines_manager
|
// Synchronisiert rang mit line_order aus facture_lines_manager
|
||||||
// ============================================
|
// ============================================
|
||||||
$sqlFixRang = "UPDATE ".MAIN_DB_PREFIX."facturedet fd
|
if ($withSections) {
|
||||||
INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager flm ON flm.fk_facturedet = fd.rowid
|
$sqlFixRang = "UPDATE ".MAIN_DB_PREFIX."facturedet fd
|
||||||
SET fd.rang = flm.line_order
|
INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager flm ON flm.fk_facturedet = fd.rowid
|
||||||
WHERE flm.fk_facture = ".((int)$facture_id);
|
SET fd.rang = flm.line_order
|
||||||
$db->query($sqlFixRang);
|
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)
|
// 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);
|
$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') {
|
if ($action == 'transfer_invoice') {
|
||||||
print $form->formconfirm(
|
$baseUrl = $_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1';
|
||||||
$_SERVER['PHP_SELF'].'?id='.$order->id.'&tab=products&noredirect=1',
|
print '<div class="confirmmessage">';
|
||||||
$langs->trans('ConfirmTransferInvoiceTitle'),
|
print '<div class="confirmmessagebox" style="max-width:600px; margin:10px auto; padding:20px; border:1px solid #c0c0c0; border-radius:5px; background:#f8f8f8;">';
|
||||||
$langs->trans('ConfirmTransferInvoice'),
|
print '<h3 style="margin-top:0;">'.img_picto('', 'bill', 'class="pictofixedwidth"').$langs->trans('ConfirmTransferInvoiceTitle').'</h3>';
|
||||||
'confirm_transfer_invoice',
|
print '<p>'.$langs->trans('ConfirmTransferInvoiceChoice').'</p>';
|
||||||
array(),
|
print '<div style="display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:15px;">';
|
||||||
0,
|
// Button: Mit Produktgruppen
|
||||||
1
|
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
|
// Info banner - Produktliste zeigt immer den Auftrag, andere Tabs den Stundenzettel
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue