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; } // 3. Riconciliazione automatica con Estratti Conto Bancari $recSummary = $this->reconcileStabileAccountingAndBank($stabile, $log); $log("======================================================================"); $log("🎉 Sincronizzazione contabile completata con successo per Stabile {$codStabile}!"); return [ 'success' => true, 'stabile' => $codStabile, 'gestioni' => $results, 'riconciliazione' => $recSummary, ]; } /** * 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); $soggettoId = null; if ($unitaId && Schema::hasTable('persone_unita_relazioni')) { $targetRole = ($condInq === 'I') ? 'inquilino' : 'proprietario'; $targetRuoloRate = ($condInq === '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; } $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, 'condomino_id' => $soggettoId, '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++; } } // 4. Operazione Contabile Generale (Partita Doppia / Hub Ordinarie) if (Schema::hasTable('operazioni_contabili')) { $voceSpesaId = null; if ($codSpe !== '' && Schema::hasTable('voci_spesa')) { $voceSpesaId = DB::table('voci_spesa') ->where('gestione_contabile_id', $gestione->id) ->where(function ($q) use ($codSpe) { $q->where('codice', $codSpe)->orWhere('legacy_codice', $codSpe); }) ->value('id'); } $isEntrata = str_starts_with(strtoupper($codSpe), 'INC'); $dare = ($natura2 === 'Spesa' || ! $isEntrata) ? $importo : 0.0; $avere = $isEntrata ? $importo : 0.0; $feFatturaId = null; if (Schema::hasTable('contabilita_fatture_fornitori') && $fornitoreId && $numFat !== '') { $feFatturaId = DB::table('contabilita_fatture_fornitori') ->where('stabile_id', $stabile->id) ->where('fornitore_id', $fornitoreId) ->where('numero_documento', $numFat) ->value('id'); } $protNum = is_numeric($nSpe) && (int) $nSpe > 0 ? (int) $nSpe : (int) ($idOperaz ?: $countOps + 1); DB::table('operazioni_contabili')->updateOrInsert( [ 'gestione_id' => $gestione->id, 'legacy_id' => (int) ($idOperaz ?: $protNum), ], [ 'tenant_id' => 'default', 'fornitore_id' => $fornitoreId, 'voce_spesa_id' => $voceSpesaId, 'cod_spe' => $codSpe ?: null, 'tabella' => $tabella ?: null, 'cod_for' => $codFor ?: null, 'num_fat' => $numFat ?: null, 'dt_fat' => $dtFat, 'rif_rda' => $rifRda ?: null, 'fe_fattura_id' => $feFatturaId, 'descrizione' => $benef ?: "Operazione {$codSpe} ({$tabella})", 'compet' => in_array(strtoupper(trim((string)($r['compet'] ?? ''))), ['C', 'P']) ? strtoupper(trim((string)$r['compet'])) : 'C', 'natura2' => $natura2 ?: 'Spesa', 'n_stra' => $nStra, 'protocollo_numero' => $protNum, 'protocollo_completo' => "{$gestioneTipo}{$anno}-" . str_pad((string) $protNum, 3, '0', STR_PAD_LEFT), 'dare' => $dare, 'avere' => $avere, 'conto_contabile' => $codSpe ?: null, 'stato_operazione' => 'confermata', 'data_operazione' => $dtSpe ?: "{$anno}-01-01", 'data_competenza' => $dtSpe ?: "{$anno}-01-01", 'updated_at' => now(), ] ); } 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; } /** * Riconciliazione automatica deterministica tra Estratti Conto Bancari, Incassi e Spese */ public function reconcileStabileAccountingAndBank(Stabile $stabile, ?\Closure $log = null): array { $log = $log ?: fn($msg) => null; $stabileId = (int) $stabile->id; if (! Schema::hasTable('contabilita_movimenti_banca')) { return ['reconciled_incassi' => 0, 'reconciled_spese' => 0]; } $log("🔄 Avvio riconciliazione contabile ed estratti conto per Stabile #{$stabileId}..."); $movements = DB::table('contabilita_movimenti_banca') ->where('stabile_id', $stabileId) ->get(); if ($movements->isEmpty()) { $log("ℹ️ Nessun movimento bancario registrato per questo stabile."); return ['reconciled_incassi' => 0, 'reconciled_spese' => 0]; } // 1. Precarica Unità Immobiliari e Nominativi $unitaList = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->get(['id', 'codice_unita', 'scala', 'interno', 'piano']); $relazioni = DB::table('persone_unita_relazioni as r') ->join('unita_immobiliari as u', 'u.id', '=', 'r.unita_id') ->join('anagrafiche as a', 'a.id', '=', 'r.persona_id') ->where('u.stabile_id', $stabileId) ->where('r.attivo', 1) ->select([ 'r.unita_id', 'r.persona_id', 'r.ruolo_rate', 'a.nome', 'a.cognome', 'a.ragione_sociale', 'u.scala', 'u.interno', ])->get(); // 2. Precarica Fornitori $fornitori = DB::table('fornitori')->get(['id', 'cod_forn', 'ragione_sociale', 'partita_iva', 'codice_fiscale']); // 3. Precarica Operazioni Contabili $operazioni = DB::table('operazioni_contabili as o') ->join('gestioni_contabili as g', 'g.id', '=', 'o.gestione_id') ->where('g.stabile_id', $stabileId) ->select([ 'o.id', 'o.fornitore_id', 'o.dare', 'o.num_fat', 'o.data_operazione', 'o.descrizione', 'o.cod_spe', ])->get(); // 4. Precarica Incassi $incassi = DB::table('incassi') ->where('condominio_id', $stabileId) ->get(['id', 'condomino_id', 'cod_cond_gescon', 'cond_inquil', 'importo_pagato_euro', 'data_pagamento']); $countRecIncassi = 0; $countRecSpese = 0; foreach ($movements as $mov) { $imp = (float) $mov->importo; $dataMov = (string) $mov->data; $mittente = strtoupper(trim((string) ($mov->mittente ?? ''))); $beneficiario = strtoupper(trim((string) ($mov->beneficiario ?? ''))); $desc = strtoupper(trim((string) ($mov->descrizione ?? ''))); $descEst = strtoupper(trim((string) ($mov->descrizione_estesa ?? ''))); $fullText = $mittente . ' ' . $beneficiario . ' ' . $desc . ' ' . $descEst; $updateMov = []; if ($imp > 0) { // ENTRATA / INCASSO $matchedUnitaId = null; $matchedPersonaId = null; // Cerca per nominativo anagrafica foreach ($relazioni as $rel) { $cog = strtoupper(trim((string) $rel->cognome)); $nom = strtoupper(trim((string) $rel->nome)); $rag = strtoupper(trim((string) $rel->ragione_sociale)); if ($cog !== '' && strlen($cog) >= 3 && str_contains($fullText, $cog)) { $matchedUnitaId = (int) $rel->unita_id; $matchedPersonaId = (int) $rel->persona_id; break; } if ($rag !== '' && strlen($rag) >= 3 && str_contains($fullText, $rag)) { $matchedUnitaId = (int) $rel->unita_id; $matchedPersonaId = (int) $rel->persona_id; break; } } // Cerca per scala/interno (es. "INT. 6", "B 6", "INT.6") if (! $matchedUnitaId) { foreach ($unitaList as $u) { $int = strtoupper(trim((string) $u->interno)); $sc = strtoupper(trim((string) $u->scala)); if ($int !== '') { if (preg_match('/\bINT(?:ERNO)?\.?\s*' . preg_quote($int, '/') . '\b/i', $fullText) || preg_match('/\b' . preg_quote($sc, '/') . '\s*' . preg_quote($int, '/') . '\b/i', $fullText)) { $matchedUnitaId = (int) $u->id; break; } } } } // Cerca incasso corrispondente $matchedIncasso = null; if ($matchedUnitaId) { $matchedIncasso = $incassi->first(function ($inc) use ($imp) { return abs((float) $inc->importo_pagato_euro - $imp) < 0.01; }); } else { $matchedIncasso = $incassi->first(function ($inc) use ($imp, $dataMov) { return abs((float) $inc->importo_pagato_euro - $imp) < 0.01 && abs(strtotime((string) $inc->data_pagamento) - strtotime($dataMov)) <= 86400 * 45; }); } if ($matchedIncasso) { $updateMov['incasso_id'] = $matchedIncasso->id; $updateMov['rubrica_mittente_id'] = $matchedIncasso->condomino_id ?: $matchedPersonaId; $updateMov['stato_riconciliazione'] = 'riconciliato_incasso'; $updateMov['da_confermare'] = false; DB::table('incassi')->where('id', $matchedIncasso->id)->update([ 'movimento_bancario_id' => $mov->id, 'stato_riconciliazione' => 'automatica', ]); $countRecIncassi++; } elseif ($matchedUnitaId) { $updateMov['unita_immobiliare_id'] = $matchedUnitaId; if ($matchedPersonaId) { $updateMov['rubrica_mittente_id'] = $matchedPersonaId; } $updateMov['stato_riconciliazione'] = 'riconciliato_incasso'; $updateMov['da_confermare'] = false; $countRecIncassi++; } else { $updateMov['stato_riconciliazione'] = 'da_riconciliare'; $updateMov['da_confermare'] = true; } if ($matchedUnitaId) { $updateMov['unita_immobiliare_id'] = $matchedUnitaId; } } else { // USCITA / SPESA $absImp = abs($imp); $fornId = $mov->fornitore_id; if (! $fornId && $beneficiario !== '') { foreach ($fornitori as $f) { $fRag = strtoupper(trim((string) $f->ragione_sociale)); if ($fRag !== '' && strlen($fRag) >= 4 && str_contains($fullText, $fRag)) { $fornId = (int) $f->id; $updateMov['fornitore_id'] = $fornId; break; } } } $tipoOp = $mov->tipo_operazione; if (in_array($tipoOp, ['f24', 'delega_f24'], true) || str_contains($fullText, 'AGENZIA ENTRATE') || str_contains($fullText, 'I24')) { $updateMov['stato_riconciliazione'] = 'f24_tributi'; $updateMov['da_confermare'] = false; $countRecSpese++; } elseif (in_array($tipoOp, ['cbill', 'pagamento_utenze_cbill'], true) || ! empty($mov->codice_cbill) || str_contains($fullText, 'CBILL')) { $updateMov['stato_riconciliazione'] = 'utenza_cbill'; $updateMov['da_confermare'] = false; $countRecSpese++; } elseif (in_array($tipoOp, ['commissioni', 'competenze', 'imposta_bollo', 'spese_banca'], true) || str_contains($fullText, 'COMM.') || str_contains($fullText, 'INT. E COMP.')) { $updateMov['stato_riconciliazione'] = 'spese_bancarie'; $updateMov['da_confermare'] = false; $countRecSpese++; } elseif (in_array($tipoOp, ['addebito_sdd', 'sdd'], true) || ! empty($mov->codice_sdd)) { $updateMov['stato_riconciliazione'] = 'utenza_sdd'; $updateMov['da_confermare'] = false; $countRecSpese++; } elseif ($fornId) { // Match con operazioni_contabili $rifFat = trim((string) ($mov->rif_documento ?? '')); $matchedOp = $operazioni->first(function ($op) use ($fornId, $absImp, $rifFat) { if ((int) $op->fornitore_id !== (int) $fornId) { return false; } if ($rifFat !== '' && ! empty($op->num_fat) && str_contains($rifFat, (string) $op->num_fat)) { return true; } return abs((float) $op->dare - $absImp) < 0.01; }); if ($matchedOp) { $updateMov['operazione_contabile_id'] = $matchedOp->id; $updateMov['stato_riconciliazione'] = 'riconciliato_spesa'; $updateMov['da_confermare'] = false; DB::table('operazioni_contabili')->where('id', $matchedOp->id)->update([ 'movimento_bancario_id' => $mov->id, ]); $countRecSpese++; } else { $updateMov['stato_riconciliazione'] = 'da_riconciliare'; $updateMov['da_confermare'] = false; } } else { $updateMov['stato_riconciliazione'] = 'da_riconciliare'; $updateMov['da_confermare'] = true; } } if (! empty($updateMov)) { $updateMov['updated_at'] = now(); DB::table('contabilita_movimenti_banca') ->where('id', $mov->id) ->update($updateMov); } } $log("✅ Riconciliazione completata: {$countRecIncassi} incassi/crediti riconciliati, {$countRecSpese} spese/uscite/tributi riconciliate."); return [ 'reconciled_incassi' => $countRecIncassi, 'reconciled_spese' => $countRecSpese, ]; } }