Allineamento sviluppo: risoluzione bug mdb-sql e o.num_fat, allineamento importazione voci/operazioni, integrazione tab pagamenti CBILL e migrazioni rate/operazioni

This commit is contained in:
michele 2026-07-07 16:15:57 +02:00
parent 248bdd032c
commit ab82c315fe
34 changed files with 2071 additions and 401 deletions

View File

@ -75,7 +75,30 @@ public function handle(): int
if ($year) {
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'legacy_year')) {
$query->where('legacy_year', (string) $year);
if (is_numeric($year) && strlen((string)$year) === 4 && (int)$year >= 1900) {
$cartelle = DB::connection('gescon_import')
->table('gestioni_annuali')
->whereIn('cod_stabile', $legacyStableCodes)
->get(['cartella', 'anno_ordinario', 'anno_riscaldamento', 'descrizione'])
->filter(function ($row) use ($year): bool {
$annoOrd = $this->extractLegacyYearInt((string) ($row->anno_ordinario ?? ''));
$annoRisc = $this->extractLegacyYearInt((string) ($row->anno_riscaldamento ?? ''));
$annoDesc = $this->extractLegacyYearInt((string) ($row->descrizione ?? ''));
return $annoOrd === (int) $year || $annoRisc === (int) $year || $annoDesc === (int) $year;
})
->pluck('cartella')
->filter()
->unique()
->toArray();
if ($cartelle !== []) {
$query->whereIn('legacy_year', $cartelle);
} else {
$query->where('legacy_year', (string) $year);
}
} else {
$query->where('legacy_year', (string) $year);
}
} else {
$query->whereYear('dt_spe', $year);
}

View File

@ -1014,6 +1014,12 @@ private function stepUnita(?int $limit): int
if (Schema::hasColumn('unita_immobiliari', 'deleted_at') && ! empty($exists->deleted_at)) {
$patch['deleted_at'] = null;
}
if (Schema::hasColumn('unita_immobiliari', 'note')) {
$uNote = trim((string) ($r->note ?? $r->note_cond ?? ''));
if ($uNote !== '' && (string) ($exists->note ?? '') !== $uNote) {
$patch['note'] = $uNote;
}
}
if (($unitClassification['is_condominiale'] ?? false) && Schema::hasColumn('unita_immobiliari', 'denominazione')) {
$condoName = $this->firstNonEmptyStr([
$unitClassification['denominazione'] ?? null,
@ -1078,6 +1084,7 @@ private function stepUnita(?int $limit): int
'stato_occupazione' => 'occupata_proprietario',
'attiva' => true,
'unita_demo' => false,
'note' => Schema::hasColumn('unita_immobiliari', 'note') ? (trim((string) ($r->note ?? $r->note_cond ?? '')) ?: null) : null,
'created_at' => now(),
'updated_at' => now(),
];
@ -1163,7 +1170,7 @@ private function stepUnita(?int $limit): int
}
// Cleanup: rimuovi unità non presenti in condomin (evita "alieni")
if (! $this->isDryRun && $this->option('stabile') && ! $this->option('update-only')) {
if (false && ! $this->isDryRun && $this->option('stabile') && ! $this->option('update-only')) {
$condRows = $this->currentSnapshotCondominQuery()
->select('scala', 'interno', 'cod_cond')
->get();
@ -2665,12 +2672,13 @@ private function stepVoci(?int $limit): int
}
$exists = $q->orderBy('id')->first();
if (! $exists) {
if (! $exists && $gestioneId === null) {
$exists = DB::table('voci_spesa')
->where('codice', $codiceDominio)
->when($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id'), function ($qq) use ($stabileId) {
$qq->where('stabile_id', $stabileId);
})
->whereNull('gestione_contabile_id')
->orderBy('id')
->first();
}
@ -3270,12 +3278,18 @@ private function stepOperazioni(?int $limit): int
$count++;
continue;
}
$exists = DB::table('operazioni_contabili')->where('legacy_id', $legacy)->first();
if ($exists && ! $this->option('update-only')) {
$gestioneId = $this->resolveGestioneId($o->legacy_year ?? ($o->anno ?? null), $o->compet ?? null, $o->gestione ?? null, $o->n_stra ?? null);
if (! $gestioneId) {
continue;
}
$gestioneId = $this->resolveGestioneId($o->legacy_year ?? ($o->anno ?? null), $o->compet ?? null, $o->gestione ?? null, $o->n_stra ?? null);
$exists = DB::table('operazioni_contabili')
->where('legacy_id', $legacy)
->where('gestione_id', $gestioneId)
->first();
if ($exists && ! $this->option('update-only')) {
continue;
}
$protoNum = (isset($o->n_spe) && is_numeric($o->n_spe) && (int) $o->n_spe > 0) ? (int) $o->n_spe : $this->nextProtocolNumber();
$prefix = null;
if ($gestioneId) {
@ -4224,25 +4238,19 @@ private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $n
->where('numero_emissione', $numeroEmissione)
->sum(DB::raw('COALESCE(importo_dovuto_euro, importo_dovuto, 0)'));
}
$payload = [
'stabile_id' => $stabileId,
'unita_immobiliare_id' => null,
'descrizione' => $descr,
'importo_totale' => $totale,
'numero_rate' => 1,
'importo_rata' => $totale,
'data_inizio' => $dataInizio,
'data_fine' => $dataFine,
'frequenza' => 'MENSILE',
'stato' => 'ATTIVO',
'importo_pagato' => 0,
'importo_residuo' => $totale,
'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile,
'created_at' => now(),
'updated_at' => now(),
'stabile_id' => $stabileId,
'descrizione' => $descr,
'importo_totale' => $totale,
'numero_rate' => 1,
'data_prima_rata' => $dataInizio,
'frequenza' => 'MENSILE',
'stato' => 'ATTIVO',
'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile,
'codice_piano' => \App\Models\PianoRateizzazione::generaCodicePiano(),
'created_at' => now(),
'updated_at' => now(),
];
if (! $this->isDryRun) {
$id = DB::table('piano_rateizzazione')->insertGetId($payload);
return is_numeric($id) ? (int) $id : null;
@ -5272,20 +5280,18 @@ private function importLegacySubsystemForAssemblea(int $assembleaId, array $row,
if (!$unita) continue;
// Trova il soggetto principale legato all'unità
$soggettoId = DB::table('diritti_reali_unita')
$soggettoId = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id)
->orderByRaw("CASE WHEN tipo_diritto = 'proprieta' THEN 1 ELSE 2 END")
->value('soggetto_id');
if (!$soggettoId) {
$soggettoId = DB::table('soggetti_unita_immobiliari')
->where('unita_immobiliare_id', $unita->id)
->value('soggetto_id');
}
if (!$soggettoId) continue;
$tipoPartecipazione = 'personale';
$pda = strtolower(trim((string)($pr['p_d_a'] ?? '')));
if ($pda === 'a' || $pda === 'assente') {
continue;
}
if ($pda === 'd' || strpos($pda, 'delega') !== false) {
$tipoPartecipazione = 'delega';
}
@ -5351,16 +5357,11 @@ private function importLegacySubsystemForAssemblea(int $assembleaId, array $row,
if (!$unita) continue;
$soggettoId = DB::table('diritti_reali_unita')
$soggettoId = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id)
->orderByRaw("CASE WHEN tipo_diritto = 'proprieta' THEN 1 ELSE 2 END")
->value('soggetto_id');
if (!$soggettoId) {
$soggettoId = DB::table('soggetti_unita_immobiliari')
->where('unita_immobiliare_id', $unita->id)
->value('soggetto_id');
}
if (!$soggettoId) continue;
// Determina scelta voto
@ -7253,7 +7254,7 @@ private function extractLegacySnapshotPartyMeta(object $row, string $role): arra
'citta' => trim((string) ($isTenant ? ($row->inquil_citta ?? '') : ($row->citta ?? ''))) ?: null,
'provincia' => trim((string) ($isTenant ? ($row->inquil_pr ?? '') : ($row->pr ?? ''))) ?: null,
'titolo' => trim((string) ($isTenant ? ($row->titolo_inq ?? '') : ($row->titolo_cond ?? ''))) ?: null,
'note' => trim((string) ($isTenant ? ($row->inquil_note ?? '') : ($row->note_cond ?? ''))) ?: null,
'note' => trim((string) ($isTenant ? ($row->note_inquilino ?? $row->inquil_note ?? '') : ($row->note ?? $row->note_cond ?? ''))) ?: null,
'e_lostesso_di' => $isTenant ? null : (trim((string) ($row->e_lostesso_di ?? '')) ?: null),
'codice_fiscale' => $isTenant
? (trim((string) ($row->inquilin_cod_fisc ?? $row->inquil_cod_fisc ?? '')) ?: null)
@ -7319,68 +7320,118 @@ private function extractLegacyPrefixedPayload(object $row, array $prefixes): arr
return $payload;
}
private function resolveLinkedLegacyCondIds(string $codStabile, string $startCondId): array
{
$linked = [$startCondId];
$toProcess = [$startCondId];
$visited = [];
while (! empty($toProcess)) {
$current = array_shift($toProcess);
if (isset($visited[$current])) {
continue;
}
$visited[$current] = true;
$rows = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where(function($q) use ($current) {
$q->where('cod_cond', $current);
$colName = Schema::connection('gescon_import')->hasColumn('condomin', 'id_cond') ? 'id_cond' : null;
if ($colName) {
$q->orWhere($colName, $current);
}
if (Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_id_cond')) {
$q->orWhere('legacy_id_cond', $current);
}
})
->get();
foreach ($rows as $row) {
$cc = trim((string) ($row->cod_cond ?? ''));
if ($cc !== '' && ! in_array($cc, $linked, true)) {
$linked[] = $cc;
$toProcess[] = $cc;
}
foreach (['subentro_prima_cera', 'subentro_adesso_ce'] as $field) {
if (isset($row->$field)) {
$val = trim((string) $row->$field);
if ($val !== '' && ! in_array($val, $linked, true)) {
$linked[] = $val;
$toProcess[] = $val;
}
}
}
}
}
return $linked;
}
private function loadLegacyCondominHistoryRows(object $row, ?string $legacyCondId): array
{
$codStabile = trim((string) ($row->cod_stabile ?? ''));
$codCond = trim((string) ($row->cod_cond ?? ''));
$stableCondId = $this->extractStableLegacyCondId($row, $legacyCondId);
if ($codStabile === '' || ($stableCondId === null && $codCond === '')) {
if ($codStabile === '') {
return [$row];
}
$startCondId = $stableCondId !== null && $stableCondId !== '' ? $stableCondId : ($codCond !== '' ? $codCond : null);
$linkedCondIds = [];
if ($startCondId !== null) {
$linkedCondIds = $this->resolveLinkedLegacyCondIds($codStabile, $startCondId);
}
$query = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->orderByDesc('legacy_year')
->where('cod_stabile', $codStabile);
$query->where(function($q) use ($linkedCondIds, $row, $codCond): void {
if (! empty($linkedCondIds)) {
$q->whereIn('cod_cond', $linkedCondIds);
$colName = Schema::connection('gescon_import')->hasColumn('condomin', 'id_cond') ? 'id_cond' : null;
if ($colName) {
$q->orWhereIn($colName, $linkedCondIds);
}
if (Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_id_cond')) {
$q->orWhereIn('legacy_id_cond', $linkedCondIds);
}
} elseif ($codCond !== '') {
$q->where('cod_cond', $codCond);
}
// Corrispondenza fisica (scala, interno, subalterno)
$scala = trim((string) ($row->scala ?? ''));
$interno = trim((string) ($row->interno ?? $row->int ?? ''));
$sub = trim((string) ($row->subalterno ?? $row->sub ?? ''));
if ($scala !== '' && ($interno !== '' || $sub !== '')) {
$q->orWhere(function($subQ) use ($scala, $interno, $sub): void {
$subQ->where('scala', $scala);
if ($interno !== '') {
$subQ->where(function($sub2) use ($interno): void {
$sub2->where('interno', $interno);
if (Schema::connection('gescon_import')->hasColumn('condomin', 'int')) {
$sub2->orWhere('int', $interno);
}
});
}
if ($sub !== '' && Schema::connection('gescon_import')->hasColumn('condomin', 'subalterno')) {
$subQ->where('subalterno', $sub);
}
});
}
});
$query->orderByDesc('legacy_year')
->orderByDesc('id');
if ($stableCondId !== null && $stableCondId !== '') {
$colName = Schema::connection('gescon_import')->hasColumn('condomin', 'cod_cond') ? 'cod_cond' : 'id_cond';
if (Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_id_cond')) {
$query->where(function ($subQuery) use ($stableCondId, $colName): void {
$subQuery->where($colName, $stableCondId)
->orWhere('legacy_id_cond', $stableCondId);
});
} else {
$query->where($colName, $stableCondId);
}
} elseif ($codCond !== '') {
$query->where('cod_cond', $codCond);
}
$historyRows = $query->get()->all();
if ($historyRows === [] && $codCond !== '') {
$historyRows = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where('cod_cond', $codCond)
->orderByDesc('legacy_year')
->orderByDesc('id')
->get()
->all();
}
if ($historyRows === []) {
$partyName = $this->extractLegacyPartyName($row);
if ($partyName !== null) {
$historyRows = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where(function ($subQuery) use ($partyName): void {
$subQuery->where('nom_cond', $partyName);
if (Schema::connection('gescon_import')->hasColumn('condomin', 'proprietario_denominazione')) {
$subQuery->orWhere('proprietario_denominazione', $partyName);
}
})
->orderByDesc('legacy_year')
->orderByDesc('id')
->get()
->all();
}
}
if ($historyRows === []) {
return [$row];
}
@ -7486,22 +7537,31 @@ private function loadLegacyComproprietariHistoryRows(object $row, ?string $legac
}
$codStabile = trim((string) ($row->cod_stabile ?? ''));
$idCondRaw = trim((string) ($this->extractStableLegacyCondId($row, $legacyCondId) ?? $row->cod_cond ?? ''));
$codCond = trim((string) ($row->cod_cond ?? ''));
$stableCondId = $this->extractStableLegacyCondId($row, $legacyCondId);
if ($codStabile === '' || $idCondRaw === '') {
if ($codStabile === '') {
return [];
}
$startCondId = $stableCondId !== null && $stableCondId !== '' ? $stableCondId : ($codCond !== '' ? $codCond : null);
$linkedCondIds = [];
if ($startCondId !== null) {
$linkedCondIds = $this->resolveLinkedLegacyCondIds($codStabile, $startCondId);
}
$query = DB::connection('gescon_import')
->table('comproprietari')
->where('cod_stabile', $codStabile)
->orderByDesc('legacy_year')
->orderByDesc('id');
if (is_numeric($idCondRaw)) {
$query->where('id_cond', (int) $idCondRaw);
if (! empty($linkedCondIds)) {
$query->whereIn('id_cond', $linkedCondIds);
} elseif ($startCondId !== null) {
$query->where('id_cond', $startCondId);
} else {
$query->where('id_cond', $idCondRaw);
return [];
}
$historyRows = $query->get()->all();

View File

@ -46,7 +46,7 @@ public function handle()
$this->displayImportPlan($yearsToImport);
if (!$this->option('dry-run') && !$this->confirm('Proceed with multi-year import?')) {
if (!$this->option('dry-run') && !$this->option('no-interaction') && !$this->confirm('Proceed with multi-year import?')) {
$this->info('Import cancelled by user.');
return Command::SUCCESS;
}
@ -102,7 +102,7 @@ private function scanAvailableYears(string $gesconDir): array
}
}
ksort($years);
krsort($years);
return $years;
}
@ -228,9 +228,9 @@ private function countTableRecords(string $mdbPath, string $tableName): int
}
$output = shell_exec(sprintf(
'mdb-sql %s <<< "SELECT COUNT(*) FROM %s" 2>/dev/null',
escapeshellarg($mdbPath),
escapeshellarg($tableName)
'echo "SELECT COUNT(*) FROM %s" | mdb-sql %s 2>/dev/null',
$tableName,
escapeshellarg($mdbPath)
));
if ($output && preg_match('/(\d+)/', $output, $matches)) {
@ -247,7 +247,14 @@ private function executeImport(string $tenantId, string $gesconDir, array $years
DB::beginTransaction();
try {
$service = new MultiYearGesconImportService($tenantId);
$stabileCode = basename(rtrim($gesconDir, '/'));
$stabileRow = DB::table('stabili')
->where('codice_stabile', $stabileCode)
->orWhere('codice_stabile', ltrim($stabileCode, '0'))
->first();
$stabileId = $stabileRow?->id;
$service = new MultiYearGesconImportService($tenantId, $stabileId, $gesconDir);
// Delete existing data if force
if ($this->option('force')) {
@ -289,9 +296,66 @@ private function cleanExistingData(string $tenantId, array $years): void
{
$this->warn("🗑️ Cleaning existing data for years: " . implode(', ', $years));
$stabileCode = basename(rtrim($this->argument('gescon-dir'), '/'));
$stabileRow = DB::table('stabili')
->where('codice_stabile', $stabileCode)
->orWhere('codice_stabile', ltrim($stabileCode, '0'))
->first();
$stabileId = $stabileRow?->id;
// Resolve calendar years
$calendarYears = [];
$generaleMdb = rtrim($this->argument('gescon-dir'), '/') . '/generale_stabile.mdb';
if (is_file($generaleMdb)) {
try {
$output = shell_exec(sprintf(
'echo "SELECT id_anno, anno_o FROM anni" | mdb-sql %s 2>/dev/null',
escapeshellarg($generaleMdb)
));
if ($output) {
$lines = explode("\n", $output);
foreach ($lines as $line) {
if (str_contains($line, '|')) {
$parts = explode('|', $line);
$id = (int) trim($parts[1] ?? '');
$yr = (int) trim($parts[2] ?? '');
if ($id > 0 && $yr >= 2000 && $yr <= 2100 && in_array($id, array_map('intval', $years), true)) {
$calendarYears[] = $yr;
}
}
}
}
} catch (\Throwable $e) {
}
}
if (empty($calendarYears)) {
$calendarYears = array_map('intval', $years);
}
$gestioneIds = GestioneContabile::forTenant($tenantId)
->where('stabile_id', $stabileId)
->whereIn('anno_gestione', $calendarYears)
->pluck('id')
->toArray();
if (!empty($gestioneIds)) {
// Nullify foreign keys to prevent constraint violations
DB::table('lavori_straordinari')->whereIn('gestione_id', $gestioneIds)->update(['gestione_id' => null]);
DB::table('contabilita_registrazioni')->whereIn('gestione_id', $gestioneIds)->update(['gestione_id' => null]);
DB::table('contabilita_fatture_fornitori')->whereIn('gestione_id', $gestioneIds)->update(['gestione_id' => null]);
$deletedIncassi = DB::table('incassi')->whereIn('gestione_id', $gestioneIds)->delete();
$deletedOps = DB::table('operazioni_contabili')->whereIn('gestione_id', $gestioneIds)->delete();
$deletedRDA = DB::table('registro_ritenute_acconto')->whereIn('gestione_id', $gestioneIds)->delete();
$deletedIncEc = DB::table('incassi_estratto_conto')->whereIn('gestione_id', $gestioneIds)->delete();
$this->line(" Deleted {$deletedIncassi} incassi, {$deletedOps} operazioni, {$deletedRDA} ritenute, {$deletedIncEc} incassi EC");
}
// Delete gestioni and cascade
$deletedGestioni = GestioneContabile::forTenant($tenantId)
->whereIn('anno_gestione', $years)
->where('stabile_id', $stabileId)
->whereIn('anno_gestione', $calendarYears)
->delete();
$this->line(" Deleted {$deletedGestioni} gestioni");

View File

@ -114,7 +114,10 @@ private function ensureCondominColumns(): void
return;
}
$cols = ['inquil_nome', 'inquil_cod_fisc', 'e_mail_inquilino', 'inquil_tel1', 'inquil_indir'];
$cols = [
'inquil_nome', 'inquil_cod_fisc', 'e_mail_inquilino', 'inquil_tel1', 'inquil_indir',
'subentro_prima_cera', 'subentro_adesso_ce', 'subentrato_dal', 'attivo_fino_al'
];
$connection = Schema::connection('gescon_import');
Schema::connection('gescon_import')->table('condomin', function (Blueprint $table) use ($cols, $connection): void {
@ -159,7 +162,9 @@ private function persistStagingRow(string $stagingTable, array $filtered): void
if ($stagingTable === 'operazioni' && array_key_exists('id_operaz', $filtered) && $filtered['id_operaz'] !== null) {
$query->updateOrInsert([
'id_operaz' => $filtered['id_operaz'],
'id_operaz' => $filtered['id_operaz'],
'cod_stabile' => $filtered['cod_stabile'] ?? null,
'legacy_year' => $filtered['legacy_year'] ?? null,
], $filtered);
return;

View File

@ -15,6 +15,10 @@
use Filament\Support\Contracts\TranslatableContentDriver;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Columns\DatePickerColumn;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
@ -281,7 +285,15 @@ public function table(Table $table): Table
return;
}
$tenantId = GestioneContabile::query()
->where('stabile_id', $stabileId)
->value('tenant_id');
if (! is_string($tenantId) || trim($tenantId) === '') {
$tenantId = (string) ($user->tenant_id ?? 'default');
}
$gestione = new GestioneContabile();
$gestione->tenant_id = $tenantId;
$gestione->stabile_id = $stabileId;
$gestione->anno_gestione = (int) ($data['anno_gestione'] ?? now()->year);
$gestione->tipo_gestione = (string) ($data['tipo_gestione'] ?? 'ordinaria');
@ -356,43 +368,28 @@ public function table(Table $table): Table
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('denominazione')
TextInputColumn::make('denominazione')
->label('Denominazione')
->searchable()
->wrap(),
->searchable(),
TextColumn::make('origine_dati')
->label('Origine')
->getStateUsing(function (GestioneContabile $record): string {
if (filled($record->codice_archivio_legacy)) {
return 'Legacy collegata';
}
DatePickerColumn::make('data_inizio')
->label('Data inizio')
->native(false),
return 'Locale';
})
->badge()
->color(fn(string $state): string => $state === 'Legacy collegata' ? 'success' : 'gray'),
DatePickerColumn::make('data_fine')
->label('Data fine')
->native(false),
TextColumn::make('periodo_label')
->label('Periodo')
->getStateUsing(fn(GestioneContabile $record): string => $record->periodo_label)
->wrap(),
TextColumn::make('stato')
SelectColumn::make('stato')
->label('Stato')
->formatStateUsing(fn(string $state): string => ucfirst($state))
->badge()
->color(fn(string $state): string => match ($state) {
'aperta' => 'success',
'consolidata' => 'warning',
'chiusa' => 'gray',
default => 'gray',
})
->toggleable(isToggledHiddenByDefault: true),
->options([
'aperta' => 'Aperta',
'consolidata' => 'Consolidata',
'chiusa' => 'Chiusa',
]),
IconColumn::make('gestione_attiva')
->label('Attiva')
->boolean(),
ToggleColumn::make('gestione_attiva')
->label('Attiva'),
TextColumn::make('mesi_rate')
->label('Mesi rate')

View File

@ -1766,7 +1766,10 @@ private function resolveRiscaldamentoCandidateInvoices(?string $tipoGestione = n
->where('f.stabile_id', $stabileId)
->whereNotNull('f.fattura_elettronica_id')
->when($fornitoreIds !== [], fn($builder) => $builder->whereIn('f.fornitore_id', $fornitoreIds))
->where('g.tipo_gestione', $tipoGestione)
->where(function ($b) use ($tipoGestione) {
$b->where('g.tipo_gestione', $tipoGestione)
->orWhereNull('f.gestione_id');
})
->when($dal && $al, fn($builder) => $builder->whereBetween('f.data_documento', [$dal, $al]))
->when(!($dal && $al), function($builder) use ($year) {
$builder->when(Schema::hasColumn('gestioni_contabili', 'anno_gestione'), fn($b) => $b->where('g.anno_gestione', $year))
@ -2839,7 +2842,7 @@ public function getRiscaldamentoFeEstratteRowsProperty(): array
$pagamento = is_array($payload['pagamento'] ?? null) ? $payload['pagamento'] : [];
$riepilogoLetture = is_array($payload['riepilogo_letture'] ?? null) ? $payload['riepilogo_letture'] : [];
$servizio = $this->resolveRiscaldamentoServizioForIdentifiers($servizi, $codici, $contatore) ?? $defaultServizio;
$servizio = $this->resolveRiscaldamentoServizioForIdentifiers($servizi, $codici, $contatore, $fattura);
[$periodoDal, $periodoAl] = $this->resolveRiscaldamentoPeriodsFromPayload($consumi, is_string($fattura->consumo_riferimento ?? null) ? (string) $fattura->consumo_riferimento : null);
$windowFrom = $this->extractRiscaldamentoWindowDate($payload, ['dal', 'from', 'inizio', 'start']);
@ -2916,7 +2919,9 @@ public function getRiscaldamentoFeEstratteRowsProperty(): array
'numero_fattura' => trim((string) ($fattura->numero_fattura ?? $fattura->sdi_file ?? ('FE #' . (int) $fattura->id))),
'data_fattura' => optional($fattura->data_fattura)?->format('d/m/Y'),
'data_fattura_iso' => optional($fattura->data_fattura)?->format('Y-m-d'),
'servizio' => trim((string) ($servizio?->nome ?? 'Riscaldamento stabile')),
'servizio' => $servizio
? (string) $servizio->nome
: ($fattura->fornitore?->ragione_sociale ? 'Non abbinato: ' . $fattura->fornitore->ragione_sociale : 'Non abbinata / Fornitore sconosciuto'),
'servizio_id' => (int) ($servizio?->id ?? 0),
'matricola' => $matricola,
'codice_utenza' => $codiceUtenza,
@ -3021,7 +3026,7 @@ private function formatRiscaldamentoPeriodLabel(?string $from, ?string $to): str
}
/** @param \Illuminate\Support\Collection<int, StabileServizio> $servizi */
private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $codici, array $contatore): ?StabileServizio
private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $codici, array $contatore, ?FatturaElettronica $fattura = null): ?StabileServizio
{
if (! $servizi instanceof \Illuminate\Support\Collection || $servizi->isEmpty()) {
return null;
@ -3031,20 +3036,24 @@ private function resolveRiscaldamentoServizioForIdentifiers($servizi, array $cod
$targetUtenza = $this->normalizeRiscaldamentoIdentifier($codici['utenza'] ?? null);
$targetCliente = $this->normalizeRiscaldamentoIdentifier($codici['cliente'] ?? null);
$targetContratto = $this->normalizeRiscaldamentoIdentifier($codici['contratto'] ?? null);
$fornitoreId = $fattura?->fornitore_id;
return $servizi
->map(function (StabileServizio $servizio) use ($targetMatricola, $targetUtenza, $targetCliente, $targetContratto): array {
->map(function (StabileServizio $servizio) use ($targetMatricola, $targetUtenza, $targetCliente, $targetContratto, $fornitoreId): array {
$score = 0;
if ($targetMatricola !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->contatore_matricola) === $targetMatricola) {
$score += 5;
$score += 10;
}
if ($targetUtenza !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_utenza) === $targetUtenza) {
$score += 4;
$score += 8;
}
if ($targetContratto !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_contratto) === $targetContratto) {
$score += 3;
$score += 6;
}
if ($targetCliente !== '' && $this->normalizeRiscaldamentoIdentifier($servizio->codice_cliente) === $targetCliente) {
$score += 4;
}
if ($fornitoreId && (int) $servizio->fornitore_id === (int) $fornitoreId) {
$score += 2;
}
@ -4043,8 +4052,7 @@ protected function ensureFornitoreStabileDefaultVoceSpesa(int $stabileId): void
$voceSpesa = \App\Models\VoceSpesa::query()
->where('stabile_id', $stabileId)
->where(function ($q) use ($mostCommonVoceCode) {
$q->where('conto_contabile', $mostCommonVoceCode)
->orWhere('codice', $mostCommonVoceCode)
$q->where('codice', $mostCommonVoceCode)
->orWhere('legacy_codice', $mostCommonVoceCode);
})
->first();

View File

@ -731,7 +731,7 @@ public function importElectronicReadings(): void
private function normalizeAcquaTab(string $tab): string
{
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi'];
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi', 'pagamenti_cbill'];
return in_array($tab, $allowed, true) ? $tab : 'dashboard';
}
@ -3826,4 +3826,74 @@ public function getAcquaAltreVociLegacyProperty(): array
'totale' => round((float) ($r->totale ?? 0), 2),
])->all();
}
/**
* Recupera pagamenti bancari associati ai codici CBILL delle fatture.
*/
public function getAcquaCbillPagamentiProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [];
}
// Recupera tutti i codici CBILL delle fatture elettroniche di questo stabile
$cbillCodes = DB::table('fatture_elettroniche')
->where('stabile_id', $stabileId)
->whereNotNull('pagamento_cbill')
->where('pagamento_cbill', '<>', '')
->pluck('pagamento_cbill')
->unique()
->filter()
->all();
if (empty($cbillCodes)) {
return [];
}
// Interroga i movimenti bancari contenenti uno qualsiasi di tali codici
$movements = DB::table('contabilita_movimenti_banca')
->where('stabile_id', $stabileId)
->where(function ($query) use ($cbillCodes) {
foreach ($cbillCodes as $code) {
$query->orWhere('descrizione', 'like', "%{$code}%")
->orWhere('descrizione_estesa', 'like', "%{$code}%");
}
})
->orderByDesc('data')
->get();
$rows = [];
foreach ($movements as $m) {
$matchedCbill = null;
foreach ($cbillCodes as $code) {
if (str_contains($m->descrizione, $code) || str_contains($m->descrizione_estesa, $code)) {
$matchedCbill = $code;
break;
}
}
$invoice = null;
if ($matchedCbill) {
$invoice = DB::table('fatture_elettroniche')
->where('stabile_id', $stabileId)
->where('pagamento_cbill', $matchedCbill)
->first(['id', 'numero_fattura', 'data_fattura', 'totale', 'fornitore_denominazione']);
}
$rows[] = [
'data' => $m->data,
'descrizione' => $m->descrizione,
'importo' => $m->importo,
'cbill' => $matchedCbill,
'fattura_id' => $invoice?->id ?? null,
'fattura_numero' => $invoice?->numero_fattura ?? '—',
'fattura_data' => $invoice?->data_fattura ?? '—',
'fattura_totale' => $invoice?->totale ?? 0.0,
'fornitore' => $invoice?->fornitore_denominazione ?? '—',
];
}
return $rows;
}
}

View File

@ -182,9 +182,7 @@ protected function getTableQuery(): Builder
return TabellaMillesimale::query()->whereRaw('1 = 0');
}
$orderColumn = Schema::hasColumn('tabelle_millesimali', 'nord')
? 'nord'
: (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : (Schema::hasColumn('tabelle_millesimali', 'ordinamento') ? 'ordinamento' : 'id'));
$orderColumn = DB::raw('COALESCE(nord, ordinamento, ordine_visualizzazione, 999999)');
return TabellaMillesimale::query()
->where('stabile_id', $activeStabileId)

View File

@ -223,7 +223,7 @@ public function table(Table $table): Table
$inquilini = [];
$altri = [];
$ruoli = $record->rubricaRuoliAttivi ?? collect();
$ruoli = ($record->rubricaRuoliAttivi ?? collect())->unique(fn($r) => $r->rubrica_id . '|' . $r->ruolo_standard);
foreach ($ruoli as $ruolo) {
$nome = $ruolo->contatto?->nome_completo ?? '—';
$cf = $ruolo->contatto?->codice_fiscale ? " ({$ruolo->contatto->codice_fiscale})" : '';
@ -265,18 +265,32 @@ public function table(Table $table): Table
TextColumn::make('palazzina')
->label('Pal.')
->sortable(query: function (Builder $query, string $direction): Builder {
return $query->orderBy('palazzina', $direction)
->orderBy('scala', $direction)
->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END")
->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END")
->orderBy('interno', $direction);
})
->toggleable(),
TextColumn::make('scala')
->label('Scala')
->sortable()
->toggleable(),
TextColumn::make('piano')
->label('Piano')
->sortable()
->toggleable(),
TextColumn::make('interno')
->label('Int.')
->sortable(query: function (Builder $query, string $direction): Builder {
return $query->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END")
->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END")
->orderBy('interno', $direction);
})
->formatStateUsing(function ($state, UnitaImmobiliare $record): string {
$interno = trim((string) ($state ?? ''));
if ($interno === '') {
@ -315,6 +329,7 @@ public function table(Table $table): Table
->label('Apri')
->icon('heroicon-o-arrow-right')
->url(fn(UnitaImmobiliare $record) => $unitaPageUrl . '?unita_id=' . $record->id),
]);
])
->defaultSort('palazzina', 'asc');
}
}

View File

@ -85,6 +85,8 @@ class CasseBancheMovimenti extends Page implements HasTable
public int $riconciliazioneOffset = 0;
public ?string $selectedCausaleForConfig = null;
public string $periodoTipo = 'mese';
public int $periodoAnno;
public int $periodoValore;
@ -1134,6 +1136,72 @@ protected function getHeaderActions(): array
->success()
->send();
}),
Action::make('configura_regola_causale')
->label('Configura regola causale')
->modalWidth('2xl')
->form([
TextInput::make('causale')
->label('Causale bancaria')
->required()
->disabled(),
Select::make('conto_dare_id')
->label('Conto Dare (addebito se importo > 0, es. Entrate)')
->options(fn() => PianoConti::query()->orderBy('codice')->get()->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione])->all())
->searchable()
->required(),
Select::make('conto_avere_id')
->label('Conto Avere (accredito se importo > 0, es. Uscite)')
->options(fn() => PianoConti::query()->orderBy('codice')->get()->mapWithKeys(fn(PianoConti $c) => [(string) $c->id => $c->codice . ' - ' . $c->descrizione])->all())
->searchable()
->required(),
TextInput::make('label')
->label('Nome regola (opzionale)')
->nullable(),
Textarea::make('note')
->label('Note')
->rows(2)
->nullable(),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()->title('Seleziona uno stabile')->danger()->send();
return;
}
$causale = trim((string) ($this->selectedCausaleForConfig ?? ''));
if ($causale === '') {
Notification::make()->title('Causale non valida')->danger()->send();
return;
}
try {
\App\Modules\Contabilita\Models\RegolaPrimaNota::updateOrCreate([
'stabile_id' => $stabileId,
'origine' => 'banca',
'chiave' => $causale,
], [
'label' => isset($data['label']) && trim($data['label']) !== '' ? trim($data['label']) : 'Regola causale ' . $causale,
'note' => $data['note'] ?? null,
'conto_dare_id' => $data['conto_dare_id'] ? (int) $data['conto_dare_id'] : null,
'conto_avere_id' => $data['conto_avere_id'] ? (int) $data['conto_avere_id'] : null,
'attiva' => true,
]);
Notification::make()->title('Regola configurata con successo')->success()->send();
} catch (\Throwable $e) {
Notification::make()->title('Errore')->body($e->getMessage())->danger()->send();
}
}),
];
}
@ -1698,6 +1766,317 @@ public function table(Table $table): Table
Notification::make()->title('Errore')->body($e->getMessage())->danger()->send();
}
}),
Action::make('incassa_quote_condominiali')
->label('Incassa quote')
->icon('heroicon-o-currency-euro')
->visible(function (MovimentoBanca $record): bool {
if (! Schema::hasColumn('contabilita_movimenti_banca', 'registrazione_id')) {
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 $this->getVisibleGestioneOptions($stabileId);
})
->searchable()
->visible(fn(MovimentoBanca $record) => empty($record->gestione_id))
->required(fn(MovimentoBanca $record) => empty($record->gestione_id)),
Select::make('soggetto_id')
->label('Soggetto (Condomino/Inquilino)')
->options(function (): array {
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return [];
}
return \App\Models\RataEmessaNg::query()
->join('unita_immobiliari', 'rate_emesse.unita_immobiliare_id', '=', 'unita_immobiliari.id')
->join('soggetti', 'rate_emesse.soggetto_responsabile_id', '=', 'soggetti.id')
->where('unita_immobiliari.stabile_id', $stabileId)
->where(function($q) {
$q->whereNull('rate_emesse.stato_rata')
->orWhere('rate_emesse.stato_rata', '!=', 'pagata');
})
->groupBy('soggetti.id')
->select(['soggetti.id', 'soggetti.ragione_sociale', 'soggetti.nome', 'soggetti.cognome'])
->get()
->mapWithKeys(function (object $s): array {
$label = is_string($s->ragione_sociale ?? null) && trim((string) $s->ragione_sociale) !== ''
? trim((string) $s->ragione_sociale)
: trim(((string) ($s->cognome ?? '')) . ' ' . ((string) ($s->nome ?? '')));
$label = $label !== '' ? $label : ('Soggetto #' . (string) $s->id);
return [(string) $s->id => $label];
})
->all();
})
->searchable()
->reactive()
->required()
->afterStateUpdated(function (callable $set, $state, callable $get, MovimentoBanca $record) {
if (empty($state)) {
$set('rate', []);
return;
}
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return;
}
$rate = \App\Models\RataEmessaNg::query()
->join('unita_immobiliari', 'rate_emesse.unita_immobiliare_id', '=', 'unita_immobiliari.id')
->where('unita_immobiliari.stabile_id', $stabileId)
->where('rate_emesse.soggetto_responsabile_id', (int) $state)
->where(function($q) {
$q->whereNull('rate_emesse.stato_rata')
->orWhere('rate_emesse.stato_rata', '!=', 'pagata');
})
->orderBy('rate_emesse.data_scadenza')
->orderBy('rate_emesse.id')
->select([
'rate_emesse.id',
'rate_emesse.descrizione',
'rate_emesse.data_scadenza',
'rate_emesse.importo_addebitato_soggetto',
'rate_emesse.importo_pagato',
'unita_immobiliari.codice_unita',
])
->get();
$importoBanca = round((float) ($record->importo ?? 0), 2);
$residuoBanca = $importoBanca;
$rateRows = [];
foreach ($rate as $r) {
$dovuto = round((float) $r->importo_addebitato_soggetto, 2);
$pagato = round((float) $r->importo_pagato, 2);
$aperto = round($dovuto - $pagato, 2);
if ($aperto <= 0) {
continue;
}
$daPagare = 0.0;
if ($residuoBanca > 0) {
$daPagare = min($residuoBanca, $aperto);
$residuoBanca = round($residuoBanca - $daPagare, 2);
}
$rateRows[] = [
'rata_id' => $r->id,
'codice_unita' => $r->codice_unita,
'descrizione' => $r->descrizione,
'scadenza' => $r->data_scadenza ? $r->data_scadenza->format('d/m/Y') : '',
'residuo_label' => 'Importo residuo: € ' . number_format($aperto, 2, ',', '.'),
'importo_pagato' => $daPagare,
];
}
$set('rate', $rateRows);
}),
\Filament\Forms\Components\Repeater::make('rate')
->label('Rate da incassare')
->schema([
\Filament\Forms\Components\Hidden::make('rata_id'),
\Filament\Forms\Components\Hidden::make('codice_unita'),
\Filament\Forms\Components\Hidden::make('descrizione'),
\Filament\Forms\Components\Hidden::make('scadenza'),
\Filament\Forms\Components\Hidden::make('residuo_label'),
\Filament\Forms\Components\Placeholder::make('codice_unita_lbl')
->label('Unità')
->content(fn ($get) => $get('codice_unita')),
\Filament\Forms\Components\Placeholder::make('descrizione_lbl')
->label('Descrizione')
->content(fn ($get) => $get('descrizione')),
\Filament\Forms\Components\Placeholder::make('scadenza_lbl')
->label('Scadenza')
->content(fn ($get) => $get('scadenza')),
\Filament\Forms\Components\Placeholder::make('residuo_lbl')
->label('Aperto')
->content(fn ($get) => $get('residuo_label')),
\Filament\Forms\Components\TextInput::make('importo_pagato')
->label('Importo da pagare')
->numeric()
->required()
->prefix('€'),
])
->addable(false)
->deletable(false)
->columns(5),
])
->action(function (MovimentoBanca $record, array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
try {
DB::transaction(function () use ($record, $data, $user) {
$stabileId = StabileContext::resolveActiveStabileId($user);
$gestioneId = (empty($record->gestione_id) && isset($data['gestione_id']) && is_numeric($data['gestione_id']))
? (int) $data['gestione_id']
: (int) ($record->gestione_id ?? 0);
if (empty($record->gestione_id)) {
$record->gestione_id = $gestioneId;
$record->save();
}
$dataPagamento = $record->data ?: now()->toDateString();
$importoMovimento = round((float) ($record->importo ?? 0), 2);
$rateSelections = $data['rate'] ?? [];
$totalAllocated = 0.0;
foreach ($rateSelections as $sel) {
$totalAllocated += round((float) ($sel['importo_pagato'] ?? 0), 2);
}
$totalAllocated = round($totalAllocated, 2);
if ($totalAllocated > $importoMovimento) {
throw new \RuntimeException('Il totale da pagare inserito supera l\'importo del movimento bancario.');
}
// Crea la registrazione
$reg = \App\Modules\Contabilita\Models\Registrazione::create([
'stabile_id' => $stabileId,
'gestione_id' => \App\Modules\Contabilita\Models\Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, $dataPagamento),
'data_registrazione' => $dataPagamento,
'descrizione' => 'Incasso rate da movimento banca: ' . $record->descrizione,
'created_by' => $user->id,
'updated_by' => $user->id,
]);
// Dare: Banca c/c
$contoDareId = null;
$datiBancari = DatiBancari::query()->where('iban', $record->iban)->first();
if ($datiBancari && is_string($datiBancari->codice)) {
$byCode = \App\Modules\Contabilita\Models\PianoConti::query()->where('codice', trim((string) $datiBancari->codice))->first();
if ($byCode) {
$contoDareId = (int) $byCode->id;
}
}
if (!$contoDareId) {
$contoDareId = 7;
}
\App\Modules\Contabilita\Models\Movimento::create([
'registrazione_id' => $reg->id,
'conto_id' => $contoDareId,
'tipo' => 'dare',
'importo' => $totalAllocated,
]);
// Avere & incassi per ciascuna rata
foreach ($rateSelections as $sel) {
$rataId = (int) $sel['rata_id'];
$importoPagato = round((float) ($sel['importo_pagato'] ?? 0), 2);
if ($importoPagato <= 0) {
continue;
}
$rata = \App\Models\RataEmessaNg::query()->lockForUpdate()->find($rataId);
if (!$rata) {
throw new \RuntimeException('Rata non trovata.');
}
$nuovoImportoPagato = round((float) $rata->importo_pagato + $importoPagato, 2);
$importoDovuto = round((float) $rata->importo_addebitato_soggetto, 2);
$statoRata = 'parziale';
if ($nuovoImportoPagato >= $importoDovuto) {
$statoRata = 'pagata';
}
$rata->update([
'importo_pagato' => $nuovoImportoPagato,
'stato_rata' => $statoRata,
'data_ultimo_pagamento' => $dataPagamento,
]);
$contoAvereId = null;
$condominoId = $rata->soggetto_responsabile_id;
if ($condominoId > 0) {
$condomino = \App\Models\Condomino::query()->find($condominoId);
$conto = app(\App\Modules\Contabilita\Services\ContiSoggettiService::class)->resolveContoCreditoCondomino(
$stabileId,
$condominoId,
$condomino?->display_name,
is_string($condomino?->codice_univoco ?? null) ? trim((string) $condomino->codice_univoco) : null
);
$contoAvereId = (int) $conto->id;
}
if (!$contoAvereId) {
$contoAvereId = 8;
}
\App\Modules\Contabilita\Models\Movimento::create([
'registrazione_id' => $reg->id,
'conto_id' => $contoAvereId,
'tipo' => 'avere',
'importo' => $importoPagato,
]);
\App\Models\Incasso::create([
'gestione_id' => $gestioneId,
'condominio_id' => $stabileId,
'conto_bancario_id' => $datiBancari?->id,
'condomino_id' => $condominoId,
'movimento_bancario_id' => $record->id,
'importo_pagato_euro' => $importoPagato,
'data_pagamento' => $dataPagamento,
'descrizione' => 'Incasso rata: ' . $rata->descrizione,
'stato_riconciliazione' => 'manuale',
'registrazione_id' => $reg->id,
'rate_associate' => [$rata->id],
]);
}
$reg->assertBilanciata();
$record->update([
'registrazione_id' => $reg->id,
]);
});
Notification::make()->title('Incasso rate registrato con successo.')->success()->send();
$this->resetTable();
} catch (\Throwable $e) {
Notification::make()->title('Errore durante l\'incasso')->body($e->getMessage())->danger()->send();
}
}),
]);
}
@ -2303,7 +2682,7 @@ protected function getActiveStabileId(): ?int
protected function normalizeHubTabValue(?string $value): string
{
$value = is_string($value) ? trim(strtolower($value)) : '';
return in_array($value, ['conti', 'saldi', 'movimenti', 'struttura', 'riconciliazione'], true) ? $value : 'conti';
return in_array($value, ['conti', 'saldi', 'movimenti', 'struttura', 'riconciliazione', 'associazione_causali'], true) ? $value : 'conti';
}
protected function syncSelectedContoFromId(?int $contoId): void
@ -4240,4 +4619,73 @@ protected function storeSaldoDocumento(int $stabileId, DatiBancari $conto, mixed
'caricato_da' => (int) $user->id,
]);
}
public function getCausaliAssociazioneRows(): array
{
$stabileId = $this->getActiveStabileId();
if (! $stabileId) {
return [];
}
$uniqueCausali = DB::table('contabilita_movimenti_banca')
->where('stabile_id', $stabileId)
->whereNotNull('causale')
->where('causale', '!=', '')
->select('causale')
->selectRaw('count(*) as count')
->selectRaw('max(descrizione) as sample_description')
->groupBy('causale')
->get();
$rows = [];
foreach ($uniqueCausali as $uc) {
$code = $uc->causale;
$official = DB::table('contabilita_causali_bancarie')
->where('codice', $code)
->first();
$descr = $official?->descrizione ?? $uc->sample_description;
$regola = \App\Modules\Contabilita\Models\RegolaPrimaNota::query()
->where('stabile_id', $stabileId)
->where('origine', 'banca')
->where('chiave', $code)
->first();
$rows[] = [
'causale' => $code,
'descrizione' => $descr,
'count' => $uc->count,
'regola_id' => $regola?->id,
'conto_dare' => $regola?->contoDare ? ($regola->contoDare->codice . ' - ' . $regola->contoDare->descrizione) : null,
'conto_avere' => $regola?->contoAvere ? ($regola->contoAvere->codice . ' - ' . $regola->contoAvere->descrizione) : null,
'attiva' => $regola?->attiva ?? false,
];
}
return $rows;
}
public function startConfiguringRegolaCausale(string $causale): void
{
$this->selectedCausaleForConfig = $causale;
$stabileId = $this->getActiveStabileId();
$regola = null;
if ($stabileId) {
$regola = \App\Modules\Contabilita\Models\RegolaPrimaNota::query()
->where('stabile_id', $stabileId)
->where('origine', 'banca')
->where('chiave', $causale)
->first();
}
$this->mountAction('configura_regola_causale', [
'causale' => $causale,
'conto_dare_id' => $regola?->conto_dare_id,
'conto_avere_id' => $regola?->conto_avere_id,
'label' => $regola?->label,
'note' => $regola?->note,
]);
}
}

View File

@ -3069,9 +3069,7 @@ public function getTabelleMillesimaliByTipo(string $tipoTabella): array
return TabellaMillesimale::query()
->where('stabile_id', $stabileId)
->where('tipo_tabella', $tipoTabella)
->orderBy('ordinamento')
->orderBy('codice_tabella')
->orderBy('nome_tabella')
->ordinato()
->get(['id', 'denominazione', 'nome_tabella', 'codice_tabella'])
->map(function (TabellaMillesimale $t): array {
$label = trim((string) ($t->denominazione ?? ''));

View File

@ -147,11 +147,13 @@ public function form(Schema $schema): Schema
Select::make('tipo')
->label('Tipo')
->options(['dare' => 'Dare', 'avere' => 'Avere'])
->required(),
->required()
->live(),
TextInput::make('importo')
->label('Importo')
->numeric()
->required(),
->required()
->live(onBlur: true),
])
->columns(3)
->required(),
@ -159,6 +161,46 @@ public function form(Schema $schema): Schema
]);
}
public function getHeaderSummary(): array
{
$state = is_array($this->data ?? null) ? $this->data : [];
$movimenti = $state['movimenti'] ?? [];
if (empty($movimenti) && isset($this->registrazione) && $this->registrazione->id) {
$movimenti = Movimento::query()
->where('registrazione_id', (int) $this->registrazione->id)
->orderBy('id')
->get()
->map(fn (Movimento $m) => [
'conto_id' => $m->conto_id,
'tipo' => $m->tipo,
'importo' => $m->importo,
])
->all();
}
$totDare = 0.0;
$totAvere = 0.0;
foreach ($movimenti as $m) {
$tipo = (string) ($m['tipo'] ?? '');
$importo = (float) str_replace(',', '.', str_replace('.', '', (string) ($m['importo'] ?? '0')));
if ($tipo === 'dare') {
$totDare += $importo;
} elseif ($tipo === 'avere') {
$totAvere += $importo;
}
}
$delta = round($totDare - $totAvere, 2);
return [
'totale_dare' => $totDare,
'totale_avere' => $totAvere,
'delta' => $delta,
'is_bilanciata' => abs($delta) < 0.005,
];
}
protected function getHeaderActions(): array
{
return [

View File

@ -448,10 +448,7 @@ public function getTabelleMillesimaliOptions(): array
$q = TabellaMillesimale::query()
->where('stabile_id', $stabileId)
->orderBy('ordine_visualizzazione')
->orderBy('ordinamento')
->orderBy('codice_tabella')
->orderBy('nome_tabella');
->ordinato();
if ($hasAnno) {
$q->where('anno_gestione', $anno);

View File

@ -750,6 +750,7 @@ public function applicaEnrichmentStabile(): void
public function aggiornaAnagraficheDaCondomin(): void
{
@set_time_limit(0);
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
@ -789,6 +790,7 @@ public function aggiornaAnagraficheDaCondomin(): void
public function risincronizzaRelazioniImportate(): void
{
@set_time_limit(0);
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
@ -826,6 +828,7 @@ public function risincronizzaRelazioniImportate(): void
public function aggiornaFornitoriGescon(): void
{
@set_time_limit(0);
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
@ -894,6 +897,7 @@ public function aggiornaFornitoriGescon(): void
public function refreshImportTagLegacyFornitoriGlobale(): void
{
@set_time_limit(0);
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
@ -968,6 +972,7 @@ public function refreshImportTagLegacyFornitoriGlobale(): void
public function applicaEnrichmentTuttiStabili(): void
{
@set_time_limit(0);
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
@ -1019,6 +1024,7 @@ public function applicaEnrichmentTuttiStabili(): void
public function eseguiImportSelezionato(EssentialImportService $service): void
{
@set_time_limit(0);
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
@ -1179,8 +1185,11 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
if (! empty($options['sync_staging'])) {
$singoloMdb = null;
$annoFolder = $options['anno'] ?: '0001';
if (! empty($options['path']) && ! empty($code)) {
$annoFolder = $options['anno'] ?: '0001';
if (is_numeric($annoFolder) && strlen($annoFolder) === 4 && (int)$annoFolder > 1900) {
$annoFolder = $this->resolveYearToFolder($code, $options['path'], $annoFolder);
}
$singoloMdb = rtrim($options['path'], '/') . '/' . $code . '/' . $annoFolder . '/singolo_anno.mdb';
}
@ -1189,7 +1198,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
: null;
try {
$legacyYearArg = (string) ($options['anno'] ?? '');
$legacyYearArg = $annoFolder;
if ((! empty($options['with_banche']) || ! empty($options['with_operazioni']) || ! empty($options['with_incassi'])) && ! empty($options['path']) && ! empty($code)) {
foreach ($this->resolveLegacySingoloMdbPaths($code, (string) $options['path'], $legacyYearArg !== '' ? $legacyYearArg : null) as $mdbPath => $legacyDir) {
@ -1209,7 +1218,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb,
'--table' => 's_cassa',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--legacy-year' => $annoFolder,
'--refresh-slice' => true,
]);
$outputs[] = trim((string) Artisan::output());
@ -1218,7 +1227,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb,
'--table' => 'operazioni',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--legacy-year' => $annoFolder,
'--refresh-slice' => true,
]);
$outputs[] = trim((string) Artisan::output());
@ -1227,7 +1236,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb,
'--table' => 'voc_spe',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--legacy-year' => $annoFolder,
'--refresh-slice' => true,
]);
$outputs[] = trim((string) Artisan::output());
@ -1236,7 +1245,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb,
'--table' => 'tabelle',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--legacy-year' => $annoFolder,
'--refresh-slice' => true,
]);
$outputs[] = trim((string) Artisan::output());
@ -1248,7 +1257,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $singoloMdb,
'--table' => 'incassi',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--legacy-year' => $annoFolder,
'--refresh-slice' => true,
]);
$outputs[] = trim((string) Artisan::output());
@ -1259,7 +1268,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $generaleMdb,
'--table' => 'emes_gen',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--legacy-year' => $annoFolder,
'--refresh-slice' => true,
]);
$outputs[] = trim((string) Artisan::output());
@ -1268,7 +1277,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--mdb' => $generaleMdb,
'--table' => 'emes_det',
'--stabile' => $code,
'--legacy-year' => (string) ($options['anno'] ?? ''),
'--legacy-year' => $annoFolder,
'--refresh-slice' => true,
]);
$outputs[] = trim((string) Artisan::output());
@ -1289,7 +1298,11 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'--path' => $options['path'],
];
if (! empty($options['anno'])) {
$params['--anno'] = $options['anno'];
$annoParam = $options['anno'];
if (is_numeric($annoParam) && strlen($annoParam) === 4 && (int)$annoParam > 1900) {
$annoParam = $this->resolveYearToFolder($code, $options['path'], $annoParam);
}
$params['--anno'] = $annoParam;
}
if (! empty($options['dry'])) {
$params['--dry-run'] = true;
@ -1350,6 +1363,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
public function eseguiImportAllineamentoCompleto(): void
{
@set_time_limit(0);
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
@ -1793,8 +1807,15 @@ private function applyGestioneSetup(Stabile $stabile): int
private function upsertGestioneRecord(Stabile $stabile, int $year, string $tipo, array $months, bool $active): void
{
$tenantId = GestioneContabile::query()
->where('stabile_id', $stabile->id)
->value('tenant_id');
if (! is_string($tenantId) || trim($tenantId) === '') {
$tenantId = (string) (Auth::user()?->tenant_id ?? 'default');
}
$gestione = GestioneContabile::query()->firstOrNew([
'tenant_id' => 'default',
'tenant_id' => $tenantId,
'stabile_id' => $stabile->id,
'anno_gestione' => $year,
'tipo_gestione' => $tipo,
@ -2383,4 +2404,42 @@ private function mdbExportToRows(string $mdbPath, array $tableCandidates): array
return $rows;
}
private function resolveYearToFolder(string $code, string $path, string $year): string
{
$code = str_pad(trim($code), 4, '0', STR_PAD_LEFT);
$mdb = rtrim($path, '/') . '/' . $code . '/generale_stabile.mdb';
if (! is_file($mdb) || ! is_readable($mdb)) {
return $year;
}
$rows = $this->mdbExportToRows($mdb, ['anni', 'Anni', 'ANNI']);
if (empty($rows)) {
return $year;
}
foreach ($rows as $r) {
$yearRaw = $this->firstNonEmpty($r, ['anno_o', 'anno_r', 'anno']);
$y = null;
if (is_scalar($yearRaw) && preg_match('/(19|20)\d{2}/', (string) $yearRaw, $m)) {
$y = $m[0];
}
if ($y === null) {
continue;
}
if ($y === $year) {
$dirRaw = $this->firstNonEmpty($r, ['nome_dir', 'dir', 'cartella', 'id_anno']);
$dir = trim((string) ($dirRaw ?? ''));
if ($dir === '' && is_numeric((string) ($r['id_anno'] ?? ''))) {
$dir = str_pad((string) ((int) $r['id_anno']), 4, '0', STR_PAD_LEFT);
}
if ($dir !== '') {
return $dir;
}
}
}
return $year;
}
}

View File

@ -51,7 +51,7 @@ class Ordinarie extends Page
public string $viewTab = 'operazioni';
public string $sortField = 'id_operaz';
public string $sortDirection = 'desc';
public bool $netgesconWorkflowMode = false;
public bool $netgesconWorkflowMode = true;
public bool $onlyStraordinarieMode = false;
public bool $onlyRiscaldamentoMode = false;
public ?int $selectedStraordinaria = null;
@ -3306,6 +3306,7 @@ public function openDettPersModal(int $nSpe): void
if ($activeStabileId && Schema::hasTable('tabelle_millesimali') && Schema::hasTable('dettaglio_millesimi')) {
$localRows = DB::table('tabelle_millesimali')
->where('stabile_id', (int) $activeStabileId)
->orderByRaw('COALESCE(nord, ordinamento, ordine_visualizzazione, 999999)')
->orderBy('codice_tabella')
->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione']);
@ -3716,8 +3717,7 @@ private function buildOperazioniQuery()
$query->where(function ($q) use ($s) {
$q->where('o.descrizione', 'like', $s)
->orWhere('o.conto_contabile', 'like', $s)
->orWhere('o.protocollo_completo', 'like', $s)
->orWhere('o.num_fat', 'like', $s);
->orWhere('o.protocollo_completo', 'like', $s);
});
}
@ -3739,8 +3739,8 @@ private function buildOperazioniQuery()
'o.n_stra',
'o.protocollo_completo',
DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"),
'o.num_fat',
'o.fe_uid',
DB::raw("null as num_fat"),
DB::raw("null as fe_uid"),
'o.voce_spesa_snapshot',
'o.fornitore_snapshot',
]);
@ -3767,8 +3767,8 @@ private function buildOperazioniQuery()
'o.protocollo_completo',
DB::raw("coalesce(o.dare, o.avere, 0) as importo_euro"),
DB::raw("coalesce(o.dare, o.avere, 0) as importo"),
'o.num_fat',
'o.fe_uid',
DB::raw("null as num_fat"),
DB::raw("null as fe_uid"),
'o.voce_spesa_snapshot',
'o.fornitore_snapshot',
DB::raw("0 as importo_spese"),
@ -3994,8 +3994,8 @@ private function applyOperazioniSorting($query)
'dt_spe' => 'o.data_operazione',
'cod_spe' => 'o.conto_contabile',
'importo_euro' => 'o.dare',
'num_fat' => 'o.num_fat',
'fe_uid' => 'o.fe_uid',
'num_fat' => 'o.id',
'fe_uid' => 'o.id',
default => 'o.id'
};
$query->orderBy($mappedField, $dir);

View File

@ -555,7 +555,7 @@ private function getTabelleOptions(): array
return TabellaMillesimale::query()
->where('stabile_id', $this->stabileAttivo->id)
->orderBy('codice_tabella')
->ordinato()
->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione'])
->mapWithKeys(function (TabellaMillesimale $t) {
$label = $t->codice_tabella ?: ($t->denominazione ?: ($t->nome_tabella ?: null));

View File

@ -30,11 +30,11 @@ class CausaliBancarie extends Page implements HasTable
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-tag';
protected static UnitEnum|string|null $navigationGroup = 'Super Admin';
protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
protected static ?int $navigationSort = 22;
protected static ?int $navigationSort = 20;
protected static ?string $slug = 'superadmin/causali-bancarie';
protected static ?string $slug = 'contabilita/causali-bancarie';
protected string $view = 'filament.pages.gescon.table';
@ -43,7 +43,7 @@ public static function canAccess(): bool
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin'])
&& ($user->hasAnyRole(['super-admin', 'admin']) || $user->can('contabilita.causali'))
&& Schema::hasTable('contabilita_causali_bancarie');
}

View File

@ -23,7 +23,7 @@
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tables\Actions\Action as TableAction;
use Filament\Actions\Action as TableAction;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;

View File

@ -13,31 +13,29 @@ class IncassoPagamento extends Model
protected $table = 'incassi_pagamenti';
protected $fillable = [
'amministratore_id',
'anno_gestione_id',
'stabile_id',
'anno_gestione_id',
'condomino_id',
'rata_id',
'data_incasso',
'data_valuta',
'importo',
'modalita_pagamento',
'numero_assegno',
'banca_assegno',
'numero_bonifico',
'causale',
'note_bancarie',
'cod_condomino',
'n_riferimento',
'data_pagamento',
'importo_pagato',
'importo_pagato_euro',
'n_ricevuta',
'tipo_gestione',
'mese_riferimento',
'descrizione',
'cod_cassa',
'riconciliato',
'data_riconciliazione',
'note'
'rata_id',
'movimento_banca_id',
];
protected $casts = [
'data_incasso' => 'date',
'data_valuta' => 'date',
'data_riconciliazione' => 'date',
'importo' => 'decimal:2',
'riconciliato' => 'boolean'
'data_pagamento' => 'date',
'importo_pagato' => 'decimal:2',
'importo_pagato_euro' => 'decimal:2',
'riconciliato' => 'boolean',
];
const MODALITA_CONTANTI = 'contanti';

View File

@ -588,6 +588,14 @@ public function syncRubricaContatto(): RubricaUniversale
'stato' => 'attivo',
];
if (! $rubrica && ! empty($data['codice_univoco'])) {
$existing = RubricaUniversale::where('codice_univoco', $data['codice_univoco'])->first();
if ($existing) {
$rubrica = $existing;
$this->update(['rubrica_id' => $rubrica->id]);
}
}
if ($rubrica) {
$rubrica->update($data);
} else {

View File

@ -153,15 +153,8 @@ public function scopePerStabile($query, $stabileId)
*/
public function scopeOrdinato($query)
{
if (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione')) {
$query->orderBy('ordine_visualizzazione');
} elseif (Schema::hasColumn('tabelle_millesimali', 'nord')) {
$query->orderBy('nord');
} else {
$query->orderBy('ordinamento');
}
return $query
->orderByRaw('COALESCE(ordine_visualizzazione, nord, ordinamento, 999999)')
->orderBy('codice_tabella')
->orderBy('nome_tabella');
}

View File

@ -67,6 +67,96 @@ public function getDescrizioneEstesaPulitaAttribute(): ?string
return $text !== '' ? $text : null;
}
public function getDettaglioEstrattoAttribute(): array
{
$rawText = $this->descrizione_estesa ?? $this->descrizione ?? '';
$text = trim($rawText);
$commissioni = '';
$pos = -1;
foreach (['COMM', 'SPESE', 'TRN', 'COMMISSIONI', 'COMMISSIONE'] as $key) {
$p = stripos($text, $key);
if ($p !== false && ($pos === -1 || $p < $pos)) {
$pos = $p;
}
}
if ($pos !== -1) {
$commissioni = trim(substr($text, $pos));
$text = trim(substr($text, 0, $pos));
}
$bankPrefixes = [
'BONIFICO A VOSTRO FAVORE BONIFICO SEPA DA',
'BONIFICO A VOSTRO FAVORE BONIFICO SEPA',
'BONIFICO SEPA A VOSTRO FAVORE DA',
'BONIFICO SEPA A VOSTRO FAVORE',
'BONIFICO A VOSTRO FAVORE DA',
'BONIFICO A VOSTRO FAVORE',
'BONIFICO SEPA DA',
'BONIFICO SEPA',
'BONIFICO DA',
'BONIFICO ESEGUITO DA',
'ADDEBITO CORRISPETTIVI',
'GIROCONTO DA',
'GIROCONTO',
'DISPOSIZIONE DI PAGAMENTO DA',
'DISPOSIZIONE DI PAGAMENTO',
'PAGAMENTO CBILL',
'PAGAMENTO PAGOPA',
'VERSAMENTO DI CASSA',
'VERSAMENTO DA',
'VERSAMENTO',
'PRELEVAMENTO DA',
'PRELEVAMENTO',
'PAGAMENTO CON CARTA',
'PAGAMENTO POS',
'ADDEBITO DISPOSIZIONE SEPA',
'ADDEBITO SEPA',
];
$messaggioBanca = '';
$messaggioCliente = $text;
foreach ($bankPrefixes as $prefix) {
if (stripos($text, $prefix) === 0) {
$messaggioBanca = $prefix;
$messaggioCliente = trim(substr($text, strlen($prefix)));
break;
}
}
if (empty($messaggioBanca) && !empty($this->causale)) {
$messaggioBanca = $this->causale;
}
$cbillCode = null;
if (preg_match('/(?:CBILL|PAGOPA|IUV|AVVISO|CODICE\s*AVVISO)\s*:?\s*(\d{10,20})/i', $rawText, $cbillMatches)) {
$cbillCode = $cbillMatches[1];
}
$abi = null;
if (!empty($this->raw_line)) {
$parts = explode(';', $this->raw_line);
if (count($parts) >= 2) {
$last = trim(end($parts));
if (preg_match('/^[a-zA-Z0-9]{3,5}$/', $last)) {
$abi = $last;
}
}
}
if (!$abi && is_array($this->match_data) && !empty($this->match_data['cod_abi'])) {
$abi = $this->match_data['cod_abi'];
}
return [
'abi' => $abi,
'messaggio_banca' => $messaggioBanca ?: '—',
'messaggio_cliente' => $messaggioCliente ?: '—',
'commissioni_spese' => $commissioni ?: '—',
'cbill_code' => $cbillCode,
];
}
protected function applyCleaningRules(string $text): string
{
$patterns = [];

View File

@ -185,6 +185,8 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
}
}
$isCreditNote = isset($data['tipo_documento']) && strtoupper(trim($data['tipo_documento'])) === 'TD04';
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
@ -230,7 +232,17 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
try {
/** @var FatturaElettronica $created */
$created = DB::transaction(function () use ($stabileId, $fornitoreId, $data, $xml, $hash, $originalFilename, $fornitorePiva, $extra, $importRighe, &$contabilitaFatturaId) {
$created = DB::transaction(function () use ($stabileId, $fornitoreId, $data, $xml, $hash, $originalFilename, $fornitorePiva, $extra, $importRighe, &$contabilitaFatturaId, $isCreditNote) {
$imponibile = (float) ($data['imponibile'] ?? 0);
$iva = (float) ($data['iva'] ?? 0);
$totale = (float) ($data['totale'] ?? 0);
if ($isCreditNote) {
$imponibile = -abs($imponibile);
$iva = -abs($iva);
$totale = -abs($totale);
}
$created = FatturaElettronica::query()->create([
'stabile_id' => $stabileId,
'fornitore_id' => $fornitoreId,
@ -241,9 +253,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
'fornitore_piva' => $fornitorePiva,
'fornitore_cf' => $data['fornitore_cf'] ?? null,
'fornitore_denominazione' => $data['fornitore_denominazione'] ?? 'ND',
'imponibile' => (float) ($data['imponibile'] ?? 0),
'iva' => (float) ($data['iva'] ?? 0),
'totale' => (float) ($data['totale'] ?? 0),
'imponibile' => $imponibile,
'iva' => $iva,
'totale' => $totale,
'pagamento_modalita' => $data['pagamento_modalita'] ?? null,
'pagamento_iban' => $data['pagamento_iban'] ?? null,
@ -274,7 +286,10 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
$ritenuta = is_array($data['ritenuta'] ?? null) ? $data['ritenuta'] : [];
$ritenutaImporto = (float) ($ritenuta['importo'] ?? 0);
$ritenutaAliquota = $ritenuta['aliquota'] ?? null;
$netto = round(((float) ($data['totale'] ?? 0)) - $ritenutaImporto, 2);
if ($isCreditNote) {
$ritenutaImporto = -abs($ritenutaImporto);
}
$netto = round($totale - $ritenutaImporto, 2);
$dataDoc = $data['data_fattura'] ?? null;
$dataRegistrazione = null;
@ -298,9 +313,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
'descrizione' => $data['fornitore_denominazione'] ?? null,
'valuta' => 'EUR',
'cambio' => 1,
'imponibile' => round((float) ($data['imponibile'] ?? 0), 2),
'iva' => round((float) ($data['iva'] ?? 0), 2),
'totale' => round((float) ($data['totale'] ?? 0), 2),
'imponibile' => round($imponibile, 2),
'iva' => round($iva, 2),
'totale' => round($totale, 2),
'data_scadenza' => $data['data_scadenza'] ?? null,
'modalita_pagamento' => $data['pagamento_modalita'] ?? null,
'ritenuta_aliquota' => $ritenutaAliquota,
@ -333,7 +348,7 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
'descrizione' => $data['numero_fattura'] ?? 'Fattura elettronica',
'quantita' => null,
'prezzo_unitario' => null,
'prezzo_totale' => (float) ($data['imponibile'] ?? 0),
'prezzo_totale' => $imponibile,
'aliquota_iva' => null,
'ritenuta' => null,
]];
@ -344,6 +359,9 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
if ($imponibileRiga <= 0 && isset($r['prezzo_unitario'], $r['quantita'])) {
$imponibileRiga = (float) $r['prezzo_unitario'] * (float) $r['quantita'];
}
if ($isCreditNote) {
$imponibileRiga = -abs($imponibileRiga);
}
$aliquota = $r['aliquota_iva'] ?? null;
$ivaRiga = 0.0;

View File

@ -2,8 +2,8 @@
namespace App\Services\Import;
use App\Models\GestioneContabile;
use App\Models\Incasso;
use App\Models\IncassoEstrattoConto;
use App\Models\OperazioneContabile;
use App\Models\RegistroRitenuteAcconto;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -21,12 +21,52 @@
class MultiYearGesconImportService
{
private string $tenantId;
private ?int $stabileId = null;
private ?string $baseDir = null;
private array $directoryToYearMap = [];
private array $mdbConnections = [];
private array $gestioni = [];
public function __construct(string $tenantId)
public function __construct(string $tenantId, ?int $stabileId = null, ?string $baseDir = null)
{
$this->tenantId = $tenantId;
$this->stabileId = $stabileId;
$this->baseDir = $baseDir;
if ($baseDir) {
$this->loadYearMap($baseDir);
}
}
private function loadYearMap(string $baseDir): void
{
$generaleMdb = rtrim($baseDir, '/') . '/generale_stabile.mdb';
if (!is_file($generaleMdb)) {
return;
}
try {
$output = shell_exec(sprintf(
'echo "SELECT id_anno, anno_o FROM anni" | mdb-sql %s 2>/dev/null',
escapeshellarg($generaleMdb)
));
if ($output) {
$lines = explode("\n", $output);
foreach ($lines as $line) {
if (str_contains($line, '|')) {
$parts = explode('|', $line);
$id = (int) trim($parts[1] ?? '');
$yr = (int) trim($parts[2] ?? '');
if ($id > 0 && $yr >= 2000 && $yr <= 2100) {
$dir = str_pad((string)$id, 4, '0', STR_PAD_LEFT);
$this->directoryToYearMap[$dir] = $yr;
}
}
}
}
} catch (\Throwable $e) {
Log::warning("Could not load year map from general MDB: " . $e->getMessage());
}
}
/**
@ -87,6 +127,31 @@ private function scanGesconYears(string $baseDir): array
return $years;
}
/**
* Crea gestioni contabili per tutti gli anni scansionati
*/
public function createGestioni(array $years): void
{
foreach ($years as $year => $paths) {
$this->initGestioniForYear($year, $paths);
}
}
/**
* Inizializza le gestioni contabili per un singolo anno
*/
public function initGestioniForYear(string $year, array $paths): void
{
$calendarYear = $this->directoryToYearMap[$year] ?? (int) $year;
$mdbPath = $paths['singolo_anno'];
$gestioniData = $this->scanGestioniFromMdb($mdbPath, $calendarYear);
foreach ($gestioniData as $data) {
$this->createOrUpdateGestione($data, $year);
}
}
/**
* Scansiona gestioni direttamente dal file MDB
*/
@ -104,13 +169,25 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array
'protocollo_prefix' => "O{$year}",
];
// Gestione Riscaldamento (se presente)
$opRiscaldamento = $this->queryMdb($mdbPath, "
SELECT COUNT(*) as cnt FROM operazioni
WHERE gestione = 'R' OR gestione = 'r'
");
// Legge le colonne necessarie da Operazioni
$operazioni = $this->queryMdb($mdbPath, "SELECT Gestione, n_stra FROM Operazioni");
if (! empty($opRiscaldamento) && ($opRiscaldamento[0]['cnt'] ?? 0) > 0) {
$hasRiscaldamento = false;
$straordinarie = [];
foreach ($operazioni as $op) {
$g = strtoupper(trim($op['Gestione'] ?? ''));
if ($g === 'R') {
$hasRiscaldamento = true;
} elseif ($g === 'S') {
$nStra = (int) ($op['n_stra'] ?? 0);
if ($nStra > 0) {
$straordinarie[$nStra] = true;
}
}
}
if ($hasRiscaldamento) {
$gestioni[] = [
'anno_gestione' => $year,
'tipo_gestione' => 'riscaldamento',
@ -121,18 +198,8 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array
];
}
// Gestioni Straordinarie (dinamiche)
$straordinarie = $this->queryMdb($mdbPath, "
SELECT DISTINCT N_STRA as numero
FROM operazioni
WHERE (gestione = 'S' OR gestione = 's')
AND N_STRA IS NOT NULL
AND N_STRA > 0
ORDER BY N_STRA
");
foreach ($straordinarie as $stra) {
$numero = (int) $stra['numero'];
ksort($straordinarie);
foreach (array_keys($straordinarie) as $numero) {
$gestioni[] = [
'anno_gestione' => $year,
'tipo_gestione' => 'straordinaria',
@ -150,10 +217,11 @@ private function scanGestioniFromMdb(string $mdbPath, int $year): array
/**
* Crea o aggiorna gestione
*/
private function createOrUpdateGestione(array $gestioneData): GestioneContabile
private function createOrUpdateGestione(array $gestioneData, ?string $yearKey = null): GestioneContabile
{
$gestione = GestioneContabile::updateOrCreate([
'tenant_id' => $this->tenantId,
'stabile_id' => $this->stabileId,
'anno_gestione' => $gestioneData['anno_gestione'],
'tipo_gestione' => $gestioneData['tipo_gestione'],
'numero_straordinaria' => $gestioneData['numero_straordinaria'] ?? null,
@ -166,7 +234,7 @@ private function createOrUpdateGestione(array $gestioneData): GestioneContabile
]);
// Cache per lookup veloce
$year = $gestioneData['anno_gestione'];
$year = $yearKey ?? $gestioneData['anno_gestione'];
$tipo = $gestioneData['tipo_gestione'];
if ($tipo === 'straordinaria') {
@ -189,37 +257,67 @@ private function importOperazioniForGestione(GestioneContabile $gestione, string
$whereClause = $this->buildGestioneWhereClause($gestione);
$operazioni = $this->queryMdb($mdbPath, "
SELECT *,
IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean,
IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean,
IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean
FROM operazioni
SELECT *
FROM Operazioni
WHERE {$whereClause}
ORDER BY n_operazione
");
usort($operazioni, function($a, $b) {
$idA = (int) ($a['id_operaz'] ?? 0);
$idB = (int) ($b['id_operaz'] ?? 0);
return $idA <=> $idB;
});
$imported = 0;
foreach ($operazioni as $op) {
try {
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? '');
$protocollo = $this->generateProtocollo($gestione->id, $op);
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? '');
OperazioneContabile::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestione->id,
'numero_operazione' => $op['n_operazione'],
'data_operazione' => $this->parseGesconDate($op['data_oper']),
'importo' => (float) ($op['euro'] ?? 0),
'descrizione' => $op['descrizione'] ?? '',
'cod_voce_gescon' => $op['cod_voce'] ?? '',
'cod_beneficiario_gescon' => $op['cod_ben'] ?? '',
'compet' => $op['compet_clean'],
'natura2' => $op['natura2_clean'],
'n_stra' => (int) $op['n_stra_clean'],
'protocollo_numero' => $protocollo['numero'],
'protocollo_completo' => $protocollo['completo'],
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'metadati_gescon' => json_encode($op),
$competClean = empty($op['compet']) ? 'C' : trim($op['compet']);
$natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']);
$nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra'];
$opId = $op['id_operaz'] ?? $op['id'] ?? null;
$dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null;
$importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0;
$codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? '';
$codBen = $op['cod_ben'] ?? '';
$isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata';
$amount = (float) $importo;
$dareVal = (float) ($op['importo_spese'] ?? 0);
$avereVal = (float) ($op['importo_entrate'] ?? 0);
if ($dareVal == 0 && $avereVal == 0 && $amount != 0) {
if ($isEntrata) {
$avereVal = $amount;
} else {
$dareVal = $amount;
}
}
$contoBancarioId = $this->resolveContoBancarioId($mdbPath, $op['cod_cassa'] ?? null);
DB::table('operazioni_contabili')->insert([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestione->id,
'legacy_id' => $opId,
'descrizione' => $op['note'] ?? $op['descrizione'] ?? $op['natura'] ?? 'Operazione',
'data_operazione' => $this->parseGesconDate($dtSpe),
'compet' => $competClean,
'natura2' => $natura2Clean,
'n_stra' => $nStraClean,
'protocollo_numero' => $protocollo['numero'],
'protocollo_completo' => $protocollo['completo'],
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'dare' => $dareVal,
'avere' => $avereVal,
'conto_contabile' => $codVoce,
'conto_bancario_id' => $contoBancarioId,
'stato_operazione' => 'confermata',
'created_at' => now(),
'updated_at' => now(),
]);
$imported++;
@ -396,8 +494,12 @@ private function buildGestioneJoinClause(GestioneContabile $gestione, string $al
/**
* Import dati per una specifica gestione/anno
*/
private function importGestioneYear(string $year, array $paths): array
public function importGestioneYear(string $year, array $paths): array
{
if (! isset($this->gestioni[$year])) {
$this->initGestioniForYear($year, $paths);
}
$results = [
'operazioni' => 0,
'incassi' => 0,
@ -421,7 +523,9 @@ private function importGestioneYear(string $year, array $paths): array
}
// 5. Import incassi estratto conto
$results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['singolo_anno']);
if (file_exists($paths['generale_stabile'])) {
$results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['generale_stabile']);
}
return $results;
}
@ -435,14 +539,16 @@ private function importOperazioni(string $year, string $mdbPath): int
// Query MDB per operazioni
$operazioni = $this->queryMdb($mdbPath, "
SELECT *,
IIF(ISNULL(COMPET), 'C', COMPET) as compet_clean,
IIF(ISNULL(NATURA2), '', NATURA2) as natura2_clean,
IIF(ISNULL(N_STRA), 0, N_STRA) as n_stra_clean
FROM operazioni
ORDER BY n_operazione
SELECT *
FROM Operazioni
");
usort($operazioni, function($a, $b) {
$idA = (int) ($a['id_operaz'] ?? 0);
$idB = (int) ($b['id_operaz'] ?? 0);
return $idA <=> $idB;
});
foreach ($operazioni as $op) {
try {
// Determina gestione
@ -452,24 +558,52 @@ private function importOperazioni(string $year, string $mdbPath): int
$protocollo = $this->generateProtocollo($gestioneId, $op);
// Snapshot voce spesa
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_voce'] ?? '');
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? '');
OperazioneContabile::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'numero_operazione' => $op['n_operazione'],
'data_operazione' => $this->parseGesconDate($op['data_oper']),
'importo' => (float) ($op['euro'] ?? 0),
'descrizione' => $op['descrizione'] ?? '',
'cod_voce_gescon' => $op['cod_voce'] ?? '',
'cod_beneficiario_gescon' => $op['cod_ben'] ?? '',
'compet' => $op['compet_clean'],
'natura2' => $op['natura2_clean'],
'n_stra' => (int) $op['n_stra_clean'],
'protocollo_numero' => $protocollo['numero'],
'protocollo_completo' => $protocollo['completo'],
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'metadati_gescon' => json_encode($op),
$competClean = empty($op['compet']) ? 'C' : trim($op['compet']);
$natura2Clean = empty($op['natura2']) ? '' : trim($op['natura2']);
$nStraClean = empty($op['n_stra']) ? 0 : (int) $op['n_stra'];
$opId = $op['id_operaz'] ?? $op['id'] ?? null;
$dtSpe = $op['dt_spe'] ?? $op['data_oper'] ?? null;
$importo = $op['importo_euro'] ?? $op['importo'] ?? $op['euro'] ?? 0;
$codVoce = $op['cod_spe'] ?? $op['cod_voce'] ?? '';
$codBen = $op['cod_ben'] ?? '';
$isEntrata = strtolower(trim((string) ($natura2Clean ?? ''))) === 'entrata';
$amount = (float) $importo;
$dareVal = (float) ($op['importo_spese'] ?? 0);
$avereVal = (float) ($op['importo_entrate'] ?? 0);
if ($dareVal == 0 && $avereVal == 0 && $amount != 0) {
if ($isEntrata) {
$avereVal = $amount;
} else {
$dareVal = $amount;
}
}
$contoBancarioId = $this->resolveContoBancarioId($mdbPath, $op['cod_cassa'] ?? null);
DB::table('operazioni_contabili')->insert([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'legacy_id' => $opId,
'descrizione' => $op['note'] ?? $op['descrizione'] ?? $op['natura'] ?? 'Operazione',
'data_operazione' => $this->parseGesconDate($dtSpe),
'compet' => $competClean,
'natura2' => $natura2Clean,
'n_stra' => $nStraClean,
'protocollo_numero' => $protocollo['numero'],
'protocollo_completo' => $protocollo['completo'],
'voce_spesa_snapshot' => json_encode($voceSnapshot),
'dare' => $dareVal,
'avere' => $avereVal,
'conto_contabile' => $codVoce,
'conto_bancario_id' => $contoBancarioId,
'stato_operazione' => 'confermata',
'created_at' => now(),
'updated_at' => now(),
]);
$imported++;
@ -486,6 +620,63 @@ private function importOperazioni(string $year, string $mdbPath): int
return $imported;
}
/**
* Import incassi da incassi
*/
private function importIncassi(string $year, string $mdbPath): int
{
$imported = 0;
$incassi = $this->queryMdb($mdbPath, "
SELECT * FROM incassi
");
usort($incassi, function($a, $b) {
$dateA = $a['dt_empag'] ?? '';
$dateB = $b['dt_empag'] ?? '';
return strcmp($dateA, $dateB);
});
foreach ($incassi as $inc) {
try {
$gestioneId = $this->gestioni[$year]['ordinaria']->id;
Incasso::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'condominio_id' => $this->gestioni[$year]['ordinaria']->stabile_id ?? 1,
'conto_bancario_id' => $this->resolveContoBancarioId($mdbPath, $inc['cod_cassa'] ?? null),
'id_incasso_gescon' => $inc['ID_incasso'] ?? null,
'cod_cond_gescon' => $inc['cod_cond'] ?? null,
'cond_inquil' => $inc['cond_inquil'] ?? null,
'n_riferimento' => $inc['n_riferimento'] ?? null,
'anno_rif' => (int) ($inc['anno_rif'] ?? $year),
'n_ricevuta' => $inc['n_ricevuta'] ?? null,
'anno_ricev' => $inc['anno_ricev'] ?? null,
'n_mese' => $inc['n_mese'] ?? null,
'o_r_s' => $inc['o_r_s'] ?? null,
'importo_pagato' => (float) ($inc['importo_pagato'] ?? 0),
'importo_pagato_euro' => (float) ($inc['importo_pagato_euro'] ?? 0),
'dt_empag' => $this->parseGesconDate($inc['dt_empag'] ?? null),
'descrizione' => $inc['descrizione'] ?? null,
'cod_cassa_gescon' => $inc['cod_cassa'] ?? null,
'metadati_gescon' => json_encode($inc),
]);
$imported++;
} catch (\Exception $e) {
Log::error("Error importing incasso", [
'year' => $year,
'incasso' => $inc,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/**
* Import ritenute d'acconto da Nettovers_RDA
*/
@ -493,11 +684,15 @@ private function importRitenuteAcconto(string $year, string $mdbPath): int
{
$imported = 0;
if (!$this->mdbTableExists($mdbPath, 'Nettovers_RDA')) {
return 0;
}
// Query per ritenute
$ritenute = $this->queryMdb($mdbPath, "
SELECT n.*, o.data_oper, o.euro as imponibile_operazione
SELECT n.*, o.dt_spe, o.importo_euro as imponibile_operazione
FROM Nettovers_RDA n
LEFT JOIN operazioni o ON n.Rif_RDA = o.n_operazione
LEFT JOIN Operazioni o ON n.Rif_RDA = o.id_operaz
WHERE n.Rif_RDA IS NOT NULL
ORDER BY n.Rif_RDA
");
@ -510,7 +705,7 @@ private function importRitenuteAcconto(string $year, string $mdbPath): int
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'numero_progressivo' => $ra['Rif_RDA'],
'data_competenza' => $this->parseGesconDate($ra['data_oper']),
'data_competenza' => $this->parseGesconDate($ra['dt_spe'] ?? null),
'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0),
'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20),
'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0),
@ -542,23 +737,31 @@ private function importIncassiEstrattoConto(string $year, string $mdbPath): int
$incassiEc = $this->queryMdb($mdbPath, "
SELECT * FROM Inc_da_ec
ORDER BY data_oper
");
usort($incassiEc, function($a, $b) {
$protoA = (int) ($a['protocollo'] ?? 0);
$protoB = (int) ($b['protocollo'] ?? 0);
return $protoA <=> $protoB;
});
foreach ($incassiEc as $inc) {
try {
$gestioneId = $this->gestioni[$year]['ordinaria']->id;
IncassoEstrattoConto::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'id_gescon' => $inc['id'] ?? null,
'data_operazione' => $this->parseGesconDate($inc['data_oper']),
'data_valuta' => $this->parseGesconDate($inc['data_valuta']),
'importo' => (float) ($inc['importo'] ?? 0),
'descrizione' => $inc['descrizione'] ?? '',
'stato' => 'da_riconciliare',
'metadati_gescon' => json_encode($inc),
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'id_gescon' => $inc['protocollo'] ?? null,
'data_operazione' => $this->parseGesconDate($inc['Data_pag'] ?? null),
'data_valuta' => $this->parseGesconDate($inc['Data_pag'] ?? null),
'importo' => (float) ($inc['Importo'] ?? 0),
'descrizione' => trim(($inc['Descrizione_Aggiuntiva'] ?? '') . ' ' . ($inc['Nome_condomino'] ?? '')),
'ordinante' => $inc['Nome_condomino'] ?? null,
'riferimento_bancario' => $inc['Num_incasso'] ?? null,
'file_origine' => $inc['Nome_file_pdf'] ?? null,
'stato' => 'da_riconciliare',
'metadati_gescon' => json_encode($inc),
]);
$imported++;
@ -582,11 +785,21 @@ private function importRateEmesse(string $year, string $mdbPath): int
{
$imported = 0;
// Unifica EMESS_DET, EMESS_DET_2, EMESS_GEN
$tables = ['EMESS_DET', 'EMESS_DET_2', 'EMESS_GEN'];
// Unifica emes_det, emes_det_2, emes_gen
$tables = ['emes_det', 'emes_det_2', 'emes_gen'];
foreach ($tables as $table) {
$rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table} ORDER BY data_em");
if (!$this->mdbTableExists($mdbPath, $table)) {
continue;
}
$rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table}");
usort($rate, function($a, $b) {
$dateA = $a['data_em'] ?? '';
$dateB = $b['data_em'] ?? '';
return strcmp($dateA, $dateB);
});
foreach ($rate as $rata) {
try {
@ -628,18 +841,20 @@ private function determineGestioneId(string $year, array $operazione): int
private function getOrCreateStraordinaria(string $year, int $nStra): int
{
$key = "straordinaria_{$nStra}";
$calendarYear = $this->directoryToYearMap[$year] ?? (int) $year;
if (! isset($this->gestioni[$year][$key])) {
$this->gestioni[$year][$key] = GestioneContabile::create([
'tenant_id' => $this->tenantId,
'anno_gestione' => (int) $year,
'stabile_id' => $this->stabileId,
'anno_gestione' => $calendarYear,
'tipo_gestione' => 'straordinaria',
'numero_straordinaria' => $nStra,
'denominazione' => "Gestione Straordinaria {$year} - {$nStra}",
'data_inizio' => "{$year}-01-01",
'data_fine' => "{$year}-12-31",
'denominazione' => "Gestione Straordinaria {$calendarYear} - {$nStra}",
'data_inizio' => "{$calendarYear}-01-01",
'data_fine' => "{$calendarYear}-12-31",
'stato' => 'aperta',
'protocollo_prefix' => "S{$year}-{$nStra}",
'protocollo_prefix' => "S{$calendarYear}-{$nStra}",
]);
}
@ -651,11 +866,13 @@ private function getOrCreateStraordinaria(string $year, int $nStra): int
*/
private function queryMdb(string $mdbPath, string $query): array
{
$query = preg_replace('/\s+/', ' ', trim($query));
// Usa mdb-tools per query diretta
$command = sprintf(
'mdb-sql "%s" <<< "%s"',
escapeshellarg($mdbPath),
escapeshellarg($query)
'echo %s | mdb-sql -P -F -d %s %s',
escapeshellarg($query),
escapeshellarg("\t"),
escapeshellarg($mdbPath)
);
$output = shell_exec($command);
@ -664,6 +881,55 @@ private function queryMdb(string $mdbPath, string $query): array
return $this->parseMdbOutput($output);
}
private function mdbTableExists(string $mdbPath, string $tableName): bool
{
if (!file_exists($mdbPath)) {
return false;
}
$output = shell_exec("mdb-tables -1 " . escapeshellarg($mdbPath) . " 2>/dev/null");
if (!$output) {
return false;
}
$tables = array_map('strtolower', array_map('trim', explode("\n", trim($output))));
return in_array(strtolower($tableName), $tables, true);
}
private function resolveContoBancarioId(string $mdbPath, ?string $codCassa): int
{
$stabileCode = '0021'; // Default fallback
if (preg_match('/legacy\/([^\/]+)/', str_replace('\\', '/', $mdbPath), $matches)) {
$stabileCode = $matches[1];
}
$cod = strtoupper(trim($codCassa ?? ''));
if (empty($cod)) {
$cod = 'CON'; // Default fallback to cash cassa
}
// Try to match by code: stabileCode-cod
$conto = DB::table('conti_bancari')
->where('tenant_id', $this->tenantId)
->where('codice', "{$stabileCode}-{$cod}")
->first();
if ($conto) {
return (int) $conto->id;
}
// Try fallback to any account for this stable prefix
$conto = DB::table('conti_bancari')
->where('tenant_id', $this->tenantId)
->where('codice', 'like', "{$stabileCode}-%")
->first();
if ($conto) {
return (int) $conto->id;
}
// Final fallback to 1
return 1;
}
/**
* Parser output mdb-sql in array
*/
@ -675,16 +941,35 @@ private function parseMdbOutput(string $output): array
}
$headers = array_map('trim', explode("\t", array_shift($lines)));
$headerCount = count($headers);
$data = [];
$currentValues = [];
foreach ($lines as $line) {
if (empty(trim($line))) {
if ($line === '' && empty($currentValues)) {
continue;
}
$values = explode("\t", $line);
$row = array_combine($headers, $values);
$data[] = $row;
if (empty($currentValues)) {
$currentValues = $values;
} else {
$lastIdx = count($currentValues) - 1;
$currentValues[$lastIdx] .= "\n" . array_shift($values);
$currentValues = array_merge($currentValues, $values);
}
if (count($currentValues) >= $headerCount) {
$rowValues = array_slice($currentValues, 0, $headerCount);
$leftover = array_slice($currentValues, $headerCount);
try {
$data[] = array_combine($headers, $rowValues);
} catch (\Throwable $e) {
// Ignora o logga errori per evitare crash
}
$currentValues = $leftover;
}
}
return $data;
@ -696,11 +981,14 @@ private function parseMdbOutput(string $output): array
private function generateProtocollo(int $gestioneId, array $operazione): array
{
$gestione = GestioneContabile::find($gestioneId);
$numero = $gestione->getNextProtocolNumber();
$numero = (isset($operazione['n_spe']) && is_numeric($operazione['n_spe']) && (int) $operazione['n_spe'] > 0)
? (int) $operazione['n_spe']
: (DB::table('operazioni_contabili')->where('gestione_id', $gestioneId)->max('protocollo_numero') ?? 0) + 1;
return [
'numero' => $numero,
'completo' => $gestione->protocollo_prefix . '-' . str_pad($numero, 4, '0', STR_PAD_LEFT),
'completo' => ($gestione->protocollo_prefix ?? 'O') . '-' . str_pad($numero, 4, '0', STR_PAD_LEFT),
];
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('piano_rateizzazione', function (Blueprint $table) {
$table->unsignedBigInteger('ripartizione_spese_id')->nullable()->change();
$table->unsignedBigInteger('creato_da')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('piano_rateizzazione', function (Blueprint $table) {
$table->unsignedBigInteger('ripartizione_spese_id')->nullable(false)->change();
$table->unsignedBigInteger('creato_da')->nullable(false)->change();
});
}
};

View File

@ -0,0 +1,89 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$conn = 'gescon_import';
// 1. Aggiunta colonne subentro a condomin
if (Schema::connection($conn)->hasTable('condomin')) {
Schema::connection($conn)->table('condomin', function (Blueprint $table) use ($conn): void {
if (! Schema::connection($conn)->hasColumn('condomin', 'subentro_prima_cera')) {
$table->text('subentro_prima_cera')->nullable();
}
if (! Schema::connection($conn)->hasColumn('condomin', 'subentro_adesso_ce')) {
$table->text('subentro_adesso_ce')->nullable();
}
if (! Schema::connection($conn)->hasColumn('condomin', 'subentrato_dal')) {
$table->text('subentrato_dal')->nullable();
}
if (! Schema::connection($conn)->hasColumn('condomin', 'attivo_fino_al')) {
$table->text('attivo_fino_al')->nullable();
}
});
}
// 2. Tabella rate_emissioni
if (! Schema::connection($conn)->hasTable('rate_emissioni')) {
Schema::connection($conn)->create('rate_emissioni', function (Blueprint $table): void {
$table->bigIncrements('id');
$table->string('cod_stabile', 10);
$table->unsignedInteger('numero_emissione');
$table->string('anno_emissione', 20)->nullable();
$table->date('data_emissione')->nullable();
$table->date('data_scadenza')->nullable();
$table->string('gestione_tipo', 5)->nullable();
$table->string('gestione_periodo', 50)->nullable();
$table->string('descrizione', 255)->nullable();
$table->string('nota_avvisi', 255)->nullable();
$table->string('nota_ricevute', 255)->nullable();
$table->string('nota_ccp', 255)->nullable();
$table->string('stato_stampa', 20)->nullable();
$table->string('provvisorio_definitivo', 20)->nullable();
$table->json('payload')->nullable();
$table->timestamps();
$table->unique(['cod_stabile', 'numero_emissione']);
});
}
// 3. Tabella rate_emissioni_dettaglio
if (! Schema::connection($conn)->hasTable('rate_emissioni_dettaglio')) {
Schema::connection($conn)->create('rate_emissioni_dettaglio', function (Blueprint $table): void {
$table->bigIncrements('id');
$table->string('cod_stabile', 10);
$table->unsignedInteger('numero_emissione');
$table->string('anno_emissione', 20)->nullable();
$table->string('cod_cond', 20)->nullable();
$table->string('tipo_soggetto', 1)->nullable();
$table->unsignedInteger('numero_mese')->nullable();
$table->string('tipo_quota', 2)->nullable();
$table->decimal('importo_dovuto', 14, 4)->nullable();
$table->decimal('importo_dovuto_euro', 14, 4)->nullable();
$table->date('data_emissione')->nullable();
$table->string('descrizione', 255)->nullable();
$table->unsignedInteger('numero_ricevuta')->nullable();
$table->string('anno_gestione', 20)->nullable();
$table->unsignedInteger('numero_straordinaria')->nullable();
$table->decimal('gia_pagato', 14, 4)->nullable();
$table->decimal('residuo_emesso', 14, 4)->nullable();
$table->decimal('compensato', 14, 4)->nullable();
$table->string('cond_inq', 5)->nullable();
$table->string('raggruppamento', 20)->nullable();
$table->json('payload')->nullable();
$table->timestamps();
$table->index(['cod_stabile', 'numero_emissione']);
$table->index(['cod_stabile', 'cod_cond']);
});
}
}
public function down(): void
{
// Nessun down distruttivo per staging in produzione/sviluppo
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('incassi_pagamenti', function (Blueprint $table) {
if (!Schema::hasColumn('incassi_pagamenti', 'rata_id')) {
$table->unsignedBigInteger('rata_id')->nullable()->after('anno_gestione_id');
}
if (!Schema::hasColumn('incassi_pagamenti', 'movimento_banca_id')) {
$table->unsignedBigInteger('movimento_banca_id')->nullable()->after('rata_id');
}
});
}
public function down(): void
{
Schema::table('incassi_pagamenti', function (Blueprint $table) {
if (Schema::hasColumn('incassi_pagamenti', 'rata_id')) {
$table->dropColumn('rata_id');
}
if (Schema::hasColumn('incassi_pagamenti', 'movimento_banca_id')) {
$table->dropColumn('movimento_banca_id');
}
});
}
};

View File

@ -0,0 +1,75 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::connection('gescon_import')->hasTable('operazioni')) {
Schema::connection('gescon_import')->create('operazioni', function (Blueprint $table) {
$table->bigIncrements('id_operaz');
$table->integer('n_spe')->nullable();
$table->date('dt_spe')->nullable();
$table->string('cod_spe', 20)->nullable();
$table->string('tabella')->nullable();
$table->decimal('importo', 12, 2)->nullable();
$table->decimal('importo_euro', 12, 2)->nullable();
$table->string('cod_ben')->nullable();
$table->string('benef')->nullable();
$table->string('benef2')->nullable();
$table->string('compet')->nullable();
$table->text('note')->nullable();
$table->integer('n_stra')->nullable();
$table->string('cod_cassa')->nullable();
$table->string('natura')->nullable();
$table->string('natura2')->nullable();
$table->string('cod_for')->nullable();
$table->string('num_fat')->nullable();
$table->date('dt_fat')->nullable();
$table->integer('anno')->nullable();
$table->decimal('imp_calcolato', 12, 2)->nullable();
$table->decimal('imp_calcolato_euro', 12, 2)->nullable();
$table->decimal('imp_2', 12, 2)->nullable();
$table->decimal('imp_2_euro', 12, 2)->nullable();
$table->decimal('importo_spese', 12, 2)->nullable();
$table->decimal('importo_entrate', 12, 2)->nullable();
$table->decimal('importo_crediti', 12, 2)->nullable();
$table->decimal('importo_debiti', 12, 2)->nullable();
$table->boolean('incluso')->default(false);
$table->string('nord')->nullable();
$table->boolean('temporaneo')->default(false);
$table->string('d36_41')->nullable();
$table->decimal('netto_vers_rda', 12, 2)->nullable();
$table->string('rif_rda')->nullable();
$table->decimal('importo_euro_ac', 12, 2)->nullable();
$table->decimal('importo_euro_770', 12, 2)->nullable();
$table->string('file_bonifico_telematico')->nullable();
$table->decimal('detraz_36', 12, 2)->nullable();
$table->string('etic_axivar')->nullable();
$table->boolean('in_ac')->default(false);
$table->string('gestione')->nullable();
$table->string('proviene_ors')->nullable();
$table->integer('proviene_n_stra')->nullable();
$table->string('proviene_eserc')->nullable();
$table->string('rif_ft_amm')->nullable();
$table->string('fatt_amm_fc')->nullable();
$table->string('df_tipo_lavori')->nullable();
$table->string('fe_uid')->nullable();
$table->string('cod_stabile', 10)->nullable();
$table->string('legacy_year', 20)->nullable();
$table->string('slice_id', 100)->nullable();
$table->string('legacy_file', 255)->nullable();
$table->string('row_hash', 64)->nullable();
$table->timestamps();
});
}
}
public function down(): void
{
Schema::connection('gescon_import')->dropIfExists('operazioni');
}
};

View File

@ -2,14 +2,33 @@
$gestioniTab = request()->query('gestioni_tab', 'gestioni');
$gestioniTab = in_array($gestioniTab, ['gestioni', 'periodi', 'straordinarie', 'archivio'], true) ? $gestioniTab : 'gestioni';
$baseUrl = \App\Filament\Pages\Condomini\StabilePage::getUrl(panel: 'admin-filament');
$mostraRiscaldamento = (bool) data_get($stabile->configurazione_avanzata, 'mostra_riscaldamento', false);
$mesi = [
1 => 'Gen', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mag', 6 => 'Giu',
7 => 'Lug', 8 => 'Ago', 9 => 'Set', 10 => 'Ott', 11 => 'Nov', 12 => 'Dic',
];
$gestioneSelezionata = $this->gestioneSelezionataId
? \App\Models\GestioneContabile::query()->find($this->gestioneSelezionataId)
: ($this->gestioneOrdinariaId ? \App\Models\GestioneContabile::query()->find($this->gestioneOrdinariaId) : null);
$mesi = [];
if ($gestioneSelezionata && $gestioneSelezionata->data_inizio && $gestioneSelezionata->data_fine) {
$start = \Carbon\Carbon::parse($gestioneSelezionata->data_inizio);
$end = \Carbon\Carbon::parse($gestioneSelezionata->data_fine);
$curr = $start->copy()->startOfMonth();
$limit = 0;
while ($curr->lte($end) && $limit < 24) {
$mVal = (int) $curr->month;
$mName = match ($mVal) {
1 => 'Gen', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mag', 6 => 'Giu',
7 => 'Lug', 8 => 'Ago', 9 => 'Set', 10 => 'Ott', 11 => 'Nov', 12 => 'Dic',
};
$mYear = $curr->year;
$mesi[$mVal] = "{$mName} {$mYear}";
$curr->addMonth();
$limit++;
}
}
if (empty($mesi)) {
$mesi = [
1 => 'Gen', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mag', 6 => 'Giu',
7 => 'Lug', 8 => 'Ago', 9 => 'Set', 10 => 'Ott', 11 => 'Nov', 12 => 'Dic',
];
}
$gestioneTipo = $gestioneSelezionata?->tipo_gestione;
$checklistItems = \App\Models\ChecklistFineAnnoItem::query()
->where('is_active', true)

View File

@ -65,6 +65,11 @@ class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'tariffe' ? 'bo
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'servizi' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
Servizi / Utenze (tabella)
</button>
<button type="button"
wire:click="setAcquaTab('pagamenti_cbill')"
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'pagamenti_cbill' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
Pagamenti CBILL
</button>
</nav>
</div>
@ -363,6 +368,8 @@ class="rounded border-cyan-300 text-cyan-600 focus:ring-cyan-500">
</div>
</div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-4 dark:border-indigo-700/50 dark:bg-indigo-900/10">
<div class="mb-2 text-sm font-semibold text-indigo-900 dark:text-indigo-200">Archivio legacy operazioni AC1 + AC2 (collegamento fatture)</div>
@if(!empty($legacyOpsSummary['scope_note']))
@ -913,6 +920,72 @@ class="inline-flex items-center rounded-md border border-cyan-300 bg-white px-3
{{ $this->table }}
</div>
@endif
@if($acquaTab === 'pagamenti_cbill')
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900 shadow-sm">
<div class="mb-3 text-sm font-semibold text-gray-900 dark:text-gray-100">Pagamenti abbinati tramite codice CBILL</div>
<div class="mb-3 text-xs text-gray-500">I seguenti movimenti bancari sono stati associati alle fatture elettroniche dello stabile tramite il codice CBILL estratto.</div>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-800 text-xs font-semibold text-gray-500 uppercase tracking-wider">
<th class="px-3 py-2 text-left">Data movimento</th>
<th class="px-3 py-2 text-left font-mono">Codice CBILL</th>
<th class="px-3 py-2 text-left">Fornitore</th>
<th class="px-3 py-2 text-left">Fattura Elettronica di origine</th>
<th class="px-3 py-2 text-right">Totale fattura </th>
<th class="px-3 py-2 text-left">Descrizione movimento bancario</th>
<th class="px-3 py-2 text-right">Importo pagato </th>
<th class="px-3 py-2 text-center">Azioni</th>
</tr>
</thead>
<tbody>
@forelse($this->acquaCbillPagamenti as $p)
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50/50 text-xs text-gray-700 dark:text-gray-300">
<td class="px-3 py-2 whitespace-nowrap">{{ \Carbon\Carbon::parse($p['data'])->format('d/m/Y') }}</td>
<td class="px-3 py-2 font-mono text-[11px] text-gray-900 dark:text-gray-100">{{ $p['cbill'] }}</td>
<td class="px-3 py-2">{{ $p['fornitore'] }}</td>
<td class="px-3 py-2">
@if($p['fattura_numero'] !== '—')
<span class="font-semibold text-gray-900 dark:text-gray-100">N. {{ $p['fattura_numero'] }}</span> del {{ \Carbon\Carbon::parse($p['fattura_data'])->format('d/m/Y') }}
@else
@endif
</td>
<td class="px-3 py-2 text-right font-semibold tabular-nums">{{ $p['fattura_totale'] > 0 ? number_format((float) $p['fattura_totale'], 2, ',', '.') : '—' }}</td>
<td class="px-3 py-2 max-w-xs truncate" title="{{ $p['descrizione'] }}">{{ $p['descrizione'] }}</td>
<td class="px-3 py-2 text-right font-semibold text-danger-600 tabular-nums">{{ number_format((float) $p['importo'], 2, ',', '.') }}</td>
<td class="px-3 py-2 text-center whitespace-nowrap space-x-2">
@if(!empty($p['fattura_id']))
<a href="/admin-filament/contabilita/fatture-ricevute/{{ $p['fattura_id'] }}"
target="_blank"
class="inline-flex items-center gap-1 rounded bg-sky-50 px-2.5 py-1 text-xs font-semibold text-sky-700 hover:bg-sky-100 dark:bg-sky-950 dark:text-sky-300">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L17 17.25m-1.769-1.769a3.75 3.75 0 11-5.303-5.303 3.75 3.75 0 015.303 5.303z" />
</svg>
Vedi FE
</a>
@endif
<a href="/admin-filament/contabilita/casse-banche/movimenti?tab=movimenti"
target="_blank"
class="inline-flex items-center gap-1 rounded bg-teal-50 px-2.5 py-1 text-xs font-semibold text-teal-700 hover:bg-teal-100 dark:bg-teal-950 dark:text-teal-300">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m-9 6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m3-6h.007m-.007 3h.007m-.007 3h.007m-12-6a3 3 0 013-3h15a3 3 0 013 3v6a3 3 0 01-3 3H3.75a3 3 0 01-3-3v-6z" />
</svg>
Vedi Movimenti
</a>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-3 py-4 text-center text-gray-500 dark:text-gray-400">Nessun pagamento trovato abbinato tramite codice CBILL.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endif
</div>
<!-- Modal per invio email ripartizione -->

View File

@ -84,6 +84,7 @@
<x-filament::tabs.item :active="$this->hubTab === 'movimenti'" wire:click="goToHubTab('movimenti')">Movimenti</x-filament::tabs.item>
<x-filament::tabs.item :active="$this->hubTab === 'struttura'" wire:click="goToHubTab('struttura')">Struttura e gestioni</x-filament::tabs.item>
<x-filament::tabs.item :active="$this->hubTab === 'riconciliazione'" wire:click="goToHubTab('riconciliazione')">Riconciliazione</x-filament::tabs.item>
<x-filament::tabs.item :active="$this->hubTab === 'associazione_causali'" wire:click="goToHubTab('associazione_causali')">Associazione Causali</x-filament::tabs.item>
</x-filament::tabs>
@if($this->hubTab === 'conti')
@ -622,6 +623,90 @@
{{ $this->table }}
</div>
</x-filament::section>
@elseif($this->hubTab === 'associazione_causali')
<x-filament::section>
<x-slot name="heading">Associazione Causali e Regole Bancarie</x-slot>
<x-slot name="description">Configura i conti Dare e Avere per ciascun codice causale rilevato nei movimenti banca.</x-slot>
@php($assocRows = $this->getCausaliAssociazioneRows())
<div class="mt-4 overflow-x-auto rounded-lg border">
<table class="w-full text-left text-sm divide-y divide-gray-200 dark:divide-gray-800">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Causale bancaria</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Descrizione rilevata</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300 text-center">Movimenti</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Conto Dare (Entrate)</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300">Conto Avere (Uscite)</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300 text-center">Stato regola</th>
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-300 text-right">Azioni</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-800 bg-white dark:bg-gray-900">
@forelse($assocRows as $row)
<tr>
<td class="px-4 py-3 font-mono font-bold text-gray-900 dark:text-gray-100">
{{ $row['causale'] }}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{{ $row['descrizione'] }}
</td>
<td class="px-4 py-3 text-center font-semibold">
{{ $row['count'] }}
</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">
@if($row['conto_dare'])
<span class="inline-flex items-center rounded-md bg-blue-50 dark:bg-blue-900/30 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800">
{{ $row['conto_dare'] }}
</span>
@else
<span class="text-xs text-gray-400"></span>
@endif
</td>
<td class="px-4 py-3 text-gray-900 dark:text-gray-100">
@if($row['conto_avere'])
<span class="inline-flex items-center rounded-md bg-blue-50 dark:bg-blue-900/30 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800">
{{ $row['conto_avere'] }}
</span>
@else
<span class="text-xs text-gray-400"></span>
@endif
</td>
<td class="px-4 py-3 text-center">
@if($row['attiva'])
<span class="inline-flex items-center gap-1 rounded-full bg-emerald-100 dark:bg-emerald-950/30 px-2.5 py-0.5 text-xs font-semibold text-emerald-800 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-900">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
Attiva
</span>
@else
<span class="inline-flex items-center gap-1 rounded-full bg-amber-100 dark:bg-amber-950/30 px-2.5 py-0.5 text-xs font-semibold text-amber-800 dark:text-amber-300 border border-amber-200 dark:border-amber-900">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
Configurazione mancante
</span>
@endif
</td>
<td class="px-4 py-3 text-right">
<x-filament::button
size="sm"
color="{{ $row['attiva'] ? 'gray' : 'primary' }}"
wire:click="startConfiguringRegolaCausale('{{ $row['causale'] }}')"
>
{{ $row['attiva'] ? 'Modifica regola' : 'Configura regola' }}
</x-filament::button>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-6 text-center text-gray-500">
Nessun movimento banca importato con codici causale rilevati.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</x-filament::section>
@endif
@endif
</div>

View File

@ -1,13 +1,30 @@
@php
$record = $record ?? null;
$details = $record ? $record->dettaglio_estrato : [];
@endphp
@if(!$record)
<div class="text-sm text-gray-600">Nessun movimento selezionato.</div>
<div class="text-sm text-gray-600 dark:text-gray-400 p-4 text-center">Nessun movimento selezionato.</div>
@else
<div class="space-y-3">
<div class="flex items-center justify-between">
<div class="text-xs text-gray-500">ID #{{ $record->id }}</div>
<div class="space-y-4">
<!-- Header con Azioni di Navigazione e Codici Chiave -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b pb-3 dark:border-gray-700">
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-slate-100 text-slate-800 border border-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:border-slate-700">
ID #{{ $record->id }}
</span>
@if(!empty($details['abi']))
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-blue-50 text-blue-700 border border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800">
ABI: {{ $details['abi'] }}
</span>
@endif
@if(!empty($details['cbill_code']))
<span class="text-xs font-semibold px-2.5 py-1 rounded-full bg-amber-50 text-amber-700 border border-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-800">
CBILL: {{ $details['cbill_code'] }}
</span>
@endif
</div>
<div class="flex gap-2">
@if(!empty($prevId))
<x-filament::button size="xs" color="gray" wire:click="setDetailMovement({{ (int) $prevId }})"> Prec</x-filament::button>
@ -18,76 +35,83 @@
</div>
</div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Data</div>
<div class="font-semibold">{{ optional($record->data)->format('d/m/Y') ?: '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Valuta</div>
<div class="font-semibold">{{ optional($record->valuta)->format('d/m/Y') ?: '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Importo</div>
<div class="font-semibold"> {{ number_format((float) $record->importo, 2, ',', '.') }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Causale</div>
<div class="font-semibold">{{ $record->causale ?? '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Descrizione</div>
<div class="font-semibold">{{ $record->descrizione ?? '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Descrizione estesa</div>
<div class="font-semibold">{{ $record->descrizione_estesa_pulita ?? $record->descrizione_estesa ?? '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Saldo progressivo</div>
<div class="font-semibold">
@if(isset($record->saldo_progressivo) && is_numeric($record->saldo_progressivo))
{{ number_format((float) $record->saldo_progressivo, 2, ',', '.') }}
@else
@endif
<!-- Sezione Dettagli Principali (Grid) -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Colonna 1: Date ed Importo -->
<div class="space-y-3 bg-slate-50 dark:bg-gray-800/40 p-4 rounded-xl border border-slate-100 dark:border-gray-700">
<div>
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Importo</span>
<span class="text-xl font-bold text-emerald-600 dark:text-emerald-400">
{{ number_format((float) $record->importo, 2, ',', '.') }}
</span>
</div>
<div class="grid grid-cols-2 gap-2 pt-2 border-t border-slate-200/60 dark:border-gray-700/60">
<div>
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Data Operazione</span>
<span class="text-xs font-semibold text-slate-700 dark:text-slate-300">
{{ optional($record->data)->format('d/m/Y') ?: '—' }}
</span>
</div>
<div>
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Data Valuta</span>
<span class="text-xs font-semibold text-slate-700 dark:text-slate-300">
{{ optional($record->valuta)->format('d/m/Y') ?: '—' }}
</span>
</div>
</div>
<div class="pt-2 border-t border-slate-200/60 dark:border-gray-700/60">
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Causale</span>
<span class="text-xs font-semibold text-slate-700 dark:text-slate-300">
{{ $record->causale ?? '—' }}
</span>
</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">File sorgente</div>
<div class="font-semibold">{{ $record->source_file ?? '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Disp</div>
<div class="font-semibold">{{ is_array($record->match_data ?? null) ? ($record->match_data['cod_disp'] ?? '—') : '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Cash</div>
<div class="font-semibold">{{ is_array($record->match_data ?? null) ? ($record->match_data['cash'] ?? '—') : '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500">Mittente</div>
<div class="font-semibold">{{ is_array($record->match_data ?? null) ? ($record->match_data['mittente'] ?? '—') : '—' }}</div>
</div>
<div class="rounded-lg border bg-white p-3 text-xs md:col-span-2">
<div class="text-gray-500">Riga raw importata</div>
<div class="font-semibold break-words">{{ $record->raw_line ?? '—' }}</div>
<!-- Colonna 2 e 3: Messaggi ed Estrazioni -->
<div class="md:col-span-2 space-y-3 bg-white dark:bg-gray-800/20 p-4 rounded-xl border border-slate-100 dark:border-gray-700">
<!-- Messaggio Banca (Standard) -->
<div>
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Messaggio Standard Banca</span>
<div class="text-xs font-semibold text-slate-500 dark:text-slate-400 bg-slate-50/50 dark:bg-gray-800/40 p-2.5 rounded-lg border border-slate-100 dark:border-gray-700 mt-1 font-mono">
{{ $details['messaggio_banca'] }}
</div>
</div>
<!-- Messaggio Cliente (Dati Riferimento) -->
<div>
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Riferimento / Dettaglio Cliente</span>
<div class="text-sm font-bold text-slate-800 dark:text-slate-200 bg-blue-50/20 dark:bg-blue-900/10 p-2.5 rounded-lg border border-blue-100/30 dark:border-blue-900/30 mt-1">
{{ $details['messaggio_cliente'] }}
</div>
</div>
<!-- Commissioni / Spese -->
<div>
<span class="block text-xxs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Commissioni, Spese e Codice TRN</span>
<div class="text-xs font-medium text-slate-600 dark:text-slate-400 mt-1 bg-slate-50/50 dark:bg-gray-800/40 p-2.5 rounded-lg border border-slate-100 dark:border-gray-700 font-mono">
{{ $details['commissioni_spese'] }}
</div>
</div>
</div>
</div>
@php
$match = $record->match_data;
@endphp
@if(is_array($match) && !empty($match))
<div class="rounded-lg border bg-white p-3 text-xs">
<div class="text-gray-500 mb-2">Dati estratti</div>
<ul class="list-disc pl-4 space-y-1">
@foreach($match as $k => $v)
<li><span class="font-semibold">{{ $k }}:</span> {{ is_scalar($v) ? $v : json_encode($v) }}</li>
@endforeach
</ul>
</div>
@endif
<!-- Dati Aggiuntivi / Raw Line -->
<div class="bg-slate-50/50 dark:bg-gray-800/10 p-3 rounded-lg border border-slate-100 dark:border-gray-700/60 text-xs">
<details class="group">
<summary class="flex justify-between items-center font-semibold text-slate-500 dark:text-slate-400 cursor-pointer select-none">
<span>Visualizza Tracciato Originale (Raw Line)</span>
<span class="transition group-open:rotate-180">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</span>
</summary>
<div class="mt-2.5 pt-2 border-t border-slate-200/60 dark:border-gray-700/60 text-slate-600 dark:text-slate-400 font-mono break-all leading-relaxed">
{{ $record->raw_line ?? '—' }}
</div>
</details>
</div>
</div>
@endif

View File

@ -8,14 +8,55 @@
]"
/>
<x-filament::section>
<form wire:submit.prevent="submit" class="space-y-4">
{{ $this->form }}
<div class="flex items-center gap-3">
<x-filament::button type="submit" icon="heroicon-o-check">Salva</x-filament::button>
<x-filament::button color="gray" href="{{ \App\Filament\Pages\Contabilita\PrimaNotaArchivio::getUrl(panel: 'admin-filament') }}" tag="a">Annulla</x-filament::button>
</div>
</form>
</x-filament::section>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-12">
<div class="lg:col-span-9 space-y-4">
<form wire:submit.prevent="submit" id="prima-nota-form" class="space-y-4">
{{ $this->form }}
</form>
@php($hdr = $this->getHeaderSummary())
@php($delta = (float) ($hdr['delta'] ?? 0))
@php($isBilanciata = (bool) ($hdr['is_bilanciata'] ?? false))
<x-filament::section>
<x-slot name="heading">Totali e Quadratura</x-slot>
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
<div class="rounded-lg border bg-gray-50 p-3">
<div class="text-xs text-gray-500">Totale Dare</div>
<div class="mt-1 text-lg font-semibold"> {{ number_format((float) ($hdr['totale_dare'] ?? 0), 2, ',', '.') }}</div>
</div>
<div class="rounded-lg border bg-gray-50 p-3">
<div class="text-xs text-gray-500">Totale Avere</div>
<div class="mt-1 text-lg font-semibold"> {{ number_format((float) ($hdr['totale_avere'] ?? 0), 2, ',', '.') }}</div>
</div>
<div class="rounded-lg border p-3 {{ $isBilanciata ? 'bg-success-50 border-success-200' : 'bg-danger-50 border-danger-200' }}">
<div class="text-xs text-gray-500">Quadratura (delta)</div>
<div class="mt-1 text-lg font-semibold {{ $isBilanciata ? 'text-success-700' : 'text-danger-700' }}"> {{ number_format($delta, 2, ',', '.') }}</div>
<div class="text-xs text-gray-500">
@if($isBilanciata)
Registrazione bilanciata
@else
Sbilancio: {{ number_format(abs($delta), 2, ',', '.') }}
@endif
</div>
</div>
</div>
</x-filament::section>
</div>
<div class="lg:col-span-3 space-y-4 lg:sticky lg:top-4 lg:self-start">
<x-filament::section>
<x-slot name="heading">Funzioni base</x-slot>
<div class="space-y-2">
<x-filament::button form="prima-nota-form" type="submit" color="primary" class="w-full" icon="heroicon-o-check" :disabled="!$isBilanciata">
Memorizza
</x-filament::button>
<x-filament::button color="gray" tag="a" href="{{ \App\Filament\Pages\Contabilita\PrimaNotaArchivio::getUrl(panel: 'admin-filament') }}" class="w-full">
Annulla
</x-filament::button>
</div>
</x-filament::section>
</div>
</div>
</div>
</x-filament-panels::page>

View File

@ -6,6 +6,28 @@
</div>
<div class="bg-warning-50 px-4 py-4 space-y-2">
@if(($this->tipoGestioneTab ?? 'ordinaria') === 'acqua')
<div class="mb-4 p-4 rounded-lg bg-blue-50 border border-blue-200 flex flex-col md:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-3">
<svg class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 111.086 1.086L13 13.75l.04.02a.75.75 0 11-1.08 1.08l-.71-.71a.75.75 0 010-1.06l.71-.71z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v.008H12v-.008zm0-6.75C6.885 2.25 2.25 6.885 2.25 12s4.635 9.75 9.75 9.75 9.75-4.635 9.75-9.75S17.115 2.25 12 2.25z" />
</svg>
<div>
<div class="text-sm font-semibold text-blue-900">Configurazione e Letture Acqua</div>
<div class="text-xs text-blue-700">Le tariffe, i consumi e le letture dell'acqua sono gestite nella sezione Servizi e Utenze dello Stabile.</div>
</div>
</div>
<x-filament::button
color="info"
tag="a"
href="/admin-filament/condomini/servizi-utenze"
icon="heroicon-m-arrow-right"
>
Vai a Servizi / Utenze Acqua
</x-filament::button>
</div>
@endif
<div class="flex flex-wrap items-center justify-between gap-3">
@php
$tabs = [