supplierlink3/class/actions_supplierlink3.class.php

1076 lines
39 KiB
PHP
Executable file

<?php
/* Copyright (C) 2023 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2025 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file supplierlink3/class/actions_supplierlink3.class.php
* \ingroup supplierlink3
* \brief Example hook overload.
*
* TODO: Write detailed description here.
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonhookactions.class.php';
/**
* Class ActionsSupplierLink3
*/
class ActionsSupplierLink3 extends CommonHookActions
{
/**
* @var DoliDB Database handler.
*/
public $db;
/**
* @var int Debug mode (0=off, 1=on)
*/
private $debug = 0;
/**
* @var string Debug log file path
*/
private $debugLogFile = '/tmp/supplierlink3_debug.log';
/**
* @var string Error code (or message)
*/
public $error = '';
/**
* @var string[] Errors
*/
public $errors = array();
/**
* @var mixed[] Hook results. Propagated to $hookmanager->resArray for later reuse
*/
public $results = array();
/**
* @var ?string String displayed by executeHook() immediately after return
*/
public $resprints;
/**
* @var int Priority of hook (50 is used if value is not defined)
*/
public $priority;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
/**
* @var string Shop icon class (FontAwesome)
*/
private $shopIcon = 'fas fa-store';
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
// Debug-Modus aus Dolibarr-Konfiguration laden (Standard: AUS)
$this->debug = getDolGlobalInt('SUPPLIERLINK3_DEBUG_MODE', 0);
// Shop-Icon aus Konfiguration laden
$this->shopIcon = getDolGlobalString('SUPPLIERLINK3_SHOP_ICON', 'fas fa-store');
// Nur loggen wenn Debug aktiviert
if ($this->debug) {
error_log('['.date('Y-m-d H:i:s').'] [CONSTRUCTOR] ActionsSupplierLink3 instanziiert'."\n", 3, $this->debugLogFile);
}
}
/**
* Debug-Log schreiben
*
* @param string $message Nachricht
* @param string $method Methodenname
*/
private function debugLog($message, $method = '')
{
if (!$this->debug) {
return;
}
$logLine = '['.date('Y-m-d H:i:s').']';
if ($method) {
$logLine .= ' ['.$method.']';
}
$logLine .= ' '.$message."\n";
error_log($logLine, 3, $this->debugLogFile);
}
/**
* Execute action
*
* @param array<string,mixed> $parameters Array of parameters
* @param CommonObject $object The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
* @param string $action 'add', 'update', 'view'
* @return int Return integer <0 if KO,
* =0 if OK but we want to process standard actions too,
* >0 if OK and we want to replace standard actions.
*/
public function getNomUrl($parameters, &$object, &$action)
{
global $db, $langs, $conf, $user;
$this->resprints = '';
return 0;
}
/**
* Overload the doActions function : replacing the parent's function with the one below
*
* @param array<string,mixed> $parameters Hook metadata (context, etc...)
* @param CommonObject $object The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
* @param ?string $action Current action (if set). Generally create or edit or null
* @param HookManager $hookmanager Hook manager propagated to allow calling another hook
* @return int Return integer < 0 on error, 0 on success, 1 to replace standard code
*/
public function doActions($parameters, &$object, &$action, $hookmanager)
{
$error = 0; // Error counter
/* print_r($parameters); print_r($object); echo "action: " . $action; */
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) { // do something only for the context 'somecontext1' or 'somecontext2'
// Do what you want here...
// You can for example load and use call global vars like $fieldstosearchall to overwrite them, or update the database depending on $action and GETPOST values.
if (!$error) {
$this->results = array('myreturn' => 999);
$this->resprints = 'A text to show';
return 0; // or return 1 to replace standard code
} else {
$this->errors[] = 'Error message';
return -1;
}
}
return 0;
}
/**
* Overload the doMassActions function : replacing the parent's function with the one below
*
* @param array<string,mixed> $parameters Hook metadata (context, etc...)
* @param CommonObject $object The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
* @param ?string $action Current action (if set). Generally create or edit or null
* @param HookManager $hookmanager Hook manager propagated to allow calling another hook
* @return int Return integer < 0 on error, 0 on success, 1 to replace standard code
*/
public function doMassActions($parameters, &$object, &$action, $hookmanager)
{
global $conf, $user, $langs;
$error = 0; // Error counter
/* print_r($parameters); print_r($object); echo "action: " . $action; */
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) { // do something only for the context 'somecontext1' or 'somecontext2'
// @phan-suppress-next-line PhanPluginEmptyStatementForeachLoop
foreach ($parameters['toselect'] as $objectid) {
// Do action on each object id
}
if (!$error) {
$this->results = array('myreturn' => 999);
$this->resprints = 'A text to show';
return 0; // or return 1 to replace standard code
} else {
$this->errors[] = 'Error message';
return -1;
}
}
return 0;
}
/**
* Overload the addMoreMassActions function : replacing the parent's function with the one below
*
* @param array<string,mixed> $parameters Hook metadata (context, etc...)
* @param CommonObject $object The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
* @param ?string $action Current action (if set). Generally create or edit or null
* @param HookManager $hookmanager Hook manager propagated to allow calling another hook
* @return int Return integer < 0 on error, 0 on success, 1 to replace standard code
*/
public function addMoreMassActions($parameters, &$object, &$action, $hookmanager)
{
global $conf, $user, $langs;
$error = 0; // Error counter
$disabled = 1;
/* print_r($parameters); print_r($object); echo "action: " . $action; */
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) { // do something only for the context 'somecontext1' or 'somecontext2'
$this->resprints = '<option value="0"'.($disabled ? ' disabled="disabled"' : '').'>'.$langs->trans("SupplierLink3MassAction").'</option>';
}
if (!$error) {
return 0; // or return 1 to replace standard code
} else {
$this->errors[] = 'Error message';
return -1;
}
}
/**
* Execute action before PDF (document) creation
*
* @param array<string,mixed> $parameters Array of parameters
* @param CommonObject $object Object output on PDF
* @param string $action 'add', 'update', 'view'
* @return int Return integer <0 if KO,
* =0 if OK but we want to process standard actions too,
* >0 if OK and we want to replace standard actions.
*/
public function beforePDFCreation($parameters, &$object, &$action)
{
global $conf, $user, $langs;
global $hookmanager;
$outputlangs = $langs;
$ret = 0;
$deltemp = array();
dol_syslog(get_class($this).'::executeHooks action='.$action);
/* print_r($parameters); print_r($object); echo "action: " . $action; */
// @phan-suppress-next-line PhanPluginEmptyStatementIf
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) {
// do something only for the context 'somecontext1' or 'somecontext2'
}
return $ret;
}
/**
* Execute action after PDF (document) creation
*
* @param array<string,mixed> $parameters Array of parameters
* @param CommonDocGenerator $pdfhandler PDF builder handler
* @param string $action 'add', 'update', 'view'
* @return int Return integer <0 if KO,
* =0 if OK but we want to process standard actions too,
* >0 if OK and we want to replace standard actions.
*/
public function afterPDFCreation($parameters, &$pdfhandler, &$action)
{
global $conf, $user, $langs;
global $hookmanager;
$outputlangs = $langs;
$ret = 0;
$deltemp = array();
dol_syslog(get_class($this).'::executeHooks action='.$action);
/* print_r($parameters); print_r($object); echo "action: " . $action; */
// @phan-suppress-next-line PhanPluginEmptyStatementIf
if (in_array($parameters['currentcontext'], array('somecontext1', 'somecontext2'))) {
// do something only for the context 'somecontext1' or 'somecontext2'
}
return $ret;
}
/**
* Overload the loadDataForCustomReports function : returns data to complete the customreport tool
*
* @param array<string,mixed> $parameters Hook metadata (context, etc...)
* @param ?string $action Current action (if set). Generally create or edit or null
* @param HookManager $hookmanager Hook manager propagated to allow calling another hook
* @return int Return integer < 0 on error, 0 on success, 1 to replace standard code
*/
public function loadDataForCustomReports($parameters, &$action, $hookmanager)
{
global $langs;
$langs->load("supplierlink3@supplierlink3");
$this->results = array();
$head = array();
$h = 0;
if ($parameters['tabfamily'] == 'supplierlink3') {
$head[$h][0] = dol_buildpath('/module/index.php', 1);
$head[$h][1] = $langs->trans("Home");
$head[$h][2] = 'home';
$h++;
$this->results['title'] = $langs->trans("SupplierLink3");
$this->results['picto'] = 'supplierlink3@supplierlink3';
}
$head[$h][0] = 'customreports.php?objecttype='.$parameters['objecttype'].(empty($parameters['tabfamily']) ? '' : '&tabfamily='.$parameters['tabfamily']);
$head[$h][1] = $langs->trans("CustomReports");
$head[$h][2] = 'customreports';
$this->results['head'] = $head;
$arrayoftypes = array();
//$arrayoftypes['supplierlink3_myobject'] = array('label' => 'MyObject', 'picto'=>'myobject@supplierlink3', 'ObjectClassName' => 'MyObject', 'enabled' => isModEnabled('supplierlink3'), 'ClassPath' => "/supplierlink3/class/myobject.class.php", 'langs'=>'supplierlink3@supplierlink3')
$this->results['arrayoftype'] = $arrayoftypes;
return 0;
}
/**
* Overload the restrictedArea function : check permission on an object
*
* @param array<string,mixed> $parameters Hook metadata (context, etc...)
* @param CommonObject $object The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
* @param string $action Current action (if set). Generally create or edit or null
* @param HookManager $hookmanager Hook manager propagated to allow calling another hook
* @return int Return integer <0 if KO,
* =0 if OK but we want to process standard actions too,
* >0 if OK and we want to replace standard actions.
*/
public function restrictedArea($parameters, $object, &$action, $hookmanager)
{
global $user;
if ($parameters['features'] == 'myobject') {
if ($user->hasRight('supplierlink3', 'myobject', 'read')) {
$this->results['result'] = 1;
return 1;
} else {
$this->results['result'] = 0;
return 1;
}
}
return 0;
}
/**
* Execute action completeTabsHead
*
* @param array<string,mixed> $parameters Array of parameters
* @param CommonObject $object The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
* @param string $action 'add', 'update', 'view'
* @param Hookmanager $hookmanager Hookmanager
* @return int Return integer <0 if KO,
* =0 if OK but we want to process standard actions too,
* >0 if OK and we want to replace standard actions.
*/
public function completeTabsHead(&$parameters, &$object, &$action, $hookmanager)
{
global $langs, $conf, $user;
if (!isset($parameters['object']->element)) {
return 0;
}
if ($parameters['mode'] == 'remove') {
// used to make some tabs removed
return 0;
} elseif ($parameters['mode'] == 'add') {
$langs->load('supplierlink3@supplierlink3');
// used when we want to add some tabs
$counter = count($parameters['head']);
$element = $parameters['object']->element;
$id = $parameters['object']->id;
// verifier le type d'onglet comme member_stats où ça ne doit pas apparaitre
// if (in_array($element, ['societe', 'member', 'contrat', 'fichinter', 'project', 'propal', 'commande', 'facture', 'order_supplier', 'invoice_supplier'])) {
if (in_array($element, ['context1', 'context2'])) {
$datacount = 0;
$parameters['head'][$counter][0] = dol_buildpath('/supplierlink3/supplierlink3_tab.php', 1) . '?id=' . $id . '&amp;module='.$element;
$parameters['head'][$counter][1] = $langs->trans('SupplierLink3Tab');
if ($datacount > 0) {
$parameters['head'][$counter][1] .= '<span class="badge marginleftonlyshort">' . $datacount . '</span>';
}
$parameters['head'][$counter][2] = 'supplierlink3emails';
$counter++;
}
if ($counter > 0 && (int) DOL_VERSION < 14) { // @phpstan-ignore-line
$this->results = $parameters['head'];
// return 1 to replace standard code
return 1;
} else {
// From V14 onwards, $parameters['head'] is modifiable by reference
return 0;
}
} else {
// Bad value for $parameters['mode']
return -1;
}
}
/**
* Overload the showLinkToObjectBlock function : add or replace array of object linkable
*
* @param array<string,mixed> $parameters Hook metadata (context, etc...)
* @param CommonObject $object The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
* @param ?string $action Current action (if set). Generally create or edit or null
* @param HookManager $hookmanager Hook manager propagated to allow calling another hook
* @return int Return integer < 0 on error, 0 on success, 1 to replace standard code
*/
/*public function showLinkToObjectBlock($parameters, &$object, &$action, $hookmanager)
{
$myobject = new MyObject($object->db);
$this->results = array('myobject@supplierlink3' => array(
'enabled' => isModEnabled('supplierlink3'),
'perms' => 1,
'label' => 'LinkToMyObject',
'sql' => "SELECT t.rowid, t.ref, t.ref as 'name' FROM " . $this->db->prefix() . $myobject->table_element. " as t "),);
return 1;
}*/
/* Add other hook methods here... */
/**
* Holt alle Lieferanten mit Shop-URL für ein Produkt
*
* @param int $fk_product Produkt-ID
* @return array Array mit Lieferanten-Daten, sortiert nach Preis aufsteigend
*/
private function getAllSuppliersForProduct($fk_product)
{
$suppliers = array();
$sql = "SELECT DISTINCT
pfp.fk_soc as supplier_id,
pfp.ref_fourn,
pfp.price,
pfp.quantity as min_qty,
s.nom as supplier_name,
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";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$suppliers[] = array(
'supplier_id' => $obj->supplier_id,
'ref_fourn' => $obj->ref_fourn,
'price' => $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
);
}
}
return $suppliers;
}
/**
* Generiert HTML-Badge für Lagerbestand
*
* 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 float $qtyStock Aktueller Lagerbestand
* @param float $desiredQty Wunschbestand
* @param float $alertQty Mindestbestand (seuil_stock_alerte)
* @return string HTML-Badge
*/
private function getStockBadgeHtml($qtyStock, $desiredQty, $alertQty = 0)
{
if ($qtyStock <= 0) {
// ROT: Nicht verfügbar (0 oder negativ)
$badgeClass = 'badge-danger';
$icon = 'fa-times-circle';
$tooltip = 'Nicht auf Lager';
} elseif ($alertQty > 0 && $qtyStock < $alertQty) {
// ORANGE: Unter Mindestmenge (Alarm-Schwelle)
$badgeClass = 'badge-warning';
$icon = 'fa-exclamation-triangle';
$tooltip = 'Unter Mindestmenge ('.(int)$alertQty.')';
} elseif ($desiredQty > 0 && $qtyStock < $desiredQty) {
// GRAU: Unter Wunschmenge
$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';
}
$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, ',', '.');
$html .= '</span>';
return $html;
}
/**
* Generiert Shop-Link HTML (Icon oder Modal-Trigger für mehrere Lieferanten)
*
* @param array $suppliers Array der Lieferanten
* @param int $currentSupplierId Aktueller Lieferant der Bestellung
* @param string $refFourn Lieferanten-Artikelnummer
* @param int $lineId Zeilen-ID für eindeutige Modal-ID
* @return string HTML für Shop-Link
*/
private function getShopLinkHtml($suppliers, $currentSupplierId, $refFourn, $lineId)
{
if (empty($suppliers)) {
return '';
}
// Fall 1: Nur ein Lieferant mit Shop-URL - direkter Link
if (count($suppliers) == 1) {
$supplier = $suppliers[0];
$html = '<a href="'.dol_escape_htmltag($supplier['full_url']).'" ';
$html .= 'target="supplier_'.$supplier['supplier_id'].'" ';
$html .= 'class="classfortooltip" ';
$html .= 'title="'.dol_escape_htmltag('Im Shop ansehen: '.$supplier['supplier_name']).'" ';
$html .= 'style="color: #0077b6; font-size: 14px;">';
$html .= '<i class="'.$this->shopIcon.'"></i>';
$html .= '</a>';
return $html;
}
// Fall 2: Mehrere Lieferanten - Modal-Trigger
$modalId = 'supplier_modal_'.$lineId;
// Trigger-Button mit Dropdown-Pfeil
$html = '<a href="#" class="supplier-modal-trigger classfortooltip" ';
$html .= 'data-modal="'.$modalId.'" ';
$html .= 'title="'.dol_escape_htmltag('Lieferanten-Shops ('.count($suppliers).')').'" ';
$html .= 'style="color: #0077b6; font-size: 14px;">';
$html .= '<i class="'.$this->shopIcon.'"></i>';
$html .= '<i class="fas fa-caret-down" style="font-size: 8px; margin-left: 2px;"></i>';
$html .= '</a>';
// Modal-Container (versteckt)
$html .= '<div id="'.$modalId.'" class="supplier-modal-content" style="display: none;" title="Lieferanten-Shops">';
$isFirst = true;
foreach ($suppliers as $supplier) {
$isCurrentSupplier = ($supplier['supplier_id'] == $currentSupplierId);
$rowStyle = '';
if ($isCurrentSupplier) {
$rowStyle = 'background-color: #e8f4fd;';
}
$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;">';
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 .= '</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 .= ' &middot; Mindestmenge: '.(int)$supplier['min_qty'].' Stk.';
$html .= ' &middot; 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>';
$html .= '</div>';
$isFirst = false;
}
$html .= '</div>';
return $html;
}
public function printObjectLine($parameters, &$object, &$action, $hookmanager)
{
static $css_js_added = false;
$isSupplierOrder = ($parameters['currentcontext'] == 'ordersuppliercard');
$isCustomerOrder = ($parameters['currentcontext'] == 'ordercard');
$isProposal = ($parameters['currentcontext'] == 'propalcard');
if (!$isSupplierOrder && !$isCustomerOrder && !$isProposal) {
return 0;
}
// Prüfen ob diese Funktion für den jeweiligen Kontext aktiviert ist
if ($isSupplierOrder && !getDolGlobalInt('SUPPLIERLINK3_ENABLE_SUPPLIERORDER', 1)) {
return 0;
}
if ($isCustomerOrder && !getDolGlobalInt('SUPPLIERLINK3_ENABLE_ORDERCARD', 1)) {
return 0;
}
if ($isProposal && !getDolGlobalInt('SUPPLIERLINK3_ENABLE_PROPALCARD', 1)) {
return 0;
}
// CSS und JavaScript nur einmal hinzufügen
if (!$css_js_added) {
?>
<style>
/* Verstecke Info-Icons innerhalb der Lagerbestand-Anzeige */
.stock-display-wrapper .fa-info-circle,
#tablelines tbody .fa-info-circle {
display: none !important;
}
/* Badge-Styling - 4 Zustände */
.badge.badge-danger {
background-color: #dc3545 !important;
color: #fff !important;
}
.badge.badge-warning {
background-color: #fd7e14 !important;
color: #fff !important;
}
.badge.badge-secondary {
background-color: #6c757d !important;
color: #fff !important;
}
.badge.badge-success {
background-color: #28a745 !important;
color: #fff !important;
}
/* Modal-Item Styling */
.supplier-modal-item:last-child {
border-bottom: none !important;
}
.supplier-modal-item:hover {
background-color: #f5f5f5 !important;
}
/* Button Styling */
.supplier-modal-item .button.small {
background: linear-gradient(to bottom, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #ced4da;
border-radius: 3px;
color: #495057;
text-decoration: none;
display: inline-block;
}
.supplier-modal-item .button.small:hover {
background: linear-gradient(to bottom, #e9ecef 0%, #dee2e6 100%);
color: #212529;
}
/* jQuery UI Dialog Anpassungen */
.ui-dialog .supplier-modal-content {
padding: 0 !important;
}
</style>
<script type="text/javascript">
$(document).ready(function() {
// Modal-Dialog bei Klick auf Trigger öffnen
$(document).on('click', '.supplier-modal-trigger', function(e) {
e.preventDefault();
e.stopPropagation();
var modalId = $(this).data('modal');
var $modal = $('#' + modalId);
if ($modal.length) {
$modal.dialog({
modal: true,
width: 450,
maxHeight: 400,
buttons: {
'Schließen': function() {
$(this).dialog('close');
}
},
open: function() {
// Styling für Dialog-Buttons
$(this).parent().find('.ui-dialog-buttonpane button').css({
'font-size': '12px',
'padding': '6px 15px'
});
}
});
}
});
});
</script>
<?php
$css_js_added = true;
}
$line = $parameters['line'];
// Lagerbestand, Wunschbestand und Mindestbestand abfragen
$sqlStock = "SELECT stock, 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;
}
// Lieferanten-Shop-Link Logik (nur für Lieferantenbestellungen)
if (!empty($object->socid) && $isSupplierOrder) {
$fk_supplier = $object->socid;
// Alle Lieferanten für dieses Produkt abrufen
$allSuppliers = $this->getAllSuppliersForProduct($line->fk_product);
// Shop-Icon/Modal generieren
$lineId = !empty($line->id) ? $line->id : uniqid();
$shopLinkHtml = $this->getShopLinkHtml($allSuppliers, $fk_supplier, $line->ref_fourn, $lineId);
// Lagerbestand-Badge generieren (4 Zustände: rot/orange/grau/grün)
$stockBadgeHtml = $this->getStockBadgeHtml($qtyStock, $desiredQty, $alertQty);
// Neue ref_fourn zusammenbauen:
// [Shop-Icon feste Breite] [Lagerbestand feste Breite] [Artikel-Nummer]
$newref = '<div style="display: inline-flex; align-items: center;">';
// Shop-Icon mit fester Breite (damit alle untereinander stehen)
$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;">';
$newref .= $stockBadgeHtml;
$newref .= '</span>';
// Artikel-Nummer als normaler Text
$newref .= '<span>' . dol_escape_htmltag($line->ref_fourn) . '</span>';
$newref .= '</div>';
$line->ref_fourn = $newref;
} elseif ($isCustomerOrder || $isProposal) {
// Kundenauftrag oder Angebot - Shop-Link und Lagerbestand anzeigen
// 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);
// Shop-Icon/Modal generieren (ohne aktuellen Lieferanten, da Kundenauftrag/Angebot)
$lineId = !empty($line->id) ? $line->id : uniqid();
$shopLinkHtml = $this->getShopLinkHtml($allSuppliers, 0, '', $lineId);
// Lagerbestand-Badge mit neuem System (4 Zustände: rot/orange/grau/grün)
$stockBadgeHtml = $this->getStockBadgeHtml($qtyStock, $desiredQty, $alertQty);
// Rechtsbündig: [Shop-Icon feste Breite] [Lagerbestand feste Breite]
$stockInfo = '<span style="float: right; margin-left: 15px; display: inline-flex; align-items: center;">';
// Shop-Icon mit fester Breite
$stockInfo .= '<span class="supplier-shop-col" style="display: inline-block; width: 28px; text-align: center;">';
if (!empty($shopLinkHtml)) {
$stockInfo .= $shopLinkHtml;
}
$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;">';
$stockInfo .= $stockBadgeHtml;
$stockInfo .= '</span>';
$stockInfo .= '</span>';
// An Produktlabel anhängen
if (isset($line->product_label)) {
$line->product_label .= $stockInfo;
}
}
}
return 0;
}
/**
* Hook für zusätzliche Buttons auf der Kundenauftrags-Seite
* WICHTIG: Dolibarr gibt $hookmanager->resPrint bei addMoreActionsButtons NICHT aus!
* Daher verwenden wir JavaScript um den Button dynamisch einzufügen (wie SubtotalTitle)
*/
public function addMoreActionsButtons($parameters, &$object, &$action, $hookmanager)
{
global $db;
$this->debugLog('Hook aufgerufen - context: '.(isset($parameters['currentcontext']) ? $parameters['currentcontext'] : 'nicht gesetzt'), 'addMoreActionsButtons');
$this->debugLog('Object element: '.(is_object($object) ? $object->element : 'kein Objekt'), 'addMoreActionsButtons');
$this->debugLog('Object id: '.(is_object($object) && isset($object->id) ? $object->id : 'keine ID'), 'addMoreActionsButtons');
// Nur für Kundenaufträge (Commande)
if (!is_object($object) || $object->element != 'commande') {
$this->debugLog('Abbruch: Kein Commande-Objekt', 'addMoreActionsButtons');
return 0;
}
// Prüfen ob Produktzeilen oder Freitext-Zeilen vorhanden sind
// Freitext-Zeilen haben fk_product = 0 oder NULL
$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."commandedet
WHERE fk_commande = ".(int)$object->id."
AND (product_type = 0 OR fk_product = 0 OR fk_product IS NULL)";
$resql = $db->query($sql);
$hasLines = false;
if ($resql) {
$obj = $db->fetch_object($resql);
$hasLines = ($obj->nb > 0);
$this->debugLog('Produkt-/Freitextzeilen gefunden: '.$obj->nb, 'addMoreActionsButtons');
} else {
$this->debugLog('SQL Fehler: '.$db->lasterror(), 'addMoreActionsButtons');
}
if ($hasLines) {
$buttonUrl = dol_buildpath('/custom/supplierlink3/create_supplier_order.php', 1).'?origin=commande&originid='.$object->id;
// Button per JavaScript einfügen (da Dolibarr resPrint bei diesem Hook nicht ausgibt)
print '<script>$(document).ready(function() {';
print ' if ($(".tabsAction").length > 0 && $("#btnSupplierLink3").length === 0) {';
print ' $(".tabsAction").append(\'<a id="btnSupplierLink3" class="butAction" href="'.dol_escape_js($buttonUrl).'"><i class="fas fa-truck-loading" style="margin-right: 5px;"></i>Lieferantenbestellung erstellen</a>\');';
print ' }';
print '});</script>'."\n";
$this->debugLog('Button per JavaScript eingefügt für URL: '.$buttonUrl, 'addMoreActionsButtons');
} else {
$this->debugLog('Kein Button - keine Produktzeilen', 'addMoreActionsButtons');
}
return 0;
}
public function formObjectOptions($parameters, &$object, &$action)
{
global $db;
// Auf Produktkarte UND Preis-Seite
if (strpos($parameters['context'], 'productcard') === false && strpos($parameters['context'], 'productpricecard') === false) {
return 0;
}
// Prüfen ob Produktkarte aktiviert ist
if (!getDolGlobalInt('SUPPLIERLINK3_ENABLE_PRODUCTCARD', 1)) {
return 0;
}
if ($action != 'create' && $action != 'edit') {
$sql = "SELECT DISTINCT pfp.ref_fourn, pfp.quantity, pfp.price, pfp.fk_soc, s.nom, se.shop_url
FROM ".MAIN_DB_PREFIX."product_fournisseur_price pfp
INNER JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = pfp.fk_soc
INNER JOIN ".MAIN_DB_PREFIX."societe_extrafields se ON se.fk_object = pfp.fk_soc
WHERE pfp.fk_product = ".(int)$object->id."
AND se.shop_url IS NOT NULL
AND se.shop_url != ''
ORDER BY pfp.price ASC";
$resql = $db->query($sql);
$links = array();
$minPrice = null;
// Ersten Durchlauf: günstigsten Preis finden
if ($resql) {
$results = array();
while ($obj = $db->fetch_object($resql)) {
$results[] = $obj;
if ($minPrice === null || $obj->price < $minPrice) {
$minPrice = $obj->price;
}
}
// Zweiter Durchlauf: Links bauen
foreach ($results as $obj) {
$shortName = explode(' ', $obj->nom)[0];
$tooltip = '<b><u>'.$obj->nom.'</u></b><br><b>Artikel-Nr:</b> '.$obj->ref_fourn.'<br><b>Preis:</b> '.round($obj->price,2).' EUR<br><b>Mindestmenge: </b>'.$obj->quantity;
// Günstigster Lieferant wird fett
$displayText = $shortName.' ('.$obj->ref_fourn.')';
if ($obj->price == $minPrice) {
$displayText = '<b>'.$displayText.'</b>';
}
$links[] = '<a href="'.dol_escape_htmltag($obj->shop_url).$obj->ref_fourn.'" target="supplier_'.$obj->fk_soc.'" class="classfortooltip" title="'.$tooltip.'">'.$displayText.'</a>';
}
}
if (!empty($links)) {
$linksHtml = implode(' | ', $links);
?>
<script type="text/javascript">
$(document).ready(function() {
var linksDiv = '<div class="refidno opacitymedium" style="font-size: 12px;"><?php echo addslashes($linksHtml); ?></div>';
$('.refidno').last().after(linksDiv);
});
</script>
<?php
}
}
return 0;
}
/**
* Hook für Footer der Liste - JavaScript einfügen das die Stock-Spalte ersetzt
* KEINE zusätzliche Spalte - nur die bestehende Stock-Spalte wird modifiziert
*/
public function printFieldListFooter($parameters, &$object, &$action, $hookmanager)
{
global $db;
if (strpos($parameters['currentcontext'], 'stockreplenishlist') === false) {
return 0;
}
// Prüfen ob Nachbestellung aktiviert ist
if (!getDolGlobalInt('SUPPLIERLINK3_ENABLE_REPLENISH', 1)) {
return 0;
}
// Alle Produkte mit Shop-URLs abrufen
$sql = "SELECT DISTINCT pfp.fk_product,
pfp.fk_soc as supplier_id,
pfp.ref_fourn,
pfp.price,
pfp.quantity as min_qty,
s.nom as supplier_name,
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 se.shop_url IS NOT NULL AND se.shop_url != ''
ORDER BY pfp.fk_product, pfp.price ASC";
$resql = $db->query($sql);
$productSuppliers = array();
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$productId = $obj->fk_product;
if (!isset($productSuppliers[$productId])) {
$productSuppliers[$productId] = array();
}
$productSuppliers[$productId][] = array(
'supplier_id' => $obj->supplier_id,
'supplier_name' => $obj->supplier_name,
'ref_fourn' => $obj->ref_fourn,
'price' => $obj->price,
'min_qty' => $obj->min_qty,
'full_url' => rtrim($obj->shop_url, '/').'/'.$obj->ref_fourn
);
}
}
$suppliersJson = json_encode($productSuppliers);
$jsUrl = dol_buildpath('/supplierlink3/js/replenish.js', 1);
$shopIcon = getDolGlobalString('SUPPLIERLINK3_SHOP_ICON', 'fas fa-store');
// Daten und Script direkt in den Footer schreiben (nach der Seite)
// Verwende print statt resprints um außerhalb der Tabelle zu landen
?>
<script>
window.sl3Data = <?php echo $suppliersJson; ?>;
window.sl3JsUrl = "<?php echo $jsUrl; ?>";
window.sl3ShopIcon = "<?php echo dol_escape_js($shopIcon); ?>";
console.log("SL3: Init data loaded for", Object.keys(window.sl3Data).length, "products");
// Externes Script laden wenn DOM fertig
(function() {
var loadScript = function() {
var s = document.createElement("script");
s.src = window.sl3JsUrl + "?v=" + Date.now();
document.body.appendChild(s);
console.log("SL3: Loading external script", s.src);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadScript);
} else {
loadScript();
}
})();
</script>
<?php
return 0;
}
/**
* Hook für Zeilenwerte - KEIN Output, keine zusätzliche Spalte
*/
public function printFieldListValue($parameters, &$object, &$action, $hookmanager)
{
// Kein Output - wir ersetzen die bestehende Stock-Spalte per JavaScript
return 0;
}
/**
* Hook für Spaltenüberschrift - KEIN Output, keine zusätzliche Spalte
*/
public function printFieldListTitle($parameters, &$object, &$action, $hookmanager)
{
// Kein Output
return 0;
}
/**
* Hook für Filter-Zeile - KEIN Output, keine zusätzliche Spalte
*/
public function printFieldListOption($parameters, &$object, &$action, $hookmanager)
{
// Kein Output
return 0;
}
}