netgescon-day0/app/Services/Contabilita/ContabilitaSyncService.php

952 lines
41 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\Contabilita;
use App\Models\Fornitore;
use App\Models\GestioneContabile;
use App\Models\Stabile;
use App\Models\UnitaImmobiliare;
use App\Models\VoceSpesa;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Symfony\Component\Process\Process;
class ContabilitaSyncService
{
protected string $archivesBasePath = '/mnt/gescon-archives/gescon';
public function __construct(?string $basePath = null)
{
if ($basePath !== null && is_dir($basePath)) {
$this->archivesBasePath = rtrim($basePath, '/');
}
}
/**
* Sincronizza l'intera anagrafica fornitori da dbc/Fornitori.mdb / Fornitori.mdb
*/
public function syncFornitori(?\Closure $log = null): array
{
$log = $log ?: fn($msg) => null;
$countImported = 0;
$countUpdated = 0;
$mdbPaths = [
$this->archivesBasePath . '/dbc/Fornitori.mdb',
$this->archivesBasePath . '/Fornitori.mdb',
$this->archivesBasePath . '/parti_comuni.mdb',
];
$foundMdb = null;
$rows = [];
foreach ($mdbPaths as $p) {
if (is_file($p)) {
$exported = $this->exportMdbTable($p, 'Fornitori');
if (! empty($exported)) {
$foundMdb = $p;
$rows = $exported;
break;
}
}
}
if (! $foundMdb || empty($rows)) {
$log("⚠️ Nessun fornitore con dati trovato nei percorsi MDB verificati.");
return ['imported' => 0, 'updated' => 0];
}
$log("📖 Lettura fornitori legacy da: {$foundMdb} (" . count($rows) . " posizioni)");
foreach ($rows as $r) {
$codForn = trim((string) ($r['cod_forn'] ?? $r['id_fornitore'] ?? ''));
if ($codForn === '') {
continue;
}
$cognome = trim((string) ($r['cognome'] ?? ''));
$nome = trim((string) ($r['nome'] ?? ''));
$ragSoc = trim((string) ($r['Descrizione'] ?? ''));
if ($ragSoc === '') {
$ragSoc = trim($cognome . ' ' . $nome);
}
if ($ragSoc === '') {
$ragSoc = "Fornitore #{$codForn}";
}
$cf = strtoupper(trim((string) ($r['cod_fisc'] ?? '')));
$piva = preg_replace('/[^\d]/', '', (string) ($r['p_iva'] ?? ''));
$iban = strtoupper(preg_replace('/[^A-Z0-9]/', '', (string) ($r['Cod_IBAN'] ?? '')));
$indirizzo = trim((string) ($r['indirizzo'] ?? ''));
$cap = trim((string) ($r['cap'] ?? ''));
$citta = trim((string) ($r['citta'] ?? ''));
$prov = strtoupper(trim((string) ($r['pr'] ?? '')));
$tel = trim((string) ($r['Telef_1'] ?? $r['telef_2'] ?? ''));
$cell = trim((string) ($r['Cellulare'] ?? ''));
$email = strtolower(trim((string) ($r['Indir_Email'] ?? '')));
// 1. Aggiorna o Inserisce su anagrafiche
if (Schema::hasTable('anagrafiche')) {
$existingAnag = DB::table('anagrafiche')
->where(function ($q) use ($codForn, $cf, $piva) {
$q->where('codice_univoco', "FORN_{$codForn}");
if ($cf !== '') {
$q->orWhere('codice_fiscale', $cf);
}
if ($piva !== '') {
$q->orWhere('partita_iva', $piva);
}
})->first();
$anagPayload = [
'ragione_sociale' => $ragSoc,
'cognome' => $cognome,
'nome' => $nome,
'codice_fiscale' => $cf ?: ($existingAnag?->codice_fiscale ?? null),
'partita_iva' => $piva ?: ($existingAnag?->partita_iva ?? null),
'indirizzo' => $indirizzo ?: ($existingAnag?->indirizzo ?? null),
'cap' => $cap ?: ($existingAnag?->cap ?? null),
'citta' => $citta ?: ($existingAnag?->citta ?? null),
'provincia' => $prov ?: ($existingAnag?->provincia ?? null),
'telefono' => ($tel ?: $cell) ?: ($existingAnag?->telefono ?? null),
'email' => $email ?: ($existingAnag?->email ?? null),
'tipo' => $existingAnag?->tipo ?? 'altro',
'updated_at' => now(),
];
if ($existingAnag) {
DB::table('anagrafiche')->where('id', $existingAnag->id)->update($anagPayload);
$countUpdated++;
} else {
$anagPayload['codice_univoco'] = "FORN_{$codForn}";
$anagPayload['created_at'] = now();
DB::table('anagrafiche')->insert($anagPayload);
$countImported++;
}
}
// 2. Aggiorna o Inserisce su fornitori
if (Schema::hasTable('fornitori')) {
$ammId = DB::table('amministratori')->value('id') ?? 13;
$fornData = [
'amministratore_id'=> $ammId,
'cod_forn' => (int) $codForn,
'ragione_sociale' => $ragSoc,
'nome' => $nome,
'cognome' => $cognome,
'codice_fiscale' => $cf ?: null,
'partita_iva' => $piva ?: null,
'indirizzo' => $indirizzo,
'cap' => $cap,
'citta' => $citta,
'provincia' => $prov,
'telefono' => $tel,
'cellulare' => $cell,
'email' => $email,
'iban' => $iban ?: null,
'updated_at' => now(),
];
if (Schema::hasColumn('fornitori', 'codice_univoco')) {
$fornData['codice_univoco'] = "FORN_{$codForn}";
}
$existingForn = DB::table('fornitori')
->where(function ($q) use ($codForn, $cf, $piva) {
$q->where('cod_forn', (int) $codForn);
if ($cf !== '') {
$q->orWhere('codice_fiscale', $cf);
}
if ($piva !== '') {
$q->orWhere('partita_iva', $piva);
}
})->first();
if ($existingForn) {
DB::table('fornitori')->where('id', $existingForn->id)->update($fornData);
} else {
$fornData['created_at'] = now();
DB::table('fornitori')->insert($fornData);
}
}
}
$log("✅ Sincronizzati {$countImported} nuovi fornitori, {$countUpdated} aggiornati.");
return ['imported' => $countImported, 'updated' => $countUpdated];
}
/**
* Sincronizzazione contabile completa per un dato Stabile
*/
public function syncStabile(Stabile|string|int $stabileInput, ?int $targetYear = null, bool $allYears = false, bool $dryRun = false, ?\Closure $log = null): array
{
$log = $log ?: fn($msg) => null;
$stabile = null;
if (is_object($stabileInput)) {
$stabile = $stabileInput;
} else {
$sInput = trim((string) $stabileInput);
$stabile = Stabile::where('codice_stabile', $sInput)
->orWhere('cod_stabile', $sInput)
->first();
if (! $stabile && is_numeric($sInput)) {
$stabile = Stabile::where('codice_stabile', sprintf('%04d', (int) $sInput))
->orWhere('cod_stabile', sprintf('%04d', (int) $sInput))
->first();
}
if (! $stabile && is_numeric($sInput)) {
$stabile = Stabile::find((int) $sInput);
}
}
if (! $stabile) {
$log("❌ Stabile non trovato per input: {$stabileInput}");
return ['success' => false, 'error' => 'Stabile non trovato'];
}
$codStabile = trim((string) ($stabile->codice_stabile ?? $stabile->cod_stabile ?? ''));
$stabileDir = $this->archivesBasePath . '/' . $codStabile;
if (! is_dir($stabileDir)) {
$log("❌ Directory archivio non trovata per lo stabile {$codStabile} in: {$stabileDir}");
return ['success' => false, 'error' => "Directory archivio {$stabileDir} inesistente"];
}
$log("🏢 Avvio sincronizzazione contabile per Stabile [{$codStabile}] {$stabile->denominazione}");
// 1. Sincronizza Fornitori
$this->syncFornitori($log);
// 2. Mappa Annualità da generale_stabile.mdb
$annualitaMap = $this->resolveAnnualitaMap($stabileDir, $log);
if (empty($annualitaMap)) {
$log("⚠️ Nessuna mappa annualità trovata in generale_stabile.mdb, scansiono le directory.");
$dirs = glob($stabileDir . '/[0-9][0-9][0-9][0-9]', GLOB_ONLYDIR);
foreach ($dirs as $d) {
$c = basename($d);
$annualitaMap[$c] = 2000 + (int) $c; // fallback indicativo
}
}
$results = [];
foreach ($annualitaMap as $cartella => $anno) {
if ($targetYear !== null && (int) $anno !== (int) $targetYear && ! $allYears) {
continue;
}
$mdbAnno = $stabileDir . '/' . $cartella . '/singolo_anno.mdb';
if (! is_file($mdbAnno)) {
$log(" Cartella {$cartella} (Anno {$anno}): singolo_anno.mdb non presente, salto.");
continue;
}
$log("----------------------------------------------------------------------");
$log("📂 Elaborazione Cartella [{$cartella}] -> Anno Gestione: {$anno}");
$res = $this->syncSingoloAnno($stabile, $cartella, (int) $anno, $mdbAnno, $dryRun, $log);
$results[$anno] = $res;
}
$log("======================================================================");
$log("🎉 Sincronizzazione contabile completata con successo per Stabile {$codStabile}!");
return [
'success' => true,
'stabile' => $codStabile,
'gestioni' => $results,
];
}
/**
* Risolve la mappa Cartella -> Anno da generale_stabile.mdb (tabella 'anni')
*/
protected function resolveAnnualitaMap(string $stabileDir, \Closure $log): array
{
$genMdb = $stabileDir . '/generale_stabile.mdb';
if (! is_file($genMdb)) {
return [];
}
$rows = $this->exportMdbTable($genMdb, 'anni');
$map = [];
foreach ($rows as $r) {
$cartella = trim((string) ($r['nome_dir'] ?? ''));
$annoO = (int) trim((string) ($r['anno_o'] ?? $r['anno_r'] ?? ''));
if ($cartella !== '' && $annoO > 1900) {
$map[$cartella] = $annoO;
}
}
return $map;
}
/**
* Sincronizza una singola annualità gestionale
*/
protected function syncSingoloAnno(Stabile $stabile, string $cartella, int $anno, string $mdbPath, bool $dryRun, \Closure $log): array
{
$codStabile = (string) $stabile->codice_stabile;
// 1. Risolvi / Crea GestioneContabile
$gestioneOrd = GestioneContabile::firstOrCreate(
[
'stabile_id' => $stabile->id,
'anno_gestione' => $anno,
'tipo_gestione' => 'ordinaria',
],
[
'tenant_id' => 'default',
'codice_archivio_legacy' => $cartella,
'denominazione' => "Gestione Ordinaria {$anno}",
'data_inizio' => "{$anno}-01-01",
'data_fine' => "{$anno}-12-31",
'protocollo_prefix' => 'O' . $anno,
'ultimo_protocollo' => 0,
'percentuale_fondo_riserva' => 0.00,
'rata_ordinaria_mensile' => 0.00,
'usa_millesimi_generali' => 1,
'usa_millesimi_riscaldamento' => 0,
'usa_millesimi_ascensore' => 0,
'stato' => ($anno < (int) date('Y')) ? 'chiusa' : 'aperta',
'gestione_attiva' => ($anno === (int) date('Y')) ? 1 : 0,
]
);
// 1b. Risolvi / Crea Gestione canonica (tabella gestioni con id_gestione)
$gestioneCanonicalId = null;
if (Schema::hasTable('gestioni')) {
$gestioneCanonical = DB::table('gestioni')
->where('stabile_id', $stabile->id)
->where('anno_gestione', $anno)
->where('tipo_gestione', 'Ord.')
->first();
if (! $gestioneCanonical) {
$gestioneCanonicalId = DB::table('gestioni')->insertGetId([
'stabile_id' => $stabile->id,
'anno_gestione' => $anno,
'tipo_gestione' => 'Ord.',
'data_inizio' => "{$anno}-01-01",
'data_fine' => "{$anno}-12-31",
'stato' => ($anno < (int) date('Y')) ? 'chiusa' : 'aperta',
'descrizione' => "Gestione Ordinaria {$anno}",
'created_at' => now(),
'updated_at' => now(),
]);
} else {
$gestioneCanonicalId = $gestioneCanonical->id_gestione;
}
}
// 2. Mappa Unità dello stabile (id_cond -> unita_id)
$unitaMap = $this->buildUnitaMap($stabile, $cartella, $mdbPath);
// 3. Sincronizza Dett_tab (Millesimi e Conguagli di Apertura)
$dettStats = $this->syncDettTab($stabile, $cartella, $anno, $mdbPath, $dryRun, $log);
// 4. Sincronizza Rate Emesse & Conguaglio Finale CF
$rateStats = $this->syncRate($stabile, $gestioneOrd, $cartella, $anno, $mdbPath, $unitaMap, $dryRun, $log);
// 5. Sincronizza Incassi Effettuati
$incassiStats = $this->syncIncassi($stabile, $gestioneOrd, $cartella, $anno, $mdbPath, $unitaMap, $dryRun, $log);
// 6. Sincronizza Operazioni Contabili & Fatture Fornitori
$opsStats = $this->syncOperazioni($stabile, $gestioneOrd, $gestioneCanonicalId, $cartella, $anno, $mdbPath, $dryRun, $log);
// 7. Calcola e Consolida Bilancio di Chiusura / Conguagli
$bilancioStats = $this->consolidateBilancioGestione($stabile, $gestioneOrd, $gestioneCanonicalId, $cartella, $anno, $mdbPath, $unitaMap, $dryRun, $log);
return [
'anno' => $anno,
'cartella' => $cartella,
'dett_tab' => $dettStats,
'rate' => $rateStats,
'incassi' => $incassiStats,
'operazioni' => $opsStats,
'bilancio' => $bilancioStats,
];
}
/**
* Mappatura tra id_cond / cod_cond legacy e unita_immobiliare_id
*/
protected function buildUnitaMap(Stabile $stabile, string $cartella, string $mdbPath): array
{
$map = [];
$units = UnitaImmobiliare::where('stabile_id', $stabile->id)->get();
// 1. Dalla tabella condomin di singolo_anno.mdb
$condominRows = $this->exportMdbTable($mdbPath, 'condomin');
foreach ($condominRows as $r) {
$idCond = trim((string) ($r['id_cond'] ?? ''));
$codCond = trim((string) ($r['cod_cond'] ?? ''));
$scala = trim((string) ($r['scala'] ?? ''));
$interno = trim((string) ($r['interno'] ?? ''));
if ($idCond === '' && $codCond === '') {
continue;
}
$matched = $units->first(function ($u) use ($scala, $interno) {
$uScala = trim((string) ($u->scala ?? ''));
$uInterno = trim((string) ($u->interno ?? ''));
if ($uInterno === '') return false;
if ($scala !== '' && $uScala !== '' && strtoupper($scala) !== strtoupper($uScala)) return false;
return strtolower($uInterno) === strtolower($interno) || ltrim($uInterno, '0') === ltrim($interno, '0');
});
if ($matched) {
if ($idCond !== '') $map['id_' . $idCond] = $matched->id;
if ($codCond !== '') $map['cod_' . $codCond] = $matched->id;
}
}
return $map;
}
/**
* Sincronizza Dett_tab (Millesimi e Conguagli di Apertura)
*/
protected function syncDettTab(Stabile $stabile, string $cartella, int $anno, string $mdbPath, bool $dryRun, \Closure $log): array
{
$rows = $this->exportMdbTable($mdbPath, 'dett_tab');
if (empty($rows)) {
return ['count' => 0];
}
$codStabile = (string) $stabile->codice_stabile;
$count = 0;
if (Schema::connection('gescon_import')->hasTable('dett_tab')) {
// Elimina righe pregresse per la stessa cartella/stabile per evitare duplicazioni
DB::connection('gescon_import')->table('dett_tab')
->where('cod_stabile', $codStabile)
->where('legacy_year', $cartella)
->delete();
$insertData = [];
foreach ($rows as $r) {
$codTab = trim((string) ($r['cod_tab'] ?? ''));
$idCond = trim((string) ($r['id_cond'] ?? ''));
$condInq = strtoupper(trim((string) ($r['cond_inquil'] ?? 'C')));
$mm = is_numeric($r['mm'] ?? null) ? (float) $r['mm'] : null;
$consEuro = is_numeric($r['cons_euro'] ?? null) ? (float) $r['cons_euro'] : 0.0;
$nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : null;
$unico = (! empty($r['unico']) && $r['unico'] !== '0') ? 1 : 0;
if ($codTab === '' || $idCond === '') {
continue;
}
$insertData[] = [
'cod_stabile' => $codStabile,
'legacy_year' => $cartella,
'cod_tab' => $codTab,
'id_cond' => $idCond,
'cond_inquil' => $condInq,
'mm' => $mm,
'cons_euro' => $consEuro,
'n_stra' => $nStra,
'unico' => $unico,
'created_at' => now(),
'updated_at' => now(),
];
$count++;
}
foreach (array_chunk($insertData, 200) as $chunk) {
DB::connection('gescon_import')->table('dett_tab')->insert($chunk);
}
}
$log(" 📊 Sincronizzati {$count} record dett_tab (Millesimi e Conguagli di Apertura)");
return ['count' => $count];
}
/**
* Sincronizza Rate Emesse & Conguaglio Finale CF
*/
protected function syncRate(Stabile $stabile, GestioneContabile $gestione, string $cartella, int $anno, string $mdbPath, array $unitaMap, bool $dryRun, \Closure $log): array
{
$rows = $this->exportMdbTable($mdbPath, 'rate');
if (empty($rows)) {
return ['count' => 0];
}
$count = 0;
$totDovuto = 0.0;
$piano = null;
if (Schema::hasTable('piano_rateizzazione')) {
$piano = DB::table('piano_rateizzazione')->where('stabile_id', $stabile->id)->where('descrizione', 'like', "%Gestione {$anno}%")->first();
if (! $piano) {
$pId = DB::table('piano_rateizzazione')->insertGetId([
'codice_piano' => 'PR_' . $stabile->codice_stabile . '_' . $anno,
'stabile_id' => $stabile->id,
'descrizione' => "Piano Rate Gestione {$anno}",
'tipo_piano' => 'standard',
'importo_totale' => 0.0,
'numero_rate' => 1,
'data_prima_rata' => "{$anno}-01-01",
'frequenza' => 'MENSILE',
'stato' => 'ATTIVO',
'created_at' => now(),
'updated_at' => now(),
]);
$piano = (object) ['id' => $pId];
}
}
$pianoId = $piano ? $piano->id : $gestione->id;
foreach ($rows as $r) {
$idCondomino = trim((string) ($r['id_condomino'] ?? ''));
$proprInquil = strtoupper(trim((string) ($r['propr_inquil'] ?? 'C')));
$nMese = trim((string) ($r['n_mese'] ?? ''));
$ors = strtoupper(trim((string) ($r['o_r_s'] ?? 'O')));
$importo = is_numeric($r['importo_dovuto_euro'] ?? null) ? (float) $r['importo_dovuto_euro'] : 0.0;
$dtEmpag = $this->parseDate($r['dt_empag'] ?? null);
$descrizione = trim((string) ($r['descrizione'] ?? ''));
$nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : null;
$unitaId = $unitaMap['id_' . $idCondomino] ?? null;
$isCF = ($nMese === 'CF' || $dtEmpag === '1999-12-31' || str_contains(strtoupper($descrizione), 'CONGUAGLIO FINALE'));
$rateData = [
'gestione_id' => $gestione->id,
'stabile_id' => $stabile->id,
'unita_immobiliare_id' => $unitaId,
'id_condomino_legacy' => $idCondomino,
'propr_inquil' => $proprInquil,
'n_mese' => $nMese,
'o_r_s' => $ors,
'importo_dovuto_euro' => $importo,
'data_scadenza' => $isCF ? "{$anno}-12-31" : ($dtEmpag ?: "{$anno}-01-01"),
'descrizione' => $descrizione ?: ($isCF ? "Conguaglio Finale (CF) {$anno}" : "Rata {$nMese} Gestione {$anno}"),
'n_stra' => $nStra,
'is_conguaglio_finale' => $isCF ? 1 : 0,
'updated_at' => now(),
];
if ($unitaId && Schema::hasTable('rate_emesse')) {
$soggettoId = null;
if (Schema::hasTable('persone_unita_relazioni')) {
$targetRole = ($proprInquil === 'I') ? 'inquilino' : 'proprietario';
$targetRuoloRate = ($proprInquil === 'I') ? 'I' : 'C';
$rel = DB::table('persone_unita_relazioni')
->where('unita_id', $unitaId)
->where('attivo', 1)
->where(function ($q) use ($targetRole, $targetRuoloRate) {
$q->where('ruolo_rate', $targetRuoloRate)
->orWhere('tipo_relazione', $targetRole);
})->first();
if (! $rel) {
$rel = DB::table('persone_unita_relazioni')
->where('unita_id', $unitaId)
->where('attivo', 1)
->first();
}
if ($rel && ! empty($rel->persona_id)) {
$soggettoId = (int) $rel->persona_id;
}
}
if (! $soggettoId && Schema::hasTable('anagrafiche')) {
$soggettoId = DB::table('anagrafiche')->value('id') ?? 1;
}
$soggettoId = $soggettoId ?: 1;
DB::table('rate_emesse')->updateOrInsert(
[
'piano_rateizzazione_id' => $pianoId,
'unita_immobiliare_id' => $unitaId,
'numero_rata_progressivo'=> $isCF ? 999 : (is_numeric($nMese) ? (int) $nMese : 1),
'descrizione' => $rateData['descrizione'],
],
[
'soggetto_responsabile_id' => $soggettoId,
'importo_originario_unita' => $importo,
'percentuale_addebito_soggetto'=> 100.0,
'importo_addebitato_soggetto' => $importo,
'data_scadenza' => $rateData['data_scadenza'],
'stato_rata' => 'emessa',
'note' => "Importato da Gescon {$cartella} (id_cond: {$idCondomino}, ruolo: {$proprInquil})",
'updated_at' => now(),
]
);
}
$count++;
$totDovuto += $importo;
}
$log(" 📑 Sincronizzate {$count} rate emesse (Totale Dovuto: € " . number_format($totDovuto, 2, ',', '.') . ")");
return ['count' => $count, 'totale_dovuto' => $totDovuto];
}
/**
* Sincronizza Incassi Effettuati
*/
protected function syncIncassi(Stabile $stabile, GestioneContabile $gestione, string $cartella, int $anno, string $mdbPath, array $unitaMap, bool $dryRun, \Closure $log): array
{
$rows = $this->exportMdbTable($mdbPath, 'incassi');
if (empty($rows)) {
return ['count' => 0];
}
$codStabile = (string) $stabile->codice_stabile;
$contiCache = [];
if (Schema::hasTable('conti_bancari')) {
$allConti = DB::table('conti_bancari')->get();
foreach ($allConti as $cb) {
$contiCache[$cb->codice] = $cb->id;
}
}
$count = 0;
$totIncassato = 0.0;
foreach ($rows as $r) {
$idIncasso = trim((string) ($r['ID_incasso'] ?? ''));
$codCond = trim((string) ($r['cod_cond'] ?? ''));
$condInq = strtoupper(trim((string) ($r['cond_inquil'] ?? 'C')));
$importo = is_numeric($r['importo_pagato_euro'] ?? null) ? (float) $r['importo_pagato_euro'] : 0.0;
$dtEmpag = $this->parseDate($r['dt_empag'] ?? null);
$descrizione = trim((string) ($r['descrizione'] ?? ''));
$ors = strtoupper(trim((string) ($r['o_r_s'] ?? 'O')));
$codCassa = trim((string) ($r['cod_cassa'] ?? 'CCB'));
$nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : null;
$unitaId = $unitaMap['cod_' . $codCond] ?? ($unitaMap['id_' . $codCond] ?? null);
$contoCode = sprintf('%04d-%s', (int) $codStabile, $codCassa);
$contoId = $contiCache[$contoCode] ?? null;
if (! $contoId && Schema::hasTable('conti_bancari')) {
$matchingConto = DB::table('conti_bancari')
->where('codice', 'like', sprintf('%04d-%%', (int) $codStabile))
->first();
$contoId = $matchingConto?->id ?? DB::table('conti_bancari')->value('id') ?? 1;
}
$contoId = $contoId ?: 1;
$incassoData = [
'tenant_id' => 'default',
'gestione_id' => $gestione->id,
'condominio_id' => $stabile->id,
'conto_bancario_id' => $contoId,
'anno_rif' => $anno,
'cod_cond_gescon' => $codCond,
'cond_inquil' => $condInq,
'importo_pagato_euro' => $importo,
'importo_pagato' => $importo,
'dt_empag' => $dtEmpag,
'data_pagamento' => $dtEmpag ?: "{$anno}-01-01",
'data_competenza' => "{$anno}-01-01",
'descrizione' => $descrizione ?: "Incasso Gestione {$anno} Cond. {$codCond}",
'o_r_s' => $ors,
'cod_cassa_legacy' => $codCassa,
'proviene_n_stra' => $nStra,
'updated_at' => now(),
];
if (Schema::hasTable('incassi')) {
DB::table('incassi')->updateOrInsert(
[
'gestione_id' => $gestione->id,
'cod_cond_gescon' => $codCond,
'cond_inquil' => $condInq,
'dt_empag' => $dtEmpag,
'importo_pagato_euro' => $importo,
],
$incassoData
);
}
$count++;
$totIncassato += $importo;
}
$log(" 💵 Sincronizzati {$count} incassi registrati (Totale Incassato: € " . number_format($totIncassato, 2, ',', '.') . ")");
return ['count' => $count, 'totale_incassato' => $totIncassato];
}
/**
* Sincronizza Operazioni Contabili, Fatture e Ritenute d'Acconto
*/
protected function syncOperazioni(Stabile $stabile, GestioneContabile $gestione, ?int $gestioneCanonicalId, string $cartella, int $anno, string $mdbPath, bool $dryRun, \Closure $log): array
{
$rows = $this->exportMdbTable($mdbPath, 'Operazioni');
if (empty($rows)) {
return ['count' => 0];
}
$userId = 1;
if (Schema::hasTable('users')) {
$userId = DB::table('users')->where('email', 'michele@netgescon.it')->value('id')
?? (DB::table('users')->value('id') ?: 1);
}
$countOps = 0;
$countRa = 0;
$totSpese = 0.0;
foreach ($rows as $r) {
$idOperaz = trim((string) ($r['id_operaz'] ?? ''));
$nSpe = trim((string) ($r['n_spe'] ?? ''));
$dtSpe = $this->parseDate($r['dt_spe'] ?? null);
$codSpe = trim((string) ($r['cod_spe'] ?? ''));
$tabella = trim((string) ($r['Tabella'] ?? 'TAB.A'));
$importo = is_numeric($r['importo_euro'] ?? null) ? (float) $r['importo_euro'] : 0.0;
$benef = trim((string) ($r['benef'] ?? ''));
$natura2 = trim((string) ($r['natura2'] ?? 'Spesa'));
$codFor = trim((string) ($r['cod_for'] ?? ''));
$numFat = trim((string) ($r['num_fat'] ?? ''));
$dtFat = $this->parseDate($r['dt_fat'] ?? null);
$nettoRda = trim((string) ($r['NettoVers_RDA'] ?? ''));
$rifRda = trim((string) ($r['rif_rda'] ?? ''));
$gestioneTipo= strtoupper(trim((string) ($r['Gestione'] ?? 'O')));
$nStra = is_numeric($r['n_stra'] ?? null) ? (int) $r['n_stra'] : 0;
$incluso = strtoupper(trim((string) ($r['incluso'] ?? 'S')));
if ($importo == 0.0 && $benef === '') {
continue;
}
// Risolvi fornitore
$fornitoreId = null;
if (Schema::hasTable('fornitori')) {
if ($codFor !== '') {
$forn = DB::table('fornitori')->where('cod_forn', (int) $codFor)->first();
$fornitoreId = $forn?->id;
}
if (! $fornitoreId && $benef !== '') {
$forn = DB::table('fornitori')->where('ragione_sociale', $benef)->first();
$fornitoreId = $forn?->id;
}
if (! $fornitoreId && $benef !== '') {
$ammId = DB::table('amministratori')->value('id') ?? 13;
$fornCode = $codFor !== '' ? (int) $codFor : 99000 + (int) ($idOperaz ?: rand(1, 999));
$fornitoreId = DB::table('fornitori')->insertGetId([
'amministratore_id'=> $ammId,
'cod_forn' => $fornCode,
'codice_univoco' => substr('F' . str_pad((string) $fornCode, 7, '0', STR_PAD_LEFT), 0, 8),
'ragione_sociale' => $benef,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
// 1. Registrazione Prima Nota
if (Schema::hasTable('contabilita_registrazioni')) {
$regId = DB::table('contabilita_registrazioni')->updateOrInsert(
[
'stabile_id' => $stabile->id,
'gestione_id' => $gestioneCanonicalId ?: $gestione->id,
'external_reference' => "GESCON_{$cartella}_OP_{$idOperaz}",
],
[
'data_registrazione' => $dtSpe ?: "{$anno}-01-01",
'entry_date' => $dtSpe ?: "{$anno}-01-01",
'document_number' => $numFat ?: ($nSpe ? "N. {$nSpe}" : null),
'document_type' => $natura2 === 'Spesa' ? 'fattura' : 'movimento',
'description' => $benef ?: "Operazione {$codSpe} ({$tabella})",
'status' => 'confirmed',
'user_id' => $userId,
'updated_at' => now(),
]
);
}
// 2. Fattura Fornitore
if ($natura2 === 'Spesa' && ($numFat !== '' || $fornitoreId !== null)) {
if (Schema::hasTable('contabilita_fatture_fornitori')) {
DB::table('contabilita_fatture_fornitori')->updateOrInsert(
[
'stabile_id' => $stabile->id,
'gestione_id' => $gestione->id,
'numero_documento' => $numFat ?: "OP_{$idOperaz}",
'fornitore_id' => $fornitoreId,
],
[
'data_documento' => $dtFat ?: ($dtSpe ?: "{$anno}-01-01"),
'data_registrazione' => $dtSpe ?: "{$anno}-01-01",
'descrizione' => $benef,
'imponibile' => $importo,
'totale' => $importo,
'netto_da_pagare' => $importo,
'stato' => 'registrata',
'ritenuta_aliquota' => str_contains($nettoRda, '4') ? 4.0 : (str_contains($nettoRda, '20') ? 20.0 : 0.0),
'updated_at' => now(),
]
);
}
}
// 3. Ritenute d'Acconto (RA 4% / 20%) & F24
if ($nettoRda !== '' && in_array($nettoRda, ['RDA_4', 'VER_4', 'RDA_20', 'VER_20'], true)) {
$aliquota = str_contains($nettoRda, '4') ? 4.0 : 20.0;
$isVersato = str_starts_with($nettoRda, 'VER_');
if (Schema::hasTable('registro_ritenute_acconto') && $fornitoreId) {
$numProg = is_numeric($rifRda) ? (int) $rifRda : ($countRa + 1);
DB::table('registro_ritenute_acconto')->updateOrInsert(
[
'gestione_id' => $gestione->id,
'fornitore_id' => $fornitoreId,
'rif_rda' => $rifRda ?: (string) $idOperaz,
],
[
'tenant_id' => 'default',
'numero_progressivo' => $numProg,
'data_competenza' => $dtSpe ?: "{$anno}-01-01",
'imponibile' => $importo,
'aliquota_ritenuta' => $aliquota,
'importo_ritenuta' => round($importo * ($aliquota / 100), 2),
'codice_tributo' => $aliquota == 4.0 ? '1019' : '1040',
'tipo_ritenuta' => $aliquota == 4.0 ? 'condominio' : 'professionale',
'stato_versamento' => $isVersato ? 'versata' : 'da_versare',
'data_versamento' => $isVersato ? $dtSpe : null,
'updated_at' => now(),
]
);
$countRa++;
}
}
if ($natura2 === 'Spesa') {
$totSpese += $importo;
}
$countOps++;
}
$log(" 📝 Sincronizzate {$countOps} operazioni contabili (Spese Consuntive: € " . number_format($totSpese, 2, ',', '.') . ", Ritenute RA: {$countRa})");
return ['count' => $countOps, 'totale_spese' => $totSpese, 'ritenute_ra' => $countRa];
}
/**
* Calcolo e Consolidamento del Bilancio di Chiusura / Conguagli
*/
protected function consolidateBilancioGestione(Stabile $stabile, GestioneContabile $gestione, ?int $gestioneCanonicalId, string $cartella, int $anno, string $mdbPath, array $unitaMap, bool $dryRun, \Closure $log): array
{
// 1. Calcola Totale Spese Consuntive
$totSpese = 0.0;
if (Schema::hasTable('contabilita_fatture_fornitori')) {
$totSpese = (float) DB::table('contabilita_fatture_fornitori')
->where('stabile_id', $stabile->id)
->where('gestione_id', $gestione->id)
->sum('totale');
}
// 2. Calcola Totale Entrate
$totEntrate = 0.0;
if (Schema::hasTable('rate_emesse')) {
$piano = DB::table('piano_rateizzazione')->where('stabile_id', $stabile->id)->where('descrizione', 'like', "%Gestione {$anno}%")->first();
$pianoId = $piano ? $piano->id : $gestione->id;
$totEntrate = (float) DB::table('rate_emesse')
->where('piano_rateizzazione_id', $pianoId)
->sum('importo_originario_unita');
}
// 3. Salva / Aggiorna Bilancio Consuntivo
if (Schema::hasTable('bilanci')) {
DB::table('bilanci')->updateOrInsert(
[
'stabile_id' => $stabile->id,
'gestione_id' => $gestioneCanonicalId ?: $gestione->id,
'anno_riferimento' => $anno,
'tipo_bilancio' => 'consuntivo',
],
[
'denominazione' => "Bilancio Consuntivo Gestione {$anno}",
'totale_entrate' => $totEntrate,
'totale_uscite' => $totSpese,
'saldo' => round($totEntrate - $totSpese, 2),
'stato' => ($anno < (int) date('Y')) ? 'CHIUSO' : 'DRAFT',
'updated_at' => now(),
]
);
}
$log(" ⚖️ Bilancio Consuntivo consolidato: Entrate € " . number_format($totEntrate, 2, ',', '.') . " | Spese € " . number_format($totSpese, 2, ',', '.') . " | Saldo: € " . number_format($totEntrate - $totSpese, 2, ',', '.'));
return ['entrate' => $totEntrate, 'uscite' => $totSpese, 'saldo' => $totEntrate - $totSpese];
}
/**
* Utility per esportare una tabella Access MDB in array di array associativi
*/
public function exportMdbTable(string $mdbPath, string $tableName): array
{
if (! is_file($mdbPath)) {
return [];
}
$cmd = sprintf('mdb-export -D %s -q %s %s %s', escapeshellarg('%Y-%m-%d %H:%M:%S'), escapeshellarg('^'), escapeshellarg($mdbPath), escapeshellarg($tableName));
$proc = Process::fromShellCommandline($cmd);
$proc->run();
if (! $proc->isSuccessful()) {
return [];
}
$output = $proc->getOutput();
$lines = explode("\n", trim($output));
if (empty($lines) || count($lines) < 2) {
return [];
}
$headerLine = array_shift($lines);
$headers = str_getcsv($headerLine, ',', '^');
$headers = array_map(fn($h) => trim((string) $h), $headers);
$results = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
$row = str_getcsv($line, ',', '^');
if (count($row) === count($headers)) {
$results[] = array_combine($headers, $row);
}
}
return $results;
}
/**
* Normalizza data legacy MDB in Y-m-d
*/
protected function parseDate(?string $dateStr): ?string
{
if (! $dateStr) {
return null;
}
$dateStr = trim($dateStr);
if ($dateStr === '' || $dateStr === '—') {
return null;
}
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateStr, $m)) {
return "{$m[1]}-{$m[2]}-{$m[3]}";
}
if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/', $dateStr, $m)) {
$year = (int) $m[3];
if ($year < 100) {
$year = ($year > 50) ? (1900 + $year) : (2000 + $year);
}
$month = str_pad($m[1], 2, '0', STR_PAD_LEFT);
$day = str_pad($m[2], 2, '0', STR_PAD_LEFT);
return sprintf('%04d-%02d-%02d', $year, $month, $day);
}
return null;
}
}