diff --git a/htdocs/accountancy/admin/index.php b/htdocs/accountancy/admin/index.php
index 68b92a922ea..17b9583f36e 100644
--- a/htdocs/accountancy/admin/index.php
+++ b/htdocs/accountancy/admin/index.php
@@ -68,7 +68,17 @@ $formSetup = new FormSetup($db);
// Main options
$formSetup->newItem('BANK_DISABLE_DIRECT_INPUT')->setAsYesNo();
-$formSetup->newItem('ACCOUNTANCY_COMBO_FOR_AUX')->setAsYesNo();
+// Auxiliary account select mode
+$arrval = array(
+ (string) 0 => $langs->trans("No").' - '.$langs->trans("FreeInput").'',
+ (string) 1 => $langs->trans("Yes").' - '.$langs->trans("DropDownList").'',
+ (string) 2 => $langs->trans("Yes").' - '.$langs->trans("NumberOfKeyToSearch", 1).'',
+ (string) 3 => $langs->trans("Yes").' - '.$langs->trans("NumberOfKeyToSearch", 2).'',
+ (string) 4 => $langs->trans("Yes").' - '.$langs->trans("NumberOfKeyToSearch", 3).'',
+);
+$item = $formSetup->newItem('ACCOUNTANCY_AUXACCOUNT_USE_SEARCH_TO_SELECT');
+$item->setAsSelect($arrval);
+$item->helpText = $langs->trans('UseSearchToSelectAuxAccountTooltip');
$item = $formSetup->newItem('ACCOUNTING_MANAGE_ZERO')->setAsYesNo();
$item->helpText = $langs->trans('ACCOUNTING_MANAGE_ZERO2');
diff --git a/htdocs/accountancy/admin/template/card.php b/htdocs/accountancy/admin/template/card.php
index d6bd5e6c8f6..fb27fefa6a7 100644
--- a/htdocs/accountancy/admin/template/card.php
+++ b/htdocs/accountancy/admin/template/card.php
@@ -576,11 +576,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
// It does not use the setup of "key pressed" to select a thirdparty and this hang browser on large databases.
// Also, it is not possible to use a value that is not in the list.
// Also, the label is not automatically filled when a value is selected.
- if (getDolGlobalString('ACCOUNTANCY_COMBO_FOR_AUX')) {
- print $formaccounting->select_auxaccount((GETPOSTISSET("subledger_account") ? GETPOST("subledger_account", "alpha") : $line->subledger_account), 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
- } else {
- print 'subledger_account).'" placeholder="'.dol_escape_htmltag($langs->trans("SubledgerAccount")).'">';
- }
+ print $formaccounting->select_auxaccount((GETPOSTISSET("subledger_account") ? GETPOST("subledger_account", "alpha") : $line->subledger_account), 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
// Add also input for subledger label
print ' subledger_label).'" placeholder="'.dol_escape_htmltag($langs->trans("SubledgerAccountLabel")).'">';
print '';
@@ -620,15 +616,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
print $formaccounting->select_account('', 'general_account', 1, [], 1, 1, 'maxwidth300');
print '';
print '
';
- // TODO For the moment we keep a free input text instead of a combo. The select_auxaccount has problem because:
- // It does not use the setup of "key pressed" to select a thirdparty and this hang browser on large databases.
- // Also, it is not possible to use a value that is not in the list.
- // Also, the label is not automatically filled when a value is selected.
- if (getDolGlobalString('ACCOUNTANCY_COMBO_FOR_AUX')) {
- print $formaccounting->select_auxaccount('', 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
- } else {
- print '';
- }
+ print $formaccounting->select_auxaccount('', 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
print ' ';
print '
';
- // TODO For the moment we keep a free input text instead of a combo. The select_auxaccount has problem because:
- // - It does not use the setup of "key pressed" to select a thirdparty and this hang browser on large databases.
- // - Also, it is not possible to use a value that is not in the list.
- // - Also, the label is not automatically filled when a value is selected.
- if (getDolGlobalString('ACCOUNTANCY_COMBO_FOR_AUX')) {
- print $formaccounting->select_auxaccount((GETPOSTISSET("subledger_account") ? GETPOST("subledger_account", "alpha") : $line->subledger_account), 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
- } else {
- print 'subledger_account).'" placeholder="'.dol_escape_htmltag($langs->trans("SubledgerAccount")).'">';
- }
+ print $formaccounting->select_auxaccount((GETPOSTISSET("subledger_account") ? GETPOST("subledger_account", "alpha") : $line->subledger_account), 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
// Add also input for subledger label
print ' subledger_label).'" placeholder="'.dol_escape_htmltag($langs->trans("SubledgerAccountLabel")).'">';
print '
';
- // TODO For the moment we keep a free input text instead of a combo. The select_auxaccount has problem because:
- // It does not use the setup of "key pressed" to select a thirdparty and this hang browser on large databases.
- // Also, it is not possible to use a value that is not in the list.
- // Also, the label is not automatically filled when a value is selected.
- if (getDolGlobalString('ACCOUNTANCY_COMBO_FOR_AUX')) {
- print $formaccounting->select_auxaccount('', 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
- } else {
- print '';
- }
+ print $formaccounting->select_auxaccount('', 'subledger_account', 1, 'maxwidth250', '', 'subledger_label');
print ' ';
print '
';
print '
';
diff --git a/htdocs/accountancy/bookkeeping/export.php b/htdocs/accountancy/bookkeeping/export.php
index 63f82b1137f..65b605e3a3e 100644
--- a/htdocs/accountancy/bookkeeping/export.php
+++ b/htdocs/accountancy/bookkeeping/export.php
@@ -1045,19 +1045,12 @@ if (!empty($arrayfields['t.numero_compte']['checked'])) {
// Subledger account
if (!empty($arrayfields['t.subledger_account']['checked'])) {
print '
';
- // TODO For the moment we keep a free input text instead of a combo. The select_auxaccount has problem because it does not
- // use setup of "keypress to select thirdparty" and this hangs browser on large databases.
- if (getDolGlobalString('ACCOUNTANCY_COMBO_FOR_AUX')) {
- print '
';
- // TODO For the moment we keep a free input text instead of a combo. The select_auxaccount has problem because it does not
- // use setup of keypress to select thirdparty and this hang browser on large database.
- if (getDolGlobalString('ACCOUNTANCY_COMBO_FOR_AUX')) {
- print '
';
}
diff --git a/htdocs/core/ajax/auxaccount.php b/htdocs/core/ajax/auxaccount.php
new file mode 100644
index 00000000000..ff4f35d9fea
--- /dev/null
+++ b/htdocs/core/ajax/auxaccount.php
@@ -0,0 +1,145 @@
+
+ *
+ * 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 .
+ */
+
+/**
+ * \file htdocs/core/ajax/auxaccount.php
+ * \ingroup Accountancy (Double entries)
+ * \brief Ajax endpoint to search auxiliary accounts (thirdparties + users)
+ * Called by select_auxaccount() when ACCOUNTANCY_AUXACCOUNT_USE_SEARCH_TO_SELECT >= 2
+ */
+
+// Minimum ajax defines
+if (!defined('NOTOKENRENEWAL')) {
+ define('NOTOKENRENEWAL', 1); // Disables token renewal
+}
+if (!defined('NOREQUIREMENU')) {
+ define('NOREQUIREMENU', '1');
+}
+if (!defined('NOREQUIREHTML')) {
+ define('NOREQUIREHTML', '1');
+}
+if (!defined('NOREQUIREAJAX')) {
+ define('NOREQUIREAJAX', '1');
+}
+if (!defined('NOHEADERNOFOOTER')) {
+ define('NOHEADERNOFOOTER', '1');
+}
+
+require '../../main.inc.php';
+
+/**
+ * @var Conf $conf
+ * @var DoliDB $db
+ * @var Translate $langs
+ * @var User $user
+ */
+
+// Security — must be logged in
+if (!$user->id) {
+ accessforbidden();
+}
+
+// Security
+if (!isModEnabled('accounting') || !$user->hasRight('accounting', 'mouvements', 'lire')) {
+ accessforbidden();
+}
+
+$htmlname = GETPOST('htmlname', 'aZ09') ?: 'account_num_aux';
+$searchkey = GETPOST(str_replace('.', '_', $htmlname), 'alpha');
+$outjson = GETPOSTINT('outjson');
+$limit = getDolGlobalInt('ACCOUNTANCY_AUXACCOUNT_SEARCH_LIMIT', 100);
+$minLength = getDolGlobalInt('ACCOUNTANCY_AUXACCOUNT_USE_SEARCH_TO_SELECT', 2) - 1;
+
+$results = array();
+
+if (strlen($searchkey) >= $minLength) {
+ // Search anywhere by default (use LIKE '%term%')
+ $prefix = getDolGlobalString('ACCOUNTANCY_AUXACCOUNT_DONOTSEARCH_ANYWHERE') ? '' : '%';
+
+ // --- Thirdparties (customers and/or suppliers) ---
+ $sql = "SELECT code_compta AS code_client, code_compta_fournisseur AS code_fourn, nom AS name";
+ $sql .= " FROM ".$db->prefix()."societe";
+ $sql .= " WHERE entity IN (".getEntity('societe').")";
+ $sql .= " AND (client IN (1,3) OR fournisseur = 1)";
+ $sql .= " AND (code_compta LIKE '".$db->escape($prefix.$searchkey)."%'";
+ $sql .= " OR code_compta_fournisseur LIKE '".$db->escape($prefix.$searchkey)."%'";
+ $sql .= " OR nom LIKE '".$db->escape($prefix.$searchkey)."%')";
+ $sql .= $db->order("nom", "ASC");
+ $sql .= $db->plimit($limit, 0);
+
+ $resql = $db->query($sql);
+ if ($resql) {
+ while ($obj = $db->fetch_object($resql)) {
+ if (!empty($obj->code_client)) {
+ $key = $obj->code_client;
+ $results[$key] = array(
+ 'key' => $key,
+ 'value' => $key, // value shown in input once selected
+ 'label' => $key.' ('.$obj->name.')', // label shown in dropdown list
+ );
+ }
+ if (!empty($obj->code_fourn) && !isset($results[$obj->code_fourn])) {
+ $key = $obj->code_fourn;
+ $results[$key] = array(
+ 'key' => $key,
+ 'value' => $key,
+ 'label' => $key.' ('.$obj->name.')',
+ );
+ }
+ }
+ $db->free($resql);
+ } else {
+ dol_syslog("auxaccount.php: error societe query ".$db->lasterror(), LOG_ERR);
+ }
+
+ // --- Users ---
+ $sql2 = "SELECT DISTINCT accountancy_code, lastname, firstname";
+ $sql2 .= " FROM ".$db->prefix()."user";
+ $sql2 .= " WHERE entity IN (".getEntity('user').")";
+ $sql2 .= " AND accountancy_code != ''";
+ $sql2 .= " AND (accountancy_code LIKE '".$db->escape($prefix.$searchkey)."%'";
+ $sql2 .= " OR lastname LIKE '".$db->escape($prefix.$searchkey)."%'";
+ $sql2 .= " OR firstname LIKE '".$db->escape($prefix.$searchkey)."%')";
+ $sql2 .= $db->order("accountancy_code", "ASC");
+ $sql2 .= $db->plimit($limit, 0);
+
+ $resql2 = $db->query($sql2);
+ if ($resql2) {
+ while ($obj = $db->fetch_object($resql2)) {
+ if (!empty($obj->accountancy_code) && !isset($results[$obj->accountancy_code])) {
+ $key = $obj->accountancy_code;
+ $fullname = dolGetFirstLastname($obj->firstname, $obj->lastname);
+ $results[$key] = array(
+ 'key' => $key,
+ 'value' => $key,
+ 'label' => $key.' ('.$fullname.')',
+ );
+ }
+ }
+ $db->free($resql2);
+ } else {
+ dol_syslog("auxaccount.php: error user query ".$db->lasterror(), LOG_ERR);
+ }
+
+ ksort($results);
+}
+
+// Output JSON — format expected by ajax_autocompleter(): array of {key, value, label}
+top_httphead('application/json');
+echo json_encode(array_values($results));
+
+$db->close();
diff --git a/htdocs/core/class/html.formaccounting.class.php b/htdocs/core/class/html.formaccounting.class.php
index e1480a7ab16..fb3d024f60b 100644
--- a/htdocs/core/class/html.formaccounting.class.php
+++ b/htdocs/core/class/html.formaccounting.class.php
@@ -3,7 +3,7 @@
* Copyright (C) 2013-2014 Olivier Geffroy
* Copyright (C) 2015 Ari Elbaz (elarifr)
* Copyright (C) 2016 Marcos García
- * Copyright (C) 2016-2025 Alexandre Spangaro
+ * Copyright (C) 2016-2026 Alexandre Spangaro
* Copyright (C) 2024-2026 MDW
* Copyright (C) 2024-2026 Frédéric France
*
@@ -486,20 +486,99 @@ class FormAccounting extends Form
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps
/**
* Return list of auxiliary accounts. Cumulate list from customers, suppliers and users.
+ * Behavior depends on constant ACCOUNTANCY_AUXACCOUNT_USE_SEARCH_TO_SELECT:
+ * 0 = free text input, no autocomplete, no list
+ * 1 = native HTML select with full list loaded, with select2 (original behavior)
+ * 2 = ajax search triggered after 1 character typed
+ * 3 = ajax search triggered after 2 characters typed
+ * 4 = ajax search triggered after 3 characters typed (recommended for large datasets)
*
- * @param string $selectid Preselected pcg_type
- * @param string $htmlname Name of field in html form
- * @param int|string $showempty Add an empty field
- * @param string $morecss More css
- * @param string $usecache Key to use to store result into a cache. Next call with same key will reuse the cache.
- * @param string $labelhtmlname HTML name of label for autofill of account from name.
- * @return string|int<-1,-1> String with HTML select, or -1 if error
+ * @param string $selectid Preselected account number
+ * @param string $htmlname Name of field in html form
+ * @param int|string $showempty Add an empty field
+ * @param string $morecss More css
+ * @param string $usecache Key to use for cache (mode 0 only)
+ * @param string $labelhtmlname HTML name of label input for autofill of subledger label
+ * @param string $selected_input_value Displayed text of preselected entry (ajax modes 2/3/4 only)
+ * @return string|int<-1,-1> HTML string or -1 on error
*/
- public function select_auxaccount($selectid, $htmlname = 'account_num_aux', $showempty = 0, $morecss = 'minwidth100 maxwidth300 maxwidthonsmartphone', $usecache = '', $labelhtmlname = '')
+ public function select_auxaccount($selectid, $htmlname = 'account_num_aux', $showempty = 0, $morecss = 'minwidth100 maxwidth300 maxwidthonsmartphone', $usecache = '', $labelhtmlname = '', $selected_input_value = '')
{
// phpcs:enable
- global $conf;
+ global $conf, $langs;
+ $out = '';
+ $auxaccount_mode = getDolGlobalInt('ACCOUNTANCY_AUXACCOUNT_USE_SEARCH_TO_SELECT', 0);
+
+ // MODE 0: free text input - no list, no autocomplete
+ if ($auxaccount_mode == 0) {
+ $out .= '';
+ return $out;
+ }
+
+ // MODES 2/3/4: ajax - on-demand loading via endpoint
+ // minLength: mode 2 -> 1 char, mode 3 -> 2 chars, etc
+ if (!empty($conf->use_javascript_ajax) && $auxaccount_mode >= 2) {
+ require_once DOL_DOCUMENT_ROOT.'/core/lib/ajax.lib.php';
+
+ $minLength = $auxaccount_mode - 1;
+ $urloption = 'htmlname='.urlencode($htmlname).'&outjson=1';
+
+ // Resolve displayed text from selectid if no label was provided (e.g. on page reload)
+ if ($selectid && empty($selected_input_value)) {
+ // Search in thirdparties first
+ $sql = "SELECT nom AS name FROM ".$this->db->prefix()."societe";
+ $sql .= " WHERE entity IN (".getEntity('societe').")";
+ $sql .= " AND (code_compta = '".$this->db->escape($selectid)."'";
+ $sql .= " OR code_compta_fournisseur = '".$this->db->escape($selectid)."')";
+ $sql .= $this->db->plimit(1, 0);
+ $resql = $this->db->query($sql);
+ if ($resql && ($obj = $this->db->fetch_object($resql))) {
+ $selected_input_value = $selectid.' ('.$obj->name.')';
+ $this->db->free($resql);
+ } else {
+ // Fallback: search in users
+ $sql2 = "SELECT lastname, firstname FROM ".$this->db->prefix()."user";
+ $sql2 .= " WHERE entity IN (".getEntity('user').")";
+ $sql2 .= " AND accountancy_code = '".$this->db->escape($selectid)."'";
+ $sql2 .= $this->db->plimit(1, 0);
+ $resql2 = $this->db->query($sql2);
+ if ($resql2 && ($obj2 = $this->db->fetch_object($resql2))) {
+ $selected_input_value = $selectid.' ('.dolGetFirstLastname($obj2->firstname, $obj2->lastname).')';
+ $this->db->free($resql2);
+ } else {
+ $selected_input_value = $selectid;
+ }
+ }
+ }
+
+ // Visible text input — targeted by ajax_autocompleter() JavaScript via "input#search_$htmlname"
+ $placeholder = (!empty($showempty) && !is_numeric($showempty)) ? $showempty : $langs->trans("Search");
+ $out .= '';
+
+ // Hidden input + jQuery UI autocomplete JS — the hidden id="$htmlname" is generated by ajax_autocompleter() itself
+ $out .= ajax_autocompleter($selectid, $htmlname, DOL_URL_ROOT.'/core/ajax/auxaccount.php', $urloption, $minLength, 0, array());
+
+ // Autofill subledger label — listen on "change" of the hidden field
+ // ajax_autocompleter() calls .val(ui.item.id).trigger("change") on the hidden field upon selection
+ if (!empty($labelhtmlname)) {
+ $out .= '';
+ }
+
+ return $out;
+ }
+
+ // MODE 1 (or ajax disabled): native HTML select with full list + select2 - original behavior
$aux_account = array();
if ($usecache && !empty($this->options_cache[$usecache])) {
@@ -528,11 +607,10 @@ class FormAccounting extends Form
dol_syslog(get_class($this)."::select_auxaccount ".$this->error, LOG_ERR);
return -1;
}
-
$this->db->free($resql);
// Auxiliary user account
- $sql = "SELECT DISTINCT accountancy_code, lastname, firstname ";
+ $sql = "SELECT DISTINCT accountancy_code, lastname, firstname";
$sql .= " FROM ".$this->db->prefix()."user";
$sql .= " WHERE entity IN (".getEntity('user').")";
$sql .= " ORDER BY accountancy_code";
@@ -558,21 +636,20 @@ class FormAccounting extends Form
}
}
- // Build select
- $out = '';
+ // Build native select — select2 is automatically activated by Form::selectarray() with $useajax=1
$out .= Form::selectarray($htmlname, $aux_account, $selectid, ($showempty ? (is_numeric($showempty) ? 1 : $showempty) : 0), 0, 0, '', 0, 0, 0, '', $morecss, 1);
- //automatic filling if we give the name of the subledger_label input
+
+ // Autofill subledger label on select2 selection event
if (!empty($conf->use_javascript_ajax) && !empty($labelhtmlname)) {
$out .= '';
+ jQuery(document).ready(() => {
+ $("#'.dol_escape_js($htmlname).'").on("select2:select", function(e) {
+ var regExp = /\(([^)]+)\)/;
+ const match = regExp.exec(e.params.data.text);
+ if (match) { $("input[name=\"'.dol_escape_js($labelhtmlname).'\"]").val(match[1]); }
+ });
+ });
+ ';
}
return $out;
diff --git a/htdocs/install/mysql/migration/23.0.0-24.0.0.sql b/htdocs/install/mysql/migration/23.0.0-24.0.0.sql
index a1960e624ab..0776d39479d 100644
--- a/htdocs/install/mysql/migration/23.0.0-24.0.0.sql
+++ b/htdocs/install/mysql/migration/23.0.0-24.0.0.sql
@@ -193,6 +193,8 @@ INSERT INTO llx_c_email_templates (entity, module, type_template, lang, private,
ALTER TABLE llx_c_ticket_category ADD COLUMN fk_ticket_type integer NULL;
+UPDATE llx_const SET name = __ENCRYPT('ACCOUNTANCY_AUXACCOUNT_USE_SEARCH_TO_SELECT')__ WHERE __DECRYPT('name')__ = 'ACCOUNTANCY_COMBO_FOR_AUX';
+
ALTER TABLE llx_prelevement_bons ADD COLUMN fk_user_modif integer;
create table llx_product_lang_extrafields
diff --git a/htdocs/langs/en_US/accountancy.lang b/htdocs/langs/en_US/accountancy.lang
index 50bb879da49..0eebc8b7244 100644
--- a/htdocs/langs/en_US/accountancy.lang
+++ b/htdocs/langs/en_US/accountancy.lang
@@ -170,7 +170,10 @@ ACCOUNTING_BANK_CONCILIATED=Transfer into accounting only the lines reconciliate
BANK_DISABLE_DIRECT_INPUT=Disable direct recording of transaction in bank account
ACCOUNTANCY_ER_DATE_RECORD=Use the end date of the period of an expense report as the date for the transfer to accounting, instead of the date of the expense
ACCOUNTING_ENABLE_EXPORT_DRAFT_JOURNAL=Enable draft export on journal
-ACCOUNTANCY_COMBO_FOR_AUX=Enable combo list for subsidiary account (may be slow if you have a lot of third parties, break ability to search on a part of value)
+ACCOUNTANCY_AUXACCOUNT_USE_SEARCH_TO_SELECT=Enable combo list for subsidiary account (may be slow if you have a lot of third parties, optimize the selection of auxiliary accounts)
+UseSearchToSelectAuxAccountTooltip=By default, the full list of auxiliary accounts is loaded into a combo list. When the number of third parties is high (more than 2000), this can slow down pages. You can activate a search mode to load only matching results on demand via Ajax, or switch to a free text input.
+FreeInput=Free text input
+DropDownList=Drop-down list
ACCOUNTING_DATE_START_BINDING=Disable binding & transfer in accountancy when date is below this date (the transactions before this date will be excluded by default)
ACCOUNTING_DEFAULT_PERIOD_ON_TRANSFER=On the page to transfer data into accountancy, what is the period selected by default
ACCOUNTING_LABEL_OPERATION_ON_TRANSFER=When accounting is generated, default operation label