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 '';
+
// 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 ''.$langs->trans("IdsconnectCartArticleNr").' ';
+ print ''.$langs->trans("IdsconnectCartDescription").' ';
+ print ''.$langs->trans("Qty").' ';
+ print ''.$langs->trans("IdsconnectCartUnitPrice").' (Bestellung) ';
+ print ''.$langs->trans("IdsconnectCartUnitPrice").' (Aktuell) ';
+ print ''.$langs->trans("IdsconnectPriceDeviation").' ';
+ 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 ''.$data['ref_fourn'].' ';
+ print '';
+ if (!empty($data['is_freetext'])) {
+ print '! ';
+ }
+ print $line->desc;
+ if (!empty($data['product_ref'])) {
+ print ''.$data['product_ref'].' - '.$data['product_label'].' ';
+ }
+ print ' ';
+ print ''.$line->qty.' ';
+ print ''.price($data['order_price']).' ';
+ print ''.price($data['current_price']).' ';
+ print '';
+ $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 ' ';
+ print '';
+ if (!empty($data['is_freetext']) && $user->hasRight('produit', 'creer')) {
+ print 'Freitext ';
+ }
+ print ' ';
+ print ' ';
+ }
+ 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();