1282 lines
57 KiB
PHP
1282 lines
57 KiB
PHP
<?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;
|
||
}
|
||
|
||
// 3. Riconciliazione automatica con Estratti Conto Bancari
|
||
$recSummary = $this->reconcileStabileAccountingAndBank($stabile, $log);
|
||
|
||
$log("======================================================================");
|
||
$log("🎉 Sincronizzazione contabile completata con successo per Stabile {$codStabile}!");
|
||
|
||
return [
|
||
'success' => true,
|
||
'stabile' => $codStabile,
|
||
'gestioni' => $results,
|
||
'riconciliazione' => $recSummary,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
|
||
$soggettoId = null;
|
||
if ($unitaId && Schema::hasTable('persone_unita_relazioni')) {
|
||
$targetRole = ($condInq === 'I') ? 'inquilino' : 'proprietario';
|
||
$targetRuoloRate = ($condInq === '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;
|
||
}
|
||
|
||
$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,
|
||
'condomino_id' => $soggettoId,
|
||
'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++;
|
||
}
|
||
}
|
||
|
||
// 4. Operazione Contabile Generale (Partita Doppia / Hub Ordinarie)
|
||
if (Schema::hasTable('operazioni_contabili')) {
|
||
$voceSpesaId = null;
|
||
if ($codSpe !== '' && Schema::hasTable('voci_spesa')) {
|
||
$voceSpesaId = DB::table('voci_spesa')
|
||
->where('gestione_contabile_id', $gestione->id)
|
||
->where(function ($q) use ($codSpe) {
|
||
$q->where('codice', $codSpe)->orWhere('legacy_codice', $codSpe);
|
||
})
|
||
->value('id');
|
||
}
|
||
|
||
$isEntrata = str_starts_with(strtoupper($codSpe), 'INC');
|
||
$dare = ($natura2 === 'Spesa' || ! $isEntrata) ? $importo : 0.0;
|
||
$avere = $isEntrata ? $importo : 0.0;
|
||
|
||
$feFatturaId = null;
|
||
if (Schema::hasTable('contabilita_fatture_fornitori') && $fornitoreId && $numFat !== '') {
|
||
$feFatturaId = DB::table('contabilita_fatture_fornitori')
|
||
->where('stabile_id', $stabile->id)
|
||
->where('fornitore_id', $fornitoreId)
|
||
->where('numero_documento', $numFat)
|
||
->value('id');
|
||
}
|
||
|
||
$protNum = is_numeric($nSpe) && (int) $nSpe > 0 ? (int) $nSpe : (int) ($idOperaz ?: $countOps + 1);
|
||
|
||
DB::table('operazioni_contabili')->updateOrInsert(
|
||
[
|
||
'gestione_id' => $gestione->id,
|
||
'legacy_id' => (int) ($idOperaz ?: $protNum),
|
||
],
|
||
[
|
||
'tenant_id' => 'default',
|
||
'fornitore_id' => $fornitoreId,
|
||
'voce_spesa_id' => $voceSpesaId,
|
||
'cod_spe' => $codSpe ?: null,
|
||
'tabella' => $tabella ?: null,
|
||
'cod_for' => $codFor ?: null,
|
||
'num_fat' => $numFat ?: null,
|
||
'dt_fat' => $dtFat,
|
||
'rif_rda' => $rifRda ?: null,
|
||
'fe_fattura_id' => $feFatturaId,
|
||
'descrizione' => $benef ?: "Operazione {$codSpe} ({$tabella})",
|
||
'compet' => in_array(strtoupper(trim((string)($r['compet'] ?? ''))), ['C', 'P']) ? strtoupper(trim((string)$r['compet'])) : 'C',
|
||
'natura2' => $natura2 ?: 'Spesa',
|
||
'n_stra' => $nStra,
|
||
'protocollo_numero' => $protNum,
|
||
'protocollo_completo' => "{$gestioneTipo}{$anno}-" . str_pad((string) $protNum, 3, '0', STR_PAD_LEFT),
|
||
'dare' => $dare,
|
||
'avere' => $avere,
|
||
'conto_contabile' => $codSpe ?: null,
|
||
'stato_operazione' => 'confermata',
|
||
'data_operazione' => $dtSpe ?: "{$anno}-01-01",
|
||
'data_competenza' => $dtSpe ?: "{$anno}-01-01",
|
||
'updated_at' => now(),
|
||
]
|
||
);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* Riconciliazione automatica deterministica tra Estratti Conto Bancari, Incassi e Spese
|
||
*/
|
||
public function reconcileStabileAccountingAndBank(Stabile $stabile, ?\Closure $log = null): array
|
||
{
|
||
$log = $log ?: fn($msg) => null;
|
||
$stabileId = (int) $stabile->id;
|
||
|
||
if (! Schema::hasTable('contabilita_movimenti_banca')) {
|
||
return ['reconciled_incassi' => 0, 'reconciled_spese' => 0];
|
||
}
|
||
|
||
$log("🔄 Avvio riconciliazione contabile ed estratti conto per Stabile #{$stabileId}...");
|
||
|
||
$movements = DB::table('contabilita_movimenti_banca')
|
||
->where('stabile_id', $stabileId)
|
||
->get();
|
||
|
||
if ($movements->isEmpty()) {
|
||
$log("ℹ️ Nessun movimento bancario registrato per questo stabile.");
|
||
return ['reconciled_incassi' => 0, 'reconciled_spese' => 0];
|
||
}
|
||
|
||
// 1. Precarica Unità Immobiliari e Nominativi
|
||
$unitaList = DB::table('unita_immobiliari')
|
||
->where('stabile_id', $stabileId)
|
||
->get(['id', 'codice_unita', 'scala', 'interno', 'piano']);
|
||
|
||
$relazioni = DB::table('persone_unita_relazioni as r')
|
||
->join('unita_immobiliari as u', 'u.id', '=', 'r.unita_id')
|
||
->join('anagrafiche as a', 'a.id', '=', 'r.persona_id')
|
||
->where('u.stabile_id', $stabileId)
|
||
->where('r.attivo', 1)
|
||
->select([
|
||
'r.unita_id',
|
||
'r.persona_id',
|
||
'r.ruolo_rate',
|
||
'a.nome',
|
||
'a.cognome',
|
||
'a.ragione_sociale',
|
||
'u.scala',
|
||
'u.interno',
|
||
])->get();
|
||
|
||
// 2. Precarica Fornitori
|
||
$fornitori = DB::table('fornitori')->get(['id', 'cod_forn', 'ragione_sociale', 'partita_iva', 'codice_fiscale']);
|
||
|
||
// 3. Precarica Operazioni Contabili
|
||
$operazioni = DB::table('operazioni_contabili as o')
|
||
->join('gestioni_contabili as g', 'g.id', '=', 'o.gestione_id')
|
||
->where('g.stabile_id', $stabileId)
|
||
->select([
|
||
'o.id',
|
||
'o.fornitore_id',
|
||
'o.dare',
|
||
'o.num_fat',
|
||
'o.data_operazione',
|
||
'o.descrizione',
|
||
'o.cod_spe',
|
||
])->get();
|
||
|
||
// 4. Precarica Incassi
|
||
$incassi = DB::table('incassi')
|
||
->where('condominio_id', $stabileId)
|
||
->get(['id', 'condomino_id', 'cod_cond_gescon', 'cond_inquil', 'importo_pagato_euro', 'data_pagamento']);
|
||
|
||
$countRecIncassi = 0;
|
||
$countRecSpese = 0;
|
||
|
||
foreach ($movements as $mov) {
|
||
$imp = (float) $mov->importo;
|
||
$dataMov = (string) $mov->data;
|
||
$mittente = strtoupper(trim((string) ($mov->mittente ?? '')));
|
||
$beneficiario = strtoupper(trim((string) ($mov->beneficiario ?? '')));
|
||
$desc = strtoupper(trim((string) ($mov->descrizione ?? '')));
|
||
$descEst = strtoupper(trim((string) ($mov->descrizione_estesa ?? '')));
|
||
$fullText = $mittente . ' ' . $beneficiario . ' ' . $desc . ' ' . $descEst;
|
||
|
||
$updateMov = [];
|
||
|
||
if ($imp > 0) {
|
||
// ENTRATA / INCASSO
|
||
$matchedUnitaId = null;
|
||
$matchedPersonaId = null;
|
||
|
||
// Cerca per nominativo anagrafica
|
||
foreach ($relazioni as $rel) {
|
||
$cog = strtoupper(trim((string) $rel->cognome));
|
||
$nom = strtoupper(trim((string) $rel->nome));
|
||
$rag = strtoupper(trim((string) $rel->ragione_sociale));
|
||
|
||
if ($cog !== '' && strlen($cog) >= 3 && str_contains($fullText, $cog)) {
|
||
$matchedUnitaId = (int) $rel->unita_id;
|
||
$matchedPersonaId = (int) $rel->persona_id;
|
||
break;
|
||
}
|
||
if ($rag !== '' && strlen($rag) >= 3 && str_contains($fullText, $rag)) {
|
||
$matchedUnitaId = (int) $rel->unita_id;
|
||
$matchedPersonaId = (int) $rel->persona_id;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Cerca per scala/interno (es. "INT. 6", "B 6", "INT.6")
|
||
if (! $matchedUnitaId) {
|
||
foreach ($unitaList as $u) {
|
||
$int = strtoupper(trim((string) $u->interno));
|
||
$sc = strtoupper(trim((string) $u->scala));
|
||
if ($int !== '') {
|
||
if (preg_match('/\bINT(?:ERNO)?\.?\s*' . preg_quote($int, '/') . '\b/i', $fullText)
|
||
|| preg_match('/\b' . preg_quote($sc, '/') . '\s*' . preg_quote($int, '/') . '\b/i', $fullText)) {
|
||
$matchedUnitaId = (int) $u->id;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Cerca incasso corrispondente
|
||
$matchedIncasso = null;
|
||
if ($matchedUnitaId) {
|
||
$matchedIncasso = $incassi->first(function ($inc) use ($imp) {
|
||
return abs((float) $inc->importo_pagato_euro - $imp) < 0.01;
|
||
});
|
||
} else {
|
||
$matchedIncasso = $incassi->first(function ($inc) use ($imp, $dataMov) {
|
||
return abs((float) $inc->importo_pagato_euro - $imp) < 0.01
|
||
&& abs(strtotime((string) $inc->data_pagamento) - strtotime($dataMov)) <= 86400 * 45;
|
||
});
|
||
}
|
||
|
||
if ($matchedIncasso) {
|
||
$updateMov['incasso_id'] = $matchedIncasso->id;
|
||
$updateMov['rubrica_mittente_id'] = $matchedIncasso->condomino_id ?: $matchedPersonaId;
|
||
$updateMov['stato_riconciliazione'] = 'riconciliato_incasso';
|
||
$updateMov['da_confermare'] = false;
|
||
DB::table('incassi')->where('id', $matchedIncasso->id)->update([
|
||
'movimento_bancario_id' => $mov->id,
|
||
'stato_riconciliazione' => 'automatica',
|
||
]);
|
||
$countRecIncassi++;
|
||
} elseif ($matchedUnitaId) {
|
||
$updateMov['unita_immobiliare_id'] = $matchedUnitaId;
|
||
if ($matchedPersonaId) {
|
||
$updateMov['rubrica_mittente_id'] = $matchedPersonaId;
|
||
}
|
||
$updateMov['stato_riconciliazione'] = 'riconciliato_incasso';
|
||
$updateMov['da_confermare'] = false;
|
||
$countRecIncassi++;
|
||
} else {
|
||
$updateMov['stato_riconciliazione'] = 'da_riconciliare';
|
||
$updateMov['da_confermare'] = true;
|
||
}
|
||
|
||
if ($matchedUnitaId) {
|
||
$updateMov['unita_immobiliare_id'] = $matchedUnitaId;
|
||
}
|
||
} else {
|
||
// USCITA / SPESA
|
||
$absImp = abs($imp);
|
||
$fornId = $mov->fornitore_id;
|
||
|
||
if (! $fornId && $beneficiario !== '') {
|
||
foreach ($fornitori as $f) {
|
||
$fRag = strtoupper(trim((string) $f->ragione_sociale));
|
||
if ($fRag !== '' && strlen($fRag) >= 4 && str_contains($fullText, $fRag)) {
|
||
$fornId = (int) $f->id;
|
||
$updateMov['fornitore_id'] = $fornId;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
$tipoOp = $mov->tipo_operazione;
|
||
if (in_array($tipoOp, ['f24', 'delega_f24'], true) || str_contains($fullText, 'AGENZIA ENTRATE') || str_contains($fullText, 'I24')) {
|
||
$updateMov['stato_riconciliazione'] = 'f24_tributi';
|
||
$updateMov['da_confermare'] = false;
|
||
$countRecSpese++;
|
||
} elseif (in_array($tipoOp, ['cbill', 'pagamento_utenze_cbill'], true) || ! empty($mov->codice_cbill) || str_contains($fullText, 'CBILL')) {
|
||
$updateMov['stato_riconciliazione'] = 'utenza_cbill';
|
||
$updateMov['da_confermare'] = false;
|
||
$countRecSpese++;
|
||
} elseif (in_array($tipoOp, ['commissioni', 'competenze', 'imposta_bollo', 'spese_banca'], true) || str_contains($fullText, 'COMM.') || str_contains($fullText, 'INT. E COMP.')) {
|
||
$updateMov['stato_riconciliazione'] = 'spese_bancarie';
|
||
$updateMov['da_confermare'] = false;
|
||
$countRecSpese++;
|
||
} elseif (in_array($tipoOp, ['addebito_sdd', 'sdd'], true) || ! empty($mov->codice_sdd)) {
|
||
$updateMov['stato_riconciliazione'] = 'utenza_sdd';
|
||
$updateMov['da_confermare'] = false;
|
||
$countRecSpese++;
|
||
} elseif ($fornId) {
|
||
// Match con operazioni_contabili
|
||
$rifFat = trim((string) ($mov->rif_documento ?? ''));
|
||
$matchedOp = $operazioni->first(function ($op) use ($fornId, $absImp, $rifFat) {
|
||
if ((int) $op->fornitore_id !== (int) $fornId) {
|
||
return false;
|
||
}
|
||
if ($rifFat !== '' && ! empty($op->num_fat) && str_contains($rifFat, (string) $op->num_fat)) {
|
||
return true;
|
||
}
|
||
return abs((float) $op->dare - $absImp) < 0.01;
|
||
});
|
||
|
||
if ($matchedOp) {
|
||
$updateMov['operazione_contabile_id'] = $matchedOp->id;
|
||
$updateMov['stato_riconciliazione'] = 'riconciliato_spesa';
|
||
$updateMov['da_confermare'] = false;
|
||
DB::table('operazioni_contabili')->where('id', $matchedOp->id)->update([
|
||
'movimento_bancario_id' => $mov->id,
|
||
]);
|
||
$countRecSpese++;
|
||
} else {
|
||
$updateMov['stato_riconciliazione'] = 'da_riconciliare';
|
||
$updateMov['da_confermare'] = false;
|
||
}
|
||
} else {
|
||
$updateMov['stato_riconciliazione'] = 'da_riconciliare';
|
||
$updateMov['da_confermare'] = true;
|
||
}
|
||
}
|
||
|
||
if (! empty($updateMov)) {
|
||
$updateMov['updated_at'] = now();
|
||
DB::table('contabilita_movimenti_banca')
|
||
->where('id', $mov->id)
|
||
->update($updateMov);
|
||
}
|
||
}
|
||
|
||
$log("✅ Riconciliazione completata: {$countRecIncassi} incassi/crediti riconciliati, {$countRecSpese} spese/uscite/tributi riconciliate.");
|
||
return [
|
||
'reconciled_incassi' => $countRecIncassi,
|
||
'reconciled_spese' => $countRecSpese,
|
||
];
|
||
}
|
||
}
|