370 lines
14 KiB
PHP
370 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\Models\{
|
|
Stabile,
|
|
MovimentoBancario,
|
|
TransazioneContabile,
|
|
RiconciliazioneBancaria,
|
|
ContoCorrente
|
|
};
|
|
|
|
/**
|
|
* ========================================
|
|
* CONTROLLER RICONCILIAZIONE BANCARIA
|
|
* Sistema avanzato matching automatico/manuale
|
|
* ========================================
|
|
*/
|
|
class RiconciliazioniController extends Controller
|
|
{
|
|
/**
|
|
* Dashboard riconciliazioni
|
|
*/
|
|
public function index()
|
|
{
|
|
$amministratore_id = Auth::user()->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
|
|
}
|
|
}
|