1870 lines
81 KiB
PHP
1870 lines
81 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Contabilita;
|
|
|
|
use App\Models\DatiBancari;
|
|
use App\Models\Fornitore;
|
|
use App\Models\GestioneContabile;
|
|
use App\Models\Stabile;
|
|
use App\Models\User;
|
|
use App\Modules\Contabilita\Models\CausaleBancaria;
|
|
use App\Modules\Contabilita\Models\MovimentoBanca;
|
|
use App\Modules\Contabilita\Models\PianoConti;
|
|
use App\Modules\Contabilita\Models\SaldoConto;
|
|
use App\Modules\Contabilita\Services\MovimentoBancaPrimaNotaService;
|
|
use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService;
|
|
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\Select;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\BadgeColumn;
|
|
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\Carbon;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use UnitEnum;
|
|
|
|
class CasseBancheMovimenti extends Page implements HasTable
|
|
{
|
|
use InteractsWithTable;
|
|
|
|
public ?string $iban = null;
|
|
|
|
public ?int $contoId = null;
|
|
|
|
public ?int $detailMovementId = null;
|
|
|
|
public string $periodoTipo = 'mese';
|
|
public int $periodoAnno;
|
|
public int $periodoValore;
|
|
public ?string $periodoRate = null;
|
|
|
|
/** @var array<string, array{label:string, raw:array<int,string>}> */
|
|
protected array $periodoRateMap = [];
|
|
|
|
protected static ?bool $supportsWindowFunctions = null;
|
|
|
|
protected static ?string $navigationLabel = 'Movimenti banca';
|
|
|
|
protected static ?string $title = 'Movimenti banca';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-banknotes';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
|
|
|
|
protected static ?int $navigationSort = 11;
|
|
protected static ?string $slug = 'contabilita/casse-banche/movimenti';
|
|
|
|
protected string $view = 'filament.pages.contabilita.casse-banche-movimenti';
|
|
|
|
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
|
|
{
|
|
$today = now();
|
|
$this->periodoAnno = (int) $today->year;
|
|
$this->periodoValore = (int) $today->month;
|
|
|
|
$this->contoId = is_numeric(request()->query('conto_id')) ? (int) request()->query('conto_id') : null;
|
|
$this->contoId = $this->contoId && $this->contoId > 0 ? $this->contoId : null;
|
|
|
|
$this->iban = is_string(request()->query('iban')) ? trim((string) request()->query('iban')) : null;
|
|
$this->iban = $this->iban !== '' ? $this->iban : null;
|
|
|
|
$this->mountInteractsWithTable();
|
|
|
|
// Risolvi conto selezionato: preferisci conto_id, altrimenti iban->conto_id.
|
|
$user = Auth::user();
|
|
if ($user instanceof User) {
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if ($stabileId) {
|
|
if ($this->contoId) {
|
|
$conto = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('id', $this->contoId)
|
|
->first(['id', 'iban']);
|
|
if (! $conto) {
|
|
$this->contoId = null;
|
|
} else {
|
|
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : '';
|
|
$this->iban = $iban !== '' ? $iban : null;
|
|
}
|
|
}
|
|
|
|
if (! $this->contoId && $this->iban) {
|
|
$conto = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('iban', $this->iban)
|
|
->orderBy('id')
|
|
->first(['id']);
|
|
if ($conto) {
|
|
$this->contoId = (int) $conto->id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default: primo conto con IBAN (mantiene comportamento "movimenti banca" predefinito).
|
|
if ($this->contoId === null && $this->iban === null) {
|
|
$first = $this->getDefaultIban();
|
|
$this->iban = $first !== '' ? $first : null;
|
|
if ($user instanceof User) {
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if ($stabileId && $this->iban) {
|
|
$conto = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('iban', $this->iban)
|
|
->orderBy('id')
|
|
->first(['id']);
|
|
if ($conto) {
|
|
$this->contoId = (int) $conto->id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function getPeriodoTotaliProperty(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => ''];
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => ''];
|
|
}
|
|
|
|
$anno = (int) ($this->periodoAnno ?: now()->year);
|
|
$val = (int) ($this->periodoValore ?: now()->month);
|
|
$start = null;
|
|
$end = null;
|
|
$label = '';
|
|
|
|
if ($this->periodoTipo === 'trimestre') {
|
|
$q = max(1, min(4, $val));
|
|
$start = Carbon::create($anno, (($q - 1) * 3) + 1, 1)->startOfDay();
|
|
$end = (clone $start)->addMonths(3)->subDay()->endOfDay();
|
|
$label = 'Trimestre ' . $q . '/' . $anno;
|
|
} else {
|
|
$m = max(1, min(12, $val));
|
|
$start = Carbon::create($anno, $m, 1)->startOfDay();
|
|
$end = (clone $start)->endOfMonth()->endOfDay();
|
|
$label = $start->format('m/Y');
|
|
}
|
|
|
|
$query = MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId))
|
|
->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban))
|
|
->whereDate('data', '>=', $start->toDateString())
|
|
->whereDate('data', '<=', $end->toDateString());
|
|
|
|
$entrate = (float) $query->sum(DB::raw('CASE WHEN importo > 0 THEN importo ELSE 0 END'));
|
|
$uscite = (float) $query->sum(DB::raw('CASE WHEN importo < 0 THEN ABS(importo) ELSE 0 END'));
|
|
$count = (int) $query->count();
|
|
|
|
return [
|
|
'entrate' => $entrate,
|
|
'uscite' => $uscite,
|
|
'saldo' => $entrate - $uscite,
|
|
'count' => $count,
|
|
'label' => $label,
|
|
];
|
|
}
|
|
|
|
public function getPeriodoOptionsProperty(): array
|
|
{
|
|
$months = [];
|
|
for ($m = 1; $m <= 12; $m++) {
|
|
$months[$m] = str_pad((string) $m, 2, '0', STR_PAD_LEFT);
|
|
}
|
|
return [
|
|
'mese' => $months,
|
|
'trimestre' => [1 => 'T1', 2 => 'T2', 3 => 'T3', 4 => 'T4'],
|
|
];
|
|
}
|
|
|
|
public function getPeriodoRateOptionsProperty(): array
|
|
{
|
|
$loaded = $this->loadPeriodoRateOptions();
|
|
$options = $loaded['options'];
|
|
if ($this->periodoRate === null && ! empty($options)) {
|
|
$this->periodoRate = array_key_first($options);
|
|
}
|
|
return $options;
|
|
}
|
|
|
|
public function getPeriodoRateTotaliProperty(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => ''];
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => ''];
|
|
}
|
|
|
|
$loaded = $this->loadPeriodoRateOptions();
|
|
$options = $loaded['options'];
|
|
$map = $loaded['map'];
|
|
$selected = $this->periodoRate;
|
|
if (! $selected || ! isset($map[$selected])) {
|
|
return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => ''];
|
|
}
|
|
|
|
$rawValues = $map[$selected]['raw'] ?? [];
|
|
if (empty($rawValues)) {
|
|
return ['entrate' => 0, 'uscite' => 0, 'saldo' => 0, 'count' => 0, 'label' => (string) ($map[$selected]['label'] ?? $selected)];
|
|
}
|
|
|
|
$query = MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId))
|
|
->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban))
|
|
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(match_data, '$.periodo_rate')) in (" . implode(',', array_fill(0, count($rawValues), '?')) . ")", $rawValues);
|
|
|
|
$entrate = (float) $query->sum(DB::raw('CASE WHEN importo > 0 THEN importo ELSE 0 END'));
|
|
$uscite = (float) $query->sum(DB::raw('CASE WHEN importo < 0 THEN ABS(importo) ELSE 0 END'));
|
|
$count = (int) $query->count();
|
|
|
|
return [
|
|
'entrate' => $entrate,
|
|
'uscite' => $uscite,
|
|
'saldo' => $entrate - $uscite,
|
|
'count' => $count,
|
|
'label' => (string) ($map[$selected]['label'] ?? $selected),
|
|
];
|
|
}
|
|
|
|
/** @return array{options:array<string,string>, map:array<string, array{label:string, raw:array<int,string>}>} */
|
|
protected function loadPeriodoRateOptions(): array
|
|
{
|
|
if (! empty($this->periodoRateMap)) {
|
|
$options = [];
|
|
foreach ($this->periodoRateMap as $key => $payload) {
|
|
$options[$key] = $payload['label'];
|
|
}
|
|
return ['options' => $options, 'map' => $this->periodoRateMap];
|
|
}
|
|
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return ['options' => [], 'map' => []];
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return ['options' => [], 'map' => []];
|
|
}
|
|
|
|
$values = MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId))
|
|
->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban))
|
|
->whereRaw("JSON_EXTRACT(match_data, '$.periodo_rate') IS NOT NULL")
|
|
->selectRaw("DISTINCT JSON_UNQUOTE(JSON_EXTRACT(match_data, '$.periodo_rate')) AS periodo_rate")
|
|
->pluck('periodo_rate')
|
|
->filter(fn($v) => is_string($v) && trim($v) !== '')
|
|
->values()
|
|
->all();
|
|
|
|
$map = [];
|
|
foreach ($values as $val) {
|
|
$norm = $this->normalizePeriodoRate($val);
|
|
if ($norm === null) {
|
|
continue;
|
|
}
|
|
if (! isset($map[$norm])) {
|
|
$map[$norm] = ['label' => $norm, 'raw' => []];
|
|
}
|
|
$map[$norm]['raw'][] = $val;
|
|
}
|
|
|
|
ksort($map);
|
|
$this->periodoRateMap = $map;
|
|
|
|
$options = [];
|
|
foreach ($map as $key => $payload) {
|
|
$options[$key] = $payload['label'];
|
|
}
|
|
|
|
return ['options' => $options, 'map' => $map];
|
|
}
|
|
|
|
protected function normalizePeriodoRate(?string $value): ?string
|
|
{
|
|
$v = trim((string) $value);
|
|
if ($v === '') {
|
|
return null;
|
|
}
|
|
$v = preg_replace('/\b(20)?\d{2}\b/', '', $v) ?? $v;
|
|
$v = preg_replace('/\s{2,}/', ' ', $v) ?? $v;
|
|
$v = trim($v, " \t\n\r\0\x0B-/");
|
|
return $v !== '' ? $v : null;
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('torna_riepilogo')
|
|
->label('Torna a Casse e banche')
|
|
->icon('heroicon-o-arrow-left')
|
|
->url(fn() => CasseBancheRiepilogo::getUrl(panel: 'admin-filament')),
|
|
|
|
Action::make('saldi_conti')
|
|
->label('Saldi conti')
|
|
->icon('heroicon-o-scale')
|
|
->url(fn() => SaldiContiArchivio::getUrl(panel: 'admin-filament')),
|
|
|
|
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(function (): array {
|
|
$opts = [];
|
|
foreach ($this->getDisponibilitaFinanziarie() as $c) {
|
|
$iban = is_string($c['iban'] ?? null) ? trim((string) $c['iban']) : '';
|
|
if ($iban === '') {
|
|
continue;
|
|
}
|
|
$opts[$iban] = (string) ($c['label'] ?? $iban);
|
|
}
|
|
return $opts;
|
|
})
|
|
->default(fn(): ?string => (is_string($this->iban) && trim($this->iban) !== '') ? trim($this->iban) : null)
|
|
->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(function (): 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();
|
|
})
|
|
->default(function (): ?string {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
return $this->resolveDefaultGestioneId($stabileId);
|
|
}),
|
|
|
|
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']) && $data['iban'] !== '' ? $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'] . ' · Duplicati: ' . $res['duplicates'])
|
|
->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('importa_intesa_csv')
|
|
->label('Importa estratto (Intesa CSV)')
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->modalWidth('7xl')
|
|
->form([
|
|
Select::make('iban')
|
|
->label('Conto (IBAN)')
|
|
->native(false)
|
|
->searchable()
|
|
->options(function (): array {
|
|
$opts = [];
|
|
foreach ($this->getDisponibilitaFinanziarie() as $c) {
|
|
$iban = is_string($c['iban'] ?? null) ? trim((string) $c['iban']) : '';
|
|
if ($iban === '') {
|
|
continue;
|
|
}
|
|
$opts[$iban] = (string) ($c['label'] ?? $iban);
|
|
}
|
|
return $opts;
|
|
})
|
|
->default(fn(): ?string => (is_string($this->iban) && trim($this->iban) !== '') ? trim($this->iban) : null)
|
|
->helperText('Consigliato: seleziona il conto per associare i movimenti. Se non selezionato, l\'import prova a rilevare l\'IBAN (quando presente).'),
|
|
|
|
Select::make('gestione_id')
|
|
->label('Gestione (opzionale)')
|
|
->native(false)
|
|
->searchable()
|
|
->options(function (): 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();
|
|
})
|
|
->default(function (): ?string {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
return $this->resolveDefaultGestioneId($stabileId);
|
|
}),
|
|
|
|
FileUpload::make('file')
|
|
->label('File estratto')
|
|
->directory('tmp/banca')
|
|
->disk('local')
|
|
->required(),
|
|
|
|
Toggle::make('replace')
|
|
->label('Sostituisci import nel periodo')
|
|
->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->importIntesaCsv(
|
|
$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'] . ' · Duplicati: ' . $res['duplicates'])
|
|
->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('importa_excel_xlsx')
|
|
->label('Importa estratto (Excel XLSX)')
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->modalWidth('7xl')
|
|
->form([
|
|
Select::make('conto_id')
|
|
->label('Conto')
|
|
->native(false)
|
|
->searchable()
|
|
->options(function (): array {
|
|
$opts = [];
|
|
foreach ($this->getDisponibilitaFinanziarie() as $c) {
|
|
if (! empty($c['id'])) {
|
|
$opts[$c['id']] = (string) ($c['label'] ?? ('Conto #' . $c['id']));
|
|
}
|
|
}
|
|
return $opts;
|
|
})
|
|
->default(fn(): ?int => $this->contoId)
|
|
->required(),
|
|
|
|
Select::make('gestione_id')
|
|
->label('Gestione (opzionale)')
|
|
->native(false)
|
|
->searchable()
|
|
->options(function (): 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();
|
|
})
|
|
->default(function (): ?string {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
return $this->resolveDefaultGestioneId($stabileId);
|
|
}),
|
|
|
|
FileUpload::make('file')
|
|
->label('File XLSX')
|
|
->directory('tmp/banca')
|
|
->disk('local')
|
|
->required(),
|
|
|
|
Toggle::make('replace')
|
|
->label('Sostituisci import nel periodo')
|
|
->helperText('Elimina SOLO i movimenti importati e NON riconciliati 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;
|
|
}
|
|
|
|
$contoId = isset($data['conto_id']) && is_numeric($data['conto_id']) ? (int) $data['conto_id'] : null;
|
|
if (! $contoId) {
|
|
Notification::make()->title('Seleziona un conto')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$path = (string) ($data['file'] ?? '');
|
|
if ($path === '') {
|
|
Notification::make()->title('Seleziona un file')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$full = Storage::disk('local')->path($path);
|
|
$importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser());
|
|
$res = $importer->importExcelXlsxForConto(
|
|
$full,
|
|
$stabileId,
|
|
$contoId,
|
|
basename($path),
|
|
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'] . ' · Duplicati: ' . $res['duplicates'])
|
|
->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
|
|
}
|
|
}
|
|
}),
|
|
];
|
|
}
|
|
|
|
protected function getTableQuery(): Builder
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return MovimentoBanca::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $activeStabileId) {
|
|
return MovimentoBanca::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$query = MovimentoBanca::query()
|
|
->with('gestioneContabile')
|
|
->where('stabile_id', $activeStabileId)
|
|
->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId))
|
|
->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban))
|
|
->orderBy('data')
|
|
->orderBy('id');
|
|
|
|
if ($this->dbSupportsWindowFunctions()) {
|
|
$query->select('contabilita_movimenti_banca.*')
|
|
->selectRaw(
|
|
"SUM(importo) OVER (PARTITION BY stabile_id, COALESCE(iban, '') ORDER BY data, id) AS saldo_progressivo"
|
|
);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->striped()
|
|
->filters([
|
|
Filter::make('ricerca_descrizione')
|
|
->label('Ricerca')
|
|
->form([
|
|
TextInput::make('q')
|
|
->label('Descrizione')
|
|
->placeholder('Es. Basta Monica')
|
|
->autocomplete(false),
|
|
])
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$q = trim((string) ($data['q'] ?? ''));
|
|
if ($q === '') {
|
|
return $query;
|
|
}
|
|
|
|
$qLike = '%' . mb_strtolower($q) . '%';
|
|
|
|
return $query->where(function (Builder $sub) use ($qLike): Builder {
|
|
$sub->whereRaw('LOWER(descrizione) LIKE ?', [$qLike])
|
|
->orWhereRaw('LOWER(descrizione_estesa) LIKE ?', [$qLike])
|
|
->orWhereRaw('LOWER(note) LIKE ?', [$qLike]);
|
|
|
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'mittente')) {
|
|
$sub->orWhereRaw('LOWER(mittente) LIKE ?', [$qLike]);
|
|
}
|
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'intestatario')) {
|
|
$sub->orWhereRaw('LOWER(intestatario) LIKE ?', [$qLike]);
|
|
}
|
|
if (Schema::hasColumn('contabilita_movimenti_banca', 'ordinante')) {
|
|
$sub->orWhereRaw('LOWER(ordinante) LIKE ?', [$qLike]);
|
|
}
|
|
|
|
return $sub;
|
|
});
|
|
}),
|
|
Filter::make('periodo')
|
|
->label('Periodo')
|
|
->form([
|
|
DatePicker::make('from')->label('Dal'),
|
|
DatePicker::make('to')->label('Al'),
|
|
])
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$from = $data['from'] ?? null;
|
|
$to = $data['to'] ?? null;
|
|
|
|
return $query
|
|
->when($from, fn(Builder $q) => $q->whereDate('data', '>=', $from))
|
|
->when($to, fn(Builder $q) => $q->whereDate('data', '<=', $to));
|
|
}),
|
|
|
|
Filter::make('causale_bancaria')
|
|
->label('Causale')
|
|
->form([
|
|
Select::make('causale')
|
|
->label('Causale')
|
|
->native(false)
|
|
->searchable()
|
|
->options(function (): array {
|
|
if (! DB::getSchemaBuilder()->hasTable('contabilita_causali_bancarie')) {
|
|
return [];
|
|
}
|
|
|
|
return CausaleBancaria::query()
|
|
->where('attivo', true)
|
|
->orderBy('banca')
|
|
->orderBy('codice')
|
|
->get(['banca', 'codice', 'descrizione'])
|
|
->mapWithKeys(fn(CausaleBancaria $c) => [
|
|
(string) $c->codice => (string) $c->banca . ' · ' . (string) $c->codice . ' · ' . (string) $c->descrizione,
|
|
])
|
|
->all();
|
|
}),
|
|
])
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$code = $data['causale'] ?? null;
|
|
$code = is_string($code) ? trim($code) : '';
|
|
if ($code === '') {
|
|
return $query;
|
|
}
|
|
return $query->where('causale', $code);
|
|
}),
|
|
|
|
Filter::make('gestioni')
|
|
->label('Gestioni')
|
|
->form([
|
|
Select::make('gestione_ids')
|
|
->label('Gestione')
|
|
->multiple()
|
|
->options(function (): 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();
|
|
})
|
|
->searchable(),
|
|
])
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$ids = $data['gestione_ids'] ?? null;
|
|
if (! is_array($ids) || $ids === []) {
|
|
return $query;
|
|
}
|
|
|
|
$ids = array_values(array_filter(array_map('intval', $ids), fn(int $v) => $v > 0));
|
|
if ($ids === []) {
|
|
return $query;
|
|
}
|
|
|
|
return $query->whereIn('gestione_id', $ids);
|
|
}),
|
|
])
|
|
->columns([
|
|
TextColumn::make('codice_descrizione')
|
|
->label('Codice')
|
|
->state(function (MovimentoBanca $record): string {
|
|
$descr = (string) ($record->descrizione ?? '');
|
|
$raw = strtoupper($descr);
|
|
if (str_contains($raw, 'ACCREDITO_BEU_CON_CONTABILE')) {
|
|
return 'ACCBEUCONCON';
|
|
}
|
|
if (str_contains($raw, 'COMBONMYB')) {
|
|
return 'COMBONMYB';
|
|
}
|
|
$raw = preg_replace('/[^A-Z0-9]+/', ' ', $raw) ?? $raw;
|
|
$parts = array_filter(explode(' ', $raw), fn($v) => $v !== '');
|
|
if (! empty($parts)) {
|
|
$code = '';
|
|
foreach ($parts as $p) {
|
|
$chunk = substr($p, 0, 3);
|
|
$code .= $chunk;
|
|
if (strlen($code) >= 12) {
|
|
break;
|
|
}
|
|
}
|
|
$code = substr($code, 0, 12);
|
|
if ($code !== '') {
|
|
return $code;
|
|
}
|
|
}
|
|
$raw = preg_replace('/[^A-Z0-9]+/', '_', $raw) ?? $raw;
|
|
$raw = trim($raw, '_');
|
|
$raw = preg_replace('/_+/', '_', $raw) ?? $raw;
|
|
return mb_substr($raw, 0, 32);
|
|
})
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('disp')
|
|
->label('Disp')
|
|
->state(function (MovimentoBanca $record): string {
|
|
$match = is_array($record->match_data ?? null) ? $record->match_data : [];
|
|
$disp = $match['cod_disp'] ?? null;
|
|
return is_string($disp) ? trim($disp) : '';
|
|
})
|
|
->toggleable(),
|
|
TextColumn::make('cash')
|
|
->label('Cash')
|
|
->state(function (MovimentoBanca $record): string {
|
|
$match = is_array($record->match_data ?? null) ? $record->match_data : [];
|
|
$cash = $match['cash'] ?? null;
|
|
return is_string($cash) ? trim($cash) : '';
|
|
})
|
|
->toggleable(),
|
|
TextColumn::make('mittente')
|
|
->label('Mittente')
|
|
->state(function (MovimentoBanca $record): string {
|
|
$match = is_array($record->match_data ?? null) ? $record->match_data : [];
|
|
$mitt = $match['mittente'] ?? null;
|
|
return is_string($mitt) ? trim($mitt) : '';
|
|
})
|
|
->wrap()
|
|
->toggleable(),
|
|
BadgeColumn::make('commissioni')
|
|
->label('Comm.')
|
|
->getStateUsing(function (MovimentoBanca $record): string {
|
|
$raw = strtoupper((string) ($record->descrizione ?? ''));
|
|
return str_contains($raw, 'COMBONMYB') ? 'si' : 'no';
|
|
})
|
|
->colors([
|
|
'success' => 'si',
|
|
'gray' => 'no',
|
|
])
|
|
->formatStateUsing(fn(string $state): string => $state === 'si' ? 'SI' : '—')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('disp')
|
|
->label('Disp')
|
|
->state(function (MovimentoBanca $record): string {
|
|
$match = is_array($record->match_data ?? null) ? $record->match_data : [];
|
|
$disp = $match['cod_disp'] ?? null;
|
|
return is_string($disp) ? trim($disp) : '';
|
|
})
|
|
->toggleable(),
|
|
BadgeColumn::make('da_confermare')
|
|
->label('Conferma')
|
|
->getStateUsing(fn(MovimentoBanca $record): string => ! empty($record->da_confermare) ? 'da_confermare' : 'ok')
|
|
->colors([
|
|
'warning' => 'da_confermare',
|
|
'success' => 'ok',
|
|
])
|
|
->formatStateUsing(fn(string $state): string => $state === 'da_confermare' ? 'DA CONF.' : 'OK')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
BadgeColumn::make('stato_quadratura')
|
|
->label('Stato')
|
|
->getStateUsing(fn(MovimentoBanca $record): string => $this->resolveStatoQuadratura($record))
|
|
->colors([
|
|
'gray' => 'non_collegato',
|
|
'warning' => 'collegato_non_quadrato',
|
|
'success' => 'quadrato',
|
|
])
|
|
->formatStateUsing(function (string $state): string {
|
|
return match ($state) {
|
|
'quadrato' => 'OK',
|
|
'collegato_non_quadrato' => 'ATT',
|
|
default => 'NO',
|
|
};
|
|
})
|
|
->sortable(false),
|
|
|
|
TextColumn::make('voci_collegate')
|
|
->label('Voce spesa')
|
|
->state(function (MovimentoBanca $record): string {
|
|
if (! $record->registrazione_id || ! Schema::hasColumn('contabilita_movimenti', 'voce_spesa_id')) {
|
|
return '—';
|
|
}
|
|
|
|
static $cache = [];
|
|
$regId = (int) $record->registrazione_id;
|
|
if (array_key_exists($regId, $cache)) {
|
|
return $cache[$regId];
|
|
}
|
|
|
|
$rows = DB::table('contabilita_movimenti as m')
|
|
->leftJoin('voci_spesa as v', 'v.id', '=', 'm.voce_spesa_id')
|
|
->where('m.registrazione_id', $regId)
|
|
->whereNotNull('m.voce_spesa_id')
|
|
->select('v.codice', 'v.descrizione')
|
|
->distinct()
|
|
->limit(5)
|
|
->get();
|
|
|
|
if ($rows->isEmpty()) {
|
|
return $cache[$regId] = '—';
|
|
}
|
|
|
|
$labels = [];
|
|
foreach ($rows as $row) {
|
|
$code = trim((string) ($row->codice ?? ''));
|
|
$desc = trim((string) ($row->descrizione ?? ''));
|
|
$labels[] = $code !== '' ? ($code . ($desc !== '' ? ' · ' . $desc : '')) : ($desc !== '' ? $desc : 'Voce');
|
|
}
|
|
|
|
return $cache[$regId] = implode(' | ', array_values(array_unique($labels)));
|
|
})
|
|
->wrap()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('data')
|
|
->label('Data')
|
|
->date('d/m/Y')
|
|
->sortable(),
|
|
|
|
TextColumn::make('valuta')
|
|
->label('Valuta')
|
|
->date('d/m/Y')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('descrizione')
|
|
->label('Descrizione')
|
|
->state(fn(MovimentoBanca $record): string => (string) ($record->descrizione_estesa_pulita ?? $record->descrizione_estesa ?? $record->descrizione ?? ''))
|
|
->wrap()
|
|
->searchable(),
|
|
|
|
TextColumn::make('gestioneContabile.denominazione')
|
|
->label('Gestione')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
TextColumn::make('entrate')
|
|
->label('Entrate')
|
|
->alignEnd()
|
|
->state(fn(MovimentoBanca $record): float => max(0, (float) $record->importo))
|
|
->formatStateUsing(fn($state) => $state > 0 ? ('€ ' . number_format((float) $state, 2, ',', '.')) : ''),
|
|
|
|
TextColumn::make('uscite')
|
|
->label('Uscite')
|
|
->alignEnd()
|
|
->state(fn(MovimentoBanca $record): float => max(0, abs((float) $record->importo) * ((float) $record->importo < 0 ? 1 : 0)))
|
|
->formatStateUsing(fn($state) => $state > 0 ? ('€ ' . number_format((float) $state, 2, ',', '.')) : ''),
|
|
|
|
TextColumn::make('saldo_progressivo')
|
|
->label('Saldo')
|
|
->alignEnd()
|
|
->getStateUsing(fn(MovimentoBanca $record): ?float => $this->resolveSaldoConSaldi($record))
|
|
->formatStateUsing(fn($state) => $state !== null ? ('€ ' . number_format((float) $state, 2, ',', '.')) : ''),
|
|
|
|
TextColumn::make('importo')
|
|
->label('Importo (raw)')
|
|
->alignEnd()
|
|
->toggleable(isToggledHiddenByDefault: true)
|
|
->formatStateUsing(fn($state) => '€ ' . number_format((float) $state, 2, ',', '.')),
|
|
|
|
TextColumn::make('causale')
|
|
->label('Caus.')
|
|
->toggleable(),
|
|
|
|
TextColumn::make('iban')
|
|
->label('IBAN')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
])
|
|
->actions([
|
|
Action::make('dettaglio_movimento')
|
|
->label('Dettaglio')
|
|
->icon('heroicon-o-eye')
|
|
->modalWidth('7xl')
|
|
->action(function (MovimentoBanca $record): void {
|
|
$this->detailMovementId = $record->id;
|
|
})
|
|
->modalHeading(fn(): string => 'Dettaglio movimento')
|
|
->modalContent(function () {
|
|
$record = $this->getDetailMovement();
|
|
return view('filament.pages.contabilita.partials.movimento-banca-dettaglio', [
|
|
'record' => $record,
|
|
'prevId' => $this->getAdjacentMovementId('prev'),
|
|
'nextId' => $this->getAdjacentMovementId('next'),
|
|
]);
|
|
}),
|
|
Action::make('conferma_movimento')
|
|
->label('Conferma')
|
|
->icon('heroicon-o-check-circle')
|
|
->visible(fn(MovimentoBanca $record): bool => (bool) ($record->da_confermare ?? false))
|
|
->action(function (MovimentoBanca $record): void {
|
|
if (! Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) {
|
|
return;
|
|
}
|
|
$record->da_confermare = false;
|
|
$record->save();
|
|
Notification::make()->title('Movimento confermato')->success()->send();
|
|
$this->resetTable();
|
|
}),
|
|
|
|
Action::make('genera_prima_nota')
|
|
->label('Genera prima nota')
|
|
->icon('heroicon-o-book-open')
|
|
->visible(function (MovimentoBanca $record): bool {
|
|
if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) {
|
|
return false;
|
|
}
|
|
return empty($record->registrazione_id);
|
|
})
|
|
->form([
|
|
Select::make('gestione_id')
|
|
->label('Gestione')
|
|
->options(function (): array {
|
|
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
|
return [];
|
|
}
|
|
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderByDesc('anno_gestione')
|
|
->orderBy('tipo_gestione')
|
|
->orderBy('numero_straordinaria')
|
|
->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria'])
|
|
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
(string) $g->id => $g->denominazione
|
|
. ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione
|
|
. ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '')
|
|
. ')',
|
|
])
|
|
->all();
|
|
})
|
|
->searchable()
|
|
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
|
|
->required(fn(MovimentoBanca $record) => empty($record->gestione_id)),
|
|
])
|
|
->action(function (MovimentoBanca $record, array $data): void {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non valido')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
if (empty($record->gestione_id) && isset($data['gestione_id']) && is_numeric($data['gestione_id'])) {
|
|
$record->gestione_id = (int) $data['gestione_id'];
|
|
$record->save();
|
|
}
|
|
|
|
try {
|
|
$service = app(MovimentoBancaPrimaNotaService::class);
|
|
$service->generaRegistrazioneDaMovimento($user, $record);
|
|
Notification::make()->title('Prima nota generata')->success()->send();
|
|
$this->resetTable();
|
|
} catch (\Throwable $e) {
|
|
Notification::make()->title('Errore')->body($e->getMessage())->danger()->send();
|
|
}
|
|
}),
|
|
|
|
Action::make('paga_fornitore')
|
|
->label('Paga fornitore')
|
|
->icon('heroicon-o-banknotes')
|
|
->visible(function (MovimentoBanca $record): bool {
|
|
if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) {
|
|
return false;
|
|
}
|
|
if (! Schema::hasTable('contabilita_registrazioni') || ! Schema::hasTable('contabilita_movimenti')) {
|
|
return false;
|
|
}
|
|
|
|
$importo = (float) ($record->importo ?? 0);
|
|
if ($importo >= 0) {
|
|
return false;
|
|
}
|
|
|
|
return empty($record->registrazione_id);
|
|
})
|
|
->form([
|
|
Select::make('gestione_id')
|
|
->label('Gestione')
|
|
->options(function (): array {
|
|
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
|
return [];
|
|
}
|
|
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderByDesc('anno_gestione')
|
|
->orderBy('tipo_gestione')
|
|
->orderBy('numero_straordinaria')
|
|
->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria'])
|
|
->mapWithKeys(fn(GestioneContabile $g) => [
|
|
(string) $g->id => $g->denominazione
|
|
. ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione
|
|
. ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '')
|
|
. ')',
|
|
])
|
|
->all();
|
|
})
|
|
->searchable()
|
|
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
|
|
->required(fn(MovimentoBanca $record) => empty($record->gestione_id)),
|
|
|
|
Select::make('fornitore_id')
|
|
->label('Fornitore')
|
|
->options(function (): array {
|
|
$q = Fornitore::query();
|
|
|
|
if (Schema::hasColumn('fornitori', 'ragione_sociale')) {
|
|
$q->orderBy('ragione_sociale');
|
|
} elseif (Schema::hasColumn('fornitori', 'cognome')) {
|
|
$q->orderBy('cognome');
|
|
}
|
|
|
|
return $q
|
|
->limit(500)
|
|
->get(['id', 'ragione_sociale', 'nome', 'cognome'])
|
|
->mapWithKeys(function (Fornitore $f): array {
|
|
$label = is_string($f->ragione_sociale ?? null) && trim((string) $f->ragione_sociale) !== ''
|
|
? trim((string) $f->ragione_sociale)
|
|
: trim(((string) ($f->cognome ?? '')) . ' ' . ((string) ($f->nome ?? '')));
|
|
$label = $label !== '' ? $label : ('Fornitore #' . (string) $f->id);
|
|
|
|
return [(string) $f->id => $label];
|
|
})
|
|
->all();
|
|
})
|
|
->searchable()
|
|
->required(),
|
|
|
|
Select::make('conto_finanziario_id')
|
|
->label('Conto finanziario (Banca/Cassa)')
|
|
->options(function (): array {
|
|
$base = PianoConti::query();
|
|
if (Schema::hasColumn('contabilita_piano_conti', 'tipo')) {
|
|
$base->where('tipo', 'Attività');
|
|
}
|
|
|
|
$q = (clone $base)->where(function ($w) {
|
|
$w->where('descrizione', 'like', '%Banca%')
|
|
->orWhere('descrizione', 'like', '%banca%')
|
|
->orWhere('descrizione', 'like', '%Cassa%')
|
|
->orWhere('descrizione', 'like', '%cassa%');
|
|
});
|
|
|
|
$rows = $q->orderBy('codice')->limit(500)->get(['id', 'codice', 'descrizione']);
|
|
if ($rows->count() === 0) {
|
|
$rows = $base->orderBy('codice')->limit(500)->get(['id', 'codice', 'descrizione']);
|
|
}
|
|
|
|
return $rows
|
|
->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione])
|
|
->all();
|
|
})
|
|
->searchable()
|
|
->required(),
|
|
])
|
|
->action(function (MovimentoBanca $record, array $data): void {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non valido')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$gestioneId = (empty($record->gestione_id) && isset($data['gestione_id']) && is_numeric($data['gestione_id']))
|
|
? (int) $data['gestione_id']
|
|
: (int) ($record->gestione_id ?? 0);
|
|
|
|
$fornitoreId = isset($data['fornitore_id']) && is_numeric($data['fornitore_id']) ? (int) $data['fornitore_id'] : 0;
|
|
$contoFinId = isset($data['conto_finanziario_id']) && is_numeric($data['conto_finanziario_id']) ? (int) $data['conto_finanziario_id'] : 0;
|
|
|
|
try {
|
|
$service = app(PagamentoFornitorePrimaNotaService::class);
|
|
$service->generaPagamentoDaMovimento($user, $record, $fornitoreId, [
|
|
'gestione_id' => $gestioneId,
|
|
'conto_finanziario_id' => $contoFinId,
|
|
]);
|
|
Notification::make()->title('Pagamento contabilizzato')->success()->send();
|
|
$this->resetTable();
|
|
} catch (\Throwable $e) {
|
|
Notification::make()->title('Errore')->body($e->getMessage())->danger()->send();
|
|
}
|
|
}),
|
|
]);
|
|
}
|
|
|
|
public function getDetailMovement(): ?MovimentoBanca
|
|
{
|
|
if (! $this->detailMovementId) {
|
|
return null;
|
|
}
|
|
return MovimentoBanca::query()->where('id', $this->detailMovementId)->first();
|
|
}
|
|
|
|
public function setDetailMovement(int $id): void
|
|
{
|
|
$this->detailMovementId = $id;
|
|
}
|
|
|
|
private function getAdjacentMovementId(string $dir): ?int
|
|
{
|
|
if (! $this->detailMovementId) {
|
|
return null;
|
|
}
|
|
$current = MovimentoBanca::query()->where('id', $this->detailMovementId)->first(['id', 'data']);
|
|
if (! $current) {
|
|
return null;
|
|
}
|
|
$query = MovimentoBanca::query()
|
|
->when($this->contoId !== null, fn(Builder $q): Builder => $q->where('conto_id', $this->contoId))
|
|
->when($this->contoId === null && $this->iban !== null, fn(Builder $q): Builder => $q->where('iban', $this->iban));
|
|
|
|
if ($dir === 'prev') {
|
|
$query->where(function (Builder $q) use ($current): void {
|
|
$q->where('data', '<', $current->data)
|
|
->orWhere(function (Builder $q2) use ($current): void {
|
|
$q2->where('data', $current->data)->where('id', '<', $current->id);
|
|
});
|
|
})->orderByDesc('data')->orderByDesc('id');
|
|
} else {
|
|
$query->where(function (Builder $q) use ($current): void {
|
|
$q->where('data', '>', $current->data)
|
|
->orWhere(function (Builder $q2) use ($current): void {
|
|
$q2->where('data', $current->data)->where('id', '>', $current->id);
|
|
});
|
|
})->orderBy('data')->orderBy('id');
|
|
}
|
|
|
|
return $query->value('id');
|
|
}
|
|
|
|
/** @return array<string,string> */
|
|
protected function getActiveStabileIbans(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
$stabile = StabileContext::getActiveStabile($user);
|
|
if (! $stabile) {
|
|
return [];
|
|
}
|
|
|
|
$ibans = [];
|
|
foreach (
|
|
[
|
|
$stabile->iban_principale ?? null,
|
|
$stabile->iban_secondario ?? null,
|
|
$stabile->iban_condominio ?? null,
|
|
] as $iban
|
|
) {
|
|
$iban = is_string($iban) ? trim($iban) : '';
|
|
if ($iban !== '') {
|
|
$ibans[$iban] = $iban;
|
|
}
|
|
}
|
|
|
|
return $ibans;
|
|
}
|
|
|
|
protected function resolveSaldoProgressivoConSaldoIniziale(MovimentoBanca $record): ?float
|
|
{
|
|
$base = $this->resolveSaldoProgressivo($record);
|
|
if ($base === null) {
|
|
return null;
|
|
}
|
|
|
|
$statePeriodo = $this->getTableFilterState('periodo');
|
|
$from = is_array($statePeriodo) ? ($statePeriodo['from'] ?? null) : null;
|
|
if (! $from) {
|
|
return $base;
|
|
}
|
|
|
|
$iban = is_string($this->iban) ? trim($this->iban) : '';
|
|
if ($iban === '') {
|
|
return $base;
|
|
}
|
|
|
|
$saldoIniziale = $this->resolveSaldoIniziale((int) $record->stabile_id, $iban, (string) $from);
|
|
if ($saldoIniziale === null) {
|
|
return $base;
|
|
}
|
|
|
|
return $saldoIniziale + $base;
|
|
}
|
|
|
|
protected function resolveSaldoConSaldi(MovimentoBanca $record): ?float
|
|
{
|
|
$baseProgressivo = $this->resolveSaldoProgressivo($record);
|
|
if ($baseProgressivo === null) {
|
|
return null;
|
|
}
|
|
|
|
$stabileId = (int) $record->stabile_id;
|
|
$iban = is_string($record->iban) ? trim((string) $record->iban) : '';
|
|
if ($iban === '') {
|
|
// Per conti senza IBAN, al momento i movimenti non sono importati: saldo progressivo = somma importi.
|
|
return (float) $baseProgressivo;
|
|
}
|
|
|
|
$saldoIniziale = $this->resolveSaldoInizialeDaDatiBancari($stabileId, $this->contoId, $iban);
|
|
|
|
// Offset da eventuali saldi intermedi: saldo atteso alla data_saldo meno saldo calcolato fino a quella data.
|
|
$offset = $this->resolveSaldoOffsetDaSaldi($stabileId, $this->contoId, $iban, $record->data);
|
|
|
|
return $saldoIniziale + (float) $baseProgressivo + $offset;
|
|
}
|
|
|
|
protected function resolveSaldoInizialeDaDatiBancari(int $stabileId, ?int $contoId, string $iban): float
|
|
{
|
|
static $cache = [];
|
|
$key = $stabileId . '|' . ($contoId ?: 0) . '|' . $iban;
|
|
if (array_key_exists($key, $cache)) {
|
|
return (float) $cache[$key];
|
|
}
|
|
|
|
$q = DatiBancari::query()->where('stabile_id', $stabileId);
|
|
if ($contoId) {
|
|
$q->where('id', $contoId);
|
|
} else {
|
|
$q->where('iban', $iban);
|
|
}
|
|
|
|
$row = $q->first(['saldo_iniziale']);
|
|
$cache[$key] = $row && is_numeric($row->saldo_iniziale) ? (float) $row->saldo_iniziale : 0.0;
|
|
return (float) $cache[$key];
|
|
}
|
|
|
|
protected function resolveSaldoOffsetDaSaldi(int $stabileId, ?int $contoId, string $iban, Carbon $recordDate): float
|
|
{
|
|
if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
|
return 0.0;
|
|
}
|
|
|
|
static $saldiCache = [];
|
|
$cacheKey = $stabileId . '|' . ($contoId ?: 0) . '|' . $iban;
|
|
if (! array_key_exists($cacheKey, $saldiCache)) {
|
|
$q = SaldoConto::query()->where('stabile_id', $stabileId);
|
|
if ($contoId) {
|
|
$q->where('conto_id', $contoId);
|
|
} else {
|
|
$q->where('iban', $iban);
|
|
}
|
|
$saldiCache[$cacheKey] = $q
|
|
->orderBy('data_saldo')
|
|
->orderBy('id')
|
|
->get(['id', 'data_saldo', 'saldo'])
|
|
->all();
|
|
}
|
|
|
|
$saldi = $saldiCache[$cacheKey];
|
|
if (! is_array($saldi) || $saldi === []) {
|
|
return 0.0;
|
|
}
|
|
|
|
$match = null;
|
|
foreach ($saldi as $s) {
|
|
if (! $s || ! isset($s->data_saldo)) {
|
|
continue;
|
|
}
|
|
if ($s->data_saldo->toDateString() <= $recordDate->toDateString()) {
|
|
$match = $s;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (! $match) {
|
|
return 0.0;
|
|
}
|
|
|
|
static $sumToDateCache = [];
|
|
$sumKey = $cacheKey . '|' . $match->data_saldo->toDateString() . '|' . (int) $match->id;
|
|
if (! array_key_exists($sumKey, $sumToDateCache)) {
|
|
$q = MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('iban', $iban)
|
|
->whereDate('data', '<=', $match->data_saldo->toDateString());
|
|
if ($contoId) {
|
|
$q->where('conto_id', $contoId);
|
|
}
|
|
$sumToDateCache[$sumKey] = (float) ($q->sum('importo') ?? 0.0);
|
|
}
|
|
|
|
$saldoIniziale = $this->resolveSaldoInizialeDaDatiBancari($stabileId, $contoId, $iban);
|
|
$saldoAttesoAllaDataSaldo = (float) $match->saldo;
|
|
$saldoCalcolatoAllaDataSaldo = $saldoIniziale + (float) $sumToDateCache[$sumKey];
|
|
return $saldoAttesoAllaDataSaldo - $saldoCalcolatoAllaDataSaldo;
|
|
}
|
|
|
|
protected function resolveSaldoIniziale(int $stabileId, string $iban, string $fromDate): ?float
|
|
{
|
|
if (! DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
|
return null;
|
|
}
|
|
|
|
static $cache = [];
|
|
$key = $stabileId . '|' . $iban . '|' . $fromDate;
|
|
if (array_key_exists($key, $cache)) {
|
|
return $cache[$key];
|
|
}
|
|
|
|
$row = SaldoConto::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('iban', $iban)
|
|
->whereDate('data_saldo', '<=', $fromDate)
|
|
->orderByDesc('data_saldo')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$cache[$key] = $row ? (float) $row->saldo : null;
|
|
return $cache[$key];
|
|
}
|
|
|
|
protected function resolveStatoQuadratura(MovimentoBanca $record): string
|
|
{
|
|
if ($record->registrazione_id === null) {
|
|
return 'non_collegato';
|
|
}
|
|
|
|
try {
|
|
$row = DB::table('contabilita_movimenti')
|
|
->where('registrazione_id', (int) $record->registrazione_id)
|
|
->selectRaw("SUM(CASE WHEN tipo = 'dare' THEN importo ELSE 0 END) AS dare")
|
|
->selectRaw("SUM(CASE WHEN tipo = 'avere' THEN importo ELSE 0 END) AS avere")
|
|
->first();
|
|
|
|
$dare = $row && isset($row->dare) ? (float) $row->dare : 0.0;
|
|
$avere = $row && isset($row->avere) ? (float) $row->avere : 0.0;
|
|
|
|
if (abs($dare - $avere) <= 0.01) {
|
|
return 'quadrato';
|
|
}
|
|
|
|
return 'collegato_non_quadrato';
|
|
} catch (\Throwable) {
|
|
return 'collegato_non_quadrato';
|
|
}
|
|
}
|
|
|
|
protected function resolveDefaultGestioneId(int $stabileId): ?string
|
|
{
|
|
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;
|
|
}
|
|
|
|
protected function resolveSaldoProgressivo(MovimentoBanca $record): ?float
|
|
{
|
|
if (isset($record->saldo_progressivo)) {
|
|
return is_numeric($record->saldo_progressivo) ? (float) $record->saldo_progressivo : null;
|
|
}
|
|
|
|
// Fallback (DB senza window functions): calcolo best-effort sul conto (IBAN) fino a questa riga.
|
|
$q = MovimentoBanca::query()
|
|
->where('stabile_id', (int) $record->stabile_id)
|
|
->where(function (Builder $q) use ($record) {
|
|
if ($record->iban === null) {
|
|
$q->whereNull('iban');
|
|
} else {
|
|
$q->where('iban', $record->iban);
|
|
}
|
|
})
|
|
->where(function (Builder $q) use ($record) {
|
|
$q->whereDate('data', '<', $record->data->toDateString())
|
|
->orWhere(function (Builder $q) use ($record) {
|
|
$q->whereDate('data', '=', $record->data->toDateString())
|
|
->where('id', '<=', (int) $record->id);
|
|
});
|
|
});
|
|
|
|
return (float) ($q->sum('importo') ?? 0);
|
|
}
|
|
|
|
protected function dbSupportsWindowFunctions(): bool
|
|
{
|
|
if (self::$supportsWindowFunctions !== null) {
|
|
return self::$supportsWindowFunctions;
|
|
}
|
|
|
|
try {
|
|
$driver = DB::getDriverName();
|
|
if ($driver === 'pgsql' || $driver === 'sqlite') {
|
|
return self::$supportsWindowFunctions = true;
|
|
}
|
|
|
|
if ($driver !== 'mysql') {
|
|
return self::$supportsWindowFunctions = false;
|
|
}
|
|
|
|
$row = DB::selectOne('select version() as v');
|
|
$v = is_object($row) && isset($row->v) ? (string) $row->v : '';
|
|
$vLower = strtolower($v);
|
|
|
|
if (str_contains($vLower, 'mariadb')) {
|
|
// MariaDB window functions: 10.2+
|
|
if (preg_match('/(\d+)\.(\d+)\.(\d+)/', $v, $m)) {
|
|
$major = (int) $m[1];
|
|
$minor = (int) $m[2];
|
|
return self::$supportsWindowFunctions = ($major > 10) || ($major === 10 && $minor >= 2);
|
|
}
|
|
|
|
return self::$supportsWindowFunctions = false;
|
|
}
|
|
|
|
// MySQL window functions: 8.0+
|
|
if (preg_match('/(\d+)\.(\d+)\.(\d+)/', $v, $m)) {
|
|
$major = (int) $m[1];
|
|
$minor = (int) $m[2];
|
|
return self::$supportsWindowFunctions = ($major > 8) || ($major === 8 && $minor >= 0);
|
|
}
|
|
|
|
return self::$supportsWindowFunctions = false;
|
|
} catch (\Throwable) {
|
|
return self::$supportsWindowFunctions = false;
|
|
}
|
|
}
|
|
|
|
public function getActiveStabile(): ?Stabile
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
return StabileContext::getActiveStabile($user);
|
|
}
|
|
|
|
/** @return array<int, array{iban:string,label:string,saldo:float|null}> */
|
|
public function getDisponibilitaFinanziarie(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
$conti = [];
|
|
if (DB::getSchemaBuilder()->hasTable('dati_bancari')) {
|
|
$conti = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNotNull('iban')
|
|
->where('iban', '!=', '')
|
|
->orderBy('denominazione_banca')
|
|
->orderBy('iban')
|
|
->get(['id', 'denominazione_banca', 'iban', 'saldo_iniziale'])
|
|
->map(fn(DatiBancari $r) => [
|
|
'id' => (int) $r->id,
|
|
'iban' => trim((string) $r->iban),
|
|
'label' => trim((string) ($r->denominazione_banca ?: 'Conto') . ' · ' . (string) $r->iban),
|
|
'saldo_iniziale' => is_numeric($r->saldo_iniziale) ? (float) $r->saldo_iniziale : 0.0,
|
|
])
|
|
->filter(fn(array $r) => $r['iban'] !== '')
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
$sums = MovimentoBanca::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereNotNull('iban')
|
|
->where('iban', '!=', '')
|
|
->groupBy('iban')
|
|
->selectRaw('iban, COALESCE(SUM(importo), 0) AS saldo_movimenti')
|
|
->pluck('saldo_movimenti', 'iban')
|
|
->map(fn($v) => (float) $v)
|
|
->all();
|
|
|
|
$byIban = [];
|
|
foreach ($conti as $c) {
|
|
$iban = $c['iban'];
|
|
$saldo = ($c['saldo_iniziale'] ?? 0.0) + ($sums[$iban] ?? 0.0);
|
|
$byIban[$iban] = [
|
|
'id' => $c['id'] ?? null,
|
|
'iban' => $iban,
|
|
'label' => (string) $c['label'],
|
|
'saldo' => $saldo,
|
|
];
|
|
}
|
|
|
|
foreach ($sums as $iban => $saldoMov) {
|
|
if (! isset($byIban[$iban])) {
|
|
$byIban[$iban] = [
|
|
'id' => null,
|
|
'iban' => (string) $iban,
|
|
'label' => (string) $iban,
|
|
'saldo' => (float) $saldoMov,
|
|
];
|
|
}
|
|
}
|
|
|
|
uasort($byIban, fn($a, $b) => strcmp((string) $a['label'], (string) $b['label']));
|
|
return array_values($byIban);
|
|
}
|
|
|
|
public function getSelectedIbanLabel(): ?string
|
|
{
|
|
$iban = is_string($this->iban) ? trim($this->iban) : '';
|
|
if ($iban === '') {
|
|
return null;
|
|
}
|
|
|
|
foreach ($this->getDisponibilitaFinanziarie() as $c) {
|
|
if (($c['iban'] ?? '') === $iban) {
|
|
return (string) ($c['label'] ?? $iban);
|
|
}
|
|
}
|
|
|
|
return $iban;
|
|
}
|
|
|
|
protected function getDefaultIban(): string
|
|
{
|
|
foreach ($this->getDisponibilitaFinanziarie() as $c) {
|
|
$iban = is_string($c['iban'] ?? null) ? trim((string) $c['iban']) : '';
|
|
if ($iban !== '') {
|
|
return $iban;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/** @return array{label:string,data:?string,saldo:?float} */
|
|
public function getHeaderSaldoInfo(): array
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return ['label' => 'Movimenti', 'data' => null, 'saldo' => null];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return ['label' => 'Movimenti', 'data' => null, 'saldo' => null];
|
|
}
|
|
|
|
$conto = null;
|
|
if ($this->contoId) {
|
|
$conto = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('id', $this->contoId)
|
|
->first(['id', 'denominazione_banca', 'iban']);
|
|
} elseif ($this->iban) {
|
|
$conto = DatiBancari::query()
|
|
->where('stabile_id', $stabileId)
|
|
->where('iban', $this->iban)
|
|
->orderBy('id')
|
|
->first(['id', 'denominazione_banca', 'iban']);
|
|
}
|
|
|
|
$label = $conto
|
|
? trim((string) ($conto->denominazione_banca ?: 'Conto') . ($conto->iban ? (' · ' . $conto->iban) : ''))
|
|
: (string) ($this->getSelectedIbanLabel() ?: 'Movimenti');
|
|
|
|
$movQ = MovimentoBanca::query()->where('stabile_id', $stabileId);
|
|
if ($this->contoId) {
|
|
$movQ->where('conto_id', $this->contoId);
|
|
} elseif ($this->iban) {
|
|
$movQ->where('iban', $this->iban);
|
|
}
|
|
|
|
$lastMov = $movQ->orderByDesc('data')->orderByDesc('id')->first(['data']);
|
|
|
|
// Saldo attuale: usa l'ultimo saldo inserito come base (se presente), poi somma i movimenti successivi.
|
|
$iban = $this->iban;
|
|
if (! is_string($iban) || trim($iban) === '') {
|
|
return ['label' => $label, 'data' => $lastMov ? $lastMov->data->format('d/m/Y') : null, 'saldo' => null];
|
|
}
|
|
$iban = trim($iban);
|
|
|
|
$saldoBase = null;
|
|
$dataBase = null;
|
|
$periodFrom = null;
|
|
if (is_array($this->tableFilters ?? null)) {
|
|
$periodFrom = $this->tableFilters['periodo']['from'] ?? null;
|
|
$periodFrom = is_string($periodFrom) && trim($periodFrom) !== '' ? trim($periodFrom) : null;
|
|
}
|
|
if (DB::getSchemaBuilder()->hasTable('contabilita_saldi_conti')) {
|
|
$qSaldo = SaldoConto::query()->where('stabile_id', $stabileId);
|
|
if ($this->contoId) {
|
|
$qSaldo->where('conto_id', $this->contoId);
|
|
} else {
|
|
$qSaldo->where('iban', $iban);
|
|
}
|
|
if ($periodFrom) {
|
|
$qSaldo->whereDate('data_saldo', '<=', $periodFrom);
|
|
}
|
|
$row = $qSaldo->orderByDesc('data_saldo')->orderByDesc('id')->first(['data_saldo', 'saldo']);
|
|
if ($row) {
|
|
$saldoBase = (float) $row->saldo;
|
|
$dataBase = $row->data_saldo->toDateString();
|
|
}
|
|
}
|
|
|
|
$saldoIniziale = $this->resolveSaldoInizialeDaDatiBancari($stabileId, $this->contoId, $iban);
|
|
|
|
$sumQ = MovimentoBanca::query()->where('stabile_id', $stabileId)->where('iban', $iban);
|
|
if ($this->contoId) {
|
|
$sumQ->where('conto_id', $this->contoId);
|
|
}
|
|
if ($dataBase) {
|
|
$sumQ->whereDate('data', '>', $dataBase);
|
|
}
|
|
$sumAfter = (float) ($sumQ->sum('importo') ?? 0.0);
|
|
|
|
$saldoAttuale = ($saldoBase !== null ? $saldoBase : $saldoIniziale) + $sumAfter;
|
|
$dataLabel = $lastMov ? $lastMov->data->format('d/m/Y') : ($dataBase ? Carbon::parse($dataBase)->format('d/m/Y') : null);
|
|
|
|
return ['label' => $label, 'data' => $dataLabel, 'saldo' => $saldoAttuale];
|
|
}
|
|
}
|