Version 1.8.0: Rücknahmen und Einkaufspreise in Rechnungsübernahme
- Rücknahmen (returned) werden bei Rechnungsübertragung abgezogen - Mehraufwand: Matching über fk_product bzw. MD5 der description - Auftragspositionen: Matching über fk_commandedet - Effektive Menge ≤ 0 → Produkt wird nicht übernommen - Einkaufspreise (buy_price_ht/pa_ht) werden automatisch gesetzt - Priorität: cost_price > Lieferanten-Stückpreis > PMP - Lieferanten-unitprice statt PMP verhindert falsche Margen bei Meterware Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a69e308a68
commit
6257713e5b
3 changed files with 102 additions and 7 deletions
12
README.md
12
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)
|
||||
|
|
|
|||
2
core/modules/modStundenzettel.class.php
Normal file → Executable file
2
core/modules/modStundenzettel.class.php
Normal file → Executable 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.7.0';
|
||||
$this->version = '1.8.0';
|
||||
|
||||
// Autor
|
||||
$this->editor_name = 'Data IT Solution';
|
||||
|
|
|
|||
95
stundenzettel_commande.php
Normal file → Executable file
95
stundenzettel_commande.php
Normal file → Executable file
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue