diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d81edc..09f2588 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,28 +9,46 @@ --- -## v2.3 - NetPrice-Korrektur & Produktzuordnung (24.02.2026) +## v2.9 - Preis-Vergleich & Kritische Bugfixes (12.03.2026) -### Preisberechnung korrigiert (NetPrice-Format) -- **Problem**: Sonepar sendet `NetPrice` als **Zeilengesamtpreis** (nicht als Stückpreis wie im IDS-Standard) - - Beispiel: 20 Stk Steckdose, NetPrice=72.00 → korrekt 3.60€/Stk, war fälschlich 72€/Stk → 1.440€ statt 72€ -- **Lösung**: Automatische Format-Erkennung per Voting-Heuristik in `parseCartXml()`: - - Erster Durchlauf: Prüft alle Artikel ob `NetPrice > OfferPrice` bei `Qty > 1` - - Ein rabattierter Stückpreis kann nie über dem Listenpreis liegen → muss Zeilensumme sein - - Mehrheitsentscheidung bestimmt das Format für den gesamten Warenkorb -- **Rückwärtskompatibel**: Ohne OfferPrice (Mock-Server, andere Großhändler) → IDS-Standard wird beibehalten -- **PriceBasis-Anzeige**: Wird bei erkanntem Zeilensummen-Format unterdrückt +### 🐛 KRITISCHER BUGFIX: SQL-Spaltenname korrigiert +- **Problem**: `matchProducts()` hat SQL-Fehler produziert wegen falscher Spalte `pfp.qty` (existiert nicht!) +- **Fix**: Alle `pfp.qty` → `pfp.quantity` ersetzt in: + - [class/idsconnect.class.php:578](class/idsconnect.class.php#L578) - matchProducts() SELECT Query + - [class/idsconnect.class.php:589](class/idsconnect.class.php#L589) - $obj->qty → $obj->quantity + - [tab_supplierorder.php:154,177](tab_supplierorder.php#L154) - Preis-Vergleich Queries +- **Auswirkung**: Vorher wurden ALLE Artikel als Freitext importiert (kein Produkt-Matching!) +- **Test**: Artikel 0486597 wird jetzt korrekt als COK-WAG-2273-202-2F erkannt ✅ -### Produktzuordnung bei Warenkorbübernahme -- **Problem**: Alle Artikel wurden als Freitext-Positionen in die Bestellung übernommen, nicht als Dolibarr-Produkte -- **Neue Methode**: `matchProducts()` in `idsconnect.class.php` - Batch-Lookup der Lieferanten-Artikelnummern gegen `llx_product_fournisseur_price` -- **cart_review.php** nutzt jetzt `fk_product` und `fk_prod_fourn_price` beim Erstellen der Bestellpositionen -- **Anzeige**: Neue Spalte "Dolibarr-Produkt" in der Warenkorbprüfung mit Link zum Produkt oder "Freitext" -- **Info-Banner**: Zeigt "Produkte erkannt: X/Y" als Übersicht +### 🐛 BUGFIX: Produkt-Matching erweitert +- **Fix**: `p.status = 1` → `p.tosell = 1` in matchProducts() und tab_supplierorder.php +- **Grund**: Dolibarr verwendet `tosell` für verkaufbare Produkte, nicht `status` -### XML-Parsing Priorität -- `cart_review.php` parst Warenkorb jetzt immer frisch aus `cart_xml` (statt aus `response_data`) -- Damit greifen Bugfixes im Parser auch für bereits gespeicherte Warenkörbe +### ✨ Preis-Vergleich in Warenkorb-Import (cart_review.php) +- **Basis-Price-Vergleich**: Shop-Preis (z.B. 12,88€/100 Stk) vs. DB-Preis (12,88€/100 Stk) +- **Vorteil**: Keine Rundungsfehler mehr durch Stückpreis-Umrechnung +- **Berechnung**: + - `shop_basis_price = raw_netprice` (vom Shop empfangen) + - `db_basis_price = debug_price` (aus DB geladen) + - Deviation = ((shop - db) / db) × 100 +- **DEBUG-Ausgabe** für Admins: Zeigt Shop-Preis, DB-Preis, Einzelpreis zur Kontrolle +- **Farbcodierung**: + - Grün: ≤2% Abweichung + - Gelb: 2-10% Abweichung + - Rot: >10% Abweichung + +### ✨ Preis-Vergleich in Lieferantenbestellungen (tab_supplierorder.php) +- **Admin-Option**: `IDSCONNECT_PRICE_UPDATE_ENABLED` (Standard: AUS) +- **Schwellwert**: `IDSCONNECT_PRICE_UPDATE_THRESHOLD` (Standard: 5%) +- **Anzeige**: Tabelle mit allen Positionen, die vom Schwellwert abweichen +- **Freitext-Warnung**: Zeigt an, wenn Artikel nicht mit Dolibarr-Produkten verknüpft sind +- **Keine Auto-Updates**: Nur Anzeige, keine automatische Preis-Aktualisierung + +### 🔧 Technische Details +- **matchProducts()**: Lädt jetzt `pfp.quantity`, `pfp.price` korrekt +- **Stückpreis-Berechnung**: `unit_price = price / quantity` (mit Fallback qty=1) +- **Debug-Felder**: `debug_price` und `debug_qty` für Basis-Price-Vergleich +- **Sprachdateien**: Neue Keys für Preis-Verwaltung (de_DE) --- diff --git a/admin/setup.php b/admin/setup.php index 4403a8a..6b4e4da 100755 --- a/admin/setup.php +++ b/admin/setup.php @@ -91,6 +91,9 @@ if ($action == 'update' && $user->admin) { $pin_hash = password_hash($new_pin, PASSWORD_DEFAULT); dolibarr_set_const($db, 'IDSCONNECT_WKS_PIN', $pin_hash, 'chaine', 0, '', $conf->entity); } + // Preis-Update (manuell) + dolibarr_set_const($db, 'IDSCONNECT_PRICE_UPDATE_ENABLED', GETPOSTINT('IDSCONNECT_PRICE_UPDATE_ENABLED'), 'chaine', 0, '', $conf->entity); + dolibarr_set_const($db, 'IDSCONNECT_PRICE_UPDATE_THRESHOLD', GETPOST('IDSCONNECT_PRICE_UPDATE_THRESHOLD', 'alphanohtml'), 'chaine', 0, '', $conf->entity); if (!$error) { setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); @@ -153,6 +156,27 @@ print ''; print $form->selectyesno('IDSCONNECT_LOG_ENABLED', getDolGlobalInt('IDSCONNECT_LOG_ENABLED', 1), 1); print ''; +// Überschrift: Preis-Verwaltung +print ''; +print ''.$langs->trans("IdsconnectPriceManagement").''; +print ''; + +// Preis-Update aktivieren +print ''; +print ''.$langs->trans("IdsconnectPriceUpdateEnabled").''; +print ''; +print $form->selectyesno('IDSCONNECT_PRICE_UPDATE_ENABLED', getDolGlobalInt('IDSCONNECT_PRICE_UPDATE_ENABLED', 0), 1); +print ' '.$langs->trans("IdsconnectPriceUpdateEnabledHelp").''; +print ''; + +// Preis-Update Schwellwert +print ''; +print ''.$langs->trans("IdsconnectPriceUpdateThreshold").''; +print ''; +print ''; +print ' % '.$langs->trans("IdsconnectPriceUpdateThresholdHelp").''; +print ''; + // Überschrift: WKS-Sicherheit print ''; print ''.$langs->trans("IdsconnectWksSecuritySettings").''; diff --git a/cart_review.php b/cart_review.php index 6f5a5d8..68bac47 100755 --- a/cart_review.php +++ b/cart_review.php @@ -108,6 +108,33 @@ if (!empty($items) && $supplier->fk_soc > 0) { } } +// HEURISTIK: Sonepar sendet NetPrice manchmal für Qty statt PriceBasis +// Korrigiere einzelpreis wenn NetPrice/Qty näher am DB-Preis liegt +if (!empty($items) && !empty($product_matches)) { + foreach ($items as &$item) { + $match = $product_matches[$item['artikelnr']] ?? null; + if ($match && !empty($match['stored_price']) && !empty($item['raw_netprice']) && $item['menge'] > 0) { + // Zwei Kandidaten für Stückpreis + $price_from_pricebasis = $item['einzelpreis']; // NetPrice / PriceBasis + $price_from_qty = (float) $item['raw_netprice'] / (float) $item['menge']; // NetPrice / Qty + + // Welcher ist näher am DB-Preis? + $diff_pricebasis = abs($price_from_pricebasis - (float) $match['stored_price']); + $diff_qty = abs($price_from_qty - (float) $match['stored_price']); + + // Wenn Qty-basiert deutlich näher ist (mindestens 30% Unterschied), nutze das + if ($diff_qty < $diff_pricebasis * 0.7) { + $item['einzelpreis'] = $price_from_qty; + $item['preiseinheit'] = $item['menge']; // Korrektur: NetPrice gilt für Qty, nicht PriceBasis + $item['_price_heuristic'] = 'qty-based'; // Debug-Flag + } else { + $item['_price_heuristic'] = 'pricebasis-standard'; // Debug-Flag + } + } + } + unset($item); // Referenz aufheben +} + /* * Actions @@ -183,6 +210,53 @@ if ($action == 'create_order' && $user->hasRight('fournisseur', 'commande', 'cre } } +// Manuelle Preis-Aktualisierung +if ($action == 'update_prices' && $user->hasRight('produit', 'creer')) { + if (!verifCond(GETPOST('token', 'alpha') == newToken())) { + accessforbidden('Bad CSRF token'); + } + + if (getDolGlobalInt('IDSCONNECT_PRICE_UPDATE_ENABLED')) { + $threshold = (float) getDolGlobalString('IDSCONNECT_PRICE_UPDATE_THRESHOLD', '5'); + $update_prices = GETPOST('update_price', 'array'); // Array von rowids die aktualisiert werden sollen + $updated_count = 0; + + if (!empty($update_prices) && !empty($items)) { + foreach ($items as $item) { + if (!empty($item['artikelnr']) && isset($product_matches[$item['artikelnr']])) { + $match = $product_matches[$item['artikelnr']]; + $rowid = $match['fk_prod_fourn_price']; + + // Prüfen ob diese Position aktualisiert werden soll + if (in_array($rowid, $update_prices)) { + // Mindestmengen-Preis (Basis-Preis) updaten, nicht Stückpreis + $old_basis_price = (float) $match['debug_price']; + $new_basis_price = !empty($item['raw_netprice']) ? (float) $item['raw_netprice'] : ($item['einzelpreis'] * (!empty($item['preiseinheit']) ? $item['preiseinheit'] : 1)); + + if ($old_basis_price > 0 && $new_basis_price > 0) { + $sql = "UPDATE ".$db->prefix()."product_fournisseur_price"; + $sql .= " SET price = ".((float) $new_basis_price).", tms = NOW()"; + $sql .= " WHERE rowid = ".((int) $rowid); + if ($db->query($sql)) { + $updated_count++; + dol_syslog("IDS Connect: Preis für ".$item['artikelnr']." manuell aktualisiert (Basis-Preis): ".$old_basis_price." → ".$new_basis_price." (".sprintf('%+.1f%%', (($new_basis_price - $old_basis_price) / $old_basis_price) * 100).")", LOG_INFO); + } + } + } + } + } + + if ($updated_count > 0) { + setEventMessages($updated_count.' '.$langs->trans("IdsconnectPricesUpdated"), null, 'mesgs'); + } else { + setEventMessages($langs->trans("IdsconnectNoPricesSelected"), null, 'warnings'); + } + } else { + setEventMessages($langs->trans("IdsconnectNoPricesSelected"), null, 'warnings'); + } + } +} + /* * View @@ -218,9 +292,54 @@ print ' | Empfangen: '.dol_print_date($log->date_creation, 'dayhour'); print ' | Artikel: '.count($items).''; if ($matched_count > 0) { print ' | Produkte erkannt: '.$matched_count.'/'.count($items).''; +} else { + print ' | ⚠ Keine Produkte erkannt'; } print ''; +// DEBUG: Produkt-Matching +if ($user->admin && $matched_count == 0) { + print '
'; + print 'DEBUG: Produkt-Matching
'; + print 'Sonepar fk_soc: '.$supplier->fk_soc.'
'; + print 'Gesuchte Artikelnummern: '; + $search_refs = array(); + foreach ($items as $item) { + if (!empty($item['artikelnr'])) { + $search_refs[] = $item['artikelnr']; + } + } + print implode(', ', $search_refs).'
'; + print 'Gefundene Matches: '.count($product_matches).'
'; + + // Prüfen ob Artikel in DB existieren + if ($supplier->fk_soc > 0 && !empty($search_refs)) { + $sql = "SELECT ref_fourn, fk_product FROM ".$db->prefix()."product_fournisseur_price"; + $sql .= " WHERE fk_soc = ".((int) $supplier->fk_soc); + $sql .= " LIMIT 5"; + $resql = $db->query($sql); + if ($resql) { + $count = $db->num_rows($resql); + print 'Artikel für diesen Lieferanten in DB: '.$count.'+ Einträge
'; + if ($count > 0) { + print 'Beispiele: '; + while ($obj = $db->fetch_object($resql)) { + print $obj->ref_fourn.', '; + } + } + } + } + print '
'; +} + +// Formular für Preis-Updates +$has_price_changes = false; +$threshold = (float) getDolGlobalString('IDSCONNECT_PRICE_UPDATE_THRESHOLD', '5'); + +print '
'; +print ''; +print ''; + // Artikel-Tabelle print ''; print ''; @@ -230,6 +349,7 @@ print ''; print ''; print ''; print ''; +print ''; print ''; print ''; @@ -270,21 +390,66 @@ foreach ($items as $item) { print '
'.price($item['raw_netprice']).' / '.$item['preiseinheit'].' Stk'; } print ''; + // Preis-Abweichung + print ''; print ''; print ''; } // Summe print ''; -print ''; +print ''; print ''; print ''; print '
Dolibarr-Produkt'.$langs->trans("IdsconnectCartQty").''.$langs->trans("IdsconnectCartUnit").''.$langs->trans("IdsconnectCartUnitPrice").''.$langs->trans("IdsconnectPriceDeviation").''.$langs->trans("IdsconnectCartTotalPrice").'
'; + if ($match && !empty($match['stored_price']) && $match['stored_price'] > 0) { + // Stückpreis-Vergleich (einzelpreis bereits korrekt berechnet) + $shop_unit_price = (float) $item['einzelpreis']; + $db_unit_price = (float) $match['stored_price']; + + // DEBUG für Admin: Zeige Rohdaten zur Nachvollziehbarkeit + if ($user->admin) { + print '
'; + print 'Shop: '.price($item['raw_netprice']).' / '.$item['preiseinheit'].' Stk = '.price($shop_unit_price).'/Stk
'; + print 'DB: '.price($match['debug_price']).' / '.$match['debug_qty'].' Stk = '.price($db_unit_price).'/Stk
'; + print '
'; + } + + $deviation = (($shop_unit_price - $db_unit_price) / $db_unit_price) * 100; + $abs_deviation = abs($deviation); + // Farbcode: Grün ≤2%, Gelb 2-10%, Rot >10% + if ($abs_deviation <= 2) { + $color = '#28a745'; // Grün + } elseif ($abs_deviation <= 10) { + $color = '#ffc107'; // Gelb + } else { + $color = '#dc3545'; // Rot + } + print ''.sprintf('%+.1f%%', $deviation).''; + print '
DB: '.price($db_unit_price).'/Stk'; + + // Checkbox für Preis-Update (nur bei Abweichung über Schwellwert UND wenn Feature aktiviert) + if ($abs_deviation > $threshold && getDolGlobalInt('IDSCONNECT_PRICE_UPDATE_ENABLED') && $user->hasRight('produit', 'creer')) { + $has_price_changes = true; + print '
'; + print ' '; + } + } else { + print '-'; + } + print '
'.price($line_total).'
'.$langs->trans("Total").''.$langs->trans("Total").''.price($total).'
'; +print '
'; + // Aktionsbuttons print '
'; +// Preis-Update-Button (nur wenn es Änderungen gibt) +if ($has_price_changes) { + print ''.$langs->trans("IdsconnectUpdateSelectedPrices").''; +} + if ($user->hasRight('fournisseur', 'commande', 'creer') && $supplier->fk_soc > 0) { print ''.$langs->trans("IdsconnectCartCreateOrder").''; } elseif ($supplier->fk_soc <= 0) { diff --git a/class/idsconnect.class.php b/class/idsconnect.class.php index 01fb06a..ed6700b 100755 --- a/class/idsconnect.class.php +++ b/class/idsconnect.class.php @@ -575,22 +575,30 @@ class IdsConnect return $matches; } - $sql = "SELECT pfp.fk_product, pfp.rowid as fk_prod_fourn_price, pfp.ref_fourn, p.ref, p.label"; + $sql = "SELECT pfp.fk_product, pfp.rowid as fk_prod_fourn_price, pfp.ref_fourn, p.ref, p.label, pfp.price, pfp.quantity"; $sql .= " FROM ".$this->db->prefix()."product_fournisseur_price pfp"; $sql .= " JOIN ".$this->db->prefix()."product p ON p.rowid = pfp.fk_product"; $sql .= " WHERE pfp.fk_soc = ".((int) $fk_soc); $sql .= " AND pfp.ref_fourn IN (".implode(',', $in_list).")"; - $sql .= " AND p.status = 1"; + $sql .= " AND p.tosell = 1"; // Nur Produkte die verkauft werden können $resql = $this->db->query($sql); if ($resql) { while ($obj = $this->db->fetch_object($resql)) { + // Stückpreis berechnen (price / quantity = Preis pro Einheit) + $qty = (float) $obj->quantity; + if ($qty <= 0) $qty = 1; // Fallback + $unit_price = (float) $obj->price / $qty; + // Bei Duplikaten: neuester Eintrag gewinnt $matches[$obj->ref_fourn] = array( 'fk_product' => (int) $obj->fk_product, 'fk_prod_fourn_price' => (int) $obj->fk_prod_fourn_price, 'product_ref' => $obj->ref, 'product_label' => $obj->label, + 'stored_price' => $unit_price, // Stückpreis statt Gesamtpreis + 'debug_price' => (float) $obj->price, // Rohdaten für Debug + 'debug_qty' => $qty, ); } } diff --git a/core/modules/modIdsconnect.class.php b/core/modules/modIdsconnect.class.php index 0745501..5e0816f 100755 --- a/core/modules/modIdsconnect.class.php +++ b/core/modules/modIdsconnect.class.php @@ -59,7 +59,7 @@ class modIdsconnect extends DolibarrModules $this->editor_name = 'Alles Watt laeuft'; $this->editor_url = ''; - $this->version = '2.3'; + $this->version = '3.1'; $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); diff --git a/langs/de_DE/idsconnect.lang b/langs/de_DE/idsconnect.lang index a9231fc..5c7f8e0 100755 --- a/langs/de_DE/idsconnect.lang +++ b/langs/de_DE/idsconnect.lang @@ -149,6 +149,20 @@ IdsconnectTestModeActive = TESTMODUS AKTIV IdsconnectTestModeInfo = Alle Verbindungen gehen zum lokalen Mock-Server. Keine echten Bestellungen möglich. IdsconnectLiveModeWarning = LIVE-MODUS - Verbindungen gehen zum echten Großhandel! +# +# Preis-Verwaltung +# +IdsconnectPriceManagement = Preis-Verwaltung +IdsconnectPriceDeviation = Preis-Abweichung +IdsconnectPriceUpdateEnabled = Preis-Update bei WKE-Import aktivieren +IdsconnectPriceUpdateEnabledHelp = Zeigt Checkboxen bei Preisabweichungen im Warenkorb an. Sie können dann manuell auswählen, welche Preise aktualisiert werden sollen +IdsconnectPriceUpdateThreshold = Abweichungs-Schwellwert für Preis-Update +IdsconnectPriceUpdateThresholdHelp = Checkboxen werden nur angezeigt wenn die Abweichung diesen Prozentsatz übersteigt (Standard: 5%) +IdsconnectPricesUpdated = Preis(e) erfolgreich aktualisiert +IdsconnectNoPricesSelected = Keine Preise zur Aktualisierung ausgewählt +IdsconnectUpdatePrice = Preis aktualisieren +IdsconnectUpdateSelectedPrices = Ausgewählte Preise aktualisieren + # # Berechtigungen # diff --git a/langs/en_US/idsconnect.lang b/langs/en_US/idsconnect.lang index 5c02c49..46e9dcb 100755 --- a/langs/en_US/idsconnect.lang +++ b/langs/en_US/idsconnect.lang @@ -149,6 +149,20 @@ IdsconnectTestModeActive = TEST MODE ACTIVE IdsconnectTestModeInfo = All connections go to the local mock server. No real orders possible. IdsconnectLiveModeWarning = LIVE MODE - Connections go to the real wholesale shop! +# +# Price Management +# +IdsconnectPriceManagement = Price Management +IdsconnectPriceDeviation = Price Deviation +IdsconnectPriceUpdateEnabled = Enable price update on WKE import +IdsconnectPriceUpdateEnabledHelp = Shows checkboxes for price deviations in the cart. You can then manually select which prices should be updated +IdsconnectPriceUpdateThreshold = Deviation threshold for price update +IdsconnectPriceUpdateThresholdHelp = Checkboxes are only shown if the deviation exceeds this percentage (default: 5%) +IdsconnectPricesUpdated = price(s) successfully updated +IdsconnectNoPricesSelected = No prices selected for update +IdsconnectUpdatePrice = Update price +IdsconnectUpdateSelectedPrices = Update selected prices + # # Permissions # diff --git a/tab_supplierorder.php b/tab_supplierorder.php index 934b9ad..a3bfb40 100755 --- a/tab_supplierorder.php +++ b/tab_supplierorder.php @@ -141,6 +141,161 @@ if (!empty($matchingSuppliers)) { print '
'; } +// Preis-Vergleich für Bestellpositionen +if (!empty($matchingSuppliers) && count($object->lines) > 0 && getDolGlobalInt('IDSCONNECT_PRICE_UPDATE_ENABLED')) { + print '

'; + print '

'.$langs->trans("IdsconnectPriceManagement").'

'; + + // Aktuelle Preise aus product_fournisseur_price laden + $price_data = array(); + foreach ($object->lines as $line) { + if ($line->fk_product > 0) { + // Produkt ist verknüpft → Preis aus product_fournisseur_price holen + $sql = "SELECT pfp.price, pfp.quantity, pfp.ref_fourn"; + $sql .= " FROM ".$db->prefix()."product_fournisseur_price pfp"; + $sql .= " WHERE pfp.fk_product = ".((int) $line->fk_product); + $sql .= " AND pfp.fk_soc = ".((int) $object->socid); + $sql .= " ORDER BY pfp.tms DESC LIMIT 1"; // Neuesten Preis + + $resql = $db->query($sql); + if ($resql && $obj = $db->fetch_object($resql)) { + // Stückpreis berechnen (price / qty) + $price_qty = (float) $obj->quantity; + if ($price_qty <= 0) $price_qty = 1; + $unit_price = (float) $obj->price / $price_qty; + + $price_data[$line->id] = array( + 'current_price' => $unit_price, // Stückpreis statt Gesamtpreis + 'ref_fourn' => $obj->ref_fourn, + 'order_price' => (float) $line->subprice, + 'qty' => $line->qty, + 'fk_product' => $line->fk_product + ); + } + } elseif (!empty($line->ref_supplier)) { + // Freitext → Preis über ref_supplier suchen + $sql = "SELECT pfp.price, pfp.quantity, pfp.fk_product, p.ref, p.label"; + $sql .= " FROM ".$db->prefix()."product_fournisseur_price pfp"; + $sql .= " JOIN ".$db->prefix()."product p ON p.rowid = pfp.fk_product"; + $sql .= " WHERE pfp.ref_fourn = '".$db->escape($line->ref_supplier)."'"; + $sql .= " AND pfp.fk_soc = ".((int) $object->socid); + $sql .= " AND p.tosell = 1"; + $sql .= " ORDER BY pfp.tms DESC LIMIT 1"; + + $resql = $db->query($sql); + if ($resql && $obj = $db->fetch_object($resql)) { + // Stückpreis berechnen (price / qty) + $price_qty = (float) $obj->quantity; + if ($price_qty <= 0) $price_qty = 1; + $unit_price = (float) $obj->price / $price_qty; + + $price_data[$line->id] = array( + 'current_price' => $unit_price, // Stückpreis statt Gesamtpreis + 'ref_fourn' => $line->ref_supplier, + 'order_price' => (float) $line->subprice, + 'qty' => $line->qty, + 'fk_product' => (int) $obj->fk_product, + 'product_ref' => $obj->ref, + 'product_label' => $obj->label, + 'is_freetext' => true + ); + } + } + } + + // Nur Positionen mit Abweichungen anzeigen + $threshold = (float) getDolGlobalString('IDSCONNECT_PRICE_UPDATE_THRESHOLD', '5'); + $deviations = array(); + foreach ($price_data as $line_id => $data) { + if ($data['current_price'] > 0 && $data['order_price'] > 0) { + $deviation = (($data['order_price'] - $data['current_price']) / $data['current_price']) * 100; + if (abs($deviation) > $threshold) { + $deviations[$line_id] = array_merge($data, array('deviation' => $deviation)); + } + } + } + + if (!empty($deviations)) { + print '
'; + print ''.count($deviations).' Position(en) mit Preisabweichung > '.$threshold.'%'; + print '
'; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + foreach ($deviations as $line_id => $data) { + // Zeile finden + $line = null; + foreach ($object->lines as $l) { + if ($l->id == $line_id) { + $line = $l; + break; + } + } + if (!$line) continue; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } + print '
'.$langs->trans("IdsconnectCartArticleNr").''.$langs->trans("IdsconnectCartDescription").''.$langs->trans("Qty").''.$langs->trans("IdsconnectCartUnitPrice").' (Bestellung)'.$langs->trans("IdsconnectCartUnitPrice").' (Aktuell)'.$langs->trans("IdsconnectPriceDeviation").'
'.$data['ref_fourn'].''; + if (!empty($data['is_freetext'])) { + print '! '; + } + print $line->desc; + if (!empty($data['product_ref'])) { + print '
'.$data['product_ref'].' - '.$data['product_label'].''; + } + print '
'.$line->qty.''.price($data['order_price']).''.price($data['current_price']).''; + $abs_deviation = abs($data['deviation']); + if ($abs_deviation <= 2) { + $color = '#28a745'; + } elseif ($abs_deviation <= 10) { + $color = '#ffc107'; + } else { + $color = '#dc3545'; + } + print ''.sprintf('%+.1f%%', $data['deviation']).''; + print ''; + if (!empty($data['is_freetext']) && $user->hasRight('produit', 'creer')) { + print 'Freitext'; + } + print '
'; + + if (!empty($deviations)) { + $has_freetext = false; + foreach ($deviations as $data) { + if (!empty($data['is_freetext'])) { + $has_freetext = true; + break; + } + } + if ($has_freetext) { + print '
'; + print 'Hinweis: Freitext-Positionen können nicht automatisch aktualisiert werden. '; + print 'Bitte verknüpfen Sie diese Artikel zuerst mit Dolibarr-Produkten.'; + print '
'; + } + } + } else { + print '
Keine signifikanten Preisabweichungen gefunden (Schwellwert: '.$threshold.'%)
'; + } +} + print dol_get_fiche_end(); llxFooter();