* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. */ /** * \file idsconnect/cart_review.php * \ingroup idsconnect * \brief Empfangenen Warenkorb prüfen und als Lieferantenbestellung übernehmen */ // Dolibarr laden $res = 0; if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; } $tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1; while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; } if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; } if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; } if (!$res && file_exists("../main.inc.php")) { $res = @include "../main.inc.php"; } if (!$res && file_exists("../../main.inc.php")) { $res = @include "../../main.inc.php"; } if (!$res) { die("Include of main fails"); } require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php'; dol_include_once('/idsconnect/class/idslog.class.php'); dol_include_once('/idsconnect/class/idssupplier.class.php'); dol_include_once('/idsconnect/class/idsconnect.class.php'); dol_include_once('/idsconnect/lib/idsconnect.lib.php'); /** * @var Conf $conf * @var DoliDB $db * @var Translate $langs * @var User $user */ $langs->loadLangs(array("idsconnect@idsconnect", "orders", "bills")); if (!$user->hasRight('idsconnect', 'use')) { accessforbidden(); } $log_id = GETPOSTINT('log_id'); $action = GETPOST('action', 'aZ09'); // Log laden $log = new IdsLog($db); if ($log_id > 0) { $log->fetch($log_id); } // Großhändler laden $supplier = new IdsSupplier($db); if ($log->fk_supplier > 0) { $supplier->fetch($log->fk_supplier); } // Artikel immer aus dem XML neu parsen (nutzt aktuelle Preis-Logik) $idsconnect = new IdsConnect($db); $items = array(); if (!empty($log->cart_xml)) { $items = $idsconnect->parseCartXml($log->cart_xml); if ($items === false) { $items = array(); } } // Fallback: aus response_data wenn kein XML vorhanden if (empty($items) && !empty($log->response_data)) { $response = json_decode($log->response_data, true); if (!empty($response['items'])) { $items = $response['items']; } } // Produktzuordnung: Lieferantenreferenzen gegen Dolibarr-Produkte matchen $product_matches = array(); if (!empty($items) && $supplier->fk_soc > 0) { $ref_list = array(); foreach ($items as $item) { if (!empty($item['artikelnr'])) { $ref_list[] = $item['artikelnr']; } } if (!empty($ref_list)) { $product_matches = $idsconnect->matchProducts($ref_list, $supplier->fk_soc); } } // 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 */ // Lieferantenbestellung erstellen if ($action == 'create_order' && $user->hasRight('fournisseur', 'commande', 'creer')) { if (!verifCond(GETPOST('token', 'alpha') == newToken())) { accessforbidden('Bad CSRF token'); } if ($supplier->fk_soc > 0 && !empty($items)) { $order = new CommandeFournisseur($db); $order->socid = $supplier->fk_soc; $order->note_private = 'Erstellt via IDS Connect aus Warenkorb vom '.dol_print_date($log->date_creation, 'dayhour').' ('.$supplier->label.')'; $db->begin(); $order_id = $order->create($user); if ($order_id > 0) { $line_errors = 0; foreach ($items as $item) { $vat_rate = !empty($item['mwst_satz']) ? $item['mwst_satz'] : 19; // Produkt-Match prüfen $fk_product = 0; $fk_prod_fourn_price = 0; if (!empty($item['artikelnr']) && isset($product_matches[$item['artikelnr']])) { $match = $product_matches[$item['artikelnr']]; $fk_product = $match['fk_product']; $fk_prod_fourn_price = $match['fk_prod_fourn_price']; } $result = $order->addline( $item['bezeichnung'], // desc $item['einzelpreis'], // pu_ht (Stückpreis) $item['menge'], // qty $vat_rate, // txtva 0, // txlocaltax1 0, // txlocaltax2 $fk_product, // fk_product (Dolibarr-Produkt oder 0) $fk_prod_fourn_price, // fk_prod_fourn_price $item['artikelnr'], // ref_supplier 0, // remise_percent 'HT' // price_base_type ); if ($result < 0) { $line_errors++; } } if ($line_errors == 0) { $db->commit(); // Log aktualisieren $log->updateStatus('success', $log->response_data); $log->updateCart($log->cart_xml, $order_id); setEventMessages($langs->trans("IdsconnectCartImported"), null, 'mesgs'); header('Location: '.DOL_URL_ROOT.'/fourn/commande/card.php?id='.$order_id); exit; } else { $db->rollback(); setEventMessages('Fehler beim Erstellen der Bestellpositionen', null, 'errors'); } } else { $db->rollback(); setEventMessages('Fehler beim Erstellen der Bestellung: '.$order->error, null, 'errors'); } } else { if ($supplier->fk_soc <= 0) { setEventMessages('Großhändler hat keinen verknüpften Dolibarr-Lieferanten. Bitte zuerst in der Großhändler-Konfiguration zuweisen.', null, 'errors'); } } } // 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 */ $form = new Form($db); llxHeader('', $langs->trans("IdsconnectCartReview"), '', '', 0, 0, '', '', '', 'mod-idsconnect page-cart_review'); print load_fiche_titre($langs->trans("IdsconnectCartReviewTitle"), '', 'fa-shopping-cart'); idsconnectShowTestModeBanner(); if (empty($items)) { print '
'.$langs->trans("IdsconnectCartEmpty").'
'; print '
'.$langs->trans("Back").''; llxFooter(); $db->close(); exit; } // Info-Banner $matched_count = 0; foreach ($items as $item) { if (!empty($item['artikelnr']) && isset($product_matches[$item['artikelnr']])) { $matched_count++; } } print '
'; print ''.$langs->trans("IdsconnectCartReviewInfo").'
'; print 'Großhändler: '.htmlspecialchars($supplier->label ?: '-').''; 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 ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; $total = 0; foreach ($items as $item) { $line_total = $item['gesamtpreis'] ?: ($item['menge'] * $item['einzelpreis']); $total += $line_total; // Produkt-Match für Anzeige $match = null; if (!empty($item['artikelnr']) && isset($product_matches[$item['artikelnr']])) { $match = $product_matches[$item['artikelnr']]; } print ''; print ''; print ''; // Dolibarr-Produkt print ''; print ''; print ''; print ''; // Preis-Abweichung print ''; print ''; print ''; } // Summe print ''; print ''; print ''; print ''; print '
'.$langs->trans("IdsconnectCartArticleNr").''.$langs->trans("IdsconnectCartDescription").'Dolibarr-Produkt'.$langs->trans("IdsconnectCartQty").''.$langs->trans("IdsconnectCartUnit").''.$langs->trans("IdsconnectCartUnitPrice").''.$langs->trans("IdsconnectPriceDeviation").''.$langs->trans("IdsconnectCartTotalPrice").'
'.htmlspecialchars($item['artikelnr']).''.htmlspecialchars($item['bezeichnung']); if (!empty($item['hersteller'])) { print '
'.htmlspecialchars($item['hersteller']).''; } print '
'; if ($match) { print ''; print img_picto('', 'product', 'class="paddingright"'); print htmlspecialchars($match['product_ref']).''; print '
'.htmlspecialchars($match['product_label']).''; } else { print 'Freitext'; } print '
'.($item['menge']).''.htmlspecialchars($item['einheit']).''.price($item['einzelpreis']); // Preiseinheit-Info anzeigen wenn vorhanden (z.B. "Preis/100") if (!empty($item['preiseinheit']) && $item['preiseinheit'] > 1) { print '
'.price($item['raw_netprice']).' / '.$item['preiseinheit'].' Stk'; } print '
'; 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").''.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) { print ''.$langs->trans("IdsconnectCartCreateOrder").''; } print ''.$langs->trans("Back").''; print '
'; // XML-Details (aufklappbar) if (!empty($log->cart_xml)) { print '
'; print '
'; print 'XML-Rohdaten anzeigen'; print '
'; print htmlspecialchars($log->cart_xml); print '
'; print '
'; } llxFooter(); $db->close();