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;
}
-
-
-
}