option('stabile-id') ?? 0); if ($stabileId <= 0) { $this->error('Serve --stabile-id valido.'); return 1; } $yearOpt = $this->option('year'); $year = is_numeric($yearOpt) ? (int) $yearOpt : null; $tipo = strtolower((string) ($this->option('tipo') ?? 'all')); if (! in_array($tipo, ['all', 'ordinaria', 'straordinaria', 'riscaldamento'], true)) { $this->error('Valore --tipo non valido. Usa all|ordinaria|straordinaria|riscaldamento'); return 1; } $dry = (bool) $this->option('dry'); $systemUserId = (int) (User::query()->orderBy('id')->value('id') ?? 0); $stabile = Stabile::query()->find($stabileId); $amministratoreId = (int) ($stabile?->amministratore_id ?? 0); $legacyStableCodes = array_values(array_unique(array_filter(array_map( static fn($value): string => trim((string) $value), [$stabile?->codice_stabile ?? null, $stabile?->cod_stabile ?? null] )))); if (! Schema::connection('gescon_import')->hasTable('operazioni')) { $this->warn('Tabella di staging "operazioni" non trovata. Nessun movimento da riallineare.'); return 0; } $query = DB::connection('gescon_import')->table('operazioni'); if (Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile') && $legacyStableCodes !== []) { $scopedExists = DB::connection('gescon_import') ->table('operazioni') ->whereIn('cod_stabile', $legacyStableCodes) ->exists(); if ($scopedExists) { $query->whereIn('cod_stabile', $legacyStableCodes); } else { $this->warn('Nessuna operazione legacy con cod_stabile in [' . implode(', ', $legacyStableCodes) . ']. Non importo righe non scoperte per evitare contaminazioni tra stabili.'); return 0; } } if ($year) { if (Schema::connection('gescon_import')->hasColumn('operazioni', 'legacy_year')) { $query->where('legacy_year', (string) $year); } else { $query->whereYear('dt_spe', $year); } } if ($tipo !== 'all') { $gestioneCode = match ($tipo) { 'ordinaria' => 'O', 'straordinaria' => 'S', 'riscaldamento' => 'R', default => null, }; if ($gestioneCode) { $query->where('gestione', $gestioneCode); } } $availableColumns = Schema::connection('gescon_import')->getColumnListing('operazioni'); $desiredColumns = [ 'id_operaz', 'dt_spe', 'benef', 'num_fat', 'note', 'natura', 'gestione', 'n_stra', 'cod_spe', 'tabella', 'cod_for', 'importo', 'importo_euro', 'importo_spese', 'importo_entrate', 'compet', 'cod_stabile', 'legacy_year', ]; $selectColumns = []; foreach ($desiredColumns as $col) { if (in_array($col, $availableColumns, true)) { $selectColumns[] = $col; } else { $selectColumns[] = DB::raw("NULL as `{$col}`"); } } $rows = $query ->orderBy('dt_spe') ->orderBy('id_operaz') ->get($selectColumns); $created = 0; $updated = 0; $unchanged = 0; $movimentiSynced = 0; $fornitoriLinked = 0; $vociLinked = 0; foreach ($rows as $row) { $legacyId = (int) ($row->id_operaz ?? 0); if ($legacyId <= 0) { continue; } $dataReg = ! empty($row->dt_spe) ? (string) $row->dt_spe : now()->toDateString(); $tipoGestione = match (strtoupper((string) ($row->gestione ?? 'O'))) { 'S' => 'straordinaria', 'R' => 'riscaldamento', default => 'ordinaria', }; $legacyMeta = $this->resolveLegacyAnnualMeta( (string) ($row->cod_stabile ?? ($legacyStableCodes[0] ?? '')), trim((string) ($row->legacy_year ?? '')), $tipoGestione ); $anno = $legacyMeta['anno_gestione'] ?? (int) substr($dataReg, 0, 4); $legacyArchiveCode = $legacyMeta['codice_archivio_legacy'] ?? null; $periodStart = $legacyMeta['data_inizio'] ?? null; $periodEnd = $legacyMeta['data_fine'] ?? null; $legacyLabel = $legacyMeta['label'] ?? null; $gestioneQuery = GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('anno_gestione', $anno) ->where('tipo_gestione', $tipoGestione); if ($legacyArchiveCode && Schema::hasColumn('gestioni_contabili', 'codice_archivio_legacy')) { $gestioneQuery->where(function (Builder $q) use ($legacyArchiveCode): void { $q->where('codice_archivio_legacy', $legacyArchiveCode) ->orWhereNull('codice_archivio_legacy'); }); } if ($tipoGestione === 'straordinaria') { $nStra = is_numeric($row->n_stra ?? null) ? (int) $row->n_stra : null; if ($nStra && $nStra > 0) { $gestioneQuery->where('numero_straordinaria', $nStra); } } $gestione = $gestioneQuery ->orderByRaw("CASE WHEN codice_archivio_legacy = ? THEN 0 WHEN codice_archivio_legacy IS NULL THEN 1 ELSE 2 END", [$legacyArchiveCode]) ->orderByDesc('id') ->first(); if (! $gestione instanceof GestioneContabile) { $gestione = $this->ensureLegacyGestione( $stabileId, $anno, $tipoGestione, $dataReg, is_numeric($row->n_stra ?? null) ? (int) $row->n_stra : null, $legacyArchiveCode, $periodStart, $periodEnd, $legacyLabel ); } elseif ($legacyArchiveCode && Schema::hasColumn('gestioni_contabili', 'codice_archivio_legacy')) { $updates = []; if (blank($gestione->codice_archivio_legacy) || str_starts_with((string) $gestione->codice_archivio_legacy, 'operazioni:')) { $updates['codice_archivio_legacy'] = $legacyArchiveCode; } if ($periodStart && empty($gestione->data_inizio)) { $updates['data_inizio'] = $periodStart; } if ($periodEnd && empty($gestione->data_fine)) { $updates['data_fine'] = $periodEnd; } if ($legacyLabel && empty($gestione->denominazione)) { $updates['denominazione'] = $legacyLabel; } if ($updates !== []) { $gestione->fill($updates); if (! $dry) { $gestione->save(); } } } $resolvedGestioneId = $gestione ? Registrazione::resolveGestioneIdOrDefault((int) $gestione->id, $stabileId, $dataReg) : Registrazione::resolveGestioneIdOrDefault(0, $stabileId, $dataReg); if (! $resolvedGestioneId) { $this->warn('Gestione non risolta per operazione legacy #' . $legacyId . ' anno ' . $anno . ' tipo ' . $tipoGestione . ': riga saltata.'); continue; } $legacyClassification = $this->classifyLegacyOperation($row); $voceSpesaId = $this->resolveVoceSpesaId($stabileId, $resolvedGestioneId, (string) ($row->cod_spe ?? '')); $fornitoreId = $this->resolveFornitoreId($amministratoreId, $row->cod_for ?? null); if ($voceSpesaId > 0) { $vociLinked++; } if ($fornitoreId > 0) { $fornitoriLinked++; } $payload = [ 'stabile_id' => $stabileId, 'gestione_id' => $resolvedGestioneId, 'data_registrazione' => $dataReg, 'entry_date' => $dataReg, 'description' => trim((string) ($row->benef ?: $row->note ?: $row->natura ?: 'Operazione legacy #' . $legacyId)), 'document_type' => 'LEGACY_GESCON', 'document_number' => trim((string) ($row->num_fat ?? '')) !== '' ? trim((string) $row->num_fat) : ('LEG-' . $legacyId), 'external_reference' => 'OPERAZIONE_LEGACY:' . $legacyId, 'protocol' => 'LG' . $anno . '-' . str_pad((string) $legacyId, 6, '0', STR_PAD_LEFT), 'protocollo_prefix' => 'LG' . $anno, 'protocollo_numero' => $legacyId, 'protocollo_completo' => 'LG' . $anno . '-' . $legacyId, 'status' => 'confirmed', ]; if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) { $payload['user_id'] = $systemUserId > 0 ? $systemUserId : null; } if (Schema::hasColumn('contabilita_registrazioni', 'created_by')) { $payload['created_by'] = $systemUserId > 0 ? $systemUserId : null; } if (Schema::hasColumn('contabilita_registrazioni', 'updated_by')) { $payload['updated_by'] = $systemUserId > 0 ? $systemUserId : null; } $existing = Registrazione::query() ->where('stabile_id', $stabileId) ->where('external_reference', $payload['external_reference']) ->first(); if (! $existing) { if (! $dry) { $existing = Registrazione::query()->create($payload); $this->syncMovimentiLegacy($existing, $row, $voceSpesaId, $fornitoreId, $legacyClassification); $movimentiSynced++; } $created++; continue; } $dirty = false; foreach ($payload as $field => $value) { if ((string) ($existing->{$field} ?? '') !== (string) ($value ?? '')) { $existing->{$field} = $value; $dirty = true; } } if (! $dry && $dirty) { if (empty($existing->protocol) || empty($existing->protocollo_completo)) { $existing->applyAutoProtocols(); } $existing->save(); } if (! $dry) { $this->syncMovimentiLegacy($existing, $row, $voceSpesaId, $fornitoreId, $legacyClassification); $movimentiSynced++; } if ($dirty) { $updated++; } else { $unchanged++; } } $this->info('Sync completato: creati=' . $created . ' aggiornati=' . $updated . ' invariati=' . $unchanged . ' movimenti=' . $movimentiSynced . ' voci_link=' . $vociLinked . ' fornitori_link=' . $fornitoriLinked . ($dry ? ' (DRY-RUN)' : '')); return 0; } private function resolveVoceSpesaId(int $stabileId, ?int $gestioneId, string $codSpe): int { $codSpe = trim($codSpe); if ($codSpe === '' || ! Schema::hasTable('voci_spesa')) { return 0; } $query = VoceSpesa::query() ->where('stabile_id', $stabileId) ->where(function (Builder $q) use ($codSpe): void { $q->where('legacy_codice', $codSpe) ->orWhere('codice', $codSpe); }); if ($gestioneId && Schema::hasColumn('voci_spesa', 'gestione_contabile_id')) { $clone = (clone $query)->where('gestione_contabile_id', $gestioneId)->orderBy('id')->first(); if ($clone instanceof VoceSpesa) { return (int) $clone->id; } } $voce = $query->orderByDesc('attiva')->orderBy('id')->first(); return $voce instanceof VoceSpesa ? (int) $voce->id : 0; } private function ensureLegacyGestione( int $stabileId, int $anno, string $tipoGestione, string $dataReg, ?int $numeroStraordinaria = null, ?string $legacyArchiveCode = null, ?string $periodStart = null, ?string $periodEnd = null, ?string $legacyLabel = null ): ?GestioneContabile { if ($stabileId <= 0 || $anno <= 0) { return null; } $query = GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('anno_gestione', $anno) ->where('tipo_gestione', $tipoGestione); if ($tipoGestione === 'straordinaria' && $numeroStraordinaria && $numeroStraordinaria > 0) { $query->where('numero_straordinaria', $numeroStraordinaria); } $existing = $query->orderByDesc('id')->first(); if ($existing instanceof GestioneContabile) { return $existing; } $start = $periodStart ?: sprintf('%04d-01-01', $anno); $end = $periodEnd ?: sprintf('%04d-12-31', $anno); if ($tipoGestione === 'riscaldamento') { $start = $periodStart ?: sprintf('%04d-01-01', $anno); $end = $periodEnd ?: sprintf('%04d-12-31', $anno); } $denominazione = $legacyLabel ?: ('Legacy GESCON ' . $anno . ' - ' . ucfirst($tipoGestione)); return GestioneContabile::query()->create([ 'tenant_id' => $this->resolveTenantIdForStabile($stabileId), 'stabile_id' => $stabileId, 'anno_gestione' => $anno, 'tipo_gestione' => $tipoGestione, 'numero_straordinaria' => $tipoGestione === 'straordinaria' && $numeroStraordinaria && $numeroStraordinaria > 0 ? $numeroStraordinaria : null, 'denominazione' => $denominazione, 'data_inizio' => $start, 'data_fine' => $end, 'stato' => 'chiusa', 'protocollo_prefix' => 'LEG' . $anno, 'ultimo_protocollo' => 0, 'gestione_attiva' => false, 'codice_archivio_legacy' => $legacyArchiveCode ?: ('operazioni:' . $anno), 'note_gestione' => 'Creata automaticamente per import operazioni legacy GESCON da Prima Nota. Data riferimento: ' . $dataReg, ]); } private function resolveLegacyAnnualMeta(string $codStabile, string $legacyYear, string $tipoGestione): array { $codStabile = trim($codStabile); $legacyYear = trim($legacyYear); $cacheKey = implode('|', [$codStabile, $legacyYear, $tipoGestione]); if (array_key_exists($cacheKey, $this->legacyAnnualMetaCache)) { return $this->legacyAnnualMetaCache[$cacheKey]; } if ($codStabile === '' || $legacyYear === '' || ! Schema::connection('gescon_import')->hasTable('gestioni_annuali')) { return $this->legacyAnnualMetaCache[$cacheKey] = []; } $row = DB::connection('gescon_import') ->table('gestioni_annuali') ->where('cod_stabile', $codStabile) ->where('cartella', $legacyYear) ->first([ 'cartella', 'anno_ordinario', 'anno_riscaldamento', 'descrizione', 'ordinaria_dal', 'ordinaria_al', 'riscaldamento_dal', 'riscaldamento_al', ]); if (! $row) { return $this->legacyAnnualMetaCache[$cacheKey] = []; } $isRiscaldamento = $tipoGestione === 'riscaldamento'; $annoLabel = trim((string) ($isRiscaldamento ? ($row->anno_riscaldamento ?? '') : ($row->anno_ordinario ?? ''))); $anno = $this->extractLegacyYearInt($annoLabel) ?: $this->extractLegacyYearInt((string) ($row->descrizione ?? '')) ?: $this->extractLegacyYearInt($legacyYear); $start = $isRiscaldamento ? ($row->riscaldamento_dal ?: $row->ordinaria_dal) : $row->ordinaria_dal; $end = $isRiscaldamento ? ($row->riscaldamento_al ?: $row->ordinaria_al) : $row->ordinaria_al; $label = trim((string) ($row->descrizione ?? '')); if ($label === '') { $label = ucfirst($tipoGestione) . ' ' . ($annoLabel !== '' ? $annoLabel : $legacyYear); } return $this->legacyAnnualMetaCache[$cacheKey] = [ 'cartella' => $legacyYear, 'anno_gestione' => $anno, 'anno_label' => $annoLabel !== '' ? $annoLabel : null, 'data_inizio' => $start ? (string) $start : null, 'data_fine' => $end ? (string) $end : null, 'label' => $label, 'codice_archivio_legacy' => 'legacy:gestioni_annuali:' . $legacyYear, ]; } private function extractLegacyYearInt(?string $value): ?int { $value = trim((string) ($value ?? '')); if ($value === '') { return null; } if (preg_match('/(19|20)\d{2}/', $value, $match) === 1) { return (int) $match[0]; } return is_numeric($value) && strlen($value) === 4 ? (int) $value : null; } private function resolveTenantIdForStabile(int $stabileId): string { $tenant = GestioneContabile::query() ->where('stabile_id', $stabileId) ->whereNotNull('tenant_id') ->orderByDesc('id') ->value('tenant_id'); $tenant = trim((string) ($tenant ?? '')); return $tenant !== '' ? $tenant : 'T1'; } private function resolveFornitoreId(int $amministratoreId, mixed $codFor): int { $codFor = trim((string) ($codFor ?? '')); if ($amministratoreId <= 0 || $codFor === '' || ! Schema::hasTable('fornitori')) { return 0; } $query = Fornitore::query()->where('amministratore_id', $amministratoreId); if (is_numeric($codFor)) { $fornitore = (clone $query)->where('cod_forn', (int) $codFor)->first(); if ($fornitore instanceof Fornitore) { return (int) $fornitore->id; } $fornitore = (clone $query)->where('old_id', (int) $codFor)->first(); if ($fornitore instanceof Fornitore) { return (int) $fornitore->id; } } return 0; } private function syncMovimentiLegacy(Registrazione $registrazione, object $row, int $voceSpesaId, int $fornitoreId, array $legacyClassification): void { if (! Schema::hasTable('contabilita_movimenti')) { return; } Movimento::query()->where('registrazione_id', (int) $registrazione->id)->delete(); $amount = $this->resolveOperationAmount($row); if (abs($amount) < 0.005) { return; } $metadata = [ 'source' => 'gescon_operazioni', 'legacy_id_operaz' => (int) ($row->id_operaz ?? 0), 'legacy_gestione' => trim((string) ($row->gestione ?? '')), 'legacy_compet' => $legacyClassification['compet'], 'legacy_natura2' => $legacyClassification['natura2'], 'legacy_posting_mode' => $legacyClassification['posting_mode'], 'legacy_cod_spe' => trim((string) ($row->cod_spe ?? '')), 'legacy_cod_for' => trim((string) ($row->cod_for ?? '')), 'legacy_tabella' => trim((string) ($row->tabella ?? '')), 'legacy_year' => trim((string) ($row->legacy_year ?? '')), 'fornitore_id' => $fornitoreId > 0 ? $fornitoreId : null, ]; $contoId = $this->resolveLegacyContoId((int) $registrazione->stabile_id, $legacyClassification); $payload = [ 'registrazione_id' => (int) $registrazione->id, 'conto_id' => $contoId, 'voce_spesa_id' => ($legacyClassification['allow_cost_voice'] ?? true) && $voceSpesaId > 0 ? $voceSpesaId : null, 'tipo' => $amount >= 0 ? 'dare' : 'avere', 'importo' => abs($amount), 'movement_description' => trim((string) ($row->benef ?: $row->note ?: $row->natura ?: 'Operazione legacy')), 'sequence' => 1, ]; if (Schema::hasColumn('contabilita_movimenti', 'metadata')) { $payload['metadata'] = json_encode($metadata, JSON_UNESCAPED_SLASHES); } if (Schema::hasColumn('contabilita_movimenti', 'entry_id')) { $payload['entry_id'] = (int) $registrazione->id; } if (Schema::hasColumn('contabilita_movimenti', 'debit_amount')) { $payload['debit_amount'] = $amount >= 0 ? abs($amount) : 0; } if (Schema::hasColumn('contabilita_movimenti', 'credit_amount')) { $payload['credit_amount'] = $amount < 0 ? abs($amount) : 0; } Movimento::query()->create($payload); } private function classifyLegacyOperation(object $row): array { $compet = strtoupper(trim((string) ($row->compet ?? 'C'))); if (! in_array($compet, ['C', 'P'], true)) { $compet = 'C'; } $natura2 = trim((string) ($row->natura2 ?? '')); $naturaNorm = strtoupper($natura2); $postingMode = 'cost'; $allowCostVoice = true; if ($compet === 'P') { $postingMode = 'supplier_debt_payment'; $allowCostVoice = false; } elseif (in_array($naturaNorm, ['DEBITO', 'D'], true)) { $postingMode = 'supplier_debt_open'; } elseif (in_array($naturaNorm, ['CREDITO', 'C'], true)) { $postingMode = 'credit'; $allowCostVoice = false; } elseif (in_array($naturaNorm, ['ENTRATA', 'ENTRATE', 'E'], true)) { $postingMode = 'income'; $allowCostVoice = false; } return [ 'compet' => $compet, 'natura2' => $natura2, 'posting_mode' => $postingMode, 'allow_cost_voice' => $allowCostVoice, ]; } private function resolveLegacyContoId(int $stabileId, array $legacyClassification): int { $codice = match ($legacyClassification['posting_mode'] ?? 'cost') { 'supplier_debt_payment', 'supplier_debt_open' => (string) config('contabilita.conti_speciali.debiti_fornitori_codice', '2000'), 'credit', 'income' => (string) config('contabilita.conti_speciali.crediti_condomini_codice', '1100'), default => '5100', }; return $this->resolveOrCreateSpecialPianoConto($codice); } private function resolveOrCreateSpecialPianoConto(string $codice): int { if (! Schema::hasTable('contabilita_piano_conti')) { return 0; } $existing = DB::table('contabilita_piano_conti') ->where('codice', $codice) ->value('id'); if (is_numeric($existing) && (int) $existing > 0) { return (int) $existing; } $tipoConto = match (substr($codice, 0, 1)) { '1' => 'ATTIVO', '2' => 'PASSIVO', '3' => 'PATRIMONIO', '4' => 'RICAVO', default => 'COSTO', }; $descrizione = match ($codice) { '2000' => (string) config('contabilita.conti_speciali.debiti_fornitori_descrizione', 'Debiti verso fornitori'), '1100' => (string) config('contabilita.conti_speciali.crediti_condomini_descrizione', 'Crediti verso condomini'), default => 'Servizi condominiali', }; return (int) DB::table('contabilita_piano_conti')->insertGetId([ 'codice' => $codice, 'denominazione' => $descrizione, 'tipo_conto' => $tipoConto, 'attivo' => 1, 'created_at' => now(), 'updated_at' => now(), ]); } private function resolveOperationAmount(object $row): float { foreach (['importo_euro', 'importo', 'importo_spese', 'importo_entrate'] as $field) { if (isset($row->{$field}) && is_numeric($row->{$field}) && (float) $row->{$field} != 0.0) { $value = (float) $row->{$field}; if ($field === 'importo_entrate') { return -abs($value); } return $value; } } return 0.0; } }