amministratore->id_amministratore ?? null; // Statistiche $stats = [ 'movimenti_non_riconciliati' => MovimentoBancario::whereHas('contoCorrente.stabile', function($q) use ($amministratore_id) { $q->where('amministratore_id', $amministratore_id); })->where('riconciliato', false)->count(), 'differenze_trovate' => RiconciliazioneBancaria::whereHas('contoCorrente.stabile', function($q) use ($amministratore_id) { $q->where('amministratore_id', $amministratore_id); })->where('stato', 'con_differenze')->count(), 'importo_non_riconciliato' => MovimentoBancario::whereHas('contoCorrente.stabile', function($q) use ($amministratore_id) { $q->where('amministratore_id', $amministratore_id); })->where('riconciliato', false)->sum('importo'), ]; // Conti correnti attivi $contiCorrenti = ContoCorrente::whereHas('stabile', function($q) use ($amministratore_id) { $q->where('amministratore_id', $amministratore_id); })->with('stabile')->get(); // Ultime riconciliazioni $ultimeRiconciliazioni = RiconciliazioneBancaria::whereHas('contoCorrente.stabile', function($q) use ($amministratore_id) { $q->where('amministratore_id', $amministratore_id); })->with(['contoCorrente.stabile', 'utente']) ->orderBy('data_riconciliazione', 'desc') ->take(10) ->get(); return view('admin.riconciliazioni.index', compact( 'stats', 'contiCorrenti', 'ultimeRiconciliazioni' )); } /** * Avvia processo di riconciliazione per un conto */ public function create(Request $request) { $request->validate([ 'conto_corrente_id' => 'required|exists:conti_correnti,id', 'data_da' => 'required|date', 'data_a' => 'required|date|after_or_equal:data_da' ]); $conto = ContoCorrente::with('stabile')->find($request->conto_corrente_id); // Verifica autorizzazioni $amministratore_id = Auth::user()->amministratore->id_amministratore ?? null; if ($conto->stabile->amministratore_id !== $amministratore_id) { abort(403); } // Movimenti bancari non riconciliati nel periodo $movimentiBancari = MovimentoBancario::where('conto_corrente_id', $conto->id) ->whereBetween('data_valuta', [$request->data_da, $request->data_a]) ->where('riconciliato', false) ->orderBy('data_valuta') ->get(); // Transazioni contabili nel periodo $transazioniContabili = TransazioneContabile::where('stabile_id', $conto->stabile_id) ->whereBetween('data_registrazione', [$request->data_da, $request->data_a]) ->where('riconciliata', false) ->whereHas('righe', function($q) { $q->whereHas('conto', function($q2) { $q2->where('tipo_conto', 'banca'); }); }) ->with(['righe.conto']) ->orderBy('data_registrazione') ->get(); // Matching automatico preliminare $matchingSuggeriti = $this->eseguiMatchingAutomatico($movimentiBancari, $transazioniContabili); return view('admin.riconciliazioni.create', compact( 'conto', 'movimentiBancari', 'transazioniContabili', 'matchingSuggeriti' )); } /** * Salva riconciliazione */ public function store(Request $request) { $request->validate([ 'conto_corrente_id' => 'required|exists:conti_correnti,id', 'data_da' => 'required|date', 'data_a' => 'required|date', 'saldo_iniziale' => 'required|numeric', 'saldo_finale' => 'required|numeric', 'matching' => 'required|array', 'matching.*.movimento_bancario_id' => 'required|exists:movimenti_bancari,id', 'matching.*.transazione_contabile_id' => 'nullable|exists:transazioni_contabili,id', 'matching.*.tipo_matching' => 'required|in:automatico,manuale,non_trovato', 'differenze_non_giustificate' => 'nullable|array' ]); DB::beginTransaction(); try { $conto = ContoCorrente::find($request->conto_corrente_id); // Crea record riconciliazione $riconciliazione = RiconciliazioneBancaria::create([ 'conto_corrente_id' => $request->conto_corrente_id, 'data_riconciliazione' => now(), 'periodo_da' => $request->data_da, 'periodo_a' => $request->data_a, 'saldo_iniziale_bancario' => $request->saldo_iniziale, 'saldo_finale_bancario' => $request->saldo_finale, 'saldo_iniziale_contabile' => $this->calcolaSaldoContabile($conto, $request->data_da, true), 'saldo_finale_contabile' => $this->calcolaSaldoContabile($conto, $request->data_a, false), 'stato' => 'in_progress', 'utente_id' => Auth::id() ]); $movimentiRiconciliati = 0; $importoRiconciliato = 0; $differenzeNonGiustificate = []; // Processa ogni matching foreach ($request->matching as $match) { $movimentoBancario = MovimentoBancario::find($match['movimento_bancario_id']); if ($match['tipo_matching'] === 'automatico' || $match['tipo_matching'] === 'manuale') { if (isset($match['transazione_contabile_id'])) { $transazione = TransazioneContabile::find($match['transazione_contabile_id']); // Marca come riconciliati $movimentoBancario->update([ 'riconciliato' => true, 'transazione_contabile_id' => $transazione->id, 'riconciliazione_id' => $riconciliazione->id, 'tipo_matching' => $match['tipo_matching'] ]); $transazione->update(['riconciliata' => true]); $movimentiRiconciliati++; $importoRiconciliato += abs($movimentoBancario->importo); } } else { // Movimento non trovato - possibile differenza $differenzeNonGiustificate[] = [ 'movimento_bancario_id' => $movimentoBancario->id, 'importo' => $movimentoBancario->importo, 'descrizione' => $movimentoBancario->descrizione, 'data' => $movimentoBancario->data_valuta ]; } } // Calcola stato finale $stato = 'completata'; if (count($differenzeNonGiustificate) > 0) { $stato = 'con_differenze'; } $differenzaSaldi = abs($riconciliazione->saldo_finale_bancario - $riconciliazione->saldo_finale_contabile); if ($differenzaSaldi > 0.01) { $stato = 'con_differenze'; } // Aggiorna riconciliazione $riconciliazione->update([ 'movimenti_riconciliati' => $movimentiRiconciliati, 'importo_riconciliato' => $importoRiconciliato, 'differenze_non_giustificate' => json_encode($differenzeNonGiustificate), 'differenza_saldi' => $differenzaSaldi, 'stato' => $stato, 'note_riconciliazione' => $request->note ?? null ]); DB::commit(); $message = "Riconciliazione completata. Movimenti riconciliati: {$movimentiRiconciliati}"; if ($stato === 'con_differenze') { $message .= " - ATTENZIONE: Sono presenti differenze da verificare."; } return redirect() ->route('admin.riconciliazioni.show', $riconciliazione) ->with('success', $message); } catch (\Exception $e) { DB::rollback(); return back() ->withInput() ->withErrors(['error' => 'Errore durante la riconciliazione: ' . $e->getMessage()]); } } /** * Mostra dettagli riconciliazione */ public function show(RiconciliazioneBancaria $riconciliazione) { $riconciliazione->load([ 'contoCorrente.stabile', 'movimentiBancari.transazioneContabile', 'utente' ]); $differenzeNonGiustificate = json_decode($riconciliazione->differenze_non_giustificate, true) ?? []; return view('admin.riconciliazioni.show', compact( 'riconciliazione', 'differenzeNonGiustificate' )); } /** * API: Matching automatico in tempo reale */ public function matchingAutomatico(Request $request) { $request->validate([ 'movimento_bancario_id' => 'required|exists:movimenti_bancari,id', 'conto_corrente_id' => 'required|exists:conti_correnti,id', 'data_da' => 'required|date', 'data_a' => 'required|date' ]); $movimentoBancario = MovimentoBancario::find($request->movimento_bancario_id); // Criteri di matching automatico $transazioniCandidati = TransazioneContabile::whereHas('stabile.contiCorrenti', function($q) use ($request) { $q->where('id', $request->conto_corrente_id); }) ->whereBetween('data_registrazione', [$request->data_da, $request->data_a]) ->where('riconciliata', false) ->where(function($q) use ($movimentoBancario) { // Matching per importo esatto $q->where('importo_totale', abs($movimentoBancario->importo)) // Matching per numero documento ->orWhere('numero_documento', 'LIKE', '%' . $movimentoBancario->numero_operazione . '%') // Matching per descrizione ->orWhere('descrizione', 'LIKE', '%' . substr($movimentoBancario->descrizione, 0, 20) . '%'); }) ->with(['fornitore', 'righe.conto']) ->get(); // Calcola score di matching $candidatiConScore = $transazioniCandidati->map(function($transazione) use ($movimentoBancario) { $score = 0; // Score per importo if (abs($transazione->importo_totale - abs($movimentoBancario->importo)) < 0.01) { $score += 50; } elseif (abs($transazione->importo_totale - abs($movimentoBancario->importo)) < 10) { $score += 20; } // Score per data $diffGiorni = abs($transazione->data_registrazione->diffInDays($movimentoBancario->data_valuta)); if ($diffGiorni <= 1) { $score += 30; } elseif ($diffGiorni <= 7) { $score += 15; } // Score per descrizione if (stripos($movimentoBancario->descrizione, $transazione->numero_documento) !== false) { $score += 25; } if ($transazione->fornitore && stripos($movimentoBancario->descrizione, $transazione->fornitore->ragione_sociale) !== false) { $score += 20; } $transazione->matching_score = $score; return $transazione; })->sortByDesc('matching_score'); return response()->json([ 'candidati' => $candidatiConScore->take(5)->values(), 'movimento_bancario' => $movimentoBancario ]); } /** * Esegue matching automatico preliminare */ private function eseguiMatchingAutomatico($movimentiBancari, $transazioniContabili) { $matching = []; foreach ($movimentiBancari as $movimento) { $miglioreMatch = null; $migliorScore = 0; foreach ($transazioniContabili as $transazione) { $score = 0; // Matching per importo esatto if (abs($transazione->importo_totale - abs($movimento->importo)) < 0.01) { $score += 60; } // Matching per data (più vicine = score più alto) $diffGiorni = abs($transazione->data_registrazione->diffInDays($movimento->data_valuta)); if ($diffGiorni <= 1) { $score += 30; } elseif ($diffGiorni <= 3) { $score += 15; } // Matching per numero documento if ($transazione->numero_documento && stripos($movimento->descrizione, $transazione->numero_documento) !== false) { $score += 20; } if ($score > $migliorScore && $score >= 70) { // Soglia minima per matching automatico $migliorScore = $score; $miglioreMatch = $transazione; } } $matching[] = [ 'movimento_bancario' => $movimento, 'transazione_suggerita' => $miglioreMatch, 'score' => $migliorScore, 'tipo_suggerimento' => $migliorScore >= 80 ? 'automatico' : 'manuale' ]; } return $matching; } /** * Calcola saldo contabile alla data */ private function calcolaSaldoContabile($conto, $data, $iniziale = false) { // Implementazione calcolo saldo contabile dal piano dei conti // Basato sulle transazioni registrate fino alla data specificata return 0; // Placeholder } }