diff --git a/ChangeLog.md b/ChangeLog.md index f3b9b65..f802437 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,7 @@ ## Release 1.10 +- FIX : Warning Constant INC_FROM_DOLIBARR already defined - *18/08/2025* - 1.10.5 - FIX : DA026565 Missing chiffrage element in ajax select - *2025-05-26* - 1.10.4 - FIX : DA026567 Empty ajax select - *2025-05-22* - 1.10.3 - FIX DA026311 : removed references to the deprecated 'asset' and 'ordre_fabrication' elements across the codebase. This includes adjustments to class mappings, known elements, and checks for deprecated modules - *18/04/2025* - 1.10.2 diff --git a/class/actions_related.class.php b/class/actions_related.class.php index 0b5c988..6257ea7 100644 --- a/class/actions_related.class.php +++ b/class/actions_related.class.php @@ -420,100 +420,14 @@ function blockRelated($parameters, &$object, &$action, $hookmanager, $moreStyle= + dol_buildpath('/related',1), + ]; + ?> + description = "Links elements together"; // Possible values for version are: 'development', 'experimental', 'dolibarr' or version - $this->version = '1.10.4'; + $this->version = '1.10.5'; // Url to the file with your last numberversion of this module require_once __DIR__ . '/../../class/techatm.class.php'; $this->url_last_version = \related\TechATM::getLastModuleVersionUrl($this); @@ -201,7 +201,9 @@ function init($options='') { $sql = array(); - define('INC_FROM_DOLIBARR',true); + if (!defined('INC_FROM_DOLIBARR')) { + define('INC_FROM_DOLIBARR', true); + } dol_include_once('/related/config.php'); dol_include_once('/related/script/create-maj-base.php'); diff --git a/js/related.js b/js/related.js new file mode 100644 index 0000000..8411d46 --- /dev/null +++ b/js/related.js @@ -0,0 +1,160 @@ +/* jshint -W098: suppress the `ATM_MODULE_RELATED is declared but never used` warning */ +/* jshint -W117: suppress the `'$' is not defined` warning */ +/* jshint -W116: allow if (……) return; (without brackets) */ +(function () { + "use strict"; + window.ATM_MODULE_RELATED = { + /** + * + * @param {{relatedBaseURL: string }} dolibarrContext Contexte passé depuis PHP. + */ + main(dolibarrContext) { + window.addEventListener('DOMContentLoaded', () => { + this.moveRelatedBlocksAfterTabsAction(); + this.cleanupDuplicateBlocks(); + + const ajaxURL = `${dolibarrContext.relatedBaseURL}/script/interface.php`; + this.initializeAutocomplete(ajaxURL); + }); + }, + + /** + * Moves all .blockrelated_content elements to appear after their parent .tabsAction + */ + moveRelatedBlocksAfterTabsAction() { + document.querySelectorAll('.blockrelated_content').forEach(element => { + const tabsAction = element.closest('div.tabsAction'); + if (tabsAction) { + tabsAction.insertAdjacentElement('afterend', element); + } + }); + }, + + /** + * Initializes the jQuery UI autocomplete for adding related objects + */ + initializeAutocomplete(ajaxURL) { + const $input = $('#add_related_object'); + if (!$input.length) return; + + $input.autocomplete({ + source: (request, response) => this.fetchAutocompleteData(request, response, ajaxURL), + minLength: 1, + select: (event, ui) => this.handleAutocompleteSelect(ui), + open: function() { + this.classList.remove('ui-corner-all'); + this.classList.add('ui-corner-top'); + }, + close: function() { + this.classList.remove('ui-corner-top'); + this.classList.add('ui-corner-all'); + } + }); + + // Custom renderer for categorized items + $input.autocomplete().data("uiAutocomplete")._renderItem = this.renderAutocompleteItem; + }, + + /** + * Fetches autocomplete suggestions from the server + */ + fetchAutocompleteData(request, response, ajaxURL) { + $.ajax({ + url: ajaxURL, + dataType: "json", + data: { + key: request.term, + get: 'search' + }, + success: (data) => { + const items = this.transformAutocompleteData(data); + response(items); + } + }); + }, + + /** + * Transforms server response into autocomplete items with category headers + */ + transformAutocompleteData(data) { + const items = []; + + Object.entries(data).forEach(([category, categoryData]) => { + // Add category header + items.push({ + value: category, + label: category, + object: 'title' + }); + + // Add category items + Object.entries(categoryData).forEach(([id, label]) => { + items.push({ + value: id, + label: ' ' + label, + object: category + }); + }); + }); + + return items; + }, + + /** + * Handles selection of an autocomplete item + */ + handleAutocompleteSelect(ui) { + // Prevent selection of category headers + if (ui.item.object === 'title') { + return false; + } + + // Populate hidden fields + document.getElementById('id_related_object').value = ui.item.value; + document.getElementById('add_related_object').value = ui.item.label.trim(); + document.getElementById('type_related_object').value = ui.item.object; + + // Show the add button + const addButton = document.getElementById('bt_add_related_object'); + if (addButton) { + addButton.style.display = 'inline'; + } + + return false; + }, + + /** + * Custom renderer for autocomplete items (bold category headers) + */ + renderAutocompleteItem(ul, item) { + const li = document.createElement('li'); + li.dataset.value = item.value; + li.textContent = item.label; + + if (item.object === "title") { + li.style.fontWeight = "bold"; + } + + ul[0].appendChild(li); + return $(li); + }, + + /** + * Removes duplicate .blockrelated_content blocks within .tabsAction + */ + cleanupDuplicateBlocks() { + const tabsAction = document.querySelector('div.tabsAction'); + if (!tabsAction) return; + + const blockrelated = tabsAction.querySelector('.blockrelated_content'); + if (!blockrelated) return; + + const allBlockrelated = document.querySelectorAll('.blockrelated_content'); + if (allBlockrelated.length > 1) { + blockrelated.remove(); + } else { + tabsAction.appendChild(blockrelated); + } + } + }; +}()); diff --git a/script/interface.php b/script/interface.php index 46fd297..3f474be 100644 --- a/script/interface.php +++ b/script/interface.php @@ -1,261 +1,265 @@ initHooks(array('relatedAjax')); + +// $synonyms = [ +// 'action' => 'actioncomm', +// 'entrepot' => 'stock', +// 'invoice' => 'facture', +// 'order' => 'commande', +// 'shipping' => 'expedition', +// 'adherent' => 'member', +// 'company' => 'societe', +// ]; + $supportedElements = [ + 'facture', 'commande', 'shipping', 'propal', 'project', 'task', - 'company', + 'societe', 'contact', - 'event', + 'actioncomm', 'product', 'facture_fournisseur', 'commande_fournisseur', 'fichinter', 'contrat', 'ticket', - ); - - if(isModEnabled("assetatm")) { - $TType[] = 'assetatm'; - } + ]; + + /** + * Ideally, an object type should require no specific configuration because the table name, the object name, + * the reference field, the ID field etc. should all use the same pattern and be derived from the element name, + * but this is not always the case. + * + * This array allow us to override the default name for a specific element. + * Possible keys are: + * - 'table_element': if you need to override the table name; no prefix. + * - 'join_to_soc': if set, there will be a join to llx_societe to show the third party in the dropdown + * (true by default) + * - 'ref_field': by default, 'ref', but sometimes can be named differently + * - 'ref_field2': if specified, that field will be concatenated to the ref in the json response + * - 'multicompany_element': the element name passed to getEntity() for multi-entity elements + * - 'check_read_permission': true by default; if false, won't check any permissions before searching + * - 'rights_module': if specified, will be passed as the 1st argument to $user->hasRight(); default: element name + * - 'rights_permlevel1': if speciifed, will be passed as the 2nd argument; default: 'lire' + * - 'rights_permlevel2': if specified, 3rd argument + */ + $elementConfiguration = [ + 'facture' => [], + 'commande' => [], + 'shipping' => [ + 'multicompany_element' => '', + ], + 'propal' => [], + 'project' => [], + 'project_task' => [ + 'multicompany_element' => '', + 'join_to_soc' => false, + ], + 'societe' => [ + 'ref_field' => 'nom', + 'join_to_soc' => false, + ], + 'contact' => [ + 'multicompany_element' => 'socpeople', + 'ref_field' => 'lastname', + 'rights_module' => 'societe', + 'rights_permlevel1' => 'contact', + 'rights_permlevel2' => 'lire', + ], + 'actioncomm' => [ + 'id_field' => 'id', + 'ref_field' => 'id', + 'check_read_permission' => false, + ], + 'product' => [ + 'join_to_soc' => false, + ], + 'facture_fournisseur' => [ + 'multicompany_element' => 'facture_fourn', + ], + 'commande_fournisseur' => [], + 'fichinter' => [ + 'multicompany_element' => '', + 'rights_module' => 'ficheinter', + ], + 'contrat' => [], + 'ticket' => [ + 'rights_permlevel1' => 'read' + ], + ]; + + if (isModEnabled("assetatm")) { + // todo: implémenter le hook dans le module concerné + $elementConfiguration['assetatm'] = [ + 'table' => 'assetatm', + 'ref_field' => 'serial_number', + 'id_field' => 'rowid', + 'join_to_soc' => false + ]; + } if (isModEnabled('chiffrage')) { - $TType[] = 'chiffrage'; + // todo: implémenter le hook dans le module concerné + $elementConfiguration['chiffrage'] = [ + 'table' => 'chiffrage_chiffrage', + 'multicompany_element' => 'chiffrage_chiffrage', + 'rights_permlevel1' => 'chiffrage', + 'rights_permlevel2' => 'read', + ]; } - //$TType=array('facture_fournisseur', 'commande_fournisseur'); - foreach($TType as $type) { - $Tab[$type] = _search_type($type, $keyword); - } + $parameters = ['supportedType' => &$elementConfiguration]; + $hookmanager->executeHooks('relatedAddSupportedObjectTypes', $parameters); - return $Tab; + $searchResults = []; + foreach ($elementConfiguration as $element => $typeSpecificData) { + $elementProperties = getElementProperties($element); + if (!isModEnabled($typeSpecificData['module'] ?? $elementProperties['module'] ?? $element)) { + continue; + } + if ($typeSpecificData['check_read_permission'] ?? true) { + // by default, we check if the user has read permission on this element. + $rightsModule = $typeSpecificData['rights_module'] ?? $elementProperties['element'] ?? $element; + $rightsPermLevel1 = $typeSpecificData['rights_permlevel1'] ?? 'lire'; + $rightsPermLevel2 = $typeSpecificData['rights_permlevel2'] ?? ''; + if (! $user->hasRight($rightsModule, $rightsPermLevel1, $rightsPermLevel2)) { + // user not allowed to view this element type => we don't add it to the ajax response + continue; + } + } + $searchResults[$element] = _search_type($element, $typeSpecificData, $keyword); + } + return $searchResults; } - - /** * @param string $table * @return bool */ -function _checkTableExist(string $table): bool { +function _checkTableExist(string $table): bool +{ global $db; $res = $db->query('SHOW TABLES LIKE \''.$db->escape($table).'\' '); - if (!$res) { + if (! $res) { return false; } - if ($db->num_rows($res)>0) { + if ($db->num_rows($res) > 0) { return true; - }else{ + } else { return false; } } -function _search_type($type, $keyword) { - global $db, $conf, $langs; - - $table = $db->prefix().$type; - $objname = ucfirst($type); - $id_field = 'rowid'; - $ref_field = 'ref'; - $ref_field2 = ''; - $join_to_soc = false; - $element =''; - - if ($type == 'company') { - $table = $db->prefix() . 'societe'; - $objname = 'Societe'; - $element = 'societe'; - $ref_field = 'nom'; - } elseif ($type == 'project') { - $table = $db->prefix() . 'projet'; - $objname = 'Project'; - $element = 'project'; - $join_to_soc = true; - } elseif ($type == 'task' || $type == 'project_task') { - $table = $db->prefix() . 'projet_task'; - $objname = 'Task'; - $id_field = 'rowid'; - $ref_field = 'ref'; - $join_to_soc = true; - } elseif ($type == 'event' || $type == 'action') { - $table = $db->prefix() . 'actioncomm'; - $objname = 'ActionComm'; - $id_field = 'id'; - $ref_field = 'id'; - $ref_field2 = 'label'; - $element = 'actioncomm'; - $join_to_soc = true; - } elseif ($type == 'order' || $type == 'commande') { - $table = $db->prefix() . 'commande'; - $objname = 'Commande'; - $element = 'commande'; - $join_to_soc = true; - } elseif ($type == 'shipping') { - $table = $db->prefix() . 'expedition'; - $objname = 'Expedition'; - $join_to_soc = true; - } elseif ($type == 'invoice') { - $table = $db->prefix() . 'facture'; - $objname = 'Facture'; - $ref_field = 'ref'; - $element = 'facture'; - $join_to_soc = true; - } elseif ($type == 'contact') { - $table = $db->prefix() . 'socpeople'; - $ref_field = 'lastname'; - $element = 'socpeople'; - $join_to_soc = true; - } elseif ($type == 'propal') { - $table = $db->prefix() . 'propal'; - $ref_field = 'ref'; - $element = 'propal'; - $join_to_soc = true; - } elseif ($type == 'product') { - $table = $db->prefix() . 'product'; - $ref_field = 'ref'; - $element = 'product'; +function _search_type(string $type, array $typeSpecificData, string $keyword) +{ + global $db; - } elseif ($type == 'facture_fournisseur') { - $table = $db->prefix() . 'facture_fourn'; - //$id_field='rowid'; - $objname = 'FactureFourn'; - $ref_field = 'ref'; - $element = 'facture_fourn'; - $join_to_soc = true; - } elseif ($type == 'commande_fournisseur') { - $table = $db->prefix() . 'commande_fournisseur'; - $objname = 'CommandeFournisseur'; - $ref_field = 'ref'; - $element = 'commande_fournisseur'; - $join_to_soc = true; - } elseif ($type == 'contrat') { - $table = $db->prefix() . 'contrat'; - $ref_field = 'ref'; - $element = 'contrat'; - $join_to_soc = true; - } elseif ($type == 'fichinter') { - $table = $db->prefix() . 'fichinter'; - $objname = 'Fichinter'; - $ref_field = 'ref'; - $join_to_soc = true; - } else if (isModEnabled("assetatm") && $type == 'assetatm') { - $table = $db->prefix() . 'assetatm'; - $objname = 'TAsset'; - $ref_field = 'serial_number'; - $id_field = 'rowid'; - } elseif (isModEnabled('chiffrage') && $type == 'chiffrage') { - $table = $db->prefix() . 'chiffrage_chiffrage'; - $objname = 'Chiffrage'; - $element = 'chiffrage_chiffrage'; - $join_to_soc = true; - } + // getElementProperties() is precisely here to help us handle naming exceptions for core elements + // (and there is a hook `getElementProperties` too that modules can implement if their objects + // don't use the standard naming pattern). + $elementProperties = getElementProperties($type); + $table = $typeSpecificData['table_element'] ?? $elementProperties['table_element'] ?? $type; + $id_field = $typeSpecificData['id_field'] ?? 'rowid'; + $ref_field = $typeSpecificData['ref_field'] ?? 'ref'; + $ref_field2 = $typeSpecificData['ref_field2'] ?? ''; + $join_to_soc = $typeSpecificData['join_to_soc'] ?? true; + $multicompanyElement = $typeSpecificData['multicompany_element'] ?? ''; // From Dolibarr V19 tables are created at Dolibarr installation but after module activation // so we need to check if table exist - if(!_checkTableExist($table)){ + if (! _checkTableExist($db->prefix().$table)) { return []; } $Tab = array(); - - $sql = "SELECT t.".$id_field." as rowid, CONCAT(t.".$ref_field." ".( empty($ref_field2) ? '' : ",' ',t.".$ref_field2 )." ) as ref "; - - if($join_to_soc) { - if($type == 'task') { - $sql.=",CONCAT(p.title,', ',s.nom) as client"; - } - else if($type == 'order' || $type == 'commande') { - $sql.=",CONCAT(s.nom , ', Date : ' , DATE_FORMAT(t.date_commande,'%m-%d-%Y')) as client"; - } - else { - $sql.=",s.nom as client"; + $sql = "SELECT t.".$id_field." as rowid, CONCAT(t.".$ref_field." ".(empty($ref_field2) ? '' : ",' ',t.".$ref_field2)." ) as ref "; + + if ($join_to_soc) { + if ($type == 'task') { + $sql .= ",CONCAT(p.title,', ',s.nom) as client"; + } else { + if ($type == 'order' || $type == 'commande') { + $sql .= ",CONCAT(s.nom , ', Date : ' , DATE_FORMAT(t.date_commande,'%m-%d-%Y')) as client"; + } else { + $sql .= ",s.nom as client"; + } } } - $sql.=" FROM ".$table." as t "; + $sql .= " FROM ".$db->prefix().$table." as t "; - if($join_to_soc) { - if($type == 'task') { - $sql.=" LEFT JOIN ".$db->prefix()."projet p ON (p.rowid = t.fk_projet) "; - $sql.=" LEFT JOIN ".$db->prefix()."societe s ON (s.rowid = p.fk_soc) "; - } - else { - $sql.=" LEFT JOIN ".$db->prefix()."societe s ON (s.rowid = t.fk_soc) "; + if ($join_to_soc) { + if ($type == 'task') { + $sql .= " LEFT JOIN ".$db->prefix()."projet p ON (p.rowid = t.fk_projet) "; + $sql .= " LEFT JOIN ".$db->prefix()."societe s ON (s.rowid = p.fk_soc) "; + } else { + $sql .= " LEFT JOIN ".$db->prefix()."societe s ON (s.rowid = t.fk_soc) "; } } - $sql.=" WHERE 1 "; + $sql .= " WHERE 1 "; - if(!empty($element)) - { - $sql.= ' AND t.entity IN (' . getEntity($element) . ') '; + if (! empty($multicompanyElement)) { + $sql .= ' AND t.entity IN ('.getEntity($multicompanyElement).') '; } - if ($db->type == 'pgsql' && ($ref_field=='id' || $ref_field=='rowid')) { - $sql.=" AND CAST(t.".$ref_field." AS TEXT) LIKE '".$keyword."%' "; + if ($db->type == 'pgsql' && ($ref_field == 'id' || $ref_field == 'rowid')) { + $sql .= " AND CAST(t.".$ref_field." AS TEXT) LIKE '".$keyword."%' "; } else { - $sql.=" AND t.".$ref_field." LIKE '".$keyword."%' "; + $sql .= " AND t.".$ref_field." LIKE '".$keyword."%' "; } - if (!empty($ref_field2) && $db->type == 'pgsql' && ($ref_field2=='id' || $ref_field2=='rowid')) { - $sql.=" OR CAST(t.".$ref_field2." AS TEXT) LIKE '".$keyword."%' "; - } elseif (!empty($ref_field2)) { - $sql.=" OR t.".$ref_field2." LIKE '".$keyword."%' "; + if (! empty($ref_field2) && $db->type == 'pgsql' && ($ref_field2 == 'id' || $ref_field2 == 'rowid')) { + $sql .= " OR CAST(t.".$ref_field2." AS TEXT) LIKE '".$keyword."%' "; + } elseif (! empty($ref_field2)) { + $sql .= " OR t.".$ref_field2." LIKE '".$keyword."%' "; } - $sql.=" LIMIT 20 "; -// var_dump($sql); - //$sql="SELECT ff.ref FROM ".MAIN_DB_PREFIX."facture_fourn ff WHERE "; + $sql .= " LIMIT 20 "; $res = $db->query($sql); - if($res === false) { - pre($db,true); + if ($res === false) { + return []; } $nb_results = $db->num_rows($res); - - if($nb_results == 0) { + if ($nb_results == 0) { return array(); - } - else{ - while($obj = $db->fetch_object($res)) { - + } else { + while ($obj = $db->fetch_object($res)) { $r = $obj->ref; - if(!empty($obj->client))$r.=', '.$obj->client; + if (! empty($obj->client)) { + $r .= ', '.$obj->client; + } $Tab[$obj->rowid] = $r; - } + return $Tab; } - - - }