Allineamento sviluppo: risoluzione bug mdb-sql e o.num_fat, allineamento importazione voci/operazioni, integrazione tab pagamenti CBILL e migrazioni rate/operazioni

This commit is contained in:
michele 2026-07-07 16:15:57 +02:00
parent 248bdd032c
commit ab82c315fe
34 changed files with 2071 additions and 401 deletions

View File

@ -75,7 +75,30 @@ public function handle(): int
if ($year) { if ($year) {
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'legacy_year')) { if (Schema::connection('gescon_import')->hasColumn('operazioni', 'legacy_year')) {
$query->where('legacy_year', (string) $year); if (is_numeric($year) && strlen((string)$year) === 4 && (int)$year >= 1900) {
$cartelle = DB::connection('gescon_import')
->table('gestioni_annuali')
->whereIn('cod_stabile', $legacyStableCodes)
->get(['cartella', 'anno_ordinario', 'anno_riscaldamento', 'descrizione'])
->filter(function ($row) use ($year): bool {
$annoOrd = $this->extractLegacyYearInt((string) ($row->anno_ordinario ?? ''));
$annoRisc = $this->extractLegacyYearInt((string) ($row->anno_riscaldamento ?? ''));
$annoDesc = $this->extractLegacyYearInt((string) ($row->descrizione ?? ''));
return $annoOrd === (int) $year || $annoRisc === (int) $year || $annoDesc === (int) $year;
})
->pluck('cartella')
->filter()
->unique()
->toArray();
if ($cartelle !== []) {
$query->whereIn('legacy_year', $cartelle);
} else {
$query->where('legacy_year', (string) $year);
}
} else {
$query->where('legacy_year', (string) $year);
}
} else { } else {
$query->whereYear('dt_spe', $year); $query->whereYear('dt_spe', $year);
} }

View File

@ -1014,6 +1014,12 @@ private function stepUnita(?int $limit): int
if (Schema::hasColumn('unita_immobiliari', 'deleted_at') && ! empty($exists->deleted_at)) { if (Schema::hasColumn('unita_immobiliari', 'deleted_at') && ! empty($exists->deleted_at)) {
$patch['deleted_at'] = null; $patch['deleted_at'] = null;
} }
if (Schema::hasColumn('unita_immobiliari', 'note')) {
$uNote = trim((string) ($r->note ?? $r->note_cond ?? ''));
if ($uNote !== '' && (string) ($exists->note ?? '') !== $uNote) {
$patch['note'] = $uNote;
}
}
if (($unitClassification['is_condominiale'] ?? false) && Schema::hasColumn('unita_immobiliari', 'denominazione')) { if (($unitClassification['is_condominiale'] ?? false) && Schema::hasColumn('unita_immobiliari', 'denominazione')) {
$condoName = $this->firstNonEmptyStr([ $condoName = $this->firstNonEmptyStr([
$unitClassification['denominazione'] ?? null, $unitClassification['denominazione'] ?? null,
@ -1078,6 +1084,7 @@ private function stepUnita(?int $limit): int
'stato_occupazione' => 'occupata_proprietario', 'stato_occupazione' => 'occupata_proprietario',
'attiva' => true, 'attiva' => true,
'unita_demo' => false, 'unita_demo' => false,
'note' => Schema::hasColumn('unita_immobiliari', 'note') ? (trim((string) ($r->note ?? $r->note_cond ?? '')) ?: null) : null,
'created_at' => now(), 'created_at' => now(),
'updated_at' => now(), 'updated_at' => now(),
]; ];
@ -1163,7 +1170,7 @@ private function stepUnita(?int $limit): int
} }
// Cleanup: rimuovi unità non presenti in condomin (evita "alieni") // Cleanup: rimuovi unità non presenti in condomin (evita "alieni")
if (! $this->isDryRun && $this->option('stabile') && ! $this->option('update-only')) { if (false && ! $this->isDryRun && $this->option('stabile') && ! $this->option('update-only')) {
$condRows = $this->currentSnapshotCondominQuery() $condRows = $this->currentSnapshotCondominQuery()
->select('scala', 'interno', 'cod_cond') ->select('scala', 'interno', 'cod_cond')
->get(); ->get();
@ -2665,12 +2672,13 @@ private function stepVoci(?int $limit): int
} }
$exists = $q->orderBy('id')->first(); $exists = $q->orderBy('id')->first();
if (! $exists) { if (! $exists && $gestioneId === null) {
$exists = DB::table('voci_spesa') $exists = DB::table('voci_spesa')
->where('codice', $codiceDominio) ->where('codice', $codiceDominio)
->when($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id'), function ($qq) use ($stabileId) { ->when($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id'), function ($qq) use ($stabileId) {
$qq->where('stabile_id', $stabileId); $qq->where('stabile_id', $stabileId);
}) })
->whereNull('gestione_contabile_id')
->orderBy('id') ->orderBy('id')
->first(); ->first();
} }
@ -3270,12 +3278,18 @@ private function stepOperazioni(?int $limit): int
$count++; $count++;
continue; continue;
} }
$exists = DB::table('operazioni_contabili')->where('legacy_id', $legacy)->first(); $gestioneId = $this->resolveGestioneId($o->legacy_year ?? ($o->anno ?? null), $o->compet ?? null, $o->gestione ?? null, $o->n_stra ?? null);
if ($exists && ! $this->option('update-only')) { if (! $gestioneId) {
continue; continue;
} }
$gestioneId = $this->resolveGestioneId($o->legacy_year ?? ($o->anno ?? null), $o->compet ?? null, $o->gestione ?? null, $o->n_stra ?? null); $exists = DB::table('operazioni_contabili')
->where('legacy_id', $legacy)
->where('gestione_id', $gestioneId)
->first();
if ($exists && ! $this->option('update-only')) {
continue;
}
$protoNum = (isset($o->n_spe) && is_numeric($o->n_spe) && (int) $o->n_spe > 0) ? (int) $o->n_spe : $this->nextProtocolNumber(); $protoNum = (isset($o->n_spe) && is_numeric($o->n_spe) && (int) $o->n_spe > 0) ? (int) $o->n_spe : $this->nextProtocolNumber();
$prefix = null; $prefix = null;
if ($gestioneId) { if ($gestioneId) {
@ -4224,25 +4238,19 @@ private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $n
->where('numero_emissione', $numeroEmissione) ->where('numero_emissione', $numeroEmissione)
->sum(DB::raw('COALESCE(importo_dovuto_euro, importo_dovuto, 0)')); ->sum(DB::raw('COALESCE(importo_dovuto_euro, importo_dovuto, 0)'));
} }
$payload = [ $payload = [
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'unita_immobiliare_id' => null, 'descrizione' => $descr,
'descrizione' => $descr, 'importo_totale' => $totale,
'importo_totale' => $totale, 'numero_rate' => 1,
'numero_rate' => 1, 'data_prima_rata' => $dataInizio,
'importo_rata' => $totale, 'frequenza' => 'MENSILE',
'data_inizio' => $dataInizio, 'stato' => 'ATTIVO',
'data_fine' => $dataFine, 'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile,
'frequenza' => 'MENSILE', 'codice_piano' => \App\Models\PianoRateizzazione::generaCodicePiano(),
'stato' => 'ATTIVO', 'created_at' => now(),
'importo_pagato' => 0, 'updated_at' => now(),
'importo_residuo' => $totale,
'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile,
'created_at' => now(),
'updated_at' => now(),
]; ];
if (! $this->isDryRun) { if (! $this->isDryRun) {
$id = DB::table('piano_rateizzazione')->insertGetId($payload); $id = DB::table('piano_rateizzazione')->insertGetId($payload);
return is_numeric($id) ? (int) $id : null; return is_numeric($id) ? (int) $id : null;
@ -5272,20 +5280,18 @@ private function importLegacySubsystemForAssemblea(int $assembleaId, array $row,
if (!$unita) continue; if (!$unita) continue;
// Trova il soggetto principale legato all'unità // Trova il soggetto principale legato all'unità
$soggettoId = DB::table('diritti_reali_unita') $soggettoId = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id) ->where('unita_immobiliare_id', $unita->id)
->orderByRaw("CASE WHEN tipo_diritto = 'proprieta' THEN 1 ELSE 2 END")
->value('soggetto_id'); ->value('soggetto_id');
if (!$soggettoId) {
$soggettoId = DB::table('soggetti_unita_immobiliari')
->where('unita_immobiliare_id', $unita->id)
->value('soggetto_id');
}
if (!$soggettoId) continue; if (!$soggettoId) continue;
$tipoPartecipazione = 'personale'; $tipoPartecipazione = 'personale';
$pda = strtolower(trim((string)($pr['p_d_a'] ?? ''))); $pda = strtolower(trim((string)($pr['p_d_a'] ?? '')));
if ($pda === 'a' || $pda === 'assente') {
continue;
}
if ($pda === 'd' || strpos($pda, 'delega') !== false) { if ($pda === 'd' || strpos($pda, 'delega') !== false) {
$tipoPartecipazione = 'delega'; $tipoPartecipazione = 'delega';
} }
@ -5351,16 +5357,11 @@ private function importLegacySubsystemForAssemblea(int $assembleaId, array $row,
if (!$unita) continue; if (!$unita) continue;
$soggettoId = DB::table('diritti_reali_unita') $soggettoId = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id) ->where('unita_immobiliare_id', $unita->id)
->orderByRaw("CASE WHEN tipo_diritto = 'proprieta' THEN 1 ELSE 2 END")
->value('soggetto_id'); ->value('soggetto_id');
if (!$soggettoId) {
$soggettoId = DB::table('soggetti_unita_immobiliari')
->where('unita_immobiliare_id', $unita->id)
->value('soggetto_id');
}
if (!$soggettoId) continue; if (!$soggettoId) continue;
// Determina scelta voto // Determina scelta voto
@ -7253,7 +7254,7 @@ private function extractLegacySnapshotPartyMeta(object $row, string $role): arra
'citta' => trim((string) ($isTenant ? ($row->inquil_citta ?? '') : ($row->citta ?? ''))) ?: null, 'citta' => trim((string) ($isTenant ? ($row->inquil_citta ?? '') : ($row->citta ?? ''))) ?: null,
'provincia' => trim((string) ($isTenant ? ($row->inquil_pr ?? '') : ($row->pr ?? ''))) ?: null, 'provincia' => trim((string) ($isTenant ? ($row->inquil_pr ?? '') : ($row->pr ?? ''))) ?: null,
'titolo' => trim((string) ($isTenant ? ($row->titolo_inq ?? '') : ($row->titolo_cond ?? ''))) ?: null, 'titolo' => trim((string) ($isTenant ? ($row->titolo_inq ?? '') : ($row->titolo_cond ?? ''))) ?: null,
'note' => trim((string) ($isTenant ? ($row->inquil_note ?? '') : ($row->note_cond ?? ''))) ?: null, 'note' => trim((string) ($isTenant ? ($row->note_inquilino ?? $row->inquil_note ?? '') : ($row->note ?? $row->note_cond ?? ''))) ?: null,
'e_lostesso_di' => $isTenant ? null : (trim((string) ($row->e_lostesso_di ?? '')) ?: null), 'e_lostesso_di' => $isTenant ? null : (trim((string) ($row->e_lostesso_di ?? '')) ?: null),
'codice_fiscale' => $isTenant 'codice_fiscale' => $isTenant
? (trim((string) ($row->inquilin_cod_fisc ?? $row->inquil_cod_fisc ?? '')) ?: null) ? (trim((string) ($row->inquilin_cod_fisc ?? $row->inquil_cod_fisc ?? '')) ?: null)
@ -7319,68 +7320,118 @@ private function extractLegacyPrefixedPayload(object $row, array $prefixes): arr
return $payload; return $payload;
} }
private function resolveLinkedLegacyCondIds(string $codStabile, string $startCondId): array
{
$linked = [$startCondId];
$toProcess = [$startCondId];
$visited = [];
while (! empty($toProcess)) {
$current = array_shift($toProcess);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$rows = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where(function($q) use ($current) {
$q->where('cod_cond', $current);
$colName = Schema::connection('gescon_import')->hasColumn('condomin', 'id_cond') ? 'id_cond' : null;
if ($colName) {
$q->orWhere($colName, $current);
}
if (Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_id_cond')) {
$q->orWhere('legacy_id_cond', $current);
}
})
->get();
foreach ($rows as $row) {
$cc = trim((string) ($row->cod_cond ?? ''));
if ($cc !== '' && ! in_array($cc, $linked, true)) {
$linked[] = $cc;
$toProcess[] = $cc;
}
foreach (['subentro_prima_cera', 'subentro_adesso_ce'] as $field) {
if (isset($row->$field)) {
$val = trim((string) $row->$field);
if ($val !== '' && ! in_array($val, $linked, true)) {
$linked[] = $val;
$toProcess[] = $val;
}
}
}
}
}
return $linked;
}
private function loadLegacyCondominHistoryRows(object $row, ?string $legacyCondId): array private function loadLegacyCondominHistoryRows(object $row, ?string $legacyCondId): array
{ {
$codStabile = trim((string) ($row->cod_stabile ?? '')); $codStabile = trim((string) ($row->cod_stabile ?? ''));
$codCond = trim((string) ($row->cod_cond ?? '')); $codCond = trim((string) ($row->cod_cond ?? ''));
$stableCondId = $this->extractStableLegacyCondId($row, $legacyCondId); $stableCondId = $this->extractStableLegacyCondId($row, $legacyCondId);
if ($codStabile === '' || ($stableCondId === null && $codCond === '')) { if ($codStabile === '') {
return [$row]; return [$row];
} }
$startCondId = $stableCondId !== null && $stableCondId !== '' ? $stableCondId : ($codCond !== '' ? $codCond : null);
$linkedCondIds = [];
if ($startCondId !== null) {
$linkedCondIds = $this->resolveLinkedLegacyCondIds($codStabile, $startCondId);
}
$query = DB::connection('gescon_import') $query = DB::connection('gescon_import')
->table('condomin') ->table('condomin')
->where('cod_stabile', $codStabile) ->where('cod_stabile', $codStabile);
->orderByDesc('legacy_year')
$query->where(function($q) use ($linkedCondIds, $row, $codCond): void {
if (! empty($linkedCondIds)) {
$q->whereIn('cod_cond', $linkedCondIds);
$colName = Schema::connection('gescon_import')->hasColumn('condomin', 'id_cond') ? 'id_cond' : null;
if ($colName) {
$q->orWhereIn($colName, $linkedCondIds);
}
if (Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_id_cond')) {
$q->orWhereIn('legacy_id_cond', $linkedCondIds);
}
} elseif ($codCond !== '') {
$q->where('cod_cond', $codCond);
}
// Corrispondenza fisica (scala, interno, subalterno)
$scala = trim((string) ($row->scala ?? ''));
$interno = trim((string) ($row->interno ?? $row->int ?? ''));
$sub = trim((string) ($row->subalterno ?? $row->sub ?? ''));
if ($scala !== '' && ($interno !== '' || $sub !== '')) {
$q->orWhere(function($subQ) use ($scala, $interno, $sub): void {
$subQ->where('scala', $scala);
if ($interno !== '') {
$subQ->where(function($sub2) use ($interno): void {
$sub2->where('interno', $interno);
if (Schema::connection('gescon_import')->hasColumn('condomin', 'int')) {
$sub2->orWhere('int', $interno);
}
});
}
if ($sub !== '' && Schema::connection('gescon_import')->hasColumn('condomin', 'subalterno')) {
$subQ->where('subalterno', $sub);
}
});
}
});
$query->orderByDesc('legacy_year')
->orderByDesc('id'); ->orderByDesc('id');
if ($stableCondId !== null && $stableCondId !== '') {
$colName = Schema::connection('gescon_import')->hasColumn('condomin', 'cod_cond') ? 'cod_cond' : 'id_cond';
if (Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_id_cond')) {
$query->where(function ($subQuery) use ($stableCondId, $colName): void {
$subQuery->where($colName, $stableCondId)
->orWhere('legacy_id_cond', $stableCondId);
});
} else {
$query->where($colName, $stableCondId);
}
} elseif ($codCond !== '') {
$query->where('cod_cond', $codCond);
}
$historyRows = $query->get()->all(); $historyRows = $query->get()->all();
if ($historyRows === [] && $codCond !== '') {
$historyRows = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where('cod_cond', $codCond)
->orderByDesc('legacy_year')
->orderByDesc('id')
->get()
->all();
}
if ($historyRows === []) {
$partyName = $this->extractLegacyPartyName($row);
if ($partyName !== null) {
$historyRows = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where(function ($subQuery) use ($partyName): void {
$subQuery->where('nom_cond', $partyName);
if (Schema::connection('gescon_import')->hasColumn('condomin', 'proprietario_denominazione')) {
$subQuery->orWhere('proprietario_denominazione', $partyName);
}
})
->orderByDesc('legacy_year')
->orderByDesc('id')
->get()
->all();
}
}
if ($historyRows === []) { if ($historyRows === []) {
return [$row]; return [$row];
} }
@ -7486,22 +7537,31 @@ private function loadLegacyComproprietariHistoryRows(object $row, ?string $legac
} }
$codStabile = trim((string) ($row->cod_stabile ?? '')); $codStabile = trim((string) ($row->cod_stabile ?? ''));
$idCondRaw = trim((string) ($this->extractStableLegacyCondId($row, $legacyCondId) ?? $row->cod_cond ?? '')); $codCond = trim((string) ($row->cod_cond ?? ''));
$stableCondId = $this->extractStableLegacyCondId($row, $legacyCondId);
if ($codStabile === '' || $idCondRaw === '') { if ($codStabile === '') {
return []; return [];
} }
$startCondId = $stableCondId !== null && $stableCondId !== '' ? $stableCondId : ($codCond !== '' ? $codCond : null);
$linkedCondIds = [];
if ($startCondId !== null) {
$linkedCondIds = $this->resolveLinkedLegacyCondIds($codStabile, $startCondId);
}
$query = DB::connection('gescon_import') $query = DB::connection('gescon_import')
->table('comproprietari') ->table('comproprietari')
->where('cod_stabile', $codStabile) ->where('cod_stabile', $codStabile)
->orderByDesc('legacy_year') ->orderByDesc('legacy_year')
->orderByDesc('id'); ->orderByDesc('id');
if (is_numeric($idCondRaw)) { if (! empty($linkedCondIds)) {
$query->where('id_cond', (int) $idCondRaw); $query->whereIn('id_cond', $linkedCondIds);
} elseif ($startCondId !== null) {
$query->where('id_cond', $startCondId);
} else { } else {
$query->where('id_cond', $idCondRaw); return [];
} }
$historyRows = $query->get()->all(); $historyRows = $query->get()->all();

View File

@ -46,7 +46,7 @@ public function handle()
$this->displayImportPlan($yearsToImport); $this->displayImportPlan($yearsToImport);
if (!$this->option('dry-run') && !$this->confirm('Proceed with multi-year import?')) { if (!$this->option('dry-run') && !$this->option('no-interaction') && !$this->confirm('Proceed with multi-year import?')) {
$this->info('Import cancelled by user.'); $this->info('Import cancelled by user.');
return Command::SUCCESS; return Command::SUCCESS;
} }
@ -102,7 +102,7 @@ private function scanAvailableYears(string $gesconDir): array
} }
} }
ksort($years); krsort($years);
return $years; return $years;
} }
@ -228,9 +228,9 @@ private function countTableRecords(string $mdbPath, string $tableName): int
} }
$output = shell_exec(sprintf( $output = shell_exec(sprintf(
'mdb-sql %s <<< "SELECT COUNT(*) FROM %s" 2>/dev/null', 'echo "SELECT COUNT(*) FROM %s" | mdb-sql %s 2>/dev/null',
escapeshellarg($mdbPath), $tableName,
escapeshellarg($tableName) escapeshellarg($mdbPath)
)); ));
if ($output && preg_match('/(\d+)/', $output, $matches)) { if ($output && preg_match('/(\d+)/', $output, $matches)) {
@ -247,7 +247,14 @@ private function executeImport(string $tenantId, string $gesconDir, array $years
DB::beginTransaction(); DB::beginTransaction();
try { try {
$service = new MultiYearGesconImportService($tenantId); $stabileCode = basename(rtrim($gesconDir, '/'));
$stabileRow = DB::table('stabili')
->where('codice_stabile', $stabileCode)
->orWhere('codice_stabile', ltrim($stabileCode, '0'))
->first();
$stabileId = $stabileRow?->id;
$service = new MultiYearGesconImportService($tenantId, $stabileId, $gesconDir);
// Delete existing data if force // Delete existing data if force
if ($this->option('force')) { if ($this->option('force')) {
@ -289,9 +296,66 @@ private function cleanExistingData(string $tenantId, array $years): void
{ {
$this->warn("🗑️ Cleaning existing data for years: " . implode(', ', $years)); $this->warn("🗑️ Cleaning existing data for years: " . implode(', ', $years));
$stabileCode = basename(rtrim($this->argument('gescon-dir'), '/'));
$stabileRow = DB::table('stabili')
->where('codice_stabile', $stabileCode)
->orWhere('codice_stabile', ltrim($stabileCode, '0'))
->first();
$stabileId = $stabileRow?->id;
// Resolve calendar years
$calendarYears = [];
$generaleMdb = rtrim($this->argument('gescon-dir'), '/') . '/generale_stabile.mdb';
if (is_file($generaleMdb)) {
try {
$output = shell_exec(sprintf(
'echo "SELECT id_anno, anno_o FROM anni" | mdb-sql %s 2>/dev/null',
escapeshellarg($generaleMdb)
));
if ($output) {
$lines = explode("\n", $output);
foreach ($lines as $line) {
if (str_contains($line, '|')) {
$parts = explode('|', $line);
$id = (int) trim($parts[1] ?? '');
$yr = (int) trim($parts[2] ?? '');
if ($id > 0 && $yr >= 2000 && $yr <= 2100 && in_array($id, array_map('intval', $years), true)) {
$calendarYears[] = $yr;
}
}
}
}
} catch (\Throwable $e) {
}
}
if (empty($calendarYears)) {
$calendarYears = array_map('intval', $years);
}
$gestioneIds = GestioneContabile::forTenant($tenantId)
->where('stabile_id', $stabileId)
->whereIn('anno_gestione', $calendarYears)
->pluck('id')
->toArray();
if (!empty($gestioneIds)) {
// Nullify foreign keys to prevent constraint violations
DB::table('lavori_straordinari')->whereIn('gestione_id', $gestioneIds)->update(['gestione_id' => null]);
DB::table('contabilita_registrazioni')->whereIn('gestione_id', $gestioneIds)->update(['gestione_id' => null]);
DB::table('contabilita_fatture_fornitori')->whereIn('gestione_id', $gestioneIds)->update(['gestione_id' => null]);
$deletedIncassi = DB::table('incassi')->whereIn('gestione_id', $gestioneIds)->delete();
$deletedOps = DB::table('operazioni_contabili')->whereIn('gestione_id', $gestioneIds)->delete();
$deletedRDA = DB::table('registro_ritenute_acconto')->whereIn('gestione_id', $gestioneIds)->delete();
$deletedIncEc = DB::table('incassi_estratto_conto')->whereIn('gestione_id', $gestioneIds)->delete();
$this->line(" Deleted {$deletedIncassi} incassi, {$deletedOps} operazioni, {$deletedRDA} ritenute, {$deletedIncEc} incassi EC");
}
// Delete gestioni and cascade // Delete gestioni and cascade
$deletedGestioni = GestioneContabile::forTenant($tenantId) $deletedGestioni = GestioneContabile::forTenant($tenantId)
->whereIn('anno_gestione', $years) ->where('stabile_id', $stabileId)
->whereIn('anno_gestione', $calendarYears)
->delete(); ->delete();
$this->line(" Deleted {$deletedGestioni} gestioni"); $this->line(" Deleted {$deletedGestioni} gestioni");

View File

@ -114,7 +114,10 @@ private function ensureCondominColumns(): void
return; return;
} }
$cols = ['inquil_nome', 'inquil_cod_fisc', 'e_mail_inquilino', 'inquil_tel1', 'inquil_indir']; $cols = [
'inquil_nome', 'inquil_cod_fisc', 'e_mail_inquilino', 'inquil_tel1', 'inquil_indir',
'subentro_prima_cera', 'subentro_adesso_ce', 'subentrato_dal', 'attivo_fino_al'
];
$connection = Schema::connection('gescon_import'); $connection = Schema::connection('gescon_import');
Schema::connection('gescon_import')->table('condomin', function (Blueprint $table) use ($cols, $connection): void { Schema::connection('gescon_import')->table('condomin', function (Blueprint $table) use ($cols, $connection): void {
@ -159,7 +162,9 @@ private function persistStagingRow(string $stagingTable, array $filtered): void
if ($stagingTable === 'operazioni' && array_key_exists('id_operaz', $filtered) && $filtered['id_operaz'] !== null) { if ($stagingTable === 'operazioni' && array_key_exists('id_operaz', $filtered) && $filtered['id_operaz'] !== null) {
$query->updateOrInsert([ $query->updateOrInsert([
'id_operaz' => $filtered['id_operaz'], 'id_operaz' => $filtered['id_operaz'],
'cod_stabile' => $filtered['cod_stabile'] ?? null,
'legacy_year' => $filtered['legacy_year'] ?? null,
], $filtered); ], $filtered);
return; return;

View File

@ -15,6 +15,10 @@
use Filament\Support\Contracts\TranslatableContentDriver; use Filament\Support\Contracts\TranslatableContentDriver;
use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Columns\DatePickerColumn;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable; use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table; use Filament\Tables\Table;
@ -281,7 +285,15 @@ public function table(Table $table): Table
return; return;
} }
$tenantId = GestioneContabile::query()
->where('stabile_id', $stabileId)
->value('tenant_id');
if (! is_string($tenantId) || trim($tenantId) === '') {
$tenantId = (string) ($user->tenant_id ?? 'default');
}
$gestione = new GestioneContabile(); $gestione = new GestioneContabile();
$gestione->tenant_id = $tenantId;
$gestione->stabile_id = $stabileId; $gestione->stabile_id = $stabileId;
$gestione->anno_gestione = (int) ($data['anno_gestione'] ?? now()->year); $gestione->anno_gestione = (int) ($data['anno_gestione'] ?? now()->year);
$gestione->tipo_gestione = (string) ($data['tipo_gestione'] ?? 'ordinaria'); $gestione->tipo_gestione = (string) ($data['tipo_gestione'] ?? 'ordinaria');
@ -356,43 +368,28 @@ public function table(Table $table): Table
->sortable() ->sortable()
->toggleable(isToggledHiddenByDefault: true), ->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('denominazione') TextInputColumn::make('denominazione')
->label('Denominazione') ->label('Denominazione')
->searchable() ->searchable(),
->wrap(),
TextColumn::make('origine_dati') DatePickerColumn::make('data_inizio')
->label('Origine') ->label('Data inizio')
->getStateUsing(function (GestioneContabile $record): string { ->native(false),
if (filled($record->codice_archivio_legacy)) {
return 'Legacy collegata';
}
return 'Locale'; DatePickerColumn::make('data_fine')
}) ->label('Data fine')
->badge() ->native(false),
->color(fn(string $state): string => $state === 'Legacy collegata' ? 'success' : 'gray'),
TextColumn::make('periodo_label') SelectColumn::make('stato')
->label('Periodo')
->getStateUsing(fn(GestioneContabile $record): string => $record->periodo_label)
->wrap(),
TextColumn::make('stato')
->label('Stato') ->label('Stato')
->formatStateUsing(fn(string $state): string => ucfirst($state)) ->options([
->badge() 'aperta' => 'Aperta',
->color(fn(string $state): string => match ($state) { 'consolidata' => 'Consolidata',
'aperta' => 'success', 'chiusa' => 'Chiusa',
'consolidata' => 'warning', ]),
'chiusa' => 'gray',
default => 'gray',
})
->toggleable(isToggledHiddenByDefault: true),
IconColumn::make('gestione_attiva') ToggleColumn::make('gestione_attiva')
->label('Attiva') ->label('Attiva'),
->boolean(),
TextColumn::make('mesi_rate') TextColumn::make('mesi_rate')
->label('Mesi rate') ->label('Mesi rate')

View File

@ -1766,7 +1766,10 @@ private function resolveRiscaldamentoCandidateInvoices(?string $tipoGestione = n
->where('f.stabile_id', $stabileId) ->where('f.stabile_id', $stabileId)
->whereNotNull('f.fattura_elettronica_id') ->whereNotNull('f.fattura_elettronica_id')
->when($fornitoreIds !== [], fn($builder) => $builder->whereIn('f.fornitore_id', $fornitoreIds)) ->when($fornitoreIds !== [], fn($builder) => $builder->whereIn('f.fornitore_id', $fornitoreIds))
->where('g.tipo_gestione', $tipoGestione) ->where(function ($b) use ($tipoGestione) {
$b->where('g.tipo_gestione', $tipoGestione)
->orWhereNull('f.gestione_id');
})
->when($dal && $al, fn($builder) => $builder->whereBetween('f.data_documento', [$dal, $al])) ->when($dal && $al, fn($builder) => $builder->whereBetween('f.data_documento', [$dal, $al]))
->when(!($dal && $al), function($builder) use ($year) { ->when(!($dal && $al), function($builder) use ($year) {
$builder->when(Schema::hasColumn('gestioni_contabili', 'anno_gestione'), fn($b) => $b->where('g.anno_gestione', $year)) $builder->when(Schema::hasColumn('gestioni_contabili', 'anno_gestione'), fn($b) => $b->where('g.anno_gestione', $year))
@ -2839,7 +2842,7 @@ public function getRiscaldamentoFeEstratteRowsProperty(): array
$pagamento = is_array($payload['pagamento'] ?? null) ? $payload['pagamento'] : []; $pagamento = is_array($payload['pagamento'] ?? null) ? $payload['pagamento'] : [];
$riepilogoLetture = is_array($payload['riepilogo_letture'] ?? null) ? $payload['riepilogo_letture'] : []; $riepilogoLetture = is_array($payload['riepilogo_letture'] ?? null) ? $payload['riepilogo_letture'] : [];
$servizio = $this->resolveRiscaldamentoServizioForIdentifiers($servizi, $codici, $contatore) ?? $defaultServizio; $servizio = $this->resolveRiscaldamentoServizioForIdentifiers($servizi, $codici, $contatore, $fattura);
[$periodoDal, $periodoAl] = $this->resolveRiscaldamentoPeriodsFromPayload($consumi, is_string($fattura->consumo_riferimento ?? null) ? (string) $fattura->consumo_riferimento : null); [$periodoDal, $periodoAl] = $this->resolveRiscaldamentoPeriodsFromPayload($consumi, is_string($fattura->consumo_riferimento ?? null) ? (string) $fattura->consumo_riferimento : null);
$windowFrom = $this->extractRiscaldamentoWindowDate($payload, ['dal', 'from', 'inizio', 'start']); $windowFrom = $this->extractRiscaldamentoWindowDate($payload, ['dal', 'from', 'inizio', 'start']);
@ -2916,7 +2919,9 @@ public function getRiscaldamentoFeEstratteRowsProperty(): array
'numero_fattura' => trim((string) ($fattura->numero_fattura ?? $fattura->sdi_file ?? ('FE #' . (int) $fattura->id))), 'numero_fattura' => trim((string) ($fattura->numero_fattura ?? $fattura->sdi_file ?? ('FE #' . (int) $fattura->id))),
'data_fattura' => optional($fattura->data_fattura)?->format('d/m/Y'), 'data_fattura' => optional($fattura->data_fattura)?->format('d/m/Y'),
'data_fattura_iso' => optional($fattura->data_fattura)?->format('Y-m-d'), 'data_fattura_iso' => optional($fattura->data_fattura)?->format('Y-m-d'),
'servizio' => trim((string) ($servizio?->nome ?? 'Riscaldamento stabile')), 'servizio' => $servizio
? (string) $servizio->nome
: ($fattura->fornitore?->ragione_sociale ? 'Non abbinato: ' . $fattura->fornitore->ragione_sociale : 'Non abbinata / Fornitore sconosciuto'),
'servizio_id' => (int) ($servizio?->id ?? 0), 'servizio_id' => (int) ($servizio?->id ?? 0),
'matricola' => $matricola, 'matricola' => $matricola,
'codice_utenza' => $codiceUtenza, 'codice_utenza' => $codiceUtenza,
@ -3021,7 +3026,7 @@ private function formatRiscaldamentoPeriodLabel(?string $from, ?string $to): str
} }
/** @param \Illuminate\Support\Collection<int, StabileServizio> $servizi */ /** @param \Illuminate\Support\Collection<int, StabileServizio> $servizi */
private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $codici, array $contatore): ?StabileServizio private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $codici, array $contatore, ?FatturaElettronica $fattura = null): ?StabileServizio
{ {
if (! $servizi instanceof \Illuminate\Support\Collection || $servizi->isEmpty()) { if (! $servizi instanceof \Illuminate\Support\Collection || $servizi->isEmpty()) {
return null; return null;
@ -3031,20 +3036,24 @@ private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $cod
$targetUtenza = $this->normalizeRiscaldamentoIdentifier($codici['utenza'] ?? null); $targetUtenza = $this->normalizeRiscaldamentoIdentifier($codici['utenza'] ?? null);
$targetCliente = $this->normalizeRiscaldamentoIdentifier($codici['cliente'] ?? null); $targetCliente = $this->normalizeRiscaldamentoIdentifier($codici['cliente'] ?? null);
$targetContratto = $this->normalizeRiscaldamentoIdentifier($codici['contratto'] ?? null); $targetContratto = $this->normalizeRiscaldamentoIdentifier($codici['contratto'] ?? null);
$fornitoreId = $fattura?->fornitore_id;
return $servizi return $servizi
->map(function (StabileServizio $servizio) use ($targetMatricola, $targetUtenza, $targetCliente, $targetContratto): array { ->map(function (StabileServizio $servizio) use ($targetMatricola, $targetUtenza, $targetCliente, $targetContratto, $fornitoreId): array {
$score = 0; $score = 0;
if ($targetMatricola !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->contatore_matricola) === $targetMatricola) { if ($targetMatricola !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->contatore_matricola) === $targetMatricola) {
$score += 5; $score += 10;
} }
if ($targetUtenza !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_utenza) === $targetUtenza) { if ($targetUtenza !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_utenza) === $targetUtenza) {
$score += 4; $score += 8;
} }
if ($targetContratto !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_contratto) === $targetContratto) { if ($targetContratto !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_contratto) === $targetContratto) {
$score += 3; $score += 6;
} }
if ($targetCliente !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_cliente) === $targetCliente) { if ($targetCliente !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_cliente) === $targetCliente) {
$score += 4;
}
if ($fornitoreId && (int) $servizio->fornitore_id === (int) $fornitoreId) {
$score += 2; $score += 2;
} }
@ -4043,8 +4052,7 @@ protected function ensureFornitoreStabileDefaultVoceSpesa(int $stabileId): void
$voceSpesa = \App\Models\VoceSpesa::query() $voceSpesa = \App\Models\VoceSpesa::query()
->where('stabile_id', $stabileId) ->where('stabile_id', $stabileId)
->where(function ($q) use ($mostCommonVoceCode) { ->where(function ($q) use ($mostCommonVoceCode) {
$q->where('conto_contabile', $mostCommonVoceCode) $q->where('codice', $mostCommonVoceCode)
->orWhere('codice', $mostCommonVoceCode)
->orWhere('legacy_codice', $mostCommonVoceCode); ->orWhere('legacy_codice', $mostCommonVoceCode);
}) })
->first(); ->first();

View File

@ -731,7 +731,7 @@ public function importElectronicReadings(): void
private function normalizeAcquaTab(string $tab): string private function normalizeAcquaTab(string $tab): string
{ {
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi']; $allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi', 'pagamenti_cbill'];
return in_array($tab, $allowed, true) ? $tab : 'dashboard'; return in_array($tab, $allowed, true) ? $tab : 'dashboard';
} }
@ -3826,4 +3826,74 @@ public function getAcquaAltreVociLegacyProperty(): array
'totale' => round((float) ($r->totale ?? 0), 2), 'totale' => round((float) ($r->totale ?? 0), 2),
])->all(); ])->all();
} }
/**
* Recupera pagamenti bancari associati ai codici CBILL delle fatture.
*/
public function getAcquaCbillPagamentiProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [];
}
// Recupera tutti i codici CBILL delle fatture elettroniche di questo stabile
$cbillCodes = DB::table('fatture_elettroniche')
->where('stabile_id', $stabileId)
->whereNotNull('pagamento_cbill')
->where('pagamento_cbill', '<>', '')
->pluck('pagamento_cbill')
->unique()
->filter()
->all();
if (empty($cbillCodes)) {
return [];
}
// Interroga i movimenti bancari contenenti uno qualsiasi di tali codici
$movements = DB::table('contabilita_movimenti_banca')
->where('stabile_id', $stabileId)
->where(function ($query) use ($cbillCodes) {
foreach ($cbillCodes as $code) {
$query->orWhere('descrizione', 'like', "%{$code}%")
->orWhere('descrizione_estesa', 'like', "%{$code}%");
}
})
->orderByDesc('data')
->get();
$rows = [];
foreach ($movements as $m) {
$matchedCbill = null;
foreach ($cbillCodes as $code) {
if (str_contains($m->descrizione, $code) || str_contains($m->descrizione_estesa, $code)) {
$matchedCbill = $code;
break;
}
}
$invoice = null;
if ($matchedCbill) {
$invoice = DB::table('fatture_elettroniche')
->where('stabile_id', $stabileId)
->where('pagamento_cbill', $matchedCbill)
->first(['id', 'numero_fattura', 'data_fattura', 'totale', 'fornitore_denominazione']);
}
$rows[] = [
'data' => $m->data,
'descrizione' => $m->descrizione,
'importo' => $m->importo,
'cbill' => $matchedCbill,
'fattura_id' => $invoice?->id ?? null,
'fattura_numero' => $invoice?->numero_fattura ?? '—',
'fattura_data' => $invoice?->data_fattura ?? '—',
'fattura_totale' => $invoice?->totale ?? 0.0,
'fornitore' => $invoice?->fornitore_denominazione ?? '—',
];
}
return $rows;
}
} }

View File

@ -182,9 +182,7 @@ protected function getTableQuery(): Builder
return TabellaMillesimale::query()->whereRaw('1 = 0'); return TabellaMillesimale::query()->whereRaw('1 = 0');
} }
$orderColumn = Schema::hasColumn('tabelle_millesimali', 'nord') $orderColumn = DB::raw('COALESCE(nord, ordinamento, ordine_visualizzazione, 999999)');
? 'nord'
: (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : (Schema::hasColumn('tabelle_millesimali', 'ordinamento') ? 'ordinamento' : 'id'));
return TabellaMillesimale::query() return TabellaMillesimale::query()
->where('stabile_id', $activeStabileId) ->where('stabile_id', $activeStabileId)

View File

@ -223,7 +223,7 @@ public function table(Table $table): Table
$inquilini = []; $inquilini = [];
$altri = []; $altri = [];
$ruoli = $record->rubricaRuoliAttivi ?? collect(); $ruoli = ($record->rubricaRuoliAttivi ?? collect())->unique(fn($r) => $r->rubrica_id . '|' . $r->ruolo_standard);
foreach ($ruoli as $ruolo) { foreach ($ruoli as $ruolo) {
$nome = $ruolo->contatto?->nome_completo ?? '—'; $nome = $ruolo->contatto?->nome_completo ?? '—';
$cf = $ruolo->contatto?->codice_fiscale ? " ({$ruolo->contatto->codice_fiscale})" : ''; $cf = $ruolo->contatto?->codice_fiscale ? " ({$ruolo->contatto->codice_fiscale})" : '';
@ -265,18 +265,32 @@ public function table(Table $table): Table
TextColumn::make('palazzina') TextColumn::make('palazzina')
->label('Pal.') ->label('Pal.')
->sortable(query: function (Builder $query, string $direction): Builder {
return $query->orderBy('palazzina', $direction)
->orderBy('scala', $direction)
->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END")
->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END")
->orderBy('interno', $direction);
})
->toggleable(), ->toggleable(),
TextColumn::make('scala') TextColumn::make('scala')
->label('Scala') ->label('Scala')
->sortable()
->toggleable(), ->toggleable(),
TextColumn::make('piano') TextColumn::make('piano')
->label('Piano') ->label('Piano')
->sortable()
->toggleable(), ->toggleable(),
TextColumn::make('interno') TextColumn::make('interno')
->label('Int.') ->label('Int.')
->sortable(query: function (Builder $query, string $direction): Builder {
return $query->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END")
->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END")
->orderBy('interno', $direction);
})
->formatStateUsing(function ($state, UnitaImmobiliare $record): string { ->formatStateUsing(function ($state, UnitaImmobiliare $record): string {
$interno = trim((string) ($state ?? '')); $interno = trim((string) ($state ?? ''));
if ($interno === '') { if ($interno === '') {
@ -315,6 +329,7 @@ public function table(Table $table): Table
->label('Apri') ->label('Apri')
->icon('heroicon-o-arrow-right') ->icon('heroicon-o-arrow-right')
->url(fn(UnitaImmobiliare $record) => $unitaPageUrl . '?unita_id=' . $record->id), ->url(fn(UnitaImmobiliare $record) => $unitaPageUrl . '?unita_id=' . $record->id),
]); ])
->defaultSort('palazzina', 'asc');
} }
} }

View File

@ -85,6 +85,8 @@ class CasseBancheMovimenti extends Page implements HasTable
public int $riconciliazioneOffset = 0; public int $riconciliazioneOffset = 0;
public ?string $selectedCausaleForConfig = null;
public string $periodoTipo = 'mese'; public string $periodoTipo = 'mese';
public int $periodoAnno; public int $periodoAnno;
public int $periodoValore; public int $periodoValore;
@ -1134,6 +1136,72 @@ protected function getHeaderActions(): array
->success() ->success()
->send(); ->send();
}), }),
Action::make('configura_regola_causale')
->label('Configura regola causale')
->modalWidth('2xl')
->form([
TextInput::make('causale')
->label('Causale bancaria')
->required()
->disabled(),
Select::make('conto_dare_id')
->label('Conto Dare (addebito se importo > 0, es. Entrate)')
->options(fn() => PianoConti::query()->orderBy('codice')->get()->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione])->all())
->searchable()
->required(),
Select::make('conto_avere_id')
->label('Conto Avere (accredito se importo > 0, es. Uscite)')
->options(fn() => PianoConti::query()->orderBy('codice')->get()->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione])->all())
->searchable()
->required(),
TextInput::make('label')
->label('Nome regola (opzionale)')
->nullable(),
Textarea::make('note')
->label('Note')
->rows(2)
->nullable(),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$causale = trim((string) ($this->selectedCausaleForConfig ?? ''));
if ($causale === '') {
Notification::make()->title('Causale non valida')->danger()->send();
return;
}
try {
\App\Modules\Contabilita\Models\RegolaPrimaNota::updateOrCreate([
'stabile_id' => $stabileId,
'origine' => 'banca',
'chiave' => $causale,
], [
'label' => isset($data['label']) && trim($data['label']) !== '' ? trim($data['label']) : 'Regola causale ' . $causale,
'note' => $data['note'] ?? null,
'conto_dare_id' => $data['conto_dare_id'] ? (int) $data['conto_dare_id'] : null,
'conto_avere_id' => $data['conto_avere_id'] ? (int) $data['conto_avere_id'] : null,
'attiva' => true,
]);
Notification::make()->title('Regola configurata con successo')->success()->send();
} catch (\Throwable $e) {
Notification::make()->title('Errore')->body($e->getMessage())->danger()->send();
}
}),
]; ];
} }
@ -1698,6 +1766,317 @@ public function table(Table $table): Table
Notification::make()->title('Errore')->body($e->getMessage())->danger()->send(); Notification::make()->title('Errore')->body($e->getMessage())->danger()->send();
} }
}), }),
Action::make('incassa_quote_condominiali')
->label('Incassa quote')
->icon('heroicon-o-currency-euro')
->visible(function (MovimentoBanca $record): bool {
if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) {
return false;
}
$importo = (float) ($record->importo ?? 0);
if ($importo <= 0) {
return false;
}
return empty($record->registrazione_id);
})
->form([
Select::make('gestione_id')
->label('Gestione')
->options(function (): array {
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
return [];
}
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return [];
}
return $this->getVisibleGestioneOptions($stabileId);
})
->searchable()
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
->required(fn(MovimentoBanca $record) => empty($record->gestione_id)),
Select::make('soggetto_id')
->label('Soggetto (Condomino/Inquilino)')
->options(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return [];
}
return \App\Models\RataEmessaNg::query()
->join('unita_immobiliari', 'rate_emesse.unita_immobiliare_id', '=', 'unita_immobiliari.id')
->join('soggetti', 'rate_emesse.soggetto_responsabile_id', '=', 'soggetti.id')
->where('unita_immobiliari.stabile_id', $stabileId)
->where(function($q) {
$q->whereNull('rate_emesse.stato_rata')
->orWhere('rate_emesse.stato_rata', '!=', 'pagata');
})
->groupBy('soggetti.id')
->select(['soggetti.id', 'soggetti.ragione_sociale', 'soggetti.nome', 'soggetti.cognome'])
->get()
->mapWithKeys(function (object $s): array {
$label = is_string($s->ragione_sociale ?? null) && trim((string) $s->ragione_sociale) !== ''
? trim((string) $s->ragione_sociale)
: trim(((string) ($s->cognome ?? '')) . ' ' . ((string) ($s->nome ?? '')));
$label = $label !== '' ? $label : ('Soggetto #' . (string) $s->id);
return [(string) $s->id => $label];
})
->all();
})
->searchable()
->reactive()
->required()
->afterStateUpdated(function (callable $set, $state, callable $get, MovimentoBanca $record) {
if (empty($state)) {
$set('rate', []);
return;
}
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return;
}
$rate = \App\Models\RataEmessaNg::query()
->join('unita_immobiliari', 'rate_emesse.unita_immobiliare_id', '=', 'unita_immobiliari.id')
->where('unita_immobiliari.stabile_id', $stabileId)
->where('rate_emesse.soggetto_responsabile_id', (int) $state)
->where(function($q) {
$q->whereNull('rate_emesse.stato_rata')
->orWhere('rate_emesse.stato_rata', '!=', 'pagata');
})
->orderBy('rate_emesse.data_scadenza')
->orderBy('rate_emesse.id')
->select([
'rate_emesse.id',
'rate_emesse.descrizione',
'rate_emesse.data_scadenza',
'rate_emesse.importo_addebitato_soggetto',
'rate_emesse.importo_pagato',
'unita_immobiliari.codice_unita',
])
->get();
$importoBanca = round((float) ($record->importo ?? 0), 2);
$residuoBanca = $importoBanca;
$rateRows = [];
foreach ($rate as $r) {
$dovuto = round((float) $r->importo_addebitato_soggetto, 2);
$pagato = round((float) $r->importo_pagato, 2);
$aperto = round($dovuto - $pagato, 2);
if ($aperto <= 0) {
continue;
}
$daPagare = 0.0;
if ($residuoBanca > 0) {
$daPagare = min($residuoBanca, $aperto);
$residuoBanca = round($residuoBanca - $daPagare, 2);
}
$rateRows[] = [
'rata_id' => $r->id,
'codice_unita' => $r->codice_unita,
'descrizione' => $r->descrizione,
'scadenza' => $r->data_scadenza ? $r->data_scadenza->format('d/m/Y') : '',
'residuo_label' => 'Importo residuo: € ' . number_format($aperto, 2, ',', '.'),
'importo_pagato' => $daPagare,
];
}
$set('rate', $rateRows);
}),
\Filament\Forms\Components\Repeater::make('rate')
->label('Rate da incassare')
->schema([
\Filament\Forms\Components\Hidden::make('rata_id'),
\Filament\Forms\Components\Hidden::make('codice_unita'),
\Filament\Forms\Components\Hidden::make('descrizione'),
\Filament\Forms\Components\Hidden::make('scadenza'),
\Filament\Forms\Components\Hidden::make('residuo_label'),
\Filament\Forms\Components\Placeholder::make('codice_unita_lbl')
->label('Unità')
->content(fn ($get) => $get('codice_unita')),
\Filament\Forms\Components\Placeholder::make('descrizione_lbl')
->label('Descrizione')
->content(fn ($get) => $get('descrizione')),
\Filament\Forms\Components\Placeholder::make('scadenza_lbl')
->label('Scadenza')
->content(fn ($get) => $get('scadenza')),
\Filament\Forms\Components\Placeholder::make('residuo_lbl')
->label('Aperto')
->content(fn ($get) => $get('residuo_label')),
\Filament\Forms\Components\TextInput::make('importo_pagato')
->label('Importo da pagare')
->numeric()
->required()
->prefix('€'),
])
->addable(false)
->deletable(false)
->columns(5),
])
->action(function (MovimentoBanca $record, array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
try {
DB::transaction(function () use ($record, $data, $user) {
$stabileId = StabileContext::resolveActiveStabileId($user);
$gestioneId = (empty($record->gestione_id) && isset($data['gestione_id']) && is_numeric($data['gestione_id']))
? (int) $data['gestione_id']
: (int) ($record->gestione_id ?? 0);
if (empty($record->gestione_id)) {
$record->gestione_id = $gestioneId;
$record->save();
}
$dataPagamento = $record->data ?: now()->toDateString();
$importoMovimento = round((float) ($record->importo ?? 0), 2);
$rateSelections = $data['rate'] ?? [];
$totalAllocated = 0.0;
foreach ($rateSelections as $sel) {
$totalAllocated += round((float) ($sel['importo_pagato'] ?? 0), 2);
}
$totalAllocated = round($totalAllocated, 2);
if ($totalAllocated > $importoMovimento) {
throw new \RuntimeException('Il totale da pagare inserito supera l\'importo del movimento bancario.');
}
// Crea la registrazione
$reg = \App\Modules\Contabilita\Models\Registrazione::create([
'stabile_id' => $stabileId,
'gestione_id' => \App\Modules\Contabilita\Models\Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, $dataPagamento),
'data_registrazione' => $dataPagamento,
'descrizione' => 'Incasso rate da movimento banca: ' . $record->descrizione,
'created_by' => $user->id,
'updated_by' => $user->id,
]);
// Dare: Banca c/c
$contoDareId = null;
$datiBancari = DatiBancari::query()->where('iban', $record->iban)->first();
if ($datiBancari && is_string($datiBancari->codice)) {
$byCode = \App\Modules\Contabilita\Models\PianoConti::query()->where('codice', trim((string) $datiBancari->codice))->first();
if ($byCode) {
$contoDareId = (int) $byCode->id;
}
}
if (!$contoDareId) {
$contoDareId = 7;
}
\App\Modules\Contabilita\Models\Movimento::create([
'registrazione_id' => $reg->id,
'conto_id' => $contoDareId,
'tipo' => 'dare',
'importo' => $totalAllocated,
]);
// Avere & incassi per ciascuna rata
foreach ($rateSelections as $sel) {
$rataId = (int) $sel['rata_id'];
$importoPagato = round((float) ($sel['importo_pagato'] ?? 0), 2);
if ($importoPagato <= 0) {
continue;
}
$rata = \App\Models\RataEmessaNg::query()->lockForUpdate()->find($rataId);
if (!$rata) {
throw new \RuntimeException('Rata non trovata.');
}
$nuovoImportoPagato = round((float) $rata->importo_pagato + $importoPagato, 2);
$importoDovuto = round((float) $rata->importo_addebitato_soggetto, 2);
$statoRata = 'parziale';
if ($nuovoImportoPagato >= $importoDovuto) {
$statoRata = 'pagata';
}
$rata->update([
'importo_pagato' => $nuovoImportoPagato,
'stato_rata' => $statoRata,
'data_ultimo_pagamento' => $dataPagamento,
]);
$contoAvereId = null;
$condominoId = $rata->soggetto_responsabile_id;
if ($condominoId > 0) {
$condomino = \App\Models\Condomino::query()->find($condominoId);
$conto = app(\App\Modules\Contabilita\Services\ContiSoggettiService::class)->resolveContoCreditoCondomino(
$stabileId,
$condominoId,
$condomino?->display_name,
is_string($condomino?->codice_univoco ?? null) ? trim((string) $condomino->codice_univoco) : null
);
$contoAvereId = (int) $conto->id;
}
if (!$contoAvereId) {
$contoAvereId = 8;
}
\App\Modules\Contabilita\Models\Movimento::create([
'registrazione_id' => $reg->id,
'conto_id' => $contoAvereId,
'tipo' => 'avere',
'importo' => $importoPagato,
]);
\App\Models\Incasso::create([
'gestione_id' => $gestioneId,
'condominio_id' => $stabileId,
'conto_bancario_id' => $datiBancari?->id,
'condomino_id' => $condominoId,
'movimento_bancario_id' => $record->id,
'importo_pagato_euro' => $importoPagato,
'data_pagamento' => $dataPagamento,
'descrizione' => 'Incasso rata: ' . $rata->descrizione,
'stato_riconciliazione' => 'manuale',
'registrazione_id' => $reg->id,
'rate_associate' => [$rata->id],
]);
}
$reg->assertBilanciata();
$record->update([
'registrazione_id' => $reg->id,
]);
});
Notification::make()->title('Incasso rate registrato con successo.')->success()->send();
$this->resetTable();
} catch (\Throwable $e) {
Notification::make()->title('Errore durante l\'incasso')->body($e->getMessage())->danger()->send();
}
}),
]); ]);
} }
@ -2303,7 +2682,7 @@ protected function getActiveStabileId(): ?int
protected function normalizeHubTabValue(?string $value): string protected function normalizeHubTabValue(?string $value): string
{ {
$value = is_string($value) ? trim(strtolower($value)) : ''; $value = is_string($value) ? trim(strtolower($value)) : '';
return in_array($value, ['conti', 'saldi', 'movimenti', 'struttura', 'riconciliazione'], true) ? $value : 'conti'; return in_array($value, ['conti', 'saldi', 'movimenti', 'struttura', 'riconciliazione', 'associazione_causali'], true) ? $value : 'conti';
} }
protected function syncSelectedContoFromId(?int $contoId): void protected function syncSelectedContoFromId(?int $contoId): void
@ -4240,4 +4619,73 @@ protected function storeSaldoDocumento(int $stabileId, DatiBancari $conto, mixed
'caricato_da' => (int) $user->id, 'caricato_da' => (int) $user->id,
]); ]);
} }
public function getCausaliAssociazioneRows(): array
{
$stabileId = $this->getActiveStabileId();
if (! $stabileId) {
return [];
}
$uniqueCausali = DB::table('contabilita_movimenti_banca')
->where('stabile_id', $stabileId)
->whereNotNull('causale')
->where('causale', '!=', '')
->select('causale')
->selectRaw('count(*) as count')
->selectRaw('max(descrizione) as sample_description')
->groupBy('causale')
->get();
$rows = [];
foreach ($uniqueCausali as $uc) {
$code = $uc->causale;
$official = DB::table('contabilita_causali_bancarie')
->where('codice', $code)
->first();
$descr = $official?->descrizione ?? $uc->sample_description;
$regola = \App\Modules\Contabilita\Models\RegolaPrimaNota::query()
->where('stabile_id', $stabileId)
->where('origine', 'banca')
->where('chiave', $code)
->first();
$rows[] = [
'causale' => $code,
'descrizione' => $descr,
'count' => $uc->count,
'regola_id' => $regola?->id,
'conto_dare' => $regola?->contoDare ? ($regola->contoDare->codice . ' - ' . $regola->contoDare->descrizione) : null,
'conto_avere' => $regola?->contoAvere ? ($regola->contoAvere->codice . ' - ' . $regola->contoAvere->descrizione) : null,
'attiva' => $regola?->attiva ?? false,
];
}
return $rows;
}
public function startConfiguringRegolaCausale(string $causale): void
{
$this->selectedCausaleForConfig = $causale;
$stabileId = $this->getActiveStabileId();
$regola = null;
if ($stabileId) {
$regola = \App\Modules\Contabilita\Models\RegolaPrimaNota::query()
->where('stabile_id', $stabileId)
->where('origine', 'banca')
->where('chiave', $causale)
->first();
}
$this->mountAction('configura_regola_causale', [
'causale' => $causale,
'conto_dare_id' => $regola?->conto_dare_id,
'conto_avere_id' => $regola?->conto_avere_id,
'label' => $regola?->label,
'note' => $regola?->note,
]);
}
} }

View File

@ -3069,9 +3069,7 @@ public function getTabelleMillesimaliByTipo(string $tipoTabella): array
return TabellaMillesimale::query() return TabellaMillesimale::query()
->where('stabile_id', $stabileId) ->where('stabile_id', $stabileId)
->where('tipo_tabella', $tipoTabella) ->where('tipo_tabella', $tipoTabella)
->orderBy('ordinamento') ->ordinato()
->orderBy('codice_tabella')
->orderBy('nome_tabella')
->get(['id', 'denominazione', 'nome_tabella', 'codice_tabella']) ->get(['id', 'denominazione', 'nome_tabella', 'codice_tabella'])
->map(function (TabellaMillesimale $t): array { ->map(function (TabellaMillesimale $t): array {
$label = trim((string) ($t->denominazione ?? '')); $label = trim((string) ($t->denominazione ?? ''));

View File

@ -147,11 +147,13 @@ public function form(Schema $schema): Schema
Select::make('tipo') Select::make('tipo')
->label('Tipo') ->label('Tipo')
->options(['dare' => 'Dare', 'avere' => 'Avere']) ->options(['dare' => 'Dare', 'avere' => 'Avere'])
->required(), ->required()
->live(),
TextInput::make('importo') TextInput::make('importo')
->label('Importo') ->label('Importo')
->numeric() ->numeric()
->required(), ->required()
->live(onBlur: true),
]) ])
->columns(3) ->columns(3)
->required(), ->required(),
@ -159,6 +161,46 @@ public function form(Schema $schema): Schema
]); ]);
} }
public function getHeaderSummary(): array
{
$state = is_array($this->data ?? null) ? $this->data : [];
$movimenti = $state['movimenti'] ?? [];
if (empty($movimenti) && isset($this->registrazione) && $this->registrazione->id) {
$movimenti = Movimento::query()
->where('registrazione_id', (int) $this->registrazione->id)
->orderBy('id')
->get()
->map(fn (Movimento $m) => [
'conto_id' => $m->conto_id,
'tipo' => $m->tipo,
'importo' => $m->importo,
])
->all();
}
$totDare = 0.0;
$totAvere = 0.0;
foreach ($movimenti as $m) {
$tipo = (string) ($m['tipo'] ?? '');
$importo = (float) str_replace(',', '.', str_replace('.', '', (string) ($m['importo'] ?? '0')));
if ($tipo === 'dare') {
$totDare += $importo;
} elseif ($tipo === 'avere') {
$totAvere += $importo;
}
}
$delta = round($totDare - $totAvere, 2);
return [
'totale_dare' => $totDare,
'totale_avere' => $totAvere,
'delta' => $delta,
'is_bilanciata' => abs($delta) < 0.005,
];
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [

View File

@ -448,10 +448,7 @@ public function getTabelleMillesimaliOptions(): array
$q = TabellaMillesimale::query() $q = TabellaMillesimale::query()
->where('stabile_id', $stabileId) ->where('stabile_id', $stabileId)
->orderBy('ordine_visualizzazione') ->ordinato();
->orderBy('ordinamento')
->orderBy('codice_tabella')
->orderBy('nome_tabella');
if ($hasAnno) { if ($hasAnno) {
$q->where('anno_gestione', $anno); $q->where('anno_gestione', $anno);

View File

@ -750,6 +750,7 @@ public function applicaEnrichmentStabile(): void
public function aggiornaAnagraficheDaCondomin(): void public function aggiornaAnagraficheDaCondomin(): void
{ {
@set_time_limit(0);
$user = Auth::user(); $user = Auth::user();
if (! $user instanceof User) { if (! $user instanceof User) {
abort(403); abort(403);
@ -789,6 +790,7 @@ public function aggiornaAnagraficheDaCondomin(): void
public function risincronizzaRelazioniImportate(): void public function risincronizzaRelazioniImportate(): void
{ {
@set_time_limit(0);
$user = Auth::user(); $user = Auth::user();
if (! $user instanceof User) { if (! $user instanceof User) {
abort(403); abort(403);
@ -826,6 +828,7 @@ public function risincronizzaRelazioniImportate(): void
public function aggiornaFornitoriGescon(): void public function aggiornaFornitoriGescon(): void
{ {
@set_time_limit(0);
$user = Auth::user(); $user = Auth::user();
if (! $user instanceof User) { if (! $user instanceof User) {
abort(403); abort(403);
@ -894,6 +897,7 @@ public function aggiornaFornitoriGescon(): void
public function refreshImportTagLegacyFornitoriGlobale(): void public function refreshImportTagLegacyFornitoriGlobale(): void
{ {
@set_time_limit(0);
$user = Auth::user(); $user = Auth::user();
if (! $user instanceof User) { if (! $user instanceof User) {
abort(403); abort(403);
@ -968,6 +972,7 @@ public function refreshImportTagLegacyFornitoriGlobale(): void
public function applicaEnrichmentTuttiStabili(): void public function applicaEnrichmentTuttiStabili(): void
{ {
@set_time_limit(0);
$user = Auth::user(); $user = Auth::user();
if (! $user instanceof User) { if (! $user instanceof User) {
abort(403); abort(403);
@ -1019,6 +1024,7 @@ public function applicaEnrichmentTuttiStabili(): void
public function eseguiImportSelezionato(EssentialImportService $service): void public function eseguiImportSelezionato(EssentialImportService $service): void
{ {
@set_time_limit(0);
$user = Auth::user(); $user = Auth::user();
if (! $user instanceof User) { if (! $user instanceof User) {
abort(403); abort(403);
@ -1179,8 +1185,11 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
if (! empty($options['sync_staging'])) { if (! empty($options['sync_staging'])) {
$singoloMdb = null; $singoloMdb = null;
$annoFolder = $options['anno'] ?: '0001';
if (! empty($options['path']) && ! empty($code)) { if (! empty($options['path']) && ! empty($code)) {
$annoFolder = $options['anno'] ?: '0001'; if (is_numeric($annoFolder) && strlen($annoFolder) === 4 && (int)$annoFolder > 1900) {
$annoFolder = $this->resolveYearToFolder($code, $options['path'], $annoFolder);
}
$singoloMdb = rtrim($options['path'], '/') . '/' . $code . '/' . $annoFolder . '/singolo_anno.mdb'; $singoloMdb = rtrim($options['path'], '/') . '/' . $code . '/' . $annoFolder . '/singolo_anno.mdb';
} }
@ -1189,7 +1198,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
: null; : null;
try { try {
$legacyYearArg = (string) ($options['anno'] ?? ''); $legacyYearArg = $annoFolder;
if ((! empty($options['with_banche']) || ! empty($options['with_operazioni']) || ! empty($options['with_incassi'])) && ! empty($options['path']) && ! empty($code)) { if ((! empty($options['with_banche']) || ! empty($options['with_operazioni']) || ! empty($options['with_incassi'])) && ! empty($options['path']) && ! empty($code)) {
foreach ($this->resolveLegacySingoloMdbPaths($code, (string) $options['path'], $legacyYearArg !== '' ? $legacyYearArg : null) as $mdbPath => $legacyDir) { foreach ($this->resolveLegacySingoloMdbPaths($code, (string) $options['path'], $legacyYearArg !== '' ? $legacyYearArg : null) as $mdbPath => $legacyDir) {
@ -1209,7 +1218,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb, '--mdb' => $singoloMdb,
'--table' => 's_cassa', '--table' => 's_cassa',
'--stabile' => $code, '--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''), '--legacy-year' => $annoFolder,
'--refresh-slice' => true, '--refresh-slice' => true,
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
@ -1218,7 +1227,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb, '--mdb' => $singoloMdb,
'--table' => 'operazioni', '--table' => 'operazioni',
'--stabile' => $code, '--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''), '--legacy-year' => $annoFolder,
'--refresh-slice' => true, '--refresh-slice' => true,
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
@ -1227,7 +1236,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb, '--mdb' => $singoloMdb,
'--table' => 'voc_spe', '--table' => 'voc_spe',
'--stabile' => $code, '--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''), '--legacy-year' => $annoFolder,
'--refresh-slice' => true, '--refresh-slice' => true,
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
@ -1236,7 +1245,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb, '--mdb' => $singoloMdb,
'--table' => 'tabelle', '--table' => 'tabelle',
'--stabile' => $code, '--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''), '--legacy-year' => $annoFolder,
'--refresh-slice' => true, '--refresh-slice' => true,
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
@ -1248,7 +1257,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb, '--mdb' => $singoloMdb,
'--table' => 'incassi', '--table' => 'incassi',
'--stabile' => $code, '--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''), '--legacy-year' => $annoFolder,
'--refresh-slice' => true, '--refresh-slice' => true,
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
@ -1259,7 +1268,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $generaleMdb, '--mdb' => $generaleMdb,
'--table' => 'emes_gen', '--table' => 'emes_gen',
'--stabile' => $code, '--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''), '--legacy-year' => $annoFolder,
'--refresh-slice' => true, '--refresh-slice' => true,
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
@ -1268,7 +1277,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $generaleMdb, '--mdb' => $generaleMdb,
'--table' => 'emes_det', '--table' => 'emes_det',
'--stabile' => $code, '--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''), '--legacy-year' => $annoFolder,
'--refresh-slice' => true, '--refresh-slice' => true,
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
@ -1289,7 +1298,11 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--path' => $options['path'], '--path' => $options['path'],
]; ];
if (! empty($options['anno'])) { if (! empty($options['anno'])) {
$params['--anno'] = $options['anno']; $annoParam = $options['anno'];
if (is_numeric($annoParam) && strlen($annoParam) === 4 && (int)$annoParam > 1900) {
$annoParam = $this->resolveYearToFolder($code, $options['path'], $annoParam);
}
$params['--anno'] = $annoParam;
} }
if (! empty($options['dry'])) { if (! empty($options['dry'])) {
$params['--dry-run'] = true; $params['--dry-run'] = true;
@ -1350,6 +1363,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
public function eseguiImportAllineamentoCompleto(): void public function eseguiImportAllineamentoCompleto(): void
{ {
@set_time_limit(0);
$user = Auth::user(); $user = Auth::user();
if (! $user instanceof User) { if (! $user instanceof User) {
abort(403); abort(403);
@ -1793,8 +1807,15 @@ private function applyGestioneSetup(Stabile $stabile): int
private function upsertGestioneRecord(Stabile $stabile, int $year, string $tipo, array $months, bool $active): void private function upsertGestioneRecord(Stabile $stabile, int $year, string $tipo, array $months, bool $active): void
{ {
$tenantId = GestioneContabile::query()
->where('stabile_id', $stabile->id)
->value('tenant_id');
if (! is_string($tenantId) || trim($tenantId) === '') {
$tenantId = (string) (Auth::user()?->tenant_id ?? 'default');
}
$gestione = GestioneContabile::query()->firstOrNew([ $gestione = GestioneContabile::query()->firstOrNew([
'tenant_id' => 'default', 'tenant_id' => $tenantId,
'stabile_id' => $stabile->id, 'stabile_id' => $stabile->id,
'anno_gestione' => $year, 'anno_gestione' => $year,
'tipo_gestione' => $tipo, 'tipo_gestione' => $tipo,
@ -2383,4 +2404,42 @@ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array
return $rows; return $rows;
} }
private function resolveYearToFolder(string $code, string $path, string $year): string
{
$code = str_pad(trim($code), 4, '0', STR_PAD_LEFT);
$mdb = rtrim($path, '/') . '/' . $code . '/generale_stabile.mdb';
if (! is_file($mdb) || ! is_readable($mdb)) {
return $year;
}
$rows = $this->mdbExportToRows($mdb, ['anni', 'Anni', 'ANNI']);
if (empty($rows)) {
return $year;
}
foreach ($rows as $r) {
$yearRaw = $this->firstNonEmpty($r, ['anno_o', 'anno_r', 'anno']);
$y = null;
if (is_scalar($yearRaw) && preg_match('/(19|20)\d{2}/', (string) $yearRaw, $m)) {
$y = $m[0];
}
if ($y === null) {
continue;
}
if ($y === $year) {
$dirRaw = $this->firstNonEmpty($r, ['nome_dir', 'dir', 'cartella', 'id_anno']);
$dir = trim((string) ($dirRaw ?? ''));
if ($dir === '' && is_numeric((string) ($r['id_anno'] ?? ''))) {
$dir = str_pad((string) ((int) $r['id_anno']), 4, '0', STR_PAD_LEFT);
}
if ($dir !== '') {
return $dir;
}
}
}
return $year;
}
} }

View File

@ -51,7 +51,7 @@ class Ordinarie extends Page
public string $viewTab = 'operazioni'; public string $viewTab = 'operazioni';
public string $sortField = 'id_operaz'; public string $sortField = 'id_operaz';
public string $sortDirection = 'desc'; public string $sortDirection = 'desc';
public bool $netgesconWorkflowMode = false; public bool $netgesconWorkflowMode = true;
public bool $onlyStraordinarieMode = false; public bool $onlyStraordinarieMode = false;
public bool $onlyRiscaldamentoMode = false; public bool $onlyRiscaldamentoMode = false;
public ?int $selectedStraordinaria = null; public ?int $selectedStraordinaria = null;
@ -3306,6 +3306,7 @@ public function openDettPersModal(int $nSpe): void
if ($activeStabileId && Schema::hasTable('tabelle_millesimali') && Schema::hasTable('dettaglio_millesimi')) { if ($activeStabileId && Schema::hasTable('tabelle_millesimali') && Schema::hasTable('dettaglio_millesimi')) {
$localRows = DB::table('tabelle_millesimali') $localRows = DB::table('tabelle_millesimali')
->where('stabile_id', (int) $activeStabileId) ->where('stabile_id', (int) $activeStabileId)
->orderByRaw('COALESCE(nord, ordinamento, ordine_visualizzazione, 999999)')
->orderBy('codice_tabella') ->orderBy('codice_tabella')
->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione']); ->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione']);
@ -3716,8 +3717,7 @@ private function buildOperazioniQuery()
$query->where(function ($q) use ($s) { $query->where(function ($q) use ($s) {
$q->where('o.descrizione', 'like', $s) $q->where('o.descrizione', 'like', $s)
->orWhere('o.conto_contabile', 'like', $s) ->orWhere('o.conto_contabile', 'like', $s)
->orWhere('o.protocollo_completo', 'like', $s) ->orWhere('o.protocollo_completo', 'like', $s);
->orWhere('o.num_fat', 'like', $s);
}); });
} }
@ -3739,8 +3739,8 @@ private function buildOperazioniQuery()
'o.n_stra', 'o.n_stra',
'o.protocollo_completo', 'o.protocollo_completo',
DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"), DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"),
'o.num_fat', DB::raw("null as num_fat"),
'o.fe_uid', DB::raw("null as fe_uid"),
'o.voce_spesa_snapshot', 'o.voce_spesa_snapshot',
'o.fornitore_snapshot', 'o.fornitore_snapshot',
]); ]);
@ -3767,8 +3767,8 @@ private function buildOperazioniQuery()
'o.protocollo_completo', 'o.protocollo_completo',
DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"), DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"),
DB::raw("coalesce(o.dare, o.avere, 0) as importo"), DB::raw("coalesce(o.dare, o.avere, 0) as importo"),
'o.num_fat', DB::raw("null as num_fat"),
'o.fe_uid', DB::raw("null as fe_uid"),
'o.voce_spesa_snapshot', 'o.voce_spesa_snapshot',
'o.fornitore_snapshot', 'o.fornitore_snapshot',
DB::raw("0 as importo_spese"), DB::raw("0 as importo_spese"),
@ -3994,8 +3994,8 @@ private function applyOperazioniSorting($query)
'dt_spe' => 'o.data_operazione', 'dt_spe' => 'o.data_operazione',
'cod_spe' => 'o.conto_contabile', 'cod_spe' => 'o.conto_contabile',
'importo_euro' => 'o.dare', 'importo_euro' => 'o.dare',
'num_fat' => 'o.num_fat', 'num_fat' => 'o.id',
'fe_uid' => 'o.fe_uid', 'fe_uid' => 'o.id',
default => 'o.id' default => 'o.id'
}; };
$query->orderBy($mappedField, $dir); $query->orderBy($mappedField, $dir);

View File

@ -555,7 +555,7 @@ private function getTabelleOptions(): array
return TabellaMillesimale::query() return TabellaMillesimale::query()
->where('stabile_id', $this->stabileAttivo->id) ->where('stabile_id', $this->stabileAttivo->id)
->orderBy('codice_tabella') ->ordinato()
->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione']) ->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione'])
->mapWithKeys(function (TabellaMillesimale $t) { ->mapWithKeys(function (TabellaMillesimale $t) {
$label = $t->codice_tabella ?: ($t->denominazione ?: ($t->nome_tabella ?: null)); $label = $t->codice_tabella ?: ($t->denominazione ?: ($t->nome_tabella ?: null));

View File

@ -30,11 +30,11 @@ class CausaliBancarie extends Page implements HasTable
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-tag'; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-tag';
protected static UnitEnum|string|null $navigationGroup = 'Super Admin'; protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
protected static ?int $navigationSort = 22; protected static ?int $navigationSort = 20;
protected static ?string $slug = 'superadmin/causali-bancarie'; protected static ?string $slug = 'contabilita/causali-bancarie';
protected string $view = 'filament.pages.gescon.table'; protected string $view = 'filament.pages.gescon.table';
@ -43,7 +43,7 @@ public static function canAccess(): bool
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin']) && ($user->hasAnyRole(['super-admin', 'admin']) || $user->can('contabilita.causali'))
&& Schema::hasTable('contabilita_causali_bancarie'); && Schema::hasTable('contabilita_causali_bancarie');
} }

View File

@ -23,7 +23,7 @@
use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable; use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table; use Filament\Tables\Table;
use Filament\Tables\Actions\Action as TableAction; use Filament\Actions\Action as TableAction;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;

View File

@ -13,31 +13,29 @@ class IncassoPagamento extends Model
protected $table = 'incassi_pagamenti'; protected $table = 'incassi_pagamenti';
protected $fillable = [ protected $fillable = [
'amministratore_id',
'anno_gestione_id',
'stabile_id', 'stabile_id',
'anno_gestione_id',
'condomino_id', 'condomino_id',
'rata_id', 'cod_condomino',
'data_incasso', 'n_riferimento',
'data_valuta', 'data_pagamento',
'importo', 'importo_pagato',
'modalita_pagamento', 'importo_pagato_euro',
'numero_assegno', 'n_ricevuta',
'banca_assegno', 'tipo_gestione',
'numero_bonifico', 'mese_riferimento',
'causale', 'descrizione',
'note_bancarie', 'cod_cassa',
'riconciliato', 'riconciliato',
'data_riconciliazione', 'rata_id',
'note' 'movimento_banca_id',
]; ];
protected $casts = [ protected $casts = [
'data_incasso' => 'date', 'data_pagamento' => 'date',
'data_valuta' => 'date', 'importo_pagato' => 'decimal:2',
'data_riconciliazione' => 'date', 'importo_pagato_euro' => 'decimal:2',
'importo' => 'decimal:2', 'riconciliato' => 'boolean',
'riconciliato' => 'boolean'
]; ];
const MODALITA_CONTANTI = 'contanti'; const MODALITA_CONTANTI = 'contanti';

View File

@ -588,6 +588,14 @@ public function syncRubricaContatto(): RubricaUniversale
'stato' => 'attivo', 'stato' => 'attivo',
]; ];
if (! $rubrica && ! empty($data['codice_univoco'])) {
$existing = RubricaUniversale::where('codice_univoco', $data['codice_univoco'])->first();
if ($existing) {
$rubrica = $existing;
$this->update(['rubrica_id' => $rubrica->id]);
}
}
if ($rubrica) { if ($rubrica) {
$rubrica->update($data); $rubrica->update($data);
} else { } else {

View File

@ -153,15 +153,8 @@ public function scopePerStabile($query, $stabileId)
*/ */
public function scopeOrdinato($query) public function scopeOrdinato($query)
{ {
if (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione')) {
$query->orderBy('ordine_visualizzazione');
} elseif (Schema::hasColumn('tabelle_millesimali', 'nord')) {
$query->orderBy('nord');
} else {
$query->orderBy('ordinamento');
}
return $query return $query
->orderByRaw('COALESCE(ordine_visualizzazione, nord, ordinamento, 999999)')
->orderBy('codice_tabella') ->orderBy('codice_tabella')
->orderBy('nome_tabella'); ->orderBy('nome_tabella');
} }

View File

@ -67,6 +67,96 @@ public function getDescrizioneEstesaPulitaAttribute(): ?string
return $text !== '' ? $text : null; return $text !== '' ? $text : null;
} }
public function getDettaglioEstrattoAttribute(): array
{
$rawText = $this->descrizione_estesa ?? $this->descrizione ?? '';
$text = trim($rawText);
$commissioni = '';
$pos = -1;
foreach (['COMM', 'SPESE', 'TRN', 'COMMISSIONI', 'COMMISSIONE'] as $key) {
$p = stripos($text, $key);
if ($p !== false && ($pos === -1 || $p < $pos)) {
$pos = $p;
}
}
if ($pos !== -1) {
$commissioni = trim(substr($text, $pos));
$text = trim(substr($text, 0, $pos));
}
$bankPrefixes = [
'BONIFICO A VOSTRO FAVORE BONIFICO SEPA DA',
'BONIFICO A VOSTRO FAVORE BONIFICO SEPA',
'BONIFICO SEPA A VOSTRO FAVORE DA',
'BONIFICO SEPA A VOSTRO FAVORE',
'BONIFICO A VOSTRO FAVORE DA',
'BONIFICO A VOSTRO FAVORE',
'BONIFICO SEPA DA',
'BONIFICO SEPA',
'BONIFICO DA',
'BONIFICO ESEGUITO DA',
'ADDEBITO CORRISPETTIVI',
'GIROCONTO DA',
'GIROCONTO',
'DISPOSIZIONE DI PAGAMENTO DA',
'DISPOSIZIONE DI PAGAMENTO',
'PAGAMENTO CBILL',
'PAGAMENTO PAGOPA',
'VERSAMENTO DI CASSA',
'VERSAMENTO DA',
'VERSAMENTO',
'PRELEVAMENTO DA',
'PRELEVAMENTO',
'PAGAMENTO CON CARTA',
'PAGAMENTO POS',
'ADDEBITO DISPOSIZIONE SEPA',
'ADDEBITO SEPA',
];
$messaggioBanca = '';
$messaggioCliente = $text;
foreach ($bankPrefixes as $prefix) {
if (stripos($text, $prefix) === 0) {
$messaggioBanca = $prefix;
$messaggioCliente = trim(substr($text, strlen($prefix)));
break;
}
}
if (empty($messaggioBanca) && !empty($this->causale)) {
$messaggioBanca = $this->causale;
}
$cbillCode = null;
if (preg_match('/(?:CBILL|PAGOPA|IUV|AVVISO|CODICE\s*AVVISO)\s*:?\s*(\d{10,20})/i', $rawText, $cbillMatches)) {
$cbillCode = $cbillMatches[1];
}
$abi = null;
if (!empty($this->raw_line)) {
$parts = explode(';', $this->raw_line);
if (count($parts) >= 2) {
$last = trim(end($parts));
if (preg_match('/^[a-zA-Z0-9]{3,5}$/', $last)) {
$abi = $last;
}
}
}
if (!$abi && is_array($this->match_data) && !empty($this->match_data['cod_abi'])) {
$abi = $this->match_data['cod_abi'];
}
return [
'abi' => $abi,
'messaggio_banca' => $messaggioBanca ?: '—',
'messaggio_cliente' => $messaggioCliente ?: '—',
'commissioni_spese' => $commissioni ?: '—',
'cbill_code' => $cbillCode,
];
}
protected function applyCleaningRules(string $text): string protected function applyCleaningRules(string $text): string
{ {
$patterns = []; $patterns = [];

View File

@ -185,6 +185,8 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
} }
} }
$isCreditNote = isset($data['tipo_documento']) && strtoupper(trim($data['tipo_documento'])) === 'TD04';
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId); $stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0); $amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
@ -230,7 +232,17 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
try { try {
/** @var FatturaElettronica $created */ /** @var FatturaElettronica $created */
$created = DB::transaction(function () use ($stabileId, $fornitoreId, $data, $xml, $hash, $originalFilename, $fornitorePiva, $extra, $importRighe, &$contabilitaFatturaId) { $created = DB::transaction(function () use ($stabileId, $fornitoreId, $data, $xml, $hash, $originalFilename, $fornitorePiva, $extra, $importRighe, &$contabilitaFatturaId, $isCreditNote) {
$imponibile = (float) ($data['imponibile'] ?? 0);
$iva = (float) ($data['iva'] ?? 0);
$totale = (float) ($data['totale'] ?? 0);
if ($isCreditNote) {
$imponibile = -abs($imponibile);
$iva = -abs($iva);
$totale = -abs($totale);
}
$created = FatturaElettronica::query()->create([ $created = FatturaElettronica::query()->create([
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'fornitore_id' => $fornitoreId, 'fornitore_id' => $fornitoreId,
@ -241,9 +253,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
'fornitore_piva' => $fornitorePiva, 'fornitore_piva' => $fornitorePiva,
'fornitore_cf' => $data['fornitore_cf'] ?? null, 'fornitore_cf' => $data['fornitore_cf'] ?? null,
'fornitore_denominazione' => $data['fornitore_denominazione'] ?? 'ND', 'fornitore_denominazione' => $data['fornitore_denominazione'] ?? 'ND',
'imponibile' => (float) ($data['imponibile'] ?? 0), 'imponibile' => $imponibile,
'iva' => (float) ($data['iva'] ?? 0), 'iva' => $iva,
'totale' => (float) ($data['totale'] ?? 0), 'totale' => $totale,
'pagamento_modalita' => $data['pagamento_modalita'] ?? null, 'pagamento_modalita' => $data['pagamento_modalita'] ?? null,
'pagamento_iban' => $data['pagamento_iban'] ?? null, 'pagamento_iban' => $data['pagamento_iban'] ?? null,
@ -274,7 +286,10 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
$ritenuta = is_array($data['ritenuta'] ?? null) ? $data['ritenuta'] : []; $ritenuta = is_array($data['ritenuta'] ?? null) ? $data['ritenuta'] : [];
$ritenutaImporto = (float) ($ritenuta['importo'] ?? 0); $ritenutaImporto = (float) ($ritenuta['importo'] ?? 0);
$ritenutaAliquota = $ritenuta['aliquota'] ?? null; $ritenutaAliquota = $ritenuta['aliquota'] ?? null;
$netto = round(((float) ($data['totale'] ?? 0)) - $ritenutaImporto, 2); if ($isCreditNote) {
$ritenutaImporto = -abs($ritenutaImporto);
}
$netto = round($totale - $ritenutaImporto, 2);
$dataDoc = $data['data_fattura'] ?? null; $dataDoc = $data['data_fattura'] ?? null;
$dataRegistrazione = null; $dataRegistrazione = null;
@ -298,9 +313,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
'descrizione' => $data['fornitore_denominazione'] ?? null, 'descrizione' => $data['fornitore_denominazione'] ?? null,
'valuta' => 'EUR', 'valuta' => 'EUR',
'cambio' => 1, 'cambio' => 1,
'imponibile' => round((float) ($data['imponibile'] ?? 0), 2), 'imponibile' => round($imponibile, 2),
'iva' => round((float) ($data['iva'] ?? 0), 2), 'iva' => round($iva, 2),
'totale' => round((float) ($data['totale'] ?? 0), 2), 'totale' => round($totale, 2),
'data_scadenza' => $data['data_scadenza'] ?? null, 'data_scadenza' => $data['data_scadenza'] ?? null,
'modalita_pagamento' => $data['pagamento_modalita'] ?? null, 'modalita_pagamento' => $data['pagamento_modalita'] ?? null,
'ritenuta_aliquota' => $ritenutaAliquota, 'ritenuta_aliquota' => $ritenutaAliquota,
@ -333,7 +348,7 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
'descrizione' => $data['numero_fattura'] ?? 'Fattura elettronica', 'descrizione' => $data['numero_fattura'] ?? 'Fattura elettronica',
'quantita' => null, 'quantita' => null,
'prezzo_unitario' => null, 'prezzo_unitario' => null,
'prezzo_totale' => (float) ($data['imponibile'] ?? 0), 'prezzo_totale' => $imponibile,
'aliquota_iva' => null, 'aliquota_iva' => null,
'ritenuta' => null, 'ritenuta' => null,
]]; ]];
@ -344,6 +359,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
if ($imponibileRiga <= 0 && isset($r['prezzo_unitario'], $r['quantita'])) { if ($imponibileRiga <= 0 && isset($r['prezzo_unitario'], $r['quantita'])) {
$imponibileRiga = (float) $r['prezzo_unitario'] * (float) $r['quantita']; $imponibileRiga = (float) $r['prezzo_unitario'] * (float) $r['quantita'];
} }
if ($isCreditNote) {
$imponibileRiga = -abs($imponibileRiga);
}
$aliquota = $r['aliquota_iva'] ?? null; $aliquota = $r['aliquota_iva'] ?? null;
$ivaRiga = 0.0; $ivaRiga = 0.0;

View File

@ -2,8 +2,8 @@
namespace App\Services\Import; namespace App\Services\Import;
use App\Models\GestioneContabile; use App\Models\GestioneContabile;
use App\Models\Incasso;
use App\Models\IncassoEstrattoConto; use App\Models\IncassoEstrattoConto;
use App\Models\OperazioneContabile;
use App\Models\RegistroRitenuteAcconto; use App\Models\RegistroRitenuteAcconto;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -21,12 +21,52 @@
class MultiYearGesconImportService class MultiYearGesconImportService
{ {
private string $tenantId; private string $tenantId;
private ?int $stabileId = null;
private ?string $baseDir = null;
private array $directoryToYearMap = [];
private array $mdbConnections = []; private array $mdbConnections = [];
private array $gestioni = []; private array $gestioni = [];
public function __construct(string $tenantId) public function __construct(string $tenantId, ?int $stabileId = null, ?string $baseDir = null)
{ {
$this->tenantId = $tenantId; $this->tenantId = $tenantId;
$this->stabileId = $stabileId;
$this->baseDir = $baseDir;
if ($baseDir) {
$this->loadYearMap($baseDir);
}
}
private function loadYearMap(string $baseDir): void
{
$generaleMdb = rtrim($baseDir, '/') . '/generale_stabile.mdb';
if (!is_file($generaleMdb)) {
return;
}
try {
$output = shell_exec(sprintf(
'echo "SELECT id_anno, anno_o FROM anni" | mdb-sql %s 2>/dev/null',
escapeshellarg($generaleMdb)
));
if ($output) {
$lines = explode("\n", $output);
foreach ($lines as $line) {
if (str_contains($line, '|')) {
$parts = explode('|', $line);
$id = (int) trim($parts[1] ?? '');
$yr = (int) trim($parts[2] ?? '');
if ($id > 0 && $yr >= 2000 && $yr <= 2100) {
$dir = str_pad((string)$id, 4, '0', STR_PAD_LEFT);
$this->directoryToYearMap[$dir] = $yr;
}
}
}
}
} catch (\Throwable $e) {
Log::warning("Could not load year map from general MDB: " . $e->getMessage());
}
} }
/** /**
@ -87,6 +127,31 @@ private function scanGesconYears(string $baseDir): array
return $years; return $years;
} }
/**
* Crea gestioni contabili per tutti gli anni scansionati
*/
public function createGestioni(array $years): void
{
foreach ($years as $year => $paths) {
$this->initGestioniForYear($year, $paths);
}
}
/**
* Inizializza le gestioni contabili per un singolo anno
*/
public function initGestioniForYear(string $year, array $paths): void
{
$calendarYear = $this->directoryToYearMap[$year] ?? (int) $year;
$mdbPath = $paths['singolo_anno'];
$gestioniData = $this->scanGestioniFromMdb($mdbPath, $calendarYear);
foreach ($gestioniData as $data) {
$this->createOrUpdateGestione($data, $year);
}
}
/** /**
* Scansiona gestioni direttamente dal file MDB * Scansiona gestioni direttamente dal file MDB
*/ */
@ -104,13 +169,25 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array
'protocollo_prefix' => "O{$year}", 'protocollo_prefix' => "O{$year}",
]; ];
// Gestione Riscaldamento (se presente) // Legge le colonne necessarie da Operazioni
$opRiscaldamento = $this->queryMdb($mdbPath, " $operazioni = $this->queryMdb($mdbPath, "SELECT Gestione, n_stra FROM Operazioni");
SELECT COUNT(*) as cnt FROM operazioni
WHERE gestione = 'R' OR gestione = 'r'
");
if (! empty($opRiscaldamento) && ($opRiscaldamento[0]['cnt'] ?? 0) > 0) { $hasRiscaldamento = false;
$straordinarie = [];
foreach ($operazioni as $op) {
$g = strtoupper(trim($op['Gestione'] ?? ''));
if ($g === 'R') {
$hasRiscaldamento = true;
} elseif ($g === 'S') {
$nStra = (int) ($op['n_stra'] ?? 0);
if ($nStra > 0) {
$straordinarie[$nStra] = true;
}
}
}
if ($hasRiscaldamento) {
$gestioni[] = [ $gestioni[] = [
'anno_gestione' => $year, 'anno_gestione' => $year,
'tipo_gestione' => 'riscaldamento', 'tipo_gestione' => 'riscaldamento',
@ -121,18 +198,8 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array
]; ];
} }
// Gestioni Straordinarie (dinamiche) ksort($straordinarie);
$straordinarie = $this->queryMdb($mdbPath, " foreach (array_keys($straordinarie) as $numero) {
SELECT DISTINCT N_STRA as numero
FROM operazioni
WHERE (gestione = 'S' OR gestione = 's')
AND N_STRA IS NOT NULL
AND N_STRA > 0
ORDER BY N_STRA
");
foreach ($straordinarie as $stra) {
$numero = (int) $stra['numero'];
$gestioni[] = [ $gestioni[] = [
'anno_gestione' => $year, 'anno_gestione' => $year,
'tipo_gestione' => 'straordinaria', 'tipo_gestione' => 'straordinaria',
@ -150,10 +217,11 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array
/** /**
* Crea o aggiorna gestione * Crea o aggiorna gestione
*/ */
private function createOrUpdateGestione(array $gestioneData): GestioneContabile private function createOrUpdateGestione(array $gestioneData, ?string $yearKey = null): GestioneContabile
{ {
$gestione = GestioneContabile::updateOrCreate([ $gestione = GestioneContabile::updateOrCreate([
'tenant_id' => $this->tenantId, 'tenant_id' => $this->tenantId,
'stabile_id' => $this->stabileId,
'anno_gestione' => $gestioneData['anno_gestione'], 'anno_gestione' => $gestioneData['anno_gestione'],
'tipo_gestione' => $gestioneData['tipo_gestione'], 'tipo_gestione' => $gestioneData['tipo_gestione'],
'numero_straordinaria' => $gestioneData['numero_straordinaria'] ?? null, 'numero_straordinaria' => $gestioneData['numero_straordinaria'] ?? null,
@ -166,7 +234,7 @@ private function createOrUpdateGestione(array $gestioneData): GestioneContabile
]); ]);
// Cache per lookup veloce // Cache per lookup veloce
$year = $gestioneData['anno_gestione']; $year = $yearKey ?? $gestioneData['anno_gestione'];
$tipo = $gestioneData['tipo_gestione']; $tipo = $gestioneData['tipo_gestione'];
if ($tipo === 'straordinaria') { if ($tipo === 'straordinaria') {
@ -189,37 +257,67 @@ private function importOperazioniForGestione(GestioneContabile $gestione, string
$whereClause = $this->buildGestioneWhereClause($gestione); $whereClause = $this->buildGestioneWhereClause($gestione);
$operazioni = $this->queryMdb($mdbPath, " $operazioni = $this->queryMdb($mdbPath, "
SELECT *, SELECT *
IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean, FROM Operazioni
IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean,
IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean
FROM operazioni
WHERE {$whereClause} WHERE {$whereClause}
ORDER BY n_operazione
"); ");
usort($operazioni, function($a, $b) {
$idA = (int) ($a['id_operaz'] ?? 0);
$idB = (int) ($b['id_operaz'] ?? 0);
return $idA <=> $idB;
});
$imported = 0; $imported = 0;
foreach ($operazioni as $op) { foreach ($operazioni as $op) {
try { try {
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? '');
$protocollo = $this->generateProtocollo($gestione->id, $op); $protocollo = $this->generateProtocollo($gestione->id, $op);
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? '');
OperazioneContabile::create([ $competClean = empty($op['compet']) ? 'C' : trim($op['compet']);
'tenant_id' => $this->tenantId, $natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']);
'gestione_id' => $gestione->id, $nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra'];
'numero_operazione' => $op['n_operazione'],
'data_operazione' => $this->parseGesconDate($op['data_oper']), $opId = $op['id_operaz'] ?? $op['id'] ?? null;
'importo' => (float) ($op['euro'] ?? 0), $dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null;
'descrizione' => $op['descrizione'] ?? '', $importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0;
'cod_voce_gescon' => $op['cod_voce'] ?? '', $codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? '';
'cod_beneficiario_gescon' => $op['cod_ben'] ?? '', $codBen = $op['cod_ben'] ?? '';
'compet' => $op['compet_clean'],
'natura2' => $op['natura2_clean'], $isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata';
'n_stra' => (int) $op['n_stra_clean'], $amount = (float) $importo;
'protocollo_numero' => $protocollo['numero'], $dareVal = (float) ($op['importo_spese'] ?? 0);
'protocollo_completo' => $protocollo['completo'], $avereVal = (float) ($op['importo_entrate'] ?? 0);
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'metadati_gescon' => json_encode($op), if ($dareVal == 0 && $avereVal == 0 && $amount != 0) {
if ($isEntrata) {
$avereVal = $amount;
} else {
$dareVal = $amount;
}
}
$contoBancarioId = $this->resolveContoBancarioId($mdbPath, $op['cod_cassa'] ?? null);
DB::table('operazioni_contabili')->insert([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestione->id,
'legacy_id' => $opId,
'descrizione' => $op['note'] ?? $op['descrizione'] ?? $op['natura'] ?? 'Operazione',
'data_operazione' => $this->parseGesconDate($dtSpe),
'compet' => $competClean,
'natura2' => $natura2Clean,
'n_stra' => $nStraClean,
'protocollo_numero' => $protocollo['numero'],
'protocollo_completo' => $protocollo['completo'],
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'dare' => $dareVal,
'avere' => $avereVal,
'conto_contabile' => $codVoce,
'conto_bancario_id' => $contoBancarioId,
'stato_operazione' => 'confermata',
'created_at' => now(),
'updated_at' => now(),
]); ]);
$imported++; $imported++;
@ -396,8 +494,12 @@ private function buildGestioneJoinClause(GestioneContabile $gestione, string $al
/** /**
* Import dati per una specifica gestione/anno * Import dati per una specifica gestione/anno
*/ */
private function importGestioneYear(string $year, array $paths): array public function importGestioneYear(string $year, array $paths): array
{ {
if (! isset($this->gestioni[$year])) {
$this->initGestioniForYear($year, $paths);
}
$results = [ $results = [
'operazioni' => 0, 'operazioni' => 0,
'incassi' => 0, 'incassi' => 0,
@ -421,7 +523,9 @@ private function importGestioneYear(string $year, array $paths): array
} }
// 5. Import incassi estratto conto // 5. Import incassi estratto conto
$results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['singolo_anno']); if (file_exists($paths['generale_stabile'])) {
$results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['generale_stabile']);
}
return $results; return $results;
} }
@ -435,14 +539,16 @@ private function importOperazioni(string $year, string $mdbPath): int
// Query MDB per operazioni // Query MDB per operazioni
$operazioni = $this->queryMdb($mdbPath, " $operazioni = $this->queryMdb($mdbPath, "
SELECT *, SELECT *
IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean, FROM Operazioni
IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean,
IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean
FROM operazioni
ORDER BY n_operazione
"); ");
usort($operazioni, function($a, $b) {
$idA = (int) ($a['id_operaz'] ?? 0);
$idB = (int) ($b['id_operaz'] ?? 0);
return $idA <=> $idB;
});
foreach ($operazioni as $op) { foreach ($operazioni as $op) {
try { try {
// Determina gestione // Determina gestione
@ -452,24 +558,52 @@ private function importOperazioni(string $year, string $mdbPath): int
$protocollo = $this->generateProtocollo($gestioneId, $op); $protocollo = $this->generateProtocollo($gestioneId, $op);
// Snapshot voce spesa // Snapshot voce spesa
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? ''); $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? '');
OperazioneContabile::create([ $competClean = empty($op['compet']) ? 'C' : trim($op['compet']);
'tenant_id' => $this->tenantId, $natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']);
'gestione_id' => $gestioneId, $nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra'];
'numero_operazione' => $op['n_operazione'],
'data_operazione' => $this->parseGesconDate($op['data_oper']), $opId = $op['id_operaz'] ?? $op['id'] ?? null;
'importo' => (float) ($op['euro'] ?? 0), $dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null;
'descrizione' => $op['descrizione'] ?? '', $importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0;
'cod_voce_gescon' => $op['cod_voce'] ?? '', $codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? '';
'cod_beneficiario_gescon' => $op['cod_ben'] ?? '', $codBen = $op['cod_ben'] ?? '';
'compet' => $op['compet_clean'],
'natura2' => $op['natura2_clean'], $isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata';
'n_stra' => (int) $op['n_stra_clean'], $amount = (float) $importo;
'protocollo_numero' => $protocollo['numero'], $dareVal = (float) ($op['importo_spese'] ?? 0);
'protocollo_completo' => $protocollo['completo'], $avereVal = (float) ($op['importo_entrate'] ?? 0);
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'metadati_gescon' => json_encode($op), if ($dareVal == 0 && $avereVal == 0 && $amount != 0) {
if ($isEntrata) {
$avereVal = $amount;
} else {
$dareVal = $amount;
}
}
$contoBancarioId = $this->resolveContoBancarioId($mdbPath, $op['cod_cassa'] ?? null);
DB::table('operazioni_contabili')->insert([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'legacy_id' => $opId,
'descrizione' => $op['note'] ?? $op['descrizione'] ?? $op['natura'] ?? 'Operazione',
'data_operazione' => $this->parseGesconDate($dtSpe),
'compet' => $competClean,
'natura2' => $natura2Clean,
'n_stra' => $nStraClean,
'protocollo_numero' => $protocollo['numero'],
'protocollo_completo' => $protocollo['completo'],
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'dare' => $dareVal,
'avere' => $avereVal,
'conto_contabile' => $codVoce,
'conto_bancario_id' => $contoBancarioId,
'stato_operazione' => 'confermata',
'created_at' => now(),
'updated_at' => now(),
]); ]);
$imported++; $imported++;
@ -486,6 +620,63 @@ private function importOperazioni(string $year, string $mdbPath): int
return $imported; return $imported;
} }
/**
* Import incassi da incassi
*/
private function importIncassi(string $year, string $mdbPath): int
{
$imported = 0;
$incassi = $this->queryMdb($mdbPath, "
SELECT * FROM incassi
");
usort($incassi, function($a, $b) {
$dateA = $a['dt_empag'] ?? '';
$dateB = $b['dt_empag'] ?? '';
return strcmp($dateA, $dateB);
});
foreach ($incassi as $inc) {
try {
$gestioneId = $this->gestioni[$year]['ordinaria']->id;
Incasso::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'condominio_id' => $this->gestioni[$year]['ordinaria']->stabile_id ?? 1,
'conto_bancario_id' => $this->resolveContoBancarioId($mdbPath, $inc['cod_cassa'] ?? null),
'id_incasso_gescon' => $inc['ID_incasso'] ?? null,
'cod_cond_gescon' => $inc['cod_cond'] ?? null,
'cond_inquil' => $inc['cond_inquil'] ?? null,
'n_riferimento' => $inc['n_riferimento'] ?? null,
'anno_rif' => (int) ($inc['anno_rif'] ?? $year),
'n_ricevuta' => $inc['n_ricevuta'] ?? null,
'anno_ricev' => $inc['anno_ricev'] ?? null,
'n_mese' => $inc['n_mese'] ?? null,
'o_r_s' => $inc['o_r_s'] ?? null,
'importo_pagato' => (float) ($inc['importo_pagato'] ?? 0),
'importo_pagato_euro' => (float) ($inc['importo_pagato_euro'] ?? 0),
'dt_empag' => $this->parseGesconDate($inc['dt_empag'] ?? null),
'descrizione' => $inc['descrizione'] ?? null,
'cod_cassa_gescon' => $inc['cod_cassa'] ?? null,
'metadati_gescon' => json_encode($inc),
]);
$imported++;
} catch (\Exception $e) {
Log::error("Error importing incasso", [
'year' => $year,
'incasso' => $inc,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/** /**
* Import ritenute d'acconto da Nettovers_RDA * Import ritenute d'acconto da Nettovers_RDA
*/ */
@ -493,11 +684,15 @@ private function importRitenuteAcconto(string $year, string $mdbPath): int
{ {
$imported = 0; $imported = 0;
if (!$this->mdbTableExists($mdbPath, 'Nettovers_RDA')) {
return 0;
}
// Query per ritenute // Query per ritenute
$ritenute = $this->queryMdb($mdbPath, " $ritenute = $this->queryMdb($mdbPath, "
SELECT n.*, o.data_oper, o.euro as imponibile_operazione SELECT n.*, o.dt_spe, o.importo_euro as imponibile_operazione
FROM Nettovers_RDA n FROM Nettovers_RDA n
LEFT JOIN operazioni o ON n.Rif_RDA = o.n_operazione LEFT JOIN Operazioni o ON n.Rif_RDA = o.id_operaz
WHERE n.Rif_RDA IS NOT NULL WHERE n.Rif_RDA IS NOT NULL
ORDER BY n.Rif_RDA ORDER BY n.Rif_RDA
"); ");
@ -510,7 +705,7 @@ private function importRitenuteAcconto(string $year, string $mdbPath): int
'tenant_id' => $this->tenantId, 'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId, 'gestione_id' => $gestioneId,
'numero_progressivo' => $ra['Rif_RDA'], 'numero_progressivo' => $ra['Rif_RDA'],
'data_competenza' => $this->parseGesconDate($ra['data_oper']), 'data_competenza' => $this->parseGesconDate($ra['dt_spe'] ?? null),
'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0), 'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0),
'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20), 'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20),
'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0), 'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0),
@ -542,23 +737,31 @@ private function importIncassiEstrattoConto(string $year, string $mdbPath): int
$incassiEc = $this->queryMdb($mdbPath, " $incassiEc = $this->queryMdb($mdbPath, "
SELECT * FROM Inc_da_ec SELECT * FROM Inc_da_ec
ORDER BY data_oper
"); ");
usort($incassiEc, function($a, $b) {
$protoA = (int) ($a['protocollo'] ?? 0);
$protoB = (int) ($b['protocollo'] ?? 0);
return $protoA <=> $protoB;
});
foreach ($incassiEc as $inc) { foreach ($incassiEc as $inc) {
try { try {
$gestioneId = $this->gestioni[$year]['ordinaria']->id; $gestioneId = $this->gestioni[$year]['ordinaria']->id;
IncassoEstrattoConto::create([ IncassoEstrattoConto::create([
'tenant_id' => $this->tenantId, 'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId, 'gestione_id' => $gestioneId,
'id_gescon' => $inc['id'] ?? null, 'id_gescon' => $inc['protocollo'] ?? null,
'data_operazione' => $this->parseGesconDate($inc['data_oper']), 'data_operazione' => $this->parseGesconDate($inc['Data_pag'] ?? null),
'data_valuta' => $this->parseGesconDate($inc['data_valuta']), 'data_valuta' => $this->parseGesconDate($inc['Data_pag'] ?? null),
'importo' => (float) ($inc['importo'] ?? 0), 'importo' => (float) ($inc['Importo'] ?? 0),
'descrizione' => $inc['descrizione'] ?? '', 'descrizione' => trim(($inc['Descrizione_Aggiuntiva'] ?? '') . ' ' . ($inc['Nome_condomino'] ?? '')),
'stato' => 'da_riconciliare', 'ordinante' => $inc['Nome_condomino'] ?? null,
'metadati_gescon' => json_encode($inc), 'riferimento_bancario' => $inc['Num_incasso'] ?? null,
'file_origine' => $inc['Nome_file_pdf'] ?? null,
'stato' => 'da_riconciliare',
'metadati_gescon' => json_encode($inc),
]); ]);
$imported++; $imported++;
@ -582,11 +785,21 @@ private function importRateEmesse(string $year, string $mdbPath): int
{ {
$imported = 0; $imported = 0;
// Unifica EMESS_DET, EMESS_DET_2, EMESS_GEN // Unifica emes_det, emes_det_2, emes_gen
$tables = ['EMESS_DET', 'EMESS_DET_2', 'EMESS_GEN']; $tables = ['emes_det', 'emes_det_2', 'emes_gen'];
foreach ($tables as $table) { foreach ($tables as $table) {
$rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table} ORDER BY data_em"); if (!$this->mdbTableExists($mdbPath, $table)) {
continue;
}
$rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table}");
usort($rate, function($a, $b) {
$dateA = $a['data_em'] ?? '';
$dateB = $b['data_em'] ?? '';
return strcmp($dateA, $dateB);
});
foreach ($rate as $rata) { foreach ($rate as $rata) {
try { try {
@ -628,18 +841,20 @@ private function determineGestioneId(string $year, array $operazione): int
private function getOrCreateStraordinaria(string $year, int $nStra): int private function getOrCreateStraordinaria(string $year, int $nStra): int
{ {
$key = "straordinaria_{$nStra}"; $key = "straordinaria_{$nStra}";
$calendarYear = $this->directoryToYearMap[$year] ?? (int) $year;
if (! isset($this->gestioni[$year][$key])) { if (! isset($this->gestioni[$year][$key])) {
$this->gestioni[$year][$key] = GestioneContabile::create([ $this->gestioni[$year][$key] = GestioneContabile::create([
'tenant_id' => $this->tenantId, 'tenant_id' => $this->tenantId,
'anno_gestione' => (int) $year, 'stabile_id' => $this->stabileId,
'anno_gestione' => $calendarYear,
'tipo_gestione' => 'straordinaria', 'tipo_gestione' => 'straordinaria',
'numero_straordinaria' => $nStra, 'numero_straordinaria' => $nStra,
'denominazione' => "Gestione Straordinaria {$year} - {$nStra}", 'denominazione' => "Gestione Straordinaria {$calendarYear} - {$nStra}",
'data_inizio' => "{$year}-01-01", 'data_inizio' => "{$calendarYear}-01-01",
'data_fine' => "{$year}-12-31", 'data_fine' => "{$calendarYear}-12-31",
'stato' => 'aperta', 'stato' => 'aperta',
'protocollo_prefix' => "S{$year}-{$nStra}", 'protocollo_prefix' => "S{$calendarYear}-{$nStra}",
]); ]);
} }
@ -651,11 +866,13 @@ private function getOrCreateStraordinaria(string $year, int $nStra): int
*/ */
private function queryMdb(string $mdbPath, string $query): array private function queryMdb(string $mdbPath, string $query): array
{ {
$query = preg_replace('/\s+/', ' ', trim($query));
// Usa mdb-tools per query diretta // Usa mdb-tools per query diretta
$command = sprintf( $command = sprintf(
'mdb-sql "%s" <<< "%s"', 'echo %s | mdb-sql -P -F -d %s %s',
escapeshellarg($mdbPath), escapeshellarg($query),
escapeshellarg($query) escapeshellarg("\t"),
escapeshellarg($mdbPath)
); );
$output = shell_exec($command); $output = shell_exec($command);
@ -664,6 +881,55 @@ private function queryMdb(string $mdbPath, string $query): array
return $this->parseMdbOutput($output); return $this->parseMdbOutput($output);
} }
private function mdbTableExists(string $mdbPath, string $tableName): bool
{
if (!file_exists($mdbPath)) {
return false;
}
$output = shell_exec("mdb-tables -1 " . escapeshellarg($mdbPath) . " 2>/dev/null");
if (!$output) {
return false;
}
$tables = array_map('strtolower', array_map('trim', explode("\n", trim($output))));
return in_array(strtolower($tableName), $tables, true);
}
private function resolveContoBancarioId(string $mdbPath, ?string $codCassa): int
{
$stabileCode = '0021'; // Default fallback
if (preg_match('/legacy\/([^\/]+)/', str_replace('\\', '/', $mdbPath), $matches)) {
$stabileCode = $matches[1];
}
$cod = strtoupper(trim($codCassa ?? ''));
if (empty($cod)) {
$cod = 'CON'; // Default fallback to cash cassa
}
// Try to match by code: stabileCode-cod
$conto = DB::table('conti_bancari')
->where('tenant_id', $this->tenantId)
->where('codice', "{$stabileCode}-{$cod}")
->first();
if ($conto) {
return (int) $conto->id;
}
// Try fallback to any account for this stable prefix
$conto = DB::table('conti_bancari')
->where('tenant_id', $this->tenantId)
->where('codice', 'like', "{$stabileCode}-%")
->first();
if ($conto) {
return (int) $conto->id;
}
// Final fallback to 1
return 1;
}
/** /**
* Parser output mdb-sql in array * Parser output mdb-sql in array
*/ */
@ -675,16 +941,35 @@ private function parseMdbOutput(string $output): array
} }
$headers = array_map('trim', explode("\t", array_shift($lines))); $headers = array_map('trim', explode("\t", array_shift($lines)));
$headerCount = count($headers);
$data = []; $data = [];
$currentValues = [];
foreach ($lines as $line) { foreach ($lines as $line) {
if (empty(trim($line))) { if ($line === '' && empty($currentValues)) {
continue; continue;
} }
$values = explode("\t", $line); $values = explode("\t", $line);
$row = array_combine($headers, $values); if (empty($currentValues)) {
$data[] = $row; $currentValues = $values;
} else {
$lastIdx = count($currentValues) - 1;
$currentValues[$lastIdx] .= "\n" . array_shift($values);
$currentValues = array_merge($currentValues, $values);
}
if (count($currentValues) >= $headerCount) {
$rowValues = array_slice($currentValues, 0, $headerCount);
$leftover = array_slice($currentValues, $headerCount);
try {
$data[] = array_combine($headers, $rowValues);
} catch (\Throwable $e) {
// Ignora o logga errori per evitare crash
}
$currentValues = $leftover;
}
} }
return $data; return $data;
@ -696,11 +981,14 @@ private function parseMdbOutput(string $output): array
private function generateProtocollo(int $gestioneId, array $operazione): array private function generateProtocollo(int $gestioneId, array $operazione): array
{ {
$gestione = GestioneContabile::find($gestioneId); $gestione = GestioneContabile::find($gestioneId);
$numero = $gestione->getNextProtocolNumber();
$numero = (isset($operazione['n_spe']) && is_numeric($operazione['n_spe']) && (int) $operazione['n_spe'] > 0)
? (int) $operazione['n_spe']
: (DB::table('operazioni_contabili')->where('gestione_id', $gestioneId)->max('protocollo_numero') ?? 0) + 1;
return [ return [
'numero' => $numero, 'numero' => $numero,
'completo' => $gestione->protocollo_prefix . '-' . str_pad($numero, 4, '0', STR_PAD_LEFT), 'completo' => ($gestione->protocollo_prefix ?? 'O') . '-' . str_pad($numero, 4, '0', STR_PAD_LEFT),
]; ];
} }

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('piano_rateizzazione', function (Blueprint $table) {
$table->unsignedBigInteger('ripartizione_spese_id')->nullable()->change();
$table->unsignedBigInteger('creato_da')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('piano_rateizzazione', function (Blueprint $table) {
$table->unsignedBigInteger('ripartizione_spese_id')->nullable(false)->change();
$table->unsignedBigInteger('creato_da')->nullable(false)->change();
});
}
};

View File

@ -0,0 +1,89 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$conn = 'gescon_import';
// 1. Aggiunta colonne subentro a condomin
if (Schema::connection($conn)->hasTable('condomin')) {
Schema::connection($conn)->table('condomin', function (Blueprint $table) use ($conn): void {
if (! Schema::connection($conn)->hasColumn('condomin', 'subentro_prima_cera')) {
$table->text('subentro_prima_cera')->nullable();
}
if (! Schema::connection($conn)->hasColumn('condomin', 'subentro_adesso_ce')) {
$table->text('subentro_adesso_ce')->nullable();
}
if (! Schema::connection($conn)->hasColumn('condomin', 'subentrato_dal')) {
$table->text('subentrato_dal')->nullable();
}
if (! Schema::connection($conn)->hasColumn('condomin', 'attivo_fino_al')) {
$table->text('attivo_fino_al')->nullable();
}
});
}
// 2. Tabella rate_emissioni
if (! Schema::connection($conn)->hasTable('rate_emissioni')) {
Schema::connection($conn)->create('rate_emissioni', function (Blueprint $table): void {
$table->bigIncrements('id');
$table->string('cod_stabile', 10);
$table->unsignedInteger('numero_emissione');
$table->string('anno_emissione', 20)->nullable();
$table->date('data_emissione')->nullable();
$table->date('data_scadenza')->nullable();
$table->string('gestione_tipo', 5)->nullable();
$table->string('gestione_periodo', 50)->nullable();
$table->string('descrizione', 255)->nullable();
$table->string('nota_avvisi', 255)->nullable();
$table->string('nota_ricevute', 255)->nullable();
$table->string('nota_ccp', 255)->nullable();
$table->string('stato_stampa', 20)->nullable();
$table->string('provvisorio_definitivo', 20)->nullable();
$table->json('payload')->nullable();
$table->timestamps();
$table->unique(['cod_stabile', 'numero_emissione']);
});
}
// 3. Tabella rate_emissioni_dettaglio
if (! Schema::connection($conn)->hasTable('rate_emissioni_dettaglio')) {
Schema::connection($conn)->create('rate_emissioni_dettaglio', function (Blueprint $table): void {
$table->bigIncrements('id');
$table->string('cod_stabile', 10);
$table->unsignedInteger('numero_emissione');
$table->string('anno_emissione', 20)->nullable();
$table->string('cod_cond', 20)->nullable();
$table->string('tipo_soggetto', 1)->nullable();
$table->unsignedInteger('numero_mese')->nullable();
$table->string('tipo_quota', 2)->nullable();
$table->decimal('importo_dovuto', 14, 4)->nullable();
$table->decimal('importo_dovuto_euro', 14, 4)->nullable();
$table->date('data_emissione')->nullable();
$table->string('descrizione', 255)->nullable();
$table->unsignedInteger('numero_ricevuta')->nullable();
$table->string('anno_gestione', 20)->nullable();
$table->unsignedInteger('numero_straordinaria')->nullable();
$table->decimal('gia_pagato', 14, 4)->nullable();
$table->decimal('residuo_emesso', 14, 4)->nullable();
$table->decimal('compensato', 14, 4)->nullable();
$table->string('cond_inq', 5)->nullable();
$table->string('raggruppamento', 20)->nullable();
$table->json('payload')->nullable();
$table->timestamps();
$table->index(['cod_stabile', 'numero_emissione']);
$table->index(['cod_stabile', 'cod_cond']);
});
}
}
public function down(): void
{
// Nessun down distruttivo per staging in produzione/sviluppo
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('incassi_pagamenti', function (Blueprint $table) {
if (!Schema::hasColumn('incassi_pagamenti', 'rata_id')) {
$table->unsignedBigInteger('rata_id')->nullable()->after('anno_gestione_id');
}
if (!Schema::hasColumn('incassi_pagamenti', 'movimento_banca_id')) {
$table->unsignedBigInteger('movimento_banca_id')->nullable()->after('rata_id');
}
});
}
public function down(): void
{
Schema::table('incassi_pagamenti', function (Blueprint $table) {
if (Schema::hasColumn('incassi_pagamenti', 'rata_id')) {
$table->dropColumn('rata_id');
}
if (Schema::hasColumn('incassi_pagamenti', 'movimento_banca_id')) {
$table->dropColumn('movimento_banca_id');
}
});
}
};

View File

@ -0,0 +1,75 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::connection('gescon_import')->hasTable('operazioni')) {
Schema::connection('gescon_import')->create('operazioni', function (Blueprint $table) {
$table->bigIncrements('id_operaz');
$table->integer('n_spe')->nullable();
$table->date('dt_spe')->nullable();
$table->string('cod_spe', 20)->nullable();
$table->string('tabella')->nullable();
$table->decimal('importo', 12, 2)->nullable();
$table->decimal('importo_euro', 12, 2)->nullable();
$table->string('cod_ben')->nullable();
$table->string('benef')->nullable();
$table->string('benef2')->nullable();
$table->string('compet')->nullable();
$table->text('note')->nullable();
$table->integer('n_stra')->nullable();
$table->string('cod_cassa')->nullable();
$table->string('natura')->nullable();
$table->string('natura2')->nullable();
$table->string('cod_for')->nullable();
$table->string('num_fat')->nullable();
$table->date('dt_fat')->nullable();
$table->integer('anno')->nullable();
$table->decimal('imp_calcolato', 12, 2)->nullable();
$table->decimal('imp_calcolato_euro', 12, 2)->nullable();
$table->decimal('imp_2', 12, 2)->nullable();
$table->decimal('imp_2_euro', 12, 2)->nullable();
$table->decimal('importo_spese', 12, 2)->nullable();
$table->decimal('importo_entrate', 12, 2)->nullable();
$table->decimal('importo_crediti', 12, 2)->nullable();
$table->decimal('importo_debiti', 12, 2)->nullable();
$table->boolean('incluso')->default(false);
$table->string('nord')->nullable();
$table->boolean('temporaneo')->default(false);
$table->string('d36_41')->nullable();
$table->decimal('netto_vers_rda', 12, 2)->nullable();
$table->string('rif_rda')->nullable();
$table->decimal('importo_euro_ac', 12, 2)->nullable();
$table->decimal('importo_euro_770', 12, 2)->nullable();
$table->string('file_bonifico_telematico')->nullable();
$table->decimal('detraz_36', 12, 2)->nullable();
$table->string('etic_axivar')->nullable();
$table->boolean('in_ac')->default(false);
$table->string('gestione')->nullable();
$table->string('proviene_ors')->nullable();
$table->integer('proviene_n_stra')->nullable();
$table->string('proviene_eserc')->nullable();
$table->string('rif_ft_amm')->nullable();
$table->string('fatt_amm_fc')->nullable();
$table->string('df_tipo_lavori')->nullable();
$table->string('fe_uid')->nullable();
$table->string('cod_stabile', 10)->nullable();
$table->string('legacy_year', 20)->nullable();
$table->string('slice_id', 100)->nullable();
$table->string('legacy_file', 255)->nullable();
$table->string('row_hash', 64)->nullable();
$table->timestamps();
});
}
}
public function down(): void
{
Schema::connection('gescon_import')->dropIfExists('operazioni');
}
};

View File

@ -2,14 +2,33 @@
$gestioniTab = request()->query('gestioni_tab', 'gestioni'); $gestioniTab = request()->query('gestioni_tab', 'gestioni');
$gestioniTab = in_array($gestioniTab, ['gestioni', 'periodi', 'straordinarie', 'archivio'], true) ? $gestioniTab : 'gestioni'; $gestioniTab = in_array($gestioniTab, ['gestioni', 'periodi', 'straordinarie', 'archivio'], true) ? $gestioniTab : 'gestioni';
$baseUrl = \App\Filament\Pages\Condomini\StabilePage::getUrl(panel: 'admin-filament'); $baseUrl = \App\Filament\Pages\Condomini\StabilePage::getUrl(panel: 'admin-filament');
$mostraRiscaldamento = (bool) data_get($stabile->configurazione_avanzata, 'mostra_riscaldamento', false);
$mesi = [
1 => 'Gen', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mag', 6 => 'Giu',
7 => 'Lug', 8 => 'Ago', 9 => 'Set', 10 => 'Ott', 11 => 'Nov', 12 => 'Dic',
];
$gestioneSelezionata = $this->gestioneSelezionataId $gestioneSelezionata = $this->gestioneSelezionataId
? \App\Models\GestioneContabile::query()->find($this->gestioneSelezionataId) ? \App\Models\GestioneContabile::query()->find($this->gestioneSelezionataId)
: ($this->gestioneOrdinariaId ? \App\Models\GestioneContabile::query()->find($this->gestioneOrdinariaId) : null); : ($this->gestioneOrdinariaId ? \App\Models\GestioneContabile::query()->find($this->gestioneOrdinariaId) : null);
$mesi = [];
if ($gestioneSelezionata && $gestioneSelezionata->data_inizio && $gestioneSelezionata->data_fine) {
$start = \Carbon\Carbon::parse($gestioneSelezionata->data_inizio);
$end = \Carbon\Carbon::parse($gestioneSelezionata->data_fine);
$curr = $start->copy()->startOfMonth();
$limit = 0;
while ($curr->lte($end) && $limit < 24) {
$mVal = (int) $curr->month;
$mName = match ($mVal) {
1 => 'Gen', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mag', 6 => 'Giu',
7 => 'Lug', 8 => 'Ago', 9 => 'Set', 10 => 'Ott', 11 => 'Nov', 12 => 'Dic',
};
$mYear = $curr->year;
$mesi[$mVal] = "{$mName} {$mYear}";
$curr->addMonth();
$limit++;
}
}
if (empty($mesi)) {
$mesi = [
1 => 'Gen', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mag', 6 => 'Giu',
7 => 'Lug', 8 => 'Ago', 9 => 'Set', 10 => 'Ott', 11 => 'Nov', 12 => 'Dic',
];
}
$gestioneTipo = $gestioneSelezionata?->tipo_gestione; $gestioneTipo = $gestioneSelezionata?->tipo_gestione;
$checklistItems = \App\Models\ChecklistFineAnnoItem::query() $checklistItems = \App\Models\ChecklistFineAnnoItem::query()
->where('is_active', true) ->where('is_active', true)

View File

@ -65,6 +65,11 @@ class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'tariffe' ? 'bo
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'servizi' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}"> class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'servizi' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
Servizi / Utenze (tabella) Servizi / Utenze (tabella)
</button> </button>
<button type="button"
wire:click="setAcquaTab('pagamenti_cbill')"
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'pagamenti_cbill' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
Pagamenti CBILL
</button>
</nav> </nav>
</div> </div>
@ -363,6 +368,8 @@ class="rounded border-cyan-300 text-cyan-600 focus:ring-cyan-500">
</div> </div>
</div> </div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-4 dark:border-indigo-700/50 dark:bg-indigo-900/10"> <div class="rounded-lg border border-indigo-200 bg-indigo-50 p-4 dark:border-indigo-700/50 dark:bg-indigo-900/10">
<div class="mb-2 text-sm font-semibold text-indigo-900 dark:text-indigo-200">Archivio legacy operazioni AC1 + AC2 (collegamento fatture)</div> <div class="mb-2 text-sm font-semibold text-indigo-900 dark:text-indigo-200">Archivio legacy operazioni AC1 + AC2 (collegamento fatture)</div>
@if(!empty($legacyOpsSummary['scope_note'])) @if(!empty($legacyOpsSummary['scope_note']))
@ -913,6 +920,72 @@ class="inline-flex items-center rounded-md border border-cyan-300 bg-white px-3
{{ $this->table }} {{ $this->table }}
</div> </div>
@endif @endif
@if($acquaTab === 'pagamenti_cbill')
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900 shadow-sm">
<div class="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100">Pagamenti abbinati tramite codice CBILL</div>
<div class="mb-3 text-xs text-gray-500">I seguenti movimenti bancari sono stati associati alle fatture elettroniche dello stabile tramite il codice CBILL estratto.</div>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-800 text-xs font-semibold text-gray-500 uppercase tracking-wider">
<th class="px-3 py-2 text-left">Data movimento</th>
<th class="px-3 py-2 text-left font-mono">Codice CBILL</th>
<th class="px-3 py-2 text-left">Fornitore</th>
<th class="px-3 py-2 text-left">Fattura Elettronica di origine</th>
<th class="px-3 py-2 text-right">Totale fattura </th>
<th class="px-3 py-2 text-left">Descrizione movimento bancario</th>
<th class="px-3 py-2 text-right">Importo pagato </th>
<th class="px-3 py-2 text-center">Azioni</th>
</tr>
</thead>
<tbody>
@forelse($this->acquaCbillPagamenti as $p)
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50/50 text-xs text-gray-700 dark:text-gray-300">
<td class="px-3 py-2 whitespace-nowrap">{{ \Carbon\Carbon::parse($p['data'])->format('d/m/Y') }}</td>
<td class="px-3 py-2 font-mono text-[11px] text-gray-900 dark:text-gray-100">{{ $p['cbill'] }}</td>
<td class="px-3 py-2">{{ $p['fornitore'] }}</td>
<td class="px-3 py-2">
@if($p['fattura_numero'] !== '—')
<span class="font-semibold text-gray-900 dark:text-gray-100">N. {{ $p['fattura_numero'] }}</span> del {{ \Carbon\Carbon::parse($p['fattura_data'])->format('d/m/Y') }}
@else
@endif
</td>
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ $p['fattura_totale'] > 0 ? number_format((float) $p['fattura_totale'], 2, ',', '.') : '—' }}</td>
<td class="px-3 py-2 max-w-xs truncate" title="{{ $p['descrizione'] }}">{{ $p['descrizione'] }}</td>
<td class="px-3 py-2 text-right font-semibold text-danger-600 tabular-nums">{{ number_format((float) $p['importo'], 2, ',', '.') }}</td>
<td class="px-3 py-2 text-center whitespace-nowrap space-x-2">
@if(!empty($p['fattura_id']))
<a href="/admin-filament/contabilita/fatture-ricevute/{{ $p['fattura_id'] }}"
target="_blank"
class="inline-flex items-center gap-1 rounded bg-sky-50 px-2.5 py-1 text-xs font-semibold text-sky-700 hover:bg-sky-100 dark:bg-sky-950 dark:text-sky-300">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L17 17.25m-1.769-1.769a3.75 3.75 0 11-5.303-5.303 3.75 3.75 0 015.303 5.303z" />
</svg>
Vedi FE
</a>
@endif
<a href="/admin-filament/contabilita/casse-banche/movimenti?tab=movimenti"
target="_blank"
class="inline-flex items-center gap-1 rounded bg-teal-50 px-2.5 py-1 text-xs font-semibold text-teal-700 hover:bg-teal-100 dark:bg-teal-950 dark:text-teal-300">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m-9 6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m-12-6a3 3 0 013-3h15a3 3 0 013 3v6a3 3 0 01-3 3H3.75a3 3 0 01-3-3v-6z" />
</svg>
Vedi Movimenti
</a>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-3 py-4 text-center text-gray-500 dark:text-gray-400">Nessun pagamento trovato abbinato tramite codice CBILL.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endif
</div> </div>
<!-- Modal per invio email ripartizione --> <!-- Modal per invio email ripartizione -->

View File

@ -84,6 +84,7 @@
<x-filament::tabs.item :active="$this->hubTab === 'movimenti'" wire:click="goToHubTab('movimenti')">Movimenti</x-filament::tabs.item> <x-filament::tabs.item :active="$this->hubTab === 'movimenti'" wire:click="goToHubTab('movimenti')">Movimenti</x-filament::tabs.item>
<x-filament::tabs.item :active="$this->hubTab === 'struttura'" wire:click="goToHubTab('struttura')">Struttura e gestioni</x-filament::tabs.item> <x-filament::tabs.item :active="$this->hubTab === 'struttura'" wire:click="goToHubTab('struttura')">Struttura e gestioni</x-filament::tabs.item>
<x-filament::tabs.item :active="$this->hubTab === 'riconciliazione'" wire:click="goToHubTab('riconciliazione')">Riconciliazione</x-filament::tabs.item> <x-filament::tabs.item :active="$this->hubTab === 'riconciliazione'" wire:click="goToHubTab('riconciliazione')">Riconciliazione</x-filament::tabs.item>
<x-filament::tabs.item :active="$this->hubTab === 'associazione_causali'" wire:click="goToHubTab('associazione_causali')">Associazione Causali</x-filament::tabs.item>
</x-filament::tabs> </x-filament::tabs>
@if($this->hubTab === 'conti') @if($this->hubTab === 'conti')
@ -622,6 +623,90 @@
{{ $this->table }} {{ $this->table }}
</div> </div>
</x-filament::section> </x-filament::section>
@elseif($this->hubTab === 'associazione_causali')
<x-filament::section>
<x-slot name="heading">Associazione Causali e Regole Bancarie</x-slot>
<x-slot name="description">Configura i conti Dare e Avere per ciascun codice causale rilevato nei movimenti banca.</x-slot>
@php($assocRows = $this->getCausaliAssociazioneRows())
<div class="mt-4 overflow-x-auto rounded-lg border">
<table class="w-full text-left text-sm divide-y divide-gray-200 dark:divide-gray-800">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Causale bancaria</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Descrizione rilevata</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300 text-center">Movimenti</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Conto Dare (Entrate)</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Conto Avere (Uscite)</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300 text-center">Stato regola</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300 text-right">Azioni</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-800 bg-white dark:bg-gray-900">
@forelse($assocRows as $row)
<tr>
<td class="px-4 py-3 font-mono font-bold text-gray-900 dark:text-gray-100">
{{ $row['causale'] }}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{{ $row['descrizione'] }}
</td>
<td class="px-4 py-3 text-center font-semibold">
{{ $row['count'] }}
</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">
@if($row['conto_dare'])
<span class="inline-flex items-center rounded-md bg-blue-50 dark:bg-blue-900/30 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800">
{{ $row['conto_dare'] }}
</span>
@else
<span class="text-xs text-gray-400"></span>
@endif
</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">
@if($row['conto_avere'])
<span class="inline-flex items-center rounded-md bg-blue-50 dark:bg-blue-900/30 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800">
{{ $row['conto_avere'] }}
</span>
@else
<span class="text-xs text-gray-400"></span>
@endif
</td>
<td class="px-4 py-3 text-center">
@if($row['attiva'])
<span class="inline-flex items-center gap-1 rounded-full bg-emerald-100 dark:bg-emerald-950/30 px-2.5 py-0.5 text-xs font-semibold text-emerald-800 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-900">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
Attiva
</span>
@else
<span class="inline-flex items-center gap-1 rounded-full bg-amber-100 dark:bg-amber-950/30 px-2.5 py-0.5 text-xs font-semibold text-amber-800 dark:text-amber-300 border border-amber-200 dark:border-amber-900">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
Configurazione mancante
</span>
@endif
</td>
<td class="px-4 py-3 text-right">
<x-filament::button
size="sm"
color="{{ $row['attiva'] ? 'gray' : 'primary' }}"
wire:click="startConfiguringRegolaCausale('{{ $row['causale'] }}')"
>
{{ $row['attiva'] ? 'Modifica regola' : 'Configura regola' }}
</x-filament::button>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-6 text-center text-gray-500">
Nessun movimento banca importato con codici causale rilevati.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</x-filament::section>
@endif @endif
@endif @endif
</div> </div>

View File

@ -1,13 +1,30 @@
@php @php
$record = $record ?? null; $record = $record ?? null;
$details = $record ? $record->dettaglio_estrato : [];
@endphp @endphp
@if(!$record) @if(!$record)
<div class="text-sm text-gray-600">Nessun movimento selezionato.</div> <div class="text-sm text-gray-600 dark:text-gray-400 p-4 text-center">Nessun movimento selezionato.</div>
@else @else
<div class="space-y-3"> <div class="space-y-4">
<div class="flex items-center justify-between"> <!-- Header con Azioni di Navigazione e Codici Chiave -->
<div class="text-xs text-gray-500">ID #{{ $record->id }}</div> <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b pb-3 dark:border-gray-700">
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-slate-100 text-slate-800 border border-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:border-slate-700">
ID #{{ $record->id }}
</span>
@if(!empty($details['abi']))
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-blue-50 text-blue-700 border border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800">
ABI: {{ $details['abi'] }}
</span>
@endif
@if(!empty($details['cbill_code']))
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-amber-50 text-amber-700 border border-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-800">
CBILL: {{ $details['cbill_code'] }}
</span>
@endif
</div>
<div class="flex gap-2"> <div class="flex gap-2">
@if(!empty($prevId)) @if(!empty($prevId))
<x-filament::button size="xs" color="gray" wire:click="setDetailMovement({{ (int) $prevId }})"> Prec</x-filament::button> <x-filament::button size="xs" color="gray" wire:click="setDetailMovement({{ (int) $prevId }})"> Prec</x-filament::button>
@ -18,76 +35,83 @@
</div> </div>
</div> </div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2"> <!-- Sezione Dettagli Principali (Grid) -->
<div class="rounded-lg border bg-white p-3 text-xs"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-gray-500">Data</div> <!-- Colonna 1: Date ed Importo -->
<div class="font-semibold">{{ optional($record->data)->format('d/m/Y') ?: '—' }}</div> <div class="space-y-3 bg-slate-50 dark:bg-gray-800/40 p-4 rounded-xl border border-slate-100 dark:border-gray-700">
</div> <div>
<div class="rounded-lg border bg-white p-3 text-xs"> <span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Importo</span>
<div class="text-gray-500">Valuta</div> <span class="text-xl font-bold text-emerald-600 dark:text-emerald-400">
<div class="font-semibold">{{ optional($record->valuta)->format('d/m/Y') ?: '—' }}</div> {{ number_format((float) $record->importo, 2, ',', '.') }}
</div> </span>
<div class="rounded-lg border bg-white p-3 text-xs"> </div>
<div class="text-gray-500">Importo</div>
<div class="font-semibold"> {{ number_format((float) $record->importo, 2, ',', '.') }}</div> <div class="grid grid-cols-2 gap-2 pt-2 border-t border-slate-200/60 dark:border-gray-700/60">
</div> <div>
<div class="rounded-lg border bg-white p-3 text-xs"> <span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Data Operazione</span>
<div class="text-gray-500">Causale</div> <span class="text-xs font-semibold text-slate-700 dark:text-slate-300">
<div class="font-semibold">{{ $record->causale ?? '—' }}</div> {{ optional($record->data)->format('d/m/Y') ?: '—' }}
</div> </span>
<div class="rounded-lg border bg-white p-3 text-xs"> </div>
<div class="text-gray-500">Descrizione</div> <div>
<div class="font-semibold">{{ $record->descrizione ?? '—' }}</div> <span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Data Valuta</span>
</div> <span class="text-xs font-semibold text-slate-700 dark:text-slate-300">
<div class="rounded-lg border bg-white p-3 text-xs"> {{ optional($record->valuta)->format('d/m/Y') ?: '—' }}
<div class="text-gray-500">Descrizione estesa</div> </span>
<div class="font-semibold">{{ $record->descrizione_estesa_pulita ?? $record->descrizione_estesa ?? '—' }}</div> </div>
</div> </div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Saldo progressivo</div> <div class="pt-2 border-t border-slate-200/60 dark:border-gray-700/60">
<div class="font-semibold"> <span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Causale</span>
@if(isset($record->saldo_progressivo) && is_numeric($record->saldo_progressivo)) <span class="text-xs font-semibold text-slate-700 dark:text-slate-300">
{{ number_format((float) $record->saldo_progressivo, 2, ',', '.') }} {{ $record->causale ?? '—' }}
@else </span>
@endif
</div> </div>
</div> </div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">File sorgente</div> <!-- Colonna 2 e 3: Messaggi ed Estrazioni -->
<div class="font-semibold">{{ $record->source_file ?? '—' }}</div> <div class="md:col-span-2 space-y-3 bg-white dark:bg-gray-800/20 p-4 rounded-xl border border-slate-100 dark:border-gray-700">
</div> <!-- Messaggio Banca (Standard) -->
<div class="rounded-lg border bg-white p-3 text-xs"> <div>
<div class="text-gray-500">Disp</div> <span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Messaggio Standard Banca</span>
<div class="font-semibold">{{ is_array($record->match_data ?? null) ? ($record->match_data['cod_disp'] ?? '—') : '—' }}</div> <div class="text-xs font-semibold text-slate-500 dark:text-slate-400 bg-slate-50/50 dark:bg-gray-800/40 p-2.5 rounded-lg border border-slate-100 dark:border-gray-700 mt-1 font-mono">
</div> {{ $details['messaggio_banca'] }}
<div class="rounded-lg border bg-white p-3 text-xs"> </div>
<div class="text-gray-500">Cash</div> </div>
<div class="font-semibold">{{ is_array($record->match_data ?? null) ? ($record->match_data['cash'] ?? '—') : '—' }}</div>
</div> <!-- Messaggio Cliente (Dati Riferimento) -->
<div class="rounded-lg border bg-white p-3 text-xs"> <div>
<div class="text-gray-500">Mittente</div> <span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Riferimento / Dettaglio Cliente</span>
<div class="font-semibold">{{ is_array($record->match_data ?? null) ? ($record->match_data['mittente'] ?? '—') : '—' }}</div> <div class="text-sm font-bold text-slate-800 dark:text-slate-200 bg-blue-50/20 dark:bg-blue-900/10 p-2.5 rounded-lg border border-blue-100/30 dark:border-blue-900/30 mt-1">
</div> {{ $details['messaggio_cliente'] }}
<div class="rounded-lg border bg-white p-3 text-xs md:col-span-2"> </div>
<div class="text-gray-500">Riga raw importata</div> </div>
<div class="font-semibold break-words">{{ $record->raw_line ?? '—' }}</div>
<!-- Commissioni / Spese -->
<div>
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Commissioni, Spese e Codice TRN</span>
<div class="text-xs font-medium text-slate-600 dark:text-slate-400 mt-1 bg-slate-50/50 dark:bg-gray-800/40 p-2.5 rounded-lg border border-slate-100 dark:border-gray-700 font-mono">
{{ $details['commissioni_spese'] }}
</div>
</div>
</div> </div>
</div> </div>
@php <!-- Dati Aggiuntivi / Raw Line -->
$match = $record->match_data; <div class="bg-slate-50/50 dark:bg-gray-800/10 p-3 rounded-lg border border-slate-100 dark:border-gray-700/60 text-xs">
@endphp <details class="group">
@if(is_array($match) && !empty($match)) <summary class="flex justify-between items-center font-semibold text-slate-500 dark:text-slate-400 cursor-pointer select-none">
<div class="rounded-lg border bg-white p-3 text-xs"> <span>Visualizza Tracciato Originale (Raw Line)</span>
<div class="text-gray-500 mb-2">Dati estratti</div> <span class="transition group-open:rotate-180">
<ul class="list-disc pl-4 space-y-1"> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@foreach($match as $k => $v) <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
<li><span class="font-semibold">{{ $k }}:</span> {{ is_scalar($v) ? $v : json_encode($v) }}</li> </svg>
@endforeach </span>
</ul> </summary>
</div> <div class="mt-2.5 pt-2 border-t border-slate-200/60 dark:border-gray-700/60 text-slate-600 dark:text-slate-400 font-mono break-all leading-relaxed">
@endif {{ $record->raw_line ?? '—' }}
</div>
</details>
</div>
</div> </div>
@endif @endif

View File

@ -8,14 +8,55 @@
]" ]"
/> />
<x-filament::section> <div class="grid grid-cols-1 gap-4 lg:grid-cols-12">
<form wire:submit.prevent="submit" class="space-y-4"> <div class="lg:col-span-9 space-y-4">
{{ $this->form }} <form wire:submit.prevent="submit" id="prima-nota-form" class="space-y-4">
<div class="flex items-center gap-3"> {{ $this->form }}
<x-filament::button type="submit" icon="heroicon-o-check">Salva</x-filament::button> </form>
<x-filament::button color="gray" href="{{ \App\Filament\Pages\Contabilita\PrimaNotaArchivio::getUrl(panel: 'admin-filament') }}" tag="a">Annulla</x-filament::button>
</div> @php($hdr = $this->getHeaderSummary())
</form> @php($delta = (float) ($hdr['delta'] ?? 0))
</x-filament::section> @php($isBilanciata = (bool) ($hdr['is_bilanciata'] ?? false))
<x-filament::section>
<x-slot name="heading">Totali e Quadratura</x-slot>
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
<div class="rounded-lg border bg-gray-50 p-3">
<div class="text-xs text-gray-500">Totale Dare</div>
<div class="mt-1 text-lg font-semibold"> {{ number_format((float) ($hdr['totale_dare'] ?? 0), 2, ',', '.') }}</div>
</div>
<div class="rounded-lg border bg-gray-50 p-3">
<div class="text-xs text-gray-500">Totale Avere</div>
<div class="mt-1 text-lg font-semibold"> {{ number_format((float) ($hdr['totale_avere'] ?? 0), 2, ',', '.') }}</div>
</div>
<div class="rounded-lg border p-3 {{ $isBilanciata ? 'bg-success-50 border-success-200' : 'bg-danger-50 border-danger-200' }}">
<div class="text-xs text-gray-500">Quadratura (delta)</div>
<div class="mt-1 text-lg font-semibold {{ $isBilanciata ? 'text-success-700' : 'text-danger-700' }}"> {{ number_format($delta, 2, ',', '.') }}</div>
<div class="text-xs text-gray-500">
@if($isBilanciata)
Registrazione bilanciata
@else
Sbilancio: {{ number_format(abs($delta), 2, ',', '.') }}
@endif
</div>
</div>
</div>
</x-filament::section>
</div>
<div class="lg:col-span-3 space-y-4 lg:sticky lg:top-4 lg:self-start">
<x-filament::section>
<x-slot name="heading">Funzioni base</x-slot>
<div class="space-y-2">
<x-filament::button form="prima-nota-form" type="submit" color="primary" class="w-full" icon="heroicon-o-check" :disabled="!$isBilanciata">
Memorizza
</x-filament::button>
<x-filament::button color="gray" tag="a" href="{{ \App\Filament\Pages\Contabilita\PrimaNotaArchivio::getUrl(panel: 'admin-filament') }}" class="w-full">
Annulla
</x-filament::button>
</div>
</x-filament::section>
</div>
</div>
</div> </div>
</x-filament-panels::page> </x-filament-panels::page>

View File

@ -6,6 +6,28 @@
</div> </div>
<div class="bg-warning-50 px-4 py-4 space-y-2"> <div class="bg-warning-50 px-4 py-4 space-y-2">
@if(($this->tipoGestioneTab ?? 'ordinaria') === 'acqua')
<div class="mb-4 p-4 rounded-lg bg-blue-50 border border-blue-200 flex flex-col md:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-3">
<svg class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 111.086 1.086L13 13.75l.04.02a.75.75 0 11-1.08 1.08l-.71-.71a.75.75 0 010-1.06l.71-.71z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v.008H12v-.008zm0-6.75C6.885 2.25 2.25 6.885 2.25 12s4.635 9.75 9.75 9.75 9.75-4.635 9.75-9.75S17.115 2.25 12 2.25z" />
</svg>
<div>
<div class="text-sm font-semibold text-blue-900">Configurazione e Letture Acqua</div>
<div class="text-xs text-blue-700">Le tariffe, i consumi e le letture dell'acqua sono gestite nella sezione Servizi e Utenze dello Stabile.</div>
</div>
</div>
<x-filament::button
color="info"
tag="a"
href="/admin-filament/condomini/servizi-utenze"
icon="heroicon-m-arrow-right"
>
Vai a Servizi / Utenze Acqua
</x-filament::button>
</div>
@endif
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
@php @php
$tabs = [ $tabs = [