- Freitext-Zeilen aus Kundenaufträgen werden nun angezeigt und übernommen - Neue Lieferanten-Auswahl: Automatisch (passende) / Manuell (alle) - JavaScript-basierte Umschaltung ohne Seiten-Reload - Fallback für Systeme ohne markierte Lieferanten - Button erscheint nun auch bei reinen Freitext-Aufträgen Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1076 lines
39 KiB
PHP
1076 lines
39 KiB
PHP
<?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 . '&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 .= ' · Mindestmenge: '.(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>';
|
|
|
|
$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, 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="_blank" 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;
|
|
}
|
|
}
|