diff --git a/app/Console/Commands/GesconSyncOperazioniPrimaNota.php b/app/Console/Commands/GesconSyncOperazioniPrimaNota.php index 1d7980b..70741dc 100755 --- a/app/Console/Commands/GesconSyncOperazioniPrimaNota.php +++ b/app/Console/Commands/GesconSyncOperazioniPrimaNota.php @@ -75,7 +75,30 @@ public function handle(): int if ($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 { $query->whereYear('dt_spe', $year); } diff --git a/app/Console/Commands/ImportGesconFullPipeline.php b/app/Console/Commands/ImportGesconFullPipeline.php index 2ec328b..f09bd12 100755 --- a/app/Console/Commands/ImportGesconFullPipeline.php +++ b/app/Console/Commands/ImportGesconFullPipeline.php @@ -1014,6 +1014,12 @@ private function stepUnita(?int $limit): int if (Schema::hasColumn('unita_immobiliari', 'deleted_at') && ! empty($exists->deleted_at)) { $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')) { $condoName = $this->firstNonEmptyStr([ $unitClassification['denominazione'] ?? null, @@ -1078,6 +1084,7 @@ private function stepUnita(?int $limit): int 'stato_occupazione' => 'occupata_proprietario', 'attiva' => true, 'unita_demo' => false, + 'note' => Schema::hasColumn('unita_immobiliari', 'note') ? (trim((string) ($r->note ?? $r->note_cond ?? '')) ?: null) : null, 'created_at' => now(), 'updated_at' => now(), ]; @@ -1163,7 +1170,7 @@ private function stepUnita(?int $limit): int } // 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() ->select('scala', 'interno', 'cod_cond') ->get(); @@ -2665,12 +2672,13 @@ private function stepVoci(?int $limit): int } $exists = $q->orderBy('id')->first(); - if (! $exists) { + if (! $exists && $gestioneId === null) { $exists = DB::table('voci_spesa') ->where('codice', $codiceDominio) ->when($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id'), function ($qq) use ($stabileId) { $qq->where('stabile_id', $stabileId); }) + ->whereNull('gestione_contabile_id') ->orderBy('id') ->first(); } @@ -3270,12 +3278,18 @@ private function stepOperazioni(?int $limit): int $count++; continue; } - $exists = DB::table('operazioni_contabili')->where('legacy_id', $legacy)->first(); - if ($exists && ! $this->option('update-only')) { + $gestioneId = $this->resolveGestioneId($o->legacy_year ?? ($o->anno ?? null), $o->compet ?? null, $o->gestione ?? null, $o->n_stra ?? null); + if (! $gestioneId) { 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(); $prefix = null; if ($gestioneId) { @@ -4224,25 +4238,19 @@ private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $n ->where('numero_emissione', $numeroEmissione) ->sum(DB::raw('COALESCE(importo_dovuto_euro, importo_dovuto, 0)')); } - $payload = [ - 'stabile_id' => $stabileId, - 'unita_immobiliare_id' => null, - 'descrizione' => $descr, - 'importo_totale' => $totale, - 'numero_rate' => 1, - 'importo_rata' => $totale, - 'data_inizio' => $dataInizio, - 'data_fine' => $dataFine, - 'frequenza' => 'MENSILE', - 'stato' => 'ATTIVO', - 'importo_pagato' => 0, - 'importo_residuo' => $totale, - 'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile, - 'created_at' => now(), - 'updated_at' => now(), + 'stabile_id' => $stabileId, + 'descrizione' => $descr, + 'importo_totale' => $totale, + 'numero_rate' => 1, + 'data_prima_rata' => $dataInizio, + 'frequenza' => 'MENSILE', + 'stato' => 'ATTIVO', + 'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile, + 'codice_piano' => \App\Models\PianoRateizzazione::generaCodicePiano(), + 'created_at' => now(), + 'updated_at' => now(), ]; - if (! $this->isDryRun) { $id = DB::table('piano_rateizzazione')->insertGetId($payload); return is_numeric($id) ? (int) $id : null; @@ -5272,20 +5280,18 @@ private function importLegacySubsystemForAssemblea(int $assembleaId, array $row, if (!$unita) continue; // Trova il soggetto principale legato all'unitΓ  - $soggettoId = DB::table('diritti_reali_unita') + $soggettoId = DB::table('proprieta') ->where('unita_immobiliare_id', $unita->id) + ->orderByRaw("CASE WHEN tipo_diritto = 'proprieta' THEN 1 ELSE 2 END") ->value('soggetto_id'); - if (!$soggettoId) { - $soggettoId = DB::table('soggetti_unita_immobiliari') - ->where('unita_immobiliare_id', $unita->id) - ->value('soggetto_id'); - } - if (!$soggettoId) continue; $tipoPartecipazione = 'personale'; $pda = strtolower(trim((string)($pr['p_d_a'] ?? ''))); + if ($pda === 'a' || $pda === 'assente') { + continue; + } if ($pda === 'd' || strpos($pda, 'delega') !== false) { $tipoPartecipazione = 'delega'; } @@ -5351,16 +5357,11 @@ private function importLegacySubsystemForAssemblea(int $assembleaId, array $row, if (!$unita) continue; - $soggettoId = DB::table('diritti_reali_unita') + $soggettoId = DB::table('proprieta') ->where('unita_immobiliare_id', $unita->id) + ->orderByRaw("CASE WHEN tipo_diritto = 'proprieta' THEN 1 ELSE 2 END") ->value('soggetto_id'); - if (!$soggettoId) { - $soggettoId = DB::table('soggetti_unita_immobiliari') - ->where('unita_immobiliare_id', $unita->id) - ->value('soggetto_id'); - } - if (!$soggettoId) continue; // 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, 'provincia' => trim((string) ($isTenant ? ($row->inquil_pr ?? '') : ($row->pr ?? ''))) ?: 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), 'codice_fiscale' => $isTenant ? (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; } + 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 { $codStabile = trim((string) ($row->cod_stabile ?? '')); $codCond = trim((string) ($row->cod_cond ?? '')); $stableCondId = $this->extractStableLegacyCondId($row, $legacyCondId); - if ($codStabile === '' || ($stableCondId === null && $codCond === '')) { + if ($codStabile === '') { return [$row]; } + $startCondId = $stableCondId !== null && $stableCondId !== '' ? $stableCondId : ($codCond !== '' ? $codCond : null); + $linkedCondIds = []; + if ($startCondId !== null) { + $linkedCondIds = $this->resolveLinkedLegacyCondIds($codStabile, $startCondId); + } + $query = DB::connection('gescon_import') ->table('condomin') - ->where('cod_stabile', $codStabile) - ->orderByDesc('legacy_year') + ->where('cod_stabile', $codStabile); + + $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'); - 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(); - 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 === []) { return [$row]; } @@ -7486,22 +7537,31 @@ private function loadLegacyComproprietariHistoryRows(object $row, ?string $legac } $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 []; } + $startCondId = $stableCondId !== null && $stableCondId !== '' ? $stableCondId : ($codCond !== '' ? $codCond : null); + $linkedCondIds = []; + if ($startCondId !== null) { + $linkedCondIds = $this->resolveLinkedLegacyCondIds($codStabile, $startCondId); + } + $query = DB::connection('gescon_import') ->table('comproprietari') ->where('cod_stabile', $codStabile) ->orderByDesc('legacy_year') ->orderByDesc('id'); - if (is_numeric($idCondRaw)) { - $query->where('id_cond', (int) $idCondRaw); + if (! empty($linkedCondIds)) { + $query->whereIn('id_cond', $linkedCondIds); + } elseif ($startCondId !== null) { + $query->where('id_cond', $startCondId); } else { - $query->where('id_cond', $idCondRaw); + return []; } $historyRows = $query->get()->all(); diff --git a/app/Console/Commands/ImportMultiYearGesconCommand.php b/app/Console/Commands/ImportMultiYearGesconCommand.php index 469e02a..6a15b17 100755 --- a/app/Console/Commands/ImportMultiYearGesconCommand.php +++ b/app/Console/Commands/ImportMultiYearGesconCommand.php @@ -46,7 +46,7 @@ public function handle() $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.'); return Command::SUCCESS; } @@ -102,7 +102,7 @@ private function scanAvailableYears(string $gesconDir): array } } - ksort($years); + krsort($years); return $years; } @@ -228,9 +228,9 @@ private function countTableRecords(string $mdbPath, string $tableName): int } $output = shell_exec(sprintf( - 'mdb-sql %s <<< "SELECT COUNT(*) FROM %s" 2>/dev/null', - escapeshellarg($mdbPath), - escapeshellarg($tableName) + 'echo "SELECT COUNT(*) FROM %s" | mdb-sql %s 2>/dev/null', + $tableName, + escapeshellarg($mdbPath) )); if ($output && preg_match('/(\d+)/', $output, $matches)) { @@ -247,7 +247,14 @@ private function executeImport(string $tenantId, string $gesconDir, array $years DB::beginTransaction(); 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 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)); + $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 $deletedGestioni = GestioneContabile::forTenant($tenantId) - ->whereIn('anno_gestione', $years) + ->where('stabile_id', $stabileId) + ->whereIn('anno_gestione', $calendarYears) ->delete(); $this->line(" Deleted {$deletedGestioni} gestioni"); diff --git a/app/Console/Commands/LoadGesconMdbToStaging.php b/app/Console/Commands/LoadGesconMdbToStaging.php index bee8194..15fc3fb 100755 --- a/app/Console/Commands/LoadGesconMdbToStaging.php +++ b/app/Console/Commands/LoadGesconMdbToStaging.php @@ -114,7 +114,10 @@ private function ensureCondominColumns(): void 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'); 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) { $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); return; diff --git a/app/Filament/Pages/Condomini/Components/GestioniStabileTable.php b/app/Filament/Pages/Condomini/Components/GestioniStabileTable.php index b0914ba..b008e23 100755 --- a/app/Filament/Pages/Condomini/Components/GestioniStabileTable.php +++ b/app/Filament/Pages/Condomini/Components/GestioniStabileTable.php @@ -15,6 +15,10 @@ use Filament\Support\Contracts\TranslatableContentDriver; use Filament\Tables\Columns\IconColumn; 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\Contracts\HasTable; use Filament\Tables\Table; @@ -281,7 +285,15 @@ public function table(Table $table): Table 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->tenant_id = $tenantId; $gestione->stabile_id = $stabileId; $gestione->anno_gestione = (int) ($data['anno_gestione'] ?? now()->year); $gestione->tipo_gestione = (string) ($data['tipo_gestione'] ?? 'ordinaria'); @@ -356,43 +368,28 @@ public function table(Table $table): Table ->sortable() ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('denominazione') + TextInputColumn::make('denominazione') ->label('Denominazione') - ->searchable() - ->wrap(), + ->searchable(), - TextColumn::make('origine_dati') - ->label('Origine') - ->getStateUsing(function (GestioneContabile $record): string { - if (filled($record->codice_archivio_legacy)) { - return 'Legacy collegata'; - } + DatePickerColumn::make('data_inizio') + ->label('Data inizio') + ->native(false), - return 'Locale'; - }) - ->badge() - ->color(fn(string $state): string => $state === 'Legacy collegata' ? 'success' : 'gray'), + DatePickerColumn::make('data_fine') + ->label('Data fine') + ->native(false), - TextColumn::make('periodo_label') - ->label('Periodo') - ->getStateUsing(fn(GestioneContabile $record): string => $record->periodo_label) - ->wrap(), - - TextColumn::make('stato') + SelectColumn::make('stato') ->label('Stato') - ->formatStateUsing(fn(string $state): string => ucfirst($state)) - ->badge() - ->color(fn(string $state): string => match ($state) { - 'aperta' => 'success', - 'consolidata' => 'warning', - 'chiusa' => 'gray', - default => 'gray', - }) - ->toggleable(isToggledHiddenByDefault: true), + ->options([ + 'aperta' => 'Aperta', + 'consolidata' => 'Consolidata', + 'chiusa' => 'Chiusa', + ]), - IconColumn::make('gestione_attiva') - ->label('Attiva') - ->boolean(), + ToggleColumn::make('gestione_attiva') + ->label('Attiva'), TextColumn::make('mesi_rate') ->label('Mesi rate') diff --git a/app/Filament/Pages/Condomini/RiscaldamentoStabileArchivio.php b/app/Filament/Pages/Condomini/RiscaldamentoStabileArchivio.php index 4abd820..f561337 100755 --- a/app/Filament/Pages/Condomini/RiscaldamentoStabileArchivio.php +++ b/app/Filament/Pages/Condomini/RiscaldamentoStabileArchivio.php @@ -1766,7 +1766,10 @@ private function resolveRiscaldamentoCandidateInvoices(?string $tipoGestione = n ->where('f.stabile_id', $stabileId) ->whereNotNull('f.fattura_elettronica_id') ->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), function($builder) use ($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'] : []; $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); $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))), 'data_fattura' => optional($fattura->data_fattura)?->format('d/m/Y'), '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), 'matricola' => $matricola, 'codice_utenza' => $codiceUtenza, @@ -3021,7 +3026,7 @@ private function formatRiscaldamentoPeriodLabel(?string $from, ?string $to): str } /** @param \Illuminate\Support\Collection $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()) { return null; @@ -3031,20 +3036,24 @@ private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $cod $targetUtenza = $this->normalizeRiscaldamentoIdentifier($codici['utenza'] ?? null); $targetCliente = $this->normalizeRiscaldamentoIdentifier($codici['cliente'] ?? null); $targetContratto = $this->normalizeRiscaldamentoIdentifier($codici['contratto'] ?? null); + $fornitoreId = $fattura?->fornitore_id; 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; if ($targetMatricola !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->contatore_matricola) === $targetMatricola) { - $score += 5; + $score += 10; } if ($targetUtenza !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_utenza) === $targetUtenza) { - $score += 4; + $score += 8; } if ($targetContratto !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_contratto) === $targetContratto) { - $score += 3; + $score += 6; } if ($targetCliente !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_cliente) === $targetCliente) { + $score += 4; + } + if ($fornitoreId && (int) $servizio->fornitore_id === (int) $fornitoreId) { $score += 2; } @@ -4043,8 +4052,7 @@ protected function ensureFornitoreStabileDefaultVoceSpesa(int $stabileId): void $voceSpesa = \App\Models\VoceSpesa::query() ->where('stabile_id', $stabileId) ->where(function ($q) use ($mostCommonVoceCode) { - $q->where('conto_contabile', $mostCommonVoceCode) - ->orWhere('codice', $mostCommonVoceCode) + $q->where('codice', $mostCommonVoceCode) ->orWhere('legacy_codice', $mostCommonVoceCode); }) ->first(); diff --git a/app/Filament/Pages/Condomini/ServiziStabileArchivio.php b/app/Filament/Pages/Condomini/ServiziStabileArchivio.php index 7f9fb9d..f67a01e 100755 --- a/app/Filament/Pages/Condomini/ServiziStabileArchivio.php +++ b/app/Filament/Pages/Condomini/ServiziStabileArchivio.php @@ -731,7 +731,7 @@ public function importElectronicReadings(): void 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'; } @@ -3826,4 +3826,74 @@ public function getAcquaAltreVociLegacyProperty(): array 'totale' => round((float) ($r->totale ?? 0), 2), ])->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; + } } diff --git a/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php b/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php index a6070c5..af56ddd 100755 --- a/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php +++ b/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php @@ -182,9 +182,7 @@ protected function getTableQuery(): Builder return TabellaMillesimale::query()->whereRaw('1 = 0'); } - $orderColumn = Schema::hasColumn('tabelle_millesimali', 'nord') - ? 'nord' - : (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : (Schema::hasColumn('tabelle_millesimali', 'ordinamento') ? 'ordinamento' : 'id')); + $orderColumn = DB::raw('COALESCE(nord, ordinamento, ordine_visualizzazione, 999999)'); return TabellaMillesimale::query() ->where('stabile_id', $activeStabileId) diff --git a/app/Filament/Pages/Condomini/UnitaImmobiliariArchivio.php b/app/Filament/Pages/Condomini/UnitaImmobiliariArchivio.php index 20c0ec2..60a4cea 100755 --- a/app/Filament/Pages/Condomini/UnitaImmobiliariArchivio.php +++ b/app/Filament/Pages/Condomini/UnitaImmobiliariArchivio.php @@ -223,7 +223,7 @@ public function table(Table $table): Table $inquilini = []; $altri = []; - $ruoli = $record->rubricaRuoliAttivi ?? collect(); + $ruoli = ($record->rubricaRuoliAttivi ?? collect())->unique(fn($r) => $r->rubrica_id . '|' . $r->ruolo_standard); foreach ($ruoli as $ruolo) { $nome = $ruolo->contatto?->nome_completo ?? 'β€”'; $cf = $ruolo->contatto?->codice_fiscale ? " ({$ruolo->contatto->codice_fiscale})" : ''; @@ -265,18 +265,32 @@ public function table(Table $table): Table TextColumn::make('palazzina') ->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(), TextColumn::make('scala') ->label('Scala') + ->sortable() ->toggleable(), TextColumn::make('piano') ->label('Piano') + ->sortable() ->toggleable(), TextColumn::make('interno') ->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 { $interno = trim((string) ($state ?? '')); if ($interno === '') { @@ -315,6 +329,7 @@ public function table(Table $table): Table ->label('Apri') ->icon('heroicon-o-arrow-right') ->url(fn(UnitaImmobiliare $record) => $unitaPageUrl . '?unita_id=' . $record->id), - ]); + ]) + ->defaultSort('palazzina', 'asc'); } } diff --git a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php index 89d21ff..f2ea952 100755 --- a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php +++ b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php @@ -85,6 +85,8 @@ class CasseBancheMovimenti extends Page implements HasTable public int $riconciliazioneOffset = 0; + public ?string $selectedCausaleForConfig = null; + public string $periodoTipo = 'mese'; public int $periodoAnno; public int $periodoValore; @@ -1134,6 +1136,72 @@ protected function getHeaderActions(): array ->success() ->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(); } }), + + 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 { $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 @@ -4240,4 +4619,73 @@ protected function storeSaldoDocumento(int $stabileId, DatiBancari $conto, mixed '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, + ]); + } } diff --git a/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php b/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php index 855c720..7c1f3d4 100755 --- a/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php +++ b/app/Filament/Pages/Contabilita/FatturaFornitoreScheda.php @@ -3069,9 +3069,7 @@ public function getTabelleMillesimaliByTipo(string $tipoTabella): array return TabellaMillesimale::query() ->where('stabile_id', $stabileId) ->where('tipo_tabella', $tipoTabella) - ->orderBy('ordinamento') - ->orderBy('codice_tabella') - ->orderBy('nome_tabella') + ->ordinato() ->get(['id', 'denominazione', 'nome_tabella', 'codice_tabella']) ->map(function (TabellaMillesimale $t): array { $label = trim((string) ($t->denominazione ?? '')); diff --git a/app/Filament/Pages/Contabilita/PrimaNotaModifica.php b/app/Filament/Pages/Contabilita/PrimaNotaModifica.php index 105f2c9..e5db082 100755 --- a/app/Filament/Pages/Contabilita/PrimaNotaModifica.php +++ b/app/Filament/Pages/Contabilita/PrimaNotaModifica.php @@ -147,11 +147,13 @@ public function form(Schema $schema): Schema Select::make('tipo') ->label('Tipo') ->options(['dare' => 'Dare', 'avere' => 'Avere']) - ->required(), + ->required() + ->live(), TextInput::make('importo') ->label('Importo') ->numeric() - ->required(), + ->required() + ->live(onBlur: true), ]) ->columns(3) ->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 { return [ diff --git a/app/Filament/Pages/Contabilita/VociSpesaArchivio.php b/app/Filament/Pages/Contabilita/VociSpesaArchivio.php index 13f07fa..a8287db 100755 --- a/app/Filament/Pages/Contabilita/VociSpesaArchivio.php +++ b/app/Filament/Pages/Contabilita/VociSpesaArchivio.php @@ -448,10 +448,7 @@ public function getTabelleMillesimaliOptions(): array $q = TabellaMillesimale::query() ->where('stabile_id', $stabileId) - ->orderBy('ordine_visualizzazione') - ->orderBy('ordinamento') - ->orderBy('codice_tabella') - ->orderBy('nome_tabella'); + ->ordinato(); if ($hasAnno) { $q->where('anno_gestione', $anno); diff --git a/app/Filament/Pages/Gescon/ImportazioneArchivi.php b/app/Filament/Pages/Gescon/ImportazioneArchivi.php index 78f28f8..d3c52b7 100755 --- a/app/Filament/Pages/Gescon/ImportazioneArchivi.php +++ b/app/Filament/Pages/Gescon/ImportazioneArchivi.php @@ -750,6 +750,7 @@ public function applicaEnrichmentStabile(): void public function aggiornaAnagraficheDaCondomin(): void { + @set_time_limit(0); $user = Auth::user(); if (! $user instanceof User) { abort(403); @@ -789,6 +790,7 @@ public function aggiornaAnagraficheDaCondomin(): void public function risincronizzaRelazioniImportate(): void { + @set_time_limit(0); $user = Auth::user(); if (! $user instanceof User) { abort(403); @@ -826,6 +828,7 @@ public function risincronizzaRelazioniImportate(): void public function aggiornaFornitoriGescon(): void { + @set_time_limit(0); $user = Auth::user(); if (! $user instanceof User) { abort(403); @@ -894,6 +897,7 @@ public function aggiornaFornitoriGescon(): void public function refreshImportTagLegacyFornitoriGlobale(): void { + @set_time_limit(0); $user = Auth::user(); if (! $user instanceof User) { abort(403); @@ -968,6 +972,7 @@ public function refreshImportTagLegacyFornitoriGlobale(): void public function applicaEnrichmentTuttiStabili(): void { + @set_time_limit(0); $user = Auth::user(); if (! $user instanceof User) { abort(403); @@ -1019,6 +1024,7 @@ public function applicaEnrichmentTuttiStabili(): void public function eseguiImportSelezionato(EssentialImportService $service): void { + @set_time_limit(0); $user = Auth::user(); if (! $user instanceof User) { abort(403); @@ -1179,8 +1185,11 @@ public function eseguiImportSelezionato(EssentialImportService $service): void if (! empty($options['sync_staging'])) { $singoloMdb = null; + $annoFolder = $options['anno'] ?: '0001'; 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'; } @@ -1189,7 +1198,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void : null; 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)) { 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, '--table' => 's_cassa', '--stabile' => $code, - '--legacy-year' => (string) ($options['anno'] ?? ''), + '--legacy-year' => $annoFolder, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); @@ -1218,7 +1227,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void '--mdb' => $singoloMdb, '--table' => 'operazioni', '--stabile' => $code, - '--legacy-year' => (string) ($options['anno'] ?? ''), + '--legacy-year' => $annoFolder, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); @@ -1227,7 +1236,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void '--mdb' => $singoloMdb, '--table' => 'voc_spe', '--stabile' => $code, - '--legacy-year' => (string) ($options['anno'] ?? ''), + '--legacy-year' => $annoFolder, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); @@ -1236,7 +1245,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void '--mdb' => $singoloMdb, '--table' => 'tabelle', '--stabile' => $code, - '--legacy-year' => (string) ($options['anno'] ?? ''), + '--legacy-year' => $annoFolder, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); @@ -1248,7 +1257,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void '--mdb' => $singoloMdb, '--table' => 'incassi', '--stabile' => $code, - '--legacy-year' => (string) ($options['anno'] ?? ''), + '--legacy-year' => $annoFolder, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); @@ -1259,7 +1268,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void '--mdb' => $generaleMdb, '--table' => 'emes_gen', '--stabile' => $code, - '--legacy-year' => (string) ($options['anno'] ?? ''), + '--legacy-year' => $annoFolder, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); @@ -1268,7 +1277,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void '--mdb' => $generaleMdb, '--table' => 'emes_det', '--stabile' => $code, - '--legacy-year' => (string) ($options['anno'] ?? ''), + '--legacy-year' => $annoFolder, '--refresh-slice' => true, ]); $outputs[] = trim((string) Artisan::output()); @@ -1289,7 +1298,11 @@ public function eseguiImportSelezionato(EssentialImportService $service): void '--path' => $options['path'], ]; 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'])) { $params['--dry-run'] = true; @@ -1350,6 +1363,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void public function eseguiImportAllineamentoCompleto(): void { + @set_time_limit(0); $user = Auth::user(); if (! $user instanceof User) { 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 { + $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([ - 'tenant_id' => 'default', + 'tenant_id' => $tenantId, 'stabile_id' => $stabile->id, 'anno_gestione' => $year, 'tipo_gestione' => $tipo, @@ -2383,4 +2404,42 @@ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array 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; + } } diff --git a/app/Filament/Pages/Gescon/Ordinarie.php b/app/Filament/Pages/Gescon/Ordinarie.php index 3dec1f8..36c3897 100755 --- a/app/Filament/Pages/Gescon/Ordinarie.php +++ b/app/Filament/Pages/Gescon/Ordinarie.php @@ -51,7 +51,7 @@ class Ordinarie extends Page public string $viewTab = 'operazioni'; public string $sortField = 'id_operaz'; public string $sortDirection = 'desc'; - public bool $netgesconWorkflowMode = false; + public bool $netgesconWorkflowMode = true; public bool $onlyStraordinarieMode = false; public bool $onlyRiscaldamentoMode = false; public ?int $selectedStraordinaria = null; @@ -3306,6 +3306,7 @@ public function openDettPersModal(int $nSpe): void if ($activeStabileId && Schema::hasTable('tabelle_millesimali') && Schema::hasTable('dettaglio_millesimi')) { $localRows = DB::table('tabelle_millesimali') ->where('stabile_id', (int) $activeStabileId) + ->orderByRaw('COALESCE(nord, ordinamento, ordine_visualizzazione, 999999)') ->orderBy('codice_tabella') ->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione']); @@ -3716,8 +3717,7 @@ private function buildOperazioniQuery() $query->where(function ($q) use ($s) { $q->where('o.descrizione', 'like', $s) ->orWhere('o.conto_contabile', 'like', $s) - ->orWhere('o.protocollo_completo', 'like', $s) - ->orWhere('o.num_fat', 'like', $s); + ->orWhere('o.protocollo_completo', 'like', $s); }); } @@ -3739,8 +3739,8 @@ private function buildOperazioniQuery() 'o.n_stra', 'o.protocollo_completo', DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"), - 'o.num_fat', - 'o.fe_uid', + DB::raw("null as num_fat"), + DB::raw("null as fe_uid"), 'o.voce_spesa_snapshot', 'o.fornitore_snapshot', ]); @@ -3767,8 +3767,8 @@ private function buildOperazioniQuery() 'o.protocollo_completo', DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"), DB::raw("coalesce(o.dare, o.avere, 0) as importo"), - 'o.num_fat', - 'o.fe_uid', + DB::raw("null as num_fat"), + DB::raw("null as fe_uid"), 'o.voce_spesa_snapshot', 'o.fornitore_snapshot', DB::raw("0 as importo_spese"), @@ -3994,8 +3994,8 @@ private function applyOperazioniSorting($query) 'dt_spe' => 'o.data_operazione', 'cod_spe' => 'o.conto_contabile', 'importo_euro' => 'o.dare', - 'num_fat' => 'o.num_fat', - 'fe_uid' => 'o.fe_uid', + 'num_fat' => 'o.id', + 'fe_uid' => 'o.id', default => 'o.id' }; $query->orderBy($mappedField, $dir); diff --git a/app/Filament/Pages/Gescon/RegistrazioneUnificata.php b/app/Filament/Pages/Gescon/RegistrazioneUnificata.php index 5e05d04..21b2ee8 100755 --- a/app/Filament/Pages/Gescon/RegistrazioneUnificata.php +++ b/app/Filament/Pages/Gescon/RegistrazioneUnificata.php @@ -555,7 +555,7 @@ private function getTabelleOptions(): array return TabellaMillesimale::query() ->where('stabile_id', $this->stabileAttivo->id) - ->orderBy('codice_tabella') + ->ordinato() ->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione']) ->mapWithKeys(function (TabellaMillesimale $t) { $label = $t->codice_tabella ?: ($t->denominazione ?: ($t->nome_tabella ?: null)); diff --git a/app/Filament/Pages/SuperAdmin/CausaliBancarie.php b/app/Filament/Pages/SuperAdmin/CausaliBancarie.php index f999809..9202784 100755 --- a/app/Filament/Pages/SuperAdmin/CausaliBancarie.php +++ b/app/Filament/Pages/SuperAdmin/CausaliBancarie.php @@ -30,11 +30,11 @@ class CausaliBancarie extends Page implements HasTable 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'; @@ -43,7 +43,7 @@ public static function canAccess(): bool $user = Auth::user(); return $user instanceof User - && $user->hasAnyRole(['super-admin', 'admin']) + && ($user->hasAnyRole(['super-admin', 'admin']) || $user->can('contabilita.causali')) && Schema::hasTable('contabilita_causali_bancarie'); } diff --git a/app/Filament/Pages/SuperAdmin/EstrattoreFatturePdf.php b/app/Filament/Pages/SuperAdmin/EstrattoreFatturePdf.php index 6d51527..c440708 100644 --- a/app/Filament/Pages/SuperAdmin/EstrattoreFatturePdf.php +++ b/app/Filament/Pages/SuperAdmin/EstrattoreFatturePdf.php @@ -23,7 +23,7 @@ use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Contracts\HasTable; use Filament\Tables\Table; -use Filament\Tables\Actions\Action as TableAction; +use Filament\Actions\Action as TableAction; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Schema; diff --git a/app/Models/IncassoPagamento.php b/app/Models/IncassoPagamento.php index e8fd2bc..1a27ac3 100755 --- a/app/Models/IncassoPagamento.php +++ b/app/Models/IncassoPagamento.php @@ -13,31 +13,29 @@ class IncassoPagamento extends Model protected $table = 'incassi_pagamenti'; protected $fillable = [ - 'amministratore_id', - 'anno_gestione_id', 'stabile_id', + 'anno_gestione_id', 'condomino_id', - 'rata_id', - 'data_incasso', - 'data_valuta', - 'importo', - 'modalita_pagamento', - 'numero_assegno', - 'banca_assegno', - 'numero_bonifico', - 'causale', - 'note_bancarie', + 'cod_condomino', + 'n_riferimento', + 'data_pagamento', + 'importo_pagato', + 'importo_pagato_euro', + 'n_ricevuta', + 'tipo_gestione', + 'mese_riferimento', + 'descrizione', + 'cod_cassa', 'riconciliato', - 'data_riconciliazione', - 'note' + 'rata_id', + 'movimento_banca_id', ]; protected $casts = [ - 'data_incasso' => 'date', - 'data_valuta' => 'date', - 'data_riconciliazione' => 'date', - 'importo' => 'decimal:2', - 'riconciliato' => 'boolean' + 'data_pagamento' => 'date', + 'importo_pagato' => 'decimal:2', + 'importo_pagato_euro' => 'decimal:2', + 'riconciliato' => 'boolean', ]; const MODALITA_CONTANTI = 'contanti'; diff --git a/app/Models/Stabile.php b/app/Models/Stabile.php index 79b4b9a..14b58b2 100755 --- a/app/Models/Stabile.php +++ b/app/Models/Stabile.php @@ -588,6 +588,14 @@ public function syncRubricaContatto(): RubricaUniversale '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) { $rubrica->update($data); } else { diff --git a/app/Models/TabellaMillesimale.php b/app/Models/TabellaMillesimale.php index 257b240..139daa5 100755 --- a/app/Models/TabellaMillesimale.php +++ b/app/Models/TabellaMillesimale.php @@ -153,15 +153,8 @@ public function scopePerStabile($query, $stabileId) */ 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 + ->orderByRaw('COALESCE(ordine_visualizzazione, nord, ordinamento, 999999)') ->orderBy('codice_tabella') ->orderBy('nome_tabella'); } diff --git a/app/Modules/Contabilita/Models/MovimentoBanca.php b/app/Modules/Contabilita/Models/MovimentoBanca.php index a7d7784..efd97af 100755 --- a/app/Modules/Contabilita/Models/MovimentoBanca.php +++ b/app/Modules/Contabilita/Models/MovimentoBanca.php @@ -67,6 +67,96 @@ public function getDescrizioneEstesaPulitaAttribute(): ?string 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 { $patterns = []; diff --git a/app/Services/FattureElettroniche/FatturaElettronicaImporter.php b/app/Services/FattureElettroniche/FatturaElettronicaImporter.php index b08bf5f..430162e 100755 --- a/app/Services/FattureElettroniche/FatturaElettronicaImporter.php +++ b/app/Services/FattureElettroniche/FatturaElettronicaImporter.php @@ -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); $amministratoreId = (int) ($stabile?->amministratore_id ?: 0); @@ -230,7 +232,17 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original try { /** @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([ 'stabile_id' => $stabileId, 'fornitore_id' => $fornitoreId, @@ -241,9 +253,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original 'fornitore_piva' => $fornitorePiva, 'fornitore_cf' => $data['fornitore_cf'] ?? null, 'fornitore_denominazione' => $data['fornitore_denominazione'] ?? 'ND', - 'imponibile' => (float) ($data['imponibile'] ?? 0), - 'iva' => (float) ($data['iva'] ?? 0), - 'totale' => (float) ($data['totale'] ?? 0), + 'imponibile' => $imponibile, + 'iva' => $iva, + 'totale' => $totale, 'pagamento_modalita' => $data['pagamento_modalita'] ?? 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'] : []; $ritenutaImporto = (float) ($ritenuta['importo'] ?? 0); $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; $dataRegistrazione = null; @@ -298,9 +313,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original 'descrizione' => $data['fornitore_denominazione'] ?? null, 'valuta' => 'EUR', 'cambio' => 1, - 'imponibile' => round((float) ($data['imponibile'] ?? 0), 2), - 'iva' => round((float) ($data['iva'] ?? 0), 2), - 'totale' => round((float) ($data['totale'] ?? 0), 2), + 'imponibile' => round($imponibile, 2), + 'iva' => round($iva, 2), + 'totale' => round($totale, 2), 'data_scadenza' => $data['data_scadenza'] ?? null, 'modalita_pagamento' => $data['pagamento_modalita'] ?? null, 'ritenuta_aliquota' => $ritenutaAliquota, @@ -333,7 +348,7 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original 'descrizione' => $data['numero_fattura'] ?? 'Fattura elettronica', 'quantita' => null, 'prezzo_unitario' => null, - 'prezzo_totale' => (float) ($data['imponibile'] ?? 0), + 'prezzo_totale' => $imponibile, 'aliquota_iva' => 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'])) { $imponibileRiga = (float) $r['prezzo_unitario'] * (float) $r['quantita']; } + if ($isCreditNote) { + $imponibileRiga = -abs($imponibileRiga); + } $aliquota = $r['aliquota_iva'] ?? null; $ivaRiga = 0.0; diff --git a/app/Services/Import/MultiYearGesconImportService.php b/app/Services/Import/MultiYearGesconImportService.php index f8c30fa..6426c6a 100755 --- a/app/Services/Import/MultiYearGesconImportService.php +++ b/app/Services/Import/MultiYearGesconImportService.php @@ -2,8 +2,8 @@ namespace App\Services\Import; use App\Models\GestioneContabile; +use App\Models\Incasso; use App\Models\IncassoEstrattoConto; -use App\Models\OperazioneContabile; use App\Models\RegistroRitenuteAcconto; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -21,12 +21,52 @@ class MultiYearGesconImportService { private string $tenantId; + private ?int $stabileId = null; + private ?string $baseDir = null; + private array $directoryToYearMap = []; private array $mdbConnections = []; private array $gestioni = []; - public function __construct(string $tenantId) + public function __construct(string $tenantId, ?int $stabileId = null, ?string $baseDir = null) { $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; } + /** + * 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 */ @@ -104,13 +169,25 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array 'protocollo_prefix' => "O{$year}", ]; - // Gestione Riscaldamento (se presente) - $opRiscaldamento = $this->queryMdb($mdbPath, " - SELECT COUNT(*) as cnt FROM operazioni - WHERE gestione = 'R' OR gestione = 'r' - "); + // Legge le colonne necessarie da Operazioni + $operazioni = $this->queryMdb($mdbPath, "SELECT Gestione, n_stra FROM Operazioni"); - 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[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'riscaldamento', @@ -121,18 +198,8 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array ]; } - // Gestioni Straordinarie (dinamiche) - $straordinarie = $this->queryMdb($mdbPath, " - 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']; + ksort($straordinarie); + foreach (array_keys($straordinarie) as $numero) { $gestioni[] = [ 'anno_gestione' => $year, 'tipo_gestione' => 'straordinaria', @@ -150,10 +217,11 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array /** * Crea o aggiorna gestione */ - private function createOrUpdateGestione(array $gestioneData): GestioneContabile + private function createOrUpdateGestione(array $gestioneData, ?string $yearKey = null): GestioneContabile { $gestione = GestioneContabile::updateOrCreate([ 'tenant_id' => $this->tenantId, + 'stabile_id' => $this->stabileId, 'anno_gestione' => $gestioneData['anno_gestione'], 'tipo_gestione' => $gestioneData['tipo_gestione'], 'numero_straordinaria' => $gestioneData['numero_straordinaria'] ?? null, @@ -166,7 +234,7 @@ private function createOrUpdateGestione(array $gestioneData): GestioneContabile ]); // Cache per lookup veloce - $year = $gestioneData['anno_gestione']; + $year = $yearKey ?? $gestioneData['anno_gestione']; $tipo = $gestioneData['tipo_gestione']; if ($tipo === 'straordinaria') { @@ -189,37 +257,67 @@ private function importOperazioniForGestione(GestioneContabile $gestione, string $whereClause = $this->buildGestioneWhereClause($gestione); $operazioni = $this->queryMdb($mdbPath, " - SELECT *, - IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean, - IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean, - IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean - FROM operazioni + SELECT * + FROM Operazioni 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; foreach ($operazioni as $op) { try { + $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? ''); $protocollo = $this->generateProtocollo($gestione->id, $op); - $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? ''); - OperazioneContabile::create([ - 'tenant_id' => $this->tenantId, - 'gestione_id' => $gestione->id, - 'numero_operazione' => $op['n_operazione'], - 'data_operazione' => $this->parseGesconDate($op['data_oper']), - 'importo' => (float) ($op['euro'] ?? 0), - 'descrizione' => $op['descrizione'] ?? '', - 'cod_voce_gescon' => $op['cod_voce'] ?? '', - 'cod_beneficiario_gescon' => $op['cod_ben'] ?? '', - 'compet' => $op['compet_clean'], - 'natura2' => $op['natura2_clean'], - 'n_stra' => (int) $op['n_stra_clean'], - 'protocollo_numero' => $protocollo['numero'], - 'protocollo_completo' => $protocollo['completo'], - 'voce_spesa_snapshot' => json_encode($voceSnapshot), - 'metadati_gescon' => json_encode($op), + $competClean = empty($op['compet']) ? 'C' : trim($op['compet']); + $natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']); + $nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra']; + + $opId = $op['id_operaz'] ?? $op['id'] ?? null; + $dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null; + $importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0; + $codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? ''; + $codBen = $op['cod_ben'] ?? ''; + + $isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata'; + $amount = (float) $importo; + $dareVal = (float) ($op['importo_spese'] ?? 0); + $avereVal = (float) ($op['importo_entrate'] ?? 0); + + 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++; @@ -396,8 +494,12 @@ private function buildGestioneJoinClause(GestioneContabile $gestione, string $al /** * 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 = [ 'operazioni' => 0, 'incassi' => 0, @@ -421,7 +523,9 @@ private function importGestioneYear(string $year, array $paths): array } // 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; } @@ -435,14 +539,16 @@ private function importOperazioni(string $year, string $mdbPath): int // Query MDB per operazioni $operazioni = $this->queryMdb($mdbPath, " - SELECT *, - IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean, - IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean, - IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean - FROM operazioni - ORDER BY n_operazione + SELECT * + FROM Operazioni "); + usort($operazioni, function($a, $b) { + $idA = (int) ($a['id_operaz'] ?? 0); + $idB = (int) ($b['id_operaz'] ?? 0); + return $idA <=> $idB; + }); + foreach ($operazioni as $op) { try { // Determina gestione @@ -452,24 +558,52 @@ private function importOperazioni(string $year, string $mdbPath): int $protocollo = $this->generateProtocollo($gestioneId, $op); // Snapshot voce spesa - $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? ''); + $voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? ''); - OperazioneContabile::create([ - 'tenant_id' => $this->tenantId, - 'gestione_id' => $gestioneId, - 'numero_operazione' => $op['n_operazione'], - 'data_operazione' => $this->parseGesconDate($op['data_oper']), - 'importo' => (float) ($op['euro'] ?? 0), - 'descrizione' => $op['descrizione'] ?? '', - 'cod_voce_gescon' => $op['cod_voce'] ?? '', - 'cod_beneficiario_gescon' => $op['cod_ben'] ?? '', - 'compet' => $op['compet_clean'], - 'natura2' => $op['natura2_clean'], - 'n_stra' => (int) $op['n_stra_clean'], - 'protocollo_numero' => $protocollo['numero'], - 'protocollo_completo' => $protocollo['completo'], - 'voce_spesa_snapshot' => json_encode($voceSnapshot), - 'metadati_gescon' => json_encode($op), + $competClean = empty($op['compet']) ? 'C' : trim($op['compet']); + $natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']); + $nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra']; + + $opId = $op['id_operaz'] ?? $op['id'] ?? null; + $dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null; + $importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0; + $codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? ''; + $codBen = $op['cod_ben'] ?? ''; + + $isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata'; + $amount = (float) $importo; + $dareVal = (float) ($op['importo_spese'] ?? 0); + $avereVal = (float) ($op['importo_entrate'] ?? 0); + + 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++; @@ -486,6 +620,63 @@ private function importOperazioni(string $year, string $mdbPath): int 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 */ @@ -493,11 +684,15 @@ private function importRitenuteAcconto(string $year, string $mdbPath): int { $imported = 0; + if (!$this->mdbTableExists($mdbPath, 'Nettovers_RDA')) { + return 0; + } + // Query per ritenute $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 - 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 ORDER BY n.Rif_RDA "); @@ -510,7 +705,7 @@ private function importRitenuteAcconto(string $year, string $mdbPath): int 'tenant_id' => $this->tenantId, 'gestione_id' => $gestioneId, '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), 'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20), 'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0), @@ -542,23 +737,31 @@ private function importIncassiEstrattoConto(string $year, string $mdbPath): int $incassiEc = $this->queryMdb($mdbPath, " 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) { try { $gestioneId = $this->gestioni[$year]['ordinaria']->id; IncassoEstrattoConto::create([ - 'tenant_id' => $this->tenantId, - 'gestione_id' => $gestioneId, - 'id_gescon' => $inc['id'] ?? null, - 'data_operazione' => $this->parseGesconDate($inc['data_oper']), - 'data_valuta' => $this->parseGesconDate($inc['data_valuta']), - 'importo' => (float) ($inc['importo'] ?? 0), - 'descrizione' => $inc['descrizione'] ?? '', - 'stato' => 'da_riconciliare', - 'metadati_gescon' => json_encode($inc), + 'tenant_id' => $this->tenantId, + 'gestione_id' => $gestioneId, + 'id_gescon' => $inc['protocollo'] ?? null, + 'data_operazione' => $this->parseGesconDate($inc['Data_pag'] ?? null), + 'data_valuta' => $this->parseGesconDate($inc['Data_pag'] ?? null), + 'importo' => (float) ($inc['Importo'] ?? 0), + 'descrizione' => trim(($inc['Descrizione_Aggiuntiva'] ?? '') . ' ' . ($inc['Nome_condomino'] ?? '')), + 'ordinante' => $inc['Nome_condomino'] ?? null, + 'riferimento_bancario' => $inc['Num_incasso'] ?? null, + 'file_origine' => $inc['Nome_file_pdf'] ?? null, + 'stato' => 'da_riconciliare', + 'metadati_gescon' => json_encode($inc), ]); $imported++; @@ -582,11 +785,21 @@ private function importRateEmesse(string $year, string $mdbPath): int { $imported = 0; - // Unifica EMESS_DET, EMESS_DET_2, EMESS_GEN - $tables = ['EMESS_DET', 'EMESS_DET_2', 'EMESS_GEN']; + // Unifica emes_det, emes_det_2, emes_gen + $tables = ['emes_det', 'emes_det_2', 'emes_gen']; 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) { try { @@ -628,18 +841,20 @@ private function determineGestioneId(string $year, array $operazione): int private function getOrCreateStraordinaria(string $year, int $nStra): int { $key = "straordinaria_{$nStra}"; + $calendarYear = $this->directoryToYearMap[$year] ?? (int) $year; if (! isset($this->gestioni[$year][$key])) { $this->gestioni[$year][$key] = GestioneContabile::create([ 'tenant_id' => $this->tenantId, - 'anno_gestione' => (int) $year, + 'stabile_id' => $this->stabileId, + 'anno_gestione' => $calendarYear, 'tipo_gestione' => 'straordinaria', 'numero_straordinaria' => $nStra, - 'denominazione' => "Gestione Straordinaria {$year} - {$nStra}", - 'data_inizio' => "{$year}-01-01", - 'data_fine' => "{$year}-12-31", + 'denominazione' => "Gestione Straordinaria {$calendarYear} - {$nStra}", + 'data_inizio' => "{$calendarYear}-01-01", + 'data_fine' => "{$calendarYear}-12-31", '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 { + $query = preg_replace('/\s+/', ' ', trim($query)); // Usa mdb-tools per query diretta $command = sprintf( - 'mdb-sql "%s" <<< "%s"', - escapeshellarg($mdbPath), - escapeshellarg($query) + 'echo %s | mdb-sql -P -F -d %s %s', + escapeshellarg($query), + escapeshellarg("\t"), + escapeshellarg($mdbPath) ); $output = shell_exec($command); @@ -664,6 +881,55 @@ private function queryMdb(string $mdbPath, string $query): array 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 */ @@ -675,16 +941,35 @@ private function parseMdbOutput(string $output): array } $headers = array_map('trim', explode("\t", array_shift($lines))); + $headerCount = count($headers); $data = []; + $currentValues = []; foreach ($lines as $line) { - if (empty(trim($line))) { + if ($line === '' && empty($currentValues)) { continue; } $values = explode("\t", $line); - $row = array_combine($headers, $values); - $data[] = $row; + if (empty($currentValues)) { + $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; @@ -696,11 +981,14 @@ private function parseMdbOutput(string $output): array private function generateProtocollo(int $gestioneId, array $operazione): array { $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 [ '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), ]; } diff --git a/database/migrations/2026_07_06_135806_make_ripartizione_spese_id_nullable_on_piano_rateizzazione_table.php b/database/migrations/2026_07_06_135806_make_ripartizione_spese_id_nullable_on_piano_rateizzazione_table.php new file mode 100644 index 0000000..787d2b3 --- /dev/null +++ b/database/migrations/2026_07_06_135806_make_ripartizione_spese_id_nullable_on_piano_rateizzazione_table.php @@ -0,0 +1,30 @@ +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(); + }); + } +}; diff --git a/database/migrations/2026_07_06_151000_create_missing_staging_rate_tables.php b/database/migrations/2026_07_06_151000_create_missing_staging_rate_tables.php new file mode 100644 index 0000000..1dac01f --- /dev/null +++ b/database/migrations/2026_07_06_151000_create_missing_staging_rate_tables.php @@ -0,0 +1,89 @@ +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 + } +}; diff --git a/database/migrations/2026_07_06_152000_add_rata_id_to_incassi_pagamenti.php b/database/migrations/2026_07_06_152000_add_rata_id_to_incassi_pagamenti.php new file mode 100644 index 0000000..87bf081 --- /dev/null +++ b/database/migrations/2026_07_06_152000_add_rata_id_to_incassi_pagamenti.php @@ -0,0 +1,33 @@ +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'); + } + }); + } +}; diff --git a/database/migrations/2026_07_06_183000_create_staging_operazioni_table.php b/database/migrations/2026_07_06_183000_create_staging_operazioni_table.php new file mode 100644 index 0000000..1636533 --- /dev/null +++ b/database/migrations/2026_07_06_183000_create_staging_operazioni_table.php @@ -0,0 +1,75 @@ +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'); + } +}; diff --git a/resources/views/admin/stabili/tabs/gestioni.blade.php b/resources/views/admin/stabili/tabs/gestioni.blade.php index 97d7af3..6c121dd 100755 --- a/resources/views/admin/stabili/tabs/gestioni.blade.php +++ b/resources/views/admin/stabili/tabs/gestioni.blade.php @@ -2,14 +2,33 @@ $gestioniTab = request()->query('gestioni_tab', 'gestioni'); $gestioniTab = in_array($gestioniTab, ['gestioni', 'periodi', 'straordinarie', 'archivio'], true) ? $gestioniTab : 'gestioni'; $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 ? \App\Models\GestioneContabile::query()->find($this->gestioneSelezionataId) : ($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; $checklistItems = \App\Models\ChecklistFineAnnoItem::query() ->where('is_active', true) diff --git a/resources/views/filament/pages/condomini/servizi-stabile-archivio.blade.php b/resources/views/filament/pages/condomini/servizi-stabile-archivio.blade.php index b1e193b..ed7eee5 100755 --- a/resources/views/filament/pages/condomini/servizi-stabile-archivio.blade.php +++ b/resources/views/filament/pages/condomini/servizi-stabile-archivio.blade.php @@ -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' }}"> Servizi / Utenze (tabella) + @@ -363,6 +368,8 @@ class="rounded border-cyan-300 text-cyan-600 focus:ring-cyan-500"> + +
Archivio legacy operazioni AC1 + AC2 (collegamento fatture)
@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 }}
@endif + + @if($acquaTab === 'pagamenti_cbill') +
+
Pagamenti abbinati tramite codice CBILL
+
I seguenti movimenti bancari sono stati associati alle fatture elettroniche dello stabile tramite il codice CBILL estratto.
+
+ + + + + + + + + + + + + + + @forelse($this->acquaCbillPagamenti as $p) + + + + + + + + + + + @empty + + + + @endforelse + +
Data movimentoCodice CBILLFornitoreFattura Elettronica di origineTotale fattura €Descrizione movimento bancarioImporto pagato €Azioni
{{ \Carbon\Carbon::parse($p['data'])->format('d/m/Y') }}{{ $p['cbill'] }}{{ $p['fornitore'] }} + @if($p['fattura_numero'] !== 'β€”') + N. {{ $p['fattura_numero'] }} del {{ \Carbon\Carbon::parse($p['fattura_data'])->format('d/m/Y') }} + @else + β€” + @endif + {{ $p['fattura_totale'] > 0 ? number_format((float) $p['fattura_totale'], 2, ',', '.') : 'β€”' }}{{ $p['descrizione'] }}{{ number_format((float) $p['importo'], 2, ',', '.') }} + @if(!empty($p['fattura_id'])) + + + + + Vedi FE + + @endif + + + + + Vedi Movimenti + +
Nessun pagamento trovato abbinato tramite codice CBILL.
+
+
+ @endif diff --git a/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php b/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php index 1992103..01d2f7d 100755 --- a/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php +++ b/resources/views/filament/pages/contabilita/casse-banche-movimenti.blade.php @@ -84,6 +84,7 @@ Movimenti Struttura e gestioni Riconciliazione + Associazione Causali @if($this->hubTab === 'conti') @@ -622,6 +623,90 @@ {{ $this->table }} + @elseif($this->hubTab === 'associazione_causali') + + Associazione Causali e Regole Bancarie + Configura i conti Dare e Avere per ciascun codice causale rilevato nei movimenti banca. + + @php($assocRows = $this->getCausaliAssociazioneRows()) + +
+ + + + + + + + + + + + + + @forelse($assocRows as $row) + + + + + + + + + + @empty + + + + @endforelse + +
Causale bancariaDescrizione rilevataMovimentiConto Dare (Entrate)Conto Avere (Uscite)Stato regolaAzioni
+ {{ $row['causale'] }} + + {{ $row['descrizione'] }} + + {{ $row['count'] }} + + @if($row['conto_dare']) + + {{ $row['conto_dare'] }} + + @else + β€” + @endif + + @if($row['conto_avere']) + + {{ $row['conto_avere'] }} + + @else + β€” + @endif + + @if($row['attiva']) + + + Attiva + + @else + + + Configurazione mancante + + @endif + + + {{ $row['attiva'] ? 'Modifica regola' : 'Configura regola' }} + +
+ Nessun movimento banca importato con codici causale rilevati. +
+
+
@endif @endif diff --git a/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php b/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php index 7819deb..08a4efe 100755 --- a/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php +++ b/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php @@ -1,13 +1,30 @@ @php $record = $record ?? null; + $details = $record ? $record->dettaglio_estrato : []; @endphp @if(!$record) -
Nessun movimento selezionato.
+
Nessun movimento selezionato.
@else -
-
-
ID #{{ $record->id }}
+
+ +
+
+ + ID #{{ $record->id }} + + @if(!empty($details['abi'])) + + ABI: {{ $details['abi'] }} + + @endif + @if(!empty($details['cbill_code'])) + + CBILL: {{ $details['cbill_code'] }} + + @endif +
+
@if(!empty($prevId)) ← Prec @@ -18,76 +35,83 @@
-
-
-
Data
-
{{ optional($record->data)->format('d/m/Y') ?: 'β€”' }}
-
-
-
Valuta
-
{{ optional($record->valuta)->format('d/m/Y') ?: 'β€”' }}
-
-
-
Importo
-
€ {{ number_format((float) $record->importo, 2, ',', '.') }}
-
-
-
Causale
-
{{ $record->causale ?? 'β€”' }}
-
-
-
Descrizione
-
{{ $record->descrizione ?? 'β€”' }}
-
-
-
Descrizione estesa
-
{{ $record->descrizione_estesa_pulita ?? $record->descrizione_estesa ?? 'β€”' }}
-
-
-
Saldo progressivo
-
- @if(isset($record->saldo_progressivo) && is_numeric($record->saldo_progressivo)) - € {{ number_format((float) $record->saldo_progressivo, 2, ',', '.') }} - @else - β€” - @endif + +
+ +
+
+ Importo + + € {{ number_format((float) $record->importo, 2, ',', '.') }} + +
+ +
+
+ Data Operazione + + {{ optional($record->data)->format('d/m/Y') ?: 'β€”' }} + +
+
+ Data Valuta + + {{ optional($record->valuta)->format('d/m/Y') ?: 'β€”' }} + +
+
+ +
+ Causale + + {{ $record->causale ?? 'β€”' }} +
-
-
File sorgente
-
{{ $record->source_file ?? 'β€”' }}
-
-
-
Disp
-
{{ is_array($record->match_data ?? null) ? ($record->match_data['cod_disp'] ?? 'β€”') : 'β€”' }}
-
-
-
Cash
-
{{ is_array($record->match_data ?? null) ? ($record->match_data['cash'] ?? 'β€”') : 'β€”' }}
-
-
-
Mittente
-
{{ is_array($record->match_data ?? null) ? ($record->match_data['mittente'] ?? 'β€”') : 'β€”' }}
-
-
-
Riga raw importata
-
{{ $record->raw_line ?? 'β€”' }}
+ + +
+ +
+ Messaggio Standard Banca +
+ {{ $details['messaggio_banca'] }} +
+
+ + +
+ Riferimento / Dettaglio Cliente +
+ {{ $details['messaggio_cliente'] }} +
+
+ + +
+ Commissioni, Spese e Codice TRN +
+ {{ $details['commissioni_spese'] }} +
+
- @php - $match = $record->match_data; - @endphp - @if(is_array($match) && !empty($match)) -
-
Dati estratti
-
    - @foreach($match as $k => $v) -
  • {{ $k }}: {{ is_scalar($v) ? $v : json_encode($v) }}
  • - @endforeach -
-
- @endif - + +
+
+ + Visualizza Tracciato Originale (Raw Line) + + + + + + +
+ {{ $record->raw_line ?? 'β€”' }} +
+
+
@endif diff --git a/resources/views/filament/pages/contabilita/prima-nota-modifica.blade.php b/resources/views/filament/pages/contabilita/prima-nota-modifica.blade.php index 9623c25..15da5f5 100755 --- a/resources/views/filament/pages/contabilita/prima-nota-modifica.blade.php +++ b/resources/views/filament/pages/contabilita/prima-nota-modifica.blade.php @@ -8,14 +8,55 @@ ]" /> - -
- {{ $this->form }} -
- Salva - Annulla -
-
-
+
+
+
+ {{ $this->form }} +
+ + @php($hdr = $this->getHeaderSummary()) + @php($delta = (float) ($hdr['delta'] ?? 0)) + @php($isBilanciata = (bool) ($hdr['is_bilanciata'] ?? false)) + + + Totali e Quadratura +
+
+
Totale Dare
+
€ {{ number_format((float) ($hdr['totale_dare'] ?? 0), 2, ',', '.') }}
+
+
+
Totale Avere
+
€ {{ number_format((float) ($hdr['totale_avere'] ?? 0), 2, ',', '.') }}
+
+
+
Quadratura (delta)
+
€ {{ number_format($delta, 2, ',', '.') }}
+
+ @if($isBilanciata) + Registrazione bilanciata + @else + Sbilancio: € {{ number_format(abs($delta), 2, ',', '.') }} + @endif +
+
+
+
+
+ +
+ + Funzioni base +
+ + Memorizza + + + Annulla + +
+
+
+
diff --git a/resources/views/filament/pages/contabilita/voci-spesa-prospetto.blade.php b/resources/views/filament/pages/contabilita/voci-spesa-prospetto.blade.php index 6152273..0d88f49 100755 --- a/resources/views/filament/pages/contabilita/voci-spesa-prospetto.blade.php +++ b/resources/views/filament/pages/contabilita/voci-spesa-prospetto.blade.php @@ -6,6 +6,28 @@
+ @if(($this->tipoGestioneTab ?? 'ordinaria') === 'acqua') +
+
+ + + + +
+
Configurazione e Letture Acqua
+
Le tariffe, i consumi e le letture dell'acqua sono gestite nella sezione Servizi e Utenze dello Stabile.
+
+
+ + Vai a Servizi / Utenze Acqua + +
+ @endif
@php $tabs = [