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

931 lines
31 KiB
PHP

# MIGRAZIONE ARCHITETTURA CONTABILE MULTI-ANNO
## 🎯 PROBLEMI IDENTIFICATI
### 1. **Collisione Dati Multi-Anno**
```
PROBLEMA: Import 2024 + 2025 → stesso database senza separazione
SOLUZIONE: Campo `gestione_id` + `anno_gestione` in tutte le tabelle
```
### 2. **Mancanza Continuità Contabile**
```
PROBLEMA: Dati non passano alla gestione successiva
SOLUZIONE: Bilancio apertura + scritture partita doppia + saldi portati
```
### 3. **Gestioni Separate Non Collegate**
```
PROBLEMA: O/R/S gestite separatamente → perdita visione d'insieme
SOLUZIONE: Gestione unificata con campo tipo_gestione
```
### 4. **Mancanza Tracciabilità Fiscale**
```
PROBLEMA: Ritenute d'acconto disperse in codici
SOLUZIONE: Registro RA dedicato + collegamento operazioni
```
## 🏗️ NUOVA ARCHITETTURA PROPOSTA
### **1. Gestioni Contabili (Master)**
```sql
CREATE TABLE gestioni_contabili (
id BIGINT PRIMARY KEY,
tenant_id VARCHAR(50),
anno_gestione INTEGER,
tipo_gestione ENUM('ordinaria', 'riscaldamento', 'straordinaria'),
numero_straordinaria INTEGER NULL, -- per N_STRA
denominazione VARCHAR(200),
data_inizio DATE,
data_fine DATE,
stato ENUM('aperta', 'chiusa', 'consolidata'),
bilancio_apertura JSON, -- saldi iniziali
protocollo_prefix VARCHAR(10), -- O2024, R2024, S2024-001
INDEX(tenant_id, anno_gestione, tipo_gestione)
);
```
### **2. Operazioni Contabili (Unificata)**
```sql
ALTER TABLE operazioni_contabili ADD COLUMN (
gestione_id BIGINT REFERENCES gestioni_contabili(id),
compet ENUM('C', 'P') DEFAULT 'C',
natura2 VARCHAR(10), -- per partita doppia
n_stra INTEGER NULL, -- collegamento straordinarie
protocollo_numero INTEGER, -- auto-increment per gestione
protocollo_completo VARCHAR(50), -- O2024-001, S2024-003-015
-- Tracciabilità modifiche
voce_spesa_snapshot JSON, -- snapshot voce al momento registrazione
modificato_da_user_id BIGINT NULL,
modificato_il TIMESTAMP NULL,
motivo_modifica TEXT NULL,
-- Collegamento fiscale
ritenuta_acconto_id BIGINT NULL,
fattura_elettronica_id BIGINT NULL,
INDEX(gestione_id, protocollo_numero),
INDEX(tenant_id, compet),
INDEX(natura2)
);
```
### **3. Registro Ritenute d'Acconto**
```sql
CREATE TABLE registro_ritenute_acconto (
id BIGINT PRIMARY KEY,
tenant_id VARCHAR(50),
gestione_id BIGINT REFERENCES gestioni_contabili(id),
-- Dati ritenuta
numero_progressivo INTEGER,
data_competenza DATE,
fornitore_id BIGINT,
operazione_id BIGINT, -- collegamento operazione che ha generato RA
-- Importi
imponibile DECIMAL(15,4),
aliquota_ritenuta DECIMAL(5,2),
importo_ritenuta DECIMAL(15,4),
-- Codici fiscali
codice_tributo VARCHAR(10),
rif_rda VARCHAR(50), -- riferimento Gescon
-- Versamento
stato_versamento ENUM('da_versare', 'versata', 'compensata'),
data_versamento DATE NULL,
f24_riferimento VARCHAR(100) NULL,
-- Comunicazioni
comunicazione_fornitore_inviata BOOLEAN DEFAULT FALSE,
data_comunicazione_fornitore DATE NULL,
INDEX(gestione_id, numero_progressivo),
INDEX(fornitore_id, data_competenza),
INDEX(stato_versamento)
);
```
### **4. Incassi da Estratto Conto**
```sql
CREATE TABLE incassi_estratto_conto (
id BIGINT PRIMARY KEY,
tenant_id VARCHAR(50),
gestione_id BIGINT REFERENCES gestioni_contabili(id),
-- Dati Gescon Inc_da_ec
id_gescon INTEGER,
data_operazione DATE,
data_valuta DATE,
importo DECIMAL(15,4),
descrizione TEXT,
-- Collegamento
incasso_id BIGINT NULL REFERENCES incassi(id),
movimento_bancario_id BIGINT NULL,
-- Stato riconciliazione
stato ENUM('da_riconciliare', 'riconciliato', 'anomalo'),
note_riconciliazione TEXT,
INDEX(gestione_id, data_operazione),
INDEX(stato)
);
```
## 📊 ESTRATTI E IMPORT MULTIPLI DATABASE
Invece di CSV, creiamo connessioni dirette ai file MDB:
<function_calls>
<invoke name="create_file">
<parameter name="content"><?php
namespace App\Services\Import;
use App\Models\GestioneContabile;
use App\Models\OperazioneContabile;
use App\Models\RegistroRitenuteAcconto;
use App\Models\IncassoEstrattoConto;
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 array $mdbConnections = [];
private array $gestioni = [];
public function __construct(string $tenantId)
{
$this->tenantId = $tenantId;
}
/**
* 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;
}
/**
* 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}"
];
// Gestione Riscaldamento (se presente)
$opRiscaldamento = $this->queryMdb($mdbPath, "
SELECT COUNT(*) as cnt FROM operazioni
WHERE gestione = 'R' OR gestione = 'r'
");
if (!empty($opRiscaldamento) && ($opRiscaldamento[0]['cnt'] ?? 0) > 0) {
$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}"
];
}
// 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'];
$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): GestioneContabile
{
$gestione = GestioneContabile::updateOrCreate([
'tenant_id' => $this->tenantId,
'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 = $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 *,
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
WHERE {$whereClause}
ORDER BY n_operazione
");
$imported = 0;
foreach ($operazioni as $op) {
try {
$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)
]);
$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;
}
/**
* Import rate emesse da generale_stabile
*/
private function importRateEmesse(string $mdbPath, int $year): int
{
// Questa funzione opera a livello di anno, non di gestione specifica
$imported = 0;
$tables = ['EMESS_DET', 'EMESS_DET_2', 'EMESS_GEN'];
foreach ($tables as $table) {
$rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table} ORDER BY data_em");
foreach ($rate as $rata) {
try {
// TODO: Import rate in tabella dedicata
$imported++;
} catch (\Exception $e) {
Log::error("Error importing rata", [
'table' => $table,
'rata' => $rata,
'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";
}
}
]);
// Gestione Riscaldamento
$this->gestioni[$year]['riscaldamento'] = GestioneContabile::create([
'tenant_id' => $this->tenantId,
'anno_gestione' => (int)$year,
'tipo_gestione' => 'riscaldamento',
'denominazione' => "Gestione Riscaldamento {$year}",
'data_inizio' => "{$year}-01-01",
'data_fine' => "{$year}-12-31",
'stato' => 'aperta',
'protocollo_prefix' => "R{$year}"
]);
// Le straordinarie verranno create dinamicamente
}
}
/**
* Import dati per una specifica gestione/anno
*/
private function importGestioneYear(string $year, array $paths): array
{
$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
$results['incassi_ec'] = $this->importIncassiEstrattoConto($year, $paths['singolo_anno']);
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 *,
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
");
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_voce'] ?? '');
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)
]);
$imported++;
} catch (\Exception $e) {
Log::error("Error importing operazione", [
'year' => $year,
'operazione' => $op,
'error' => $e->getMessage()
]);
}
}
return $imported;
}
/**
* Import ritenute d'acconto da Nettovers_RDA
*/
private function importRitenuteAcconto(string $year, string $mdbPath): int
{
$imported = 0;
// Query per ritenute
$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
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['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', // 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
ORDER BY data_oper
");
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)
]);
$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 EMESS_DET, EMESS_DET_2, EMESS_GEN
$tables = ['EMESS_DET', 'EMESS_DET_2', 'EMESS_GEN'];
foreach ($tables as $table) {
$rate = $this->queryMdb($mdbPath, "SELECT * FROM {$table} ORDER BY data_em");
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}";
if (!isset($this->gestioni[$year][$key])) {
$this->gestioni[$year][$key] = GestioneContabile::create([
'tenant_id' => $this->tenantId,
'anno_gestione' => (int)$year,
'tipo_gestione' => 'straordinaria',
'numero_straordinaria' => $nStra,
'denominazione' => "Gestione Straordinaria {$year} - {$nStra}",
'data_inizio' => "{$year}-01-01",
'data_fine' => "{$year}-12-31",
'stato' => 'aperta',
'protocollo_prefix' => "S{$year}-{$nStra}"
]);
}
return $this->gestioni[$year][$key]->id;
}
/**
* Query diretta su file MDB
*/
private function queryMdb(string $mdbPath, string $query): array
{
// Usa mdb-tools per query diretta
$command = sprintf(
'mdb-sql "%s" <<< "%s"',
escapeshellarg($mdbPath),
escapeshellarg($query)
);
$output = shell_exec($command);
// Parser output MDB in array associativo
return $this->parseMdbOutput($output);
}
/**
* 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)));
$data = [];
foreach ($lines as $line) {
if (empty(trim($line))) continue;
$values = explode("\t", $line);
$row = array_combine($headers, $values);
$data[] = $row;
}
return $data;
}
/**
* Genera protocollo per operazione
*/
private function generateProtocollo(int $gestioneId, array $operazione): array
{
$gestione = GestioneContabile::find($gestioneId);
$numero = $gestione->getNextProtocolNumber();
return [
'numero' => $numero,
'completo' => $gestione->protocollo_prefix . '-' . 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;
}
}
// Altri metodi helper...
}