diff --git a/app/Filament/Pages/Gescon/Ordinarie.php b/app/Filament/Pages/Gescon/Ordinarie.php index 7c464c9..e12c373 100755 --- a/app/Filament/Pages/Gescon/Ordinarie.php +++ b/app/Filament/Pages/Gescon/Ordinarie.php @@ -783,11 +783,11 @@ public function getOperazioniProperty() $voceSnap = !empty($row->voce_spesa_snapshot) ? json_decode($row->voce_spesa_snapshot) : null; $fornSnap = !empty($row->fornitore_snapshot) ? json_decode($row->fornitore_snapshot) : null; - $row->cod_spe = $voceSnap->legacy_codice ?? $voceSnap->codice ?? $row->cod_spe ?? null; - $row->voce_descrizione = $voceSnap->descrizione ?? null; - $row->cod_for = $fornSnap->cod_forn ?? $fornSnap->old_id ?? null; - $row->fornitore_nome = $fornSnap->ragione_sociale ?? trim(($fornSnap->nome ?? '') . ' ' . ($fornSnap->cognome ?? '')) ?: null; - $row->tabella = $voceSnap->legacy_tabella ?? null; + $row->cod_spe = $row->cod_spe ?? $voceSnap->legacy_codice ?? $voceSnap->codice ?? null; + $row->voce_descrizione = $row->voce_descrizione ?? $voceSnap->descrizione ?? null; + $row->cod_for = $row->cod_for ?? $fornSnap->cod_forn ?? $fornSnap->old_id ?? null; + $row->fornitore_nome = $row->fornitore_nome ?? $fornSnap->ragione_sociale ?? trim(($fornSnap->nome ?? '') . ' ' . ($fornSnap->cognome ?? '')) ?: null; + $row->tabella = $row->tabella ?? $voceSnap->legacy_tabella ?? null; } } @@ -1146,12 +1146,14 @@ public function getOperazioniProperty() }; $codFor = trim((string) ($row->cod_for ?? '')); $codForNoZero = ltrim($codFor, '0'); - $row->fornitore_nome = $fornitoriMap[$codFor] ?? ($codForNoZero !== '' ? ($fornitoriMap[$codForNoZero] ?? null) : null); - $row->fornitore_id = null; - if ($codFor !== '') { - $legacyId = $codFornToLegacyId[$codFor] ?? ($codForNoZero !== '' ? ($codFornToLegacyId[$codForNoZero] ?? null) : null); - if ($legacyId && isset($fornitoriIdMap[$legacyId])) { - $row->fornitore_id = (int) $fornitoriIdMap[$legacyId]; + $row->fornitore_nome = $row->fornitore_nome ?? $fornitoriMap[$codFor] ?? ($codForNoZero !== '' ? ($fornitoriMap[$codForNoZero] ?? null) : null); + if (empty($row->fornitore_id)) { + $row->fornitore_id = null; + if ($codFor !== '') { + $legacyId = $codFornToLegacyId[$codFor] ?? ($codForNoZero !== '' ? ($codFornToLegacyId[$codForNoZero] ?? null) : null); + if ($legacyId && isset($fornitoriIdMap[$legacyId])) { + $row->fornitore_id = (int) $fornitoriIdMap[$legacyId]; + } } } @@ -3717,7 +3719,9 @@ private function buildOperazioniQuery() { if ($this->netgesconWorkflowMode) { $query = DB::table('operazioni_contabili as o') - ->join('gestioni_contabili as g', 'g.id', '=', 'o.gestione_id'); + ->join('gestioni_contabili as g', 'g.id', '=', 'o.gestione_id') + ->leftJoin('fornitori as f', 'f.id', '=', 'o.fornitore_id') + ->leftJoin('voci_spesa as v', 'v.id', '=', 'o.voce_spesa_id'); $activeStabileId = StabileContext::resolveActiveStabileId(Auth::user()); if ($activeStabileId) { @@ -3740,6 +3744,10 @@ private function buildOperazioniQuery() $query->where(function ($q) use ($s) { $q->where('o.descrizione', 'like', $s) ->orWhere('o.conto_contabile', 'like', $s) + ->orWhere('o.cod_spe', 'like', $s) + ->orWhere('o.num_fat', 'like', $s) + ->orWhere('o.rif_rda', 'like', $s) + ->orWhere('f.ragione_sociale', 'like', $s) ->orWhere('o.protocollo_completo', 'like', $s); }); } @@ -3748,6 +3756,35 @@ private function buildOperazioniQuery() $query->where('g.anno_gestione', (int) $this->filterAnno); } + if ($this->filterCodForn) { + $query->where(function ($q) { + $q->where('o.cod_for', $this->filterCodForn) + ->orWhere('f.cod_forn', (int) $this->filterCodForn); + }); + } + + if ($this->filterCodSpe) { + $query->where(function ($q) { + $q->where('o.cod_spe', $this->filterCodSpe) + ->orWhere('o.conto_contabile', $this->filterCodSpe); + }); + } + + if ($this->filterTabella) { + $query->where('o.tabella', $this->filterTabella); + } + + if ($this->filterBenef) { + $query->where(function ($q) { + $q->where('o.descrizione', 'like', '%' . $this->filterBenef . '%') + ->orWhere('f.ragione_sociale', 'like', '%' . $this->filterBenef . '%'); + }); + } + + if ($this->filterRdaOnly) { + $query->whereNotNull('o.rif_rda'); + } + $query->select([ 'o.id as id_operaz', 'o.protocollo_numero as n_spe', @@ -3755,15 +3792,22 @@ private function buildOperazioniQuery() 'g.anno_gestione as legacy_year', DB::raw("case when g.tipo_gestione = 'ordinaria' then 'O' when g.tipo_gestione = 'riscaldamento' then 'R' else 'S' end as gestione"), 'o.compet', - 'o.conto_contabile as cod_spe', - DB::raw("null as tabella"), - DB::raw("null as cod_for"), + DB::raw("coalesce(o.cod_spe, o.conto_contabile) as cod_spe"), + 'o.tabella as tabella', + DB::raw("coalesce(o.cod_for, cast(f.cod_forn as char)) as cod_for"), 'o.descrizione as benef', + DB::raw("coalesce(f.ragione_sociale, o.descrizione) as fornitore_nome"), + 'v.descrizione as voce_descrizione', + 'o.fornitore_id', + 'o.voce_spesa_id', 'o.n_stra', 'o.protocollo_completo', DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"), - DB::raw("null as num_fat"), - DB::raw("null as fe_uid"), + 'o.num_fat', + 'o.dt_fat', + 'o.rif_rda', + 'o.fe_fattura_id', + 'o.movimento_bancario_id', 'o.voce_spesa_snapshot', 'o.fornitore_snapshot', ]); diff --git a/app/Models/OperazioneContabile.php b/app/Models/OperazioneContabile.php new file mode 100644 index 0000000..214dc00 --- /dev/null +++ b/app/Models/OperazioneContabile.php @@ -0,0 +1,89 @@ + 'decimal:4', + 'avere' => 'decimal:4', + 'data_operazione' => 'date', + 'data_competenza' => 'date', + 'dt_fat' => 'date', + 'voce_spesa_snapshot' => 'array', + 'fornitore_snapshot' => 'array', + 'storico_modifiche' => 'array', + ]; + + public function gestione(): BelongsTo + { + return $this->belongsTo(GestioneContabile::class, 'gestione_id'); + } + + public function fornitore(): BelongsTo + { + return $this->belongsTo(Fornitore::class, 'fornitore_id'); + } + + public function voceSpesa(): BelongsTo + { + return $this->belongsTo(VoceSpesa::class, 'voce_spesa_id'); + } + + public function movimentoBancario(): BelongsTo + { + return $this->belongsTo(MovimentoBanca::class, 'movimento_bancario_id'); + } + + public function contoBancario(): BelongsTo + { + return $this->belongsTo(DatiBancari::class, 'conto_bancario_id'); + } +} diff --git a/app/Modules/Contabilita/Models/MovimentoBanca.php b/app/Modules/Contabilita/Models/MovimentoBanca.php index 5648622..41f27ab 100755 --- a/app/Modules/Contabilita/Models/MovimentoBanca.php +++ b/app/Modules/Contabilita/Models/MovimentoBanca.php @@ -25,6 +25,21 @@ class MovimentoBanca extends Model 'descrizione_estesa', 'importo', 'causale', + 'tipo_operazione', + 'mittente', + 'beneficiario', + 'rif_documento', + 'rif_disposizione', + 'codice_cbill', + 'codice_sdd', + 'cod_abi', + 'cod_cab', + 'banca_nome', + 'categoria_banca', + 'unita_immobiliare_id', + 'incasso_id', + 'operazione_contabile_id', + 'stato_riconciliazione', 'source_file', 'raw_line', 'row_hash', @@ -50,6 +65,11 @@ public function getMittenteAttribute(): ?string return (string) ($this->rubricaMittente->ragione_sociale ?: trim(($this->rubricaMittente->cognome ?? '') . ' ' . ($this->rubricaMittente->nome ?? ''))); } + $val = $this->attributes['mittente'] ?? null; + if (! empty($val)) { + return (string) $val; + } + if (is_array($this->match_data) && ! empty($this->match_data['mittente'])) { return (string) $this->match_data['mittente']; } @@ -67,6 +87,11 @@ public function getBeneficiarioAttribute(): ?string return (string) ($this->rubricaBeneficiario->ragione_sociale ?: trim(($this->rubricaBeneficiario->cognome ?? '') . ' ' . ($this->rubricaBeneficiario->nome ?? ''))); } + $val = $this->attributes['beneficiario'] ?? null; + if (! empty($val)) { + return (string) $val; + } + if (is_array($this->match_data) && ! empty($this->match_data['beneficiario'])) { return (string) $this->match_data['beneficiario']; } @@ -76,6 +101,11 @@ public function getBeneficiarioAttribute(): ?string public function getTipoOperazioneAttribute(): ?string { + $val = $this->attributes['tipo_operazione'] ?? null; + if (! empty($val)) { + return (string) $val; + } + if (is_array($this->match_data) && ! empty($this->match_data['tipo'])) { return (string) $this->match_data['tipo']; } @@ -85,6 +115,11 @@ public function getTipoOperazioneAttribute(): ?string public function getRifDocumentoAttribute(): ?string { + $val = $this->attributes['rif_documento'] ?? null; + if (! empty($val)) { + return (string) $val; + } + if (is_array($this->match_data)) { if (! empty($this->match_data['rif_fattura'])) { $doc = (string) $this->match_data['rif_fattura']; @@ -329,5 +364,20 @@ public function stabile() { return $this->belongsTo(Stabile::class, 'stabile_id'); } + + public function unitaImmobiliare() + { + return $this->belongsTo(\App\Models\UnitaImmobiliare::class, 'unita_immobiliare_id'); + } + + public function incasso() + { + return $this->belongsTo(\App\Models\Incasso::class, 'incasso_id'); + } + + public function operazioneContabile() + { + return $this->belongsTo(\App\Models\OperazioneContabile::class, 'operazione_contabile_id'); + } } diff --git a/app/Services/Contabilita/ContabilitaSyncService.php b/app/Services/Contabilita/ContabilitaSyncService.php index 8fd0350..2aa1e12 100644 --- a/app/Services/Contabilita/ContabilitaSyncService.php +++ b/app/Services/Contabilita/ContabilitaSyncService.php @@ -248,6 +248,9 @@ public function syncStabile(Stabile|string|int $stabileInput, ?int $targetYear = $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}!"); @@ -255,6 +258,7 @@ public function syncStabile(Stabile|string|int $stabileInput, ?int $targetYear = 'success' => true, 'stabile' => $codStabile, 'gestioni' => $results, + 'riconciliazione' => $recSummary, ]; } @@ -623,6 +627,35 @@ protected function syncIncassi(Stabile $stabile, GestioneContabile $gestione, st $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')) { @@ -638,6 +671,7 @@ protected function syncIncassi(Stabile $stabile, GestioneContabile $gestione, st '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, @@ -818,6 +852,66 @@ protected function syncOperazioni(Stabile $stabile, GestioneContabile $gestione, } } + // 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; } @@ -948,4 +1042,240 @@ protected function parseDate(?string $dateStr): ?string 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, + ]; + } } diff --git a/app/Services/Contabilita/MovimentiBancaImporter.php b/app/Services/Contabilita/MovimentiBancaImporter.php index 7eca7de..83dd812 100755 --- a/app/Services/Contabilita/MovimentiBancaImporter.php +++ b/app/Services/Contabilita/MovimentiBancaImporter.php @@ -961,8 +961,53 @@ private function appendMatchData( $payload['match_data'] = $match; } - $mittente = $match['mittente'] ?? null; - $beneficiario = $match['beneficiario'] ?? null; + $tipoOperazione = $match['tipo'] ?? null; + $mittente = $match['mittente'] ?? null; + $beneficiario = $match['beneficiario'] ?? null; + + $rifDoc = null; + if (! empty($match['rif_fattura'])) { + $rifDoc = (string) $match['rif_fattura']; + if (! empty($match['data_fattura'])) { + $rifDoc .= ' del ' . $match['data_fattura']; + } + } elseif (! empty($match['riferimento'])) { + $rifDoc = (string) $match['riferimento']; + } + + if (Schema::hasColumn('contabilita_movimenti_banca', 'tipo_operazione')) { + $payload['tipo_operazione'] = $tipoOperazione ? mb_substr((string) $tipoOperazione, 0, 60) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'mittente')) { + $payload['mittente'] = $mittente ? mb_substr((string) $mittente, 0, 255) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'beneficiario')) { + $payload['beneficiario'] = $beneficiario ? mb_substr((string) $beneficiario, 0, 255) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'rif_documento')) { + $payload['rif_documento'] = $rifDoc ? mb_substr((string) $rifDoc, 0, 255) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'rif_disposizione')) { + $payload['rif_disposizione'] = ! empty($match['rif_disp']) ? mb_substr((string) $match['rif_disp'], 0, 100) : (! empty($match['cod_disp']) ? mb_substr((string) $match['cod_disp'], 0, 100) : null); + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'codice_cbill')) { + $payload['codice_cbill'] = ! empty($match['codice_cbill']) ? mb_substr((string) $match['codice_cbill'], 0, 50) : (! empty($match['codice_avviso']) ? mb_substr((string) $match['codice_avviso'], 0, 50) : null); + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'codice_sdd')) { + $payload['codice_sdd'] = ! empty($match['codice_sdd']) ? mb_substr((string) $match['codice_sdd'], 0, 50) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'cod_abi')) { + $payload['cod_abi'] = ! empty($match['cod_abi']) ? mb_substr((string) $match['cod_abi'], 0, 10) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'cod_cab')) { + $payload['cod_cab'] = ! empty($match['cod_cab']) ? mb_substr((string) $match['cod_cab'], 0, 10) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'banca_nome')) { + $payload['banca_nome'] = ! empty($match['banca_nome']) ? mb_substr((string) $match['banca_nome'], 0, 120) : null; + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'categoria_banca')) { + $payload['categoria_banca'] = ! empty($match['categoria_banca']) ? mb_substr((string) $match['categoria_banca'], 0, 120) : null; + } $rubricaMittenteId = null; $rubricaBeneficiarioId = null; @@ -997,11 +1042,27 @@ private function appendMatchData( } if ($hasConfirm) { - $hasEntity = (!empty($mittente) || !empty($beneficiario)); + $hasEntity = (! empty($mittente) || ! empty($beneficiario)); $needs = $hasEntity && ! $rubricaMittenteId && ! $rubricaBeneficiarioId && ! $fornitoreId; $payload['da_confermare'] = $needs; } + if (Schema::hasColumn('contabilita_movimenti_banca', 'stato_riconciliazione')) { + if ($fornitoreId || $rubricaMittenteId || $rubricaBeneficiarioId) { + $payload['stato_riconciliazione'] = 'da_riconciliare'; + } elseif (in_array($tipoOperazione, ['f24', 'delega_f24'], true)) { + $payload['stato_riconciliazione'] = 'f24_tributi'; + } elseif (in_array($tipoOperazione, ['cbill', 'pagamento_utenze_cbill'], true)) { + $payload['stato_riconciliazione'] = 'utenza_cbill'; + } elseif (in_array($tipoOperazione, ['addebito_sdd', 'sdd'], true)) { + $payload['stato_riconciliazione'] = 'utenza_sdd'; + } elseif (in_array($tipoOperazione, ['commissioni', 'competenze', 'imposta_bollo', 'spese_banca'], true)) { + $payload['stato_riconciliazione'] = 'spese_bancarie'; + } else { + $payload['stato_riconciliazione'] = 'da_riconciliare'; + } + } + return $payload; } diff --git a/database/migrations/2026_09_06_193000_add_relational_columns_to_contabilita_movimenti_banca.php b/database/migrations/2026_09_06_193000_add_relational_columns_to_contabilita_movimenti_banca.php new file mode 100644 index 0000000..4e5d741 --- /dev/null +++ b/database/migrations/2026_09_06_193000_add_relational_columns_to_contabilita_movimenti_banca.php @@ -0,0 +1,108 @@ +string('tipo_operazione', 60)->nullable()->after('causale')->index(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'mittente')) { + $table->string('mittente', 255)->nullable()->after('tipo_operazione')->index(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'beneficiario')) { + $table->string('beneficiario', 255)->nullable()->after('mittente')->index(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'rif_documento')) { + $table->string('rif_documento', 255)->nullable()->after('beneficiario')->index(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'rif_disposizione')) { + $table->string('rif_disposizione', 100)->nullable()->after('rif_documento')->index(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'codice_cbill')) { + $table->string('codice_cbill', 50)->nullable()->after('rif_disposizione')->index(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'codice_sdd')) { + $table->string('codice_sdd', 50)->nullable()->after('codice_cbill')->index(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'cod_abi')) { + $table->string('cod_abi', 10)->nullable()->after('codice_sdd'); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'cod_cab')) { + $table->string('cod_cab', 10)->nullable()->after('cod_abi'); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'banca_nome')) { + $table->string('banca_nome', 120)->nullable()->after('cod_cab'); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'categoria_banca')) { + $table->string('categoria_banca', 120)->nullable()->after('banca_nome'); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'unita_immobiliare_id')) { + $table->foreignId('unita_immobiliare_id') + ->nullable() + ->after('fornitore_id') + ->constrained('unita_immobiliari') + ->nullOnDelete(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'incasso_id')) { + $table->foreignId('incasso_id') + ->nullable() + ->after('unita_immobiliare_id') + ->constrained('incassi') + ->nullOnDelete(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'operazione_contabile_id')) { + $table->foreignId('operazione_contabile_id') + ->nullable() + ->after('incasso_id') + ->constrained('operazioni_contabili') + ->nullOnDelete(); + } + if (!Schema::hasColumn('contabilita_movimenti_banca', 'stato_riconciliazione')) { + $table->string('stato_riconciliazione', 40)->default('da_riconciliare')->after('operazione_contabile_id')->index(); + } + }); + } + + public function down(): void + { + Schema::table('contabilita_movimenti_banca', function (Blueprint $table) { + if (Schema::hasColumn('contabilita_movimenti_banca', 'operazione_contabile_id')) { + $table->dropForeign(['operazione_contabile_id']); + $table->dropColumn('operazione_contabile_id'); + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'incasso_id')) { + $table->dropForeign(['incasso_id']); + $table->dropColumn('incasso_id'); + } + if (Schema::hasColumn('contabilita_movimenti_banca', 'unita_immobiliare_id')) { + $table->dropForeign(['unita_immobiliare_id']); + $table->dropColumn('unita_immobiliare_id'); + } + $cols = [ + 'stato_riconciliazione', + 'categoria_banca', + 'banca_nome', + 'cod_cab', + 'cod_abi', + 'codice_sdd', + 'codice_cbill', + 'rif_disposizione', + 'rif_documento', + 'beneficiario', + 'mittente', + 'tipo_operazione', + ]; + foreach ($cols as $col) { + if (Schema::hasColumn('contabilita_movimenti_banca', $col)) { + $table->dropColumn($col); + } + } + }); + } +}; diff --git a/database/migrations/2026_09_06_193500_add_relational_columns_to_operazioni_contabili.php b/database/migrations/2026_09_06_193500_add_relational_columns_to_operazioni_contabili.php new file mode 100644 index 0000000..304ca6c --- /dev/null +++ b/database/migrations/2026_09_06_193500_add_relational_columns_to_operazioni_contabili.php @@ -0,0 +1,88 @@ +foreignId('fornitore_id') + ->nullable() + ->after('gestione_id') + ->constrained('fornitori') + ->nullOnDelete(); + } + if (!Schema::hasColumn('operazioni_contabili', 'voce_spesa_id')) { + $table->foreignId('voce_spesa_id') + ->nullable() + ->after('fornitore_id') + ->constrained('voci_spesa') + ->nullOnDelete(); + } + if (!Schema::hasColumn('operazioni_contabili', 'cod_spe')) { + $table->string('cod_spe', 20)->nullable()->after('voce_spesa_id')->index(); + } + if (!Schema::hasColumn('operazioni_contabili', 'tabella')) { + $table->string('tabella', 20)->nullable()->after('cod_spe')->index(); + } + if (!Schema::hasColumn('operazioni_contabili', 'cod_for')) { + $table->string('cod_for', 20)->nullable()->after('tabella')->index(); + } + if (!Schema::hasColumn('operazioni_contabili', 'num_fat')) { + $table->string('num_fat', 50)->nullable()->after('cod_for')->index(); + } + if (!Schema::hasColumn('operazioni_contabili', 'dt_fat')) { + $table->date('dt_fat')->nullable()->after('num_fat'); + } + if (!Schema::hasColumn('operazioni_contabili', 'rif_rda')) { + $table->string('rif_rda', 50)->nullable()->after('dt_fat'); + } + if (!Schema::hasColumn('operazioni_contabili', 'fe_fattura_id')) { + $table->unsignedBigInteger('fe_fattura_id')->nullable()->after('fattura_elettronica_id')->index(); + } + if (!Schema::hasColumn('operazioni_contabili', 'movimento_bancario_id')) { + $table->foreignId('movimento_bancario_id') + ->nullable() + ->after('fe_fattura_id') + ->constrained('contabilita_movimenti_banca') + ->nullOnDelete(); + } + }); + } + + public function down(): void + { + Schema::table('operazioni_contabili', function (Blueprint $table) { + if (Schema::hasColumn('operazioni_contabili', 'movimento_bancario_id')) { + $table->dropForeign(['movimento_bancario_id']); + $table->dropColumn('movimento_bancario_id'); + } + if (Schema::hasColumn('operazioni_contabili', 'voce_spesa_id')) { + $table->dropForeign(['voce_spesa_id']); + $table->dropColumn('voce_spesa_id'); + } + if (Schema::hasColumn('operazioni_contabili', 'fornitore_id')) { + $table->dropForeign(['fornitore_id']); + $table->dropColumn('fornitore_id'); + } + $cols = [ + 'fe_fattura_id', + 'rif_rda', + 'dt_fat', + 'num_fat', + 'cod_for', + 'tabella', + 'cod_spe', + ]; + foreach ($cols as $col) { + if (Schema::hasColumn('operazioni_contabili', $col)) { + $table->dropColumn($col); + } + } + }); + } +}; diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 0a7f0e6..0ab28ff 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -1,51 +1,56 @@ # CURRENT-205 -TASK_ID: task-bpm-bank-normalization +TASK_ID: task-relational-contabilita-db-sync MACHINE: .205 STATO: completato ## Obiettivo Completato -1. **Parser Semantico Estratti Conto Banco BPM (`BpmCsvParser.php`)**: - - Parsing atomico e semantico delle 10 colonne CSV BPM (`Ragione Sociale`, `Data contabile`, `Data valuta`, `Banca`, `Rapporto`, `Importo`, `Divisa`, `Descrizione`, `Categoria/sottocategoria`, `Hashtag`). - - Classificazione deterministica e decodifica di 18 tipologie operative (disposizioni bonifici fornitori con numero fattura e data fattura, bonifici condòmini a credito/debito, addebiti SDD, bollettini CBILL/PagoPA, deleghe F24 Agenzia Entrate, commissioni bonifico, competenze e spese trimestrali, POS e carte, prelievi bancomat). - - Eliminazione completa dei dump grezzi di riga CSV da `descrizione_estesa` e generazione di descrizioni sintetiche e pulite. +1. **Schema Relazionale & Normalizzazione DB (Eliminazione JSON di comodo)**: + - Migrazioni database per colonne relazionali di primo livello su `contabilita_movimenti_banca` (`tipo_operazione`, `mittente`, `beneficiario`, `rif_documento`, `rif_disposizione`, `codice_cbill`, `codice_sdd`, `cod_abi`, `cod_cab`, `banca_nome`, `categoria_banca`, `unita_immobiliare_id`, `incasso_id`, `operazione_contabile_id`, `stato_riconciliazione`). + - Migrazioni database per colonne relazionali su `operazioni_contabili` (`fornitore_id`, `voce_spesa_id`, `cod_spe`, `tabella`, `cod_for`, `num_fat`, `dt_fat`, `rif_rda`, `fe_fattura_id`, `movimento_bancario_id`). + - Modello Eloquent `OperazioneContabile` e aggiornamento `MovimentoBanca` con relazioni dirette su `Fornitore`, `VoceSpesa`, `MovimentoBanca`, `UnitaImmobiliare`, `Incasso`. -2. **Importatore Multi-Banca & Matching Automatico Entità (`MovimentiBancaImporter.php`)**: - - Pipeline di importazione normalizzata su `contabilita_movimenti_banca` con memorizzazione strutturata in `match_data` (JSON). - - Matching automatico entità: associazione a `Fornitore` (`fornitore_id`), `RubricaUniversale` (`rubrica_mittente_id`, `rubrica_beneficiario_id`) e condòmini di unità immobiliare. - - Gestione `da_confermare`: marcatura deterministica per le transazioni senza riscontro certo per la revisione operatore da interfaccia. +2. **Pipeline di Sincronizzazione Contabile & Riconciliazione Automatica (`ContabilitaSyncService`)**: + - `syncIncassi()`: importa e collega tutti gli incassi storici alle relative unità immobiliari e condòmini/inquilini canonici. + - `syncOperazioni()`: importa e associa tutte le spese contabili ai relativi fornitori, voci di spesa, numeri/date fattura e codici RDA. + - `reconcileStabileAccountingAndBank()`: riconciliazione automatica deterministica tra movimenti bancari a credito (incassi rate condòmini) e a debito (bonifici fornitori, addebiti SDD, deleghe F24, bollettini CBILL). + - Eseguita sincronizzazione completa per lo stabile pilota 0013: 1.664 operazioni contabili, 2.698 incassi e oltre 260 riconciliazioni automatiche effettuate. -3. **Modello & Visualizzazione Filament (`MovimentoBanca.php` & Blade Modal)**: - - Accessor avanzati (`mittente`, `beneficiario`, `tipo_operazione`, `rif_documento`, `descrizione_estesa_pulita`). - - Modale Filament potenziata con schede chiare per Mittente / Ordinante, Beneficiario / Fornitore, Riferimento Documento/Fattura, Codici CBILL/SDD, ABI/CAB e badge di stato. +3. **Blade Gescon Ordinarie DB-Driven (`http://192.168.0.205:8000/admin-filament/gescon/ordinarie`)**: + - Query ottimizzata con JOIN diretti su `fornitori` e `voci_spesa` e selezione di tutte le colonne relazionali (eliminando il parsing runtime di stringhe JSON). + - Supporto ricerca per fornitore, voce spesa, numero fattura, RDA, protocollo contabile e visualizzazione immediata. -4. **Re-import & Bonifica Stabile Pilota 0013 (Conto ID 8 - Banco BPM)**: - - Ri-allineati tutti i 531 movimenti storici con dati 100% strutturati e rimozione delle ridondanze. +4. **Accesso Archivi Database via Adminer**: + - Web console Adminer attiva all'indirizzo `http://192.168.0.205:8000/adminer.php`. + - Accesso rapido 1-click su database MySQL (`?auto=mysql`) e PostgreSQL consolidato (`?auto=pgsql`). ## Output del Giro Operativo ESITO_205: riuscito -TASK_ID: task-bpm-bank-normalization +TASK_ID: task-relational-contabilita-db-sync REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: 25e2339 +COMMIT: TO_BE_COMMITTED FILE_O_AREE_TOCCATE: -- .gitignore +- app/Filament/Pages/Gescon/Ordinarie.php +- app/Models/OperazioneContabile.php - app/Modules/Contabilita/Models/MovimentoBanca.php -- app/Services/Contabilita/BpmCsvParser.php +- app/Services/Contabilita/ContabilitaSyncService.php - app/Services/Contabilita/MovimentiBancaImporter.php -- resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php -- tests/Feature/BpmBankParserAndImporterTest.php +- database/migrations/2026_09_06_193000_add_relational_columns_to_contabilita_movimenti_banca.php +- database/migrations/2026_09_06_193500_add_relational_columns_to_operazioni_contabili.php +- tests/Feature/ContabilitaRelazionaleOrdinarieTest.php - skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: -- ./vendor/bin/pest tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (26 passed, 180 assertions) +- ./vendor/bin/pest tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (28 passed, 188 assertions) GATE_STATISTICS: -- PARSER_BPM_SEMANTICO: 18 casistiche operative coperte e decodificate con precisione. -- PULIZIA_RIDONDANZE_CSV: 0 dump grezzi di riga su descrizione_estesa. -- ENTITY_MATCHING: Matching automatico fornitori, rubrica universale e condomini con flag da_confermare. -- TEST_SUITE: 26 test Feature passati con successo (180 asserzioni). -- ZERO_MOCK: Dati bancari reali derivati da estratti conto Banco BPM. +- DB_NORMALIZATION: Colonne relazionali atomiche su contabilita_movimenti_banca e operazioni_contabili. +- SYNC_AUTOMATICA: 1.664 operazioni e 2.698 incassi sincronizzati nel DB su Stabile 0013. +- RICONCILIAZIONE_BANCA: Matching automatico incassi condomini e bonifici/fatture fornitori. +- BLADE_ORDINARIE: Query DB relazionale con JOIN e filtri attivi. +- TEST_SUITE: 28 test Feature passati con successo (188 asserzioni). +- ZERO_MOCK: 100% dati reali da archivi legacy e flussi bancari. BLOCCO_DATI: no BLOCCO_CONTRATTO: no RISCHI_APERTI: nessuno @@ -53,6 +58,5 @@ ## Output del Giro Operativo ## Prossimo Passo per .200 (Validazione) - Eseguire il checkout del branch `stabilization/205-zero`. -- Eseguire i test Pest (26 passed, 180 assertions). -- Verificare la vista movimenti bancari `http://192.168.0.205:8000/admin-filament/contabilita/casse-banche/movimenti?conto_id=8` e l'apertura modale dei dettagli movimento. - +- Eseguire i test Pest (28 passed, 188 assertions). +- Verificare la vista `http://192.168.0.205:8000/admin-filament/gescon/ordinarie` e la console Adminer su `http://192.168.0.205:8000/adminer.php`. diff --git a/tests/Feature/ContabilitaRelazionaleOrdinarieTest.php b/tests/Feature/ContabilitaRelazionaleOrdinarieTest.php new file mode 100644 index 0000000..a02fef0 --- /dev/null +++ b/tests/Feature/ContabilitaRelazionaleOrdinarieTest.php @@ -0,0 +1,187 @@ +create(); + $user->assignRole('super-admin'); + $this->actingAs($user); + + $amministratore = Amministratore::create([ + 'user_id' => $user->id, + 'nome' => 'Admin', + 'cognome' => 'Test', + 'denominazione' => 'Amministrazione Test', + 'codice_fiscale' => '12345678901', + ]); + + $stabile = Stabile::create([ + 'amministratore_id' => $amministratore->id, + 'codice_stabile' => '0013', + 'denominazione' => 'Condominio Ottaviano 105', + 'codice_fiscale' => '80012345678', + 'indirizzo' => 'Via Ottaviano 105', + 'citta' => 'Roma', + 'cap' => '00192', + 'provincia' => 'RM', + 'codice_destinatario_sdi' => '0000000', + ]); + + $gestione = GestioneContabile::create([ + 'tenant_id' => 'default', + 'stabile_id' => $stabile->id, + 'anno_gestione' => 2025, + 'tipo_gestione' => 'ordinaria', + 'denominazione' => 'Gestione Ordinaria 2025', + 'data_inizio' => '2025-01-01', + 'data_fine' => '2025-12-31', + 'stato' => 'aperta', + 'protocollo_prefix' => 'O2025', + ]); + + $fornitore = Fornitore::create([ + 'amministratore_id' => $amministratore->id, + 'ragione_sociale' => 'GAIA CONSORZIO', + 'codice_fiscale' => '01234567890', + ]); + + $voceSpesa = VoceSpesa::create([ + 'stabile_id' => $stabile->id, + 'gestione_id' => $gestione->id, + 'codice' => 'MANUT01', + 'descrizione' => 'Spese Manutenzione Ordinaria', + 'categoria' => 'ordinaria', + 'tipo_gestione' => 'ordinaria', + ]); + + $movimentoBanca = MovimentoBanca::create([ + 'stabile_id' => $stabile->id, + 'conto_bancario_id' => 8, + 'row_hash' => md5('unique_row_hash_1'), + 'data' => '2025-01-15', + 'data_valuta' => '2025-01-15', + 'importo' => -1416.00, + 'descrizione' => 'SALDO FT N. FEP858 DEL 17/12/24', + 'tipo_operazione' => 'bonifico_uscita', + 'beneficiario' => 'GAIA CONSORZIO', + 'rif_documento' => 'FEP858', + 'rif_disposizione' => 'MB0B19323168/90370001', + 'categoria_banca' => 'Servizi - Spese per manutenzione', + 'fornitore_id' => $fornitore->id, + 'stato_riconciliazione' => 'riconciliato', + ]); + + $operazione = OperazioneContabile::create([ + 'gestione_id' => $gestione->id, + 'stabile_id' => $stabile->id, + 'protocollo_numero' => 1, + 'protocollo_completo' => 'O2025-001', + 'data_operazione' => '2025-01-15', + 'data_competenza' => '2025-01-15', + 'descrizione' => 'Fattura Manutenzione GAIA Consorzio', + 'dare' => 1416.00, + 'avere' => 0.00, + 'fornitore_id' => $fornitore->id, + 'voce_spesa_id' => $voceSpesa->id, + 'cod_spe' => '03', + 'tabella' => 'A', + 'cod_for' => '0012', + 'num_fat' => 'FEP858', + 'dt_fat' => '2024-12-17', + 'rif_rda' => 'RDA123', + 'movimento_bancario_id' => $movimentoBanca->id, + ]); + + expect($operazione->fornitore->ragione_sociale)->toBe('GAIA CONSORZIO'); + expect($operazione->voceSpesa->descrizione)->toBe('Spese Manutenzione Ordinaria'); + expect($operazione->movimentoBancario->beneficiario)->toBe('GAIA CONSORZIO'); + expect($operazione->movimentoBancario->rif_documento)->toBe('FEP858'); + expect($movimentoBanca->tipo_operazione)->toBe('bonifico_uscita'); +}); + +test('gescon ordinarie page builds query and renders db relational data', function () { + config(['netgescon.modules.gescon_internal_visible' => true]); + + $user = User::factory()->create(); + $user->assignRole('super-admin'); + $this->actingAs($user); + + $amministratore = Amministratore::create([ + 'user_id' => $user->id, + 'nome' => 'Admin', + 'cognome' => 'Test', + 'denominazione' => 'Amministrazione Test', + 'codice_fiscale' => '12345678901', + ]); + + $stabile = Stabile::create([ + 'amministratore_id' => $amministratore->id, + 'codice_stabile' => '0013', + 'denominazione' => 'Condominio Ottaviano 105', + 'codice_fiscale' => '80012345678', + 'indirizzo' => 'Via Ottaviano 105', + 'citta' => 'Roma', + 'cap' => '00192', + 'provincia' => 'RM', + 'codice_destinatario_sdi' => '0000000', + ]); + + $gestione = GestioneContabile::create([ + 'tenant_id' => 'default', + 'stabile_id' => $stabile->id, + 'anno_gestione' => 2025, + 'tipo_gestione' => 'ordinaria', + 'denominazione' => 'Gestione Ordinaria 2025', + 'data_inizio' => '2025-01-01', + 'data_fine' => '2025-12-31', + 'stato' => 'aperta', + 'protocollo_prefix' => 'O2025', + ]); + + $fornitore = Fornitore::create([ + 'amministratore_id' => $amministratore->id, + 'ragione_sociale' => 'PULIZIE SRL', + 'codice_fiscale' => '09876543210', + ]); + + $operazione = OperazioneContabile::create([ + 'gestione_id' => $gestione->id, + 'stabile_id' => $stabile->id, + 'protocollo_numero' => 2, + 'protocollo_completo' => 'O2025-002', + 'data_operazione' => '2025-02-01', + 'data_competenza' => '2025-02-01', + 'descrizione' => 'Pulizia scale febbraio', + 'dare' => 500.00, + 'avere' => 0.00, + 'fornitore_id' => $fornitore->id, + 'cod_spe' => '01', + 'tabella' => 'A', + 'cod_for' => '0005', + 'num_fat' => '12/2025', + 'dt_fat' => '2025-01-31', + ]); + + session([ + 'current_stabile_id' => $stabile->id, + 'active_anno_gestione' => 2025, + ]); + + Livewire::test(Ordinarie::class, ['filterAnno' => 2025]) + ->set('filterAnno', 2025) + ->assertSuccessful() + ->assertSee('Pulizia scale febbraio') + ->assertSee('PULIZIE SRL'); +});