761 lines
30 KiB
PHP
761 lines
30 KiB
PHP
<?php
|
||
namespace App\Modules\Contabilita\Services;
|
||
|
||
use App\Models\FornitoreStabileImpostazione;
|
||
use App\Models\GestioneContabile;
|
||
use App\Models\User;
|
||
use App\Models\VoceSpesa;
|
||
use App\Modules\Contabilita\Models\FatturaFornitore;
|
||
use App\Modules\Contabilita\Models\Movimento;
|
||
use App\Modules\Contabilita\Models\PianoConti;
|
||
use App\Modules\Contabilita\Models\Registrazione;
|
||
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use RuntimeException;
|
||
|
||
class FatturaFornitorePrimaNotaService
|
||
{
|
||
private function resolveContoCostoFromVoce(VoceSpesa $voce, int $stabileId): ?int
|
||
{
|
||
$codes = [];
|
||
foreach (['sottoconto_pd', 'conto_pd', 'codice', 'legacy_codice'] as $field) {
|
||
$val = trim((string) ($voce->{$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<int> $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),
|
||
);
|
||
}
|
||
}
|