Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 488 additions & 0 deletions modules/note_spese/actions.php

Large diffs are not rendered by default.

149 changes: 149 additions & 0 deletions modules/note_spese/add.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

include_once __DIR__.'/../../core.php';

$period_start = $_SESSION['period_start'] ?? date('Y-01-01');
$period_end = $_SESSION['period_end'] ?? date('Y-12-31');
$today = date('Y-m-d');
$default_date = noteSpeseIsDateInPeriod($today, $period_start, $period_end) ? $today : $period_end;
$lang = (int) Models\Locale::getDefault()->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'] ?? '') : '';
$default_notes = $is_duplicate ? (string) ($duplicate_source['note'] ?? '') : '';

$current_user = auth_osm()->getUser();
$current_operator = (int) ($current_user['id_anagrafica'] ?? 0);
$source_operator = $is_duplicate ? (int) ($duplicate_source['id_operatore'] ?? 0) : 0;
$operator_candidate = $source_operator > 0 ? $source_operator : $current_operator;
$default_operator = noteSpeseOperatorExists($dbo, $operator_candidate) ? ($operator_candidate ?: null) : null;

$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);
?>

<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#ns-manuale">
<i class="fa fa-plus mr-1"></i><?php echo $is_duplicate ? tr('Duplica spesa') : tr('Nuova spesa'); ?>
</a>
</li>
<?php if (!$is_duplicate) { ?>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#ns-excel">
<i class="fa fa-table mr-1"></i><?php echo tr('Incolla dati'); ?>
</a>
</li>
<?php } ?>
</ul>

<div class="tab-content pt-3">
<div class="tab-pane fade show active" id="ns-manuale">
<?php if (!$is_duplicate) { ?>
<div class="alert alert-light border py-2 mb-3">
<i class="fa fa-user mr-1"></i>
<?php echo tr('Le Note spese registrano costi anticipati personalmente da un Operatore. Le uscite aziendali senza fattura seguono flussi distinti e non vengono importate automaticamente da Scadenzario o Automezzi.'); ?>
</div>
<?php } ?>

<form action="" method="post" id="add-form">
<input type="hidden" name="op" value="add">
<input type="hidden" name="backto" value="record-edit">

<?php if ($is_duplicate) { ?>
<div class="alert alert-info py-2">
<i class="fa fa-copy mr-1"></i><?php echo tr('Copia della spesa #_ID_: modifica i dati prima di salvarla. Gli allegati e l’origine non vengono copiati. La copia entrerà come Da verificare.', ['_ID_' => $duplicate_id]); ?>
</div>
<?php } ?>

<div class="row">
<div class="col-md-3">
{[ "type": "date", "label": "<?php echo tr('Data'); ?>", "name": "data", "required": 1, "value": "<?php echo prepareToField($default_date); ?>" ]}
</div>
<div class="col-md-3">
{[ "type": "select", "label": "<?php echo tr('Tipologia'); ?>", "name": "id_tipologia", "required": 1, "value": "<?php echo (int) $default_category; ?>", "values": "query=<?php echo $category_query; ?>" ]}
</div>
<div class="col-md-3">
{[ "type": "select", "label": "<?php echo tr('Operatore'); ?>", "name": "id_operatore", "required": 1, "value": "<?php echo (int) $default_operator; ?>", "values": "query=<?php echo $operator_query; ?>", "help": "<?php echo tr('Persona che ha anticipato personalmente la spesa.'); ?>" ]}
</div>
<div class="col-md-3">
{[ "type": "number", "label": "<?php echo tr('Importo'); ?>", "name": "importo", "required": 1, "value": "<?php echo $default_amount !== null ? prepareToField(number_format($default_amount, 2, '.', '')) : ''; ?>", "icon-after": "€", "min-value": "0.01", "decimals": 2 ]}
</div>
</div>

<div class="row">
<div class="col-md-8">
{[ "type": "text", "label": "<?php echo tr('Descrizione'); ?>", "name": "descrizione", "required": 1, "value": "<?php echo prepareToField($default_description); ?>" ]}
</div>
<div class="col-md-4">
{[ "type": "text", "label": "<?php echo tr('Controparte'); ?>", "name": "controparte", "value": "<?php echo prepareToField($default_counterparty); ?>", "help": "<?php echo tr('Esempio: Telepass, Q8, Comune, Ristorante.'); ?>" ]}
</div>
</div>

<div class="row">
<div class="col-md-12">
{[ "type": "textarea", "label": "<?php echo tr('Note'); ?>", "name": "note", "value": "<?php echo prepareToField($default_notes); ?>" ]}
</div>
</div>

<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="fa fa-check-square-o mr-1"></i><?php echo tr('La nuova registrazione entrerà come Da verificare e dovrà essere confermata esplicitamente.'); ?>
</small>
<button type="submit" class="btn btn-primary">
<i class="fa <?php echo $is_duplicate ? 'fa-copy' : 'fa-plus'; ?>"></i>
<?php echo $is_duplicate ? tr('Crea copia') : tr('Aggiungi spesa'); ?>
</button>
</div>
</form>
</div>

<?php if (!$is_duplicate) { ?>
<div class="tab-pane fade" id="ns-excel">
<div class="alert alert-light border py-2 mb-3">
<strong><?php echo tr('Formati supportati'); ?></strong><br>
<span class="d-block"><code><?php echo tr('Data | Descrizione | Importo'); ?></code> — <?php echo tr('la tipologia viene proposta automaticamente.'); ?></span>
<span class="d-block"><code><?php echo tr('Data | Tipologia | Descrizione | Importo | Controparte | Note'); ?></code></span>
<small class="text-muted"><?php echo tr('Sono accettati dati separati da TAB o punto e virgola. Tutte le righe importate entrano come Da verificare.'); ?></small>
</div>

<form action="" method="post">
<input type="hidden" name="op" value="import_excel">
<input type="hidden" name="backto" value="record-list">

<div class="row">
<div class="col-md-5">
{[ "type": "select", "label": "<?php echo tr('Operatore'); ?>", "name": "id_operatore_excel", "required": 1, "value": "<?php echo (int) $default_operator; ?>", "values": "query=<?php echo $operator_query; ?>", "help": "<?php echo tr('L’Operatore selezionato viene associato a tutte le righe incollate.'); ?>" ]}
</div>
</div>

<div class="form-group">
<textarea class="form-control" name="righe_excel" rows="9" placeholder="07/08/2026&#9;Parcheggio cliente ABC&#9;8,00&#10;08/08/2026&#9;Vitto&#9;Pranzo trasferta&#9;18,50&#9;Ristorante XYZ&#9;Ricevuta"></textarea>
</div>

<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="fa fa-info-circle mr-1"></i><?php echo tr('Le righe riconducibili a costi aziendali estranei alle Note spese vengono ignorate; gli importi negativi vengono convertiti in positivo.'); ?>
</small>
<button type="submit" class="btn btn-secondary"><i class="fa fa-paste"></i> <?php echo tr('Importa righe'); ?></button>
</div>
</form>
</div>
<?php } ?>
</div>
211 changes: 211 additions & 0 deletions modules/note_spese/bulk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php

include_once __DIR__.'/../../core.php';

$ids = array_values(array_unique(array_filter(array_map('intval', (array) $id_records))));

switch (post('op')) {
case 'confirm_bulk':
Permissions::check('rw');
if (empty($ids)) {
break;
}

$id_stato = noteSpeseGetStatusId($dbo, 'confermato');
if (empty($id_stato)) {
flash()->error(tr('Stato di conferma non disponibile.'));
break;
}

// Una Nota spesa 1.9.0 è confermabile solo se identifica l'Operatore
// che ha anticipato personalmente il costo.
$missing_operator_rows = $dbo->fetchArray(
'SELECT `id` FROM `co_note_spese` WHERE `id` IN ('.implode(',', $ids).') '
.'AND COALESCE(`id_operatore`, 0) = 0'
);
$missing_operator_ids = array_map('intval', array_column($missing_operator_rows, 'id'));

$inactive_category_rows = $dbo->fetchArray(
'SELECT n.`id` FROM `co_note_spese` n '
.'LEFT JOIN `co_note_spese_tipologie` t ON t.`id` = n.`id_tipologia` '
.'WHERE n.`id` IN ('.implode(',', $ids).') AND COALESCE(t.`enabled`, 0) != 1'
);
$inactive_category_ids = array_map('intval', array_column($inactive_category_rows, 'id'));

// I duplicati esatti restano da verificare: per confermarli è necessario
// aprire la singola spesa e verificarne la correttezza.
$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, $missing_operator_ids, $inactive_category_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)]));
}
if (!empty($missing_operator_ids)) {
flash()->warning(tr('_COUNT_ spese senza Operatore non sono state confermate: associare prima la persona che ha anticipato il costo.', ['_COUNT_' => count($missing_operator_ids)]));
}
if (!empty($inactive_category_ids)) {
flash()->warning(tr('_COUNT_ spese con Tipologia non più attiva non sono state confermate: riclassificarle prima della conferma.', ['_COUNT_' => count($inactive_category_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 n.`id`, n.`data`, n.`id_tipologia`, n.`descrizione`, n.`importo`, n.`id_anagrafica`, n.`id_operatore`, n.`controparte`, n.`note`, t.`enabled` AS `categoria_attiva` '
.'FROM `co_note_spese` n LEFT JOIN `co_note_spese_tipologie` t ON t.`id` = n.`id_tipologia` '
.'WHERE n.`id` IN ('.implode(',', $ids).') ORDER BY n.`id` ASC'
);

$duplicated = 0;
$skipped_without_operator = 0;
$skipped_inactive_category = 0;
foreach ($rows as $row) {
if (empty($row['id_operatore'])) {
++$skipped_without_operator;
continue;
}
if (empty($row['categoria_attiva'])) {
++$skipped_inactive_category;
continue;
}

$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' => (int) $row['id_operatore'],
'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]));
}
if ($skipped_without_operator > 0) {
flash()->warning(tr('_COUNT_ registrazioni storiche senza Operatore non sono state duplicate.', ['_COUNT_' => $skipped_without_operator]));
}
if ($skipped_inactive_category > 0) {
flash()->warning(tr('_COUNT_ registrazioni con Tipologia non più attiva non sono state duplicate.', ['_COUNT_' => $skipped_inactive_category]));
}
if ($duplicated === 0 && $skipped_without_operator === 0 && $skipped_inactive_category === 0) {
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('Saranno confermate solo le Note spese con Operatore, Tipologia attiva e senza duplicazioni rilevate.'),
'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 Da verificare per ogni Nota spesa valida. Le registrazioni senza Operatore o con Tipologia non attiva vengono ignorate.'),
'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',
],
],
];
16 changes: 16 additions & 0 deletions modules/note_spese/buttons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

include_once __DIR__.'/../../core.php';

if (($structure->permission ?? null) === 'rw' && !empty($id_record)) {
$duplicate_url = base_path_osm().'/add.php?id_module='.(int) $id_module.'&duplicate_id='.(int) $id_record;
?>
<button type="button"
class="btn btn-primary"
data-widget="modal"
data-title="<?php echo prepareToField(tr('Duplica nota spesa')); ?>"
data-href="<?php echo prepareToField($duplicate_url); ?>">
<i class="fa fa-copy"></i> <?php echo tr('Duplica'); ?>
</button>
<?php
}
Loading