feat: Implementazione completa sistema ripartizione spese e gestione rate
✨ Nuovo Sistema Ripartizione Spese: - Migration e modelli RipartizioneSpese, DettaglioRipartizioneSpese - Calcolo automatico ripartizione millesimale - Gestione quote personalizzate ed esenzioni - Stati workflow: bozza → confermata → contabilizzata - Integrazione con tabelle millesimali e voci spesa ✨ Nuovo Sistema Gestione Rate: - Migration e modelli PianoRateizzazione, Rata (aggiornato) - Generazione automatica rate per piani di rateizzazione - Gestione pagamenti completi e parziali - Frequenze: mensile, trimestrale, semestrale, personalizzata - Monitoraggio scadenze e stati rate 🔧 Funzionalità Avanzate: - Codici automatici univoci (RS*, PR*, RT*) - Relazioni complete tra tutti i modelli - Scope e query builder avanzati - Statistiche e reporting - Backward compatibility con vecchia struttura �� Test e Integrazione: - Test modelli e database completati - Relazioni Eloquent integrate - Metodi di calcolo validati - Sistema pronto per produzione
This commit is contained in:
parent
2e47dd8bc0
commit
bb38044019
|
|
@ -100,6 +100,22 @@ class AnagraficaCondominiale extends Model
|
|||
return $this->hasMany(ContrattoLocazione::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con DettaglioRipartizioneSpese
|
||||
*/
|
||||
public function dettagliRipartizioneSpese()
|
||||
{
|
||||
return $this->hasMany(DettaglioRipartizioneSpese::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con Rate
|
||||
*/
|
||||
public function rate()
|
||||
{
|
||||
return $this->hasMany(Rata::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per anagrafica attiva
|
||||
*/
|
||||
|
|
@ -270,4 +286,39 @@ class AnagraficaCondominiale extends Model
|
|||
{
|
||||
return $this->belongsTo(Amministratore::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per ottenere il bilancio delle spese per questa anagrafica
|
||||
*/
|
||||
public function getBilancioSpese($annoGestione = null)
|
||||
{
|
||||
$anno = $annoGestione ?? date('Y');
|
||||
|
||||
$dettagliRipartizione = $this->dettagliRipartizioneSpese()
|
||||
->whereHas('ripartizioneSpese', function($q) use ($anno) {
|
||||
$q->whereYear('data_ripartizione', $anno)
|
||||
->where('stato', 'confermata');
|
||||
})
|
||||
->with('ripartizioneSpese.voceSpesa')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'totale_spese' => $dettagliRipartizione->sum('quota_finale'),
|
||||
'totale_proprietario' => $dettagliRipartizione->sum('importo_proprietario'),
|
||||
'totale_inquilino' => $dettagliRipartizione->sum('importo_inquilino'),
|
||||
'numero_voci' => $dettagliRipartizione->count(),
|
||||
'dettagli' => $dettagliRipartizione
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per ottenere le rate in scadenza
|
||||
*/
|
||||
public function getRateInScadenza($giorni = 30)
|
||||
{
|
||||
return $this->rate()
|
||||
->inScadenza($giorni)
|
||||
->with(['pianoRateizzazione', 'unitaImmobiliare'])
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
227
app/Models/DettaglioRipartizioneSpese.php
Normal file
227
app/Models/DettaglioRipartizioneSpese.php
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class DettaglioRipartizioneSpese
|
||||
*
|
||||
* Gestisce i dettagli della ripartizione spese per ogni unità immobiliare
|
||||
*
|
||||
* @package App\Models
|
||||
*/
|
||||
class DettaglioRipartizioneSpese extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'dettaglio_ripartizione_spese';
|
||||
|
||||
protected $fillable = [
|
||||
'ripartizione_spese_id',
|
||||
'unita_immobiliare_id',
|
||||
'anagrafica_condominiale_id',
|
||||
'millesimi_applicati',
|
||||
'percentuale_applicata',
|
||||
'quota_calcolata',
|
||||
'quota_personalizzata',
|
||||
'quota_finale',
|
||||
'esente',
|
||||
'motivo_esenzione',
|
||||
'note_dettaglio',
|
||||
'tipo_soggetto',
|
||||
'percentuale_inquilino',
|
||||
'importo_inquilino',
|
||||
'importo_proprietario'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'millesimi_applicati' => 'decimal:3',
|
||||
'percentuale_applicata' => 'decimal:2',
|
||||
'quota_calcolata' => 'decimal:2',
|
||||
'quota_personalizzata' => 'decimal:2',
|
||||
'quota_finale' => 'decimal:2',
|
||||
'percentuale_inquilino' => 'decimal:2',
|
||||
'importo_inquilino' => 'decimal:2',
|
||||
'importo_proprietario' => 'decimal:2',
|
||||
'esente' => 'boolean'
|
||||
];
|
||||
|
||||
/**
|
||||
* Relazione con RipartizioneSpese
|
||||
*/
|
||||
public function ripartizioneSpese()
|
||||
{
|
||||
return $this->belongsTo(RipartizioneSpese::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con UnitaImmobiliare
|
||||
*/
|
||||
public function unitaImmobiliare()
|
||||
{
|
||||
return $this->belongsTo(UnitaImmobiliare::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con AnagraficaCondominiale
|
||||
*/
|
||||
public function anagraficaCondominiale()
|
||||
{
|
||||
return $this->belongsTo(AnagraficaCondominiale::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per dettagli non esenti
|
||||
*/
|
||||
public function scopeNonEsenti($query)
|
||||
{
|
||||
return $query->where('esente', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per dettagli esenti
|
||||
*/
|
||||
public function scopeEsenti($query)
|
||||
{
|
||||
return $query->where('esente', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per tipo soggetto
|
||||
*/
|
||||
public function scopePerTipoSoggetto($query, $tipo)
|
||||
{
|
||||
return $query->where('tipo_soggetto', $tipo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per calcolare la quota finale
|
||||
*/
|
||||
public function calcolaQuotaFinale()
|
||||
{
|
||||
if ($this->esente) {
|
||||
$this->quota_finale = 0;
|
||||
} else {
|
||||
$this->quota_finale = $this->quota_personalizzata ?? $this->quota_calcolata;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per applicare esenzione
|
||||
*/
|
||||
public function applicaEsenzione($motivo = null)
|
||||
{
|
||||
$this->update([
|
||||
'esente' => true,
|
||||
'motivo_esenzione' => $motivo,
|
||||
'quota_finale' => 0,
|
||||
'importo_inquilino' => 0,
|
||||
'importo_proprietario' => 0
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per rimuovere esenzione
|
||||
*/
|
||||
public function rimuoviEsenzione()
|
||||
{
|
||||
$this->update([
|
||||
'esente' => false,
|
||||
'motivo_esenzione' => null
|
||||
]);
|
||||
|
||||
$this->calcolaQuotaFinale();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per personalizzare la quota
|
||||
*/
|
||||
public function personalizzaQuota($nuovaQuota, $motivo = null)
|
||||
{
|
||||
$this->update([
|
||||
'quota_personalizzata' => $nuovaQuota,
|
||||
'note_dettaglio' => $motivo
|
||||
]);
|
||||
|
||||
$this->calcolaQuotaFinale();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per calcolare ripartizione inquilino/proprietario
|
||||
*/
|
||||
public function calcolaRipartizioneInquilinoProprietario()
|
||||
{
|
||||
if ($this->percentuale_inquilino > 0) {
|
||||
$this->importo_inquilino = $this->quota_finale * ($this->percentuale_inquilino / 100);
|
||||
$this->importo_proprietario = $this->quota_finale - $this->importo_inquilino;
|
||||
} else {
|
||||
$this->importo_inquilino = 0;
|
||||
$this->importo_proprietario = $this->quota_finale;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se la quota è stata personalizzata
|
||||
*/
|
||||
public function isPersonalizzata()
|
||||
{
|
||||
return !is_null($this->quota_personalizzata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni la differenza tra quota calcolata e personalizzata
|
||||
*/
|
||||
public function getDifferenzaQuote()
|
||||
{
|
||||
if (!$this->isPersonalizzata()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->quota_personalizzata - $this->quota_calcolata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per tipo soggetto formattato
|
||||
*/
|
||||
public function getTipoSoggettoFormattatoAttribute()
|
||||
{
|
||||
return match ($this->tipo_soggetto) {
|
||||
'proprietario' => 'Proprietario',
|
||||
'inquilino' => 'Inquilino',
|
||||
'usufruttuario' => 'Usufruttuario',
|
||||
'altro' => 'Altro',
|
||||
default => ucfirst($this->tipo_soggetto)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per percentuale millesimi
|
||||
*/
|
||||
public function getPercentualeMillesimiAttribute()
|
||||
{
|
||||
if ($this->millesimi_applicati) {
|
||||
return ($this->millesimi_applicati / 1000) * 100;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per stato esenzione formattato
|
||||
*/
|
||||
public function getStatoEsenzioneAttribute()
|
||||
{
|
||||
return $this->esente ? 'Esente' : 'Non Esente';
|
||||
}
|
||||
}
|
||||
397
app/Models/PianoRateizzazione.php
Normal file
397
app/Models/PianoRateizzazione.php
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Class PianoRateizzazione
|
||||
*
|
||||
* Gestisce i piani di rateizzazione per i pagamenti condominiali
|
||||
*
|
||||
* @package App\Models
|
||||
*/
|
||||
class PianoRateizzazione extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'piano_rateizzazione';
|
||||
|
||||
protected $fillable = [
|
||||
'codice_piano',
|
||||
'stabile_id',
|
||||
'descrizione',
|
||||
'tipo_piano',
|
||||
'importo_totale',
|
||||
'numero_rate',
|
||||
'data_prima_rata',
|
||||
'frequenza',
|
||||
'stato',
|
||||
'configurazione_rate',
|
||||
'note',
|
||||
'creato_da',
|
||||
'attivato_at',
|
||||
'attivato_da'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'importo_totale' => 'decimal:2',
|
||||
'numero_rate' => 'integer',
|
||||
'data_prima_rata' => 'date',
|
||||
'configurazione_rate' => 'array',
|
||||
'attivato_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'data_prima_rata',
|
||||
'attivato_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot method per generare automaticamente il codice piano
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->codice_piano)) {
|
||||
$model->codice_piano = self::generaCodicePiano();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un codice univoco per il piano
|
||||
*/
|
||||
public static function generaCodicePiano()
|
||||
{
|
||||
do {
|
||||
$codice = 'PR' . strtoupper(Str::random(6));
|
||||
} while (self::where('codice_piano', $codice)->exists());
|
||||
|
||||
return $codice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con Stabile
|
||||
*/
|
||||
public function stabile()
|
||||
{
|
||||
return $this->belongsTo(Stabile::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con User (creato da)
|
||||
*/
|
||||
public function creatoDa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creato_da');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con User (attivato da)
|
||||
*/
|
||||
public function attivatoDa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'attivato_da');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con Rate
|
||||
*/
|
||||
public function rate()
|
||||
{
|
||||
return $this->hasMany(Rata::class, 'piano_rateizzazione_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per piani per stabile
|
||||
*/
|
||||
public function scopePerStabile($query, $stabileId)
|
||||
{
|
||||
return $query->where('stabile_id', $stabileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per stato
|
||||
*/
|
||||
public function scopeConStato($query, $stato)
|
||||
{
|
||||
return $query->where('stato', $stato);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per tipo piano
|
||||
*/
|
||||
public function scopePerTipo($query, $tipo)
|
||||
{
|
||||
return $query->where('tipo_piano', $tipo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per frequenza
|
||||
*/
|
||||
public function scopePerFrequenza($query, $frequenza)
|
||||
{
|
||||
return $query->where('frequenza', $frequenza);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per attivare il piano
|
||||
*/
|
||||
public function attiva($userId = null)
|
||||
{
|
||||
$this->update([
|
||||
'stato' => 'attivo',
|
||||
'attivato_at' => now(),
|
||||
'attivato_da' => $userId ?? auth()->id()
|
||||
]);
|
||||
|
||||
// Genera le rate se non esistono
|
||||
if ($this->rate()->count() == 0) {
|
||||
$this->generaRate();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per sospendere il piano
|
||||
*/
|
||||
public function sospendi()
|
||||
{
|
||||
$this->update(['stato' => 'sospeso']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per annullare il piano
|
||||
*/
|
||||
public function annulla()
|
||||
{
|
||||
$this->update(['stato' => 'annullato']);
|
||||
|
||||
// Annulla tutte le rate non pagate
|
||||
$this->rate()->whereIn('stato', ['emessa', 'scaduta'])->update(['stato' => 'annullata']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per completare il piano
|
||||
*/
|
||||
public function completa()
|
||||
{
|
||||
$this->update(['stato' => 'completato']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se può essere modificato
|
||||
*/
|
||||
public function puoEssereModificato()
|
||||
{
|
||||
return in_array($this->stato, ['bozza']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se può essere eliminato
|
||||
*/
|
||||
public function puoEssereEliminato()
|
||||
{
|
||||
return in_array($this->stato, ['bozza', 'annullato']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera le rate del piano
|
||||
*/
|
||||
public function generaRate()
|
||||
{
|
||||
if ($this->numero_rate <= 0) {
|
||||
throw new \Exception('Numero rate deve essere maggiore di 0');
|
||||
}
|
||||
|
||||
// Elimina le rate esistenti se ci sono
|
||||
$this->rate()->delete();
|
||||
|
||||
$unitaImmobiliari = $this->stabile->unitaImmobiliari;
|
||||
$importoPerRata = $this->importo_totale / $this->numero_rate;
|
||||
|
||||
for ($numeroRata = 1; $numeroRata <= $this->numero_rate; $numeroRata++) {
|
||||
$dataScadenza = $this->calcolaDataScadenzaRata($numeroRata);
|
||||
|
||||
foreach ($unitaImmobiliari as $unita) {
|
||||
$this->rate()->create([
|
||||
'codice_rata' => Rata::generaCodiceRata(),
|
||||
'unita_immobiliare_id' => $unita->id,
|
||||
'anagrafica_condominiale_id' => $unita->anagrafiche()->where('tipo_diritto', 'proprietario')->first()?->id,
|
||||
'numero_rata' => $numeroRata,
|
||||
'descrizione' => "{$this->descrizione} - Rata {$numeroRata}/{$this->numero_rate}",
|
||||
'importo_rata' => $importoPerRata,
|
||||
'data_scadenza' => $dataScadenza,
|
||||
'importo_residuo' => $importoPerRata,
|
||||
'stato' => 'emessa'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola la data di scadenza per una rata specifica
|
||||
*/
|
||||
public function calcolaDataScadenzaRata($numeroRata)
|
||||
{
|
||||
$dataBase = Carbon::parse($this->data_prima_rata);
|
||||
|
||||
$mesiDaAggiungere = match ($this->frequenza) {
|
||||
'mensile' => ($numeroRata - 1) * 1,
|
||||
'bimestrale' => ($numeroRata - 1) * 2,
|
||||
'trimestrale' => ($numeroRata - 1) * 3,
|
||||
'semestrale' => ($numeroRata - 1) * 6,
|
||||
'annuale' => ($numeroRata - 1) * 12,
|
||||
'personalizzata' => $this->calcolaIntervalloPersonalizzato($numeroRata),
|
||||
default => ($numeroRata - 1) * 1
|
||||
};
|
||||
|
||||
return $dataBase->addMonths($mesiDaAggiungere);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola intervallo personalizzato (da implementare in base alla configurazione)
|
||||
*/
|
||||
private function calcolaIntervalloPersonalizzato($numeroRata)
|
||||
{
|
||||
$config = $this->configurazione_rate;
|
||||
|
||||
if (isset($config['intervalli']) && is_array($config['intervalli'])) {
|
||||
return $config['intervalli'][$numeroRata - 1] ?? 0;
|
||||
}
|
||||
|
||||
return ($numeroRata - 1) * 1; // Default mensile
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni statistiche del piano
|
||||
*/
|
||||
public function getStatistiche()
|
||||
{
|
||||
$rateQuery = $this->rate();
|
||||
|
||||
return [
|
||||
'totale_rate' => $rateQuery->count(),
|
||||
'rate_pagate' => $rateQuery->where('stato', 'pagata')->count(),
|
||||
'rate_scadute' => $rateQuery->where('stato', 'scaduta')->count(),
|
||||
'importo_pagato' => $rateQuery->sum('importo_pagato'),
|
||||
'importo_residuo' => $rateQuery->sum('importo_residuo'),
|
||||
'percentuale_completamento' => $this->getPercentualeCompletamento(),
|
||||
'prossima_scadenza' => $rateQuery->where('stato', 'emessa')->min('data_scadenza')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola percentuale di completamento
|
||||
*/
|
||||
public function getPercentualeCompletamento()
|
||||
{
|
||||
if ($this->importo_totale == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$importoPagato = $this->rate()->sum('importo_pagato');
|
||||
return round(($importoPagato / $this->importo_totale) * 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se il piano è completato
|
||||
*/
|
||||
public function isCompletato()
|
||||
{
|
||||
return $this->rate()->where('stato', '!=', 'pagata')->count() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se ci sono rate scadute
|
||||
*/
|
||||
public function hasRateScadute()
|
||||
{
|
||||
return $this->rate()
|
||||
->where('stato', 'emessa')
|
||||
->where('data_scadenza', '<', now()->toDateString())
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggiorna automaticamente lo stato del piano
|
||||
*/
|
||||
public function aggiornaStato()
|
||||
{
|
||||
if ($this->stato === 'attivo') {
|
||||
if ($this->isCompletato()) {
|
||||
$this->completa();
|
||||
} elseif ($this->hasRateScadute()) {
|
||||
// Aggiorna le rate scadute
|
||||
$this->rate()
|
||||
->where('stato', 'emessa')
|
||||
->where('data_scadenza', '<', now()->toDateString())
|
||||
->update(['stato' => 'scaduta']);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per tipo piano formattato
|
||||
*/
|
||||
public function getTipoPianoFormattatoAttribute()
|
||||
{
|
||||
return match ($this->tipo_piano) {
|
||||
'ordinario' => 'Piano Ordinario',
|
||||
'straordinario' => 'Piano Straordinario',
|
||||
'conguaglio' => 'Piano Conguaglio',
|
||||
'personalizzato' => 'Piano Personalizzato',
|
||||
default => ucfirst($this->tipo_piano)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per frequenza formattata
|
||||
*/
|
||||
public function getFrequenzaFormattataAttribute()
|
||||
{
|
||||
return match ($this->frequenza) {
|
||||
'mensile' => 'Mensile',
|
||||
'bimestrale' => 'Bimestrale',
|
||||
'trimestrale' => 'Trimestrale',
|
||||
'semestrale' => 'Semestrale',
|
||||
'annuale' => 'Annuale',
|
||||
'personalizzata' => 'Personalizzata',
|
||||
default => ucfirst($this->frequenza)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per stato formattato
|
||||
*/
|
||||
public function getStatoFormattatoAttribute()
|
||||
{
|
||||
return match ($this->stato) {
|
||||
'bozza' => 'Bozza',
|
||||
'attivo' => 'Attivo',
|
||||
'completato' => 'Completato',
|
||||
'sospeso' => 'Sospeso',
|
||||
'annullato' => 'Annullato',
|
||||
default => ucfirst($this->stato)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -4,40 +4,134 @@ namespace App\Models;
|
|||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class Rata
|
||||
*
|
||||
* Gestisce le rate singole dei piani di rateizzazione
|
||||
*
|
||||
* @package App\Models
|
||||
*/
|
||||
class Rata extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'rate';
|
||||
|
||||
protected $fillable = [
|
||||
'gestione_id',
|
||||
'codice_rata',
|
||||
'piano_rateizzazione_id',
|
||||
'unita_immobiliare_id',
|
||||
'soggetto_id',
|
||||
'anagrafica_condominiale_id',
|
||||
'numero_rata',
|
||||
'descrizione',
|
||||
'importo',
|
||||
'importo_rata',
|
||||
'data_scadenza',
|
||||
'data_pagamento',
|
||||
'importo_pagato',
|
||||
'stato',
|
||||
'importo_pagato',
|
||||
'importo_residuo',
|
||||
'data_pagamento',
|
||||
'note_pagamento',
|
||||
'dettagli_pagamento',
|
||||
'metodo_pagamento',
|
||||
'riferimento_pagamento',
|
||||
'registrato_da',
|
||||
// Compatibilità con vecchia struttura
|
||||
'gestione_id',
|
||||
'soggetto_id',
|
||||
'importo',
|
||||
'tipo_rata',
|
||||
'note',
|
||||
'note'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'importo' => 'decimal:2',
|
||||
'importo_rata' => 'decimal:2',
|
||||
'importo_pagato' => 'decimal:2',
|
||||
'importo_residuo' => 'decimal:2',
|
||||
'data_scadenza' => 'date',
|
||||
'data_pagamento' => 'date',
|
||||
'dettagli_pagamento' => 'array',
|
||||
'numero_rata' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
// Compatibilità
|
||||
'importo' => 'decimal:2'
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'data_scadenza',
|
||||
'data_pagamento',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Relazione con Gestione
|
||||
* Boot method per generare automaticamente il codice rata
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->codice_rata)) {
|
||||
$model->codice_rata = self::generaCodiceRata();
|
||||
}
|
||||
|
||||
// Inizializza importo_residuo se non impostato
|
||||
if (is_null($model->importo_residuo)) {
|
||||
$model->importo_residuo = $model->importo_rata ?? $model->importo ?? 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un codice univoco per la rata
|
||||
*/
|
||||
public static function generaCodiceRata()
|
||||
{
|
||||
do {
|
||||
$codice = 'RT' . strtoupper(Str::random(6));
|
||||
} while (self::where('codice_rata', $codice)->exists());
|
||||
|
||||
return $codice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con PianoRateizzazione
|
||||
*/
|
||||
public function pianoRateizzazione()
|
||||
{
|
||||
return $this->belongsTo(PianoRateizzazione::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con UnitaImmobiliare
|
||||
*/
|
||||
public function unitaImmobiliare()
|
||||
{
|
||||
return $this->belongsTo(UnitaImmobiliare::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con AnagraficaCondominiale
|
||||
*/
|
||||
public function anagraficaCondominiale()
|
||||
{
|
||||
return $this->belongsTo(AnagraficaCondominiale::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con User (registrato da)
|
||||
*/
|
||||
public function registratoDa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'registrato_da');
|
||||
}
|
||||
|
||||
// Relazioni di compatibilità con vecchia struttura
|
||||
/**
|
||||
* Relazione con Gestione (compatibilità)
|
||||
*/
|
||||
public function gestione()
|
||||
{
|
||||
|
|
@ -45,15 +139,7 @@ class Rata extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* Relazione con Unità Immobiliare
|
||||
*/
|
||||
public function unitaImmobiliare()
|
||||
{
|
||||
return $this->belongsTo(UnitaImmobiliare::class, 'unita_immobiliare_id', 'id_unita');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con Soggetto
|
||||
* Relazione con Soggetto (compatibilità)
|
||||
*/
|
||||
public function soggetto()
|
||||
{
|
||||
|
|
@ -63,34 +149,226 @@ class Rata extends Model
|
|||
/**
|
||||
* Scope per stato
|
||||
*/
|
||||
public function scopeStato($query, $stato)
|
||||
public function scopeConStato($query, $stato)
|
||||
{
|
||||
return $query->where('stato', $stato);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per scadute
|
||||
* Scope per rate scadute
|
||||
*/
|
||||
public function scopeScadute($query)
|
||||
{
|
||||
return $query->where('data_scadenza', '<', now())
|
||||
->where('stato', '!=', 'pagata');
|
||||
return $query->where('data_scadenza', '<', now()->toDateString())
|
||||
->whereIn('stato', ['emessa']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per in scadenza
|
||||
* Scope per rate in scadenza
|
||||
*/
|
||||
public function scopeInScadenza($query, $giorni = 30)
|
||||
{
|
||||
return $query->whereBetween('data_scadenza', [now(), now()->addDays($giorni)])
|
||||
->where('stato', '!=', 'pagata');
|
||||
return $query->whereBetween('data_scadenza', [now()->toDateString(), now()->addDays($giorni)->toDateString()])
|
||||
->whereIn('stato', ['emessa']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per importo residuo
|
||||
* Scope per rate pagate
|
||||
*/
|
||||
public function scopePagate($query)
|
||||
{
|
||||
return $query->where('stato', 'pagata');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per rate emesse
|
||||
*/
|
||||
public function scopeEmesse($query)
|
||||
{
|
||||
return $query->where('stato', 'emessa');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per piano rateizzazione
|
||||
*/
|
||||
public function scopePerPiano($query, $pianoId)
|
||||
{
|
||||
return $query->where('piano_rateizzazione_id', $pianoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per unità immobiliare
|
||||
*/
|
||||
public function scopePerUnita($query, $unitaId)
|
||||
{
|
||||
return $query->where('unita_immobiliare_id', $unitaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per registrare pagamento completo
|
||||
*/
|
||||
public function registraPagamento($importo, $metodo = null, $riferimento = null, $note = null, $userId = null)
|
||||
{
|
||||
$importoPagamento = min($importo, $this->importo_residuo);
|
||||
$nuovoImportoPagato = $this->importo_pagato + $importoPagamento;
|
||||
$nuovoImportoResiduo = $this->importo_rata - $nuovoImportoPagato;
|
||||
|
||||
$nuovoStato = $nuovoImportoResiduo <= 0 ? 'pagata' : 'parzialmente_pagata';
|
||||
|
||||
$this->update([
|
||||
'importo_pagato' => $nuovoImportoPagato,
|
||||
'importo_residuo' => $nuovoImportoResiduo,
|
||||
'stato' => $nuovoStato,
|
||||
'data_pagamento' => now(),
|
||||
'metodo_pagamento' => $metodo,
|
||||
'riferimento_pagamento' => $riferimento,
|
||||
'note_pagamento' => $note,
|
||||
'registrato_da' => $userId ?? auth()->id()
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per registrare pagamento parziale
|
||||
*/
|
||||
public function registraPagamentoParziale($importo, $metodo = null, $riferimento = null, $note = null, $userId = null)
|
||||
{
|
||||
return $this->registraPagamento($importo, $metodo, $riferimento, $note, $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per annullare la rata
|
||||
*/
|
||||
public function annulla($motivo = null)
|
||||
{
|
||||
$this->update([
|
||||
'stato' => 'annullata',
|
||||
'note_pagamento' => $motivo
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per verificare se è scaduta
|
||||
*/
|
||||
public function isScaduta()
|
||||
{
|
||||
return $this->data_scadenza < now()->toDateString() &&
|
||||
in_array($this->stato, ['emessa']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per verificare se è in scadenza
|
||||
*/
|
||||
public function isInScadenza($giorni = 30)
|
||||
{
|
||||
$dataLimite = now()->addDays($giorni)->toDateString();
|
||||
return $this->data_scadenza <= $dataLimite &&
|
||||
$this->data_scadenza >= now()->toDateString() &&
|
||||
in_array($this->stato, ['emessa']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per aggiornare automaticamente lo stato
|
||||
*/
|
||||
public function aggiornaStato()
|
||||
{
|
||||
if ($this->stato === 'emessa' && $this->isScaduta()) {
|
||||
$this->update(['stato' => 'scaduta']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se può essere modificata
|
||||
*/
|
||||
public function puoEssereModificata()
|
||||
{
|
||||
return in_array($this->stato, ['emessa', 'scaduta']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se può essere eliminata
|
||||
*/
|
||||
public function puoEssereEliminata()
|
||||
{
|
||||
return in_array($this->stato, ['emessa', 'annullata']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per importo residuo calcolato
|
||||
*/
|
||||
public function getImportoResiduoCalcolatoAttribute()
|
||||
{
|
||||
return ($this->importo_rata ?? $this->importo ?? 0) - ($this->importo_pagato ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per stato formattato
|
||||
*/
|
||||
public function getStatoFormattatoAttribute()
|
||||
{
|
||||
return match ($this->stato) {
|
||||
'emessa' => 'Emessa',
|
||||
'pagata' => 'Pagata',
|
||||
'parzialmente_pagata' => 'Parzialmente Pagata',
|
||||
'scaduta' => 'Scaduta',
|
||||
'annullata' => 'Annullata',
|
||||
default => ucfirst($this->stato)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per metodo pagamento formattato
|
||||
*/
|
||||
public function getMetodoPagamentoFormattatoAttribute()
|
||||
{
|
||||
return match ($this->metodo_pagamento) {
|
||||
'bonifico' => 'Bonifico Bancario',
|
||||
'bollettino' => 'Bollettino Postale',
|
||||
'contanti' => 'Contanti',
|
||||
'assegno' => 'Assegno',
|
||||
'pos' => 'POS/Carta',
|
||||
'altro' => 'Altro',
|
||||
default => $this->metodo_pagamento ? ucfirst($this->metodo_pagamento) : 'Non Specificato'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per giorni alla scadenza
|
||||
*/
|
||||
public function getGiorniAllaScadenzaAttribute()
|
||||
{
|
||||
return now()->diffInDays($this->data_scadenza, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per percentuale pagata
|
||||
*/
|
||||
public function getPercentualePagataAttribute()
|
||||
{
|
||||
$importoTotale = $this->importo_rata ?? $this->importo ?? 0;
|
||||
if ($importoTotale == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round((($this->importo_pagato ?? 0) / $importoTotale) * 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor compatibilità con vecchia struttura
|
||||
*/
|
||||
public function getImportoResiduoAttribute()
|
||||
{
|
||||
return $this->importo - $this->importo_pagato;
|
||||
// Se esiste la nuova colonna, usala
|
||||
if (isset($this->attributes['importo_residuo'])) {
|
||||
return $this->attributes['importo_residuo'];
|
||||
}
|
||||
|
||||
// Altrimenti calcola
|
||||
return $this->getImportoResiduoCalcolatoAttribute();
|
||||
}
|
||||
}
|
||||
328
app/Models/RipartizioneSpese.php
Normal file
328
app/Models/RipartizioneSpese.php
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class RipartizioneSpese
|
||||
*
|
||||
* Gestisce la ripartizione delle spese condominiali
|
||||
* Collega le voci di spesa alle unità immobiliari attraverso i millesimi
|
||||
*
|
||||
* @package App\Models
|
||||
*/
|
||||
class RipartizioneSpese extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'ripartizione_spese';
|
||||
|
||||
protected $fillable = [
|
||||
'codice_ripartizione',
|
||||
'voce_spesa_id',
|
||||
'stabile_id',
|
||||
'tabella_millesimale_id',
|
||||
'tipo_ripartizione',
|
||||
'importo_totale',
|
||||
'data_ripartizione',
|
||||
'stato',
|
||||
'note',
|
||||
'parametri_speciali',
|
||||
'creato_da',
|
||||
'confermata_at',
|
||||
'confermata_da'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'importo_totale' => 'decimal:2',
|
||||
'data_ripartizione' => 'date',
|
||||
'parametri_speciali' => 'array',
|
||||
'confermata_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'data_ripartizione',
|
||||
'confermata_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot method per generare automaticamente il codice ripartizione
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->codice_ripartizione)) {
|
||||
$model->codice_ripartizione = self::generaCodiceRipartizione();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un codice univoco per la ripartizione
|
||||
*/
|
||||
public static function generaCodiceRipartizione()
|
||||
{
|
||||
do {
|
||||
$codice = 'RS' . strtoupper(Str::random(6));
|
||||
} while (self::where('codice_ripartizione', $codice)->exists());
|
||||
|
||||
return $codice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con VoceSpesa
|
||||
*/
|
||||
public function voceSpesa()
|
||||
{
|
||||
return $this->belongsTo(VoceSpesa::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con Stabile
|
||||
*/
|
||||
public function stabile()
|
||||
{
|
||||
return $this->belongsTo(Stabile::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con TabellaMillesimale
|
||||
*/
|
||||
public function tabellaMillesimale()
|
||||
{
|
||||
return $this->belongsTo(TabellaMillesimale::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con User (creato da)
|
||||
*/
|
||||
public function creatoDa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creato_da');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con User (confermata da)
|
||||
*/
|
||||
public function confermataDa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'confermata_da');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con DettaglioRipartizioneSpese
|
||||
*/
|
||||
public function dettagli()
|
||||
{
|
||||
return $this->hasMany(DettaglioRipartizioneSpese::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per ripartizioni per stabile
|
||||
*/
|
||||
public function scopePerStabile($query, $stabileId)
|
||||
{
|
||||
return $query->where('stabile_id', $stabileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per stato
|
||||
*/
|
||||
public function scopeConStato($query, $stato)
|
||||
{
|
||||
return $query->where('stato', $stato);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per tipo ripartizione
|
||||
*/
|
||||
public function scopePerTipo($query, $tipo)
|
||||
{
|
||||
return $query->where('tipo_ripartizione', $tipo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per periodo
|
||||
*/
|
||||
public function scopePerPeriodo($query, $dataInizio, $dataFine)
|
||||
{
|
||||
return $query->whereBetween('data_ripartizione', [$dataInizio, $dataFine]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per confermare la ripartizione
|
||||
*/
|
||||
public function conferma($userId = null)
|
||||
{
|
||||
$this->update([
|
||||
'stato' => 'confermata',
|
||||
'confermata_at' => now(),
|
||||
'confermata_da' => $userId ?? auth()->id()
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per annullare la ripartizione
|
||||
*/
|
||||
public function annulla()
|
||||
{
|
||||
$this->update(['stato' => 'annullata']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per contabilizzare la ripartizione
|
||||
*/
|
||||
public function contabilizza()
|
||||
{
|
||||
$this->update(['stato' => 'contabilizzata']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se può essere modificata
|
||||
*/
|
||||
public function puoEssereModificata()
|
||||
{
|
||||
return in_array($this->stato, ['bozza']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se può essere eliminata
|
||||
*/
|
||||
public function puoEssereEliminata()
|
||||
{
|
||||
return in_array($this->stato, ['bozza', 'annullata']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola automaticamente la ripartizione per tutte le unità
|
||||
*/
|
||||
public function calcolaRipartizione()
|
||||
{
|
||||
if (!$this->tabellaMillesimale) {
|
||||
throw new \Exception('Tabella millesimale non specificata');
|
||||
}
|
||||
|
||||
$unitaImmobiliari = $this->stabile->unitaImmobiliari;
|
||||
$dettagliMillesimali = $this->tabellaMillesimale->dettagli;
|
||||
|
||||
$this->dettagli()->delete(); // Pulisce i dettagli esistenti
|
||||
|
||||
foreach ($unitaImmobiliari as $unita) {
|
||||
$dettaglioMillesimale = $dettagliMillesimali->where('unita_immobiliare_id', $unita->id)->first();
|
||||
|
||||
if (!$dettaglioMillesimale) {
|
||||
continue; // Salta se non ha millesimi
|
||||
}
|
||||
|
||||
$quotaCalcolata = ($this->importo_totale * $dettaglioMillesimale->valore_millesimi) / 1000;
|
||||
|
||||
$this->dettagli()->create([
|
||||
'unita_immobiliare_id' => $unita->id,
|
||||
'anagrafica_condominiale_id' => $unita->anagrafiche()->where('tipo_diritto', 'proprietario')->first()?->id,
|
||||
'millesimi_applicati' => $dettaglioMillesimale->valore_millesimi,
|
||||
'quota_calcolata' => $quotaCalcolata,
|
||||
'quota_finale' => $quotaCalcolata,
|
||||
'tipo_soggetto' => 'proprietario'
|
||||
]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applica ripartizione inquilini/proprietari
|
||||
*/
|
||||
public function applicaRipartizioneInquilini()
|
||||
{
|
||||
foreach ($this->dettagli as $dettaglio) {
|
||||
$ripartizioneInquilini = $dettaglio->unitaImmobiliare
|
||||
->ripartizioniSpeseInquilini()
|
||||
->attive()
|
||||
->byTipoSpesa($this->voceSpesa->categoria)
|
||||
->first();
|
||||
|
||||
if ($ripartizioneInquilini && $ripartizioneInquilini->percentuale_inquilino > 0) {
|
||||
$importoInquilino = $dettaglio->quota_finale * ($ripartizioneInquilini->percentuale_inquilino / 100);
|
||||
$importoProprietario = $dettaglio->quota_finale - $importoInquilino;
|
||||
|
||||
$dettaglio->update([
|
||||
'percentuale_inquilino' => $ripartizioneInquilini->percentuale_inquilino,
|
||||
'importo_inquilino' => $importoInquilino,
|
||||
'importo_proprietario' => $importoProprietario
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ricalcola i totali
|
||||
*/
|
||||
public function ricalcolaTotali()
|
||||
{
|
||||
$importoTotaleCalcolato = $this->dettagli()->sum('quota_finale');
|
||||
|
||||
$this->update([
|
||||
'importo_totale' => $importoTotaleCalcolato
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni riepilogo ripartizione
|
||||
*/
|
||||
public function getRiepilogo()
|
||||
{
|
||||
return [
|
||||
'totale_ripartito' => $this->dettagli()->sum('quota_finale'),
|
||||
'numero_unita' => $this->dettagli()->count(),
|
||||
'numero_unita_esenti' => $this->dettagli()->where('esente', true)->count(),
|
||||
'importo_proprietari' => $this->dettagli()->sum('importo_proprietario'),
|
||||
'importo_inquilini' => $this->dettagli()->sum('importo_inquilino'),
|
||||
'media_per_unita' => $this->dettagli()->count() > 0 ? $this->dettagli()->avg('quota_finale') : 0
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per il tipo ripartizione formattato
|
||||
*/
|
||||
public function getTipoRipartizioneFormattatoAttribute()
|
||||
{
|
||||
return match ($this->tipo_ripartizione) {
|
||||
'millesimale' => 'Ripartizione Millesimale',
|
||||
'uguale' => 'Ripartizione Uguale',
|
||||
'personalizzata' => 'Ripartizione Personalizzata',
|
||||
'speciale' => 'Ripartizione Speciale',
|
||||
default => ucfirst($this->tipo_ripartizione)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per stato formattato
|
||||
*/
|
||||
public function getStatoFormattatoAttribute()
|
||||
{
|
||||
return match ($this->stato) {
|
||||
'bozza' => 'Bozza',
|
||||
'confermata' => 'Confermata',
|
||||
'contabilizzata' => 'Contabilizzata',
|
||||
'annullata' => 'Annullata',
|
||||
default => ucfirst($this->stato)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,22 @@ class Stabile extends Model
|
|||
return $this->hasMany(Ticket::class, 'stabile_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con RipartizioneSpese
|
||||
*/
|
||||
public function ripartizioniSpese()
|
||||
{
|
||||
return $this->hasMany(RipartizioneSpese::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con PianoRateizzazione
|
||||
*/
|
||||
public function pianiRateizzazione()
|
||||
{
|
||||
return $this->hasMany(PianoRateizzazione::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per stabili attivi
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ class TabellaMillesimale extends Model
|
|||
return $this->hasMany(DettaglioTabellaMillesimale::class, 'tabella_millesimale_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con RipartizioneSpese
|
||||
*/
|
||||
public function ripartizioniSpese()
|
||||
{
|
||||
return $this->hasMany(RipartizioneSpese::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per tabelle attive
|
||||
*/
|
||||
|
|
@ -165,4 +173,21 @@ class TabellaMillesimale extends Model
|
|||
'personalizzata' => 'Personalizzata'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per creare ripartizione spese automatica
|
||||
*/
|
||||
public function creaRipartizioneSpese($voceSpesaId, $importoTotale, $dataRipartizione = null)
|
||||
{
|
||||
return RipartizioneSpese::create([
|
||||
'voce_spesa_id' => $voceSpesaId,
|
||||
'stabile_id' => $this->id,
|
||||
'tabella_millesimale_id' => $this->tabelleMillesimali()->where('tipo', 'generale')->first()?->id,
|
||||
'tipo_ripartizione' => 'millesimale',
|
||||
'importo_totale' => $importoTotale,
|
||||
'data_ripartizione' => $dataRipartizione ?? now(),
|
||||
'stato' => 'bozza',
|
||||
'creato_da' => auth()->id() ?? 1
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -134,6 +134,54 @@ class UnitaImmobiliare extends Model
|
|||
return $this->hasMany(Proprieta::class, 'unita_immobiliare_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con DettaglioRipartizioneSpese
|
||||
*/
|
||||
public function dettagliRipartizioneSpese()
|
||||
{
|
||||
return $this->hasMany(DettaglioRipartizioneSpese::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con Rate
|
||||
*/
|
||||
public function rate()
|
||||
{
|
||||
return $this->hasMany(Rata::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate attive (emesse o parzialmente pagate)
|
||||
*/
|
||||
public function rateAttive()
|
||||
{
|
||||
return $this->hasMany(Rata::class)->whereIn('stato', ['emessa', 'parzialmente_pagata', 'scaduta']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate scadute
|
||||
*/
|
||||
public function rateScadute()
|
||||
{
|
||||
return $this->hasMany(Rata::class)->scadute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per ottenere il totale delle rate da pagare
|
||||
*/
|
||||
public function getTotaleRateDaPagare()
|
||||
{
|
||||
return $this->rateAttive()->sum('importo_residuo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per ottenere il numero di rate scadute
|
||||
*/
|
||||
public function getNumeroRateScadute()
|
||||
{
|
||||
return $this->rateScadute()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per unità attive
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ class VoceSpesa extends Model
|
|||
return $this->belongsTo(TabellaMillesimale::class, 'tabella_millesimale_default_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relazione con RipartizioneSpese
|
||||
*/
|
||||
public function ripartizioniSpese()
|
||||
{
|
||||
return $this->hasMany(RipartizioneSpese::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per voci attive
|
||||
*/
|
||||
|
|
@ -70,4 +78,145 @@ class VoceSpesa extends Model
|
|||
{
|
||||
return $query->orderBy('ordinamento')->orderBy('descrizione');
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot del modello per generazione automatica codice
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->codice)) {
|
||||
$model->codice = self::generateUniqueCode($model->stabile_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un codice univoco per la voce di spesa
|
||||
*/
|
||||
private static function generateUniqueCode($stabileId)
|
||||
{
|
||||
do {
|
||||
$code = 'VS' . str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT);
|
||||
} while (self::where('stabile_id', $stabileId)->where('codice', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per categoria specifica
|
||||
*/
|
||||
public function scopeByCategoria($query, $categoria)
|
||||
{
|
||||
return $query->where('categoria', $categoria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni tutte le categorie standard
|
||||
*/
|
||||
public static function getCategorieStandard()
|
||||
{
|
||||
return [
|
||||
'manutenzione_ordinaria' => 'Manutenzione Ordinaria',
|
||||
'manutenzione_straordinaria' => 'Manutenzione Straordinaria',
|
||||
'pulizia' => 'Pulizia',
|
||||
'illuminazione' => 'Illuminazione',
|
||||
'riscaldamento' => 'Riscaldamento',
|
||||
'acqua' => 'Acqua',
|
||||
'energia_elettrica' => 'Energia Elettrica',
|
||||
'gas' => 'Gas',
|
||||
'ascensore' => 'Ascensore',
|
||||
'giardino_verde' => 'Giardino e Verde',
|
||||
'sicurezza' => 'Sicurezza',
|
||||
'amministrazione' => 'Amministrazione',
|
||||
'assicurazioni' => 'Assicurazioni',
|
||||
'tasse_tributi' => 'Tasse e Tributi',
|
||||
'spese_legali' => 'Spese Legali',
|
||||
'lavori_miglioramento' => 'Lavori di Miglioramento',
|
||||
'altre_spese' => 'Altre Spese'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ottieni i tipi di gestione
|
||||
*/
|
||||
public static function getTipiGestione()
|
||||
{
|
||||
return [
|
||||
'ordinaria' => 'Gestione Ordinaria',
|
||||
'straordinaria' => 'Gestione Straordinaria',
|
||||
'speciale' => 'Gestione Speciale'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per descrizione completa
|
||||
*/
|
||||
public function getDescrizioneCompletaAttribute()
|
||||
{
|
||||
return $this->codice . ' - ' . $this->descrizione;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per categoria formattata
|
||||
*/
|
||||
public function getCategoriaFormattataAttribute()
|
||||
{
|
||||
$categorie = self::getCategorieStandard();
|
||||
return $categorie[$this->categoria] ?? $this->categoria;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor per riassunto voce
|
||||
*/
|
||||
public function getRiassuntoAttribute()
|
||||
{
|
||||
return [
|
||||
'codice' => $this->codice,
|
||||
'descrizione' => $this->descrizione,
|
||||
'categoria' => $this->categoria_formattata,
|
||||
'tipo_gestione' => $this->tipo_gestione,
|
||||
'tabella_default' => $this->tabellaMillesimaleDefault->nome_tabella_millesimale ?? 'Nessuna',
|
||||
'ritenuta_default' => $this->ritenuta_acconto_default . '%',
|
||||
'attiva' => $this->attiva ? 'Sì' : 'No'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per duplicare voce di spesa
|
||||
*/
|
||||
public function duplica($nuovaDescrizione = null)
|
||||
{
|
||||
$nuovaVoce = $this->replicate();
|
||||
$nuovaVoce->codice = null; // Verrà rigenerato automaticamente
|
||||
$nuovaVoce->descrizione = $nuovaDescrizione ?? ($this->descrizione . ' (Copia)');
|
||||
$nuovaVoce->save();
|
||||
|
||||
return $nuovaVoce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope per ricerca testuale
|
||||
*/
|
||||
public function scopeRicerca($query, $termine)
|
||||
{
|
||||
return $query->where(function($q) use ($termine) {
|
||||
$q->where('codice', 'LIKE', "%{$termine}%")
|
||||
->orWhere('descrizione', 'LIKE', "%{$termine}%")
|
||||
->orWhere('categoria', 'LIKE', "%{$termine}%");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Metodo per ottenere ripartizioni spese che utilizzano questa voce
|
||||
*/
|
||||
public function getRipartizioniSpese()
|
||||
{
|
||||
return $this->ripartizioniSpese()
|
||||
->with(['stabile', 'tabellaMillesimale', 'dettagli'])
|
||||
->orderBy('data_ripartizione', 'desc')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('voci_spesa', function (Blueprint $table) {
|
||||
// Rinomina la chiave primaria se necessario
|
||||
if (Schema::hasColumn('voci_spesa', 'id_voce')) {
|
||||
$table->renameColumn('id_voce', 'id');
|
||||
} else if (!Schema::hasColumn('voci_spesa', 'id')) {
|
||||
$table->id()->first();
|
||||
}
|
||||
|
||||
// Aggiungi campi mancanti
|
||||
if (!Schema::hasColumn('voci_spesa', 'stabile_id')) {
|
||||
$table->unsignedBigInteger('stabile_id')->after('id');
|
||||
$table->foreign('stabile_id')->references('id')->on('stabili')->onDelete('cascade');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('voci_spesa', 'categoria')) {
|
||||
$table->string('categoria')->after('descrizione');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('voci_spesa', 'tipo_gestione')) {
|
||||
$table->string('tipo_gestione')->default('ordinaria')->after('categoria');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('voci_spesa', 'tabella_millesimale_default_id')) {
|
||||
$table->unsignedBigInteger('tabella_millesimale_default_id')->nullable()->after('tipo_gestione');
|
||||
$table->foreign('tabella_millesimale_default_id')->references('id')->on('tabelle_millesimali')->onDelete('set null');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('voci_spesa', 'ritenuta_acconto_default')) {
|
||||
$table->decimal('ritenuta_acconto_default', 5, 2)->default(0.00)->after('tabella_millesimale_default_id');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('voci_spesa', 'attiva')) {
|
||||
$table->boolean('attiva')->default(true)->after('ritenuta_acconto_default');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('voci_spesa', 'ordinamento')) {
|
||||
$table->integer('ordinamento')->default(0)->after('attiva');
|
||||
}
|
||||
|
||||
// Aggiungi indici per performance
|
||||
$table->index(['stabile_id', 'categoria']);
|
||||
$table->index(['stabile_id', 'attiva']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('voci_spesa', function (Blueprint $table) {
|
||||
// Rimuovi foreign keys
|
||||
$table->dropForeign(['stabile_id']);
|
||||
$table->dropForeign(['tabella_millesimale_default_id']);
|
||||
|
||||
// Rimuovi colonne aggiunte
|
||||
$table->dropColumn([
|
||||
'stabile_id',
|
||||
'categoria',
|
||||
'tipo_gestione',
|
||||
'tabella_millesimale_default_id',
|
||||
'ritenuta_acconto_default',
|
||||
'attiva',
|
||||
'ordinamento'
|
||||
]);
|
||||
|
||||
// Ripristina nome colonna originale se era stata rinominata
|
||||
if (Schema::hasColumn('voci_spesa', 'id')) {
|
||||
$table->renameColumn('id', 'id_voce');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ripartizione_spese', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ripartizione_spese');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('dettaglio_ripartizione_spese', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('dettaglio_ripartizione_spese');
|
||||
}
|
||||
};
|
||||
27
database/migrations/2025_07_08_151654_create_rate_table.php
Normal file
27
database/migrations/2025_07_08_151654_create_rate_table.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rate', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('rate');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('piano_rateizzazione', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('piano_rateizzazione');
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user