1076 lines
42 KiB
PHP
Executable File
1076 lines
42 KiB
PHP
Executable File
<?php
|
|
namespace App\Filament\Pages\Contabilita;
|
|
|
|
use App\Models\DatiBancari;
|
|
use App\Models\DocumentoStabile;
|
|
use App\Models\GestioneContabile;
|
|
use App\Models\Stabile;
|
|
use App\Models\User;
|
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
|
use App\Modules\Contabilita\Models\SaldoConto;
|
|
use App\Services\Contabilita\ExcelMovimentiParser;
|
|
use App\Services\Contabilita\IntesaCsvParser;
|
|
use App\Services\Contabilita\MovimentiBancaImporter;
|
|
use App\Services\Contabilita\UnicreditWriParser;
|
|
use App\Support\AnnoGestioneContext;
|
|
use App\Support\GestioneContext;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\DatePicker;
|
|
use Filament\Forms\Components\FileUpload;
|
|
use Filament\Forms\Components\Repeater;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Filters\Filter;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use UnitEnum;
|
|
|
|
class CasseBancheRiepilogo extends Page implements HasTable
|
|
{
|
|
use InteractsWithTable;
|
|
|
|
/** @var array<string, ?int> */
|
|
public array $interbankMappings = [];
|
|
|
|
protected static ?bool $supportsWindowFunctions = null;
|
|
|
|
protected static ?string $navigationLabel = 'Casse e banche';
|
|
|
|
protected static ?string $title = 'Casse e banche';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-banknotes';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
|
|
|
|
protected static ?int $navigationSort = 10;
|
|
|
|
protected static ?string $slug = 'contabilita/casse-banche';
|
|
|
|
protected string $view = 'filament.pages.contabilita.casse-banche-riepilogo';
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if (! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
|
return true;
|
|
}
|
|
|
|
return $user->can('contabilita.casse-banche');
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->mountInteractsWithTable();
|
|
$this->loadInterbankMappings();
|
|
}
|
|
|
|
public function getActiveStabile(): ?Stabile
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
return StabileContext::getActiveStabile($user);
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('importa_unicredit_wri')
|
|
->label('Importa estratto (Unicredit WRI)')
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->modalWidth('7xl')
|
|
->form([
|
|
Select::make('iban')
|
|
->label('Conto (IBAN)')
|
|
->native(false)
|
|
->searchable()
|
|
->options(fn(): array=> $this->getIbanOptions())
|
|
->helperText('Opzionale: se non selezionato, l\'import prova a rilevare il conto dai dati.'),
|
|
|
|
Select::make('gestione_id')
|
|
->label('Gestione (opzionale)')
|
|
->native(false)
|
|
->searchable()
|
|
->options(fn(): array=> $this->getGestioniOptions())
|
|
->default(fn(): ?string => $this->resolveDefaultGestioneId()),
|
|
|
|
FileUpload::make('file')
|
|
->label('File estratto')
|
|
->directory('tmp/banca')
|
|
->disk('local')
|
|
->required(),
|
|
|
|
Toggle::make('replace')
|
|
->label('Sostituisci import nel periodo (consigliato se hai già importato con valori “slittati”)')
|
|
->helperText('Elimina SOLO i movimenti importati e NON riconciliati (registrazione vuota) per lo stesso conto e per l\'intervallo date del file, poi reimporta.')
|
|
->default(false),
|
|
])
|
|
->action(function (array $data): void {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non valido')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Seleziona uno stabile')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$path = (string) ($data['file'] ?? '');
|
|
if ($path === '') {
|
|
Notification::make()->title('Seleziona un file')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$content = Storage::disk('local')->get($path);
|
|
$importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser());
|
|
$res = $importer->importUnicreditWri(
|
|
$content,
|
|
$stabileId,
|
|
basename($path),
|
|
isset($data['iban']) && is_string($data['iban']) && trim($data['iban']) !== '' ? trim((string) $data['iban']) : null,
|
|
isset($data['gestione_id']) && is_numeric($data['gestione_id']) ? (int) $data['gestione_id'] : null,
|
|
isset($data['replace']) ? (bool) $data['replace'] : false,
|
|
);
|
|
|
|
Notification::make()
|
|
->title('Import completato')
|
|
->body('Importati: ' . ($res['imported'] ?? 0) . ' · Duplicati: ' . ($res['duplicates'] ?? 0))
|
|
->success()
|
|
->send();
|
|
} catch (\Throwable $e) {
|
|
Notification::make()->title('Errore import')->body($e->getMessage())->danger()->send();
|
|
} finally {
|
|
try {
|
|
Storage::disk('local')->delete($path);
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
}
|
|
}),
|
|
|
|
Action::make('inserisci_saldi')
|
|
->label('Inserisci saldi (periodo)')
|
|
->icon('heroicon-o-scale')
|
|
->modalWidth('7xl')
|
|
->form([
|
|
Select::make('iban')
|
|
->label('Conto (IBAN)')
|
|
->native(false)
|
|
->searchable()
|
|
->options(fn(): array=> $this->getIbanOptions())
|
|
->required(),
|
|
|
|
Textarea::make('note')
|
|
->label('Note (opzionale)')
|
|
->rows(2)
|
|
->maxLength(255),
|
|
|
|
Repeater::make('saldi')
|
|
->label('Saldi')
|
|
->defaultItems(1)
|
|
->reorderable(true)
|
|
->schema([
|
|
DatePicker::make('data_saldo')
|
|
->label('Data')
|
|
->required(),
|
|
|
|
TextInput::make('saldo')
|
|
->label('Saldo')
|
|
->numeric()
|
|
->required(),
|
|
])
|
|
->columns(2)
|
|
->required(),
|
|
])
|
|
->action(function (array $data): void {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non valido')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Seleziona uno stabile')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$iban = isset($data['iban']) && is_string($data['iban']) ? trim((string) $data['iban']) : '';
|
|
if ($iban === '') {
|
|
Notification::make()->title('Seleziona un IBAN')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$rows = $data['saldi'] ?? [];
|
|
if (! is_array($rows) || $rows === []) {
|
|
Notification::make()->title('Inserisci almeno un saldo')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$note = isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : '';
|
|
|
|
$contoId = null;
|
|
try {
|
|
if (DB::getSchemaBuilder()->hasTable('dati_bancari') && DB::getSchemaBuilder()->hasColumn('contabilita_saldi_conti', 'conto_id')) {
|
|
$contoId = DatiBancari::query()
|
|
->where('stabile_id', (int) $stabileId)
|
|
->where('iban', $iban)
|
|
->orderBy('id')
|
|
->value('id');
|
|
$contoId = is_numeric($contoId) ? (int) $contoId : null;
|
|
}
|
|
} catch (\Throwable) {
|
|
$contoId = null;
|
|
}
|
|
|
|
$created = 0;
|
|
DB::transaction(function () use ($rows, $stabileId, $contoId, $iban, $note, &$created, $user): void {
|
|
foreach ($rows as $row) {
|
|
$date = $row['data_saldo'] ?? null;
|
|
$saldo = $row['saldo'] ?? null;
|
|
|
|
if (! $date || ! is_numeric($saldo)) {
|
|
continue;
|
|
}
|
|
|
|
SaldoConto::query()->create([
|
|
'stabile_id' => (int) $stabileId,
|
|
'conto_id' => $contoId,
|
|
'iban' => $iban,
|
|
'data_saldo' => $date,
|
|
'saldo' => (float) $saldo,
|
|
'note' => $note !== '' ? $note : null,
|
|
'created_by' => (int) ($user->id ?? 0) ?: null,
|
|
'updated_by' => (int) ($user->id ?? 0) ?: null,
|
|
]);
|
|
$created++;
|
|
}
|
|
});
|
|
|
|
Notification::make()
|
|
->title('Saldi salvati')
|
|
->body('Inseriti: ' . $created)
|
|
->success()
|
|
->send();
|
|
}),
|
|
];
|
|
}
|
|
|
|
protected function getTableQuery(): Builder
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return DatiBancari::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return DatiBancari::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
return DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderBy('stato_conto')
|
|
->orderBy('denominazione_banca')
|
|
->orderBy('iban')
|
|
->orderBy('id');
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->striped()
|
|
->filters([
|
|
Filter::make('periodo')
|
|
->label('Periodo')
|
|
->form([
|
|
DatePicker::make('from')->label('Dal'),
|
|
DatePicker::make('to')->label('Al'),
|
|
]),
|
|
|
|
Filter::make('gestioni')
|
|
->label('Gestioni')
|
|
->form([
|
|
Select::make('gestione_ids')
|
|
->label('Gestione')
|
|
->multiple()
|
|
->options(fn(): array=> $this->getGestioniOptions())
|
|
->searchable(),
|
|
]),
|
|
])
|
|
->columns([
|
|
TextColumn::make('denominazione_banca')
|
|
->label('Banca')
|
|
->searchable()
|
|
->wrap(),
|
|
|
|
TextColumn::make('tipo_conto')
|
|
->label('Tipo')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('intestazione_conto')
|
|
->label('Intestazione')
|
|
->wrap()
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('iban')
|
|
->label('IBAN')
|
|
->searchable()
|
|
->copyable(),
|
|
|
|
TextColumn::make('saldo_calcolato')
|
|
->label('Saldo')
|
|
->alignEnd()
|
|
->getStateUsing(fn(DatiBancari $record): ?float => $this->resolveSaldoConto($record))
|
|
->formatStateUsing(fn($state) => $state !== null ? ('€ ' . number_format((float) $state, 2, ',', '.')) : '—'),
|
|
|
|
TextColumn::make('saldo_ufficiale')
|
|
->label('Saldo ufficiale')
|
|
->alignEnd()
|
|
->getStateUsing(fn(DatiBancari $record): ?float => $this->resolveLatestSaldoSnapshotValue($record))
|
|
->formatStateUsing(fn($state) => $state !== null ? ('€ ' . number_format((float) $state, 2, ',', '.')) : '—')
|
|
->toggleable(),
|
|
|
|
TextColumn::make('ultimo_estratto')
|
|
->label('Ultimo estratto')
|
|
->getStateUsing(fn(DatiBancari $record): string => $this->resolveLatestSaldoSnapshotLabel($record))
|
|
->wrap()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('stato_conto')
|
|
->label('Stato')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
])
|
|
->actions([
|
|
Action::make('movimenti')
|
|
->label('Movimenti')
|
|
->icon('heroicon-o-arrow-right')
|
|
->url(function (DatiBancari $record): string {
|
|
return CasseBancheMovimenti::getUrl(panel: 'admin-filament') . '?conto_id=' . (int) $record->id;
|
|
}),
|
|
]);
|
|
}
|
|
|
|
public function getTotaleDisponibilita(): ?float
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
|
|
$periodo = $this->getTableFilterState('periodo');
|
|
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
|
|
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
|
|
|
|
$gestioni = $this->getTableFilterState('gestioni');
|
|
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
|
|
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
|
|
|
|
$conti = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->get();
|
|
|
|
if ($conti->isEmpty()) {
|
|
return 0.0;
|
|
}
|
|
|
|
$total = 0.0;
|
|
foreach ($conti as $c) {
|
|
$saldo = $this->resolveSaldoConto($c);
|
|
if ($saldo !== null) {
|
|
$total += $saldo;
|
|
}
|
|
}
|
|
|
|
return $total;
|
|
}
|
|
|
|
protected function resolveSaldoConto(DatiBancari $record): ?float
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
|
|
$iban = is_string($record->iban) ? trim((string) $record->iban) : '';
|
|
|
|
$periodo = $this->getTableFilterState('periodo');
|
|
$from = is_array($periodo) ? ($periodo['from'] ?? null) : null;
|
|
$to = is_array($periodo) ? ($periodo['to'] ?? null) : null;
|
|
|
|
$gestioni = $this->getTableFilterState('gestioni');
|
|
$gestioneIds = is_array($gestioni) ? ($gestioni['gestione_ids'] ?? null) : null;
|
|
$gestioneIds = is_array($gestioneIds) ? array_values(array_filter(array_map('intval', $gestioneIds), fn(int $v) => $v > 0)) : [];
|
|
|
|
$saldoIniziale = is_numeric($record->saldo_iniziale) ? (float) $record->saldo_iniziale : 0.0;
|
|
|
|
$q = MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->when($record->id, fn($q) => $q->where('conto_id', (int) $record->id))
|
|
->when($iban !== '', fn($q) => $q->where('iban', $iban));
|
|
|
|
if ($from) {
|
|
$q->whereDate('data', '>=', $from);
|
|
}
|
|
if ($to) {
|
|
$q->whereDate('data', '<=', $to);
|
|
}
|
|
if ($gestioneIds !== []) {
|
|
$q->whereIn('gestione_id', $gestioneIds);
|
|
}
|
|
|
|
// Base da saldi intermedi: se c'è un saldo <= from (se filtrato) o altrimenti ultimo saldo disponibile.
|
|
$baseSaldo = null;
|
|
$baseDate = null;
|
|
if (DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
|
$qSaldo = SaldoConto::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('conto_id', (int) $record->id);
|
|
|
|
if ($from) {
|
|
$qSaldo->whereDate('data_saldo', '<=', (string) $from);
|
|
}
|
|
|
|
$row = $qSaldo->orderByDesc('data_saldo')->orderByDesc('id')->first(['data_saldo', 'saldo']);
|
|
if ($row) {
|
|
$baseSaldo = (float) $row->saldo;
|
|
$baseDate = $row->data_saldo->toDateString();
|
|
}
|
|
}
|
|
|
|
if ($from && $baseSaldo !== null) {
|
|
// Se filtrato da una data, la base è già il saldo al from (o più vicino precedente).
|
|
$saldoIniziale = $baseSaldo;
|
|
}
|
|
|
|
if ($baseSaldo !== null && ! $from) {
|
|
// Non filtrato: usa ultimo saldo inserito come base.
|
|
$saldoIniziale = $baseSaldo;
|
|
}
|
|
|
|
if ($baseDate && ! $from) {
|
|
$q->whereDate('data', '>', $baseDate);
|
|
}
|
|
|
|
$sum = (float) ($q->sum('importo') ?? 0.0);
|
|
return $saldoIniziale + $sum;
|
|
}
|
|
|
|
protected function resolveSaldoIniziale(int $stabileId, string $iban, string $fromDate): ?float
|
|
{
|
|
if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
|
return null;
|
|
}
|
|
|
|
$row = SaldoConto::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('iban', $iban)
|
|
->whereDate('data_saldo', '<=', $fromDate)
|
|
->orderByDesc('data_saldo')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
return $row ? (float) $row->saldo : null;
|
|
}
|
|
|
|
protected function resolveLatestSaldoSnapshot(DatiBancari $record): ?SaldoConto
|
|
{
|
|
static $cache = [];
|
|
|
|
$cacheKey = (int) $record->id;
|
|
if (array_key_exists($cacheKey, $cache)) {
|
|
return $cache[$cacheKey];
|
|
}
|
|
|
|
if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
|
return $cache[$cacheKey] = null;
|
|
}
|
|
|
|
return $cache[$cacheKey] = SaldoConto::query()
|
|
->with('documento')
|
|
->where('stabile_id', (int) $record->stabile_id)
|
|
->where('conto_id', (int) $record->id)
|
|
->orderByDesc('data_saldo')
|
|
->orderByDesc('id')
|
|
->first();
|
|
}
|
|
|
|
protected function resolveLatestSaldoSnapshotValue(DatiBancari $record): ?float
|
|
{
|
|
$snapshot = $this->resolveLatestSaldoSnapshot($record);
|
|
|
|
return $snapshot ? (float) $snapshot->saldo : null;
|
|
}
|
|
|
|
protected function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string
|
|
{
|
|
$snapshot = $this->resolveLatestSaldoSnapshot($record);
|
|
if (! $snapshot) {
|
|
return 'Nessuno estratto registrato';
|
|
}
|
|
|
|
$parts = [];
|
|
$parts[] = $snapshot->tipo_estratto
|
|
? ($this->getTipoEstrattoOptions()[$snapshot->tipo_estratto] ?? 'Saldo registrato')
|
|
: 'Saldo registrato';
|
|
if ($snapshot->periodo_da || $snapshot->periodo_a) {
|
|
$from = $snapshot->periodo_da?->format('d/m/Y');
|
|
$to = $snapshot->periodo_a?->format('d/m/Y');
|
|
$parts[] = trim(($from ?: '...') . ' - ' . ($to ?: '...'));
|
|
}
|
|
if ($snapshot->documento instanceof DocumentoStabile) {
|
|
$parts[] = $snapshot->documento->nome_originale ?: 'Documento allegato';
|
|
}
|
|
|
|
return implode(' · ', $parts);
|
|
}
|
|
|
|
protected function getTipoEstrattoOptions(): array
|
|
{
|
|
return [
|
|
'estratto_conto' => 'Estratto conto',
|
|
'saldo_banca' => 'Saldo banca',
|
|
'riconciliazione' => 'Riconciliazione',
|
|
'altro' => 'Altro',
|
|
];
|
|
}
|
|
|
|
protected function getContiOptions(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderBy('denominazione_banca')
|
|
->orderBy('legacy_cod_cassa')
|
|
->orderBy('numero_conto')
|
|
->orderBy('id')
|
|
->get(['id', 'denominazione_banca', 'iban', 'legacy_cod_cassa', 'numero_conto'])
|
|
->mapWithKeys(function (DatiBancari $conto): array {
|
|
$parts = [];
|
|
$parts[] = trim((string) ($conto->denominazione_banca ?: 'Conto'));
|
|
|
|
$legacyCodCassa = is_string($conto->legacy_cod_cassa) ? trim((string) $conto->legacy_cod_cassa) : '';
|
|
$numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : '';
|
|
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : '';
|
|
|
|
if ($legacyCodCassa !== '') {
|
|
$parts[] = 'cassa ' . strtoupper($legacyCodCassa);
|
|
}
|
|
if ($iban !== '') {
|
|
$parts[] = $iban;
|
|
} elseif ($numeroConto !== '') {
|
|
$parts[] = 'n. ' . $numeroConto;
|
|
}
|
|
|
|
return [(int) $conto->id => implode(' · ', $parts)];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
/** @return array<string,string> */
|
|
protected function getIbanOptions(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNotNull('iban')
|
|
->where('iban', '!=', '')
|
|
->orderBy('denominazione_banca')
|
|
->orderBy('iban')
|
|
->limit(500)
|
|
->get(['iban', 'denominazione_banca'])
|
|
->mapWithKeys(fn(DatiBancari $r) => [
|
|
trim((string) $r->iban) => trim((string) ($r->denominazione_banca ?: 'Conto') . ' · ' . (string) $r->iban),
|
|
])
|
|
->all();
|
|
}
|
|
|
|
/** @return array<string,string> */
|
|
protected function getGestioniOptions(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
|
return [];
|
|
}
|
|
|
|
return GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderByDesc('anno_gestione')
|
|
->orderBy('tipo_gestione')
|
|
->orderByDesc('id')
|
|
->get(['id', 'denominazione', 'protocollo_prefix', 'stato'])
|
|
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
(string) $g->id => trim(($g->protocollo_prefix ? $g->protocollo_prefix . ' · ' : '') . $g->denominazione) . ($g->stato !== 'aperta' ? (' (' . $g->stato . ')') : ''),
|
|
])
|
|
->all();
|
|
}
|
|
|
|
protected function resolveDefaultGestioneId(): ?string
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
|
|
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
|
return null;
|
|
}
|
|
|
|
$anno = AnnoGestioneContext::resolveActiveAnno();
|
|
$tipo = GestioneContext::resolveActiveGestione();
|
|
|
|
$match = GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('anno_gestione', $anno)
|
|
->where('tipo_gestione', $tipo)
|
|
->where('stato', 'aperta')
|
|
->orderByDesc('gestione_attiva')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if (! $match) {
|
|
$match = GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('stato', 'aperta')
|
|
->orderByDesc('gestione_attiva')
|
|
->orderByDesc('id')
|
|
->first();
|
|
}
|
|
|
|
return $match ? (string) $match->id : null;
|
|
}
|
|
|
|
public function loadInterbankMappings(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return;
|
|
}
|
|
|
|
$codes = ['198', '219', '012', '048', '208', 'F24'];
|
|
$this->interbankMappings = [];
|
|
|
|
foreach ($codes as $code) {
|
|
$rule = \App\Modules\Contabilita\Models\RegolaPrimaNota::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('origine', 'banca')
|
|
->where('chiave', $code)
|
|
->first();
|
|
|
|
$this->interbankMappings[$code] = $rule ? (int) $rule->conto_avere_id : null;
|
|
}
|
|
}
|
|
|
|
public function saveInterbankMappings(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Seleziona uno stabile')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$bancaContoId = DB::table('contabilita_piano_conti')->where('codice', '1020')->value('id') ?: 7;
|
|
|
|
$labels = [
|
|
'198' => 'Canone Conto / Costo Fisso (198)',
|
|
'219' => 'Imposta di Bollo (219)',
|
|
'012' => 'Bollettino PagoPA / CBILL (012)',
|
|
'048' => 'Spese / Commissioni Bancarie (048)',
|
|
'208' => 'Interessi Passivi (208)',
|
|
'F24' => 'Deleghe F24 (RA)',
|
|
];
|
|
|
|
foreach ($this->interbankMappings as $code => $contoAvereId) {
|
|
if ($contoAvereId) {
|
|
\App\Modules\Contabilita\Models\RegolaPrimaNota::updateOrCreate([
|
|
'stabile_id' => $stabileId,
|
|
'origine' => 'banca',
|
|
'chiave' => $code,
|
|
], [
|
|
'label' => $labels[$code] ?? "Regola interbancaria {$code}",
|
|
'conto_dare_id' => $bancaContoId,
|
|
'conto_avere_id' => (int) $contoAvereId,
|
|
'attiva' => true,
|
|
]);
|
|
} else {
|
|
\App\Modules\Contabilita\Models\RegolaPrimaNota::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('origine', 'banca')
|
|
->where('chiave', $code)
|
|
->delete();
|
|
}
|
|
}
|
|
|
|
Notification::make()->title('Configurazione salvata con successo.')->success()->send();
|
|
}
|
|
|
|
/** @return array<string, string> */
|
|
public function getPianoContiOptions(): array
|
|
{
|
|
return DB::table('contabilita_piano_conti')
|
|
->where('attivo', true)
|
|
->orderBy('codice')
|
|
->get(['id', 'codice', 'denominazione'])
|
|
->mapWithKeys(fn($r) => [(string) $r->id => "{$r->codice} - {$r->denominazione}"])
|
|
->all();
|
|
}
|
|
|
|
public function riconciliaSpeseBancarieAutomaticamente(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Errore')->body('Seleziona uno stabile prima.')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$gestioneId = null;
|
|
if (Schema::hasTable('gestioni_contabili')) {
|
|
$gestioneId = $this->resolveDefaultGestioneId();
|
|
if ($gestioneId) {
|
|
$gestioneId = (int) $gestioneId;
|
|
}
|
|
}
|
|
|
|
if (! $gestioneId) {
|
|
Notification::make()->title('Errore')->body('Nessuna gestione contabile attiva e aperta trovata per questo stabile.')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$movements = \App\Modules\Contabilita\Models\MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNull('registrazione_id')
|
|
->get();
|
|
|
|
if ($movements->isEmpty()) {
|
|
Notification::make()->title('Riconciliazione completata')->body('Nessun movimento bancario non registrato trovato.')->info()->send();
|
|
return;
|
|
}
|
|
|
|
$primaNotaService = new \App\Modules\Contabilita\Services\MovimentoBancaPrimaNotaService();
|
|
$fornitori = \App\Models\Fornitore::query()->get(['id', 'ragione_sociale']);
|
|
|
|
$registeredCosts = 0;
|
|
$matchedCbill = 0;
|
|
$matchedF24 = 0;
|
|
|
|
foreach ($movements as $m) {
|
|
$causale = trim((string) $m->causale);
|
|
$descText = strtoupper(($m->descrizione ?? '') . ' ' . ($m->descrizione_estesa ?? ''));
|
|
|
|
// Fallback per identificare spese bancarie, bollettini CBILL o F24 dalla descrizione
|
|
if (str_contains($descText, 'PAGAMENTO DELEGHE') || str_contains($descText, 'F24') || str_contains($descText, 'F23') || str_contains($descText, 'ENTRATEL') || str_contains($descText, 'PAGAMENTO FISCO')) {
|
|
$causale = 'F24';
|
|
} elseif ($causale === '' || ! in_array($causale, ['198', '219', '048', '208', '012'], true)) {
|
|
if (str_contains($descText, 'IMPOSTA BOLLO') || str_contains($descText, 'IMPOSTA DI BOLLO') || str_contains($descText, 'DPR642/72') || str_contains($descText, 'BOLLO CONTO')) {
|
|
$causale = '219';
|
|
} elseif (str_contains($descText, 'COMPETENZE') || str_contains($descText, 'INTERESSI') || str_contains($descText, 'ONERI') || str_contains($descText, 'TENUTA CONTO') || str_contains($descText, 'SPESE COMPILAZIONE')) {
|
|
$causale = '198';
|
|
} elseif (str_contains($descText, 'COMMISSION') || str_contains($descText, 'COMMESS')) {
|
|
$causale = '048';
|
|
} elseif (str_contains($descText, 'PAGAGO') || str_contains($descText, 'PAGOPA') || str_contains($descText, 'CBILL') || str_contains($descText, 'BOLLETTINO')) {
|
|
$causale = '012';
|
|
}
|
|
}
|
|
|
|
if ($causale !== $m->causale) {
|
|
$m->causale = $causale;
|
|
$m->save();
|
|
}
|
|
|
|
// A. Ritenute d'Acconto / F24
|
|
if ($causale === 'F24' && (float) $m->importo < 0) {
|
|
$outstandingRAs = \App\Models\RegistroRitenuteAcconto::query()
|
|
->where('stato_versamento', 'da_versare')
|
|
->whereHas('gestione', function ($q) use ($stabileId) {
|
|
$q->where('stabile_id', $stabileId);
|
|
})
|
|
->get();
|
|
|
|
$targetAmount = abs((float) $m->importo);
|
|
$matchedRAs = collect();
|
|
|
|
// Caso A.1: Cerca una singola ritenuta con importo esatto
|
|
$singleMatch = $outstandingRAs->first(fn($ra) => abs((float) $ra->importo_ritenuta - $targetAmount) < 0.01);
|
|
if ($singleMatch) {
|
|
$matchedRAs->push($singleMatch);
|
|
} else {
|
|
// Caso A.2: Cerca se la somma di tutte le ritenute in sospeso corrisponde esattamente al pagamento
|
|
$totalOutstandingSum = $outstandingRAs->sum(fn($ra) => (float) $ra->importo_ritenuta);
|
|
if (abs($totalOutstandingSum - $targetAmount) < 0.01) {
|
|
$matchedRAs = $outstandingRAs;
|
|
}
|
|
}
|
|
|
|
if ($matchedRAs->isNotEmpty()) {
|
|
foreach ($matchedRAs as $ra) {
|
|
$ra->stato_versamento = 'versata';
|
|
$ra->data_versamento = $m->data;
|
|
$ra->importo_versato = $ra->importo_ritenuta;
|
|
$ra->f24_riferimento = substr('F24 ' . $m->descrizione, 0, 100);
|
|
$ra->save();
|
|
}
|
|
|
|
// Registra in Prima Nota se è presente la regola F24
|
|
$rule = \App\Modules\Contabilita\Models\RegolaPrimaNota::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('origine', 'banca')
|
|
->where('chiave', 'F24')
|
|
->where('attiva', true)
|
|
->first();
|
|
|
|
if ($rule) {
|
|
try {
|
|
if (! $m->gestione_id) {
|
|
$m->gestione_id = $gestioneId;
|
|
$m->save();
|
|
}
|
|
$primaNotaService->generaRegistrazioneDaMovimento($user, $m);
|
|
} catch (\Throwable $e) {
|
|
\Illuminate\Support\Facades\Log::error("Errore registrazione Prima Nota F24: " . $e->getMessage());
|
|
}
|
|
} else {
|
|
// Se non c'è una regola contabile configurata, creiamo comunque una registrazione di default per quadrare il movimento
|
|
try {
|
|
if (! $m->gestione_id) {
|
|
$m->gestione_id = $gestioneId;
|
|
$m->save();
|
|
}
|
|
$primaNotaService->generaRegistrazioneDaMovimento($user, $m);
|
|
} catch (\Throwable $e) {
|
|
// Ignora se fallisce
|
|
}
|
|
}
|
|
|
|
$matchedF24++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// B. Spese Bancarie Standard (198, 219, 048, 208)
|
|
if (in_array($causale, ['198', '219', '048', '208'], true)) {
|
|
$rule = \App\Modules\Contabilita\Models\RegolaPrimaNota::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('origine', 'banca')
|
|
->where('chiave', $causale)
|
|
->where('attiva', true)
|
|
->first();
|
|
|
|
if ($rule) {
|
|
try {
|
|
if (! $m->gestione_id) {
|
|
$m->gestione_id = $gestioneId;
|
|
$m->save();
|
|
}
|
|
$primaNotaService->generaRegistrazioneDaMovimento($user, $m);
|
|
$registeredCosts++;
|
|
} catch (\Throwable $e) {
|
|
dump("EXCEPTION: " . $e->getMessage() . "\n" . $e->getTraceAsString());
|
|
}
|
|
}
|
|
}
|
|
|
|
// C. PagoPA / CBILL (012)
|
|
if ($causale === '012' && (float) $m->importo < 0) {
|
|
// Tenta di estrarre un codice CBILL / avviso numerico di 15-20 cifre dalla descrizione
|
|
$cbillCode = null;
|
|
$descCombined = ($m->descrizione ?? '') . ' ' . ($m->descrizione_estesa ?? '');
|
|
if (preg_match('/\b([0-9]{15,20})\b/', $descCombined, $cbillMatches)) {
|
|
$cbillCode = trim($cbillMatches[1]);
|
|
}
|
|
|
|
$invoice = null;
|
|
$fornIdMatched = null;
|
|
|
|
if ($cbillCode) {
|
|
$invoiceRow = DB::table('contabilita_fatture_fornitori as f')
|
|
->join('fatture_elettroniche as fe', 'fe.id', '=', 'f.fattura_elettronica_id')
|
|
->where('f.stabile_id', $stabileId)
|
|
->where(function ($q) {
|
|
$q->where('f.stato', '!=', 'pagato')
|
|
->orWhereNull('f.stato')
|
|
->orWhereNull('f.data_pagamento');
|
|
})
|
|
->where(function ($q) use ($cbillCode) {
|
|
$q->where('fe.pagamento_cbill', $cbillCode)
|
|
->orWhere('fe.pagamento_codice_avviso', $cbillCode);
|
|
})
|
|
->select('f.*')
|
|
->first();
|
|
|
|
if ($invoiceRow) {
|
|
$invoice = $invoiceRow;
|
|
$fornIdMatched = $invoiceRow->fornitore_id;
|
|
}
|
|
}
|
|
|
|
// Fallback sul matching tramite ragione sociale del fornitore e importo
|
|
if (! $invoice) {
|
|
$descNormalized = $this->normalizeStringForMatching($descCombined);
|
|
foreach ($fornitori as $forn) {
|
|
$ragSociale = $this->normalizeStringForMatching((string) $forn->ragione_sociale);
|
|
if ($ragSociale !== '' && str_contains($descNormalized, $ragSociale)) {
|
|
$invoiceRow = DB::table('contabilita_fatture_fornitori')
|
|
->where('stabile_id', $stabileId)
|
|
->where('fornitore_id', $forn->id)
|
|
->where(function ($q) {
|
|
$q->where('stato', '!=', 'pagato')
|
|
->orWhereNull('stato')
|
|
->orWhereNull('data_pagamento');
|
|
})
|
|
->where(function ($q) use ($m) {
|
|
$targetVal = abs((float) $m->importo);
|
|
$q->where('totale', $targetVal)
|
|
->orWhere('netto_da_pagare', $targetVal);
|
|
})
|
|
->first();
|
|
|
|
if ($invoiceRow) {
|
|
$invoice = $invoiceRow;
|
|
$fornIdMatched = $forn->id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($invoice) {
|
|
DB::table('contabilita_fatture_fornitori')
|
|
->where('id', $invoice->id)
|
|
->update([
|
|
'stato' => 'pagato',
|
|
'data_pagamento' => $m->data,
|
|
'movimento_pagamento_id' => $m->id,
|
|
]);
|
|
|
|
$matchData = is_string($m->match_data) ? json_decode($m->match_data, true) : (is_array($m->match_data) ? $m->match_data : []);
|
|
$matchData['riconciliato_operativo'] = true;
|
|
$matchData['riconciliazione_tipo'] = 'fattura_fornitore';
|
|
$matchData['riconciliazione_label'] = 'Pagamento CBILL/PagoPA fattura #' . $invoice->numero_documento;
|
|
$matchData['riconciliazione_at'] = now()->toDateTimeString();
|
|
$matchData['fattura_fornitore_id'] = $invoice->id;
|
|
|
|
if ($fornIdMatched) {
|
|
$m->fornitore_id = $fornIdMatched;
|
|
}
|
|
$m->registrazione_id = $invoice->registrazione_id;
|
|
$m->match_data = $matchData;
|
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) {
|
|
$m->da_confermare = false;
|
|
}
|
|
$m->save();
|
|
|
|
$matchedCbill++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($registeredCosts > 0 || $matchedCbill > 0 || $matchedF24 > 0) {
|
|
Notification::make()
|
|
->title('Elaborazione completata')
|
|
->body("Registrate {$registeredCosts} spese bancarie. Riconciliati {$matchedCbill} bollettini CBILL e {$matchedF24} deleghe F24.")
|
|
->success()
|
|
->send();
|
|
} else {
|
|
Notification::make()->title('Nessuna operazione eseguita')->body('Non è stato possibile abbinare spese bancarie, bollettini CBILL o deleghe F24 con le regole attuali.')->warning()->send();
|
|
}
|
|
|
|
$this->loadInterbankMappings();
|
|
}
|
|
|
|
private function normalizeStringForMatching(string $str): string
|
|
{
|
|
$str = strtoupper($str);
|
|
// Rimuovi suffissi aziendali comuni per facilitare il matching
|
|
$str = str_replace(
|
|
['S.P.A.', 'S.R.L.', 'S.N.C.', 'S.P.A', 'S.R.L', 'S.N.C', ' SPA', ' SRL', ' SNC', ' COOP', ' S.S.', ' S.S', ' S.P.A.'],
|
|
' ',
|
|
$str
|
|
);
|
|
// Rimuovi punteggiatura e spazi multipli
|
|
$str = preg_replace('/[^A-Z0-9\s]/', '', $str);
|
|
$str = preg_replace('/\s+/', ' ', $str);
|
|
return trim($str);
|
|
}
|
|
}
|