netgescon-day0/app/Services/Import/MultiYearGesconImportService.php

1034 lines
36 KiB
PHP
Executable File

<?php
namespace App\Services\Import;
use App\Models\GestioneContabile;
use App\Models\Incasso;
use App\Models\IncassoEstrattoConto;
use App\Models\RegistroRitenuteAcconto;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* MULTI-YEAR GESCON IMPORT SERVICE
*
* Gestisce import diretto da file MDB multipli con:
* - Separazione gestioni per anno
* - Continuità contabile multi-anno
* - Partita doppia con NATURA2
* - Tracciabilità modifiche
* - Registro ritenute d'acconto
*/
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, ?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());
}
}
/**
* Import completo multi-anno da directory Gescon
*/
public function importFromGesconDirectory(string $baseDir): array
{
Log::info("Starting multi-year Gescon import", ['tenant' => $this->tenantId, 'dir' => $baseDir]);
// 1. Scansiona directory per anni
$years = $this->scanGesconYears($baseDir);
// 2. Crea gestioni contabili per ogni anno
$this->createGestioni($years);
// 3. Import dati per ogni gestione
$results = [];
foreach ($years as $year => $paths) {
$results[$year] = $this->importGestioneYear($year, $paths);
}
// 4. Crea bilanci di apertura per continuità
$this->createBilanciApertura();
return [
'success' => true,
'gestioni_create' => count($this->gestioni),
'years_imported' => array_keys($results),
'details' => $results,
];
}
/**
* Scansiona directory Gescon per identificare anni
*/
private function scanGesconYears(string $baseDir): array
{
$years = [];
// Pattern directory: /Gescon/2024/singolo_anno.mdb, /Gescon/2025/singolo_anno.mdb
$gesconDirs = glob($baseDir . '/*/');
foreach ($gesconDirs as $dir) {
$year = basename(rtrim($dir, '/'));
if (is_numeric($year) && strlen($year) == 4) {
$paths = [
'singolo_anno' => $dir . 'singolo_anno.mdb',
'generale_stabile' => $dir . 'generale_stabile.mdb',
];
// Verifica esistenza file
if (file_exists($paths['singolo_anno'])) {
$years[$year] = $paths;
}
}
}
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
*/
private function scanGestioniFromMdb(string $mdbPath, int $year): array
{
$gestioni = [];
// Gestione Ordinaria (sempre presente)
$gestioni[] = [
'anno_gestione' => $year,
'tipo_gestione' => 'ordinaria',
'denominazione' => "Gestione Ordinaria {$year}",
'data_inizio' => "{$year}-01-01",
'data_fine' => "{$year}-12-31",
'protocollo_prefix' => "O{$year}",
];
// Legge le colonne necessarie da Operazioni
$operazioni = $this->queryMdb($mdbPath, "SELECT Gestione, n_stra FROM Operazioni");
$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',
'denominazione' => "Gestione Riscaldamento {$year}",
'data_inizio' => "{$year}-01-01",
'data_fine' => "{$year}-12-31",
'protocollo_prefix' => "R{$year}",
];
}
ksort($straordinarie);
foreach (array_keys($straordinarie) as $numero) {
$gestioni[] = [
'anno_gestione' => $year,
'tipo_gestione' => 'straordinaria',
'numero_straordinaria' => $numero,
'denominazione' => "Gestione Straordinaria {$year} - {$numero}",
'data_inizio' => "{$year}-01-01",
'data_fine' => "{$year}-12-31",
'protocollo_prefix' => "S{$year}-{$numero}",
];
}
return $gestioni;
}
/**
* Crea o aggiorna gestione
*/
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,
], [
'denominazione' => $gestioneData['denominazione'],
'data_inizio' => $gestioneData['data_inizio'],
'data_fine' => $gestioneData['data_fine'],
'stato' => 'aperta',
'protocollo_prefix' => $gestioneData['protocollo_prefix'],
]);
// Cache per lookup veloce
$year = $yearKey ?? $gestioneData['anno_gestione'];
$tipo = $gestioneData['tipo_gestione'];
if ($tipo === 'straordinaria') {
$key = "straordinaria_{$gestioneData['numero_straordinaria']}";
} else {
$key = $tipo;
}
$this->gestioni[$year][$key] = $gestione;
return $gestione;
}
/**
* Import operazioni per gestione specifica
*/
private function importOperazioniForGestione(GestioneContabile $gestione, string $mdbPath): int
{
// Filtro per tipo gestione
$whereClause = $this->buildGestioneWhereClause($gestione);
$operazioni = $this->queryMdb($mdbPath, "
SELECT *
FROM Operazioni
WHERE {$whereClause}
");
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);
$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++;
} catch (\Exception $e) {
Log::error("Error importing operazione for gestione", [
'gestione_id' => $gestione->id,
'operazione' => $op,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/**
* Import incassi per gestione
*/
private function importIncassiForGestione(GestioneContabile $gestione, string $mdbPath): int
{
$whereClause = $this->buildGestioneWhereClause($gestione);
$incassi = $this->queryMdb($mdbPath, "
SELECT * FROM incassi
WHERE {$whereClause}
ORDER BY data_inc
");
$imported = 0;
foreach ($incassi as $inc) {
try {
// Import incasso...
$imported++;
} catch (\Exception $e) {
Log::error("Error importing incasso", [
'gestione_id' => $gestione->id,
'incasso' => $inc,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/**
* Import ritenute per gestione
*/
private function importRitenuteFOrGestione(GestioneContabile $gestione, string $mdbPath): int
{
$ritenute = $this->queryMdb($mdbPath, "
SELECT n.*, o.data_oper, o.euro as imponibile_operazione
FROM Nettovers_RDA n
LEFT JOIN operazioni o ON n.Rif_RDA = o.n_operazione
WHERE n.Rif_RDA IS NOT NULL
AND {$this->buildGestioneJoinClause($gestione, 'o')}
ORDER BY n.Rif_RDA
");
$imported = 0;
foreach ($ritenute as $ra) {
try {
RegistroRitenuteAcconto::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestione->id,
'numero_progressivo' => $ra['Rif_RDA'],
'data_competenza' => $this->parseGesconDate($ra['data_oper']),
'imponibile' => (float) ($ra['imponibile_operazione'] ?? 0),
'aliquota_ritenuta' => (float) ($ra['aliquota'] ?? 20),
'importo_ritenuta' => (float) ($ra['ritenuta'] ?? 0),
'rif_rda' => $ra['Rif_RDA'],
'stato_versamento' => 'da_versare',
'metadati_gescon' => json_encode($ra),
]);
$imported++;
} catch (\Exception $e) {
Log::error("Error importing ritenuta", [
'gestione_id' => $gestione->id,
'ritenuta' => $ra,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/**
* Import incassi estratto conto per gestione
*/
private function importIncassiEstrattoContoForGestione(GestioneContabile $gestione, string $mdbPath): int
{
$incassiEc = $this->queryMdb($mdbPath, "
SELECT * FROM Inc_da_ec
ORDER BY data_oper
");
$imported = 0;
foreach ($incassiEc as $inc) {
try {
IncassoEstrattoConto::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestione->id,
'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),
]);
$imported++;
} catch (\Exception $e) {
Log::error("Error importing incasso EC", [
'gestione_id' => $gestione->id,
'incasso' => $inc,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/**
* Costruisce WHERE clause per filtrare per gestione
*/
private function buildGestioneWhereClause(GestioneContabile $gestione): string
{
switch ($gestione->tipo_gestione) {
case 'ordinaria':
return "(gestione = 'O' OR gestione = 'o' OR gestione IS NULL OR gestione = '')";
case 'riscaldamento':
return "(gestione = 'R' OR gestione = 'r')";
case 'straordinaria':
$numero = $gestione->numero_straordinaria;
return "(gestione = 'S' OR gestione = 's') AND N_STRA = {$numero}";
default:
return "1=1";
}
}
/**
* Costruisce JOIN clause per filtrare per gestione
*/
private function buildGestioneJoinClause(GestioneContabile $gestione, string $alias = ''): string
{
$prefix = $alias ? "{$alias}." : '';
switch ($gestione->tipo_gestione) {
case 'ordinaria':
return "({$prefix}gestione = 'O' OR {$prefix}gestione = 'o' OR {$prefix}gestione IS NULL OR {$prefix}gestione = '')";
case 'riscaldamento':
return "({$prefix}gestione = 'R' OR {$prefix}gestione = 'r')";
case 'straordinaria':
$numero = $gestione->numero_straordinaria;
return "({$prefix}gestione = 'S' OR {$prefix}gestione = 's') AND {$prefix}N_STRA = {$numero}";
default:
return "1=1";
}
}
/**
* Import dati per una specifica gestione/anno
*/
public function importGestioneYear(string $year, array $paths): array
{
if (! isset($this->gestioni[$year])) {
$this->initGestioniForYear($year, $paths);
}
$results = [
'operazioni' => 0,
'incassi' => 0,
'ritenute' => 0,
'rate_emesse' => 0,
'incassi_ec' => 0,
];
// 1. Import operazioni contabili
$results['operazioni'] = $this->importOperazioni($year, $paths['singolo_anno']);
// 2. Import incassi
$results['incassi'] = $this->importIncassi($year, $paths['singolo_anno']);
// 3. Import ritenute d'acconto
$results['ritenute'] = $this->importRitenuteAcconto($year, $paths['singolo_anno']);
// 4. Import rate emesse (da generale_stabile.mdb)
if (file_exists($paths['generale_stabile'])) {
$results['rate_emesse'] = $this->importRateEmesse($year, $paths['generale_stabile']);
}
// 5. Import incassi estratto conto
if (file_exists($paths['generale_stabile'])) {
$results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['generale_stabile']);
}
return $results;
}
/**
* Import operazioni con gestione COMPET e NATURA2
*/
private function importOperazioni(string $year, string $mdbPath): int
{
$imported = 0;
// Query MDB per operazioni
$operazioni = $this->queryMdb($mdbPath, "
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
$gestioneId = $this->determineGestioneId($year, $op);
// Genera protocollo
$protocollo = $this->generateProtocollo($gestioneId, $op);
// Snapshot voce spesa
$voceSnapshot = $this->getVoceSpesaSnapshot($mdbPath, $op['cod_spe'] ?? '');
$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++;
} catch (\Exception $e) {
Log::error("Error importing operazione", [
'year' => $year,
'operazione' => $op,
'error' => $e->getMessage(),
]);
}
}
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
*/
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.dt_spe, o.importo_euro as imponibile_operazione
FROM Nettovers_RDA n
LEFT JOIN Operazioni o ON n.Rif_RDA = o.id_operaz
WHERE n.Rif_RDA IS NOT NULL
ORDER BY n.Rif_RDA
");
foreach ($ritenute as $ra) {
try {
$gestioneId = $this->gestioni[$year]['ordinaria']->id;
RegistroRitenuteAcconto::create([
'tenant_id' => $this->tenantId,
'gestione_id' => $gestioneId,
'numero_progressivo' => $ra['Rif_RDA'],
'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),
'rif_rda' => $ra['Rif_RDA'],
'stato_versamento' => 'da_versare', // Default
'metadati_gescon' => json_encode($ra),
]);
$imported++;
} catch (\Exception $e) {
Log::error("Error importing ritenuta", [
'year' => $year,
'ritenuta' => $ra,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/**
* Import incassi estratto conto da Inc_da_ec
*/
private function importIncassiEstrattoConto(string $year, string $mdbPath): int
{
$imported = 0;
$incassiEc = $this->queryMdb($mdbPath, "
SELECT * FROM Inc_da_ec
");
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['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++;
} catch (\Exception $e) {
Log::error("Error importing incasso EC", [
'year' => $year,
'incasso' => $inc,
'error' => $e->getMessage(),
]);
}
}
return $imported;
}
/**
* Import rate emesse da generale_stabile.mdb
*/
private function importRateEmesse(string $year, string $mdbPath): int
{
$imported = 0;
// Unifica emes_det, emes_det_2, emes_gen
$tables = ['emes_det', 'emes_det_2', 'emes_gen'];
foreach ($tables as $table) {
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 {
// Logica import rate...
$imported++;
} catch (\Exception $e) {
Log::error("Error importing rata", ['table' => $table, 'rata' => $rata, 'error' => $e->getMessage()]);
}
}
}
return $imported;
}
/**
* Determina gestione ID basata su dati operazione
*/
private function determineGestioneId(string $year, array $operazione): int
{
$gestione = $operazione['gestione'] ?? 'O';
$nStra = (int) ($operazione['N_STRA'] ?? 0);
switch (strtoupper($gestione)) {
case 'R':
return $this->gestioni[$year]['riscaldamento']->id;
case 'S':
// Crea gestione straordinaria se non esiste
return $this->getOrCreateStraordinaria($year, $nStra);
default:
return $this->gestioni[$year]['ordinaria']->id;
}
}
/**
* Ottiene o crea gestione straordinaria
*/
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,
'stabile_id' => $this->stabileId,
'anno_gestione' => $calendarYear,
'tipo_gestione' => 'straordinaria',
'numero_straordinaria' => $nStra,
'denominazione' => "Gestione Straordinaria {$calendarYear} - {$nStra}",
'data_inizio' => "{$calendarYear}-01-01",
'data_fine' => "{$calendarYear}-12-31",
'stato' => 'aperta',
'protocollo_prefix' => "S{$calendarYear}-{$nStra}",
]);
}
return $this->gestioni[$year][$key]->id;
}
/**
* Query diretta su file MDB
*/
private function queryMdb(string $mdbPath, string $query): array
{
$query = preg_replace('/\s+/', ' ', trim($query));
// Usa mdb-tools per query diretta
$command = sprintf(
'echo %s | mdb-sql -P -F -d %s %s',
escapeshellarg($query),
escapeshellarg("\t"),
escapeshellarg($mdbPath)
);
$output = shell_exec($command);
// Parser output MDB in array associativo
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
*/
private function parseMdbOutput(string $output): array
{
$lines = explode("\n", trim($output));
if (empty($lines)) {
return [];
}
$headers = array_map('trim', explode("\t", array_shift($lines)));
$headerCount = count($headers);
$data = [];
$currentValues = [];
foreach ($lines as $line) {
if ($line === '' && empty($currentValues)) {
continue;
}
$values = explode("\t", $line);
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;
}
/**
* Genera protocollo per operazione
*/
private function generateProtocollo(int $gestioneId, array $operazione): array
{
$gestione = GestioneContabile::find($gestioneId);
$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 ?? 'O') . '-' . str_pad($numero, 4, '0', STR_PAD_LEFT),
];
}
/**
* Snapshot voce spesa
*/
private function getVoceSpesaSnapshot(string $mdbPath, string $codVoce): array
{
if (empty($codVoce)) {
return [];
}
$voci = $this->queryMdb($mdbPath, "
SELECT * FROM voci_spesa
WHERE cod_voce = '{$codVoce}'
");
return $voci[0] ?? [];
}
/**
* Parse data Gescon
*/
private function parseGesconDate($dateValue): ?string
{
if (empty($dateValue)) {
return null;
}
// Prova vari formati date Gescon/Access
$formats = ['Y-m-d', 'd/m/Y', 'm/d/Y', 'Y-m-d H:i:s'];
foreach ($formats as $format) {
$date = \DateTime::createFromFormat($format, $dateValue);
if ($date !== false) {
return $date->format('Y-m-d');
}
}
return null;
}
}