IDS Connect v3.1 - Preis-Vergleich & Produktmatching-Fix
Kritische Bugfixes: - FIX: Produktmatching korrigiert (qty→quantity, status→tosell in SQL) Vorher wurden alle Artikel als Freitext importiert, jetzt korrekte Verknüpfung mit Dolibarr-Produkten über product_fournisseur_price Neue Features: - FEAT: Sonepar NetPrice-Heuristik implementiert Erkennt automatisch ob NetPrice für PriceBasis (IDS-Standard) oder Order-Qty (Sonepar-Variante) ist. 2-Pass-Algorithmus vergleicht beide Interpretationen mit DB-Preis und wählt korrekte aus. Löst +100% Preisabweichungs-Problem bei Sonepar-Warenkörben. - FEAT: Preis-Vergleich in cart_review.php und tab_supplierorder.php Zeigt Abweichungen zwischen Shop-Preisen und gespeicherten Dolibarr-Preisen mit farbiger Markierung (rot >10%, gelb 2-10%, grün ≤2%) Manuelle Preis-Aktualisierung über Checkboxen (keine Automatik) - Admin-Option IDSCONNECT_PRICE_UPDATE_ENABLED mit konfigurierbarem Schwellwert - Übersetzungen für de_DE und en_US erweitert Dateien: - class/idsconnect.class.php: matchProducts() qty→quantity Fix - cart_review.php: Heuristik + Preis-Vergleich UI - tab_supplierorder.php: Preis-Vergleich für Bestellungen - admin/setup.php: Neue Preis-Update-Optionen - CHANGELOG.md: Dokumentation v2.9 und v3.1 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cf1b2815fc
commit
f1e5a47674
8 changed files with 421 additions and 23 deletions
56
CHANGELOG.md
56
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)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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 '<td>';
|
|||
print $form->selectyesno('IDSCONNECT_LOG_ENABLED', getDolGlobalInt('IDSCONNECT_LOG_ENABLED', 1), 1);
|
||||
print '</td></tr>';
|
||||
|
||||
// Überschrift: Preis-Verwaltung
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("IdsconnectPriceManagement").'</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Preis-Update aktivieren
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("IdsconnectPriceUpdateEnabled").'</td>';
|
||||
print '<td>';
|
||||
print $form->selectyesno('IDSCONNECT_PRICE_UPDATE_ENABLED', getDolGlobalInt('IDSCONNECT_PRICE_UPDATE_ENABLED', 0), 1);
|
||||
print ' <span class="opacitymedium">'.$langs->trans("IdsconnectPriceUpdateEnabledHelp").'</span>';
|
||||
print '</td></tr>';
|
||||
|
||||
// Preis-Update Schwellwert
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("IdsconnectPriceUpdateThreshold").'</td>';
|
||||
print '<td>';
|
||||
print '<input type="number" name="IDSCONNECT_PRICE_UPDATE_THRESHOLD" value="'.getDolGlobalString('IDSCONNECT_PRICE_UPDATE_THRESHOLD', '5').'" min="0" max="100" step="0.1" class="width100">';
|
||||
print ' % <span class="opacitymedium">'.$langs->trans("IdsconnectPriceUpdateThresholdHelp").'</span>';
|
||||
print '</td></tr>';
|
||||
|
||||
// Überschrift: WKS-Sicherheit
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="2">'.$langs->trans("IdsconnectWksSecuritySettings").'</td>';
|
||||
|
|
|
|||
167
cart_review.php
167
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: <strong>'.count($items).'</strong>';
|
||||
if ($matched_count > 0) {
|
||||
print ' | Produkte erkannt: <strong>'.$matched_count.'/'.count($items).'</strong>';
|
||||
} else {
|
||||
print ' | <span style="color:orange;">⚠ Keine Produkte erkannt</span>';
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
// DEBUG: Produkt-Matching
|
||||
if ($user->admin && $matched_count == 0) {
|
||||
print '<div class="warning">';
|
||||
print '<strong>DEBUG: Produkt-Matching</strong><br>';
|
||||
print 'Sonepar fk_soc: <strong>'.$supplier->fk_soc.'</strong><br>';
|
||||
print 'Gesuchte Artikelnummern: <strong>';
|
||||
$search_refs = array();
|
||||
foreach ($items as $item) {
|
||||
if (!empty($item['artikelnr'])) {
|
||||
$search_refs[] = $item['artikelnr'];
|
||||
}
|
||||
}
|
||||
print implode(', ', $search_refs).'</strong><br>';
|
||||
print 'Gefundene Matches: <strong>'.count($product_matches).'</strong><br>';
|
||||
|
||||
// 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: <strong>'.$count.'+ Einträge</strong><br>';
|
||||
if ($count > 0) {
|
||||
print 'Beispiele: ';
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
print $obj->ref_fourn.', ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Formular für Preis-Updates
|
||||
$has_price_changes = false;
|
||||
$threshold = (float) getDolGlobalString('IDSCONNECT_PRICE_UPDATE_THRESHOLD', '5');
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'?log_id='.$log->id.'" name="price_update_form">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="update_prices">';
|
||||
|
||||
// Artikel-Tabelle
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
|
|
@ -230,6 +349,7 @@ print '<th>Dolibarr-Produkt</th>';
|
|||
print '<th class="right">'.$langs->trans("IdsconnectCartQty").'</th>';
|
||||
print '<th>'.$langs->trans("IdsconnectCartUnit").'</th>';
|
||||
print '<th class="right">'.$langs->trans("IdsconnectCartUnitPrice").'</th>';
|
||||
print '<th class="right">'.$langs->trans("IdsconnectPriceDeviation").'</th>';
|
||||
print '<th class="right">'.$langs->trans("IdsconnectCartTotalPrice").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
|
|
@ -270,21 +390,66 @@ foreach ($items as $item) {
|
|||
print '<br><span class="opacitymedium" style="font-size:0.85em">'.price($item['raw_netprice']).' / '.$item['preiseinheit'].' Stk</span>';
|
||||
}
|
||||
print '</td>';
|
||||
// Preis-Abweichung
|
||||
print '<td class="right">';
|
||||
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 '<div style="font-size:0.7em; color:#666; text-align:left;">';
|
||||
print 'Shop: '.price($item['raw_netprice']).' / '.$item['preiseinheit'].' Stk = '.price($shop_unit_price).'/Stk<br>';
|
||||
print 'DB: '.price($match['debug_price']).' / '.$match['debug_qty'].' Stk = '.price($db_unit_price).'/Stk<br>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
$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 '<span style="color:'.$color.'; font-weight:bold;">'.sprintf('%+.1f%%', $deviation).'</span>';
|
||||
print '<br><span class="opacitymedium" style="font-size:0.85em">DB: '.price($db_unit_price).'/Stk</span>';
|
||||
|
||||
// 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 '<br><input type="checkbox" name="update_price[]" value="'.$match['fk_prod_fourn_price'].'" id="price_'.$match['fk_prod_fourn_price'].'">';
|
||||
print ' <label for="price_'.$match['fk_prod_fourn_price'].'" style="font-size:0.85em; cursor:pointer;">'.$langs->trans("IdsconnectUpdatePrice").'</label>';
|
||||
}
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td class="right">'.price($line_total).'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Summe
|
||||
print '<tr class="liste_total">';
|
||||
print '<td colspan="6" class="right"><strong>'.$langs->trans("Total").'</strong></td>';
|
||||
print '<td colspan="7" class="right"><strong>'.$langs->trans("Total").'</strong></td>';
|
||||
print '<td class="right"><strong>'.price($total).'</strong></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print '</form>';
|
||||
|
||||
// Aktionsbuttons
|
||||
print '<div class="tabsAction">';
|
||||
|
||||
// Preis-Update-Button (nur wenn es Änderungen gibt)
|
||||
if ($has_price_changes) {
|
||||
print '<a class="butAction" href="javascript:document.price_update_form.submit();">'.$langs->trans("IdsconnectUpdateSelectedPrices").'</a>';
|
||||
}
|
||||
|
||||
if ($user->hasRight('fournisseur', 'commande', 'creer') && $supplier->fk_soc > 0) {
|
||||
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?log_id='.$log->id.'&action=create_order&token='.newToken().'">'.$langs->trans("IdsconnectCartCreateOrder").'</a>';
|
||||
} elseif ($supplier->fk_soc <= 0) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -141,6 +141,161 @@ if (!empty($matchingSuppliers)) {
|
|||
print '</div>';
|
||||
}
|
||||
|
||||
// Preis-Vergleich für Bestellpositionen
|
||||
if (!empty($matchingSuppliers) && count($object->lines) > 0 && getDolGlobalInt('IDSCONNECT_PRICE_UPDATE_ENABLED')) {
|
||||
print '<br><br>';
|
||||
print '<h3>'.$langs->trans("IdsconnectPriceManagement").'</h3>';
|
||||
|
||||
// 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 '<div class="info">';
|
||||
print '<strong>'.count($deviations).' Position(en) mit Preisabweichung > '.$threshold.'%</strong>';
|
||||
print '</div>';
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("IdsconnectCartArticleNr").'</th>';
|
||||
print '<th>'.$langs->trans("IdsconnectCartDescription").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Qty").'</th>';
|
||||
print '<th class="right">'.$langs->trans("IdsconnectCartUnitPrice").' (Bestellung)</th>';
|
||||
print '<th class="right">'.$langs->trans("IdsconnectCartUnitPrice").' (Aktuell)</th>';
|
||||
print '<th class="right">'.$langs->trans("IdsconnectPriceDeviation").'</th>';
|
||||
print '<th></th>';
|
||||
print '</tr>';
|
||||
|
||||
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 '<tr class="oddeven">';
|
||||
print '<td>'.$data['ref_fourn'].'</td>';
|
||||
print '<td>';
|
||||
if (!empty($data['is_freetext'])) {
|
||||
print '<span class="badge badge-warning" title="Freitext-Position">!</span> ';
|
||||
}
|
||||
print $line->desc;
|
||||
if (!empty($data['product_ref'])) {
|
||||
print '<br><span class="opacitymedium">'.$data['product_ref'].' - '.$data['product_label'].'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td class="right">'.$line->qty.'</td>';
|
||||
print '<td class="right">'.price($data['order_price']).'</td>';
|
||||
print '<td class="right">'.price($data['current_price']).'</td>';
|
||||
print '<td class="right">';
|
||||
$abs_deviation = abs($data['deviation']);
|
||||
if ($abs_deviation <= 2) {
|
||||
$color = '#28a745';
|
||||
} elseif ($abs_deviation <= 10) {
|
||||
$color = '#ffc107';
|
||||
} else {
|
||||
$color = '#dc3545';
|
||||
}
|
||||
print '<span style="color:'.$color.'; font-weight:bold;">'.sprintf('%+.1f%%', $data['deviation']).'</span>';
|
||||
print '</td>';
|
||||
print '<td class="right">';
|
||||
if (!empty($data['is_freetext']) && $user->hasRight('produit', 'creer')) {
|
||||
print '<a class="butActionRefused" href="#" title="Position ist Freitext - kann nicht automatisch aktualisiert werden">Freitext</a>';
|
||||
}
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
print '</table>';
|
||||
|
||||
if (!empty($deviations)) {
|
||||
$has_freetext = false;
|
||||
foreach ($deviations as $data) {
|
||||
if (!empty($data['is_freetext'])) {
|
||||
$has_freetext = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($has_freetext) {
|
||||
print '<br><div class="warning">';
|
||||
print '<strong>Hinweis:</strong> Freitext-Positionen können nicht automatisch aktualisiert werden. ';
|
||||
print 'Bitte verknüpfen Sie diese Artikel zuerst mit Dolibarr-Produkten.';
|
||||
print '</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print '<div class="opacitymedium">Keine signifikanten Preisabweichungen gefunden (Schwellwert: '.$threshold.'%)</div>';
|
||||
}
|
||||
}
|
||||
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
|
|
|
|||
Loading…
Reference in a new issue