diff --git a/README.md b/README.md index 1b3d2ca..ecf1358 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stundenzettel Modul für Dolibarr -**Version:** 1.7.0 +**Version:** 1.8.0 **Autor:** Data IT Solution **Kompatibilität:** Dolibarr 16.0+ **Lizenz:** GPL v3 @@ -117,6 +117,16 @@ Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung ## Changelog +### 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) + - Auftragspositionen: Rücknahmen werden über fk_commandedet abgezogen + - Produkte mit effektiver Menge ≤ 0 werden komplett ausgelassen +- **Einkaufspreise in Rechnungsübernahme**: buy_price_ht (pa_ht) wird jetzt automatisch gesetzt + - Priorität: 1. cost_price (manuell gesetzt), 2. bester Lieferanten-Stückpreis, 3. PMP + - Ermöglicht korrekte Margenberechnung in der Rechnung + - Freitext-Produkte ohne Produktverknüpfung bleiben bei EK=0 + ### Version 1.7.0 - **Rücknahme-Anzeige verbessert**: Rücknahmen werden mit rotem Badge (-X) angezeigt - **Rücknahme beeinflusst Beauftragt**: Zurückgenommene Mengen werden von der Zielmenge abgezogen (soll nicht wieder verbaut werden) diff --git a/core/modules/modStundenzettel.class.php b/core/modules/modStundenzettel.class.php old mode 100644 new mode 100755 index 8021a1c..719fc07 --- a/core/modules/modStundenzettel.class.php +++ b/core/modules/modStundenzettel.class.php @@ -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.7.0'; + $this->version = '1.8.0'; // Autor $this->editor_name = 'Data IT Solution'; diff --git a/stundenzettel_commande.php b/stundenzettel_commande.php old mode 100644 new mode 100755 index d4f45fd..701f245 --- a/stundenzettel_commande.php +++ b/stundenzettel_commande.php @@ -380,6 +380,37 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes $facture_id = $facture->create($user); if ($facture_id > 0) { + // Einkaufspreise aller Produkte vorladen + // Priorität: 1. cost_price (manuell gesetzt), 2. bester Lieferanten-Stückpreis, 3. PMP + $buyPrices = array(); // fk_product => buy_price + $sqlBuyPrices = "SELECT p.rowid, p.cost_price, p.pmp,"; + // Bester Lieferanten-Stückpreis (günstigster unitprice) + $sqlBuyPrices .= " (SELECT MIN(pfp.unitprice) FROM ".MAIN_DB_PREFIX."product_fournisseur_price pfp"; + $sqlBuyPrices .= " WHERE pfp.fk_product = p.rowid AND pfp.unitprice > 0) as best_supplier_unitprice"; + $sqlBuyPrices .= " FROM ".MAIN_DB_PREFIX."product p"; + $sqlBuyPrices .= " WHERE p.rowid IN ("; + $sqlBuyPrices .= " SELECT DISTINCT cd.fk_product FROM ".MAIN_DB_PREFIX."commandedet cd WHERE cd.fk_commande = ".((int)$order->id)." AND cd.fk_product IS NOT NULL"; + $sqlBuyPrices .= " UNION"; + $sqlBuyPrices .= " SELECT DISTINCT sp.fk_product FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlBuyPrices .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlBuyPrices .= " WHERE s.fk_commande = ".((int)$order->id)." AND sp.fk_product IS NOT NULL"; + $sqlBuyPrices .= " )"; + $resqlBuyPrices = $db->query($sqlBuyPrices); + if ($resqlBuyPrices) { + while ($objBp = $db->fetch_object($resqlBuyPrices)) { + // Priorität: cost_price > Lieferanten-Stückpreis > PMP + if (!empty($objBp->cost_price) && $objBp->cost_price > 0) { + $buyPrices[(int)$objBp->rowid] = (float)$objBp->cost_price; + } elseif (!empty($objBp->best_supplier_unitprice) && $objBp->best_supplier_unitprice > 0) { + $buyPrices[(int)$objBp->rowid] = (float)$objBp->best_supplier_unitprice; + } elseif (!empty($objBp->pmp) && $objBp->pmp > 0) { + $buyPrices[(int)$objBp->rowid] = (float)$objBp->pmp; + } else { + $buyPrices[(int)$objBp->rowid] = 0; + } + } + } + // Sammle alle Produkt-Mengen aus Stundenzetteln (gruppiert nach fk_commandedet) $productQtys = array(); $sqlQty = "SELECT sp.fk_commandedet, SUM(sp.qty_done) as total_qty"; @@ -397,6 +428,44 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes } } + // Rücknahmen laden für Abzug bei Mehraufwand und Auftragspositionen + // (wird vor beiden Bereichen geladen, da beide Rücknahmen berücksichtigen müssen) + $returnedQtys = array(); // key => qty (für Mehraufwand: prod_ID oder desc_MD5) + $returnedByCommandedet = array(); // fk_commandedet => qty (für Auftragspositionen) + $sqlReturned = "SELECT sp.fk_product, sp.fk_commandedet, sp.product_label, sp.description, SUM(sp.qty_done) as qty_returned"; + $sqlReturned .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp"; + $sqlReturned .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel"; + $sqlReturned .= " WHERE s.fk_commande = ".((int)$order->id); + $sqlReturned .= " AND sp.origin = 'returned'"; + $sqlReturned .= " GROUP BY sp.fk_product, sp.fk_commandedet, sp.product_label, sp.description"; + $resqlReturned = $db->query($sqlReturned); + if ($resqlReturned) { + while ($objRet = $db->fetch_object($resqlReturned)) { + // Für Mehraufwand-Matching (fk_product oder description) + if ($objRet->fk_product > 0) { + $retKey = 'prod_'.$objRet->fk_product; + } else { + $retKey = 'desc_'.md5(trim(strip_tags($objRet->description))); + } + $returnedQtys[$retKey] = (isset($returnedQtys[$retKey]) ? $returnedQtys[$retKey] : 0) + (float)$objRet->qty_returned; + + // Für Auftragspositionen-Matching (fk_commandedet) + if (!empty($objRet->fk_commandedet)) { + $returnedByCommandedet[$objRet->fk_commandedet] = (isset($returnedByCommandedet[$objRet->fk_commandedet]) ? $returnedByCommandedet[$objRet->fk_commandedet] : 0) + (float)$objRet->qty_returned; + } + } + } + + // Rücknahmen von Auftragspositionen abziehen + foreach ($returnedByCommandedet as $cmdDetId => $retQty) { + if (isset($productQtys[$cmdDetId])) { + $productQtys[$cmdDetId] -= $retQty; + if ($productQtys[$cmdDetId] <= 0) { + unset($productQtys[$cmdDetId]); + } + } + } + // Sammle Mehraufwand und zusätzlich verbaute Produkte // (origin='additional' = beauftragt, origin='added' ohne fk_commandedet = verbaut ohne Auftragszeile) $additionalProducts = array(); @@ -414,7 +483,19 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes if ($resqlAdd) { while ($objAdd = $db->fetch_object($resqlAdd)) { // Für die Rechnung zählt die verbaute Menge (added), nicht nur beauftragte (additional) - $objAdd->invoice_qty = floatval($objAdd->qty_added) > 0 ? floatval($objAdd->qty_added) : floatval($objAdd->qty_additional); + $invoiceQty = floatval($objAdd->qty_added) > 0 ? floatval($objAdd->qty_added) : floatval($objAdd->qty_additional); + + // Rücknahmen abziehen + if ($objAdd->fk_product > 0) { + $retKey = 'prod_'.$objAdd->fk_product; + } else { + $retKey = 'desc_'.md5(trim(strip_tags($objAdd->description))); + } + if (isset($returnedQtys[$retKey])) { + $invoiceQty -= $returnedQtys[$retKey]; + } + + $objAdd->invoice_qty = $invoiceQty; if ($objAdd->invoice_qty > 0) { $additionalProducts[] = $objAdd; } @@ -513,6 +594,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; if ($qtyToInvoice > 0) { + $prodBuyPrice = isset($buyPrices[(int)$prod->fk_product]) ? $buyPrices[(int)$prod->fk_product] : 0; $result = $facture->addline( $prod->description, // 1: desc $prod->subprice, // 2: pu_ht @@ -536,7 +618,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice - 0, // 23: pa_ht + $prodBuyPrice, // 23: pa_ht (Einkaufspreis) '', // 24: label array(), // 25: array_options 100, // 26: situation_percent @@ -631,6 +713,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes $qtyToInvoice = isset($productQtys[$prod->fk_commandedet]) ? $productQtys[$prod->fk_commandedet] : 0; if ($qtyToInvoice > 0) { + $prodBuyPrice = isset($buyPrices[(int)$prod->fk_product]) ? $buyPrices[(int)$prod->fk_product] : 0; $result = $facture->addline( $prod->description, // 1: desc $prod->subprice, // 2: pu_ht @@ -654,7 +737,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice - 0, // 23: pa_ht + $prodBuyPrice, // 23: pa_ht (Einkaufspreis) '', // 24: label array(), // 25: array_options 100, // 26: situation_percent @@ -732,6 +815,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes $usePrice = $customerPriceInfo['price']; $useTvaTx = $customerPriceInfo['tva_tx']; + $addBuyPrice = isset($buyPrices[(int)$addProd->fk_product]) ? $buyPrices[(int)$addProd->fk_product] : 0; $productResult = $facture->addline( $product->label, // 1: desc $usePrice, // 2: pu_ht - kundenspezifischer Preis @@ -755,7 +839,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice - 0, // 23: pa_ht + $addBuyPrice, // 23: pa_ht (Einkaufspreis) '', // 24: label array(), // 25: array_options 100, // 26: situation_percent @@ -932,6 +1016,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes } // Rechnungszeile hinzufügen + $serviceBuyPrice = isset($buyPrices[$productId]) ? $buyPrices[$productId] : 0; $hoursResult = $facture->addline( $lineDesc, // 1: desc $hourlyPrice, // 2: pu_ht @@ -955,7 +1040,7 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes 0, // 20: origin_id 0, // 21: fk_parent_line null, // 22: fk_fournprice - 0, // 23: pa_ht + $serviceBuyPrice, // 23: pa_ht (Einkaufspreis) '', // 24: label array(), // 25: array_options 100, // 26: situation_percent