Version 2.3: Preisvergleich und theoretischer Lagerbestand
- Preisvergleich zwischen Lieferanten mit prozentualem Unterschied - Günstigster-Lieferant-Badge (grünes Kürzel) in Kundenaufträgen - Preis-Indikator (Grün/Orange/Rot) in Lieferantenbestellungen - Lagerbestand-Anzeige: Physisch | Theoretisch (Dolibarr-Standard) - Tooltip mit HTML Entity für Zeilenumbrüche korrigiert - Verwendet Dolibarrs native stock_theorique Berechnung Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff52c0bece
commit
c976123d81
5 changed files with 407 additions and 97 deletions
|
|
@ -455,12 +455,13 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
/* Add other hook methods here... */
|
||||
|
||||
/**
|
||||
* Holt alle Lieferanten mit Shop-URL für ein Produkt
|
||||
* Holt ALLE Lieferanten für ein Produkt (auch ohne Shop-URL für Preisvergleich)
|
||||
*
|
||||
* @param int $fk_product Produkt-ID
|
||||
* @param bool $onlyWithShopUrl Nur Lieferanten mit Shop-URL zurückgeben
|
||||
* @return array Array mit Lieferanten-Daten, sortiert nach Preis aufsteigend
|
||||
*/
|
||||
private function getAllSuppliersForProduct($fk_product)
|
||||
private function getAllSuppliersForProduct($fk_product, $onlyWithShopUrl = true)
|
||||
{
|
||||
$suppliers = array();
|
||||
|
||||
|
|
@ -470,26 +471,42 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
pfp.price,
|
||||
pfp.quantity as min_qty,
|
||||
s.nom as supplier_name,
|
||||
s.name_alias as supplier_alias,
|
||||
se.shop_url
|
||||
FROM ".MAIN_DB_PREFIX."product_fournisseur_price pfp
|
||||
INNER JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = pfp.fk_soc
|
||||
LEFT JOIN ".MAIN_DB_PREFIX."societe_extrafields se ON se.fk_object = pfp.fk_soc
|
||||
WHERE pfp.fk_product = ".(int)$fk_product."
|
||||
AND se.shop_url IS NOT NULL
|
||||
AND se.shop_url != ''
|
||||
ORDER BY pfp.price ASC";
|
||||
WHERE pfp.fk_product = ".(int)$fk_product;
|
||||
|
||||
if ($onlyWithShopUrl) {
|
||||
$sql .= " AND se.shop_url IS NOT NULL AND se.shop_url != ''";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY pfp.price ASC";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$position = 0;
|
||||
$totalSuppliers = $this->db->num_rows($resql);
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$position++;
|
||||
// Kürzel: Erste 3 Zeichen des Alias oder Namens (Großbuchstaben)
|
||||
$shortName = !empty($obj->supplier_alias) ? $obj->supplier_alias : $obj->supplier_name;
|
||||
$shortCode = strtoupper(substr(preg_replace('/[^a-zA-Z0-9]/', '', $shortName), 0, 3));
|
||||
|
||||
$suppliers[] = array(
|
||||
'supplier_id' => $obj->supplier_id,
|
||||
'ref_fourn' => $obj->ref_fourn,
|
||||
'price' => $obj->price,
|
||||
'price' => (float)$obj->price,
|
||||
'min_qty' => $obj->min_qty,
|
||||
'supplier_name' => $obj->supplier_name,
|
||||
'shop_url' => rtrim($obj->shop_url, '/'),
|
||||
'full_url' => rtrim($obj->shop_url, '/') . '/' . $obj->ref_fourn
|
||||
'supplier_code' => $shortCode,
|
||||
'shop_url' => $obj->shop_url ? rtrim($obj->shop_url, '/') : '',
|
||||
'full_url' => $obj->shop_url ? rtrim($obj->shop_url, '/') . '/' . $obj->ref_fourn : '',
|
||||
'position' => $position,
|
||||
'total_suppliers' => $totalSuppliers,
|
||||
'is_cheapest' => ($position == 1),
|
||||
'is_most_expensive' => ($position == $totalSuppliers && $totalSuppliers > 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -498,46 +515,211 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
}
|
||||
|
||||
/**
|
||||
* Generiert HTML-Badge für Lagerbestand
|
||||
* Holt Lagerbestand-Daten aus Dolibarr (Physisch + Theoretisch)
|
||||
* Verwendet Dolibarrs native stock_theorique Berechnung
|
||||
*
|
||||
* 4 Zustände:
|
||||
* - ROT: stock <= 0 (Nicht verfügbar)
|
||||
* - ORANGE: stock < seuil_stock_alerte (Unter Mindestmenge)
|
||||
* - GRAU: stock < desiredstock (Unter Wunschmenge)
|
||||
* - GRÜN: stock >= desiredstock (Ausreichend)
|
||||
* @param int $fk_product Produkt-ID
|
||||
* @return array Array mit 'physical', 'theoretical'
|
||||
*/
|
||||
private function getStockData($fk_product)
|
||||
{
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
|
||||
$result = array(
|
||||
'physical' => 0,
|
||||
'theoretical' => 0
|
||||
);
|
||||
|
||||
$product = new Product($this->db);
|
||||
if ($product->fetch($fk_product) > 0) {
|
||||
// Physischer Bestand (stock_reel)
|
||||
$result['physical'] = (float)$product->stock_reel;
|
||||
|
||||
// Theoretischer Bestand - Dolibarrs native Berechnung laden
|
||||
$product->load_stock();
|
||||
$result['theoretical'] = (float)$product->stock_theorique;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die Preisposition eines Lieferanten für ein Produkt
|
||||
*
|
||||
* @param float $qtyStock Aktueller Lagerbestand
|
||||
* @param int $fk_product Produkt-ID
|
||||
* @param int $supplier_id Lieferanten-ID
|
||||
* @return array Array mit 'position', 'total', 'price_class' (cheapest/average/expensive)
|
||||
*/
|
||||
private function getSupplierPricePosition($fk_product, $supplier_id)
|
||||
{
|
||||
$result = array(
|
||||
'position' => 0,
|
||||
'total' => 0,
|
||||
'price_class' => 'unknown',
|
||||
'price' => 0,
|
||||
'min_price' => 0,
|
||||
'max_price' => 0,
|
||||
'percent_diff' => 0
|
||||
);
|
||||
|
||||
// Alle Lieferanten für dieses Produkt (auch ohne Shop-URL)
|
||||
$suppliers = $this->getAllSuppliersForProduct($fk_product, false);
|
||||
|
||||
if (empty($suppliers)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['total'] = count($suppliers);
|
||||
$result['min_price'] = $suppliers[0]['price'];
|
||||
$result['max_price'] = $suppliers[count($suppliers) - 1]['price'];
|
||||
|
||||
foreach ($suppliers as $sup) {
|
||||
if ($sup['supplier_id'] == $supplier_id) {
|
||||
$result['position'] = $sup['position'];
|
||||
$result['price'] = $sup['price'];
|
||||
|
||||
// Prozentuale Differenz zum günstigsten
|
||||
if ($result['min_price'] > 0 && $sup['price'] > $result['min_price']) {
|
||||
$result['percent_diff'] = round((($sup['price'] - $result['min_price']) / $result['min_price']) * 100, 1);
|
||||
}
|
||||
|
||||
// Preisklasse bestimmen
|
||||
if ($sup['is_cheapest']) {
|
||||
$result['price_class'] = 'cheapest';
|
||||
} elseif ($sup['is_most_expensive']) {
|
||||
$result['price_class'] = 'expensive';
|
||||
} else {
|
||||
$result['price_class'] = 'average';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert HTML-Badge für Lagerbestand mit Physisch|Theoretisch Anzeige
|
||||
*
|
||||
* Format: [Physisch | Theoretisch] - Links physisch, rechts theoretisch (Dolibarr-Standard)
|
||||
* Farbe basiert auf theoretischem Bestand
|
||||
*
|
||||
* @param int $fk_product Produkt-ID
|
||||
* @param float $desiredQty Wunschbestand
|
||||
* @param float $alertQty Mindestbestand (seuil_stock_alerte)
|
||||
* @return string HTML-Badge
|
||||
*/
|
||||
private function getStockBadgeHtml($qtyStock, $desiredQty, $alertQty = 0)
|
||||
private function getStockBadgeHtml($fk_product, $desiredQty, $alertQty = 0)
|
||||
{
|
||||
if ($qtyStock <= 0) {
|
||||
// ROT: Nicht verfügbar (0 oder negativ)
|
||||
// Lagerbestand aus Dolibarr holen (verwendet stock_theorique)
|
||||
$stockData = $this->getStockData($fk_product);
|
||||
$physicalStock = $stockData['physical'];
|
||||
$theoreticalStock = $stockData['theoretical'];
|
||||
|
||||
// Farbe basiert auf theoretischem Bestand (verfügbar)
|
||||
if ($theoreticalStock <= 0) {
|
||||
$badgeClass = 'badge-danger';
|
||||
$icon = 'fa-times-circle';
|
||||
$tooltip = 'Nicht auf Lager';
|
||||
} elseif ($alertQty > 0 && $qtyStock < $alertQty) {
|
||||
// ORANGE: Unter Mindestmenge (Alarm-Schwelle)
|
||||
$tooltip = 'Nicht verfügbar';
|
||||
} elseif ($alertQty > 0 && $theoreticalStock < $alertQty) {
|
||||
$badgeClass = 'badge-warning';
|
||||
$icon = 'fa-exclamation-triangle';
|
||||
$tooltip = 'Unter Mindestmenge ('.(int)$alertQty.')';
|
||||
} elseif ($desiredQty > 0 && $qtyStock < $desiredQty) {
|
||||
// GRAU: Unter Wunschmenge
|
||||
} elseif ($desiredQty > 0 && $theoreticalStock < $desiredQty) {
|
||||
$badgeClass = 'badge-secondary';
|
||||
$icon = 'fa-box-open';
|
||||
$tooltip = 'Unter Wunschmenge ('.(int)$desiredQty.')';
|
||||
} else {
|
||||
// GRÜN: Ausreichend auf Lager
|
||||
$badgeClass = 'badge-success';
|
||||
$icon = 'fa-check-circle';
|
||||
$tooltip = 'Ausreichend auf Lager';
|
||||
$tooltip = 'Ausreichend verfügbar';
|
||||
}
|
||||
|
||||
$html = '<span class="badge '.$badgeClass.' classfortooltip" title="'.dol_escape_htmltag($tooltip).'" style="font-size: 11px;">';
|
||||
$html .= '<i class="fas '.$icon.'" style="margin-right: 4px;"></i>';
|
||||
$html .= number_format($qtyStock, 0, ',', '.');
|
||||
// Differenz berechnen (offene Aufträge/Lieferungen)
|
||||
$reserved = $physicalStock - $theoreticalStock;
|
||||
|
||||
// Erweiterter Tooltip mit Details - HTML Entity für Zeilenumbruch in title-Attribut
|
||||
$tooltipFull = $tooltip.' ';
|
||||
$tooltipFull .= 'Physisch: '.(int)$physicalStock.' ';
|
||||
$tooltipFull .= 'Theoretisch: '.(int)$theoreticalStock;
|
||||
if ($reserved != 0) {
|
||||
$tooltipFull .= ' Differenz: '.($reserved > 0 ? '+' : '').(int)$reserved;
|
||||
}
|
||||
|
||||
// Badge mit zwei Werten: Physisch | Theoretisch
|
||||
$html = '<span class="badge '.$badgeClass.' classfortooltip" title="'.$tooltipFull.'" style="font-size: 11px; white-space: nowrap;">';
|
||||
$html .= '<i class="fas '.$icon.'" style="margin-right: 3px;"></i>';
|
||||
$html .= '<span style="font-weight: bold;">'.(int)$physicalStock.'</span>';
|
||||
$html .= '<span style="opacity: 0.7; margin: 0 2px;">|</span>';
|
||||
$html .= '<span style="opacity: 0.8;">'.(int)$theoreticalStock.'</span>';
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Lieferanten-Badge für Kundenauftrag/Angebot
|
||||
* Zeigt Kürzel des günstigsten Lieferanten in grünem Badge
|
||||
*
|
||||
* @param array $suppliers Lieferanten-Array (sortiert nach Preis)
|
||||
* @return string HTML für Lieferanten-Badge
|
||||
*/
|
||||
private function getCheapestSupplierBadge($suppliers)
|
||||
{
|
||||
if (empty($suppliers)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$cheapest = $suppliers[0];
|
||||
$code = $cheapest['supplier_code'];
|
||||
$tooltip = 'Günstigster: '.$cheapest['supplier_name'].' ('.number_format($cheapest['price'], 2, ',', '.').' EUR)';
|
||||
|
||||
$html = '<span class="badge classfortooltip" style="background-color: #28a745; color: #fff; font-size: 9px; padding: 2px 4px; margin-left: 3px;" ';
|
||||
$html .= 'title="'.dol_escape_htmltag($tooltip).'">';
|
||||
$html .= dol_escape_htmltag($code);
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Preis-Indikator-Badge für Lieferantenbestellung
|
||||
* Grün = günstigster, Orange = Durchschnitt, Rot = teuerster
|
||||
*
|
||||
* @param int $fk_product Produkt-ID
|
||||
* @param int $supplier_id Lieferanten-ID der Bestellung
|
||||
* @return string HTML für Preis-Indikator
|
||||
*/
|
||||
private function getPriceIndicatorBadge($fk_product, $supplier_id)
|
||||
{
|
||||
$pricePos = $this->getSupplierPricePosition($fk_product, $supplier_id);
|
||||
|
||||
if ($pricePos['total'] <= 1) {
|
||||
// Nur ein Lieferant - kein Vergleich möglich
|
||||
return '';
|
||||
}
|
||||
|
||||
switch ($pricePos['price_class']) {
|
||||
case 'cheapest':
|
||||
$color = '#28a745'; // Grün
|
||||
$icon = 'fa-check';
|
||||
$tooltip = 'Günstigster Lieferant';
|
||||
break;
|
||||
case 'expensive':
|
||||
$color = '#dc3545'; // Rot
|
||||
$icon = 'fa-arrow-up';
|
||||
$tooltip = 'Teuerster Lieferant (+'.$pricePos['percent_diff'].'%)';
|
||||
break;
|
||||
default: // average
|
||||
$color = '#fd7e14'; // Orange
|
||||
$icon = 'fa-minus';
|
||||
$tooltip = 'Mittelpreis (+'.$pricePos['percent_diff'].'%)';
|
||||
}
|
||||
|
||||
$html = '<span class="classfortooltip" title="'.dol_escape_htmltag($tooltip).'" ';
|
||||
$html .= 'style="display: inline-block; width: 14px; height: 14px; border-radius: 50%; background-color: '.$color.'; ';
|
||||
$html .= 'text-align: center; line-height: 14px; margin-left: 4px;">';
|
||||
$html .= '<i class="fas '.$icon.'" style="color: #fff; font-size: 8px;"></i>';
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
|
|
@ -545,19 +727,25 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
|
||||
/**
|
||||
* Generiert Shop-Link HTML (Icon oder Modal-Trigger für mehrere Lieferanten)
|
||||
* Mit Preisvergleich und prozentualer Differenz
|
||||
*
|
||||
* @param array $suppliers Array der Lieferanten
|
||||
* @param int $currentSupplierId Aktueller Lieferant der Bestellung
|
||||
* @param array $suppliers Array der Lieferanten (mit Shop-URL)
|
||||
* @param int $currentSupplierId Aktueller Lieferant der Bestellung (0 für Kundenauftrag)
|
||||
* @param string $refFourn Lieferanten-Artikelnummer
|
||||
* @param int $lineId Zeilen-ID für eindeutige Modal-ID
|
||||
* @param int $fk_product Produkt-ID für vollständigen Preisvergleich
|
||||
* @return string HTML für Shop-Link
|
||||
*/
|
||||
private function getShopLinkHtml($suppliers, $currentSupplierId, $refFourn, $lineId)
|
||||
private function getShopLinkHtml($suppliers, $currentSupplierId, $refFourn, $lineId, $fk_product = 0)
|
||||
{
|
||||
if (empty($suppliers)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Alle Lieferanten für Preisvergleich holen (auch ohne Shop-URL)
|
||||
$allSuppliers = $fk_product ? $this->getAllSuppliersForProduct($fk_product, false) : $suppliers;
|
||||
$minPrice = !empty($allSuppliers) ? $allSuppliers[0]['price'] : 0;
|
||||
|
||||
// Fall 1: Nur ein Lieferant mit Shop-URL - direkter Link
|
||||
if (count($suppliers) == 1) {
|
||||
$supplier = $suppliers[0];
|
||||
|
|
@ -584,43 +772,61 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
$html .= '</a>';
|
||||
|
||||
// Modal-Container (versteckt)
|
||||
$html .= '<div id="'.$modalId.'" class="supplier-modal-content" style="display: none;" title="Lieferanten-Shops">';
|
||||
$html .= '<div id="'.$modalId.'" class="supplier-modal-content" style="display: none;" title="Lieferanten-Vergleich">';
|
||||
|
||||
$isFirst = true;
|
||||
foreach ($suppliers as $supplier) {
|
||||
$isCurrentSupplier = ($supplier['supplier_id'] == $currentSupplierId);
|
||||
|
||||
// Prozentuale Differenz zum günstigsten berechnen
|
||||
$percentDiff = 0;
|
||||
if ($minPrice > 0 && $supplier['price'] > $minPrice) {
|
||||
$percentDiff = round((($supplier['price'] - $minPrice) / $minPrice) * 100, 1);
|
||||
}
|
||||
|
||||
$rowStyle = '';
|
||||
if ($isCurrentSupplier) {
|
||||
$rowStyle = 'background-color: #e8f4fd;';
|
||||
if ($isFirst) {
|
||||
$rowStyle = 'background-color: #d4edda;'; // Hellgrün für günstigsten
|
||||
} elseif ($isCurrentSupplier) {
|
||||
$rowStyle = 'background-color: #e8f4fd;'; // Hellblau für aktuellen
|
||||
}
|
||||
|
||||
$html .= '<div class="supplier-modal-item" style="padding: 10px; border-bottom: 1px solid #eee; '.$rowStyle.'">';
|
||||
|
||||
// Lieferanten-Name mit Stern für günstigsten
|
||||
$html .= '<div style="font-weight: '.($isFirst ? 'bold' : 'normal').'; margin-bottom: 5px;">';
|
||||
$html .= '<div style="font-weight: '.($isFirst ? 'bold' : 'normal').'; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center;">';
|
||||
$html .= '<span>';
|
||||
if ($isFirst) {
|
||||
$html .= '<i class="fas fa-star" style="color: gold; margin-right: 5px;" title="Günstigster Lieferant"></i>';
|
||||
}
|
||||
$html .= dol_escape_htmltag($supplier['supplier_name']);
|
||||
if ($isCurrentSupplier) {
|
||||
$html .= ' <span style="font-size: 10px; color: #666;">(aktueller Lieferant)</span>';
|
||||
$html .= ' <span style="font-size: 10px; color: #666;">(aktuell)</span>';
|
||||
}
|
||||
$html .= '</span>';
|
||||
|
||||
// Prozent-Badge rechts
|
||||
if ($percentDiff > 0) {
|
||||
$diffColor = $percentDiff > 20 ? '#dc3545' : ($percentDiff > 10 ? '#fd7e14' : '#6c757d');
|
||||
$html .= '<span style="font-size: 10px; color: '.$diffColor.'; font-weight: bold;">+'.$percentDiff.'%</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
// Preis und Mindestmenge
|
||||
$html .= '<div style="font-size: 12px; color: #666; margin-bottom: 8px;">';
|
||||
$html .= '<strong>'.number_format($supplier['price'], 2, ',', '.').' EUR</strong>';
|
||||
$html .= ' · Mindestmenge: '.(int)$supplier['min_qty'].' Stk.';
|
||||
$html .= '<strong style="font-size: 13px;">'.number_format($supplier['price'], 2, ',', '.').' EUR</strong>';
|
||||
$html .= ' · ab '.(int)$supplier['min_qty'].' Stk.';
|
||||
$html .= ' · Art.-Nr: '.dol_escape_htmltag($supplier['ref_fourn']);
|
||||
$html .= '</div>';
|
||||
|
||||
// Shop-Link Button
|
||||
$html .= '<a href="'.dol_escape_htmltag($supplier['full_url']).'" ';
|
||||
$html .= 'target="supplier_'.$supplier['supplier_id'].'" ';
|
||||
$html .= 'class="button small" style="font-size: 11px; padding: 4px 10px;">';
|
||||
$html .= '<i class="fas fa-external-link-alt" style="margin-right: 5px;"></i>Im Shop öffnen';
|
||||
$html .= '</a>';
|
||||
// Shop-Link Button (nur wenn Shop-URL vorhanden)
|
||||
if (!empty($supplier['full_url'])) {
|
||||
$html .= '<a href="'.dol_escape_htmltag($supplier['full_url']).'" ';
|
||||
$html .= 'target="supplier_'.$supplier['supplier_id'].'" ';
|
||||
$html .= 'class="button small" style="font-size: 11px; padding: 4px 10px;">';
|
||||
$html .= '<i class="fas fa-external-link-alt" style="margin-right: 5px;"></i>Im Shop öffnen';
|
||||
$html .= '</a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
|
|
@ -748,18 +954,21 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
|
||||
$line = $parameters['line'];
|
||||
|
||||
// Lagerbestand, Wunschbestand und Mindestbestand abfragen
|
||||
$sqlStock = "SELECT stock, desiredstock, seuil_stock_alerte
|
||||
// Nur für Produkte verarbeiten (nicht für Freitext-Zeilen oder Dienstleistungen)
|
||||
if (empty($line->fk_product)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Wunschbestand und Mindestbestand abfragen
|
||||
$sqlStock = "SELECT desiredstock, seuil_stock_alerte
|
||||
FROM ".MAIN_DB_PREFIX."product
|
||||
WHERE rowid = ".(int)$line->fk_product;
|
||||
|
||||
$resStock = $this->db->query($sqlStock);
|
||||
$qtyStock = 0;
|
||||
$desiredQty = 0;
|
||||
$alertQty = 0;
|
||||
if ($resStock && $this->db->num_rows($resStock) > 0) {
|
||||
$objStock = $this->db->fetch_object($resStock);
|
||||
$qtyStock = (float) $objStock->stock;
|
||||
$desiredQty = (float) $objStock->desiredstock;
|
||||
$alertQty = (float) $objStock->seuil_stock_alerte;
|
||||
}
|
||||
|
|
@ -768,29 +977,37 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
if (!empty($object->socid) && $isSupplierOrder) {
|
||||
$fk_supplier = $object->socid;
|
||||
|
||||
// Alle Lieferanten für dieses Produkt abrufen
|
||||
$allSuppliers = $this->getAllSuppliersForProduct($line->fk_product);
|
||||
// Alle Lieferanten für dieses Produkt abrufen (mit Shop-URL)
|
||||
$allSuppliers = $this->getAllSuppliersForProduct($line->fk_product, true);
|
||||
|
||||
// Shop-Icon/Modal generieren
|
||||
// Shop-Icon/Modal generieren mit Produkt-ID für Preisvergleich
|
||||
$lineId = !empty($line->id) ? $line->id : uniqid();
|
||||
$shopLinkHtml = $this->getShopLinkHtml($allSuppliers, $fk_supplier, $line->ref_fourn, $lineId);
|
||||
$shopLinkHtml = $this->getShopLinkHtml($allSuppliers, $fk_supplier, $line->ref_fourn, $lineId, $line->fk_product);
|
||||
|
||||
// Lagerbestand-Badge generieren (4 Zustände: rot/orange/grau/grün)
|
||||
$stockBadgeHtml = $this->getStockBadgeHtml($qtyStock, $desiredQty, $alertQty);
|
||||
// Preis-Indikator für diesen Lieferanten (Grün/Orange/Rot)
|
||||
$priceIndicator = $this->getPriceIndicatorBadge($line->fk_product, $fk_supplier);
|
||||
|
||||
// Lagerbestand-Badge generieren (mit virtuellem Bestand)
|
||||
$stockBadgeHtml = $this->getStockBadgeHtml($line->fk_product, $desiredQty, $alertQty);
|
||||
|
||||
// Neue ref_fourn zusammenbauen:
|
||||
// [Shop-Icon feste Breite] [Lagerbestand feste Breite] [Artikel-Nummer]
|
||||
// [Shop-Icon] [Preis-Indikator] [Lagerbestand] [Artikel-Nummer]
|
||||
$newref = '<div style="display: inline-flex; align-items: center;">';
|
||||
|
||||
// Shop-Icon mit fester Breite (damit alle untereinander stehen)
|
||||
// Shop-Icon mit fester Breite
|
||||
$newref .= '<span class="supplier-shop-col" style="display: inline-block; width: 28px; text-align: center;">';
|
||||
if (!empty($shopLinkHtml)) {
|
||||
$newref .= $shopLinkHtml;
|
||||
}
|
||||
$newref .= '</span>';
|
||||
|
||||
// Lagerbestand-Badge mit fester Breite (rechtsbündig für gleichmäßige Ausrichtung)
|
||||
$newref .= '<span class="supplier-stock-col" style="display: inline-block; min-width: 55px; text-align: right; margin-right: 8px;">';
|
||||
// Preis-Indikator (Grün/Orange/Rot Punkt)
|
||||
if (!empty($priceIndicator)) {
|
||||
$newref .= '<span style="display: inline-block; width: 18px; text-align: center;">'.$priceIndicator.'</span>';
|
||||
}
|
||||
|
||||
// Lagerbestand-Badge (Virtuell|Physisch)
|
||||
$newref .= '<span class="supplier-stock-col" style="display: inline-block; min-width: 70px; text-align: right; margin-right: 8px;">';
|
||||
$newref .= $stockBadgeHtml;
|
||||
$newref .= '</span>';
|
||||
|
||||
|
|
@ -806,17 +1023,20 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
// NUR für Produkte, NICHT für Dienstleistungen
|
||||
if (!empty($line->fk_product) && $line->product_type == 0) {
|
||||
|
||||
// Alle Lieferanten für dieses Produkt abrufen
|
||||
$allSuppliers = $this->getAllSuppliersForProduct($line->fk_product);
|
||||
// Alle Lieferanten für dieses Produkt abrufen (mit Shop-URL)
|
||||
$allSuppliers = $this->getAllSuppliersForProduct($line->fk_product, true);
|
||||
|
||||
// Shop-Icon/Modal generieren (ohne aktuellen Lieferanten, da Kundenauftrag/Angebot)
|
||||
// Shop-Icon/Modal generieren mit Produkt-ID für Preisvergleich
|
||||
$lineId = !empty($line->id) ? $line->id : uniqid();
|
||||
$shopLinkHtml = $this->getShopLinkHtml($allSuppliers, 0, '', $lineId);
|
||||
$shopLinkHtml = $this->getShopLinkHtml($allSuppliers, 0, '', $lineId, $line->fk_product);
|
||||
|
||||
// Lagerbestand-Badge mit neuem System (4 Zustände: rot/orange/grau/grün)
|
||||
$stockBadgeHtml = $this->getStockBadgeHtml($qtyStock, $desiredQty, $alertQty);
|
||||
// Günstigster Lieferant Badge (Kürzel in grün)
|
||||
$cheapestBadge = $this->getCheapestSupplierBadge($this->getAllSuppliersForProduct($line->fk_product, false));
|
||||
|
||||
// Rechtsbündig: [Shop-Icon feste Breite] [Lagerbestand feste Breite]
|
||||
// Lagerbestand-Badge (mit virtuellem Bestand)
|
||||
$stockBadgeHtml = $this->getStockBadgeHtml($line->fk_product, $desiredQty, $alertQty);
|
||||
|
||||
// Rechtsbündig: [Shop-Icon] [Lieferanten-Badge] [Lagerbestand]
|
||||
$stockInfo = '<span style="float: right; margin-left: 15px; display: inline-flex; align-items: center;">';
|
||||
|
||||
// Shop-Icon mit fester Breite
|
||||
|
|
@ -826,8 +1046,13 @@ class ActionsSupplierLink3 extends CommonHookActions
|
|||
}
|
||||
$stockInfo .= '</span>';
|
||||
|
||||
// Lagerbestand-Badge mit fester Breite (rechtsbündig für gleichmäßige Ausrichtung)
|
||||
$stockInfo .= '<span class="supplier-stock-col" style="display: inline-block; min-width: 55px; text-align: right;">';
|
||||
// Günstigster Lieferant Badge (z.B. "SON" in grün)
|
||||
if (!empty($cheapestBadge)) {
|
||||
$stockInfo .= $cheapestBadge;
|
||||
}
|
||||
|
||||
// Lagerbestand-Badge (Virtuell|Physisch)
|
||||
$stockInfo .= '<span class="supplier-stock-col" style="display: inline-block; min-width: 70px; text-align: right;">';
|
||||
$stockInfo .= $stockBadgeHtml;
|
||||
$stockInfo .= '</span>';
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class modSupplierLink3 extends DolibarrModules
|
|||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@supplierlink3'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '2.2';
|
||||
$this->version = '2.3';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
|
|||
101
js/replenish.js
101
js/replenish.js
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* SupplierLink3 - Replenish page enhancements
|
||||
* Ersetzt die "Aktueller Lagerbestand"-Spalte mit Badge + Shop-Link
|
||||
* Version 2.3: Lagerbestand (Physisch|Theoretisch) wie Dolibarr Standard
|
||||
*/
|
||||
console.log('SL3: replenish.js loaded');
|
||||
|
||||
|
|
@ -24,40 +25,58 @@ $(document).ready(function() {
|
|||
'.badge.badge-success { background-color: #28a745 !important; color: #fff !important; }' +
|
||||
'.sl3-stock-wrapper { display: inline-flex; align-items: center; justify-content: flex-end; }' +
|
||||
'.sl3-shop-col { display: inline-block; width: 28px; text-align: center; }' +
|
||||
'.sl3-badge-col { display: inline-block; min-width: 55px; text-align: right; }' +
|
||||
'.sl3-supplier-badge { display: inline-block; background-color: #28a745; color: #fff; font-size: 9px; padding: 2px 4px; border-radius: 3px; margin-left: 3px; }' +
|
||||
'.sl3-badge-col { display: inline-block; min-width: 70px; text-align: right; }' +
|
||||
'.sl3-modal-item { padding: 10px; border-bottom: 1px solid #eee; }' +
|
||||
'.sl3-modal-item:hover { background-color: #f5f5f5; }' +
|
||||
'.sl3-modal-item:last-child { border-bottom: none; }' +
|
||||
'.sl3-modal-item.cheapest { background-color: #d4edda; }' +
|
||||
'</style>');
|
||||
|
||||
// Modal-Handler
|
||||
// Modal-Handler mit Preisvergleich
|
||||
$(document).on('click', '.sl3-modal-trigger', function(e) {
|
||||
e.preventDefault();
|
||||
var suppliers = $(this).data('suppliers');
|
||||
if (!suppliers || suppliers.length === 0) return;
|
||||
|
||||
var minPrice = suppliers[0].price;
|
||||
var content = '';
|
||||
|
||||
for (var i = 0; i < suppliers.length; i++) {
|
||||
var sup = suppliers[i];
|
||||
var isFirst = (i === 0);
|
||||
var bgStyle = isFirst ? 'background-color: #e8f4fd;' : '';
|
||||
var bgClass = isFirst ? 'cheapest' : '';
|
||||
var star = isFirst ? '<i class="fas fa-star" style="color: gold; margin-right: 5px;"></i>' : '';
|
||||
|
||||
content += '<div class="sl3-modal-item" style="' + bgStyle + '">';
|
||||
content += '<div style="font-weight: ' + (isFirst ? 'bold' : 'normal') + '; margin-bottom: 5px;">' + star + sup.supplier_name + '</div>';
|
||||
content += '<div style="font-size: 12px; color: #666; margin-bottom: 8px;">';
|
||||
content += '<strong>' + parseFloat(sup.price).toFixed(2).replace('.', ',') + ' EUR</strong>';
|
||||
content += ' - Art.-Nr: ' + sup.ref_fourn;
|
||||
if (sup.min_qty > 1) content += ' (ab ' + sup.min_qty + ' St.)';
|
||||
// Prozentuale Differenz zum günstigsten
|
||||
var percentDiff = 0;
|
||||
var percentHtml = '';
|
||||
if (minPrice > 0 && sup.price > minPrice) {
|
||||
percentDiff = ((sup.price - minPrice) / minPrice * 100).toFixed(1);
|
||||
var diffColor = percentDiff > 20 ? '#dc3545' : (percentDiff > 10 ? '#fd7e14' : '#6c757d');
|
||||
percentHtml = '<span style="font-size: 10px; color: ' + diffColor + '; font-weight: bold;">+' + percentDiff + '%</span>';
|
||||
}
|
||||
|
||||
content += '<div class="sl3-modal-item ' + bgClass + '">';
|
||||
content += '<div style="font-weight: ' + (isFirst ? 'bold' : 'normal') + '; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center;">';
|
||||
content += '<span>' + star + sup.supplier_name + '</span>';
|
||||
content += percentHtml;
|
||||
content += '</div>';
|
||||
content += '<a href="' + sup.full_url + '" target="supplier_' + sup.supplier_id + '" class="button small">Im Shop</a>';
|
||||
content += '<div style="font-size: 12px; color: #666; margin-bottom: 8px;">';
|
||||
content += '<strong style="font-size: 13px;">' + parseFloat(sup.price).toFixed(2).replace('.', ',') + ' EUR</strong>';
|
||||
content += ' · ab ' + sup.min_qty + ' St.';
|
||||
content += ' · Art.-Nr: ' + sup.ref_fourn;
|
||||
content += '</div>';
|
||||
if (sup.full_url) {
|
||||
content += '<a href="' + sup.full_url + '" target="supplier_' + sup.supplier_id + '" class="button small">Im Shop öffnen</a>';
|
||||
}
|
||||
content += '</div>';
|
||||
}
|
||||
|
||||
$('<div>').html(content).dialog({
|
||||
modal: true,
|
||||
title: 'Lieferanten-Shops',
|
||||
width: 400,
|
||||
title: 'Lieferanten-Vergleich',
|
||||
width: 420,
|
||||
buttons: { 'Schließen': function() { $(this).dialog('close'); } }
|
||||
});
|
||||
});
|
||||
|
|
@ -86,7 +105,7 @@ $(document).ready(function() {
|
|||
|
||||
console.log('SL3: Column indices found:', colIndex);
|
||||
|
||||
// Fallback auf feste Indizes wenn nicht gefunden (für Produkte ohne Service-Spalte)
|
||||
// Fallback auf feste Indizes wenn nicht gefunden
|
||||
if (colIndex.stock < 0) colIndex.stock = 5;
|
||||
if (colIndex.desired < 0) colIndex.desired = 3;
|
||||
if (colIndex.alert < 0) colIndex.alert = 4;
|
||||
|
|
@ -101,18 +120,15 @@ $(document).ready(function() {
|
|||
// Produkt-Link finden um die Produkt-ID zu extrahieren
|
||||
var $productLink = $row.find('td a[href*="product/card.php?id="]').first();
|
||||
if ($productLink.length === 0) {
|
||||
// Versuche alternative Selektoren
|
||||
$productLink = $row.find('td a[href*="product.php?id="]').first();
|
||||
}
|
||||
if ($productLink.length === 0) {
|
||||
console.log('SL3: Row', rowsFound, '- no product link found');
|
||||
return;
|
||||
}
|
||||
|
||||
var href = $productLink.attr('href');
|
||||
var match = href.match(/id=(\d+)/);
|
||||
if (!match) {
|
||||
console.log('SL3: Row', rowsFound, '- no ID in href:', href);
|
||||
return;
|
||||
}
|
||||
var productId = match[1];
|
||||
|
|
@ -122,31 +138,53 @@ $(document).ready(function() {
|
|||
var $stockCell = $row.find('td').eq(colIndex.stock);
|
||||
if ($stockCell.length === 0) return;
|
||||
|
||||
// Stock-Wert aus der Zelle lesen
|
||||
var stock = parseFloat($stockCell.text().trim()) || 0;
|
||||
// Stock-Wert aus der Zelle lesen (physischer Bestand)
|
||||
var physicalStock = parseFloat($stockCell.text().trim()) || 0;
|
||||
|
||||
// Desired-Stock und Alert-Stock
|
||||
var desired = parseFloat($row.find('td').eq(colIndex.desired).text().trim()) || 0;
|
||||
var alert = parseFloat($row.find('td').eq(colIndex.alert).text().trim()) || 0;
|
||||
|
||||
// Badge-Klasse bestimmen
|
||||
// In der Replenish-Liste haben wir nur den physischen Bestand
|
||||
// Theoretischer Bestand wird hier gleich dem physischen gesetzt
|
||||
// (Der echte theoretische Bestand wird nur in den Orders angezeigt)
|
||||
var theoreticalStock = physicalStock;
|
||||
|
||||
// Badge-Klasse basierend auf physischem Bestand
|
||||
var badgeClass = 'badge-success';
|
||||
if (stock < 1) {
|
||||
var icon = 'fa-check-circle';
|
||||
var tooltip = 'Ausreichend verfügbar';
|
||||
|
||||
if (physicalStock < 1) {
|
||||
badgeClass = 'badge-danger';
|
||||
} else if (alert > 0 && stock <= alert) {
|
||||
icon = 'fa-times-circle';
|
||||
tooltip = 'Nicht verfügbar';
|
||||
} else if (alert > 0 && physicalStock <= alert) {
|
||||
badgeClass = 'badge-warning';
|
||||
} else if (desired > 0 && stock < desired) {
|
||||
icon = 'fa-exclamation-triangle';
|
||||
tooltip = 'Unter Mindestmenge (' + alert + ')';
|
||||
} else if (desired > 0 && physicalStock < desired) {
|
||||
badgeClass = 'badge-secondary';
|
||||
icon = 'fa-box-open';
|
||||
tooltip = 'Unter Wunschmenge (' + desired + ')';
|
||||
}
|
||||
|
||||
// Shop-Link erstellen (immer mit Container für feste Breite)
|
||||
// Shop-Link und günstigster Lieferant ermitteln
|
||||
var shopIconHtml = '';
|
||||
var supplierBadgeHtml = '';
|
||||
var suppliers = productSuppliers[productId];
|
||||
|
||||
if (suppliers && suppliers.length > 0) {
|
||||
// Günstigster Lieferant Badge
|
||||
var cheapest = suppliers[0];
|
||||
var shortCode = cheapest.supplier_name.replace(/[^a-zA-Z0-9]/g, '').substring(0, 3).toUpperCase();
|
||||
supplierBadgeHtml = '<span class="sl3-supplier-badge classfortooltip" title="Günstigster: ' +
|
||||
cheapest.supplier_name + ' (' + parseFloat(cheapest.price).toFixed(2).replace('.', ',') + ' EUR)">' +
|
||||
shortCode + '</span>';
|
||||
|
||||
if (suppliers.length === 1) {
|
||||
var sup = suppliers[0];
|
||||
shopIconHtml = '<a href="' + sup.full_url + '" target="supplier_' + sup.supplier_id + '" ' +
|
||||
'class="classfortooltip" title="' + sup.supplier_name + '" style="color: #0077b6; font-size: 14px;">' +
|
||||
shopIconHtml = '<a href="' + cheapest.full_url + '" target="supplier_' + cheapest.supplier_id + '" ' +
|
||||
'class="classfortooltip" title="' + cheapest.supplier_name + '" style="color: #0077b6; font-size: 14px;">' +
|
||||
'<i class="' + shopIcon + '"></i></a>';
|
||||
} else {
|
||||
shopIconHtml = '<a href="#" class="sl3-modal-trigger" data-suppliers=\'' + JSON.stringify(suppliers) + '\' ' +
|
||||
|
|
@ -155,10 +193,17 @@ $(document).ready(function() {
|
|||
}
|
||||
}
|
||||
|
||||
// Stock-Zelle ersetzen: inline-flex wie im Kundenauftrag
|
||||
// Stock-Zelle ersetzen mit neuem Format: Physisch | Theoretisch
|
||||
var html = '<div class="sl3-stock-wrapper">' +
|
||||
'<span class="sl3-shop-col">' + shopIconHtml + '</span>' +
|
||||
'<span class="sl3-badge-col"><span class="badge ' + badgeClass + '">' + Math.floor(stock) + '</span></span>' +
|
||||
supplierBadgeHtml +
|
||||
'<span class="sl3-badge-col">' +
|
||||
'<span class="badge ' + badgeClass + ' classfortooltip" title="' + tooltip + '" style="font-size: 11px; white-space: nowrap;">' +
|
||||
'<i class="fas ' + icon + '" style="margin-right: 3px;"></i>' +
|
||||
'<span style="font-weight: bold;">' + Math.floor(physicalStock) + '</span>' +
|
||||
'<span style="opacity: 0.7; margin: 0 2px;">|</span>' +
|
||||
'<span style="opacity: 0.8;">' + Math.floor(theoreticalStock) + '</span>' +
|
||||
'</span></span>' +
|
||||
'</div>';
|
||||
$stockCell.html(html).addClass('right').css('text-align', 'right');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -95,3 +95,23 @@ SL3_CreateSupplierOrder = Lieferantenbestellung erstellen
|
|||
SL3_SelectSupplier = Lieferant wählen
|
||||
SL3_SelectProducts = Produkte auswählen
|
||||
SL3_NoSuppliers = Keine Lieferanten verfügbar
|
||||
|
||||
#
|
||||
# Price comparison (v2.3)
|
||||
#
|
||||
SL3_CheapestSupplier = Günstigster Lieferant
|
||||
SL3_PriceCheapest = Günstigster Preis
|
||||
SL3_PriceAverage = Durchschnittlicher Preis
|
||||
SL3_PriceExpensive = Teuerster Preis
|
||||
SL3_SupplierComparison = Lieferanten-Vergleich
|
||||
SL3_PercentMore = teurer
|
||||
|
||||
#
|
||||
# Stock display (v2.3)
|
||||
#
|
||||
SL3_TheoreticalStock = Theoretischer Bestand
|
||||
SL3_PhysicalStock = Physischer Bestand
|
||||
SL3_StockDifference = Differenz (offene Aufträge/Lieferungen)
|
||||
SL3_StockTooltipPhysical = Physisch
|
||||
SL3_StockTooltipTheoretical = Theoretisch
|
||||
SL3_StockTooltipDifference = Differenz
|
||||
|
|
|
|||
|
|
@ -95,3 +95,23 @@ SL3_CreateSupplierOrder = Create Supplier Order
|
|||
SL3_SelectSupplier = Select supplier
|
||||
SL3_SelectProducts = Select products
|
||||
SL3_NoSuppliers = No suppliers available
|
||||
|
||||
#
|
||||
# Price comparison (v2.3)
|
||||
#
|
||||
SL3_CheapestSupplier = Cheapest supplier
|
||||
SL3_PriceCheapest = Cheapest price
|
||||
SL3_PriceAverage = Average price
|
||||
SL3_PriceExpensive = Most expensive
|
||||
SL3_SupplierComparison = Supplier Comparison
|
||||
SL3_PercentMore = more expensive
|
||||
|
||||
#
|
||||
# Stock display (v2.3)
|
||||
#
|
||||
SL3_TheoreticalStock = Theoretical stock
|
||||
SL3_PhysicalStock = Physical stock
|
||||
SL3_StockDifference = Difference (open orders/shipments)
|
||||
SL3_StockTooltipPhysical = Physical
|
||||
SL3_StockTooltipTheoretical = Theoretical
|
||||
SL3_StockTooltipDifference = Difference
|
||||
|
|
|
|||
Loading…
Reference in a new issue