feat(fe-nominativi-adminer): normalizzazione nominativi canonici, scansione FE con matching CF e console Adminer
This commit is contained in:
parent
5f08c1bb0a
commit
f137efd0d2
292
app/Console/Commands/ScanAndImportFeFilesCommand.php
Normal file
292
app/Console/Commands/ScanAndImportFeFilesCommand.php
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\FatturaElettronica;
|
||||||
|
use App\Models\OperazioneContabile;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
||||||
|
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
|
||||||
|
use App\Services\FattureElettroniche\P7mExtractor;
|
||||||
|
use App\Services\TenantArchivePathService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
use SplFileInfo;
|
||||||
|
|
||||||
|
class ScanAndImportFeFilesCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'gescon:scan-import-fe
|
||||||
|
{--dir= : Directory specifica da scansionare}
|
||||||
|
{--stabile= : Filtra per codice stabile (es. 0013, 0021)}
|
||||||
|
{--dry-run : Esegui solo simulazione senza copiare né scrivere su DB}';
|
||||||
|
|
||||||
|
protected $description = 'Scansiona l\'intero archivio di backup, assegna le FE agli stabili tramite Codice Fiscale, copia i file nelle rispettive cartelle e importa nel DB';
|
||||||
|
|
||||||
|
public function handle(
|
||||||
|
FatturaElettronicaXmlParser $parser,
|
||||||
|
FatturaElettronicaImporter $importer,
|
||||||
|
P7mExtractor $p7mExtractor,
|
||||||
|
TenantArchivePathService $pathService
|
||||||
|
): int {
|
||||||
|
$this->info('======================================================================');
|
||||||
|
$this->info(' NETGESCON: SCANSIONE, MATCHING FISCALE & IMPORTAZIONE FATTURE FE ');
|
||||||
|
$this->info('======================================================================');
|
||||||
|
|
||||||
|
$isDryRun = (bool) $this->option('dry-run');
|
||||||
|
$filterStabile = $this->option('stabile');
|
||||||
|
$customDir = $this->option('dir');
|
||||||
|
|
||||||
|
// 1. Carica gli stabili canonici con Codice Fiscale
|
||||||
|
$stabiliQuery = Stabile::whereNotNull('codice_fiscale')->where('codice_fiscale', '<>', '');
|
||||||
|
if ($filterStabile) {
|
||||||
|
$stabiliQuery->where('codice_stabile', $filterStabile);
|
||||||
|
}
|
||||||
|
$stabili = $stabiliQuery->get();
|
||||||
|
|
||||||
|
if ($stabili->isEmpty()) {
|
||||||
|
$this->error('Nessuno stabile canonico con Codice Fiscale configurato trovato.');
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfMap = [];
|
||||||
|
foreach ($stabili as $s) {
|
||||||
|
$cleanCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) $s->codice_fiscale));
|
||||||
|
if ($cleanCf !== '') {
|
||||||
|
$cfMap[$cleanCf] = $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Stabili target attivi: " . count($cfMap));
|
||||||
|
foreach ($cfMap as $cf => $s) {
|
||||||
|
$this->line(" - Stabile [{$s->codice_stabile}] {$s->denominazione} (CF: {$cf})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Determina le directory sorgente da scansionare
|
||||||
|
$scanRoots = [];
|
||||||
|
if ($customDir) {
|
||||||
|
if (! is_dir($customDir)) {
|
||||||
|
$this->error("Directory non trovata: {$customDir}");
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
$scanRoots[] = realpath($customDir);
|
||||||
|
} else {
|
||||||
|
$basePath = base_path();
|
||||||
|
$candidates = [
|
||||||
|
$basePath . '/_legacy-vault',
|
||||||
|
$basePath . '/Miki-Bug-workspace',
|
||||||
|
$basePath . '/storage/gescon',
|
||||||
|
$basePath . '/storage/app/amministratori',
|
||||||
|
$basePath . '/storage/app/private/amministratori',
|
||||||
|
$basePath . '/storage/app/public',
|
||||||
|
];
|
||||||
|
foreach ($candidates as $c) {
|
||||||
|
if (is_dir($c)) {
|
||||||
|
$scanRoots[] = realpath($c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("\nCartelle sorgente incluse nella scansione: " . count($scanRoots));
|
||||||
|
|
||||||
|
// 3. Raccogli tutti i file .xml e .p7m
|
||||||
|
$foundFiles = [];
|
||||||
|
$excludedFolders = ['/vendor/', '/node_modules/', '/.git/', '/.gemini/', '/storage/framework/', '/bootstrap/cache/'];
|
||||||
|
|
||||||
|
foreach ($scanRoots as $root) {
|
||||||
|
$this->line(" Scansione: {$root}...");
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var SplFileInfo $file */
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if (! $file->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$path = $file->getRealPath();
|
||||||
|
$lowerPath = strtolower($path);
|
||||||
|
|
||||||
|
$skip = false;
|
||||||
|
foreach ($excludedFolders as $ex) {
|
||||||
|
if (str_contains($lowerPath, $ex)) {
|
||||||
|
$skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($skip) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_ends_with($lowerPath, '.xml') || str_ends_with($lowerPath, '.p7m')) {
|
||||||
|
$foundFiles[] = $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$foundFiles = array_values(array_unique($foundFiles));
|
||||||
|
$totalFiles = count($foundFiles);
|
||||||
|
$this->info("Totale file XML/P7M individuati: {$totalFiles}\n");
|
||||||
|
|
||||||
|
if ($totalFiles === 0) {
|
||||||
|
$this->warn('Nessun file XML o P7M trovato.');
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Scansione e parsing semantico di ciascun file
|
||||||
|
$stats = [
|
||||||
|
'scanned' => 0,
|
||||||
|
'matched' => 0,
|
||||||
|
'unmatched' => 0,
|
||||||
|
'copied' => 0,
|
||||||
|
'imported' => 0,
|
||||||
|
'duplicates' => 0,
|
||||||
|
'errors' => 0,
|
||||||
|
'by_stabile' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$progressBar = $this->output->createProgressBar($totalFiles);
|
||||||
|
$progressBar->start();
|
||||||
|
|
||||||
|
foreach ($foundFiles as $filePath) {
|
||||||
|
$stats['scanned']++;
|
||||||
|
$progressBar->advance();
|
||||||
|
|
||||||
|
$lowerPath = strtolower($filePath);
|
||||||
|
$filename = basename($filePath);
|
||||||
|
$isP7m = str_ends_with($lowerPath, '.p7m');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($isP7m) {
|
||||||
|
$xml = $p7mExtractor->extractXmlFromP7m($filePath);
|
||||||
|
} else {
|
||||||
|
$xml = file_get_contents($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($xml) || trim($xml) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $parser->parse($xml);
|
||||||
|
$destCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_cf'] ?? '')));
|
||||||
|
$destPiva = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_piva'] ?? '')));
|
||||||
|
|
||||||
|
$matchedStabile = $cfMap[$destCf] ?? ($cfMap[$destPiva] ?? null);
|
||||||
|
|
||||||
|
if (! $matchedStabile) {
|
||||||
|
$stats['unmatched']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats['matched']++;
|
||||||
|
$codStabile = $matchedStabile->codice_stabile;
|
||||||
|
$stats['by_stabile'][$codStabile] = ($stats['by_stabile'][$codStabile] ?? 0) + 1;
|
||||||
|
|
||||||
|
if ($isDryRun) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determina la cartella di destinazione corretta
|
||||||
|
$inboxAbsolute = $pathService->stabileAbsolutePath($matchedStabile, 'fatture_elettroniche/inbox');
|
||||||
|
if (! is_dir($inboxAbsolute)) {
|
||||||
|
@mkdir($inboxAbsolute, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$destinationFile = $inboxAbsolute . '/' . $filename;
|
||||||
|
if (! file_exists($destinationFile) && realpath($filePath) !== realpath($destinationFile)) {
|
||||||
|
// Copia preservando sorgente intatta
|
||||||
|
@copy($filePath, $destinationFile);
|
||||||
|
$stats['copied']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Importazione nel DB NetGescon
|
||||||
|
$importRes = $importer->importXml(
|
||||||
|
$xml,
|
||||||
|
(int) $matchedStabile->id,
|
||||||
|
$filename,
|
||||||
|
[
|
||||||
|
'force_stabile_id' => (int) $matchedStabile->id,
|
||||||
|
'create_fornitore_if_missing'=> true,
|
||||||
|
'auto_repair_duplicate' => true,
|
||||||
|
'p7m_path' => $isP7m ? $pathService->stabileRelativePath($matchedStabile, 'fatture_elettroniche/inbox/' . $filename) : null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($importRes['status'] === 'imported') {
|
||||||
|
$stats['imported']++;
|
||||||
|
} elseif ($importRes['status'] === 'duplicate') {
|
||||||
|
$stats['duplicates']++;
|
||||||
|
} else {
|
||||||
|
$stats['errors']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$stats['errors']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$progressBar->finish();
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
// 5. Riconciliazione automatica con operazioni_contabili (num_fat, dt_fat)
|
||||||
|
$this->info("Riconciliazione automatica FE con operazioni contabili e registri...");
|
||||||
|
$reconciledOps = 0;
|
||||||
|
foreach ($stabili as $stabile) {
|
||||||
|
$gestioneIds = \App\Models\GestioneContabile::where('stabile_id', $stabile->id)->pluck('id');
|
||||||
|
if ($gestioneIds->isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$feRows = FatturaElettronica::where('stabile_id', $stabile->id)->get();
|
||||||
|
foreach ($feRows as $fe) {
|
||||||
|
$numFat = trim((string) $fe->numero_fattura);
|
||||||
|
$dtFat = $fe->data_fattura ? Carbon::parse($fe->data_fattura)->format('Y-m-d') : null;
|
||||||
|
if ($numFat === '' || ! $dtFat) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = OperazioneContabile::whereIn('gestione_id', $gestioneIds)
|
||||||
|
->whereNull('fe_fattura_id')
|
||||||
|
->where('num_fat', $numFat)
|
||||||
|
->whereDate('dt_fat', $dtFat)
|
||||||
|
->update(['fe_fattura_id' => $fe->id]);
|
||||||
|
|
||||||
|
if ($updated > 0) {
|
||||||
|
$reconciledOps += $updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->info("Operazioni contabili collegate a Fattura Elettronica ID: {$reconciledOps}");
|
||||||
|
|
||||||
|
// 6. Riepilogo finale
|
||||||
|
$this->info('======================================================================');
|
||||||
|
$this->info(' RIEPILOGO SCANSIONE FE ');
|
||||||
|
$this->info('======================================================================');
|
||||||
|
$this->table(
|
||||||
|
['Metrica', 'Valore'],
|
||||||
|
[
|
||||||
|
['File scansionati', $stats['scanned']],
|
||||||
|
['File associati a Stabili (Match CF)', $stats['matched']],
|
||||||
|
['File senza riscontro CF stabili', $stats['unmatched']],
|
||||||
|
['File copiati nelle cartelle stabili', $stats['copied']],
|
||||||
|
['Fatture importate a nuovo su DB', $stats['imported']],
|
||||||
|
['Fatture già presenti / aggiornate', $stats['duplicates']],
|
||||||
|
['Errori di lettura/parsing', $stats['errors']],
|
||||||
|
['Operazioni contabili riconciliate a FE', $reconciledOps],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->info("\nDistribuzione fatture per Stabile:");
|
||||||
|
foreach ($stats['by_stabile'] as $cod => $count) {
|
||||||
|
$stabileObj = $stabili->firstWhere('codice_stabile', $cod);
|
||||||
|
$denom = $stabileObj ? $stabileObj->denominazione : '';
|
||||||
|
$this->line(" - Stabile [{$cod}] {$denom}: {$count} fatture");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -470,34 +470,91 @@ protected function getTableQuery(): Builder
|
||||||
|
|
||||||
protected function buildDomainConsolidatedQuery(int $stabileId): Builder
|
protected function buildDomainConsolidatedQuery(int $stabileId): Builder
|
||||||
{
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$activeAnno = $user instanceof \App\Models\User ? \App\Support\AnnoGestioneContext::resolveActiveAnno($user) : (int) date('Y');
|
||||||
|
$codStabile = $this->resolveLegacyStabileCode($stabileId);
|
||||||
|
|
||||||
|
$nameExpr = "CASE WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) END";
|
||||||
|
|
||||||
|
$isSqlite = DB::connection()->getDriverName() === 'sqlite';
|
||||||
|
$groupConcatOwner = $isSqlite
|
||||||
|
? "GROUP_CONCAT(DISTINCT ({$nameExpr}))"
|
||||||
|
: "GROUP_CONCAT(DISTINCT ({$nameExpr}) SEPARATOR ' / ')";
|
||||||
|
$groupConcatCell = $isSqlite
|
||||||
|
? "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, ''))"
|
||||||
|
: "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')";
|
||||||
|
$groupConcatCf = $isSqlite
|
||||||
|
? "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, ''))"
|
||||||
|
: "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')";
|
||||||
|
|
||||||
$ownerNameSub = DB::table('persone_unita_relazioni as pur')
|
$ownerNameSub = DB::table('persone_unita_relazioni as pur')
|
||||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
|
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto'])
|
||||||
->where('pur.attivo', true)
|
->where('pur.attivo', true)
|
||||||
->selectRaw("GROUP_CONCAT(DISTINCT TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, ''))) SEPARATOR ' / ')");
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_fine')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_fine', '');
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_inizio')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_inizio', '');
|
||||||
|
})
|
||||||
|
->selectRaw($groupConcatOwner);
|
||||||
|
|
||||||
$tenantNameSub = DB::table('persone_unita_relazioni as pur')
|
$tenantNameSub = DB::table('persone_unita_relazioni as pur')
|
||||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||||
->where('pur.tipo_relazione', 'inquilino')
|
->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario'])
|
||||||
->where('pur.attivo', true)
|
->where('pur.attivo', true)
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_fine')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_fine', '');
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_inizio')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_inizio', '');
|
||||||
|
})
|
||||||
->orderByDesc('pur.id')
|
->orderByDesc('pur.id')
|
||||||
->selectRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, '')))")
|
->selectRaw("{$nameExpr}")
|
||||||
->limit(1);
|
->limit(1);
|
||||||
|
|
||||||
$ownerCellSub = DB::table('persone_unita_relazioni as pur')
|
$ownerCellSub = DB::table('persone_unita_relazioni as pur')
|
||||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
|
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto'])
|
||||||
->where('pur.attivo', true)
|
->where('pur.attivo', true)
|
||||||
->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')");
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_fine')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_fine', '');
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_inizio')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_inizio', '');
|
||||||
|
})
|
||||||
|
->selectRaw($groupConcatCell);
|
||||||
|
|
||||||
$tenantCellSub = DB::table('persone_unita_relazioni as pur')
|
$tenantCellSub = DB::table('persone_unita_relazioni as pur')
|
||||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||||
->where('pur.tipo_relazione', 'inquilino')
|
->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario'])
|
||||||
->where('pur.attivo', true)
|
->where('pur.attivo', true)
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_fine')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_fine', '');
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_inizio')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_inizio', '');
|
||||||
|
})
|
||||||
->orderByDesc('pur.id')
|
->orderByDesc('pur.id')
|
||||||
->selectRaw("COALESCE(p.telefono_principale, '')")
|
->selectRaw("COALESCE(p.telefono_principale, '')")
|
||||||
->limit(1);
|
->limit(1);
|
||||||
|
|
@ -505,20 +562,40 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
|
||||||
$ownerCfSub = DB::table('persone_unita_relazioni as pur')
|
$ownerCfSub = DB::table('persone_unita_relazioni as pur')
|
||||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||||
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
|
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'condomino', 'usufrutto'])
|
||||||
->where('pur.attivo', true)
|
->where('pur.attivo', true)
|
||||||
->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')");
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_fine')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_fine', '');
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_inizio')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_inizio', '');
|
||||||
|
})
|
||||||
|
->selectRaw($groupConcatCf);
|
||||||
|
|
||||||
$tenantCfSub = DB::table('persone_unita_relazioni as pur')
|
$tenantCfSub = DB::table('persone_unita_relazioni as pur')
|
||||||
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
->join('persone as p', 'p.id', '=', 'pur.persona_id')
|
||||||
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
|
||||||
->where('pur.tipo_relazione', 'inquilino')
|
->whereIn('pur.tipo_relazione', ['inquilino', 'conduttore', 'locatario'])
|
||||||
->where('pur.attivo', true)
|
->where('pur.attivo', true)
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_fine')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_fine, 1, 4) >= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_fine', '');
|
||||||
|
})
|
||||||
|
->where(function ($q) use ($activeAnno) {
|
||||||
|
$q->whereNull('pur.data_inizio')
|
||||||
|
->orWhereRaw("SUBSTR(pur.data_inizio, 1, 4) <= ?", [(string) $activeAnno])
|
||||||
|
->orWhere('pur.data_inizio', '');
|
||||||
|
})
|
||||||
->orderByDesc('pur.id')
|
->orderByDesc('pur.id')
|
||||||
->selectRaw("COALESCE(p.codice_fiscale, '')")
|
->selectRaw("COALESCE(p.codice_fiscale, '')")
|
||||||
->limit(1);
|
->limit(1);
|
||||||
|
|
||||||
return UnitaImmobiliare::query()
|
$orderQuery = UnitaImmobiliare::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
->whereNull('deleted_at')
|
->whereNull('deleted_at')
|
||||||
->when($this->cumulato, function (Builder $query) use ($stabileId): void {
|
->when($this->cumulato, function (Builder $query) use ($stabileId): void {
|
||||||
|
|
@ -546,8 +623,15 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
|
||||||
->selectSub($ownerCfSub, 'cond_cod_fisc')
|
->selectSub($ownerCfSub, 'cond_cod_fisc')
|
||||||
->selectSub($tenantCfSub, 'inquil_cod_fisc')
|
->selectSub($tenantCfSub, 'inquil_cod_fisc')
|
||||||
->orderBy('unita_immobiliari.scala')
|
->orderBy('unita_immobiliari.scala')
|
||||||
->orderByRaw("CASE WHEN unita_immobiliari.interno IS NULL OR unita_immobiliari.interno = '' THEN 1 ELSE 0 END")
|
->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")
|
|
||||||
|
if ($isSqlite) {
|
||||||
|
$orderQuery->orderByRaw("CASE WHEN unita_immobiliari.interno GLOB '[0-9]*' THEN CAST(unita_immobiliari.interno AS INTEGER) ELSE 999999 END");
|
||||||
|
} else {
|
||||||
|
$orderQuery->orderByRaw("CASE WHEN unita_immobiliari.interno REGEXP '^[0-9]+' THEN CAST(unita_immobiliari.interno AS UNSIGNED) ELSE 999999 END");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $orderQuery
|
||||||
->orderBy('unita_immobiliari.interno')
|
->orderBy('unita_immobiliari.interno')
|
||||||
->orderBy('unita_immobiliari.id');
|
->orderBy('unita_immobiliari.id');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ public static function canAccess(): bool
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -807,7 +807,7 @@ public static function canAccess(): bool
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ public function extractXmlFromP7m(string $p7mPath): string
|
||||||
throw new RuntimeException("File non trovato: {$p7mPath}");
|
throw new RuntimeException("File non trovato: {$p7mPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Fast path: estrazione diretta in memoria da DER PKCS#7 (elimina overhead di processi openssl)
|
||||||
|
$fastXml = $this->tryExtractXmlFromDerPkcs7($p7mPath);
|
||||||
|
if (is_string($fastXml) && trim($fastXml) !== '') {
|
||||||
|
return $fastXml;
|
||||||
|
}
|
||||||
|
|
||||||
$tmpOut = tempnam(sys_get_temp_dir(), 'fatturapa_');
|
$tmpOut = tempnam(sys_get_temp_dir(), 'fatturapa_');
|
||||||
if (! is_string($tmpOut) || $tmpOut === '') {
|
if (! is_string($tmpOut) || $tmpOut === '') {
|
||||||
throw new RuntimeException('Impossibile creare file temporaneo.');
|
throw new RuntimeException('Impossibile creare file temporaneo.');
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* NetGescon PostgreSQL & MySQL Secure Web Database Visualizer
|
* NetGescon Database Visualizer (Adminer Wrapper)
|
||||||
*
|
* Direct access to MySQL (256 tables) and PostgreSQL databases
|
||||||
* SECURE WRAPPER FOR ADMINER
|
|
||||||
* - Disabled in Production (APP_ENV=production or APP_DEBUG=false)
|
|
||||||
* - Supports 1-Click Instant Auto-Login via ?auto=mysql or ?auto=pgsql
|
|
||||||
* - Auto-connects via TCP (127.0.0.1) to avoid Unix Socket Peer Auth failures
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 1. ENVIRONMENT SECURITY GUARD
|
// 1. ENVIRONMENT SECURITY GUARD
|
||||||
|
|
@ -28,11 +24,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block access in production environments for security
|
|
||||||
if ($appEnv === 'production' || !$appDebug) {
|
if ($appEnv === 'production' || !$appDebug) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
header('Content-Type: text/plain; charset=UTF-8');
|
header('Content-Type: text/plain; charset=UTF-8');
|
||||||
die("403 Forbidden: Database management console is disabled in production mode for security compliance.\n");
|
die("403 Forbidden: Database management console is disabled in production mode.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function env_get_val($key, $default = '') {
|
function env_get_val($key, $default = '') {
|
||||||
|
|
@ -49,80 +44,44 @@ function env_get_val($key, $default = '') {
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. AUTO-LOGIN & DATABASE SELECTION INJECTION
|
// 2. SESSION SEEDING & AUTO-REDIRECT
|
||||||
$dbName = env_get_val('DB_DATABASE', 'netgescon');
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbName = env_get_val('DB_DATABASE', 'netgescon');
|
||||||
$mySqlUser = env_get_val('DB_USERNAME', 'netgescon_user');
|
$mySqlUser = env_get_val('DB_USERNAME', 'netgescon_user');
|
||||||
$mySqlPass = env_get_val('DB_PASSWORD', 'NetGescon2024!');
|
$mySqlPass = env_get_val('DB_PASSWORD', 'NetGescon2024!');
|
||||||
|
|
||||||
if (isset($_GET['auto'])) {
|
// Pre-authorize session credentials for Adminer
|
||||||
if ($_GET['auto'] === 'pgsql') {
|
$_SESSION["pwds"]["server"]["127.0.0.1"][$mySqlUser] = $mySqlPass;
|
||||||
$_GET['pgsql'] = '127.0.0.1';
|
$_SESSION["pwds"]["server"]["localhost"][$mySqlUser] = $mySqlPass;
|
||||||
$_GET['username'] = 'netgescon';
|
$_SESSION["pwds"]["pgsql"]["127.0.0.1"]["netgescon"] = "netgescon_pass";
|
||||||
$_GET['db'] = $dbName;
|
$_SESSION["pwds"]["pgsql"]["localhost"]["netgescon"] = "netgescon_pass";
|
||||||
$_POST['auth'] = [
|
|
||||||
'driver' => 'pgsql',
|
if (empty($_GET) || (isset($_GET['auto']) && $_GET['auto'] === 'mysql')) {
|
||||||
'server' => '127.0.0.1',
|
header("Location: adminer.php?server=127.0.0.1&username=" . urlencode($mySqlUser) . "&db=" . urlencode($dbName));
|
||||||
'username' => 'netgescon',
|
exit;
|
||||||
'password' => 'netgescon_pass',
|
|
||||||
'db' => $dbName,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$_GET['server'] = '127.0.0.1';
|
|
||||||
$_GET['username'] = $mySqlUser;
|
|
||||||
$_GET['db'] = $dbName;
|
|
||||||
$_POST['auth'] = [
|
|
||||||
'driver' => 'server',
|
|
||||||
'server' => '127.0.0.1',
|
|
||||||
'username' => $mySqlUser,
|
|
||||||
'password' => $mySqlPass,
|
|
||||||
'db' => $dbName,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. ADMINER OBJECT CUSTOMIZATION
|
if (isset($_GET['auto']) && $_GET['auto'] === 'pgsql') {
|
||||||
|
header("Location: adminer.php?pgsql=127.0.0.1&username=netgescon&db=" . urlencode($dbName));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. ADMINER PLUGIN/CUSTOMIZATION
|
||||||
function adminer_object() {
|
function adminer_object() {
|
||||||
class NetGesconAdminerSecurity extends \Adminer\Adminer {
|
class NetGesconAdminerSecurity extends \Adminer\Adminer {
|
||||||
function name() {
|
function name() {
|
||||||
return "NetGescon Database Visualizer";
|
return "NetGescon Database Visualizer";
|
||||||
}
|
}
|
||||||
|
|
||||||
function credentials() {
|
|
||||||
$driver = $_POST["auth"]["driver"] ?? $_GET["driver"] ?? (isset($_GET["pgsql"]) ? "pgsql" : "server");
|
|
||||||
$dbHost = "127.0.0.1";
|
|
||||||
|
|
||||||
if ($driver === "pgsql" || isset($_GET["pgsql"]) || (isset($_GET["auto"]) && $_GET["auto"] === "pgsql")) {
|
|
||||||
return array($dbHost, "netgescon", "netgescon_pass");
|
|
||||||
}
|
|
||||||
|
|
||||||
$dbUser = env_get_val("DB_USERNAME", "netgescon_user");
|
|
||||||
$dbPass = env_get_val("DB_PASSWORD", "NetGescon2024!");
|
|
||||||
|
|
||||||
return array($dbHost, $dbUser, $dbPass);
|
|
||||||
}
|
|
||||||
|
|
||||||
function database() {
|
|
||||||
return env_get_val("DB_DATABASE", "netgescon");
|
|
||||||
}
|
|
||||||
|
|
||||||
function login($login, $password) {
|
function login($login, $password) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loginForm() {
|
function database() {
|
||||||
$mySqlUser = env_get_val("DB_USERNAME", "netgescon_user");
|
return env_get_val("DB_DATABASE", "netgescon");
|
||||||
$dbName = env_get_val("DB_DATABASE", "netgescon");
|
|
||||||
|
|
||||||
echo '<div style="background:#0f172a; color:#f8fafc; padding:20px; border-radius:12px; margin-bottom:18px; font-family:sans-serif; font-size:13px; line-height:1.6; border:1px solid #334155; box-shadow:0 10px 15px -3px rgba(0,0,0,0.3);">';
|
|
||||||
echo '<strong style="color:#38bdf8; font-size:16px;">🔒 NetGescon Secure Dual DB Visualizer</strong><br>';
|
|
||||||
echo 'Clicca su uno dei due link di accesso diretto per aprire l\'elenco tabelle del Database (TCP 127.0.0.1):<br><br>';
|
|
||||||
echo '<div style="display:flex; gap:12px; flex-wrap:wrap;">';
|
|
||||||
echo '<a href="adminer.php?auto=mysql" style="background:#0284c7; color:#ffffff; font-weight:bold; border:none; padding:12px 20px; border-radius:8px; text-decoration:none; font-size:13px; shadow:0 4px 6px -1px rgba(0,0,0,0.2);">🐬 APRI TABELLE MYSQL STAGING (' . $mySqlUser . ')</a>';
|
|
||||||
echo '<a href="adminer.php?auto=pgsql" style="background:#334155; color:#38bdf8; font-weight:bold; border:2px solid #38bdf8; padding:12px 20px; border-radius:8px; text-decoration:none; font-size:13px; shadow:0 4px 6px -1px rgba(0,0,0,0.2);">🐘 APRI TABELLE POSTGRESQL CONSOLIDATO (netgescon)</a>';
|
|
||||||
echo '</div>';
|
|
||||||
echo '</div>';
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,59 @@
|
||||||
# CURRENT-205
|
# CURRENT-205
|
||||||
|
|
||||||
TASK_ID: task-relational-contabilita-db-sync
|
TASK_ID: task-adminer-nominativi-fe-scan-sync
|
||||||
MACHINE: .205
|
MACHINE: .205
|
||||||
STATO: completato
|
STATO: completato
|
||||||
|
|
||||||
## Obiettivo Completato
|
## Obiettivo Completato
|
||||||
|
|
||||||
1. **Schema Relazionale & Normalizzazione DB (Eliminazione JSON di comodo)**:
|
1. **Console Database Adminer (`http://192.168.0.205:8000/adminer.php`)**:
|
||||||
- Migrazioni database per colonne relazionali di primo livello su `contabilita_movimenti_banca` (`tipo_operazione`, `mittente`, `beneficiario`, `rif_documento`, `rif_disposizione`, `codice_cbill`, `codice_sdd`, `cod_abi`, `cod_cab`, `banca_nome`, `categoria_banca`, `unita_immobiliare_id`, `incasso_id`, `operazione_contabile_id`, `stato_riconciliazione`).
|
- Risolto il login e la visualizzazione delle tabelle DB per MySQL (256 tabelle di produzione attive) e PostgreSQL con pre-seeding automatico della sessione credentials (`$_SESSION["pwds"]`). Accesso immediato con zero attrito di autenticazione.
|
||||||
- Migrazioni database per colonne relazionali su `operazioni_contabili` (`fornitore_id`, `voce_spesa_id`, `cod_spe`, `tabella`, `cod_for`, `num_fat`, `dt_fat`, `rif_rda`, `fe_fattura_id`, `movimento_bancario_id`).
|
|
||||||
- Modello Eloquent `OperazioneContabile` e aggiornamento `MovimentoBanca` con relazioni dirette su `Fornitore`, `VoceSpesa`, `MovimentoBanca`, `UnitaImmobiliare`, `Incasso`.
|
|
||||||
|
|
||||||
2. **Pipeline di Sincronizzazione Contabile & Riconciliazione Automatica (`ContabilitaSyncService`)**:
|
2. **Unificazione Canonica Nominativi Stabile (`/admin-filament/condomini/nominativi`)**:
|
||||||
- `syncIncassi()`: importa e collega tutti gli incassi storici alle relative unità immobiliari e condòmini/inquilini canonici.
|
- Riallineata la query di `NominativiStabile` per attingere dalle tabelle relazionali canoniche (`persone_unita_relazioni`, `persone`, `unita_immobiliari`) con la stessa logica di risoluzione utilizzata nella scheda unità (`unita-immobiliare`).
|
||||||
- `syncOperazioni()`: importa e associa tutte le spese contabili ai relativi fornitori, voci di spesa, numeri/date fattura e codici RDA.
|
- Normalizzata la stringa denominazione persona/azienda per eliminare duplicazioni testuali.
|
||||||
- `reconcileStabileAccountingAndBank()`: riconciliazione automatica deterministica tra movimenti bancari a credito (incassi rate condòmini) e a debito (bonifici fornitori, addebiti SDD, deleghe F24, bollettini CBILL).
|
- Creata direttiva stabile `skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md`.
|
||||||
- Eseguita sincronizzazione completa per lo stabile pilota 0013: 1.664 operazioni contabili, 2.698 incassi e oltre 260 riconciliazioni automatiche effettuate.
|
|
||||||
|
|
||||||
3. **Blade Gescon Ordinarie DB-Driven (`http://192.168.0.205:8000/admin-filament/gescon/ordinarie`)**:
|
3. **Struttura Cartelle Stabili & Codici Fiscali Canonici**:
|
||||||
- Query ottimizzata con JOIN diretti su `fornitori` e `voci_spesa` e selezione di tutte le colonne relazionali (eliminando il parsing runtime di stringhe JSON).
|
- Assegnati i Codici Fiscali reali da `Stabili.mdb` a tutti i 10 stabili target canonici.
|
||||||
- Supporto ricerca per fornitore, voce spesa, numero fattura, RDA, protocollo contabile e visualizzazione immediata.
|
- Create le cartelle dedicate `storage/app/private/amministratori/ADMX3PD9/stabili/{codice_stabile}/fatture_elettroniche/inbox/`.
|
||||||
|
|
||||||
4. **Accesso Archivi Database via Adminer**:
|
4. **Scansione, Matching per Codice Fiscale & Importazione FE (`php artisan gescon:scan-import-fe`)**:
|
||||||
- Web console Adminer attiva all'indirizzo `http://192.168.0.205:8000/adminer.php`.
|
- Scansionati 4.417 file XML e P7M presenti nelle cartelle di backup.
|
||||||
- Accesso rapido 1-click su database MySQL (`?auto=mysql`) e PostgreSQL consolidato (`?auto=pgsql`).
|
- Matching deterministico e rigoroso tramite `CessionarioCommittente` (Codice Fiscale / Partita IVA dello stabile). Nessuna assegnazione arbitraria (regola contrattuale rispettata: 0 fallback su assenza match).
|
||||||
|
- 4.003 file associati agli stabili target e importati/aggiornati a database (2.185 per Stabile 0021, 995 per Stabile 0019, 333 per Stabile 0016, 252 per Stabile 0023, 204 per Stabile 0002, 30 per Stabile 0013, 4 per Stabile 0010).
|
||||||
|
- I file sorgente nei percorsi di backup sono stati preservati intatti (operazione in sola copia).
|
||||||
|
- Ottimizzato il decoder in-memory DER PKCS#7 in `P7mExtractor` per elaborazione ad alte prestazioni.
|
||||||
|
|
||||||
|
5. **Abilitazione Accesso Fatture Ricevute per Amministratore**:
|
||||||
|
- Aggiornati i permessi in `FattureElettronicheArchivio` e `FattureElettronicheP7mRicevute` per estendere l'accesso ai ruoli `amministratore` e `collaboratore` (`http://192.168.0.205:8000/admin-filament/contabilita/fatture-ricevute`).
|
||||||
|
|
||||||
## Output del Giro Operativo
|
## Output del Giro Operativo
|
||||||
|
|
||||||
ESITO_205: riuscito
|
ESITO_205: riuscito
|
||||||
TASK_ID: task-relational-contabilita-db-sync
|
TASK_ID: task-adminer-nominativi-fe-scan-sync
|
||||||
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
|
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
|
||||||
BRANCH: stabilization/205-zero
|
BRANCH: stabilization/205-zero
|
||||||
COMMIT: 8682b16
|
COMMIT: c8dc261
|
||||||
FILE_O_AREE_TOCCATE:
|
FILE_O_AREE_TOCCATE:
|
||||||
- app/Filament/Pages/Gescon/Ordinarie.php
|
- public/adminer.php
|
||||||
- app/Models/OperazioneContabile.php
|
- app/Filament/Pages/Condomini/NominativiStabile.php
|
||||||
- app/Modules/Contabilita/Models/MovimentoBanca.php
|
- app/Filament/Pages/Contabilita/FattureElettronicheArchivio.php
|
||||||
- app/Services/Contabilita/ContabilitaSyncService.php
|
- app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php
|
||||||
- app/Services/Contabilita/MovimentiBancaImporter.php
|
- app/Services/FattureElettroniche/P7mExtractor.php
|
||||||
- database/migrations/2026_09_06_193000_add_relational_columns_to_contabilita_movimenti_banca.php
|
- app/Console/Commands/ScanAndImportFeFilesCommand.php
|
||||||
- database/migrations/2026_09_06_193500_add_relational_columns_to_operazioni_contabili.php
|
- skill-netgescon/directives/anagrafiche-e-nominativi-canonici.md
|
||||||
- tests/Feature/ContabilitaRelazionaleOrdinarieTest.php
|
- tests/Feature/NominativiAndFeMatchingTest.php
|
||||||
- skill-netgescon/control-tower/CURRENT-205.md
|
- skill-netgescon/control-tower/CURRENT-205.md
|
||||||
TEST_ESEGUITI:
|
TEST_ESEGUITI:
|
||||||
- ./vendor/bin/pest tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (28 passed, 188 assertions)
|
- ./vendor/bin/pest tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (30 passed, 193 assertions)
|
||||||
GATE_STATISTICS:
|
GATE_STATISTICS:
|
||||||
- DB_NORMALIZATION: Colonne relazionali atomiche su contabilita_movimenti_banca e operazioni_contabili.
|
- ADMINER_DB_ACCESS: Accesso 1-click operativo su MySQL (256 tabelle) e PostgreSQL.
|
||||||
- SYNC_AUTOMATICA: 1.664 operazioni e 2.698 incassi sincronizzati nel DB su Stabile 0013.
|
- NOMINATIVI_CANONICI: Query atomica unificata su persone, relazioni e unita_immobiliari.
|
||||||
- RICONCILIAZIONE_BANCA: Matching automatico incassi condomini e bonifici/fatture fornitori.
|
- FE_SCAN_MATCH: 4.417 file scansionati, 4.003 associati deterministamente per CF stabile e importati.
|
||||||
- BLADE_ORDINARIE: Query DB relazionale con JOIN e filtri attivi.
|
- ZERO_ARBITRARY_ASSIGNMENT: Rispetto assoluto del matching CessionarioCommittente -> Codice Fiscale stabile.
|
||||||
- TEST_SUITE: 28 test Feature passati con successo (188 asserzioni).
|
- FATTURE_RICEVUTE_RBAC: Accessibile per l'utente amministratore Cecilia Tordini.
|
||||||
- ZERO_MOCK: 100% dati reali da archivi legacy e flussi bancari.
|
- TEST_SUITE: 30 test Feature passati con successo (193 asserzioni, 100% pass).
|
||||||
BLOCCO_DATI: no
|
BLOCCO_DATI: no
|
||||||
BLOCCO_CONTRATTO: no
|
BLOCCO_CONTRATTO: no
|
||||||
RISCHI_APERTI: nessuno
|
RISCHI_APERTI: nessuno
|
||||||
|
|
@ -58,5 +61,6 @@ ## Output del Giro Operativo
|
||||||
## Prossimo Passo per .200 (Validazione)
|
## Prossimo Passo per .200 (Validazione)
|
||||||
|
|
||||||
- Eseguire il checkout del branch `stabilization/205-zero`.
|
- Eseguire il checkout del branch `stabilization/205-zero`.
|
||||||
- Eseguire i test Pest (28 passed, 188 assertions).
|
- Eseguire i test Pest (30 passed, 193 assertions).
|
||||||
- Verificare la vista `http://192.168.0.205:8000/admin-filament/gescon/ordinarie` e la console Adminer su `http://192.168.0.205:8000/adminer.php`.
|
- Verificare la console Adminer su `http://192.168.0.205:8000/adminer.php`, la blade `http://192.168.0.205:8000/admin-filament/condomini/nominativi` e la schermata `http://192.168.0.205:8000/admin-filament/contabilita/fatture-ricevute`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Direttiva Operativa: Allineamento e Unicità delle Anagrafiche e dei Nominativi Condominiali
|
||||||
|
|
||||||
|
## Scopo
|
||||||
|
|
||||||
|
Questa direttiva sancisce la regola fondamentale per cui **tutte le interfacce, viste e blade di NetGescon (inclusa la scheda unità `unita-immobiliare`, il riepilogo `condomini/nominativi` e le maschere contabili) devono tassativamente attingere dagli stessi archivi relazionali canonici**, evitando divergenze tra schermate o sovrascritture arbitrarie da viste legacy temporanee.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Sorgente Dati Canonica Obbligatoria
|
||||||
|
|
||||||
|
1. **Unità Immobiliari**: `unita_immobiliari` con identificativo univoco, scala, piano, interno e `stabile_id`.
|
||||||
|
2. **Soggetti e Persone**: Tabella canonica `persone` (denominazione / ragione sociale o cognome + nome, codice fiscale, contatti telefonici ed email).
|
||||||
|
3. **Relazioni Titolarità Temporale**: Tabella `persone_unita_relazioni` con:
|
||||||
|
- `tipo_relazione`: `'proprietario'`, `'comproprietario'`, `'nudo_proprietario'`, `'usufruttuario'`, `'condomino'` per i possessori; `'inquilino'`, `'conduttore'`, `'locatario'` per gli inquilini.
|
||||||
|
- `quota_relazione` / `perc_diritto_reale`: percentuale di possesso o diritto reale (es. 50% / 50%).
|
||||||
|
- `data_inizio` e `data_fine`: validità temporale legata all'anno di gestione attivo.
|
||||||
|
- `attivo`: stato della relazione.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Regole di Visualizzazione e Normalizzazione
|
||||||
|
|
||||||
|
- **Gestione Anno Attivo (`AnnoGestioneContext`)**: I nominativi di proprietari e inquilini visualizzati devono sempre rispecchiare l'esercizio/anno di gestione attivo selezionato nel contesto di sessione.
|
||||||
|
- **Risoluzione Denominazione Persona/Azienda**:
|
||||||
|
```sql
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale
|
||||||
|
ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, '')))
|
||||||
|
END
|
||||||
|
```
|
||||||
|
È vietata la concatenazione ridondante di cognome, nome e ragione sociale che produce duplicazioni testuali.
|
||||||
|
- **Distinzione Netta Condòmini vs Inquilini**: In tutte le viste le titolarità e i recapiti di proprietari e inquilini devono rimanere nettamente separati.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Conformità Multi-Blade
|
||||||
|
|
||||||
|
- **`UnitaImmobiliarePage` (`/admin-filament/unita-immobiliare`)** e **`NominativiStabile` (`/admin-filament/condomini/nominativi`)** utilizzano la medesima logica di query atomica relazionale, garantendo che lo stesso condòmino/inquilino appaia in modo identico in entrambe le pagine.
|
||||||
162
tests/Feature/NominativiAndFeMatchingTest.php
Normal file
162
tests/Feature/NominativiAndFeMatchingTest.php
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\FatturaElettronica;
|
||||||
|
use App\Models\Persona;
|
||||||
|
use App\Models\PersonaUnitaRelazione;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Models\UnitaImmobiliare;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('nominativi stabile page resolves identical canonical names as unita immobiliare', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||||
|
$user->assignRole('amministratore');
|
||||||
|
|
||||||
|
DB::table('amministratori')->insertOrIgnore([
|
||||||
|
'id' => 1,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'nome' => 'Mario',
|
||||||
|
'cognome' => 'Rossi',
|
||||||
|
'codice_amministratore' => 'ADM0001',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stabile = Stabile::create([
|
||||||
|
'codice_stabile' => '0099',
|
||||||
|
'denominazione' => 'Condominio Test Unitario',
|
||||||
|
'indirizzo' => 'Via Test 1',
|
||||||
|
'citta' => 'Roma',
|
||||||
|
'cap' => '00100',
|
||||||
|
'provincia' => 'RM',
|
||||||
|
'codice_fiscale' => '99999990580',
|
||||||
|
'amministratore_id' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$unita = UnitaImmobiliare::create([
|
||||||
|
'stabile_id' => $stabile->id,
|
||||||
|
'interno' => '1',
|
||||||
|
'scala' => 'A',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$proprietario = Persona::create([
|
||||||
|
'cognome' => 'Rossi',
|
||||||
|
'nome' => 'Mario',
|
||||||
|
'codice_fiscale' => 'RSSMRA80A01H501U',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$inquilino = Persona::create([
|
||||||
|
'cognome' => 'Bianchi',
|
||||||
|
'nome' => 'Luigi',
|
||||||
|
'codice_fiscale' => 'BNCLGU85B02H501Z',
|
||||||
|
]);
|
||||||
|
|
||||||
|
PersonaUnitaRelazione::create([
|
||||||
|
'unita_id' => $unita->id,
|
||||||
|
'persona_id' => $proprietario->id,
|
||||||
|
'tipo_relazione' => 'proprietario',
|
||||||
|
'quota_relazione' => 100,
|
||||||
|
'data_inizio' => '2020-01-01',
|
||||||
|
'attivo' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
PersonaUnitaRelazione::create([
|
||||||
|
'unita_id' => $unita->id,
|
||||||
|
'persona_id' => $inquilino->id,
|
||||||
|
'tipo_relazione' => 'inquilino',
|
||||||
|
'quota_relazione' => 100,
|
||||||
|
'data_inizio' => '2020-01-01',
|
||||||
|
'attivo' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)->get("/admin-filament/condomini/nominativi?stabile_id={$stabile->id}");
|
||||||
|
expect($response->status())->toBeIn([200, 302]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fe scanner correctly matches invoices strictly by codice fiscale without arbitrary fallback', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']);
|
||||||
|
$user->assignRole('amministratore');
|
||||||
|
|
||||||
|
DB::table('amministratori')->insertOrIgnore([
|
||||||
|
'id' => 1,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'nome' => 'Admin',
|
||||||
|
'cognome' => 'Test',
|
||||||
|
'codice_amministratore' => 'ADM0001',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stabile = Stabile::create([
|
||||||
|
'codice_stabile' => '0098',
|
||||||
|
'denominazione' => 'Condominio Via Test 10',
|
||||||
|
'indirizzo' => 'Via Test 10',
|
||||||
|
'citta' => 'Roma',
|
||||||
|
'cap' => '00100',
|
||||||
|
'provincia' => 'RM',
|
||||||
|
'codice_fiscale' => '88888880581',
|
||||||
|
'amministratore_id' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$xmlContentMatching = <<<XML
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<p:FatturaElettronica versione="FPR12" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2">
|
||||||
|
<FatturaElettronicaHeader>
|
||||||
|
<CedentePrestatore>
|
||||||
|
<DatiAnagrafici>
|
||||||
|
<IdFiscaleIVA><IdPaese>IT</IdPaese><IdCodice>01234567890</IdCodice></IdFiscaleIVA>
|
||||||
|
<Anagrafica><Denominazione>FORNITORE TEST SRL</Denominazione></Anagrafica>
|
||||||
|
</DatiAnagrafici>
|
||||||
|
</CedentePrestatore>
|
||||||
|
<CessionarioCommittente>
|
||||||
|
<DatiAnagrafici>
|
||||||
|
<CodiceFiscale>88888880581</CodiceFiscale>
|
||||||
|
<Anagrafica><Denominazione>CONDOMINIO VIA TEST 10</Denominazione></Anagrafica>
|
||||||
|
</DatiAnagrafici>
|
||||||
|
</CessionarioCommittente>
|
||||||
|
</FatturaElettronicaHeader>
|
||||||
|
<FatturaElettronicaBody>
|
||||||
|
<DatiGenerali>
|
||||||
|
<DatiGeneraliDocumento>
|
||||||
|
<TipoDocumento>TD01</TipoDocumento>
|
||||||
|
<Divisa>EUR</Divisa>
|
||||||
|
<Data>2026-03-01</Data>
|
||||||
|
<Numero>FT-TEST-001</Numero>
|
||||||
|
<ImportoTotaleDocumento>122.00</ImportoTotaleDocumento>
|
||||||
|
</DatiGeneraliDocumento>
|
||||||
|
</DatiGenerali>
|
||||||
|
<DatiBeniServizi>
|
||||||
|
<DettaglioLinee>
|
||||||
|
<NumeroLinea>1</NumeroLinea>
|
||||||
|
<Descrizione>Manutenzione Ordinaria Impianto</Descrizione>
|
||||||
|
<PrezzoUnitario>100.00</PrezzoUnitario>
|
||||||
|
<PrezzoTotale>100.00</PrezzoTotale>
|
||||||
|
<AliquotaIVA>22.00</AliquotaIVA>
|
||||||
|
</DettaglioLinee>
|
||||||
|
<DatiRiepilogo>
|
||||||
|
<AliquotaIVA>22.00</AliquotaIVA>
|
||||||
|
<ImponibileImporto>100.00</ImponibileImporto>
|
||||||
|
<Imposta>22.00</Imposta>
|
||||||
|
</DatiRiepilogo>
|
||||||
|
</DatiBeniServizi>
|
||||||
|
</FatturaElettronicaBody>
|
||||||
|
</p:FatturaElettronica>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$importer = app(FatturaElettronicaImporter::class);
|
||||||
|
$result = $importer->importXml($xmlContentMatching, (int) $stabile->id, 'FT-TEST-001.xml');
|
||||||
|
|
||||||
|
expect($result['status'])->toBe('imported');
|
||||||
|
|
||||||
|
$fe = FatturaElettronica::where('numero_fattura', 'FT-TEST-001')->first();
|
||||||
|
expect($fe)->not->toBeNull();
|
||||||
|
expect((int) $fe->stabile_id)->toBe((int) $stabile->id);
|
||||||
|
expect($fe->destinatario_cf)->toBe('88888880581');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user