Unify bank import flow and add MPS QIF support
This commit is contained in:
parent
19453c050f
commit
f1836763ae
|
|
@ -14,6 +14,7 @@
|
|||
use App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService;
|
||||
use App\Services\Contabilita\ExcelMovimentiParser;
|
||||
use App\Services\Contabilita\IntesaCsvParser;
|
||||
use App\Services\Contabilita\MpsQifParser;
|
||||
use App\Services\Contabilita\MovimentiBancaImporter;
|
||||
use App\Services\Contabilita\UnicreditWriParser;
|
||||
use App\Support\AnnoGestioneContext;
|
||||
|
|
@ -348,272 +349,45 @@ protected function getHeaderActions(): array
|
|||
->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)')
|
||||
Action::make('importa_estratto_unificato')
|
||||
->label('Importa estratto')
|
||||
->icon('heroicon-o-arrow-up-tray')
|
||||
->modalWidth('7xl')
|
||||
->form([
|
||||
Select::make('conto_id')
|
||||
->label('Conto')
|
||||
->label('Conto / cassa')
|
||||
->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']));
|
||||
foreach ($this->getContiImportabili() as $c) {
|
||||
$contoId = isset($c['id']) && is_numeric($c['id']) ? (int) $c['id'] : null;
|
||||
if (! $contoId) {
|
||||
continue;
|
||||
}
|
||||
$opts[$contoId] = (string) ($c['label'] ?? ('Conto #' . $contoId));
|
||||
}
|
||||
return $opts;
|
||||
})
|
||||
->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')
|
||||
->label('Gestione (opzionale)')
|
||||
|
|
@ -658,9 +432,20 @@ protected function getHeaderActions(): array
|
|||
}),
|
||||
|
||||
FileUpload::make('file')
|
||||
->label('File XLSX')
|
||||
->label('File estratto')
|
||||
->directory('tmp/banca')
|
||||
->disk('local')
|
||||
->acceptedFileTypes([
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.csv',
|
||||
'.txt',
|
||||
'.wri',
|
||||
'.qif',
|
||||
'.xlsx',
|
||||
])
|
||||
->required(),
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -693,21 +488,69 @@ protected function getHeaderActions(): array
|
|||
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 {
|
||||
$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,
|
||||
);
|
||||
$importer = $this->makeMovimentiImporter();
|
||||
$filename = basename($path);
|
||||
|
||||
$res = match ($format) {
|
||||
'xlsx' => $importer->importExcelXlsxForConto(
|
||||
Storage::disk('local')->path($path),
|
||||
$stabileId,
|
||||
$contoId,
|
||||
$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()
|
||||
->title('Import completato')
|
||||
->body('Importati: ' . $res['imported'] . ' · Duplicati: ' . $res['duplicates'])
|
||||
->body('Formato: ' . $format . ' · Importati: ' . $res['imported'] . ' · Duplicati: ' . $res['duplicates'])
|
||||
->success()
|
||||
->send();
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -1751,6 +1594,123 @@ public function getDisponibilitaFinanziarie(): array
|
|||
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
|
||||
{
|
||||
$iban = is_string($this->iban) ? trim($this->iban) : '';
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class MovimentiBancaImporter
|
|||
{
|
||||
private UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser;
|
||||
private CedhouseCsvParser $cedhouseCsvParser;
|
||||
private MpsQifParser $mpsQifParser;
|
||||
|
||||
public function __construct(
|
||||
private readonly UnicreditWriParser $unicreditParser,
|
||||
|
|
@ -20,9 +21,11 @@ public function __construct(
|
|||
private readonly ExcelMovimentiParser $excelParser,
|
||||
?UnicreditSemicolonCsvParser $unicreditSemicolonCsvParser = null,
|
||||
?CedhouseCsvParser $cedhouseCsvParser = null,
|
||||
?MpsQifParser $mpsQifParser = null,
|
||||
) {
|
||||
$this->unicreditSemicolonCsvParser = $unicreditSemicolonCsvParser ?? new UnicreditSemicolonCsvParser();
|
||||
$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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @return array{imported:int, duplicates:int, meta?:array<string,mixed>}
|
||||
|
|
|
|||
162
app/Services/Contabilita/MpsQifParser.php
Normal file
162
app/Services/Contabilita/MpsQifParser.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,8 +19,28 @@
|
|||
$periodoOptions = $this->periodoOptions;
|
||||
$periodoRateOptions = $this->periodoRateOptions;
|
||||
$periodoRate = $this->periodoRateTotali;
|
||||
$contoImport = $this->getSelectedContoImportInfo();
|
||||
@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>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="text-sm font-medium">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user