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>
302 lines
10 KiB
PHP
Executable file
302 lines
10 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* 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/tab_supplierorder.php
|
|
* \ingroup idsconnect
|
|
* \brief IDS Connect Tab in Lieferantenbestellungen
|
|
*/
|
|
|
|
// 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/lib/fourn.lib.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"));
|
|
|
|
if (!$user->hasRight('idsconnect', 'use')) {
|
|
accessforbidden();
|
|
}
|
|
|
|
$id = GETPOSTINT('id');
|
|
|
|
// Bestellung laden
|
|
$object = new CommandeFournisseur($db);
|
|
$object->fetch($id);
|
|
$object->fetch_thirdparty();
|
|
|
|
/*
|
|
* View
|
|
*/
|
|
|
|
llxHeader('', $langs->trans("IdsConnectTab"), '', '', 0, 0, '', '', '', 'mod-idsconnect page-tab_supplierorder');
|
|
|
|
// Tabs der Lieferantenbestellung
|
|
$head = ordersupplier_prepare_head($object);
|
|
print dol_get_fiche_head($head, 'idsconnect', $langs->trans("SupplierOrder"), -1, 'order');
|
|
|
|
// Bestellungs-Info
|
|
print '<table class="border centpercent tableforfield">';
|
|
print '<tr><td class="titlefield">'.$langs->trans("Ref").'</td><td>'.$object->getNomUrl(1).'</td></tr>';
|
|
print '<tr><td>'.$langs->trans("Supplier").'</td><td>'.$object->thirdparty->getNomUrl(1).'</td></tr>';
|
|
print '</table>';
|
|
|
|
print '<br>';
|
|
|
|
idsconnectShowTestModeBanner();
|
|
|
|
// Passende Großhändler für diesen Lieferanten finden
|
|
$supplierObj = new IdsSupplier($db);
|
|
$allSuppliers = $supplierObj->fetchAll(1);
|
|
$matchingSuppliers = array();
|
|
|
|
if (is_array($allSuppliers)) {
|
|
foreach ($allSuppliers as $sup) {
|
|
// Großhändler die mit diesem Dolibarr-Lieferanten verknüpft sind
|
|
if ($sup->fk_soc == $object->socid) {
|
|
$matchingSuppliers[] = $sup;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($matchingSuppliers)) {
|
|
print '<h3>'.$langs->trans("IdsconnectSuppliers").'</h3>';
|
|
|
|
foreach ($matchingSuppliers as $sup) {
|
|
print '<div class="idsconnect-supplier-card">';
|
|
print '<div class="supplier-name">'.htmlspecialchars($sup->label).' <span class="opacitymedium">('.htmlspecialchars($sup->ref).')</span></div>';
|
|
print '<div class="supplier-url">'.htmlspecialchars($sup->ids_url).' | IDS '.htmlspecialchars($sup->ids_version).'</div>';
|
|
|
|
if ($sup->testmode) {
|
|
print ' <span class="badge badge-warning">Testmodus</span>';
|
|
}
|
|
|
|
print '<div class="idsconnect-action-buttons" style="margin-top:10px;">';
|
|
|
|
// Shop öffnen (WKE)
|
|
print '<a class="butAction" href="'.DOL_URL_ROOT.'/custom/idsconnect/launch.php?supplier_id='.$sup->id.'&ids_action=WKE&token='.newToken().'" target="_blank">';
|
|
print $langs->trans("IdsconnectOpenShop");
|
|
print '</a>';
|
|
|
|
// Warenkorb aus Bestellung senden (WKS)
|
|
if (count($object->lines) > 0) {
|
|
print '<a class="butAction" href="'.DOL_URL_ROOT.'/custom/idsconnect/launch.php?supplier_id='.$sup->id.'&ids_action=WKS&fk_commande='.$object->id.'&token='.newToken().'" target="_blank">';
|
|
print $langs->trans("IdsconnectSendCart");
|
|
print '</a>';
|
|
}
|
|
|
|
print '</div></div>';
|
|
}
|
|
} else {
|
|
print '<div class="opacitymedium">';
|
|
print 'Kein IDS Connect Großhändler für diesen Lieferanten konfiguriert.<br>';
|
|
if ($user->hasRight('idsconnect', 'config')) {
|
|
print '<a href="'.DOL_URL_ROOT.'/custom/idsconnect/supplier_card.php?action=create&fk_soc='.$object->socid.'">';
|
|
print $langs->trans("IdsconnectNewSupplier");
|
|
print '</a>';
|
|
}
|
|
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();
|
|
$db->close();
|