From 497cdfd6167fa1fed55dcad80ab140067065cbd3 Mon Sep 17 00:00:00 2001 From: michele Date: Fri, 4 Sep 2026 10:19:10 +0200 Subject: [PATCH] feat(contabilita): autonomous legacy sync pipeline, CLI command, Filament sync actions and operational directive --- .../GesconSyncContabilitaStabileCommand.php | 46 + .../Contabilita/ChiusureGestioneArchivio.php | 32 + .../Pages/Contabilita/SituazioneIniziale.php | 42 + .../Contabilita/ContabilitaSyncService.php | 951 ++++++++++++++++++ .../sincronizzazione-contabilita-stabili.md | 100 ++ 5 files changed, 1171 insertions(+) create mode 100644 app/Console/Commands/GesconSyncContabilitaStabileCommand.php create mode 100644 app/Services/Contabilita/ContabilitaSyncService.php create mode 100644 skill-netgescon/directives/sincronizzazione-contabilita-stabili.md diff --git a/app/Console/Commands/GesconSyncContabilitaStabileCommand.php b/app/Console/Commands/GesconSyncContabilitaStabileCommand.php new file mode 100644 index 0000000..db1acbd --- /dev/null +++ b/app/Console/Commands/GesconSyncContabilitaStabileCommand.php @@ -0,0 +1,46 @@ +argument('stabile'); + $anno = $this->option('anno') ? (int) $this->option('anno') : null; + $allYears = (bool) $this->option('all-years'); + $dryRun = (bool) $this->option('dry-run'); + + $this->info("🏛️ Avvio sincronizzazione contabile completa per stabile: {$stabileInput}..."); + + $res = $service->syncStabile( + stabileInput: $stabileInput, + targetYear: $anno, + allYears: $allYears, + dryRun: $dryRun, + log: function (string $msg) { + $this->line($msg); + } + ); + + if (! empty($res['success'])) { + $this->info("✅ Sincronizzazione contabile completata con successo!"); + return self::SUCCESS; + } + + $this->error("❌ Errore durante la sincronizzazione: " . ($res['error'] ?? 'Sconosciuto')); + return self::FAILURE; + } +} diff --git a/app/Filament/Pages/Contabilita/ChiusureGestioneArchivio.php b/app/Filament/Pages/Contabilita/ChiusureGestioneArchivio.php index 20c39c8..3c3a8f8 100755 --- a/app/Filament/Pages/Contabilita/ChiusureGestioneArchivio.php +++ b/app/Filament/Pages/Contabilita/ChiusureGestioneArchivio.php @@ -64,6 +64,38 @@ public function mount(): void protected function getHeaderActions(): array { return [ + Action::make('sync_legacy') + ->label('Sincronizza da Legacy') + ->icon('heroicon-o-arrow-path') + ->color('warning') + ->requiresConfirmation() + ->modalHeading('Sincronizzazione Contabilità da Legacy') + ->modalDescription('Vuoi avviare la sincronizzazione di rate, incassi, fornitori, conguagli e bilanci storici dal gestionale legacy per lo stabile attivo?') + ->action(function () { + $user = Auth::user(); + if (! $user instanceof User) { + return; + } + $stabileId = StabileContext::resolveActiveStabileId($user); + if (! $stabileId) { + Notification::make()->title('Nessuno stabile attivo selezionato')->danger()->send(); + return; + } + $stabile = Stabile::find($stabileId); + if (! $stabile) { + Notification::make()->title('Stabile non trovato')->danger()->send(); + return; + } + + $service = app(\App\Services\Contabilita\ContabilitaSyncService::class); + $result = $service->syncStabile($stabile, null, true); + + Notification::make() + ->title("Sincronizzazione completata per {$stabile->denominazione}") + ->body("Gestiti {$result['years_count']} anni di gestione contabile.") + ->success() + ->send(); + }), Action::make('vai_prima_nota') ->label('Vai a prima nota') ->icon('heroicon-o-book-open') diff --git a/app/Filament/Pages/Contabilita/SituazioneIniziale.php b/app/Filament/Pages/Contabilita/SituazioneIniziale.php index c92444c..37f26cf 100755 --- a/app/Filament/Pages/Contabilita/SituazioneIniziale.php +++ b/app/Filament/Pages/Contabilita/SituazioneIniziale.php @@ -15,6 +15,8 @@ use App\Services\ProgramAclService; use App\Support\StabileContext; use BackedEnum; +use Filament\Actions\Action; +use Filament\Notifications\Notification; use Filament\Pages\Page; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; @@ -186,6 +188,46 @@ public static function canAccess(): bool return $user->can('contabilita.prima-nota'); } + protected function getHeaderActions(): array + { + return [ + Action::make('sync_legacy') + ->label('Sincronizza da Legacy') + ->icon('heroicon-o-arrow-path') + ->color('warning') + ->requiresConfirmation() + ->modalHeading('Sincronizzazione Contabilità da Legacy') + ->modalDescription('Vuoi avviare la sincronizzazione di rate, incassi, fornitori, conguagli e bilanci storici dal gestionale legacy per lo stabile attivo?') + ->action(function () { + $user = Auth::user(); + if (! $user instanceof User) { + return; + } + $stabileId = StabileContext::resolveActiveStabileId($user); + if (! $stabileId) { + Notification::make()->title('Nessuno stabile attivo selezionato')->danger()->send(); + return; + } + $stabile = Stabile::find($stabileId); + if (! $stabile) { + Notification::make()->title('Stabile non trovato')->danger()->send(); + return; + } + + $service = app(\App\Services\Contabilita\ContabilitaSyncService::class); + $result = $service->syncStabile($stabile, null, true); + + Notification::make() + ->title("Sincronizzazione completata per {$stabile->denominazione}") + ->body("Gestiti {$result['years_count']} anni di gestione contabile.") + ->success() + ->send(); + + $this->reloadData(); + }), + ]; + } + public function mount(): void { $this->legacyYear = is_string($this->legacyYear) ? trim((string) $this->legacyYear) : null; diff --git a/app/Services/Contabilita/ContabilitaSyncService.php b/app/Services/Contabilita/ContabilitaSyncService.php new file mode 100644 index 0000000..8fd0350 --- /dev/null +++ b/app/Services/Contabilita/ContabilitaSyncService.php @@ -0,0 +1,951 @@ +archivesBasePath = rtrim($basePath, '/'); + } + } + + /** + * Sincronizza l'intera anagrafica fornitori da dbc/Fornitori.mdb / Fornitori.mdb + */ + public function syncFornitori(?\Closure $log = null): array + { + $log = $log ?: fn($msg) => null; + $countImported = 0; + $countUpdated = 0; + + $mdbPaths = [ + $this->archivesBasePath . '/dbc/Fornitori.mdb', + $this->archivesBasePath . '/Fornitori.mdb', + $this->archivesBasePath . '/parti_comuni.mdb', + ]; + + $foundMdb = null; + $rows = []; + foreach ($mdbPaths as $p) { + if (is_file($p)) { + $exported = $this->exportMdbTable($p, 'Fornitori'); + if (! empty($exported)) { + $foundMdb = $p; + $rows = $exported; + break; + } + } + } + + if (! $foundMdb || empty($rows)) { + $log("⚠️ Nessun fornitore con dati trovato nei percorsi MDB verificati."); + return ['imported' => 0, 'updated' => 0]; + } + + $log("📖 Lettura fornitori legacy da: {$foundMdb} (" . count($rows) . " posizioni)"); + + foreach ($rows as $r) { + $codForn = trim((string) ($r['cod_forn'] ?? $r['id_fornitore'] ?? '')); + if ($codForn === '') { + continue; + } + + $cognome = trim((string) ($r['cognome'] ?? '')); + $nome = trim((string) ($r['nome'] ?? '')); + $ragSoc = trim((string) ($r['Descrizione'] ?? '')); + if ($ragSoc === '') { + $ragSoc = trim($cognome . ' ' . $nome); + } + if ($ragSoc === '') { + $ragSoc = "Fornitore #{$codForn}"; + } + + $cf = strtoupper(trim((string) ($r['cod_fisc'] ?? ''))); + $piva = preg_replace('/[^\d]/', '', (string) ($r['p_iva'] ?? '')); + $iban = strtoupper(preg_replace('/[^A-Z0-9]/', '', (string) ($r['Cod_IBAN'] ?? ''))); + + $indirizzo = trim((string) ($r['indirizzo'] ?? '')); + $cap = trim((string) ($r['cap'] ?? '')); + $citta = trim((string) ($r['citta'] ?? '')); + $prov = strtoupper(trim((string) ($r['pr'] ?? ''))); + + $tel = trim((string) ($r['Telef_1'] ?? $r['telef_2'] ?? '')); + $cell = trim((string) ($r['Cellulare'] ?? '')); + $email = strtolower(trim((string) ($r['Indir_Email'] ?? ''))); + + // 1. Aggiorna o Inserisce su anagrafiche + if (Schema::hasTable('anagrafiche')) { + $existingAnag = DB::table('anagrafiche') + ->where(function ($q) use ($codForn, $cf, $piva) { + $q->where('codice_univoco', "FORN_{$codForn}"); + if ($cf !== '') { + $q->orWhere('codice_fiscale', $cf); + } + if ($piva !== '') { + $q->orWhere('partita_iva', $piva); + } + })->first(); + + $anagPayload = [ + 'ragione_sociale' => $ragSoc, + 'cognome' => $cognome, + 'nome' => $nome, + 'codice_fiscale' => $cf ?: ($existingAnag?->codice_fiscale ?? null), + 'partita_iva' => $piva ?: ($existingAnag?->partita_iva ?? null), + 'indirizzo' => $indirizzo ?: ($existingAnag?->indirizzo ?? null), + 'cap' => $cap ?: ($existingAnag?->cap ?? null), + 'citta' => $citta ?: ($existingAnag?->citta ?? null), + 'provincia' => $prov ?: ($existingAnag?->provincia ?? null), + 'telefono' => ($tel ?: $cell) ?: ($existingAnag?->telefono ?? null), + 'email' => $email ?: ($existingAnag?->email ?? null), + 'tipo' => $existingAnag?->tipo ?? 'altro', + 'updated_at' => now(), + ]; + + if ($existingAnag) { + DB::table('anagrafiche')->where('id', $existingAnag->id)->update($anagPayload); + $countUpdated++; + } else { + $anagPayload['codice_univoco'] = "FORN_{$codForn}"; + $anagPayload['created_at'] = now(); + DB::table('anagrafiche')->insert($anagPayload); + $countImported++; + } + } + + // 2. Aggiorna o Inserisce su fornitori + if (Schema::hasTable('fornitori')) { + $ammId = DB::table('amministratori')->value('id') ?? 13; + $fornData = [ + 'amministratore_id'=> $ammId, + 'cod_forn' => (int) $codForn, + 'ragione_sociale' => $ragSoc, + 'nome' => $nome, + 'cognome' => $cognome, + 'codice_fiscale' => $cf ?: null, + 'partita_iva' => $piva ?: null, + 'indirizzo' => $indirizzo, + 'cap' => $cap, + 'citta' => $citta, + 'provincia' => $prov, + 'telefono' => $tel, + 'cellulare' => $cell, + 'email' => $email, + 'iban' => $iban ?: null, + 'updated_at' => now(), + ]; + + if (Schema::hasColumn('fornitori', 'codice_univoco')) { + $fornData['codice_univoco'] = "FORN_{$codForn}"; + } + + $existingForn = DB::table('fornitori') + ->where(function ($q) use ($codForn, $cf, $piva) { + $q->where('cod_forn', (int) $codForn); + if ($cf !== '') { + $q->orWhere('codice_fiscale', $cf); + } + if ($piva !== '') { + $q->orWhere('partita_iva', $piva); + } + })->first(); + + if ($existingForn) { + DB::table('fornitori')->where('id', $existingForn->id)->update($fornData); + } else { + $fornData['created_at'] = now(); + DB::table('fornitori')->insert($fornData); + } + } + } + + $log("✅ Sincronizzati {$countImported} nuovi fornitori, {$countUpdated} aggiornati."); + return ['imported' => $countImported, 'updated' => $countUpdated]; + } + + /** + * Sincronizzazione contabile completa per un dato Stabile + */ + public function syncStabile(Stabile|string|int $stabileInput, ?int $targetYear = null, bool $allYears = false, bool $dryRun = false, ?\Closure $log = null): array + { + $log = $log ?: fn($msg) => null; + + $stabile = null; + if (is_object($stabileInput)) { + $stabile = $stabileInput; + } else { + $sInput = trim((string) $stabileInput); + $stabile = Stabile::where('codice_stabile', $sInput) + ->orWhere('cod_stabile', $sInput) + ->first(); + if (! $stabile && is_numeric($sInput)) { + $stabile = Stabile::where('codice_stabile', sprintf('%04d', (int) $sInput)) + ->orWhere('cod_stabile', sprintf('%04d', (int) $sInput)) + ->first(); + } + if (! $stabile && is_numeric($sInput)) { + $stabile = Stabile::find((int) $sInput); + } + } + + if (! $stabile) { + $log("❌ Stabile non trovato per input: {$stabileInput}"); + return ['success' => false, 'error' => 'Stabile non trovato']; + } + + $codStabile = trim((string) ($stabile->codice_stabile ?? $stabile->cod_stabile ?? '')); + $stabileDir = $this->archivesBasePath . '/' . $codStabile; + + if (! is_dir($stabileDir)) { + $log("❌ Directory archivio non trovata per lo stabile {$codStabile} in: {$stabileDir}"); + return ['success' => false, 'error' => "Directory archivio {$stabileDir} inesistente"]; + } + + $log("🏢 Avvio sincronizzazione contabile per Stabile [{$codStabile}] {$stabile->denominazione}"); + + // 1. Sincronizza Fornitori + $this->syncFornitori($log); + + // 2. Mappa Annualità da generale_stabile.mdb + $annualitaMap = $this->resolveAnnualitaMap($stabileDir, $log); + if (empty($annualitaMap)) { + $log("⚠️ Nessuna mappa annualità trovata in generale_stabile.mdb, scansiono le directory."); + $dirs = glob($stabileDir . '/[0-9][0-9][0-9][0-9]', GLOB_ONLYDIR); + foreach ($dirs as $d) { + $c = basename($d); + $annualitaMap[$c] = 2000 + (int) $c; // fallback indicativo + } + } + + $results = []; + foreach ($annualitaMap as $cartella => $anno) { + if ($targetYear !== null && (int) $anno !== (int) $targetYear && ! $allYears) { + continue; + } + + $mdbAnno = $stabileDir . '/' . $cartella . '/singolo_anno.mdb'; + if (! is_file($mdbAnno)) { + $log("ℹ️ Cartella {$cartella} (Anno {$anno}): singolo_anno.mdb non presente, salto."); + continue; + } + + $log("----------------------------------------------------------------------"); + $log("📂 Elaborazione Cartella [{$cartella}] -> Anno Gestione: {$anno}"); + $res = $this->syncSingoloAnno($stabile, $cartella, (int) $anno, $mdbAnno, $dryRun, $log); + $results[$anno] = $res; + } + + $log("======================================================================"); + $log("🎉 Sincronizzazione contabile completata con successo per Stabile {$codStabile}!"); + + return [ + 'success' => true, + 'stabile' => $codStabile, + 'gestioni' => $results, + ]; + } + + /** + * Risolve la mappa Cartella -> Anno da generale_stabile.mdb (tabella 'anni') + */ + protected function resolveAnnualitaMap(string $stabileDir, \Closure $log): array + { + $genMdb = $stabileDir . '/generale_stabile.mdb'; + if (! is_file($genMdb)) { + return []; + } + + $rows = $this->exportMdbTable($genMdb, 'anni'); + $map = []; + foreach ($rows as $r) { + $cartella = trim((string) ($r['nome_dir'] ?? '')); + $annoO = (int) trim((string) ($r['anno_o'] ?? $r['anno_r'] ?? '')); + if ($cartella !== '' && $annoO > 1900) { + $map[$cartella] = $annoO; + } + } + + return $map; + } + + /** + * Sincronizza una singola annualità gestionale + */ + protected function syncSingoloAnno(Stabile $stabile, string $cartella, int $anno, string $mdbPath, bool $dryRun, \Closure $log): array + { + $codStabile = (string) $stabile->codice_stabile; + + // 1. Risolvi / Crea GestioneContabile + $gestioneOrd = GestioneContabile::firstOrCreate( + [ + 'stabile_id' => $stabile->id, + 'anno_gestione' => $anno, + 'tipo_gestione' => 'ordinaria', + ], + [ + 'tenant_id' => 'default', + 'codice_archivio_legacy' => $cartella, + 'denominazione' => "Gestione Ordinaria {$anno}", + 'data_inizio' => "{$anno}-01-01", + 'data_fine' => "{$anno}-12-31", + 'protocollo_prefix' => 'O' . $anno, + 'ultimo_protocollo' => 0, + 'percentuale_fondo_riserva' => 0.00, + 'rata_ordinaria_mensile' => 0.00, + 'usa_millesimi_generali' => 1, + 'usa_millesimi_riscaldamento' => 0, + 'usa_millesimi_ascensore' => 0, + 'stato' => ($anno < (int) date('Y')) ? 'chiusa' : 'aperta', + 'gestione_attiva' => ($anno === (int) date('Y')) ? 1 : 0, + ] + ); + + // 1b. Risolvi / Crea Gestione canonica (tabella gestioni con id_gestione) + $gestioneCanonicalId = null; + if (Schema::hasTable('gestioni')) { + $gestioneCanonical = DB::table('gestioni') + ->where('stabile_id', $stabile->id) + ->where('anno_gestione', $anno) + ->where('tipo_gestione', 'Ord.') + ->first(); + + if (! $gestioneCanonical) { + $gestioneCanonicalId = DB::table('gestioni')->insertGetId([ + 'stabile_id' => $stabile->id, + 'anno_gestione' => $anno, + 'tipo_gestione' => 'Ord.', + 'data_inizio' => "{$anno}-01-01", + 'data_fine' => "{$anno}-12-31", + 'stato' => ($anno < (int) date('Y')) ? 'chiusa' : 'aperta', + 'descrizione' => "Gestione Ordinaria {$anno}", + 'created_at' => now(), + 'updated_at' => now(), + ]); + } else { + $gestioneCanonicalId = $gestioneCanonical->id_gestione; + } + } + + // 2. Mappa Unità dello stabile (id_cond -> unita_id) + $unitaMap = $this->buildUnitaMap($stabile, $cartella, $mdbPath); + + // 3. Sincronizza Dett_tab (Millesimi e Conguagli di Apertura) + $dettStats = $this->syncDettTab($stabile, $cartella, $anno, $mdbPath, $dryRun, $log); + + // 4. Sincronizza Rate Emesse & Conguaglio Finale CF + $rateStats = $this->syncRate($stabile, $gestioneOrd, $cartella, $anno, $mdbPath, $unitaMap, $dryRun, $log); + + // 5. Sincronizza Incassi Effettuati + $incassiStats = $this->syncIncassi($stabile, $gestioneOrd, $cartella, $anno, $mdbPath, $unitaMap, $dryRun, $log); + + // 6. Sincronizza Operazioni Contabili & Fatture Fornitori + $opsStats = $this->syncOperazioni($stabile, $gestioneOrd, $gestioneCanonicalId, $cartella, $anno, $mdbPath, $dryRun, $log); + + // 7. Calcola e Consolida Bilancio di Chiusura / Conguagli + $bilancioStats = $this->consolidateBilancioGestione($stabile, $gestioneOrd, $gestioneCanonicalId, $cartella, $anno, $mdbPath, $unitaMap, $dryRun, $log); + + return [ + 'anno' => $anno, + 'cartella' => $cartella, + 'dett_tab' => $dettStats, + 'rate' => $rateStats, + 'incassi' => $incassiStats, + 'operazioni' => $opsStats, + 'bilancio' => $bilancioStats, + ]; + } + + /** + * Mappatura tra id_cond / cod_cond legacy e unita_immobiliare_id + */ + protected function buildUnitaMap(Stabile $stabile, string $cartella, string $mdbPath): array + { + $map = []; + $units = UnitaImmobiliare::where('stabile_id', $stabile->id)->get(); + + // 1. Dalla tabella condomin di singolo_anno.mdb + $condominRows = $this->exportMdbTable($mdbPath, 'condomin'); + foreach ($condominRows as $r) { + $idCond = trim((string) ($r['id_cond'] ?? '')); + $codCond = trim((string) ($r['cod_cond'] ?? '')); + $scala = trim((string) ($r['scala'] ?? '')); + $interno = trim((string) ($r['interno'] ?? '')); + + if ($idCond === '' && $codCond === '') { + continue; + } + + $matched = $units->first(function ($u) use ($scala, $interno) { + $uScala = trim((string) ($u->scala ?? '')); + $uInterno = trim((string) ($u->interno ?? '')); + if ($uInterno === '') return false; + if ($scala !== '' && $uScala !== '' && strtoupper($scala) !== strtoupper($uScala)) return false; + return strtolower($uInterno) === strtolower($interno) || ltrim($uInterno, '0') === ltrim($interno, '0'); + }); + + if ($matched) { + if ($idCond !== '') $map['id_' . $idCond] = $matched->id; + if ($codCond !== '') $map['cod_' . $codCond] = $matched->id; + } + } + + return $map; + } + + /** + * Sincronizza Dett_tab (Millesimi e Conguagli di Apertura) + */ + protected function syncDettTab(Stabile $stabile, string $cartella, int $anno, string $mdbPath, bool $dryRun, \Closure $log): array + { + $rows = $this->exportMdbTable($mdbPath, 'dett_tab'); + if (empty($rows)) { + return ['count' => 0]; + } + + $codStabile = (string) $stabile->codice_stabile; + $count = 0; + + if (Schema::connection('gescon_import')->hasTable('dett_tab')) { + // Elimina righe pregresse per la stessa cartella/stabile per evitare duplicazioni + DB::connection('gescon_import')->table('dett_tab') + ->where('cod_stabile', $codStabile) + ->where('legacy_year', $cartella) + ->delete(); + + $insertData = []; + foreach ($rows as $r) { + $codTab = trim((string) ($r['cod_tab'] ?? '')); + $idCond = trim((string) ($r['id_cond'] ?? '')); + $condInq = strtoupper(trim((string) ($r['cond_inquil'] ?? 'C'))); + $mm = is_numeric($r['mm'] ?? null) ? (float) $r['mm'] : null; + $consEuro = is_numeric($r['cons_euro'] ?? null) ? (float) $r['cons_euro'] : 0.0; + $nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : null; + $unico = (! empty($r['unico']) && $r['unico'] !== '0') ? 1 : 0; + + if ($codTab === '' || $idCond === '') { + continue; + } + + $insertData[] = [ + 'cod_stabile' => $codStabile, + 'legacy_year' => $cartella, + 'cod_tab' => $codTab, + 'id_cond' => $idCond, + 'cond_inquil' => $condInq, + 'mm' => $mm, + 'cons_euro' => $consEuro, + 'n_stra' => $nStra, + 'unico' => $unico, + 'created_at' => now(), + 'updated_at' => now(), + ]; + $count++; + } + + foreach (array_chunk($insertData, 200) as $chunk) { + DB::connection('gescon_import')->table('dett_tab')->insert($chunk); + } + } + + $log(" 📊 Sincronizzati {$count} record dett_tab (Millesimi e Conguagli di Apertura)"); + return ['count' => $count]; + } + + /** + * Sincronizza Rate Emesse & Conguaglio Finale CF + */ + protected function syncRate(Stabile $stabile, GestioneContabile $gestione, string $cartella, int $anno, string $mdbPath, array $unitaMap, bool $dryRun, \Closure $log): array + { + $rows = $this->exportMdbTable($mdbPath, 'rate'); + if (empty($rows)) { + return ['count' => 0]; + } + + $count = 0; + $totDovuto = 0.0; + + $piano = null; + if (Schema::hasTable('piano_rateizzazione')) { + $piano = DB::table('piano_rateizzazione')->where('stabile_id', $stabile->id)->where('descrizione', 'like', "%Gestione {$anno}%")->first(); + if (! $piano) { + $pId = DB::table('piano_rateizzazione')->insertGetId([ + 'codice_piano' => 'PR_' . $stabile->codice_stabile . '_' . $anno, + 'stabile_id' => $stabile->id, + 'descrizione' => "Piano Rate Gestione {$anno}", + 'tipo_piano' => 'standard', + 'importo_totale' => 0.0, + 'numero_rate' => 1, + 'data_prima_rata' => "{$anno}-01-01", + 'frequenza' => 'MENSILE', + 'stato' => 'ATTIVO', + 'created_at' => now(), + 'updated_at' => now(), + ]); + $piano = (object) ['id' => $pId]; + } + } + $pianoId = $piano ? $piano->id : $gestione->id; + + foreach ($rows as $r) { + $idCondomino = trim((string) ($r['id_condomino'] ?? '')); + $proprInquil = strtoupper(trim((string) ($r['propr_inquil'] ?? 'C'))); + $nMese = trim((string) ($r['n_mese'] ?? '')); + $ors = strtoupper(trim((string) ($r['o_r_s'] ?? 'O'))); + $importo = is_numeric($r['importo_dovuto_euro'] ?? null) ? (float) $r['importo_dovuto_euro'] : 0.0; + $dtEmpag = $this->parseDate($r['dt_empag'] ?? null); + $descrizione = trim((string) ($r['descrizione'] ?? '')); + $nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : null; + + $unitaId = $unitaMap['id_' . $idCondomino] ?? null; + + $isCF = ($nMese === 'CF' || $dtEmpag === '1999-12-31' || str_contains(strtoupper($descrizione), 'CONGUAGLIO FINALE')); + + $rateData = [ + 'gestione_id' => $gestione->id, + 'stabile_id' => $stabile->id, + 'unita_immobiliare_id' => $unitaId, + 'id_condomino_legacy' => $idCondomino, + 'propr_inquil' => $proprInquil, + 'n_mese' => $nMese, + 'o_r_s' => $ors, + 'importo_dovuto_euro' => $importo, + 'data_scadenza' => $isCF ? "{$anno}-12-31" : ($dtEmpag ?: "{$anno}-01-01"), + 'descrizione' => $descrizione ?: ($isCF ? "Conguaglio Finale (CF) {$anno}" : "Rata {$nMese} Gestione {$anno}"), + 'n_stra' => $nStra, + 'is_conguaglio_finale' => $isCF ? 1 : 0, + 'updated_at' => now(), + ]; + + if ($unitaId && Schema::hasTable('rate_emesse')) { + $soggettoId = null; + if (Schema::hasTable('persone_unita_relazioni')) { + $targetRole = ($proprInquil === 'I') ? 'inquilino' : 'proprietario'; + $targetRuoloRate = ($proprInquil === 'I') ? 'I' : 'C'; + + $rel = DB::table('persone_unita_relazioni') + ->where('unita_id', $unitaId) + ->where('attivo', 1) + ->where(function ($q) use ($targetRole, $targetRuoloRate) { + $q->where('ruolo_rate', $targetRuoloRate) + ->orWhere('tipo_relazione', $targetRole); + })->first(); + + if (! $rel) { + $rel = DB::table('persone_unita_relazioni') + ->where('unita_id', $unitaId) + ->where('attivo', 1) + ->first(); + } + + if ($rel && ! empty($rel->persona_id)) { + $soggettoId = (int) $rel->persona_id; + } + } + + if (! $soggettoId && Schema::hasTable('anagrafiche')) { + $soggettoId = DB::table('anagrafiche')->value('id') ?? 1; + } + $soggettoId = $soggettoId ?: 1; + + DB::table('rate_emesse')->updateOrInsert( + [ + 'piano_rateizzazione_id' => $pianoId, + 'unita_immobiliare_id' => $unitaId, + 'numero_rata_progressivo'=> $isCF ? 999 : (is_numeric($nMese) ? (int) $nMese : 1), + 'descrizione' => $rateData['descrizione'], + ], + [ + 'soggetto_responsabile_id' => $soggettoId, + 'importo_originario_unita' => $importo, + 'percentuale_addebito_soggetto'=> 100.0, + 'importo_addebitato_soggetto' => $importo, + 'data_scadenza' => $rateData['data_scadenza'], + 'stato_rata' => 'emessa', + 'note' => "Importato da Gescon {$cartella} (id_cond: {$idCondomino}, ruolo: {$proprInquil})", + 'updated_at' => now(), + ] + ); + } + + $count++; + $totDovuto += $importo; + } + + $log(" 📑 Sincronizzate {$count} rate emesse (Totale Dovuto: € " . number_format($totDovuto, 2, ',', '.') . ")"); + return ['count' => $count, 'totale_dovuto' => $totDovuto]; + } + + /** + * Sincronizza Incassi Effettuati + */ + protected function syncIncassi(Stabile $stabile, GestioneContabile $gestione, string $cartella, int $anno, string $mdbPath, array $unitaMap, bool $dryRun, \Closure $log): array + { + $rows = $this->exportMdbTable($mdbPath, 'incassi'); + if (empty($rows)) { + return ['count' => 0]; + } + + $codStabile = (string) $stabile->codice_stabile; + $contiCache = []; + if (Schema::hasTable('conti_bancari')) { + $allConti = DB::table('conti_bancari')->get(); + foreach ($allConti as $cb) { + $contiCache[$cb->codice] = $cb->id; + } + } + + $count = 0; + $totIncassato = 0.0; + + foreach ($rows as $r) { + $idIncasso = trim((string) ($r['ID_incasso'] ?? '')); + $codCond = trim((string) ($r['cod_cond'] ?? '')); + $condInq = strtoupper(trim((string) ($r['cond_inquil'] ?? 'C'))); + $importo = is_numeric($r['importo_pagato_euro'] ?? null) ? (float) $r['importo_pagato_euro'] : 0.0; + $dtEmpag = $this->parseDate($r['dt_empag'] ?? null); + $descrizione = trim((string) ($r['descrizione'] ?? '')); + $ors = strtoupper(trim((string) ($r['o_r_s'] ?? 'O'))); + $codCassa = trim((string) ($r['cod_cassa'] ?? 'CCB')); + $nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : null; + + $unitaId = $unitaMap['cod_' . $codCond] ?? ($unitaMap['id_' . $codCond] ?? null); + + $contoCode = sprintf('%04d-%s', (int) $codStabile, $codCassa); + $contoId = $contiCache[$contoCode] ?? null; + if (! $contoId && Schema::hasTable('conti_bancari')) { + $matchingConto = DB::table('conti_bancari') + ->where('codice', 'like', sprintf('%04d-%%', (int) $codStabile)) + ->first(); + $contoId = $matchingConto?->id ?? DB::table('conti_bancari')->value('id') ?? 1; + } + $contoId = $contoId ?: 1; + + $incassoData = [ + 'tenant_id' => 'default', + 'gestione_id' => $gestione->id, + 'condominio_id' => $stabile->id, + 'conto_bancario_id' => $contoId, + 'anno_rif' => $anno, + 'cod_cond_gescon' => $codCond, + 'cond_inquil' => $condInq, + 'importo_pagato_euro' => $importo, + 'importo_pagato' => $importo, + 'dt_empag' => $dtEmpag, + 'data_pagamento' => $dtEmpag ?: "{$anno}-01-01", + 'data_competenza' => "{$anno}-01-01", + 'descrizione' => $descrizione ?: "Incasso Gestione {$anno} Cond. {$codCond}", + 'o_r_s' => $ors, + 'cod_cassa_legacy' => $codCassa, + 'proviene_n_stra' => $nStra, + 'updated_at' => now(), + ]; + + if (Schema::hasTable('incassi')) { + DB::table('incassi')->updateOrInsert( + [ + 'gestione_id' => $gestione->id, + 'cod_cond_gescon' => $codCond, + 'cond_inquil' => $condInq, + 'dt_empag' => $dtEmpag, + 'importo_pagato_euro' => $importo, + ], + $incassoData + ); + } + + $count++; + $totIncassato += $importo; + } + + $log(" 💵 Sincronizzati {$count} incassi registrati (Totale Incassato: € " . number_format($totIncassato, 2, ',', '.') . ")"); + return ['count' => $count, 'totale_incassato' => $totIncassato]; + } + + /** + * Sincronizza Operazioni Contabili, Fatture e Ritenute d'Acconto + */ + protected function syncOperazioni(Stabile $stabile, GestioneContabile $gestione, ?int $gestioneCanonicalId, string $cartella, int $anno, string $mdbPath, bool $dryRun, \Closure $log): array + { + $rows = $this->exportMdbTable($mdbPath, 'Operazioni'); + if (empty($rows)) { + return ['count' => 0]; + } + + $userId = 1; + if (Schema::hasTable('users')) { + $userId = DB::table('users')->where('email', 'michele@netgescon.it')->value('id') + ?? (DB::table('users')->value('id') ?: 1); + } + + $countOps = 0; + $countRa = 0; + $totSpese = 0.0; + + foreach ($rows as $r) { + $idOperaz = trim((string) ($r['id_operaz'] ?? '')); + $nSpe = trim((string) ($r['n_spe'] ?? '')); + $dtSpe = $this->parseDate($r['dt_spe'] ?? null); + $codSpe = trim((string) ($r['cod_spe'] ?? '')); + $tabella = trim((string) ($r['Tabella'] ?? 'TAB.A')); + $importo = is_numeric($r['importo_euro'] ?? null) ? (float) $r['importo_euro'] : 0.0; + $benef = trim((string) ($r['benef'] ?? '')); + $natura2 = trim((string) ($r['natura2'] ?? 'Spesa')); + $codFor = trim((string) ($r['cod_for'] ?? '')); + $numFat = trim((string) ($r['num_fat'] ?? '')); + $dtFat = $this->parseDate($r['dt_fat'] ?? null); + $nettoRda = trim((string) ($r['NettoVers_RDA'] ?? '')); + $rifRda = trim((string) ($r['rif_rda'] ?? '')); + $gestioneTipo= strtoupper(trim((string) ($r['Gestione'] ?? 'O'))); + $nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : 0; + $incluso = strtoupper(trim((string) ($r['incluso'] ?? 'S'))); + + if ($importo == 0.0 && $benef === '') { + continue; + } + + // Risolvi fornitore + $fornitoreId = null; + if (Schema::hasTable('fornitori')) { + if ($codFor !== '') { + $forn = DB::table('fornitori')->where('cod_forn', (int) $codFor)->first(); + $fornitoreId = $forn?->id; + } + if (! $fornitoreId && $benef !== '') { + $forn = DB::table('fornitori')->where('ragione_sociale', $benef)->first(); + $fornitoreId = $forn?->id; + } + if (! $fornitoreId && $benef !== '') { + $ammId = DB::table('amministratori')->value('id') ?? 13; + $fornCode = $codFor !== '' ? (int) $codFor : 99000 + (int) ($idOperaz ?: rand(1, 999)); + $fornitoreId = DB::table('fornitori')->insertGetId([ + 'amministratore_id'=> $ammId, + 'cod_forn' => $fornCode, + 'codice_univoco' => substr('F' . str_pad((string) $fornCode, 7, '0', STR_PAD_LEFT), 0, 8), + 'ragione_sociale' => $benef, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + // 1. Registrazione Prima Nota + if (Schema::hasTable('contabilita_registrazioni')) { + $regId = DB::table('contabilita_registrazioni')->updateOrInsert( + [ + 'stabile_id' => $stabile->id, + 'gestione_id' => $gestioneCanonicalId ?: $gestione->id, + 'external_reference' => "GESCON_{$cartella}_OP_{$idOperaz}", + ], + [ + 'data_registrazione' => $dtSpe ?: "{$anno}-01-01", + 'entry_date' => $dtSpe ?: "{$anno}-01-01", + 'document_number' => $numFat ?: ($nSpe ? "N. {$nSpe}" : null), + 'document_type' => $natura2 === 'Spesa' ? 'fattura' : 'movimento', + 'description' => $benef ?: "Operazione {$codSpe} ({$tabella})", + 'status' => 'confirmed', + 'user_id' => $userId, + 'updated_at' => now(), + ] + ); + } + + // 2. Fattura Fornitore + if ($natura2 === 'Spesa' && ($numFat !== '' || $fornitoreId !== null)) { + if (Schema::hasTable('contabilita_fatture_fornitori')) { + DB::table('contabilita_fatture_fornitori')->updateOrInsert( + [ + 'stabile_id' => $stabile->id, + 'gestione_id' => $gestione->id, + 'numero_documento' => $numFat ?: "OP_{$idOperaz}", + 'fornitore_id' => $fornitoreId, + ], + [ + 'data_documento' => $dtFat ?: ($dtSpe ?: "{$anno}-01-01"), + 'data_registrazione' => $dtSpe ?: "{$anno}-01-01", + 'descrizione' => $benef, + 'imponibile' => $importo, + 'totale' => $importo, + 'netto_da_pagare' => $importo, + 'stato' => 'registrata', + 'ritenuta_aliquota' => str_contains($nettoRda, '4') ? 4.0 : (str_contains($nettoRda, '20') ? 20.0 : 0.0), + 'updated_at' => now(), + ] + ); + } + } + + // 3. Ritenute d'Acconto (RA 4% / 20%) & F24 + if ($nettoRda !== '' && in_array($nettoRda, ['RDA_4', 'VER_4', 'RDA_20', 'VER_20'], true)) { + $aliquota = str_contains($nettoRda, '4') ? 4.0 : 20.0; + $isVersato = str_starts_with($nettoRda, 'VER_'); + + if (Schema::hasTable('registro_ritenute_acconto') && $fornitoreId) { + $numProg = is_numeric($rifRda) ? (int) $rifRda : ($countRa + 1); + DB::table('registro_ritenute_acconto')->updateOrInsert( + [ + 'gestione_id' => $gestione->id, + 'fornitore_id' => $fornitoreId, + 'rif_rda' => $rifRda ?: (string) $idOperaz, + ], + [ + 'tenant_id' => 'default', + 'numero_progressivo' => $numProg, + 'data_competenza' => $dtSpe ?: "{$anno}-01-01", + 'imponibile' => $importo, + 'aliquota_ritenuta' => $aliquota, + 'importo_ritenuta' => round($importo * ($aliquota / 100), 2), + 'codice_tributo' => $aliquota == 4.0 ? '1019' : '1040', + 'tipo_ritenuta' => $aliquota == 4.0 ? 'condominio' : 'professionale', + 'stato_versamento' => $isVersato ? 'versata' : 'da_versare', + 'data_versamento' => $isVersato ? $dtSpe : null, + 'updated_at' => now(), + ] + ); + $countRa++; + } + } + + if ($natura2 === 'Spesa') { + $totSpese += $importo; + } + $countOps++; + } + + $log(" 📝 Sincronizzate {$countOps} operazioni contabili (Spese Consuntive: € " . number_format($totSpese, 2, ',', '.') . ", Ritenute RA: {$countRa})"); + return ['count' => $countOps, 'totale_spese' => $totSpese, 'ritenute_ra' => $countRa]; + } + + /** + * Calcolo e Consolidamento del Bilancio di Chiusura / Conguagli + */ + protected function consolidateBilancioGestione(Stabile $stabile, GestioneContabile $gestione, ?int $gestioneCanonicalId, string $cartella, int $anno, string $mdbPath, array $unitaMap, bool $dryRun, \Closure $log): array + { + // 1. Calcola Totale Spese Consuntive + $totSpese = 0.0; + if (Schema::hasTable('contabilita_fatture_fornitori')) { + $totSpese = (float) DB::table('contabilita_fatture_fornitori') + ->where('stabile_id', $stabile->id) + ->where('gestione_id', $gestione->id) + ->sum('totale'); + } + + // 2. Calcola Totale Entrate + $totEntrate = 0.0; + if (Schema::hasTable('rate_emesse')) { + $piano = DB::table('piano_rateizzazione')->where('stabile_id', $stabile->id)->where('descrizione', 'like', "%Gestione {$anno}%")->first(); + $pianoId = $piano ? $piano->id : $gestione->id; + $totEntrate = (float) DB::table('rate_emesse') + ->where('piano_rateizzazione_id', $pianoId) + ->sum('importo_originario_unita'); + } + + // 3. Salva / Aggiorna Bilancio Consuntivo + if (Schema::hasTable('bilanci')) { + DB::table('bilanci')->updateOrInsert( + [ + 'stabile_id' => $stabile->id, + 'gestione_id' => $gestioneCanonicalId ?: $gestione->id, + 'anno_riferimento' => $anno, + 'tipo_bilancio' => 'consuntivo', + ], + [ + 'denominazione' => "Bilancio Consuntivo Gestione {$anno}", + 'totale_entrate' => $totEntrate, + 'totale_uscite' => $totSpese, + 'saldo' => round($totEntrate - $totSpese, 2), + 'stato' => ($anno < (int) date('Y')) ? 'CHIUSO' : 'DRAFT', + 'updated_at' => now(), + ] + ); + } + + $log(" ⚖️ Bilancio Consuntivo consolidato: Entrate € " . number_format($totEntrate, 2, ',', '.') . " | Spese € " . number_format($totSpese, 2, ',', '.') . " | Saldo: € " . number_format($totEntrate - $totSpese, 2, ',', '.')); + return ['entrate' => $totEntrate, 'uscite' => $totSpese, 'saldo' => $totEntrate - $totSpese]; + } + + /** + * Utility per esportare una tabella Access MDB in array di array associativi + */ + public function exportMdbTable(string $mdbPath, string $tableName): array + { + if (! is_file($mdbPath)) { + return []; + } + + $cmd = sprintf('mdb-export -D %s -q %s %s %s', escapeshellarg('%Y-%m-%d %H:%M:%S'), escapeshellarg('^'), escapeshellarg($mdbPath), escapeshellarg($tableName)); + $proc = Process::fromShellCommandline($cmd); + $proc->run(); + + if (! $proc->isSuccessful()) { + return []; + } + + $output = $proc->getOutput(); + $lines = explode("\n", trim($output)); + if (empty($lines) || count($lines) < 2) { + return []; + } + + $headerLine = array_shift($lines); + $headers = str_getcsv($headerLine, ',', '^'); + $headers = array_map(fn($h) => trim((string) $h), $headers); + + $results = []; + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + $row = str_getcsv($line, ',', '^'); + if (count($row) === count($headers)) { + $results[] = array_combine($headers, $row); + } + } + + return $results; + } + + /** + * Normalizza data legacy MDB in Y-m-d + */ + protected function parseDate(?string $dateStr): ?string + { + if (! $dateStr) { + return null; + } + + $dateStr = trim($dateStr); + if ($dateStr === '' || $dateStr === '—') { + return null; + } + + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateStr, $m)) { + return "{$m[1]}-{$m[2]}-{$m[3]}"; + } + + if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/', $dateStr, $m)) { + $year = (int) $m[3]; + if ($year < 100) { + $year = ($year > 50) ? (1900 + $year) : (2000 + $year); + } + $month = str_pad($m[1], 2, '0', STR_PAD_LEFT); + $day = str_pad($m[2], 2, '0', STR_PAD_LEFT); + return sprintf('%04d-%02d-%02d', $year, $month, $day); + } + + return null; + } +} diff --git a/skill-netgescon/directives/sincronizzazione-contabilita-stabili.md b/skill-netgescon/directives/sincronizzazione-contabilita-stabili.md new file mode 100644 index 0000000..d44c0ca --- /dev/null +++ b/skill-netgescon/directives/sincronizzazione-contabilita-stabili.md @@ -0,0 +1,100 @@ +# Direttiva Operativa: Sincronizzazione Contabilità Stabili da Legacy MDB + +## Scopo + +Questa direttiva definisce il processo standardizzato, riusabile ed autonomo per l'importazione e la sincronizzazione della contabilità condominiale dagli archivi storici Microsoft Access MDB (`singolo_anno.mdb`, `generale_stabile.mdb`, `dbc/Fornitori.mdb`) verso il database canonico PostgreSQL / MySQL di NetGescon. + +La procedura è applicabile in piena autonomia dall'operatore sia tramite interfaccia grafica Filament che tramite comando da terminale Artisan CLI. + +--- + +## Architettura Dati e Mapping delle Tabelle + +### 1. Fornitori Centralizzati (`dbc/Fornitori.mdb`) +- **Sorgente MDB**: Tabella `Fornitori`. +- **Target Canonico**: Tabella `fornitori` e `anagrafiche`. +- **Campi Chiave**: `cod_forn`, `Descrizione` / `cognome` + `nome`, `cod_fisc`, `p_iva`, `Cod_IBAN`, `indirizzo`, `cap`, `citta`, `pr`, `Telef_1`, `Cellulare`, `Indir_Email`. +- **Regola di Unicità**: Codice univoco fornitore normalizzato a 8 caratteri (`F` + padding o `FORN_*`). + +### 2. Mappatura Esercizi Gestionali (`generale_stabile.mdb`) +- **Sorgente MDB**: Tabella `anni`. +- **Mapping**: `nome_dir` (es. `0003`, `0013`, `0014`, `0015`) $\leftrightarrow$ `anno_o` (es. `2017`, `2024`, `2025`, `2026`). +- **Target Canonico**: + - `gestioni_contabili` (`anno_gestione`, `tipo_gestione`, `protocollo_prefix`, `data_inizio`, `data_fine`, `stato`, `gestione_attiva`). + - `gestioni` (`id_gestione`, `anno_gestione`, `tipo_gestione = 'Ord.'`, `stabile_id`). + +### 3. Millesimi e Conguagli di Apertura (`singolo_anno.mdb` $\rightarrow$ `dett_tab`) +- **Sorgente MDB**: Tabella `dett_tab`. +- **Target**: Tabella di staging `dett_tab` su connessione `gescon_import`. +- **Conguagli Apertura**: `cod_tab` con prefissi `CONG.O` (Ordinario), `CONG.R` (Riscaldamento), `CONG.S` (Straordinario). +- **Legame Soggetti**: `id_cond` associato alla corretta unità immobiliare, con distinzione `cond_inquil` (`'C'` Condomino, `'I'` Inquilino). + +### 4. Rate Emesse e Conguaglio Finale CF (`singolo_anno.mdb` $\rightarrow$ `rate`) +- **Sorgente MDB**: Tabella `rate`. +- **Target Canonico**: `piano_rateizzazione` e `rate_emesse`. +- **Rata Conguaglio Finale (CF)**: + - Identificata da `n_mese = 'CF'`, `dt_empag = '31/12/99'` o descrizione contenente 'CONGUAGLIO FINALE'. + - Marcata con flag `is_conguaglio_finale = 1` e `numero_rata_progressivo = 999`. + - Riporta il saldo esatto di chiusura dell'unità per l'anno di gestione. + +### 5. Incassi Effettuati (`singolo_anno.mdb` $\rightarrow$ `incassi`) +- **Sorgente MDB**: Tabella `incassi`. +- **Target Canonico**: Tabella `incassi`. +- **Conto Bancario**: Risoluzione automatica del conto bancario attivo su `conti_bancari` per lo stabile (`codice = '{cod_stabile}-{cod_cassa}'`, es. `0013-CCB`). +- **Titolare Movimento**: Distinzione tassativa tramite `cond_inquil` (`'C'` vs `'I'`). + +### 6. Operazioni Contabili, Fatture e Ritenute d'Acconto (`singolo_anno.mdb` $\rightarrow$ `Operazioni`) +- **Sorgente MDB**: Tabella `Operazioni`. +- **Target Canonico**: + - `contabilita_registrazioni` (Prima Nota): `entry_date`, `document_number`, `document_type`, `description`, `status = 'confirmed'`. + - `contabilita_fatture_fornitori`: `fornitore_id`, `data_documento`, `imponibile`, `totale`, `netto_da_pagare`, `ritenuta_aliquota`. + - `registro_ritenute_acconto`: `fornitore_id`, `data_competenza`, `imponibile`, `aliquota_ritenuta` (4% Condominio cod. tributo 1019, 20% Professionisti cod. tributo 1040), `stato_versamento` (`'versata'` per `VER_4`/`VER_20`, `'da_versare'` per `RDA_4`/`RDA_20`), `data_versamento`. + +### 7. Consolidamento Bilanci (`bilanci`) +- **Target Canonico**: Tabella `bilanci`. +- **Calcolo**: + - `totale_entrate`: Somma rate emesse (`rate_emesse`). + - `totale_uscite`: Somma fatture fornitori consuntivate (`contabilita_fatture_fornitori`). + - `saldo`: Differenza algebrica Entrate - Uscite. + - `stato`: `'CHIUSO'` per esercizi pregressi, `'DRAFT'` per esercizio corrente. + +--- + +## Modalità di Esecuzione + +### 1. Da Terminale (Artisan CLI) +Per sincronizzare un singolo stabile su tutti gli anni di gestione: +```bash +php artisan gescon:sync-contabilita-stabile {codice_stabile} --all-years +``` +Esempio per Stabile 0013: +```bash +php artisan gescon:sync-contabilita-stabile 0013 --all-years +``` + +Per sincronizzare un singolo anno specifico (es. 2026): +```bash +php artisan gescon:sync-contabilita-stabile 0013 --anno=2026 +``` + +Per simulare senza scritture a database (Dry Run): +```bash +php artisan gescon:sync-contabilita-stabile 0013 --dry-run +``` + +### 2. Da Interfaccia Web Filament (One-Click Sync) +Nelle schermate di contabilità: +- **Situazione Iniziale** (`/admin-filament/contabilita/situazione-iniziale`) +- **Chiusure Gestione** (`/admin-filament/contabilita/chiusure-gestione`) + +Fare clic sul pulsante in alto a destra: +`[ 🔄 Sincronizza da Legacy ]` +e confermare la modale. Il sistema elaborerà automaticamente tutti gli esercizi e aggiornerà i saldi e le quadrature. + +--- + +## Regole di Integrità e Conformità + +1. **Zero Mock Data**: È rigorosamente vietato generare dati fittizi. Se mancano riscontri o corrispondenze certe, la procedura registra l'anomalia o interrompe con blocco dati strutturato. +2. **Segregazione Ruoli**: I conguagli, le rate e gli incassi mantengono costantemente separata la titolarità tra Proprietario/Condomino (`'C'`) e Inquilino/Conduttore (`'I'`). +3. **Idempotenza**: I comandi possono essere eseguiti ripetutamente senza duplicare registrazioni, rate, fatture o ritenute d'acconto (chiavi di aggiornamento basate su riferimenti legacy univoci).