diff --git a/modules/partitario/dettagli_conto2.php b/modules/partitario/dettagli_conto2.php index 8da3cafc4..d9718b4f1 100644 --- a/modules/partitario/dettagli_conto2.php +++ b/modules/partitario/dettagli_conto2.php @@ -20,235 +20,552 @@ include_once __DIR__.'/../../core.php'; +/** + * Costruisce le celle HTML di una riga sottoconto. Riusata sia dal render completo + * (mastri sotto soglia, client-side) sia dalla risposta JSON server-side (oltre soglia), + * così la query e il markup della riga vivono una volta sola. + * + * @return array Celle: [sottoconto, importo, (importo reddito se Economico), vuota] + */ +function partitario_sottoconto_cells($conto_terzo, $conto_secondo, $conto_primo) +{ + $is_economico = $conto_primo['descrizione'] == 'Economico'; + $numero_movimenti = $conto_terzo['numero_movimenti']; + + $totale_conto = $conto_terzo['totale']; + $totale_reddito = $conto_terzo['totale_reddito']; + if ($conto_primo['descrizione'] != 'Patrimoniale') { + $totale_conto = -$totale_conto ?: 0; + $totale_reddito = -$totale_reddito ?: 0; + } + + $cella = ''; + + // Possibilità di esplodere i movimenti del conto + if (!empty($numero_movimenti)) { + $cella .= ''; + } + + // Span con i pulsanti + $cella .= ''; + + // Possibilità di visionare l'anagrafica + $id_anagrafica = $conto_terzo['id_anagrafica']; + $anagrafica_deleted = $conto_terzo['deleted_at']; + if (isset($id_anagrafica)) { + $cella .= Modules::link('Anagrafiche', $id_anagrafica, ' '); + } + + // Stampa mastrino + if (!empty($numero_movimenti)) { + $cella .= Prints::getLink('Mastrino', $conto_terzo['id'], 'btn-info btn-xs', '', null, 'lev=3'); + } + + // Pulsante per aggiornare il totale reddito del conto di livello 3 + $cella .= ''; + + // Pulsante per modificare il nome del conto di livello 3 + $cella .= ''; + + // Possibilità di eliminare il conto se non ci sono movimenti collegati + if ($numero_movimenti <= 0) { + $cella .= ''; + } + + $cella .= ''; + + // Span con info del conto + contenitore per l'espansione dei movimenti + $deducibile = $conto_terzo['percentuale_deducibile'] != '100.00' + ? tr('(deducibile al _PERC_%', ['_PERC_' => Translator::numberToLocale($conto_terzo['percentuale_deducibile'], 0)]).')' + : ''; + $cella .= ' '.$conto_secondo['numero'].'.'.$conto_terzo['numero'].' '.$conto_terzo['descrizione'].' '.$deducibile.''; + $cella .= '
'; + + $cells = [$cella, moneyFormat($totale_conto, 2)]; + if ($is_economico) { + $cells[] = moneyFormat($totale_reddito, 2); + } + $cells[] = ''; + + return $cells; +} + $id_conto = get('id_conto'); $conto_secondo = $dbo->selectOne('co_piano_dei_conti2', '*', ['id' => $id_conto]); $conto_primo = $dbo->selectOne('co_piano_dei_conti1', '*', ['id' => $conto_secondo['id_piano_dei_conti1']]); +$is_economico = $conto_primo['descrizione'] == 'Economico'; + +// Oltre la soglia configurabile i sottoconti del mastro vengono mostrati come DataTable +// (ricerca + impaginazione server-side). Il conteggio si basa sul totale dei sottoconti +// del mastro, indipendentemente da eventuali filtri. +$soglia_datatable = (int) setting('Soglia datatable sottoconti'); +if ($soglia_datatable <= 0) { + $soglia_datatable = 500; +} -// Livello 3 -$query3 = 'SELECT `co_piano_dei_conti3`.*, movimenti.numero_movimenti, movimenti.totale, movimenti.totale_reddito, id_anagrafica, anagrafica.deleted_at - FROM `co_piano_dei_conti3` - LEFT OUTER JOIN ( - SELECT id as id_anagrafica, - id_conto_cliente, - id_conto_fornitore, - deleted_at - FROM an_anagrafiche - ) AS anagrafica ON co_piano_dei_conti3.id IN (anagrafica.id_conto_cliente, anagrafica.id_conto_fornitore) - LEFT OUTER JOIN ( - SELECT COUNT(id_conto) AS numero_movimenti, - id_conto, - SUM( - CASE - WHEN co_movimenti.data BETWEEN '.prepare($_SESSION['period_start']).' AND '.prepare($_SESSION['period_end']).' THEN - totale - ELSE - 0 - END - ) AS totale, - SUM( - CASE - WHEN data_inizio_competenza IS NULL OR data_fine_competenza IS NULL THEN - totale_reddito - ELSE - totale_reddito * ( - DATEDIFF( - LEAST(data_fine_competenza, '.prepare($_SESSION['period_end']).'), - GREATEST(data_inizio_competenza, '.prepare($_SESSION['period_start']).') - ) + 1 - ) / ( - DATEDIFF(data_fine_competenza, data_inizio_competenza) + 1 - ) - END - ) AS totale_reddito - FROM co_movimenti - WHERE ( - (data BETWEEN '.prepare($_SESSION['period_start']).' AND '.prepare($_SESSION['period_end']).') - OR - (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND - data_fine_competenza >= '.prepare($_SESSION['period_start']).' AND - data_inizio_competenza <= '.prepare($_SESSION['period_end']).') - OR - (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND - data_inizio_competenza < '.prepare($_SESSION['period_start']).' AND - data_fine_competenza > '.prepare($_SESSION['period_end']).') - OR - (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND - data_inizio_competenza <= '.prepare($_SESSION['period_end']).' AND - data_inizio_competenza >= '.prepare($_SESSION['period_start']).') - OR - (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND - data_fine_competenza >= '.prepare($_SESSION['period_start']).' AND - data_fine_competenza <= '.prepare($_SESSION['period_end']).') - ) GROUP BY id_conto - ) movimenti ON co_piano_dei_conti3.id=movimenti.id_conto +// Subquery dei movimenti per conto (periodo + competenza). Definita una volta e riusata +// sia dalla query di pagina sia dal totale del footer. +$movimenti_subquery = '( + SELECT COUNT(id_conto) AS numero_movimenti, + id_conto, + SUM( + CASE + WHEN co_movimenti.data BETWEEN '.prepare($_SESSION['period_start']).' AND '.prepare($_SESSION['period_end']).' THEN totale + ELSE 0 + END + ) AS totale, + SUM( + CASE + WHEN data_inizio_competenza IS NULL OR data_fine_competenza IS NULL THEN totale_reddito + ELSE totale_reddito * ( + DATEDIFF( + LEAST(data_fine_competenza, '.prepare($_SESSION['period_end']).'), + GREATEST(data_inizio_competenza, '.prepare($_SESSION['period_start']).') + ) + 1 + ) / (DATEDIFF(data_fine_competenza, data_inizio_competenza) + 1) + END + ) AS totale_reddito + FROM co_movimenti + WHERE ( + (data BETWEEN '.prepare($_SESSION['period_start']).' AND '.prepare($_SESSION['period_end']).') + OR (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND data_fine_competenza >= '.prepare($_SESSION['period_start']).' AND data_inizio_competenza <= '.prepare($_SESSION['period_end']).') + OR (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND data_inizio_competenza < '.prepare($_SESSION['period_start']).' AND data_fine_competenza > '.prepare($_SESSION['period_end']).') + OR (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND data_inizio_competenza <= '.prepare($_SESSION['period_end']).' AND data_inizio_competenza >= '.prepare($_SESSION['period_start']).') + OR (data_inizio_competenza IS NOT NULL AND data_fine_competenza IS NOT NULL AND data_fine_competenza >= '.prepare($_SESSION['period_start']).' AND data_fine_competenza <= '.prepare($_SESSION['period_end']).') + ) GROUP BY id_conto +)'; + +// Anagrafica collegata al conto: due LEFT JOIN diretti e indicizzati su an_anagrafiche +// (come cliente o fornitore) invece di un join IN su derived table, che non usava indici. +$anagrafica_select = 'COALESCE(ac.id, af.id) AS id_anagrafica, COALESCE(ac.deleted_at, af.deleted_at) AS deleted_at'; +$anagrafica_join = function ($src) { + return ' + LEFT JOIN an_anagrafiche ac ON ac.id_conto_cliente = '.$src.'.id + LEFT JOIN an_anagrafiche af ON af.id_conto_fornitore = '.$src.'.id'; +}; + +// Elenco COMPLETO dei sottoconti (mastri sotto soglia, client-side): join su tutta la tabella. +$query3_full = 'SELECT `co_piano_dei_conti3`.*, movimenti.numero_movimenti, movimenti.totale, movimenti.totale_reddito, '.$anagrafica_select.' + FROM `co_piano_dei_conti3`'.$anagrafica_join('co_piano_dei_conti3').' + LEFT OUTER JOIN '.$movimenti_subquery.' movimenti ON co_piano_dei_conti3.id=movimenti.id_conto WHERE `id_piano_dei_conti2` = '.prepare($conto_secondo['id']).' ORDER BY numero ASC'; -$terzo_livello = $dbo->fetchArray($query3); +// Conteggio totale sottoconti del mastro (decide la soglia ed è recordsTotal della DataTable). +$total_sottoconti = (int) $dbo->fetchOne('SELECT COUNT(*) AS tot FROM `co_piano_dei_conti3` WHERE `id_piano_dei_conti2` = '.prepare($conto_secondo['id']))['tot']; +$usa_datatable = $total_sottoconti > $soglia_datatable; +$datatable_id = 'sottoconti-datatable-'.$conto_secondo['id']; +$root_id = 'sottoconti-root-'.$conto_secondo['id']; + +// Filtro di ricerca server-side: descrizione oppure numero "mastro.sottoconto". +// Valore letto raw per non farlo alterare dal formatter degli input. +$search_arr = get('search', true); +$search_value = (is_array($search_arr) && isset($search_arr['value'])) ? trim((string) $search_arr['value']) : ''; +$search_where = ''; +if ($search_value !== '') { + $like = prepare('%'.$search_value.'%'); + $search_where = ' AND (`co_piano_dei_conti3`.`descrizione` LIKE '.$like.' OR CONCAT('.prepare($conto_secondo['numero']).', \'.\', `co_piano_dei_conti3`.`numero`) LIKE '.$like.')'; +} -if (!empty($terzo_livello)) { - echo ' -| ';
-
- // Possibilità di esplodere i movimenti del conto
- if (!empty($numero_movimenti)) {
+
+ ';
- // Stampa mastrino
- if (!empty($numero_movimenti)) {
- echo '
- '.Prints::getLink('Mastrino', $conto_terzo['id'], 'btn-info btn-xs', '', null, 'lev=3');
+ // Colonne (3 Patrimoniale, 4 Economico) — importi allineati a destra via className.
+ $columns_js = '{ data: "0" }, { data: "1", className: "text-right" }';
+ if ($is_economico) {
+ $columns_js .= ', { data: "2", className: "text-right" }';
}
+ $columns_js .= ', { data: "'.($is_economico ? '3' : '2').'" }';
- // Pulsante per aggiornare il totale reddito del conto di livello 3
echo '
- ';
+
+';
+ } else {
+ // Sotto soglia: elenco completo client-side (comportamento invariato).
+ $terzo_livello = $dbo->fetchArray($query3_full);
+ $totale_conto2 = 0;
+ $totale_reddito2 = 0;
echo '
- ';
+
+
+ ';
-}
+ }
-echo '
+ // Script condiviso: hover sui pulsanti + espansione movimenti. Event delegation sul
+ // contenitore (le righe arrivano dinamicamente in server-side ad ogni draw).
+ echo '
';
+
+ echo '';
+}
diff --git a/modules/partitario/edit.php b/modules/partitario/edit.php
index e54cc7f28..c5a8ece85 100755
--- a/modules/partitario/edit.php
+++ b/modules/partitario/edit.php
@@ -487,6 +487,21 @@ function eliminaConto(id_conto, level) {
});
}
+ // I sottoconti dei mastri oltre soglia sono renderizzati come DataTable: le loro
+ // righe non vanno mostrate/nascoste direttamente (ci pensa la DataTable), altrimenti
+ // si rompe l\'impaginazione. Questi helper distinguono i due casi.
+ function sottocontoNonInDatatable() {
+ return $(this).closest(".js-sottoconti-datatable").length === 0;
+ }
+
+ function forEachSottocontiDatatable(callback) {
+ $(".js-sottoconti-datatable").each(function () {
+ if ($.fn.DataTable.isDataTable(this)) {
+ callback($(this).DataTable());
+ }
+ });
+ }
+
$("#button-search").on("click", function(){
var text = $("#input-cerca").val();
@@ -506,14 +521,18 @@ function eliminaConto(id_conto, level) {
$(this).find(".search").click();
}
});
- $(".conto3").show();
+ // Azzera il filtro delle DataTable dei sottoconti
+ forEachSottocontiDatatable(function (dt) {
+ dt.search("").draw();
+ });
+ $(".conto3").filter(sottocontoNonInDatatable).show();
$(".conto1").show();
$(".conto2").show();
$(".totali").show();
} else {
$(".conto1").hide();
$(".conto2").hide();
- $(".conto3").hide();
+ $(".conto3").filter(sottocontoNonInDatatable).hide();
$(".totali").hide();
results.conti2.forEach(function(item) {
$("#conto2-"+ item).parent().parent().parent().parent().parent().show();
@@ -529,10 +548,19 @@ function eliminaConto(id_conto, level) {
});
results.conti3.forEach(function(item) {
- $("#conto3-"+ item).show();
+ var $row = $("#conto3-"+ item);
+ if ($row.length && sottocontoNonInDatatable.call($row[0])) {
+ $row.show();
+ }
});
-
+ // I mastri oltre soglia filtrano i sottoconti tramite la ricerca
+ // interna della DataTable (la regola "datatable se oltre soglia"
+ // resta valida anche con risultati filtrati). I mastri espansi ora
+ // dalla ricerca si auto-filtrano in fase di init.
+ forEachSottocontiDatatable(function (dt) {
+ dt.search(text).draw();
+ });
}
}
});
diff --git a/update/2_13.sql b/update/2_13.sql
new file mode 100644
index 000000000..8f515c7a5
--- /dev/null
+++ b/update/2_13.sql
@@ -0,0 +1,14 @@
+-- Soglia oltre la quale i sottoconti di un mastro del Piano dei conti vengono mostrati come tabella con ricerca e impaginazione
+INSERT INTO `zz_settings` (`nome`, `valore`, `tipo`, `editable`, `sezione`, `order`, `is_user_setting`) VALUES
+('Soglia datatable sottoconti', '500', 'integer', 1, 'Piano dei conti', 10, 0);
+
+INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES
+(1, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Soglia datatable sottoconti'), 'Soglia datatable sottoconti', 'Numero di sottoconti oltre il quale, espandendo un mastro nel Piano dei conti, i sottoconti vengono mostrati in una tabella con ricerca e impaginazione invece dell\'elenco semplice. Default 500.'),
+(2, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Soglia datatable sottoconti'), 'Subaccount datatable threshold', 'Number of subaccounts above which, when expanding an account in the Chart of accounts, subaccounts are shown in a searchable, paginated table instead of the plain list. Default 500.');
+
+-- Indice per la selezione paginata dei sottoconti per mastro
+ALTER TABLE `co_piano_dei_conti3` ADD INDEX `idx_id_piano_dei_conti2_numero` (`id_piano_dei_conti2`, `numero`);
+
+-- Indici per il join anagrafica del dettaglio sottoconti
+ALTER TABLE `an_anagrafiche` ADD INDEX `idx_id_conto_cliente` (`id_conto_cliente`);
+ALTER TABLE `an_anagrafiche` ADD INDEX `idx_id_conto_fornitore` (`id_conto_fornitore`);
|