Unify bank import flow and add MPS QIF support

This commit is contained in:
michele 2026-04-15 21:46:33 +00:00
parent 19453c050f
commit f1836763ae
4 changed files with 483 additions and 266 deletions

View File

@ -14,6 +14,7 @@
use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService; use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService;
use App\Services\Contabilita\ExcelMovimentiParser; use App\Services\Contabilita\ExcelMovimentiParser;
use App\Services\Contabilita\IntesaCsvParser; use App\Services\Contabilita\IntesaCsvParser;
use App\Services\Contabilita\MpsQifParser;
use App\Services\Contabilita\MovimentiBancaImporter; use App\Services\Contabilita\MovimentiBancaImporter;
use App\Services\Contabilita\UnicreditWriParser; use App\Services\Contabilita\UnicreditWriParser;
use App\Support\AnnoGestioneContext; use App\Support\AnnoGestioneContext;
@ -348,272 +349,45 @@ protected function getHeaderActions(): array
->icon('heroicon-o-scale') ->icon('heroicon-o-scale')
->url(fn() => SaldiContiArchivio::getUrl(panel: 'admin-filament')), ->url(fn() => SaldiContiArchivio::getUrl(panel: 'admin-filament')),
Action::make('importa_unicredit_wri') Action::make('importa_estratto_unificato')
->label('Importa estratto (Unicredit WRI)') ->label('Importa estratto')
->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') ->icon('heroicon-o-arrow-up-tray')
->modalWidth('7xl') ->modalWidth('7xl')
->form([ ->form([
Select::make('conto_id') Select::make('conto_id')
->label('Conto') ->label('Conto / cassa')
->native(false) ->native(false)
->searchable() ->searchable()
->options(function (): array { ->options(function (): array {
$opts = []; $opts = [];
foreach ($this->getDisponibilitaFinanziarie() as $c) { foreach ($this->getContiImportabili() as $c) {
if (! empty($c['id'])) { $contoId = isset($c['id']) && is_numeric($c['id']) ? (int) $c['id'] : null;
$opts[$c['id']] = (string) ($c['label'] ?? ('Conto #' . $c['id'])); if (! $contoId) {
continue;
} }
$opts[$contoId] = (string) ($c['label'] ?? ('Conto #' . $contoId));
} }
return $opts; return $opts;
}) })
->default(fn(): ?int => $this->contoId) ->default(fn(): ?int => $this->contoId)
->required(), ->required()
->helperText('Obbligatorio: l\'import viene sempre legato al conto/cassa selezionato, usando il codice legacy quando presente.'),
Select::make('formato')
->label('Formato file')
->native(false)
->options(function (): array {
return [
'auto_csv' => 'CSV banca (rilevamento automatico)',
'mps_qif' => 'MPS QIF',
'unicredit_wri' => 'Unicredit WRI',
'intesa_csv' => 'Intesa CSV',
'xlsx' => 'Excel XLSX',
];
})
->default(fn(): string => $this->resolvePreferredImportFormatForConto($this->contoId))
->required()
->helperText('Un solo flusso di import: scegli il formato corretto per il conto selezionato. Per Monte dei Paschi usa QIF.'),
Select::make('gestione_id') Select::make('gestione_id')
->label('Gestione (opzionale)') ->label('Gestione (opzionale)')
@ -658,9 +432,20 @@ protected function getHeaderActions(): array
}), }),
FileUpload::make('file') FileUpload::make('file')
->label('File XLSX') ->label('File estratto')
->directory('tmp/banca') ->directory('tmp/banca')
->disk('local') ->disk('local')
->acceptedFileTypes([
'text/plain',
'text/csv',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'.csv',
'.txt',
'.wri',
'.qif',
'.xlsx',
])
->required(), ->required(),
Toggle::make('replace') Toggle::make('replace')
@ -683,7 +468,17 @@ protected function getHeaderActions(): array
$contoId = isset($data['conto_id']) && is_numeric($data['conto_id']) ? (int) $data['conto_id'] : null; $contoId = isset($data['conto_id']) && is_numeric($data['conto_id']) ? (int) $data['conto_id'] : null;
if (! $contoId) { if (! $contoId) {
Notification::make()->title('Seleziona un conto')->danger()->send(); Notification::make()->title('Seleziona un conto o una cassa')->danger()->send();
return;
}
$conto = DatiBancari::query()
->where('stabile_id', $stabileId)
->where('id', $contoId)
->first(['id', 'iban']);
if (! $conto) {
Notification::make()->title('Conto non valido per lo stabile attivo')->danger()->send();
return; return;
} }
@ -693,21 +488,69 @@ protected function getHeaderActions(): array
return; return;
} }
$format = isset($data['formato']) && is_string($data['formato']) ? trim((string) $data['formato']) : '';
if ($format === '') {
Notification::make()->title('Seleziona il formato del file')->danger()->send();
return;
}
$gestioneId = isset($data['gestione_id']) && is_numeric($data['gestione_id']) ? (int) $data['gestione_id'] : null;
$replace = isset($data['replace']) ? (bool) $data['replace'] : false;
try { try {
$full = Storage::disk('local')->path($path); $importer = $this->makeMovimentiImporter();
$importer = new MovimentiBancaImporter(new UnicreditWriParser(), new IntesaCsvParser(), new ExcelMovimentiParser()); $filename = basename($path);
$res = $importer->importExcelXlsxForConto(
$full, $res = match ($format) {
$stabileId, 'xlsx' => $importer->importExcelXlsxForConto(
$contoId, Storage::disk('local')->path($path),
basename($path), $stabileId,
isset($data['gestione_id']) && is_numeric($data['gestione_id']) ? (int) $data['gestione_id'] : null, $contoId,
isset($data['replace']) ? (bool) $data['replace'] : false, $filename,
); $gestioneId,
$replace,
),
'unicredit_wri' => $importer->importUnicreditWriForConto(
Storage::disk('local')->get($path),
$stabileId,
$contoId,
$filename,
$gestioneId,
$replace,
),
'intesa_csv' => $importer->importIntesaCsvForConto(
Storage::disk('local')->get($path),
$stabileId,
$contoId,
$filename,
$gestioneId,
$replace,
),
'mps_qif' => $importer->importMpsQifForConto(
Storage::disk('local')->get($path),
$stabileId,
$contoId,
$filename,
$gestioneId,
$replace,
),
default => $importer->importCsvAutoForConto(
Storage::disk('local')->get($path),
$stabileId,
$contoId,
$filename,
$gestioneId,
$replace,
),
};
$this->contoId = $contoId;
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : '';
$this->iban = $iban !== '' ? $iban : null;
Notification::make() Notification::make()
->title('Import completato') ->title('Import completato')
->body('Importati: ' . $res['imported'] . ' · Duplicati: ' . $res['duplicates']) ->body('Formato: ' . $format . ' · Importati: ' . $res['imported'] . ' · Duplicati: ' . $res['duplicates'])
->success() ->success()
->send(); ->send();
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -1751,6 +1594,123 @@ public function getDisponibilitaFinanziarie(): array
return array_values($byIban); return array_values($byIban);
} }
/** @return array<int, array{id:int,iban:?string,label:string,legacy_cod_cassa:?string,numero_conto:?string,denominazione_banca:?string}> */
public function getContiImportabili(): array
{
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId || ! DB::getSchemaBuilder()->hasTable('dati_bancari')) {
return [];
}
return DatiBancari::query()
->where('stabile_id', $stabileId)
->orderBy('denominazione_banca')
->orderBy('legacy_cod_cassa')
->orderBy('iban')
->orderBy('numero_conto')
->get(['id', 'denominazione_banca', 'iban', 'numero_conto', 'legacy_cod_cassa'])
->map(function (DatiBancari $conto): array {
$iban = is_string($conto->iban) ? trim((string) $conto->iban) : null;
$iban = $iban !== '' ? $iban : null;
$numeroConto = is_string($conto->numero_conto) ? trim((string) $conto->numero_conto) : null;
$numeroConto = $numeroConto !== '' ? $numeroConto : null;
$legacyCodCassa = is_string($conto->legacy_cod_cassa) ? strtoupper(trim((string) $conto->legacy_cod_cassa)) : null;
$legacyCodCassa = $legacyCodCassa !== '' ? $legacyCodCassa : null;
$denominazione = is_string($conto->denominazione_banca) ? trim((string) $conto->denominazione_banca) : null;
$denominazione = $denominazione !== '' ? $denominazione : null;
$parts = [];
$parts[] = $denominazione ?: 'Conto';
if ($legacyCodCassa) {
$parts[] = 'cassa ' . $legacyCodCassa;
}
if ($iban) {
$parts[] = $iban;
} elseif ($numeroConto) {
$parts[] = 'n. ' . $numeroConto;
}
return [
'id' => (int) $conto->id,
'iban' => $iban,
'label' => implode(' · ', $parts),
'legacy_cod_cassa' => $legacyCodCassa,
'numero_conto' => $numeroConto,
'denominazione_banca' => $denominazione,
];
})
->values()
->all();
}
/** @return array{id:int,iban:?string,label:string,legacy_cod_cassa:?string,numero_conto:?string,denominazione_banca:?string}|null */
public function getSelectedContoImportInfo(): ?array
{
if (! $this->contoId) {
return null;
}
foreach ($this->getContiImportabili() as $conto) {
if ((int) ($conto['id'] ?? 0) === $this->contoId) {
return $conto;
}
}
return null;
}
protected function resolvePreferredImportFormatForConto(?int $contoId): string
{
if (! $contoId) {
return 'auto_csv';
}
foreach ($this->getContiImportabili() as $conto) {
if ((int) ($conto['id'] ?? 0) !== $contoId) {
continue;
}
$haystack = strtolower(implode(' ', array_filter([
$conto['denominazione_banca'] ?? null,
$conto['legacy_cod_cassa'] ?? null,
$conto['numero_conto'] ?? null,
], static fn($value): bool => is_string($value) && trim($value) !== '')));
if (str_contains($haystack, 'mps') || str_contains($haystack, 'monte dei paschi') || str_contains($haystack, 'paschi')) {
return 'mps_qif';
}
if (str_contains($haystack, 'intesa')) {
return 'intesa_csv';
}
if (str_contains($haystack, 'unicredit')) {
return 'unicredit_wri';
}
return 'auto_csv';
}
return 'auto_csv';
}
protected function makeMovimentiImporter(): MovimentiBancaImporter
{
return new MovimentiBancaImporter(
new UnicreditWriParser(),
new IntesaCsvParser(),
new ExcelMovimentiParser(),
null,
null,
new MpsQifParser(),
);
}
public function getSelectedIbanLabel(): ?string public function getSelectedIbanLabel(): ?string
{ {
$iban = is_string($this->iban) ? trim($this->iban) : ''; $iban = is_string($this->iban) ? trim($this->iban) : '';

View File

@ -13,6 +13,7 @@ class MovimentiBancaImporter
{ {
private UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser; private UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser;
private CedhouseCsvParser $cedhouseCsvParser; private CedhouseCsvParser $cedhouseCsvParser;
private MpsQifParser $mpsQifParser;
public function __construct( public function __construct(
private readonly UnicreditWriParser $unicreditParser, private readonly UnicreditWriParser $unicreditParser,
@ -20,9 +21,11 @@ public function __construct(
private readonly ExcelMovimentiParser $excelParser, private readonly ExcelMovimentiParser $excelParser,
?UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser = null, ?UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser = null,
?CedhouseCsvParser $cedhouseCsvParser = null, ?CedhouseCsvParser $cedhouseCsvParser = null,
?MpsQifParser $mpsQifParser = null,
) { ) {
$this->unicreditSemicolonCsvParser = $unicreditSemicolonCsvParser ?? new UnicreditSemicolonCsvParser(); $this->unicreditSemicolonCsvParser = $unicreditSemicolonCsvParser ?? new UnicreditSemicolonCsvParser();
$this->cedhouseCsvParser = $cedhouseCsvParser ?? new CedhouseCsvParser(); $this->cedhouseCsvParser = $cedhouseCsvParser ?? new CedhouseCsvParser();
$this->mpsQifParser = $mpsQifParser ?? new MpsQifParser();
} }
/** /**
@ -256,6 +259,78 @@ public function importCsvAutoForConto(
throw new \RuntimeException('Formato CSV non riconosciuto. Tentativi: ' . implode(' | ', $errors)); throw new \RuntimeException('Formato CSV non riconosciuto. Tentativi: ' . implode(' | ', $errors));
} }
/**
* @return array{imported:int, duplicates:int, meta?:array<string,mixed>}
*/
public function importUnicreditWriForConto(
string $content,
int $stabileId,
int $contoId,
?string $sourceFile = null,
?int $gestioneId = null,
bool $replace = false,
): array {
$parsed = $this->unicreditParser->parse($content);
return $this->importParsedRowsForConto(
$parsed,
$stabileId,
$contoId,
$sourceFile,
$gestioneId,
$replace,
'unicredit_wri',
);
}
/**
* @return array{imported:int, duplicates:int, meta?:array<string,mixed>}
*/
public function importIntesaCsvForConto(
string $content,
int $stabileId,
int $contoId,
?string $sourceFile = null,
?int $gestioneId = null,
bool $replace = false,
): array {
$parsed = $this->intesaParser->parse($content);
return $this->importParsedRowsForConto(
$parsed,
$stabileId,
$contoId,
$sourceFile,
$gestioneId,
$replace,
'intesa_csv',
);
}
/**
* @return array{imported:int, duplicates:int, meta?:array<string,mixed>}
*/
public function importMpsQifForConto(
string $content,
int $stabileId,
int $contoId,
?string $sourceFile = null,
?int $gestioneId = null,
bool $replace = false,
): array {
$parsed = $this->mpsQifParser->parse($content);
return $this->importParsedRowsForConto(
$parsed,
$stabileId,
$contoId,
$sourceFile,
$gestioneId,
$replace,
'mps_qif',
);
}
/** /**
* @param array{rows?:array<int,array{data:\Carbon\Carbon,valuta:\Carbon\Carbon|null,descrizione:string,descrizione_estesa?:string|null,importo:float,causale:string|null,raw_line:string}>, meta?:array<string,mixed>} $parsed * @param array{rows?:array<int,array{data:\Carbon\Carbon,valuta:\Carbon\Carbon|null,descrizione:string,descrizione_estesa?:string|null,importo:float,causale:string|null,raw_line:string}>, meta?:array<string,mixed>} $parsed
* @return array{imported:int, duplicates:int, meta?:array<string,mixed>} * @return array{imported:int, duplicates:int, meta?:array<string,mixed>}

View File

@ -0,0 +1,162 @@
<?php
namespace App\Services\Contabilita;
use Carbon\Carbon;
class MpsQifParser
{
/**
* @return array{rows: array<int, array{data: Carbon, valuta: Carbon|null, descrizione: string, descrizione_estesa?: string|null, importo: float, causale: string|null, raw_line: string}>, meta: array<string,mixed>}
*/
public function parse(string $content): array
{
$content = str_replace(["\r\n", "\r"], "\n", $content);
$lines = array_values(array_filter(explode("\n", $content), static fn(string $line): bool => trim($line) !== ''));
if ($lines === []) {
throw new \RuntimeException('File QIF vuoto.');
}
$type = null;
$rows = [];
$transaction = [];
foreach ($lines as $line) {
$line = preg_replace('/^\xEF\xBB\xBF/', '', $line) ?? $line;
$trimmed = trim($line);
if ($trimmed === '') {
continue;
}
if (str_starts_with($trimmed, '!Type:')) {
$type = substr($trimmed, 6) ?: null;
continue;
}
if ($trimmed === '^') {
$row = $this->buildRow($transaction, $type);
if ($row !== null) {
$rows[] = $row;
}
$transaction = [];
continue;
}
$code = substr($trimmed, 0, 1);
$value = trim(substr($trimmed, 1));
if (! isset($transaction[$code])) {
$transaction[$code] = [];
}
$transaction[$code][] = $value;
}
if ($transaction !== []) {
$row = $this->buildRow($transaction, $type);
if ($row !== null) {
$rows[] = $row;
}
}
if ($rows === []) {
throw new \RuntimeException('Nessun movimento valido trovato nel file QIF.');
}
return [
'rows' => $rows,
'meta' => [
'format' => 'mps_qif',
'qif_type' => $type,
],
];
}
/**
* @param array<string, array<int, string>> $transaction
* @return array{data: Carbon, valuta: Carbon|null, descrizione: string, descrizione_estesa?: string|null, importo: float, causale: string|null, raw_line: string}|null
*/
private function buildRow(array $transaction, ?string $type): ?array
{
$date = $this->parseDate($transaction['D'][0] ?? null);
$amount = $this->parseMoney($transaction['T'][0] ?? null);
if (! $date || $amount === null) {
return null;
}
$payee = $this->joinParts($transaction['P'] ?? []);
$memo = $this->joinParts($transaction['M'] ?? []);
$description = $payee !== '' ? $payee : ($memo !== '' ? mb_substr($memo, 0, 255) : 'Movimento QIF');
$extended = trim($payee . ($payee !== '' && $memo !== '' ? ' · ' : '') . $memo);
$rawLines = [];
foreach ($transaction as $code => $values) {
foreach ($values as $value) {
$rawLines[] = $code . $value;
}
}
return [
'data' => $date,
'valuta' => $date->copy(),
'descrizione' => $description,
'descrizione_estesa' => $extended !== '' ? $extended : null,
'importo' => $amount,
'causale' => $type ? ('QIF:' . strtoupper($type)) : 'QIF',
'raw_line' => implode("\n", $rawLines),
];
}
/**
* @param array<int, string> $parts
*/
private function joinParts(array $parts): string
{
$parts = array_map(static fn(string $value): string => trim($value), $parts);
$parts = array_values(array_filter($parts, static fn(string $value): bool => $value !== ''));
return trim(implode(' ', $parts));
}
private function parseDate(?string $value): ?Carbon
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
foreach (['d/m/Y', 'd/m/y', 'm/d/Y', 'm/d/y'] as $format) {
try {
return Carbon::createFromFormat($format, $value)->startOfDay();
} catch (\Throwable) {
// Try next format.
}
}
return null;
}
private function parseMoney(?string $value): ?float
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
$value = str_replace(["\u{00A0}", "\t", ' '], '', $value);
$value = str_ireplace(['EUR', 'EURO', '€'], '', $value);
$value = preg_replace('/[^0-9,\.\-\+]/', '', $value) ?? $value;
if (str_contains($value, ',') && str_contains($value, '.')) {
$value = str_replace('.', '', $value);
$value = str_replace(',', '.', $value);
} elseif (str_contains($value, ',')) {
$value = str_replace(',', '.', $value);
}
return is_numeric($value) ? (float) $value : null;
}
}

View File

@ -19,8 +19,28 @@
$periodoOptions = $this->periodoOptions; $periodoOptions = $this->periodoOptions;
$periodoRateOptions = $this->periodoRateOptions; $periodoRateOptions = $this->periodoRateOptions;
$periodoRate = $this->periodoRateTotali; $periodoRate = $this->periodoRateTotali;
$contoImport = $this->getSelectedContoImportInfo();
@endphp @endphp
<x-filament::section>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold">Import unico estratti</div>
<div class="mt-1 text-xs text-gray-600">
Seleziona il conto o la cassa corretta e importa il file nel formato relativo. Il collegamento al conto resta esplicito, senza inferenze dal file.
</div>
@if($contoImport)
<div class="mt-2 text-xs text-gray-700">
Conto selezionato: <span class="font-medium">{{ $contoImport['label'] }}</span>
</div>
@endif
</div>
<x-filament::button type="button" color="primary" wire:click="mountAction('importa_estratto_unificato')">
Importa estratto
</x-filament::button>
</div>
</x-filament::section>
<x-filament::section> <x-filament::section>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
<div class="text-sm font-medium"> <div class="text-sm font-medium">