From 0c005442d5e7d5d80224b53c74d6e1c1bd171d70 Mon Sep 17 00:00:00 2001 From: Sajo <75281007+sajotrei@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:47:02 +0200 Subject: [PATCH 1/4] feat: aggiungi gestione note spese --- modules/note_spese/actions.php | 459 ++++++++++++++++++ modules/note_spese/add.php | 307 ++++++++++++ modules/note_spese/bulk.php | 169 +++++++ modules/note_spese/buttons.php | 16 + modules/note_spese/controller_after.php | 344 +++++++++++++ modules/note_spese/controller_before.php | 37 ++ modules/note_spese/edit.php | 112 +++++ modules/note_spese/export.php | 60 +++ modules/note_spese/init.php | 17 + modules/note_spese/modutil.php | 346 +++++++++++++ .../note_spese/src/PendingExpensesHook.php | 83 ++++ modules/note_spese/update/tables.php | 9 + modules/note_spese/widgets/indicatori.php | 94 ++++ modules/tipologie_note_spese/actions.php | 116 +++++ modules/tipologie_note_spese/add.php | 23 + modules/tipologie_note_spese/edit.php | 38 ++ modules/tipologie_note_spese/init.php | 13 + templates/note_spese/body.php | 80 +++ templates/note_spese/footer.php | 13 + templates/note_spese/header.php | 23 + templates/note_spese/init.php | 50 ++ templates/note_spese/settings.php | 14 + update/2_12.sql | 205 ++++++++ update/tables.php | 5 + 24 files changed, 2633 insertions(+) create mode 100644 modules/note_spese/actions.php create mode 100644 modules/note_spese/add.php create mode 100644 modules/note_spese/bulk.php create mode 100644 modules/note_spese/buttons.php create mode 100644 modules/note_spese/controller_after.php create mode 100644 modules/note_spese/controller_before.php create mode 100644 modules/note_spese/edit.php create mode 100644 modules/note_spese/export.php create mode 100644 modules/note_spese/init.php create mode 100644 modules/note_spese/modutil.php create mode 100644 modules/note_spese/src/PendingExpensesHook.php create mode 100644 modules/note_spese/update/tables.php create mode 100644 modules/note_spese/widgets/indicatori.php create mode 100644 modules/tipologie_note_spese/actions.php create mode 100644 modules/tipologie_note_spese/add.php create mode 100644 modules/tipologie_note_spese/edit.php create mode 100644 modules/tipologie_note_spese/init.php create mode 100644 templates/note_spese/body.php create mode 100644 templates/note_spese/footer.php create mode 100644 templates/note_spese/header.php create mode 100644 templates/note_spese/init.php create mode 100644 templates/note_spese/settings.php diff --git a/modules/note_spese/actions.php b/modules/note_spese/actions.php new file mode 100644 index 000000000..cc102dd73 --- /dev/null +++ b/modules/note_spese/actions.php @@ -0,0 +1,459 @@ + 0; +} + +switch (post('op')) { + case 'inline_update': + Permissions::check('rw'); + header('Content-Type: application/json; charset=UTF-8'); + + $record_id = (int) $id_record; + $field = trim((string) post('field')); + $value = post('value'); + if ($record_id <= 0 || !in_array($field, ['data', 'descrizione', 'controparte', 'importo'], true)) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => tr('Modifica rapida non valida.')]); + break; + } + + $current = $dbo->fetchOne('SELECT * FROM `co_note_spese` WHERE `id` = '.prepare($record_id).' LIMIT 1'); + if (empty($current)) { + http_response_code(404); + echo json_encode(['success' => false, 'message' => tr('Nota spesa non trovata.')]); + break; + } + + $update = []; + $outside_period = false; + if ($field === 'data') { + $parsed = noteSpeseParseDate($value); + if (empty($parsed)) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => tr('Data non valida.')]); + break; + } + $update['data'] = $parsed; + $outside_period = !noteSpeseIsDateInPeriod($parsed, $_SESSION['period_start'] ?? date('Y-01-01'), $_SESSION['period_end'] ?? date('Y-12-31')); + } elseif ($field === 'importo') { + $parsed = noteSpeseParseAmount($value); + if ($parsed === null || $parsed <= 0) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => tr('Importo non valido.')]); + break; + } + $update['importo'] = number_format($parsed, 2, '.', ''); + } else { + $parsed = trim(strip_tags((string) $value)); + if ($field === 'descrizione' && $parsed === '') { + http_response_code(422); + echo json_encode(['success' => false, 'message' => tr('Descrizione non valida.')]); + break; + } + if ((function_exists('mb_strlen') ? mb_strlen($parsed, 'UTF-8') : strlen($parsed)) > 255) { + http_response_code(422); + echo json_encode(['success' => false, 'message' => tr('Valore troppo lungo.')]); + break; + } + $update[$field] = $parsed !== '' ? $parsed : null; + } + + $new_value = reset($update); + $old_value = $current[$field] ?? null; + $changed = $field === 'importo' + ? number_format((float) $old_value, 2, '.', '') !== number_format((float) $new_value, 2, '.', '') + : trim((string) $old_value) !== trim((string) ($new_value ?? '')); + + $requires_review = false; + if ($changed) { + $confirmed = noteSpeseGetStatusId($dbo, 'confermato'); + $review = noteSpeseGetStatusId($dbo, 'da_verificare'); + if (!empty($confirmed) && !empty($review) && (int) $current['id_stato'] === $confirmed) { + $update['id_stato'] = $review; + $requires_review = true; + } + $dbo->update('co_note_spese', $update, ['id' => $record_id]); + } + + echo json_encode([ + 'success' => true, + 'outside_period' => $outside_period, + 'requires_review' => $requires_review, + 'message' => $requires_review ? tr('Nota spesa modificata e riportata Da verificare.') : tr('Nota spesa aggiornata.'), + ]); + break; + + case 'add': + Permissions::check('rw'); + + $data = noteSpeseParseDate(post('data')); + $id_tipologia = (int) post('id_tipologia'); + $descrizione = trim((string) post('descrizione')); + $importo = noteSpeseParseAmount(post('importo')); + $controparte = trim((string) post('controparte')); + $id_operatore = (int) post('id_operatore') ?: null; + $note = trim((string) post('note')); + $confirmed = noteSpeseGetStatusId($dbo, 'confermato'); + $review = noteSpeseGetStatusId($dbo, 'da_verificare'); + $category = $dbo->fetchOne('SELECT `id` FROM `co_note_spese_tipologie` WHERE `id` = '.prepare($id_tipologia).' AND `enabled` = 1 LIMIT 1'); + + if (!noteSpeseValidateBaseData($data, $id_tipologia, $descrizione, $importo) || empty($confirmed) || empty($review) || empty($category) || !noteSpeseOperatorExists($dbo, $id_operatore)) { + flash()->error(tr('Compilare correttamente data, tipologia, descrizione e importo.')); + break; + } + + $duplicate = noteSpeseFindDuplicate($dbo, $data, $importo, $descrizione, $controparte, null, $id_operatore); + if (!empty($duplicate)) { + $note = noteSpeseAppendNote($note, tr('Possibile duplicato della spesa #_ID_.', ['_ID_' => (int) $duplicate['id']])); + } + + $dbo->insert('co_note_spese', [ + 'data' => $data, + 'id_tipologia' => $id_tipologia, + 'id_stato' => !empty($duplicate) ? $review : $confirmed, + 'descrizione' => $descrizione, + 'importo' => $importo, + 'id_anagrafica' => null, + 'id_operatore' => $id_operatore, + 'controparte' => $controparte ?: null, + 'origine' => 'manuale', + 'id_origine' => null, + 'note' => $note ?: null, + ]); + $id_record = $dbo->lastInsertedID(); + + if (!empty($duplicate)) { + flash()->warning(tr('Spesa aggiunta come Da verificare: esiste una possibile duplicazione con la spesa #_ID_.', ['_ID_' => (int) $duplicate['id']])); + } else { + flash()->info(tr('Spesa aggiunta come Confermata.')); + } + break; + + case 'update': + Permissions::check('rw'); + if (empty($id_record)) { + break; + } + + $current = $dbo->fetchOne('SELECT * FROM `co_note_spese` WHERE `id` = '.prepare($id_record).' LIMIT 1'); + if (empty($current)) { + break; + } + + $data = noteSpeseParseDate(post('data')); + $id_tipologia = (int) post('id_tipologia'); + $id_stato = (int) post('id_stato'); + $descrizione = trim((string) post('descrizione')); + $importo = noteSpeseParseAmount(post('importo')); + $controparte = trim((string) post('controparte')); + $id_operatore = (int) post('id_operatore') ?: null; + $note = trim((string) post('note')); + + $valid_status = $dbo->fetchOne('SELECT `id` FROM `co_note_spese_stati` WHERE `id` = '.prepare($id_stato).' LIMIT 1'); + $valid_category = $dbo->fetchOne('SELECT `id` FROM `co_note_spese_tipologie` WHERE `id` = '.prepare($id_tipologia).' AND (`enabled` = 1 OR `id` = '.prepare((int) $current['id_tipologia']).') LIMIT 1'); + if (!noteSpeseValidateBaseData($data, $id_tipologia, $descrizione, $importo) || empty($valid_status) || empty($valid_category) || !noteSpeseOperatorExists($dbo, $id_operatore, $current['id_operatore'] ?? null)) { + flash()->error(tr('Compilare correttamente i dati della spesa.')); + break; + } + + $substantive_changed = (string) $current['data'] !== (string) $data + || (int) $current['id_tipologia'] !== $id_tipologia + || trim((string) $current['descrizione']) !== $descrizione + || number_format((float) $current['importo'], 2, '.', '') !== number_format((float) $importo, 2, '.', '') + || trim((string) ($current['controparte'] ?? '')) !== $controparte + || (int) ($current['id_operatore'] ?? 0) !== (int) ($id_operatore ?? 0); + + $confirmed = noteSpeseGetStatusId($dbo, 'confermato'); + $review = noteSpeseGetStatusId($dbo, 'da_verificare'); + $reset_to_review = $substantive_changed && (int) $current['id_stato'] === (int) $confirmed && $id_stato === (int) $confirmed && !empty($review); + if ($reset_to_review) { + $id_stato = $review; + } + + $dbo->update('co_note_spese', [ + 'data' => $data, + 'id_tipologia' => $id_tipologia, + 'id_stato' => $id_stato, + 'descrizione' => $descrizione, + 'importo' => $importo, + 'controparte' => $controparte ?: null, + 'id_operatore' => $id_operatore, + 'note' => $note ?: null, + ], ['id' => $id_record]); + + if ($reset_to_review) { + flash()->warning(tr('La spesa era Confermata: dopo la modifica è stata riportata Da verificare.')); + } else { + flash()->info(tr('Spesa aggiornata correttamente.')); + } + break; + + case 'delete': + Permissions::check('rw'); + if (!empty($id_record) && noteSpeseDeleteRecord($dbo, $id_module, (int) $id_record)) { + flash()->info(tr('Spesa eliminata correttamente.')); + } + break; + + case 'import_rifornimenti': + Permissions::check('rw'); + $source_module = Models\Module::where('name', 'Automezzi')->first(); + if (empty($source_module)) { + flash()->error(tr('Modulo origine non disponibile.')); + break; + } + Permissions::addModule($source_module->id); + Permissions::check(['r', 'rw']); + + $ids = array_values(array_unique(array_filter(array_map('intval', (array) post('rifornimenti'))))); + $category = noteSpeseGetCategory($dbo, 'carburante'); + $review = noteSpeseGetStatusId($dbo, 'da_verificare'); + if (empty($ids) || empty($category) || empty($review)) { + flash()->warning(tr('Selezionare almeno un rifornimento da importare.')); + break; + } + + $period_start = $_SESSION['period_start'] ?? date('Y-01-01'); + $period_end_ts = ($_SESSION['period_end'] ?? date('Y-12-31')).' 23:59:59'; + $imported = 0; + $skipped = 0; + $dbo->beginTransaction(); + try { + foreach ($ids as $id) { + if (!empty($dbo->fetchOne('SELECT `id` FROM `co_note_spese` WHERE `origine` = '.prepare('automezzi_rifornimento').' AND `id_origine` = '.prepare($id).' LIMIT 1'))) { + ++$skipped; + continue; + } + + $source = $dbo->fetchOne( + 'SELECT r.*, v.`id_tecnico`, v.`id_sede`, s.`nome` AS automezzo_nome, s.`targa`, g.`descrizione` AS gestore, a.`ragione_sociale` AS tecnico ' + .'FROM `an_automezzi_rifornimenti` r ' + .'LEFT JOIN `an_automezzi_viaggi` v ON v.`id` = r.`id_viaggio` ' + .'LEFT JOIN `an_sedi` s ON s.`id` = v.`id_sede` ' + .'LEFT JOIN `an_automezzi_gestori` g ON g.`id` = r.`id_gestore` ' + .'LEFT JOIN `an_anagrafiche` a ON a.`id` = v.`id_tecnico` ' + .'WHERE r.`id` = '.prepare($id).' AND r.`data` >= '.prepare($period_start).' AND r.`data` <= '.prepare($period_end_ts).' LIMIT 1' + ); + if (empty($source)) { + ++$skipped; + continue; + } + + $date = noteSpeseParseDate(substr((string) $source['data'], 0, 10)); + $amount = noteSpeseParseAmount($source['costo']); + if (empty($date) || $amount === null || $amount <= 0) { + ++$skipped; + continue; + } + + $vehicle = trim(($source['automezzo_nome'] ?: '').(!empty($source['targa']) ? ' - '.$source['targa'] : '')); + $description = tr('Rifornimento').($vehicle !== '' ? ' - '.$vehicle : ''); + $counterparty = trim((string) ($source['gestore'] ?: $source['luogo'] ?: '')); + $id_operatore = !empty($source['id_tecnico']) && noteSpeseAnagraficaExists($dbo, $source['id_tecnico']) ? (int) $source['id_tecnico'] : null; + $notes = [tr('Importato dal registro rifornimenti.')]; + $duplicate = noteSpeseFindDuplicate($dbo, $date, $amount, $description, $counterparty, null, $id_operatore); + if (!empty($duplicate)) { + $notes[] = tr('Possibile duplicato della spesa #_ID_.', ['_ID_' => (int) $duplicate['id']]); + } + + $dbo->insert('co_note_spese', [ + 'data' => $date, + 'id_tipologia' => $category['id'], + 'id_stato' => $review, + 'descrizione' => $description, + 'importo' => $amount, + 'id_anagrafica' => null, + 'id_operatore' => $id_operatore, + 'controparte' => $counterparty ?: null, + 'origine' => 'automezzi_rifornimento', + 'id_origine' => $id, + 'note' => implode("\n", $notes), + ]); + ++$imported; + } + $dbo->commitTransaction(); + } catch (Throwable $e) { + $dbo->rollbackTransaction(); + throw $e; + } + flash()->info(tr('Importazione completata: _IMPORTED_ importate, _SKIPPED_ ignorate.', ['_IMPORTED_' => $imported, '_SKIPPED_' => $skipped])); + break; + + case 'import_scadenzario': + Permissions::check('rw'); + $source_module = Models\Module::where('name', 'Scadenzario')->first(); + if (empty($source_module)) { + flash()->error(tr('Modulo origine non disponibile.')); + break; + } + Permissions::addModule($source_module->id); + Permissions::check(['r', 'rw']); + + $ids = array_values(array_unique(array_filter(array_map('intval', (array) post('scadenze'))))); + $review = noteSpeseGetStatusId($dbo, 'da_verificare'); + $period_start = $_SESSION['period_start'] ?? date('Y-01-01'); + $period_end = $_SESSION['period_end'] ?? date('Y-12-31'); + $imported = 0; + $skipped = 0; + if (empty($ids) || empty($review)) { + flash()->warning(tr('Selezionare almeno una scadenza da importare.')); + break; + } + + $dbo->beginTransaction(); + try { + foreach ($ids as $id) { + if (!empty($dbo->fetchOne('SELECT `id` FROM `co_note_spese` WHERE `origine` = '.prepare('scadenzario_generico').' AND `id_origine` = '.prepare($id).' LIMIT 1'))) { + ++$skipped; + continue; + } + + $source = $dbo->fetchOne( + 'SELECT s.*, a.`ragione_sociale` FROM `co_scadenzario` s ' + .'LEFT JOIN `an_anagrafiche` a ON a.`id` = s.`id_anagrafica` ' + .'WHERE s.`id` = '.prepare($id).' AND (s.`id_documento` IS NULL OR s.`id_documento` = 0) ' + .'AND s.`da_pagare` < 0 AND s.`scadenza` >= '.prepare($period_start).' AND s.`scadenza` <= '.prepare($period_end).' LIMIT 1' + ); + if (empty($source)) { + ++$skipped; + continue; + } + + $date = noteSpeseParseDate(substr((string) $source['scadenza'], 0, 10)); + $amount = abs((float) $source['da_pagare']); + $category = noteSpeseGetCategory($dbo, trim((string) $source['descrizione']).' '.trim((string) $source['tipo']).' '.trim((string) $source['ragione_sociale'])); + if (empty($date) || $amount <= 0 || empty($category)) { + ++$skipped; + continue; + } + + $description = trim((string) $source['descrizione']) ?: tr('Scadenza generica'); + $counterparty = trim((string) $source['ragione_sociale']); + $notes = [tr('Importato da Scadenzario generico. Verificare competenza e documentazione prima della conferma.')]; + $duplicate = noteSpeseFindDuplicate($dbo, $date, $amount, $description, $counterparty); + if (!empty($duplicate)) { + $notes[] = tr('Possibile duplicato della spesa #_ID_.', ['_ID_' => (int) $duplicate['id']]); + } + + $id_anagrafica = !empty($source['id_anagrafica']) && noteSpeseAnagraficaExists($dbo, $source['id_anagrafica']) ? (int) $source['id_anagrafica'] : null; + $dbo->insert('co_note_spese', [ + 'data' => $date, + 'id_tipologia' => $category['id'], + 'id_stato' => $review, + 'descrizione' => $description, + 'importo' => $amount, + 'id_anagrafica' => $id_anagrafica, + 'id_operatore' => null, + 'controparte' => $counterparty ?: null, + 'origine' => 'scadenzario_generico', + 'id_origine' => $id, + 'note' => implode("\n", $notes), + ]); + ++$imported; + } + $dbo->commitTransaction(); + } catch (Throwable $e) { + $dbo->rollbackTransaction(); + throw $e; + } + flash()->info(tr('Importazione Scadenzario completata: _IMPORTED_ importate, _SKIPPED_ ignorate.', ['_IMPORTED_' => $imported, '_SKIPPED_' => $skipped])); + break; + + case 'import_excel': + Permissions::check('rw'); + $raw = trim((string) post('righe_excel')); + if ($raw === '') { + flash()->warning(tr('Incollare almeno una riga.')); + break; + } + + $review = noteSpeseGetStatusId($dbo, 'da_verificare'); + $rows = preg_split('/\R/u', $raw); + $imported = 0; + $skipped = 0; + $duplicates = 0; + $dbo->beginTransaction(); + try { + foreach ($rows as $row) { + $row = trim($row); + if ($row === '') { + continue; + } + $columns = strpos($row, "\t") !== false ? explode("\t", $row) : str_getcsv($row, ';'); + $columns = array_map(static fn ($item) => trim((string) $item), $columns); + if (!empty($columns[0]) && in_array(noteSpeseLower($columns[0]), ['data', 'date'], true)) { + continue; + } + + $category_raw = ''; + $counterparty = ''; + $user_notes = ''; + if (count($columns) === 3) { + [$date_raw, $description, $amount_raw] = $columns; + $category = noteSpeseGetCategory($dbo, $description); + } elseif (count($columns) >= 4) { + [$date_raw, $category_raw, $description, $amount_raw] = array_slice($columns, 0, 4); + $counterparty = $columns[4] ?? ''; + $user_notes = $columns[5] ?? ''; + $category = noteSpeseGetCategory($dbo, $category_raw); + } else { + ++$skipped; + continue; + } + + $date = noteSpeseParseDate($date_raw); + $amount = noteSpeseParseAmount($amount_raw); + $amount = $amount !== null ? abs($amount) : null; + if (empty($date) || empty($category) || trim($description) === '' || $amount === null || $amount <= 0 || empty($review)) { + ++$skipped; + continue; + } + + $duplicate = noteSpeseFindDuplicate($dbo, $date, $amount, $description, $counterparty); + if (!empty($duplicate) && ($duplicate['origine'] ?? '') === 'excel') { + ++$duplicates; + continue; + } + + $notes = []; + if ($category_raw !== '' && strcasecmp($category_raw, (string) $category['descrizione']) !== 0 && strcasecmp($category_raw, (string) $category['codice']) !== 0) { + $notes[] = tr('Categoria originale: _CATEGORY_', ['_CATEGORY_' => $category_raw]); + } + if ($user_notes !== '') { + $notes[] = $user_notes; + } + if (!empty($duplicate)) { + $notes[] = tr('Possibile duplicato della spesa #_ID_.', ['_ID_' => (int) $duplicate['id']]); + } + + $dbo->insert('co_note_spese', [ + 'data' => $date, + 'id_tipologia' => $category['id'], + 'id_stato' => $review, + 'descrizione' => trim($description), + 'importo' => $amount, + 'id_anagrafica' => null, + 'id_operatore' => null, + 'controparte' => trim($counterparty) ?: null, + 'origine' => 'excel', + 'id_origine' => null, + 'note' => !empty($notes) ? implode("\n", $notes) : null, + ]); + ++$imported; + } + $dbo->commitTransaction(); + } catch (Throwable $e) { + $dbo->rollbackTransaction(); + throw $e; + } + flash()->info(tr('Importazione completata: _IMPORTED_ importate, _SKIPPED_ non valide, _DUPLICATES_ duplicate già importate ignorate.', [ + '_IMPORTED_' => $imported, + '_SKIPPED_' => $skipped, + '_DUPLICATES_' => $duplicates, + ])); + break; +} diff --git a/modules/note_spese/add.php b/modules/note_spese/add.php new file mode 100644 index 000000000..5bb018436 --- /dev/null +++ b/modules/note_spese/add.php @@ -0,0 +1,307 @@ +id; + +$duplicate_id = (int) get('duplicate_id'); +$duplicate_source = []; +if ($duplicate_id > 0) { + $duplicate_source = $dbo->fetchOne('SELECT `data`, `id_tipologia`, `descrizione`, `importo`, `controparte`, `id_operatore`, `note` FROM `co_note_spese` WHERE `id` = '.prepare($duplicate_id).' LIMIT 1') ?: []; +} + +$is_duplicate = !empty($duplicate_source); +$default_date = $is_duplicate ? (string) $duplicate_source['data'] : $default_date; +$default_category = $is_duplicate ? (int) $duplicate_source['id_tipologia'] : null; +$default_amount = $is_duplicate ? (float) $duplicate_source['importo'] : null; +$default_description = $is_duplicate ? (string) $duplicate_source['descrizione'] : ''; +$default_counterparty = $is_duplicate ? (string) ($duplicate_source['controparte'] ?? '') : ''; +$current_user = auth_osm()->getUser(); +$current_operator = (int) ($current_user['id_anagrafica'] ?? 0); +$operator_candidate = $is_duplicate ? (int) ($duplicate_source['id_operatore'] ?? 0) : $current_operator; +$default_operator = noteSpeseOperatorExists($dbo, $operator_candidate) ? ($operator_candidate ?: null) : null; +$default_notes = $is_duplicate ? (string) ($duplicate_source['note'] ?? '') : ''; + +$automezzi_module = Models\Module::where('name', 'Automezzi')->first(); +$scadenzario_module = Models\Module::where('name', 'Scadenzario')->first(); +$can_read_automezzi = !empty($automezzi_module) && in_array(Modules::getPermission($automezzi_module->id), ['r', 'rw'], true); +$can_read_scadenzario = !empty($scadenzario_module) && in_array(Modules::getPermission($scadenzario_module->id), ['r', 'rw'], true); + +$rifornimenti_summary = $can_read_automezzi ? $dbo->fetchOne( + 'SELECT COUNT(*) AS totale, COALESCE(SUM(r.`costo`), 0) AS importo FROM `an_automezzi_rifornimenti` r ' + .'WHERE r.`data` >= '.prepare($period_start).' AND r.`data` <= '.prepare($period_end_ts).' ' + .'AND NOT EXISTS (SELECT 1 FROM `co_note_spese` n WHERE n.`origine` = '.prepare('automezzi_rifornimento').' AND n.`id_origine` = r.`id`)' +) : ['totale' => 0, 'importo' => 0]; +$rifornimenti_count = (int) ($rifornimenti_summary['totale'] ?? 0); + +$rifornimenti = $can_read_automezzi ? $dbo->fetchArray( + 'SELECT r.`id`, r.`data`, r.`luogo`, r.`costo`, g.`descrizione` AS gestore, ' + .'s.`nome` AS automezzo_nome, s.`targa`, a.`ragione_sociale` AS tecnico ' + .'FROM `an_automezzi_rifornimenti` r ' + .'LEFT JOIN `an_automezzi_viaggi` v ON v.`id` = r.`id_viaggio` ' + .'LEFT JOIN `an_sedi` s ON s.`id` = v.`id_sede` ' + .'LEFT JOIN `an_automezzi_gestori` g ON g.`id` = r.`id_gestore` ' + .'LEFT JOIN `an_anagrafiche` a ON a.`id` = v.`id_tecnico` ' + .'WHERE r.`data` >= '.prepare($period_start).' AND r.`data` <= '.prepare($period_end_ts).' ' + .'AND NOT EXISTS (SELECT 1 FROM `co_note_spese` n WHERE n.`origine` = '.prepare('automezzi_rifornimento').' AND n.`id_origine` = r.`id`) ' + .'ORDER BY r.`data` DESC, r.`id` DESC LIMIT 200' +) : []; + +$scadenze_summary = $can_read_scadenzario ? $dbo->fetchOne( + 'SELECT COUNT(*) AS totale, COALESCE(SUM(ABS(s.`da_pagare`)), 0) AS importo FROM `co_scadenzario` s ' + .'WHERE (s.`id_documento` IS NULL OR s.`id_documento` = 0) AND s.`da_pagare` < 0 ' + .'AND s.`scadenza` >= '.prepare($period_start).' AND s.`scadenza` <= '.prepare($period_end).' ' + .'AND NOT EXISTS (SELECT 1 FROM `co_note_spese` n WHERE n.`origine` = '.prepare('scadenzario_generico').' AND n.`id_origine` = s.`id`)' +) : ['totale' => 0, 'importo' => 0]; +$scadenze_count = (int) ($scadenze_summary['totale'] ?? 0); + +$scadenze = $can_read_scadenzario ? $dbo->fetchArray( + 'SELECT s.`id`, s.`scadenza`, s.`data_emissione`, s.`descrizione`, s.`tipo`, s.`da_pagare`, ' + .'a.`ragione_sociale` ' + .'FROM `co_scadenzario` s ' + .'LEFT JOIN `an_anagrafiche` a ON a.`id` = s.`id_anagrafica` ' + .'WHERE (s.`id_documento` IS NULL OR s.`id_documento` = 0) AND s.`da_pagare` < 0 ' + .'AND s.`scadenza` >= '.prepare($period_start).' AND s.`scadenza` <= '.prepare($period_end).' ' + .'AND NOT EXISTS (SELECT 1 FROM `co_note_spese` n WHERE n.`origine` = '.prepare('scadenzario_generico').' AND n.`id_origine` = s.`id`) ' + .'ORDER BY s.`scadenza` DESC, s.`id` DESC LIMIT 200' +) : []; + +$category_query = 'SELECT t.`id`, COALESCE(l.`title`, t.`descrizione`) AS `descrizione` ' + .'FROM `co_note_spese_tipologie` t ' + .'LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` AND l.`id_lang` = '.$lang.' ' + .'WHERE t.`enabled` = 1 ORDER BY t.`ordine`, `descrizione`'; +$operator_query = noteSpeseOperatorSelectQuery($default_operator); +?> + + + +
+
+
+ + + + +
+ $duplicate_id]); ?> +
+ + +
+
+ {[ "type": "date", "label": "", "name": "data", "required": 1, "value": "" ]} +
+
+ {[ "type": "select", "label": "", "name": "id_tipologia", "required": 1, "value": "", "values": "query=" ]} +
+
+ {[ "type": "select", "label": "", "name": "id_operatore", "value": "", "values": "query=", "help": "" ]} +
+
+ {[ "type": "number", "label": "", "name": "importo", "required": 1, "value": "", "icon-after": "€", "min-value": "0.01", "decimals": 2 ]} +
+
+ +
+
+ {[ "type": "text", "label": "", "name": "descrizione", "required": 1, "value": "" ]} +
+
+ {[ "type": "text", "label": "", "name": "controparte", "value": "", "help": "" ]} +
+
+ +
+
+ {[ "type": "textarea", "label": "", "name": "note", "value": "" ]} +
+
+ +
+ Translator::dateToLocale($period_start), + '_END_' => Translator::dateToLocale($period_end), + ]); ?> + +
+
+
+ + +
+
+ $rifornimenti_count, + '_TOTAL_' => moneyFormat($rifornimenti_summary['importo'] ?? 0, 2), + ]); ?> + +
+ 200) { ?> +
$rifornimenti_count]); ?>
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+ +
+ + + +
+
+ $scadenze_count, + '_TOTAL_' => moneyFormat($scadenze_summary['importo'] ?? 0, 2), + ]); ?> + +
+ 200) { ?> +
$scadenze_count]); ?>
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+
+ +
+ +
+ + + +
+
+
+ + + +
+
+ + +
+ +
+
+ + +
+
+
+ +
+ + diff --git a/modules/note_spese/bulk.php b/modules/note_spese/bulk.php new file mode 100644 index 000000000..ccf3f8f04 --- /dev/null +++ b/modules/note_spese/bulk.php @@ -0,0 +1,169 @@ +error(tr('Stato di conferma non disponibile.')); + break; + } + + // I duplicati esatti restano da verificare: per confermarli è necessario aprire la singola spesa. + $duplicate_rows = $dbo->fetchArray( + 'SELECT DISTINCT a.`id` FROM `co_note_spese` a ' + .'INNER JOIN `co_note_spese` b ON b.`id` != a.`id` ' + .'AND b.`data` = a.`data` ' + .'AND b.`importo` = a.`importo` ' + .'AND LOWER(TRIM(b.`descrizione`)) = LOWER(TRIM(a.`descrizione`)) ' + .'AND LOWER(TRIM(COALESCE(b.`controparte`, ""))) = LOWER(TRIM(COALESCE(a.`controparte`, ""))) ' + .'AND COALESCE(b.`id_operatore`, 0) = COALESCE(a.`id_operatore`, 0) ' + .'INNER JOIN `co_note_spese_stati` bst ON bst.`id` = b.`id_stato` AND bst.`name` != '.prepare('escluso').' ' + .'WHERE a.`id` IN ('.implode(',', $ids).')' + ); + $duplicate_ids = array_map('intval', array_column($duplicate_rows, 'id')); + $confirm_ids = array_values(array_diff($ids, $duplicate_ids)); + + if (!empty($confirm_ids)) { + $dbo->query('UPDATE `co_note_spese` SET `id_stato` = '.prepare($id_stato).' WHERE `id` IN ('.implode(',', $confirm_ids).')'); + flash()->info(tr('_COUNT_ spese confermate.', ['_COUNT_' => count($confirm_ids)])); + } + if (!empty($duplicate_ids)) { + flash()->warning(tr('_COUNT_ possibili duplicati non sono stati confermati: aprire le singole righe per verificarli.', ['_COUNT_' => count($duplicate_ids)])); + } + break; + + case 'review_bulk': + case 'exclude_bulk': + Permissions::check('rw'); + if (empty($ids)) { + break; + } + + $status_name = post('op') === 'review_bulk' ? 'da_verificare' : 'escluso'; + $id_stato = noteSpeseGetStatusId($dbo, $status_name); + if (!empty($id_stato)) { + $dbo->query('UPDATE `co_note_spese` SET `id_stato` = '.prepare($id_stato).' WHERE `id` IN ('.implode(',', $ids).')'); + flash()->info(tr('_COUNT_ spese aggiornate.', ['_COUNT_' => count($ids)])); + } + break; + + case 'duplicate_bulk': + Permissions::check('rw'); + if (empty($ids)) { + break; + } + + $id_stato = noteSpeseGetStatusId($dbo, 'da_verificare'); + if (empty($id_stato)) { + flash()->error(tr('Stato Da verificare non disponibile.')); + break; + } + + $rows = $dbo->fetchArray( + 'SELECT `data`, `id_tipologia`, `descrizione`, `importo`, `id_anagrafica`, `id_operatore`, `controparte`, `note` ' + .'FROM `co_note_spese` WHERE `id` IN ('.implode(',', $ids).') ORDER BY `id` ASC' + ); + + $duplicated = 0; + foreach ($rows as $row) { + $dbo->insert('co_note_spese', [ + 'data' => $row['data'], + 'id_tipologia' => (int) $row['id_tipologia'], + 'id_stato' => $id_stato, + 'descrizione' => $row['descrizione'], + 'importo' => number_format((float) $row['importo'], 2, '.', ''), + 'id_anagrafica' => !empty($row['id_anagrafica']) ? (int) $row['id_anagrafica'] : null, + 'id_operatore' => !empty($row['id_operatore']) ? (int) $row['id_operatore'] : null, + 'controparte' => $row['controparte'] ?: null, + 'origine' => 'manuale', + 'id_origine' => null, + 'note' => $row['note'] ?: null, + ]); + ++$duplicated; + } + + if ($duplicated > 0) { + flash()->info(tr('_COUNT_ note spese duplicate. Le copie sono Da verificare e senza allegati.', ['_COUNT_' => $duplicated])); + } else { + flash()->warning(tr('Nessuna nota spesa duplicata.')); + } + break; + + case 'delete_bulk': + Permissions::check('rw'); + if (empty($ids)) { + break; + } + + $deleted = 0; + foreach ($ids as $id) { + $exists = $dbo->fetchOne('SELECT `id` FROM `co_note_spese` WHERE `id` = '.prepare($id).' LIMIT 1'); + if (!empty($exists) && noteSpeseDeleteRecord($dbo, $id_module, $id)) { + ++$deleted; + } + } + + if ($deleted > 0) { + flash()->info(tr('_COUNT_ spese eliminate.', ['_COUNT_' => $deleted])); + } else { + flash()->warning(tr('Nessuna spesa eliminata.')); + } + break; +} + +return [ + 'confirm_bulk' => [ + 'text' => tr('Conferma'), + 'data' => [ + 'title' => tr('Confermare le spese selezionate?'), + 'msg' => tr('Le spese selezionate saranno incluse nella stampa, nel CSV e nei totali. Eventuali possibili duplicati resteranno da verificare.'), + 'button' => tr('Conferma'), + 'class' => 'btn btn-lg btn-success', + ], + ], + 'review_bulk' => [ + 'text' => tr('Segna da verificare'), + 'data' => [ + 'title' => tr('Segnare le spese come da verificare?'), + 'msg' => tr('Le spese selezionate non saranno incluse nella stampa, nel CSV e nei totali finché non verranno confermate.'), + 'button' => tr('Segna da verificare'), + 'class' => 'btn btn-lg btn-warning', + ], + ], + 'exclude_bulk' => [ + 'text' => tr('Escludi'), + 'data' => [ + 'title' => tr('Escludere le spese selezionate?'), + 'msg' => tr('Le spese resteranno registrate ma saranno escluse dalla stampa, dal CSV e dai totali.'), + 'button' => tr('Escludi'), + 'class' => 'btn btn-lg btn-secondary', + ], + ], + 'duplicate_bulk' => [ + 'text' => tr('Duplica'), + 'data' => [ + 'title' => tr('Duplicare le note spese selezionate?'), + 'msg' => tr('Verrà creata una copia per ogni nota spesa selezionata. Le copie manterranno inizialmente data e importo originali, saranno Da verificare e senza allegati; potrai modificare rapidamente data e importo direttamente dall’elenco.'), + 'button' => tr('Duplica'), + 'class' => 'btn btn-lg btn-primary', + ], + ], + 'delete_bulk' => [ + 'text' => tr('Elimina'), + 'data' => [ + 'title' => tr('Eliminare le spese selezionate?'), + 'msg' => tr('Le spese selezionate e i relativi allegati saranno eliminati definitivamente.'), + 'button' => tr('Elimina'), + 'class' => 'btn btn-lg btn-danger', + ], + ], +]; diff --git a/modules/note_spese/buttons.php b/modules/note_spese/buttons.php new file mode 100644 index 000000000..802db836c --- /dev/null +++ b/modules/note_spese/buttons.php @@ -0,0 +1,16 @@ +permission ?? null) === 'rw' && !empty($id_record)) { + $duplicate_url = base_path_osm().'/add.php?id_module='.(int) $id_module.'&duplicate_id='.(int) $id_record; + ?> + + permission === 'rw'; +$user = auth_osm()->getUser(); +$group_id = (int) ($user->id_gruppo ?? 0); +$editable_names = $can_inline ? [ + 'Data' => 'data', + 'Descrizione' => 'descrizione', + 'Controparte' => 'controparte', + 'Importo' => 'importo', +] : []; + +$views = $dbo->fetchArray( + 'SELECT v.`name` FROM `zz_views` v ' + .'INNER JOIN `zz_group_view` gv ON gv.`id_vista` = v.`id` ' + .'WHERE v.`id_module` = '.prepare((int) $id_module).' ' + .'AND gv.`id_gruppo` = '.prepare($group_id).' ' + .'AND v.`visible` = 1 ' + .'ORDER BY v.`order` ASC' +); + +// La prima colonna DataTables è il selettore, quindi gli indici delle viste +// visibili partono da 1. +$editable_columns = []; +$amount_column = null; +$column_index = 1; +foreach ($views as $view) { + $name = (string) ($view['name'] ?? ''); + if ($name === 'Importo') { + $amount_column = $column_index; + } + if (isset($editable_names[$name])) { + $editable_columns[$column_index] = $editable_names[$name]; + } + ++$column_index; +} + +if (empty($editable_columns) && $amount_column === null) { + return; +} + +$separators = formatter()->getNumberSeparators(); +?> + + diff --git a/modules/note_spese/controller_before.php b/modules/note_spese/controller_before.php new file mode 100644 index 000000000..ab6db6759 --- /dev/null +++ b/modules/note_spese/controller_before.php @@ -0,0 +1,37 @@ +fetchOne('SELECT `id` FROM `zz_prints` WHERE `name` = '.prepare('Nota spese').' AND `enabled` = 1 LIMIT 1'); +$print_id = (int) ($print['id'] ?? 0); + +$print_url = $print_id > 0 ? base_path_osm().'/pdfgen.php?id_print='.$print_id.'&id_record=0' : null; +$csv_url = base_path_osm().'/modules/note_spese/export.php?id_module='.(int) $id_module; +?> + +
+
+ + Translator::dateToLocale($period_start), + '_END_' => Translator::dateToLocale($period_end), + ]); ?> + permission === 'rw') { ?> + + + + +
+
+ + + + + + + + +
+
diff --git a/modules/note_spese/edit.php b/modules/note_spese/edit.php new file mode 100644 index 000000000..766e49f3f --- /dev/null +++ b/modules/note_spese/edit.php @@ -0,0 +1,112 @@ +id; +$category_query = 'SELECT t.`id`, COALESCE(l.`title`, t.`descrizione`) AS `descrizione` ' + .'FROM `co_note_spese_tipologie` t LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` AND l.`id_lang` = '.$lang.' ' + .'WHERE t.`enabled` = 1 OR t.`id` = '.(int) $record['id_tipologia'].' ORDER BY t.`ordine`, `descrizione`'; +$status_query = 'SELECT st.`id`, COALESCE(l.`title`, st.`name`) AS `descrizione` ' + .'FROM `co_note_spese_stati` st LEFT JOIN `co_note_spese_stati_lang` l ON l.`id_record` = st.`id` AND l.`id_lang` = '.$lang.' ' + .'ORDER BY st.`ordine`, st.`id`'; +$operator_query = noteSpeseOperatorSelectQuery($record['id_operatore'] ?? null); + +$source_label = noteSpeseSourceLabel($record['origine'] ?? 'manuale'); +$source_url = null; +$source_hint = null; + +if (($record['origine'] ?? '') === 'automezzi_rifornimento' && !empty($record['id_origine'])) { + $module_source = Module::where('name', 'Automezzi')->first(); + $can_read_source = !empty($module_source) && in_array(Modules::getPermission($module_source->id), ['r', 'rw'], true); + if ($can_read_source) { + $source = $dbo->fetchOne( + 'SELECT v.`id_sede` FROM `an_automezzi_rifornimenti` r ' + .'LEFT JOIN `an_automezzi_viaggi` v ON v.`id` = r.`id_viaggio` WHERE r.`id` = '.prepare($record['id_origine']).' LIMIT 1' + ); + if (!empty($source['id_sede'])) { + $source_url = base_path_osm().'/editor.php?id_module='.$module_source->id.'&id_record='.(int) $source['id_sede']; + $source_hint = tr('Rifornimento #_ID_', ['_ID_' => (int) $record['id_origine']]); + } + } +} elseif (($record['origine'] ?? '') === 'scadenzario_generico' && !empty($record['id_origine'])) { + $module_source = Module::where('name', 'Scadenzario')->first(); + if (!empty($module_source) && in_array(Modules::getPermission($module_source->id), ['r', 'rw'], true)) { + $source_url = base_path_osm().'/editor.php?id_module='.$module_source->id.'&id_record='.(int) $record['id_origine']; + $source_hint = tr('Scadenza #_ID_', ['_ID_' => (int) $record['id_origine']]); + } +} + +$duplicate = noteSpeseFindDuplicate( + $dbo, + $record['data'] ?? null, + $record['importo'] ?? null, + $record['descrizione'] ?? '', + $record['controparte'] ?? '', + (int) $id_record, + $record['id_operatore'] ?? null +); + +$period_start = $_SESSION['period_start'] ?? date('Y-01-01'); +$period_end = $_SESSION['period_end'] ?? date('Y-12-31'); +$is_in_period = noteSpeseIsDateInPeriod($record['data'] ?? null, $period_start, $period_end); +?> + +
+ + + +
+

+
+
+
{[ "type": "date", "label": "", "name": "data", "required": 1, "value": "$data$" ]}
+
{[ "type": "select", "label": "", "name": "id_tipologia", "required": 1, "value": "$id_tipologia$", "values": "query=" ]}
+
{[ "type": "select", "label": "", "name": "id_operatore", "value": "$id_operatore$", "values": "query=", "help": "" ]}
+
{[ "type": "number", "label": "", "name": "importo", "required": 1, "value": "$importo$", "icon-after": "€", "min-value": "0.01", "decimals": 2 ]}
+
+ +
+
{[ "type": "text", "label": "", "name": "descrizione", "required": 1, "value": "$descrizione$" ]}
+
{[ "type": "text", "label": "", "name": "controparte", "value": "$controparte$" ]}
+
{[ "type": "select", "label": "", "name": "id_stato", "required": 1, "value": "$id_stato$", "values": "query=" ]}
+
+ +
{[ "type": "textarea", "label": "", "name": "note", "value": "$note$" ]}
+
+
+
+ + +
+ + Translator::dateToLocale($period_start), + '_END_' => Translator::dateToLocale($period_end), + ]); ?> +
+ + + +
+ (int) $duplicate['id']]); ?> + +
+ + + +
+
+ : + +
+ + + +
+ + +{( "name": "filelist_and_upload", "id_module": "$id_module$", "id_record": "$id_record$" )} + + diff --git a/modules/note_spese/export.php b/modules/note_spese/export.php new file mode 100644 index 000000000..bae036dab --- /dev/null +++ b/modules/note_spese/export.php @@ -0,0 +1,60 @@ +fetchOne('SELECT `id` FROM `zz_modules` WHERE `name` = '.prepare('Note spese').' LIMIT 1')['id'] ?? 0); +if ($id_module_note_spese <= 0) { + exit(tr('Accesso negato')); +} +Permissions::addModule($id_module_note_spese); +Permissions::check(['r', 'rw']); + +$date_start = ($_SESSION['period_start'] ?? date('Y-01-01')); +$date_end = ($_SESSION['period_end'] ?? date('Y-12-31')); +$lang = (int) Models\Locale::getDefault()->id; +$rows = $dbo->fetchArray( + 'SELECT n.*, COALESCE(tl.`title`, t.`descrizione`) AS tipologia, ' + .'COALESCE(NULLIF(n.`controparte`, ""), a.`ragione_sociale`, "") AS controparte_display, ' + .'COALESCE(op.`ragione_sociale`, "") AS operatore, ' + .'(SELECT COUNT(*) FROM `zz_files` f WHERE f.`id_module` = '.prepare($id_module_note_spese).' AND f.`id_plugin` IS NULL ' + .'AND f.`id_record` = n.`id` AND (f.`key` IS NULL OR f.`key` = "")) AS allegati ' + .'FROM `co_note_spese` n ' + .'INNER JOIN `co_note_spese_stati` st ON st.`id` = n.`id_stato` AND st.`name` = '.prepare('confermato').' ' + .'LEFT JOIN `co_note_spese_tipologie` t ON t.`id` = n.`id_tipologia` ' + .'LEFT JOIN `co_note_spese_tipologie_lang` tl ON tl.`id_record` = t.`id` AND tl.`id_lang` = '.prepare($lang).' ' + .'LEFT JOIN `an_anagrafiche` a ON a.`id` = n.`id_anagrafica` ' + .'LEFT JOIN `an_anagrafiche` op ON op.`id` = n.`id_operatore` ' + .'WHERE n.`data` >= '.prepare($date_start).' AND n.`data` <= '.prepare($date_end).' ' + .'ORDER BY n.`data`, n.`id`' +); + +if (ob_get_length()) { + ob_clean(); +} + +$filename = 'note_spese_'.$date_start.'_'.$date_end.'.csv'; +header('Content-Type: text/csv; charset=UTF-8'); +header('Content-Disposition: attachment; filename="'.$filename.'"'); +header('Pragma: no-cache'); +header('Expires: 0'); + +$out = fopen('php://output', 'wb'); +fwrite($out, "\xEF\xBB\xBF"); +fputcsv($out, [tr('Data'), tr('Tipologia'), tr('Descrizione'), tr('Controparte'), tr('Operatore'), tr('Importo'), tr('Allegati'), tr('Origine'), tr('Note')], ';', '"', ''); + +foreach ($rows as $row) { + fputcsv($out, [ + Translator::dateToLocale($row['data']), + noteSpeseCsvSafeCell($row['tipologia']), + noteSpeseCsvSafeCell($row['descrizione']), + noteSpeseCsvSafeCell($row['controparte_display']), + noteSpeseCsvSafeCell($row['operatore']), + number_format((float) $row['importo'], 2, ',', ''), + (int) $row['allegati'], + noteSpeseCsvSafeCell(noteSpeseSourceLabel($row['origine'])), + noteSpeseCsvSafeCell(preg_replace('/\s+/u', ' ', (string) $row['note'])), + ], ';', '"', ''); +} + +fclose($out); +exit; diff --git a/modules/note_spese/init.php b/modules/note_spese/init.php new file mode 100644 index 000000000..1807aa41b --- /dev/null +++ b/modules/note_spese/init.php @@ -0,0 +1,17 @@ +id; + $record = $dbo->fetchOne( + 'SELECT n.*, COALESCE(tl.`title`, t.`descrizione`) AS `tipologia`, ' + .'COALESCE(sl.`title`, st.`name`) AS `stato`, st.`name` AS `stato_name`, st.`colore` AS `stato_colore` ' + .'FROM `co_note_spese` n ' + .'LEFT JOIN `co_note_spese_tipologie` t ON t.`id` = n.`id_tipologia` ' + .'LEFT JOIN `co_note_spese_tipologie_lang` tl ON tl.`id_record` = t.`id` AND tl.`id_lang` = '.prepare($lang).' ' + .'LEFT JOIN `co_note_spese_stati` st ON st.`id` = n.`id_stato` ' + .'LEFT JOIN `co_note_spese_stati_lang` sl ON sl.`id_record` = st.`id` AND sl.`id_lang` = '.prepare($lang).' ' + .'WHERE n.`id` = '.prepare($id_record) + ); +} diff --git a/modules/note_spese/modutil.php b/modules/note_spese/modutil.php new file mode 100644 index 000000000..599deb529 --- /dev/null +++ b/modules/note_spese/modutil.php @@ -0,0 +1,346 @@ + $lastDot) { + $value = str_replace('.', '', $value); + $value = str_replace(',', '.', $value); + } else { + $value = str_replace(',', '', $value); + } + } elseif ($lastComma !== false || $lastDot !== false) { + $separator = $lastComma !== false ? ',' : '.'; + $parts = explode($separator, $value); + + if (count($parts) > 2) { + $groupsAreThousands = true; + foreach (array_slice($parts, 1) as $part) { + if (strlen($part) !== 3 || !ctype_digit($part)) { + $groupsAreThousands = false; + break; + } + } + + if ($groupsAreThousands) { + $value = implode('', $parts); + } else { + $decimal = array_pop($parts); + $value = implode('', $parts).'.'.$decimal; + } + } else { + [$integer, $decimal] = array_pad($parts, 2, ''); + // Nel registro gli importi hanno due decimali: un singolo gruppo di + // tre cifre viene quindi interpretato come separatore delle migliaia. + if ($decimal !== '' && strlen($decimal) === 3 && ctype_digit(ltrim($integer, '+-')) && ctype_digit($decimal)) { + $value = $integer.$decimal; + } elseif ($separator === ',') { + $value = str_replace(',', '.', $value); + } + } + } + + return is_numeric($value) ? round((float) $value, 2) : null; +} + +function noteSpeseParseDate($value) +{ + $value = trim((string) $value); + $formats = ['d/m/Y', 'd-m-Y', 'Y-m-d', 'd/m/y', 'd-m-y']; + + foreach ($formats as $format) { + $date = DateTime::createFromFormat('!'.$format, $value); + if ($date && $date->format($format) === $value) { + return $date->format('Y-m-d'); + } + } + + return null; +} + +function noteSpeseIsDateInPeriod($date, $periodStart, $periodEnd) +{ + $date = noteSpeseParseDate($date); + $periodStart = noteSpeseParseDate($periodStart); + $periodEnd = noteSpeseParseDate($periodEnd); + + return !empty($date) && !empty($periodStart) && !empty($periodEnd) && $date >= $periodStart && $date <= $periodEnd; +} + +function noteSpeseGuessCategoryCode($value) +{ + $value = noteSpeseLower(trim(strip_tags((string) $value))); + + $rules = [ + 'carburante' => ['carburante', 'benzina', 'diesel', 'gasolio', 'rifornimento'], + 'pedaggio' => ['pedaggio', 'autostrada', 'telepass'], + 'parcheggio' => ['parcheggio', 'parking', 'sosta'], + 'vitto' => ['pranzo', 'cena', 'ristorante', 'ristorazione', 'bar', 'vitto'], + 'alloggio' => ['hotel', 'albergo', 'alloggio', 'pernottamento'], + 'trasporto' => ['taxi', 'treno', 'aereo', 'trasporto', 'bus', 'autobus'], + 'materiale_consumo' => ['materiale di consumo', 'consumabile', 'cancelleria'], + 'assicurazioni' => ['assicurazione', 'assicurazioni', 'polizza', 'polizze'], + 'affitti' => ['affitto', 'locazione', 'canone locazione'], + 'contributi_tributi' => ['inps', 'inail', 'contributo', 'contributi', 'tributo', 'tributi', 'f24'], + 'spese_bancarie' => ['commissione bancaria', 'commissioni bancarie', 'spese bancarie', 'spesa bancaria', 'commissione', 'commissioni'], + 'personale' => ['stipendio', 'stipendi', 'cedolino', 'cedolini', 'busta paga'], + ]; + + foreach ($rules as $code => $keywords) { + foreach ($keywords as $keyword) { + if (noteSpeseContains($value, $keyword)) { + return $code; + } + } + } + + return 'altro'; +} + +function noteSpeseFindDuplicate($dbo, $date, $amount, $description, $counterparty = '', $excludeId = null, $operatorId = null) +{ + $date = noteSpeseParseDate($date); + $amount = noteSpeseParseAmount($amount); + $description = noteSpeseNormalizeText($description); + $counterparty = noteSpeseNormalizeText($counterparty); + $operatorId = (int) $operatorId; + + if (empty($date) || $amount === null || $amount <= 0 || $description === '') { + return null; + } + + $whereExclude = !empty($excludeId) ? ' AND `id` != '.prepare((int) $excludeId) : ''; + + return $dbo->fetchOne( + 'SELECT n.`id`, n.`data`, n.`importo`, n.`descrizione`, n.`controparte`, n.`origine`, n.`id_stato` ' + .'FROM `co_note_spese` n ' + .'LEFT JOIN `co_note_spese_stati` st ON st.`id` = n.`id_stato` ' + .'WHERE n.`data` = '.prepare($date) + .' AND n.`importo` = '.prepare(number_format($amount, 2, '.', '')) + .' AND LOWER(TRIM(n.`descrizione`)) = '.prepare($description) + .' AND LOWER(TRIM(COALESCE(n.`controparte`, ""))) = '.prepare($counterparty) + .' AND COALESCE(n.`id_operatore`, 0) = '.prepare($operatorId) + .' AND COALESCE(st.`name`, "") != '.prepare('escluso') + .str_replace('`id`', 'n.`id`', $whereExclude) + .' ORDER BY n.`id` ASC LIMIT 1' + ); +} + + +function noteSpeseAnagraficaExists($dbo, $idAnagrafica) +{ + $idAnagrafica = (int) $idAnagrafica; + if ($idAnagrafica <= 0) { + return false; + } + + return !empty($dbo->fetchOne( + 'SELECT `id` FROM `an_anagrafiche` WHERE `id` = '.prepare($idAnagrafica).' LIMIT 1' + )); +} + + +function noteSpeseOperatorSelectQuery($currentId = null) +{ + $currentId = (int) $currentId; + + $activeTechnician = '(a.`deleted_at` IS NULL ' + .'AND EXISTS (' + .'SELECT 1 FROM `an_tipi_anagrafiche_anagrafiche` ta ' + .'INNER JOIN `an_tipi_anagrafiche` t ON t.`id` = ta.`id_tipo_anagrafica` ' + .'WHERE ta.`id_anagrafica` = a.`id` AND t.`name` = \'Tecnico\') ' + .'AND (NOT EXISTS (SELECT 1 FROM `zz_users` ux WHERE ux.`id_anagrafica` = a.`id`) ' + .'OR EXISTS (SELECT 1 FROM `zz_users` ua WHERE ua.`id_anagrafica` = a.`id` AND ua.`enabled` = 1)))'; + + $where = $currentId > 0 + ? '('.$activeTechnician.' OR a.`id` = '.$currentId.')' + : $activeTechnician; + + return 'SELECT DISTINCT a.`id` AS `id`, ' + .'CONCAT(a.`ragione_sociale`, IF(COALESCE(a.`codice`, \'\') = \'\', \'\', CONCAT(\' - \', a.`codice`)), ' + .'IF(('.$activeTechnician.'), \'\', \' (non attivo)\')) AS `descrizione` ' + .'FROM `an_anagrafiche` a ' + .'WHERE '.$where.' ' + .'ORDER BY a.`ragione_sociale`'; +} + +function noteSpeseOperatorExists($dbo, $operatorId, $allowedCurrentId = null) +{ + $operatorId = (int) $operatorId; + $allowedCurrentId = (int) $allowedCurrentId; + if ($operatorId <= 0) { + return true; + } + + if ($allowedCurrentId > 0 && $operatorId === $allowedCurrentId) { + return !empty($dbo->fetchOne( + 'SELECT `id` FROM `an_anagrafiche` WHERE `id` = '.prepare($operatorId).' LIMIT 1' + )); + } + + return !empty($dbo->fetchOne( + 'SELECT a.`id` FROM `an_anagrafiche` a ' + .'WHERE a.`id` = '.prepare($operatorId).' ' + .'AND a.`deleted_at` IS NULL ' + .'AND EXISTS (' + .'SELECT 1 FROM `an_tipi_anagrafiche_anagrafiche` ta ' + .'INNER JOIN `an_tipi_anagrafiche` t ON t.`id` = ta.`id_tipo_anagrafica` ' + .'WHERE ta.`id_anagrafica` = a.`id` AND t.`name` = '.prepare('Tecnico').') ' + .'AND (NOT EXISTS (SELECT 1 FROM `zz_users` ux WHERE ux.`id_anagrafica` = a.`id`) ' + .'OR EXISTS (SELECT 1 FROM `zz_users` ua WHERE ua.`id_anagrafica` = a.`id` AND ua.`enabled` = 1)) ' + .'LIMIT 1' + )); +} + +function noteSpeseAppendNote($note, $line) +{ + $note = trim((string) $note); + $line = trim((string) $line); + + if ($line === '') { + return $note !== '' ? $note : null; + } + + if ($note === '') { + return $line; + } + + if (noteSpeseContains($note, $line)) { + return $note; + } + + return $note."\n".$line; +} + +function noteSpeseDeleteRecord($dbo, $idModule, $idRecord) +{ + $idModule = (int) $idModule; + $idRecord = (int) $idRecord; + if ($idModule <= 0 || $idRecord <= 0) { + return false; + } + + Uploads::deleteLinked([ + 'id_module' => $idModule, + 'id_plugin' => null, + 'id_record' => $idRecord, + 'key' => null, + ]); + + return (bool) $dbo->delete('co_note_spese', ['id' => $idRecord]); +} + +function noteSpeseSourceLabel($source) +{ + return match ((string) $source) { + 'automezzi_rifornimento' => tr('Automezzi'), + 'scadenzario_generico' => tr('Scadenzario'), + 'excel' => tr('Importazione dati'), + default => tr('Manuale'), + }; +} + +function noteSpeseGetStatusId($dbo, $name) +{ + $row = $dbo->fetchOne('SELECT `id` FROM `co_note_spese_stati` WHERE `name` = '.prepare($name).' LIMIT 1'); + + return !empty($row['id']) ? (int) $row['id'] : null; +} + +function noteSpeseGetCategory($dbo, $value) +{ + $value = trim((string) $value); + $lang = (int) Models\Locale::getDefault()->id; + + if ($value !== '') { + $row = $dbo->fetchOne( + 'SELECT t.`id`, t.`codice`, COALESCE(l.`title`, t.`descrizione`) AS `descrizione` ' + .'FROM `co_note_spese_tipologie` t ' + .'LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` AND l.`id_lang` = '.prepare($lang).' ' + .'WHERE t.`enabled` = 1 AND (' + .'LOWER(t.`codice`) = LOWER('.prepare($value).') OR ' + .'LOWER(t.`descrizione`) = LOWER('.prepare($value).') OR ' + .'LOWER(l.`title`) = LOWER('.prepare($value).')) LIMIT 1' + ); + if (!empty($row)) { + return $row; + } + } + + $code = noteSpeseGuessCategoryCode($value); + $row = $dbo->fetchOne( + 'SELECT t.`id`, t.`codice`, COALESCE(l.`title`, t.`descrizione`) AS `descrizione` ' + .'FROM `co_note_spese_tipologie` t ' + .'LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` AND l.`id_lang` = '.prepare($lang).' ' + .'WHERE t.`enabled` = 1 AND t.`codice` = '.prepare($code).' LIMIT 1' + ); + + if (!empty($row)) { + return $row; + } + + return $dbo->fetchOne( + 'SELECT t.`id`, t.`codice`, COALESCE(l.`title`, t.`descrizione`) AS `descrizione` ' + .'FROM `co_note_spese_tipologie` t ' + .'LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` AND l.`id_lang` = '.prepare($lang).' ' + .'WHERE t.`enabled` = 1 ORDER BY t.`ordine`, t.`id` LIMIT 1' + ); +} diff --git a/modules/note_spese/src/PendingExpensesHook.php b/modules/note_spese/src/PendingExpensesHook.php new file mode 100644 index 000000000..cd0993113 --- /dev/null +++ b/modules/note_spese/src/PendingExpensesHook.php @@ -0,0 +1,83 @@ +first(); + + if (empty($module) || !$dbo->tableExists('co_note_spese')) { + return [ + 'icon' => 'fa fa-money text-yellow', + 'link' => '', + 'message' => '', + 'show' => false, + ]; + } + + $periodStart = $_SESSION['period_start'] ?? date('Y-01-01'); + $periodEnd = $_SESSION['period_end'] ?? date('Y-12-31'); + $periodEndTimestamp = $periodEnd.' 23:59:59'; + $count = 0; + + $automezzi = Module::where('name', 'Automezzi')->first(); + $canReadAutomezzi = !empty($automezzi) + && in_array(\Modules::getPermission($automezzi->id), ['r', 'rw'], true) + && $dbo->tableExists('an_automezzi_rifornimenti'); + + if ($canReadAutomezzi) { + $result = $dbo->fetchOne( + 'SELECT COUNT(*) AS totale FROM `an_automezzi_rifornimenti` r ' + .'WHERE r.`data` >= '.prepare($periodStart).' AND r.`data` <= '.prepare($periodEndTimestamp).' ' + .'AND NOT EXISTS (SELECT 1 FROM `co_note_spese` n WHERE n.`origine` = '.prepare('automezzi_rifornimento').' AND n.`id_origine` = r.`id`)' + ); + $count += (int) ($result['totale'] ?? 0); + } + + $scadenzario = Module::where('name', 'Scadenzario')->first(); + $canReadScadenzario = !empty($scadenzario) + && in_array(\Modules::getPermission($scadenzario->id), ['r', 'rw'], true) + && $dbo->tableExists('co_scadenzario'); + + if ($canReadScadenzario) { + $result = $dbo->fetchOne( + 'SELECT COUNT(*) AS totale FROM `co_scadenzario` s ' + .'WHERE (s.`id_documento` IS NULL OR s.`id_documento` = 0) AND s.`da_pagare` < 0 ' + .'AND s.`scadenza` >= '.prepare($periodStart).' AND s.`scadenza` <= '.prepare($periodEnd).' ' + .'AND NOT EXISTS (SELECT 1 FROM `co_note_spese` n WHERE n.`origine` = '.prepare('scadenzario_generico').' AND n.`id_origine` = s.`id`)' + ); + $count += (int) ($result['totale'] ?? 0); + } + + $message = $count === 1 + ? tr("C'e' 1 spesa da importare") + : tr('Ci sono _NUM_ spese da importare', ['_NUM_' => $count]); + + return [ + 'icon' => 'fa fa-money text-yellow', + 'link' => base_path_osm().'/controller.php?id_module='.$module->id, + 'message' => $message, + 'show' => $count > 0, + ]; + } +} diff --git a/modules/note_spese/update/tables.php b/modules/note_spese/update/tables.php new file mode 100644 index 000000000..ca8e6fdf4 --- /dev/null +++ b/modules/note_spese/update/tables.php @@ -0,0 +1,9 @@ +fetchOne( + 'SELECT ' + .'COALESCE(SUM(IF(st.`name` = '.prepare('confermato').', n.`importo`, 0)), 0) AS confermato_totale, ' + .'SUM(IF(st.`name` = '.prepare('confermato').', 1, 0)) AS confermato_righe, ' + .'COALESCE(SUM(IF(st.`name` = '.prepare('da_verificare').', n.`importo`, 0)), 0) AS verifica_totale, ' + .'SUM(IF(st.`name` = '.prepare('da_verificare').', 1, 0)) AS verifica_righe ' + .'FROM `co_note_spese` n ' + .'LEFT JOIN `co_note_spese_stati` st ON st.`id` = n.`id_stato` ' + .'WHERE n.`data` >= '.prepare($period_start).' AND n.`data` <= '.prepare($period_end) + ) ?: []; + + $summary['senza_allegati'] = 0; + if ($id_module_note_spese > 0) { + $result = $dbo->fetchOne( + 'SELECT COUNT(*) AS totale ' + .'FROM `co_note_spese` n ' + .'INNER JOIN `co_note_spese_stati` st ON st.`id` = n.`id_stato` AND st.`name` = '.prepare('confermato').' ' + .'WHERE n.`data` >= '.prepare($period_start).' AND n.`data` <= '.prepare($period_end).' ' + .'AND NOT EXISTS (' + .'SELECT 1 FROM `zz_files` f ' + .'WHERE f.`id_module` = '.prepare($id_module_note_spese).' ' + .'AND f.`id_plugin` IS NULL ' + .'AND f.`id_record` = n.`id` ' + .'AND (f.`key` IS NULL OR f.`key` = "")' + .')' + ); + $summary['senza_allegati'] = (int) ($result['totale'] ?? 0); + } + + $cache[$cache_key] = $summary; + + return $summary; + } +} + +if (!function_exists('noteSpeseWidgetValue')) { + function noteSpeseWidgetValue($primary, $secondary) + { + return '' + .''.$primary.'' + .''.$secondary.'' + .''; + } +} + +$summary = noteSpeseWidgetSummary($dbo, $period_start, $period_end, $id_module_note_spese); +$name = $widget['name'] ?? ''; + +switch ($name) { + case 'Note spese - confermate': + echo noteSpeseWidgetValue( + moneyFormat($summary['confermato_totale'] ?? 0, 2), + tr('_NUM_ registrazioni', ['_NUM_' => (int) ($summary['confermato_righe'] ?? 0)]) + ); + break; + + case 'Note spese - da verificare': + echo noteSpeseWidgetValue( + moneyFormat($summary['verifica_totale'] ?? 0, 2), + tr('_NUM_ da verificare', ['_NUM_' => (int) ($summary['verifica_righe'] ?? 0)]) + ); + break; + + case 'Note spese - senza allegati': + echo noteSpeseWidgetValue( + (int) ($summary['senza_allegati'] ?? 0), + tr('spese confermate') + ); + break; + + default: + echo noteSpeseWidgetValue('0', ' '); + break; +} diff --git a/modules/tipologie_note_spese/actions.php b/modules/tipologie_note_spese/actions.php new file mode 100644 index 000000000..a38133643 --- /dev/null +++ b/modules/tipologie_note_spese/actions.php @@ -0,0 +1,116 @@ +id; + + if ($descrizione === '') { + flash()->error(tr('Inserire una descrizione.')); + break; + } + + $duplicate = $dbo->fetchOne( + 'SELECT t.`id` FROM `co_note_spese_tipologie` t ' + .'LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` ' + .'WHERE LOWER(t.`descrizione`) = LOWER('.prepare($descrizione).') OR LOWER(l.`title`) = LOWER('.prepare($descrizione).') LIMIT 1' + ); + if (!empty($duplicate)) { + flash()->error(tr('Esiste già una tipologia con questa descrizione.')); + break; + } + + $dbo->insert('co_note_spese_tipologie', [ + 'codice' => null, + 'descrizione' => $descrizione, + 'ordine' => $ordine ?: 100, + 'enabled' => 1, + 'can_delete' => 1, + ]); + $id_record = $dbo->lastInsertedID(); + + $dbo->insert('co_note_spese_tipologie_lang', [ + 'id_lang' => $id_lang, + 'id_record' => $id_record, + 'title' => $descrizione, + ]); + + flash()->info(tr('Tipologia aggiunta correttamente.')); + break; + + case 'update': + Permissions::check('rw'); + + if (empty($id_record)) { + break; + } + + $descrizione = trim((string) post('descrizione')); + $ordine = max(0, (int) post('ordine')); + $enabled = (int) post('enabled'); + $id_lang = (int) Models\Locale::getDefault()->id; + + if ($descrizione === '') { + flash()->error(tr('Inserire una descrizione.')); + break; + } + + $duplicate = $dbo->fetchOne( + 'SELECT t.`id` FROM `co_note_spese_tipologie` t ' + .'LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` ' + .'WHERE t.`id` != '.prepare($id_record).' AND (LOWER(t.`descrizione`) = LOWER('.prepare($descrizione).') ' + .'OR LOWER(l.`title`) = LOWER('.prepare($descrizione).')) LIMIT 1' + ); + if (!empty($duplicate)) { + flash()->error(tr('Esiste già una tipologia con questa descrizione.')); + break; + } + + $category_data = [ + 'ordine' => $ordine, + 'enabled' => $enabled ? 1 : 0, + ]; + if (Models\Locale::getDefault()->id == Models\Locale::getPredefined()->id) { + $category_data['descrizione'] = $descrizione; + } + $dbo->update('co_note_spese_tipologie', $category_data, ['id' => $id_record]); + + $translation = $dbo->fetchOne( + 'SELECT `id` FROM `co_note_spese_tipologie_lang` WHERE `id_lang` = '.prepare($id_lang).' AND `id_record` = '.prepare($id_record).' LIMIT 1' + ); + if (!empty($translation)) { + $dbo->update('co_note_spese_tipologie_lang', ['title' => $descrizione], ['id' => $translation['id']]); + } else { + $dbo->insert('co_note_spese_tipologie_lang', [ + 'id_lang' => $id_lang, + 'id_record' => $id_record, + 'title' => $descrizione, + ]); + } + + flash()->info(tr('Tipologia aggiornata correttamente.')); + break; + + case 'delete': + Permissions::check('rw'); + + if (empty($id_record)) { + break; + } + + $used = $dbo->fetchNum('SELECT `id` FROM `co_note_spese` WHERE `id_tipologia` = '.prepare($id_record)); + $record = $dbo->fetchOne('SELECT `can_delete` FROM `co_note_spese_tipologie` WHERE `id` = '.prepare($id_record)); + + if (empty($used) && !empty($record['can_delete'])) { + $dbo->delete('co_note_spese_tipologie', ['id' => $id_record]); + flash()->info(tr('Tipologia eliminata correttamente.')); + } else { + flash()->error(tr('La tipologia non può essere eliminata perché è predefinita o già utilizzata.')); + } + break; +} diff --git a/modules/tipologie_note_spese/add.php b/modules/tipologie_note_spese/add.php new file mode 100644 index 000000000..8bcd9fb8a --- /dev/null +++ b/modules/tipologie_note_spese/add.php @@ -0,0 +1,23 @@ + +
+ + + +
+
+ {[ "type": "text", "label": "", "name": "descrizione", "required": 1 ]} +
+
+ {[ "type": "number", "label": "", "name": "ordine", "value": "100", "decimals": 0, "min-value": "0" ]} +
+
+ +
+
+ +
+
+
diff --git a/modules/tipologie_note_spese/edit.php b/modules/tipologie_note_spese/edit.php new file mode 100644 index 000000000..6e0917bc1 --- /dev/null +++ b/modules/tipologie_note_spese/edit.php @@ -0,0 +1,38 @@ + +
+ + + +
+
+

+
+
+
+
+ {[ "type": "text", "label": "", "name": "descrizione", "required": 1, "value": "$title$" ]} +
+
+ {[ "type": "number", "label": "", "name": "ordine", "value": "$ordine$", "decimals": 0, "min-value": "0" ]} +
+
+ {[ "type": "checkbox", "label": "", "name": "enabled", "value": "$enabled$" ]} +
+
+
+
+
+ +fetchNum('SELECT `id` FROM `co_note_spese` WHERE `id_tipologia` = '.prepare($id_record)); +if (!empty($used)) { + echo '
'.tr('La tipologia è già utilizzata: può essere disattivata ma non eliminata.').'
'; +} elseif (!empty($record['can_delete'])) { + echo ' '.tr('Elimina').''; +} else { + echo '
'.tr('Questa è una tipologia predefinita: può essere disattivata ma non eliminata.').'
'; +} +?> diff --git a/modules/tipologie_note_spese/init.php b/modules/tipologie_note_spese/init.php new file mode 100644 index 000000000..5e8b2c91d --- /dev/null +++ b/modules/tipologie_note_spese/init.php @@ -0,0 +1,13 @@ +id; + $record = $dbo->fetchOne( + 'SELECT t.*, COALESCE(l.`title`, t.`descrizione`) AS `title` ' + .'FROM `co_note_spese_tipologie` t ' + .'LEFT JOIN `co_note_spese_tipologie_lang` l ON l.`id_record` = t.`id` AND l.`id_lang` = '.prepare($id_lang).' ' + .'WHERE t.`id` = '.prepare($id_record).' LIMIT 1' + ); +} diff --git a/templates/note_spese/body.php b/templates/note_spese/body.php new file mode 100644 index 000000000..a5a5a0ff9 --- /dev/null +++ b/templates/note_spese/body.php @@ -0,0 +1,80 @@ + Translator::dateToLocale($date_start), + '_END_' => Translator::dateToLocale($date_end), +]); + +$total = 0; +$total_attachments = 0; + +// Titolo essenziale: la stampa è una Nota spese, il periodo è quello globale selezionato. +echo '' + .'' + .'' + .'' + .'
'.tr('Nota spese').'
' + .'
'.$period.'
'.tr('Solo confermate').'
'; + +echo '' + .'' + .'' + .'' + .'' + .'' + .'' + .'' + .'' + .''; + +if (empty($rows)) { + echo ''; +} else { + foreach ($rows as $row) { + $total += (float) $row['importo']; + $total_attachments += (int) $row['allegati']; + $description = htmlentities((string) $row['descrizione']); + if (!empty($row['note'])) { + $description .= '
'.nl2br(htmlentities((string) $row['note'])).''; + } + + echo '' + .'' + .'' + .'' + .'' + .'' + .'' + .'' + .''; + } +} + +echo '' + .'' + .'' + .'' + .'
'.tr('Data').''.tr('Tipologia').''.tr('Descrizione').''.tr('Controparte').''.tr('Operatore').''.tr('Allegati').''.tr('Importo').'
'.tr('Nessuna nota spesa confermata nel periodo selezionato.').'
'.Translator::dateToLocale($row['data']).''.htmlentities((string) $row['tipologia']).''.$description.''.htmlentities((string) $row['controparte_display']).''.htmlentities((string) ($row['operatore'] ?: '-')).''.(int) $row['allegati'].''.moneyFormat($row['importo'], 2).'
'.tr('Totale', [], ['upper' => true]).':'.moneyFormat($total, 2).'
'; + +if (!empty($rows)) { + echo '' + .'
' + .''.tr('Registrazioni').': '.count($rows) + .'     '.tr('Allegati').': '.$total_attachments + .'     '.tr('Senza allegati').': '.(int) $without_attachments + .'
'; +} + +if (!empty($groups)) { + echo '
'.tr('Totali per tipologia').'
' + .'' + .''; + + foreach ($groups as $group) { + echo ''; + } + + echo '
'.tr('Tipologia').''.tr('Righe').''.tr('Totale').'
'.htmlentities((string) $group['tipologia']).''.(int) $group['righe'].''.moneyFormat($group['totale'], 2).'
'; +} diff --git a/templates/note_spese/footer.php b/templates/note_spese/footer.php new file mode 100644 index 000000000..5dc5b1265 --- /dev/null +++ b/templates/note_spese/footer.php @@ -0,0 +1,13 @@ + + + '.tr('Nota spese').' + '.tr('Pagina _PAGE_ di _TOTAL_', [ + '_PAGE_' => '{PAGENO}', + '_TOTAL_' => '{nb}', +]).' + +'; diff --git a/templates/note_spese/header.php b/templates/note_spese/header.php new file mode 100644 index 000000000..f656c413f --- /dev/null +++ b/templates/note_spese/header.php @@ -0,0 +1,23 @@ + + + + '.htmlentities($identity).'' + .(!empty($fiscal) ? '  |  '.htmlentities(implode(' · ', $fiscal)) : '').' + + '.tr('Documento interno').' + +'; diff --git a/templates/note_spese/init.php b/templates/note_spese/init.php new file mode 100644 index 000000000..eea4d2736 --- /dev/null +++ b/templates/note_spese/init.php @@ -0,0 +1,50 @@ +id; +$id_module_note_spese = (int) ($dbo->fetchOne('SELECT `id` FROM `zz_modules` WHERE `name` = '.prepare('Note spese').' LIMIT 1')['id'] ?? 0); + +$rows = $dbo->fetchArray( + 'SELECT n.*, COALESCE(tl.`title`, t.`descrizione`) AS tipologia, ' + .'COALESCE(NULLIF(n.`controparte`, ""), a.`ragione_sociale`, "") AS controparte_display, ' + .'COALESCE(op.`ragione_sociale`, "") AS operatore, ' + .'(SELECT COUNT(*) FROM `zz_files` f WHERE f.`id_module` = '.prepare($id_module_note_spese).' AND f.`id_plugin` IS NULL ' + .'AND f.`id_record` = n.`id` AND (f.`key` IS NULL OR f.`key` = "")) AS allegati ' + .'FROM `co_note_spese` n ' + .'INNER JOIN `co_note_spese_stati` st ON st.`id` = n.`id_stato` AND st.`name` = '.prepare('confermato').' ' + .'INNER JOIN `co_note_spese_tipologie` t ON t.`id` = n.`id_tipologia` ' + .'LEFT JOIN `co_note_spese_tipologie_lang` tl ON tl.`id_record` = t.`id` AND tl.`id_lang` = '.prepare($id_lang).' ' + .'LEFT JOIN `an_anagrafiche` a ON a.`id` = n.`id_anagrafica` ' + .'LEFT JOIN `an_anagrafiche` op ON op.`id` = n.`id_operatore` ' + .'WHERE n.`data` >= '.prepare($date_start).' AND n.`data` <= '.prepare($date_end).' ' + .'ORDER BY n.`data` ASC, n.`id` ASC' +); + +$groups = $dbo->fetchArray( + 'SELECT COALESCE(tl.`title`, t.`descrizione`, '.prepare(tr('Senza tipologia')).') AS tipologia, SUM(n.`importo`) AS totale, COUNT(*) AS righe ' + .'FROM `co_note_spese` n ' + .'INNER JOIN `co_note_spese_stati` st ON st.`id` = n.`id_stato` AND st.`name` = '.prepare('confermato').' ' + .'LEFT JOIN `co_note_spese_tipologie` t ON t.`id` = n.`id_tipologia` ' + .'LEFT JOIN `co_note_spese_tipologie_lang` tl ON tl.`id_record` = t.`id` AND tl.`id_lang` = '.prepare($id_lang).' ' + .'WHERE n.`data` >= '.prepare($date_start).' AND n.`data` <= '.prepare($date_end).' ' + .'GROUP BY t.`id`, tl.`title`, t.`descrizione`, t.`ordine` ' + .'ORDER BY t.`ordine`, tipologia' +); + +$without_attachments = 0; +foreach ($rows as $row) { + if (empty($row['allegati'])) { + ++$without_attachments; + } +} diff --git a/templates/note_spese/settings.php b/templates/note_spese/settings.php new file mode 100644 index 000000000..8ebba37c4 --- /dev/null +++ b/templates/note_spese/settings.php @@ -0,0 +1,14 @@ + 'L', + 'format' => 'A4', + 'font-size' => 8, + 'margins' => [ + 'top' => 'auto', + 'bottom' => 'auto', + 'left' => 10, + 'right' => 10, + ], + 'header-font-size' => 7, +]; diff --git a/update/2_12.sql b/update/2_12.sql index 201536992..11ff70f58 100644 --- a/update/2_12.sql +++ b/update/2_12.sql @@ -102,3 +102,208 @@ INSERT INTO `zz_settings` (`nome`, `valore`, `tipo`, `editable`, `sezione`, `ord INSERT INTO `zz_settings_lang` (`id_lang`, `id_record`, `title`, `help`) VALUES (1, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Tipologia anagrafica predefinita'), 'Tipologia anagrafica predefinita', 'Tipologia (Azienda, Ente pubblico o Privato) preselezionata automaticamente nella finestra di aggiunta di una nuova anagrafica. Se non impostata, nessuna tipologia viene preselezionata.'), (2, (SELECT `id` FROM `zz_settings` WHERE `nome` = 'Tipologia anagrafica predefinita'), 'Default entity classification', 'Classification (Company, Public entity or Private) automatically preselected in the new entity creation window. If not set, no classification is preselected.'); + +-- Modulo Note spese (#1461) +CREATE TABLE `co_note_spese_tipologie` ( + `id` INT NOT NULL AUTO_INCREMENT, + `codice` VARCHAR(50) NULL, + `descrizione` VARCHAR(100) NOT NULL, + `ordine` INT NOT NULL DEFAULT 100, + `enabled` TINYINT(1) NOT NULL DEFAULT 1, + `can_delete` TINYINT(1) NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `co_note_spese_tipologie_codice_unique` (`codice`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `co_note_spese_tipologie_lang` ( + `id` INT NOT NULL AUTO_INCREMENT, + `id_lang` INT NOT NULL, + `id_record` INT NOT NULL, + `title` VARCHAR(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `co_note_spese_tipologie_lang_unique` (`id_lang`, `id_record`), + KEY `co_note_spese_tipologie_lang_record_index` (`id_record`), + CONSTRAINT `co_note_spese_tipologie_lang_ibfk_1` FOREIGN KEY (`id_record`) REFERENCES `co_note_spese_tipologie` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `co_note_spese_tipologie_lang_ibfk_2` FOREIGN KEY (`id_lang`) REFERENCES `zz_langs` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `co_note_spese_stati` ( + `id` INT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(50) NOT NULL, + `colore` VARCHAR(30) NOT NULL DEFAULT 'secondary', + `ordine` INT NOT NULL DEFAULT 100, + `can_delete` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `co_note_spese_stati_name_unique` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `co_note_spese_stati_lang` ( + `id` INT NOT NULL AUTO_INCREMENT, + `id_lang` INT NOT NULL, + `id_record` INT NOT NULL, + `title` VARCHAR(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `co_note_spese_stati_lang_unique` (`id_lang`, `id_record`), + KEY `co_note_spese_stati_lang_record_index` (`id_record`), + CONSTRAINT `co_note_spese_stati_lang_ibfk_1` FOREIGN KEY (`id_record`) REFERENCES `co_note_spese_stati` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `co_note_spese_stati_lang_ibfk_2` FOREIGN KEY (`id_lang`) REFERENCES `zz_langs` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `co_note_spese` ( + `id` INT NOT NULL AUTO_INCREMENT, + `data` DATE NOT NULL, + `id_tipologia` INT NOT NULL, + `id_stato` INT NOT NULL, + `descrizione` VARCHAR(255) NOT NULL, + `importo` DECIMAL(12,2) NOT NULL DEFAULT 0.00, + `id_anagrafica` INT NULL, + `id_operatore` INT NULL, + `controparte` VARCHAR(255) NULL, + `origine` VARCHAR(50) NOT NULL DEFAULT 'manuale', + `id_origine` INT NULL, + `note` TEXT NULL, + `created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `co_note_spese_data_index` (`data`), + KEY `co_note_spese_tipologia_index` (`id_tipologia`), + KEY `co_note_spese_stato_index` (`id_stato`), + KEY `co_note_spese_anagrafica_index` (`id_anagrafica`), + KEY `co_note_spese_operatore_index` (`id_operatore`), + UNIQUE KEY `co_note_spese_origine_unique` (`origine`, `id_origine`), + CONSTRAINT `co_note_spese_ibfk_1` FOREIGN KEY (`id_tipologia`) REFERENCES `co_note_spese_tipologie` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT `co_note_spese_ibfk_2` FOREIGN KEY (`id_stato`) REFERENCES `co_note_spese_stati` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT `co_note_spese_ibfk_3` FOREIGN KEY (`id_anagrafica`) REFERENCES `an_anagrafiche` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `co_note_spese_ibfk_4` FOREIGN KEY (`id_operatore`) REFERENCES `an_anagrafiche` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO `co_note_spese_tipologie` (`codice`, `descrizione`, `ordine`, `enabled`, `can_delete`) VALUES +('carburante', 'Carburante', 10, 1, 0), +('pedaggio', 'Pedaggio / Autostrada', 20, 1, 0), +('parcheggio', 'Parcheggio', 30, 1, 0), +('vitto', 'Vitto', 40, 1, 0), +('alloggio', 'Alloggio', 50, 1, 0), +('trasporto', 'Trasporto', 60, 1, 0), +('materiale_consumo', 'Materiale di consumo', 70, 1, 0), +('assicurazioni', 'Assicurazioni', 80, 1, 0), +('affitti', 'Canoni / Affitti', 90, 1, 0), +('contributi_tributi', 'Contributi / Tributi', 100, 1, 0), +('spese_bancarie', 'Spese bancarie / Commissioni', 110, 1, 0), +('personale', 'Personale', 120, 1, 0), +('altro', 'Altro', 1000, 1, 0); + +INSERT INTO `co_note_spese_tipologie_lang` (`id_lang`, `id_record`, `title`) +SELECT 1, `id`, `descrizione` FROM `co_note_spese_tipologie`; +INSERT INTO `co_note_spese_tipologie_lang` (`id_lang`, `id_record`, `title`) +SELECT 2, `id`, CASE `codice` + WHEN 'carburante' THEN 'Fuel' + WHEN 'pedaggio' THEN 'Toll / motorway' + WHEN 'parcheggio' THEN 'Parking' + WHEN 'vitto' THEN 'Meals' + WHEN 'alloggio' THEN 'Accommodation' + WHEN 'trasporto' THEN 'Transport' + WHEN 'materiale_consumo' THEN 'Consumables' + WHEN 'assicurazioni' THEN 'Insurance' + WHEN 'affitti' THEN 'Rent / leases' + WHEN 'contributi_tributi' THEN 'Contributions / taxes' + WHEN 'spese_bancarie' THEN 'Bank fees / commissions' + WHEN 'personale' THEN 'Personnel' + ELSE 'Other' +END FROM `co_note_spese_tipologie`; + +INSERT INTO `co_note_spese_stati` (`name`, `colore`, `ordine`, `can_delete`) VALUES +('da_verificare', 'warning', 10, 0), +('confermato', 'success', 20, 0), +('escluso', 'secondary', 30, 0); + +INSERT INTO `co_note_spese_stati_lang` (`id_lang`, `id_record`, `title`) +SELECT 1, `id`, CASE `name` WHEN 'da_verificare' THEN 'Da verificare' WHEN 'confermato' THEN 'Confermata' ELSE 'Esclusa' END FROM `co_note_spese_stati`; +INSERT INTO `co_note_spese_stati_lang` (`id_lang`, `id_record`, `title`) +SELECT 2, `id`, CASE `name` WHEN 'da_verificare' THEN 'To review' WHEN 'confermato' THEN 'Confirmed' ELSE 'Excluded' END FROM `co_note_spese_stati`; + +INSERT INTO `zz_modules` (`name`, `directory`, `attachments_directory`, `options`, `options2`, `icon`, `version`, `compatibility`, `order`, `parent`, `default`, `enabled`) VALUES +('Note spese', 'note_spese', 'note_spese', +'SELECT |select| FROM `co_note_spese` LEFT JOIN `co_note_spese_tipologie` ON `co_note_spese_tipologie`.`id` = `co_note_spese`.`id_tipologia` LEFT JOIN `co_note_spese_tipologie_lang` ON (`co_note_spese_tipologie_lang`.`id_record` = `co_note_spese_tipologie`.`id` AND `co_note_spese_tipologie_lang`.|lang|) LEFT JOIN `co_note_spese_stati` ON `co_note_spese_stati`.`id` = `co_note_spese`.`id_stato` LEFT JOIN `co_note_spese_stati_lang` ON (`co_note_spese_stati_lang`.`id_record` = `co_note_spese_stati`.`id` AND `co_note_spese_stati_lang`.|lang|) LEFT JOIN `an_anagrafiche` ON `an_anagrafiche`.`id` = `co_note_spese`.`id_anagrafica` LEFT JOIN `an_anagrafiche` AS `an_operatori` ON `an_operatori`.`id` = `co_note_spese`.`id_operatore` WHERE 1=1 |date_period(`co_note_spese`.`data`)| HAVING 2=2 ORDER BY `co_note_spese`.`data` DESC, `co_note_spese`.`id` DESC', +'', 'fa fa-money', '2.12', '2.12', 20, COALESCE((SELECT `parent` FROM `zz_modules` WHERE `name` = 'Prima nota'), (SELECT `id` FROM `zz_modules` WHERE `name` = 'Contabilità')), 1, 1), +('Tipologie note spese', 'tipologie_note_spese', 'tipologie_note_spese', +'SELECT |select| FROM `co_note_spese_tipologie` LEFT JOIN `co_note_spese_tipologie_lang` ON (`co_note_spese_tipologie_lang`.`id_record` = `co_note_spese_tipologie`.`id` AND `co_note_spese_tipologie_lang`.|lang|) WHERE 1=1 HAVING 2=2 ORDER BY `co_note_spese_tipologie`.`ordine`, COALESCE(`co_note_spese_tipologie_lang`.`title`, `co_note_spese_tipologie`.`descrizione`)', +'', 'fa fa-tags', '2.12', '2.12', 20, (SELECT `id` FROM `zz_modules` WHERE `name` = 'Tabelle'), 1, 1); + +INSERT INTO `zz_modules_lang` (`id_lang`, `id_record`, `title`, `meta_title`) VALUES +(1, (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Note spese', 'Nota spesa - {descrizione}'), +(2, (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Expense notes', 'Expense note - {descrizione}'), +(1, (SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'), 'Tipologie note spese', 'Tipologia nota spesa - {title}'), +(2, (SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'), 'Expense categories', 'Expense category - {title}'); + +INSERT INTO `zz_views` (`id_module`, `name`, `query`, `order`, `search`, `visible`, `format`, `html_format`, `summable`, `default`) VALUES +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'id', '`co_note_spese`.`id`', 1, 0, 0, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Data', '`co_note_spese`.`data`', 2, 1, 1, 1, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Tipologia', 'COALESCE(`co_note_spese_tipologie_lang`.`title`, `co_note_spese_tipologie`.`descrizione`)', 3, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Descrizione', '`co_note_spese`.`descrizione`', 4, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Controparte', 'COALESCE(NULLIF(`co_note_spese`.`controparte`, ''''), `an_anagrafiche`.`ragione_sociale`, '''')', 5, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Operatore', 'COALESCE(`an_operatori`.`ragione_sociale`, '''')', 6, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'icon_Stato', 'CASE `co_note_spese_stati`.`name` WHEN ''confermato'' THEN ''fa fa-check-circle fa-lg text-success'' WHEN ''da_verificare'' THEN ''fa fa-exclamation-triangle fa-lg text-warning'' ELSE ''fa fa-ban fa-lg text-secondary'' END', 7, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'icon_title_Stato', 'COALESCE(`co_note_spese_stati_lang`.`title`, `co_note_spese_stati`.`name`)', 8, 0, 0, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Origine', 'CASE `co_note_spese`.`origine` WHEN ''automezzi_rifornimento'' THEN ''Automezzi'' WHEN ''scadenzario_generico'' THEN ''Scadenzario'' WHEN ''excel'' THEN ''Importazione'' ELSE ''Manuale'' END', 9, 1, 0, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'icon_Allegati', 'IF((SELECT COUNT(*) FROM `zz_files` WHERE `zz_files`.`id_module` = (SELECT `id` FROM `zz_modules` WHERE `name` = ''Note spese'') AND `zz_files`.`id_plugin` IS NULL AND `zz_files`.`id_record` = `co_note_spese`.`id` AND (`zz_files`.`key` IS NULL OR `zz_files`.`key` = '''')) > 0, ''fa fa-paperclip fa-lg text-success'', ''fa fa-paperclip fa-lg text-warning'')', 10, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'icon_title_Allegati', 'CAST((SELECT COUNT(*) FROM `zz_files` WHERE `zz_files`.`id_module` = (SELECT `id` FROM `zz_modules` WHERE `name` = ''Note spese'') AND `zz_files`.`id_plugin` IS NULL AND `zz_files`.`id_record` = `co_note_spese`.`id` AND (`zz_files`.`key` IS NULL OR `zz_files`.`key` = '''')) AS CHAR)', 11, 0, 0, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'Importo', '`co_note_spese`.`importo`', 12, 1, 1, 1, 0, 1, 1); + +INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) +SELECT 1, `id`, `name` FROM `zz_views` WHERE `id_module` = (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'); +INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) +SELECT 2, `id`, CASE `name` + WHEN 'Data' THEN 'Date' WHEN 'Tipologia' THEN 'Category' WHEN 'Descrizione' THEN 'Description' + WHEN 'Controparte' THEN 'Counterparty' WHEN 'Operatore' THEN 'Operator' + WHEN 'icon_Stato' THEN 'icon_Status' WHEN 'icon_title_Stato' THEN 'icon_title_Status' + WHEN 'Origine' THEN 'Source' WHEN 'icon_Allegati' THEN 'icon_Attachments' + WHEN 'icon_title_Allegati' THEN 'icon_title_Attachments' WHEN 'Importo' THEN 'Amount' ELSE `name` END +FROM `zz_views` WHERE `id_module` = (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'); + +INSERT INTO `zz_views` (`id_module`, `name`, `query`, `order`, `search`, `visible`, `format`, `html_format`, `summable`, `default`) VALUES +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'), 'id', '`co_note_spese_tipologie`.`id`', 1, 0, 0, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'), 'Descrizione', 'COALESCE(`co_note_spese_tipologie_lang`.`title`, `co_note_spese_tipologie`.`descrizione`)', 2, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'), 'Ordine', '`co_note_spese_tipologie`.`ordine`', 3, 1, 1, 0, 0, 0, 1), +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'), 'Attiva', 'IF(`co_note_spese_tipologie`.`enabled` = 1, ''SI'', ''NO'')', 4, 1, 1, 0, 0, 0, 1); + +INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) +SELECT 1, `id`, `name` FROM `zz_views` WHERE `id_module` = (SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'); +INSERT INTO `zz_views_lang` (`id_lang`, `id_record`, `title`) +SELECT 2, `id`, CASE `name` WHEN 'Descrizione' THEN 'Description' WHEN 'Ordine' THEN 'Order' WHEN 'Attiva' THEN 'Enabled' ELSE `name` END +FROM `zz_views` WHERE `id_module` = (SELECT `id` FROM `zz_modules` WHERE `name` = 'Tipologie note spese'); + +INSERT INTO `zz_prints` (`id_module`, `is_record`, `name`, `directory`, `previous`, `options`, `icon`, `version`, `compatibility`, `order`, `predefined`, `enabled`) VALUES +((SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 0, 'Nota spese', 'note_spese', '', '', 'fa fa-print', '2.12', '2.12', 0, 0, 1); +INSERT INTO `zz_prints_lang` (`id_lang`, `id_record`, `title`, `filename`) VALUES +(1, (SELECT `id` FROM `zz_prints` WHERE `name` = 'Nota spese'), 'Nota spese', 'Nota spese'), +(2, (SELECT `id` FROM `zz_prints` WHERE `name` = 'Nota spese'), 'Expense notes', 'Expense notes'); + +INSERT INTO `zz_widgets` (`name`, `type`, `id_module`, `location`, `class`, `query`, `bgcolor`, `icon`, `print_link`, `more_link`, `more_link_type`, `php_include`, `enabled`, `order`, `help`) VALUES +('Note spese - confermate', 'custom', (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'controller_top', 'col-md-4', '', 'success', 'fa fa-check-circle', '', '', 'link', 'modules/note_spese/widgets/indicatori.php', 1, 1, 'Totale e numero delle spese confermate nel periodo selezionato.'), +('Note spese - da verificare', 'custom', (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'controller_top', 'col-md-4', '', 'warning', 'fa fa-exclamation-triangle', '', '', 'link', 'modules/note_spese/widgets/indicatori.php', 1, 2, 'Importo e numero delle note spese che richiedono verifica.'), +('Note spese - senza allegati', 'custom', (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese'), 'controller_top', 'col-md-4', '', 'info', 'fa fa-paperclip', '', '', 'link', 'modules/note_spese/widgets/indicatori.php', 1, 3, 'Spese confermate del periodo che non hanno ancora allegati.'); + +INSERT INTO `zz_widgets_lang` (`id_lang`, `id_record`, `title`, `text`) VALUES +(1, (SELECT `id` FROM `zz_widgets` WHERE `name` = 'Note spese - confermate'), 'Spese confermate', 'Spese confermate'), +(2, (SELECT `id` FROM `zz_widgets` WHERE `name` = 'Note spese - confermate'), 'Confirmed expenses', 'Confirmed expenses'), +(1, (SELECT `id` FROM `zz_widgets` WHERE `name` = 'Note spese - da verificare'), 'Da verificare', 'Da verificare'), +(2, (SELECT `id` FROM `zz_widgets` WHERE `name` = 'Note spese - da verificare'), 'To review', 'To review'), +(1, (SELECT `id` FROM `zz_widgets` WHERE `name` = 'Note spese - senza allegati'), 'Senza allegati', 'Senza allegati'), +(2, (SELECT `id` FROM `zz_widgets` WHERE `name` = 'Note spese - senza allegati'), 'Without attachments', 'Without attachments'); + +INSERT INTO `zz_hooks` (`name`, `class`, `enabled`, `id_module`) VALUES +('Note spese da importare', 'Modules\\NoteSpese\\PendingExpensesHook', 1, (SELECT `id` FROM `zz_modules` WHERE `name` = 'Note spese')); +INSERT INTO `zz_hooks_lang` (`id_lang`, `id_record`, `title`) VALUES +(1, (SELECT `id` FROM `zz_hooks` WHERE `name` = 'Note spese da importare'), 'Note spese da importare'), +(2, (SELECT `id` FROM `zz_hooks` WHERE `name` = 'Note spese da importare'), 'Expense notes to import'); + +INSERT INTO `zz_permissions` (`id_gruppo`, `id_module`, `permessi`) +SELECT g.`id`, m.`id`, 'rw' +FROM `zz_groups` g +CROSS JOIN `zz_modules` m +WHERE g.`nome` = 'Amministratori' AND m.`name` IN ('Note spese', 'Tipologie note spese'); + +INSERT INTO `zz_group_view` (`id_gruppo`, `id_vista`) +SELECT g.`id`, v.`id` +FROM `zz_groups` g +INNER JOIN `zz_views` v ON v.`id_module` IN (SELECT `id` FROM `zz_modules` WHERE `name` IN ('Note spese', 'Tipologie note spese')); diff --git a/update/tables.php b/update/tables.php index ad542e845..eeb2d5a40 100755 --- a/update/tables.php +++ b/update/tables.php @@ -43,6 +43,11 @@ 'co_mandati_sepa', 'co_movimenti', 'co_movimenti_modelli', + 'co_note_spese', + 'co_note_spese_stati', + 'co_note_spese_stati_lang', + 'co_note_spese_tipologie', + 'co_note_spese_tipologie_lang', 'co_pagamenti', 'co_pagamenti_lang', 'co_piano_dei_conti1', From f66d2022c8e1bb0b5d392d711d7def3c1fa07ef6 Mon Sep 17 00:00:00 2001 From: Sajo <75281007+sajotrei@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:02:27 +0200 Subject: [PATCH 2/4] Aggiunge intestazione GPL e autore a Note spese --- modules/note_spese/init.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/modules/note_spese/init.php b/modules/note_spese/init.php index 1807aa41b..e86d2a9d5 100644 --- a/modules/note_spese/init.php +++ b/modules/note_spese/init.php @@ -1,5 +1,30 @@ . + */ + +/** + * Modulo Note spese. + * + * @author sajotrei + * @link https://github.com/sajotrei + */ + include_once __DIR__.'/../../core.php'; if (!empty($id_record)) { From 0c8a2cbf457a148890c1d5652c4cbcedbd84ab92 Mon Sep 17 00:00:00 2001 From: Sajo <75281007+sajotrei@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:03:23 +0200 Subject: [PATCH 3/4] Aggiunge intestazione GPL e autore alle tipologie Note spese --- modules/tipologie_note_spese/init.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/modules/tipologie_note_spese/init.php b/modules/tipologie_note_spese/init.php index 5e8b2c91d..50f4b2887 100644 --- a/modules/tipologie_note_spese/init.php +++ b/modules/tipologie_note_spese/init.php @@ -1,5 +1,30 @@ . + */ + +/** + * Tipologie del modulo Note spese. + * + * @author sajotrei + * @link https://github.com/sajotrei + */ + include_once __DIR__.'/../../core.php'; if (!empty($id_record)) { From 89433a3a7d24455f2307f86e0626b114775bcf61 Mon Sep 17 00:00:00 2001 From: Sajo <75281007+sajotrei@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:03:41 +0200 Subject: [PATCH 4/4] Aggiunge intestazione GPL e autore alla stampa Note spese --- templates/note_spese/init.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/templates/note_spese/init.php b/templates/note_spese/init.php index eea4d2736..07e5c6e43 100644 --- a/templates/note_spese/init.php +++ b/templates/note_spese/init.php @@ -1,5 +1,30 @@ . + */ + +/** + * Stampa riepilogativa del modulo Note spese. + * + * @author sajotrei + * @link https://github.com/sajotrei + */ + include_once __DIR__.'/../../core.php'; // La stampa e' di periodo e non e' legata a una singola anagrafica/documento.