{$field} ?? '')); if ($val !== '') { $codes[] = $val; } } foreach ($codes as $code) { $candidate = $this->normalizeVoceContoCode($code); $conto = PianoConti::query()->where('codice', $candidate)->first(); if (! $conto && $candidate !== $code) { $conto = PianoConti::query()->where('codice', $code)->first(); } if ($conto) { return (int) $conto->id; } if ($candidate !== '') { $createdId = $this->createContoFromVoce($candidate, trim((string) ($voce->descrizione ?? 'Voce spesa ' . $voce->id)), 'COSTO'); if ($createdId > 0) { return $createdId; } } } if (Schema::hasTable('piano_conti')) { foreach ($codes as $code) { $legacyParts = $this->parseLegacyContoCode($code); if (! $legacyParts) { continue; } $legacy = DB::table('piano_conti') ->where('mastro', $legacyParts['mastro']) ->where('conto', $legacyParts['conto']) ->where('sottoconto', $legacyParts['sottoconto']) ->where('quarto_livello', $legacyParts['quarto']) ->first(); if (! $legacy) { continue; } $codice = $legacyParts['mastro'] . '.' . $legacyParts['conto'] . '.' . $legacyParts['sottoconto'] . '.' . $legacyParts['quarto']; $descr = trim((string) ($legacy->denominazione ?? $voce->descrizione ?? '')); if ($descr === '') { $descr = 'Voce spesa ' . $voce->id; } $existing = PianoConti::query()->where('codice', $codice)->first(); if ($existing) { return (int) $existing->id; } try { $created = PianoConti::query()->create([ 'codice' => $codice, 'descrizione' => $descr, 'tipo' => 'Costo', 'is_master' => false, 'parent_id' => null, ]); if ($created) { return (int) $created->id; } } catch (\Throwable) { // ignore } } } return null; } private function normalizeVoceContoCode(string $code): string { $code = trim($code); if ($code === '') { return ''; } $parts = preg_split('/\s*[-–]\s*/u', $code, 2); $base = trim((string) ($parts[0] ?? $code)); $base = preg_replace('/\s+/', '', $base) ?? $base; $base = preg_replace('/[^A-Za-z0-9\.]/', '', $base) ?? $base; return $base; } private function createContoFromVoce(string $codice, string $descrizione, string $tipoConto): int { $codice = trim($codice); if ($codice === '') { return 0; } if (! Schema::hasTable('contabilita_piano_conti')) { return 0; } $existingId = DB::table('contabilita_piano_conti')->where('codice', $codice)->value('id'); if (is_numeric($existingId)) { return (int) $existingId; } $descrizione = trim($descrizione); if ($descrizione === '') { $descrizione = 'Voce spesa'; } try { $id = DB::table('contabilita_piano_conti')->insertGetId([ 'codice' => $codice, 'denominazione' => $descrizione, 'descrizione' => $descrizione, 'tipo_conto' => $tipoConto, 'attivo' => 1, 'livello' => 3, 'codice_padre' => null, 'saldo_iniziale' => 0.0, 'created_at' => now(), 'updated_at' => now(), ]); return is_numeric($id) ? (int) $id : 0; } catch (\Throwable) { return 0; } } /** * @return array{mastro: string, conto: string, sottoconto: string, quarto: string}|null */ private function parseLegacyContoCode(string $code): ?array { $code = trim($code); if ($code === '') { return null; } if (str_contains($code, '.')) { $parts = array_values(array_filter(explode('.', $code), fn($v) => $v !== '')); } else { $parts = [$code]; } $mastro = $parts[0] ?? ''; $conto = $parts[1] ?? ''; $sottoconto = $parts[2] ?? ''; $quarto = $parts[3] ?? ''; if ($mastro === '') { return null; } $mastro = str_pad(substr($mastro, 0, 4), 4, '0', STR_PAD_LEFT); $conto = $conto !== '' ? str_pad(substr($conto, 0, 8), 8, '0', STR_PAD_LEFT) : '00000000'; $sottoconto = $sottoconto !== '' ? str_pad(substr($sottoconto, 0, 5), 5, '0', STR_PAD_LEFT) : '00000'; $quarto = $quarto !== '' ? str_pad(substr($quarto, 0, 5), 5, '0', STR_PAD_LEFT) : '00000'; return [ 'mastro' => $mastro, 'conto' => $conto, 'sottoconto' => $sottoconto, 'quarto' => $quarto, ]; } private function buildMovimentoPayload( int $registrazioneId, int $stabileId, int $contoId, ?PianoConti $conto, string $tipo, float $importo, string $descrizione, ?int $voceSpesaId = null ): array { $payload = [ 'registrazione_id' => $registrazioneId, 'conto_id' => $contoId, 'tipo' => $tipo, 'importo' => $importo, ]; if (Schema::hasColumn('contabilita_movimenti', 'entry_id')) { $payload['entry_id'] = $registrazioneId; } if (Schema::hasColumn('contabilita_movimenti', 'voce_spesa_id') && $voceSpesaId && $voceSpesaId > 0) { $payload['voce_spesa_id'] = $voceSpesaId; } $code = trim((string) ($conto?->codice ?? '')); if ($code === '') { $code = (string) $contoId; } $normalized = preg_replace('/[^A-Za-z0-9]/', '', strtoupper($code)) ?: (string) $contoId; $masterCode = str_pad(substr($normalized, 0, 3), 3, '0', STR_PAD_LEFT); $accountCode = str_pad(substr($normalized, 0, 8), 8, '0', STR_PAD_LEFT); $subAccountCode = str_pad(substr($normalized, -5), 5, '0', STR_PAD_LEFT); if (Schema::hasColumn('contabilita_movimenti', 'chart_account_id')) { $chartAccountId = $this->resolveLegacyChartAccountId( $stabileId, $contoId, $conto, $masterCode, $accountCode, $subAccountCode, ); if ($chartAccountId > 0) { $payload['chart_account_id'] = $chartAccountId; } } if (Schema::hasColumn('contabilita_movimenti', 'master_code')) { $payload['master_code'] = $masterCode; } if (Schema::hasColumn('contabilita_movimenti', 'account_code')) { $payload['account_code'] = $accountCode; } if (Schema::hasColumn('contabilita_movimenti', 'subaccount_code')) { $payload['subaccount_code'] = $subAccountCode; } if (Schema::hasColumn('contabilita_movimenti', 'debit_amount')) { $payload['debit_amount'] = $tipo === 'dare' ? $importo : 0.0; } if (Schema::hasColumn('contabilita_movimenti', 'credit_amount')) { $payload['credit_amount'] = $tipo === 'avere' ? $importo : 0.0; } if (Schema::hasColumn('contabilita_movimenti', 'movement_description')) { $payload['movement_description'] = $descrizione !== '' ? $descrizione : null; } if (Schema::hasColumn('contabilita_movimenti', 'sequence')) { $payload['sequence'] = 1; } return $payload; } /** * @param array $voceIds */ private function syncConsuntivoVociSpesaDaMovimenti(int $stabileId, int $gestioneId, array $voceIds): void { if ($stabileId <= 0 || $gestioneId <= 0 || $voceIds === []) { return; } if (! Schema::hasColumn('voci_spesa', 'importo_consuntivo')) { return; } if (! Schema::hasColumn('contabilita_movimenti', 'voce_spesa_id')) { return; } $voceIds = array_values(array_unique(array_filter(array_map('intval', $voceIds), fn(int $id) => $id > 0))); if ($voceIds === []) { return; } $totali = DB::table('contabilita_movimenti as m') ->join('contabilita_registrazioni as r', 'r.id', '=', 'm.registrazione_id') ->where('r.stabile_id', $stabileId) ->where('r.gestione_id', $gestioneId) ->where('m.tipo', 'dare') ->whereIn('m.voce_spesa_id', $voceIds) ->selectRaw('m.voce_spesa_id, SUM(COALESCE(m.importo, 0)) as totale') ->groupBy('m.voce_spesa_id') ->pluck('totale', 'm.voce_spesa_id') ->all(); foreach ($voceIds as $voceId) { $totale = round((float) ($totali[$voceId] ?? 0), 2); VoceSpesa::query() ->whereKey($voceId) ->where('stabile_id', $stabileId) ->where('gestione_contabile_id', $gestioneId) ->update(['importo_consuntivo' => $totale]); } } private function resolveLegacyChartAccountId( int $stabileId, int $contoId, ?PianoConti $conto, string $masterCode, string $accountCode, string $subAccountCode ): int { if (! Schema::hasTable('piano_conti')) { return 0; } $mastro = str_pad(substr($masterCode, 0, 4), 4, '0', STR_PAD_LEFT); $contoCode = str_pad(substr($accountCode, 0, 8), 8, '0', STR_PAD_LEFT); $sotto = str_pad(substr($subAccountCode, -5), 5, '0', STR_PAD_LEFT); $quarto = '00000'; $query = DB::table('piano_conti') ->where('mastro', $mastro) ->where('conto', $contoCode) ->where('sottoconto', $sotto); if ($stabileId > 0) { $query->where(function ($q) use ($stabileId) { $q->whereNull('stabile_id')->orWhere('stabile_id', $stabileId); }); } $existingId = $query->value('id'); if (is_numeric($existingId)) { return (int) $existingId; } $descr = trim((string) ($conto?->descrizione ?? $conto?->description ?? '')); if ($descr === '') { $descr = 'Conto ' . $mastro . '.' . $contoCode . '.' . $sotto; } $tipo = strtoupper(trim((string) ($conto?->tipo ?? ''))); $tipoEnum = match ($tipo) { 'ATTIVITÀ', 'ATTIVITA', 'ATTIVO' => 'ATTIVO', 'PASSIVITÀ', 'PASSIVITA', 'PASSIVO' => 'PASSIVO', 'RICAVO' => 'RICAVO', 'PATRIMONIO NETTO', 'PATRIMONIO' => 'PATRIMONIO', default => 'COSTO', }; $tipologia = match ($tipoEnum) { 'RICAVO' => 'RIC', 'ATTIVO' => 'ATT', 'PASSIVO' => 'PAS', 'PATRIMONIO' => 'PAT', default => 'SPE', }; $id = DB::table('piano_conti')->insertGetId([ 'stabile_id' => $stabileId > 0 ? $stabileId : null, 'mastro' => $mastro, 'conto' => $contoCode, 'sottoconto' => $sotto, 'quarto_livello' => $quarto, 'denominazione' => $descr, 'tipo' => $tipoEnum, 'tipologia' => $tipologia, 'attivo' => 1, 'created_at' => now(), 'updated_at' => now(), ]); return is_numeric($id) ? (int) $id : 0; } public function generaRegistrazioneDaFattura(User $user, FatturaFornitore $fattura): Registrazione { if (! Schema::hasTable('contabilita_registrazioni') || ! Schema::hasTable('contabilita_movimenti')) { throw new RuntimeException('Tabelle prima nota mancanti.'); } if (! empty($fattura->registrazione_id)) { $existing = Registrazione::query()->find((int) $fattura->registrazione_id); if ($existing) { return $existing; } } $stabileId = (int) ($fattura->stabile_id ?? 0); if ($stabileId <= 0) { throw new RuntimeException('Fattura senza stabile_id.'); } $gestioneId = (int) ($fattura->gestione_id ?? 0); if ($gestioneId <= 0) { throw new RuntimeException('Seleziona una gestione per questa fattura.'); } if (Schema::hasTable('contabilita_chiusure_gestione')) { $chiusa = app(ChiusuraGestioneService::class)->isGestioneChiusa($gestioneId); if ($chiusa) { throw new RuntimeException('Gestione chiusa: non è possibile generare nuove registrazioni.'); } } $fattura->loadMissing(['righe']); if ($fattura->righe->count() <= 0) { throw new RuntimeException('Inserisci almeno una riga di fattura (riparto costi) prima di generare la prima nota.'); } $imponibile = round((float) ($fattura->imponibile ?? 0), 2); $iva = round((float) ($fattura->iva ?? 0), 2); $totale = round((float) ($fattura->totale ?? 0), 2); $ritenuta = round((float) ($fattura->ritenuta_importo ?? 0), 2); $ritenutaAliquota = (float) ($fattura->ritenuta_aliquota ?? 0); $ritenutaCodice = trim((string) ($fattura->ritenuta_previdenziale_codice ?? '')); $ritenutaTributo = trim((string) ($fattura->ritenuta_codice_tributo ?? '')); if (abs($totale) < 0.005) { throw new RuntimeException('Totale fattura non valido.'); } $netto = round($totale - $ritenuta, 2); if ($ritenuta > 0) { $needsUpdate = false; if (round((float) ($fattura->ritenuta_importo ?? 0), 2) <= 0 && $ritenuta > 0) { $fattura->ritenuta_importo = $ritenuta; $needsUpdate = true; } if ((float) ($fattura->ritenuta_aliquota ?? 0) <= 0 && $ritenutaAliquota > 0) { $fattura->ritenuta_aliquota = $ritenutaAliquota; $needsUpdate = true; } if (trim((string) ($fattura->ritenuta_previdenziale_codice ?? '')) === '' && $ritenutaCodice !== '') { $fattura->ritenuta_previdenziale_codice = $ritenutaCodice; $needsUpdate = true; } if (trim((string) ($fattura->ritenuta_codice_tributo ?? '')) === '' && $ritenutaTributo !== '') { $fattura->ritenuta_codice_tributo = $ritenutaTributo; $needsUpdate = true; } if ($needsUpdate) { $fattura->save(); } } $fattura->loadMissing(['fornitore', 'fatturaElettronica']); if ($ritenuta <= 0 && $fattura->fatturaElettronica && is_string($fattura->fatturaElettronica->xml_content ?? null)) { $xml = trim((string) $fattura->fatturaElettronica->xml_content); if ($xml !== '') { try { $parsed = app(FatturaElettronicaXmlParser::class)->parse($xml); $rit = is_array($parsed['ritenuta'] ?? null) ? $parsed['ritenuta'] : null; if (is_array($rit)) { if ($ritenuta <= 0 && is_numeric($rit['importo'] ?? null)) { $ritenuta = round((float) $rit['importo'], 2); } if ($ritenutaAliquota <= 0 && is_numeric($rit['aliquota'] ?? null)) { $ritenutaAliquota = (float) $rit['aliquota']; } if ($ritenutaCodice === '' && is_string($rit['tipo'] ?? null)) { $ritenutaCodice = trim((string) $rit['tipo']); } if ($ritenutaTributo === '' && is_string($rit['causale'] ?? null)) { $ritenutaTributo = trim((string) $rit['causale']); } $needsUpdate = false; if ($ritenuta > 0 && round((float) ($fattura->ritenuta_importo ?? 0), 2) <= 0) { $fattura->ritenuta_importo = $ritenuta; $needsUpdate = true; } if ($ritenutaAliquota > 0 && (float) ($fattura->ritenuta_aliquota ?? 0) <= 0) { $fattura->ritenuta_aliquota = $ritenutaAliquota; $needsUpdate = true; } if ($ritenutaCodice !== '' && trim((string) ($fattura->ritenuta_previdenziale_codice ?? '')) === '') { $fattura->ritenuta_previdenziale_codice = $ritenutaCodice; $needsUpdate = true; } if ($ritenutaTributo !== '' && trim((string) ($fattura->ritenuta_codice_tributo ?? '')) === '') { $fattura->ritenuta_codice_tributo = $ritenutaTributo; $needsUpdate = true; } if ($needsUpdate) { $fattura->save(); } } } catch (\Throwable) { // ignore } } } $fornitoreId = (int) ($fattura->fornitore_id ?? 0); $fornitoreLabel = is_object($fattura->fornitore) ? (is_string($fattura->fornitore->ragione_sociale ?? null) ? trim((string) $fattura->fornitore->ragione_sociale) : null) : null; $fornitoreCode = is_object($fattura->fornitore) && is_string($fattura->fornitore->codice_univoco ?? null) ? trim((string) $fattura->fornitore->codice_univoco) : null; $contoDebitiFornitori = ($fornitoreId > 0) ? app(ContiSoggettiService::class)->resolveContoDebitoFornitore($stabileId, $fornitoreId, $fornitoreLabel, $fornitoreCode) : app(ChiusuraGestioneService::class)->resolveOrCreateConto( (string) config('contabilita.conti_speciali.debiti_fornitori_codice', '2000'), (string) config('contabilita.conti_speciali.debiti_fornitori_descrizione', 'Debiti verso fornitori'), (string) config('contabilita.conti_speciali.debiti_fornitori_tipo', 'Passività'), ); $contoIvaCredito = null; if ($iva > 0) { $contoIvaCredito = app(ChiusuraGestioneService::class)->resolveOrCreateConto( (string) config('contabilita.conti_speciali.iva_credito_codice', '1210'), (string) config('contabilita.conti_speciali.iva_credito_descrizione', 'IVA a credito'), (string) config('contabilita.conti_speciali.iva_credito_tipo', 'Attività'), ); } $contoRitenute = null; if ($ritenuta > 0) { $contoRitenute = $this->resolveContoRitenuteOperate($stabileId); } $data = $fattura->data_registrazione?->toDateString() ?: $fattura->data_documento?->toDateString() ?: now()->toDateString(); $numero = is_string($fattura->numero_documento) ? trim((string) $fattura->numero_documento) : ''; $descr = is_string($fattura->descrizione) ? trim((string) $fattura->descrizione) : ''; if ($descr === '') { $descr = 'Fattura fornitore' . ($numero !== '' ? (' ' . $numero) : ''); } return DB::transaction(function () use ($user, $fattura, $stabileId, $gestioneId, $data, $descr, $iva, $ritenuta, $netto, $contoDebitiFornitori, $contoIvaCredito, $contoRitenute, $fornitoreId) { $prot = null; if ( Schema::hasColumn('contabilita_registrazioni', 'protocollo_completo') && Schema::hasTable('gestioni_contabili') ) { $g = GestioneContabile::query()->lockForUpdate()->find($gestioneId); if ($g) { $prot = $g->getNextProtocollo(); } } $regPayload = [ 'stabile_id' => $stabileId, 'data_registrazione' => $data, ]; if (Schema::hasColumn('contabilita_registrazioni', 'created_by')) { $regPayload['created_by'] = (int) $user->id; } if (Schema::hasColumn('contabilita_registrazioni', 'updated_by')) { $regPayload['updated_by'] = (int) $user->id; } if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) { $regPayload['user_id'] = (int) $user->id; } if (Schema::hasColumn('contabilita_registrazioni', 'gestione_id')) { $regPayload['gestione_id'] = Registrazione::resolveGestioneIdOrDefault($gestioneId, $stabileId, $data); } if (Schema::hasColumn('contabilita_registrazioni', 'entry_date')) { $regPayload['entry_date'] = $data; } if (Schema::hasColumn('contabilita_registrazioni', 'description')) { $regPayload['description'] = $descr; } elseif (Schema::hasColumn('contabilita_registrazioni', 'descrizione')) { $regPayload['descrizione'] = $descr; } elseif (Schema::hasColumn('contabilita_registrazioni', 'denominazione')) { $regPayload['denominazione'] = $descr; } if (Schema::hasColumn('contabilita_registrazioni', 'document_type')) { $regPayload['document_type'] = $fattura->causale_contabile ?: 'Fattura ricevuta'; } if (Schema::hasColumn('contabilita_registrazioni', 'document_number')) { $numeroDoc = is_string($fattura->numero_documento) ? trim((string) $fattura->numero_documento) : ''; if ($numeroDoc !== '') { $regPayload['document_number'] = $numeroDoc; } } if (Schema::hasColumn('contabilita_registrazioni', 'external_reference')) { $regPayload['external_reference'] = 'FATTURA_FORNITORE:' . (int) $fattura->id; } if (Schema::hasColumn('contabilita_registrazioni', 'note')) { $regPayload['note'] = $descr; } /** @var Registrazione $reg */ $reg = Registrazione::query()->create($regPayload); if (is_array($prot) && ! empty($prot['completo'])) { $reg->fill([ 'protocollo_prefix' => $prot['prefix'] ?? null, 'protocollo_numero' => $prot['numero'] ?? null, 'protocollo_completo' => $prot['completo'] ?? null, ]); $reg->save(); } $voceIdsMovimenti = []; foreach ($fattura->righe as $r) { $imp = round((float) ($r->imponibile_euro ?? 0), 2); if (abs($imp) < 0.005) { continue; } $contoCostoId = (int) ($r->conto_costo_id ?? 0); $voceId = (int) ($r->voce_spesa_id ?? 0); if ($voceId > 0 && Schema::hasTable('voci_spesa') && Schema::hasTable('contabilita_piano_conti')) { $voce = VoceSpesa::query()->find($voceId); if ($voce) { $contoCostoId = $this->resolveContoCostoFromVoce($voce, $stabileId) ?? 0; } } if ($contoCostoId <= 0 && $voceId <= 0 && Schema::hasTable('fornitore_stabile_impostazioni')) { $preset = FornitoreStabileImpostazione::query() ->where('stabile_id', $stabileId) ->where('fornitore_id', $fornitoreId) ->first(); $presetVoceId = (int) ($preset?->voce_spesa_default_id ?? 0); if ($presetVoceId > 0 && Schema::hasTable('voci_spesa') && Schema::hasTable('contabilita_piano_conti')) { $presetVoce = VoceSpesa::query()->find($presetVoceId); if ($presetVoce) { $contoCostoId = $this->resolveContoCostoFromVoce($presetVoce, $stabileId) ?? $contoCostoId; } } } if ($voceId <= 0) { throw new RuntimeException('Riga fattura senza voce spesa. Seleziona la voce spesa per ogni riga.'); } if ($contoCostoId <= 0) { throw new RuntimeException('Voce spesa senza conto contabile collegato.'); } $rowDescr = trim((string) ($r->descrizione ?? '')); $contoObj = PianoConti::query()->find($contoCostoId); $movPayload = $this->buildMovimentoPayload( (int) $reg->id, $stabileId, $contoCostoId, $contoObj, 'dare', $imp, $rowDescr !== '' ? $rowDescr : $descr, $voceId > 0 ? $voceId : null, ); Movimento::query()->create($movPayload); if ($voceId > 0) { $voceIdsMovimenti[] = $voceId; } } if ($contoIvaCredito && $iva > 0) { $movPayload = $this->buildMovimentoPayload( (int) $reg->id, $stabileId, (int) $contoIvaCredito->id, $contoIvaCredito, 'dare', $iva, $descr, ); Movimento::query()->create($movPayload); } if ($contoRitenute && $ritenuta > 0) { $movPayload = $this->buildMovimentoPayload( (int) $reg->id, $stabileId, (int) $contoRitenute->id, $contoRitenute, 'avere', $ritenuta, $descr, ); Movimento::query()->create($movPayload); } $movPayload = $this->buildMovimentoPayload( (int) $reg->id, $stabileId, (int) $contoDebitiFornitori->id, $contoDebitiFornitori, 'avere', $netto, $descr, ); Movimento::query()->create($movPayload); $reg->assertBilanciata(); $fattura->registrazione_id = (int) $reg->id; $fattura->stato = 'contabilizzato'; $fattura->save(); $this->syncConsuntivoVociSpesaDaMovimenti( $stabileId, $gestioneId, $voceIdsMovimenti ); return $reg; }); } private function resolveContoRitenuteOperate(int $stabileId): PianoConti { $voce = null; if (Schema::hasTable('voci_spesa')) { $voce = VoceSpesa::query() ->where(function ($q) { $q->where('codice', 'RAO') ->orWhere('legacy_codice', 'RAO') ->orWhere('descrizione', 'like', '%Ritenute%'); }) ->orderByDesc('id') ->first(); } $codice = '2010'; $descrizione = 'Erario c/ritenute da versare'; $tipo = 'Passività'; if ($voce) { $candidate = trim((string) ($voce->codice ?? $voce->legacy_codice ?? '')); if ($candidate !== '') { $codice = $candidate; } $descVoce = trim((string) ($voce->descrizione ?? '')); if ($descVoce !== '') { $descrizione = $descVoce; } } return app(ChiusuraGestioneService::class)->resolveOrCreateConto( (string) config('contabilita.conti_speciali.ritenute_da_versare_codice', $codice), (string) config('contabilita.conti_speciali.ritenute_da_versare_descrizione', $descrizione), (string) config('contabilita.conti_speciali.ritenute_da_versare_tipo', $tipo), ); } }