1383 lines
47 KiB
Markdown
1383 lines
47 KiB
Markdown
# 🌡️ **NetGesCon Laravel - Gestione Consumi Acqua e Riscaldamento**
|
|
|
|
## 📍 **OVERVIEW SISTEMA CONSUMI**
|
|
Modulo completo per gestione consumi acqua e riscaldamento con supporto per letture manuali, import da ditta, collegamento fatture, gestione millesimi non standard e calcolo automatico ripartizione.
|
|
|
|
---
|
|
|
|
## 🏗️ **ARCHITETTURA DATABASE CONSUMI**
|
|
|
|
### **1. Tabella Contatori**
|
|
```sql
|
|
-- Migration: create_contatori_table.php
|
|
CREATE TABLE contatori (
|
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
|
stabile_id BIGINT UNSIGNED NOT NULL,
|
|
unita_immobiliare_id BIGINT UNSIGNED NULL, -- NULL per contatori generali
|
|
tipo_contatore ENUM('acqua_fredda', 'acqua_calda', 'riscaldamento', 'gas', 'energia_elettrica') NOT NULL,
|
|
codice_contatore VARCHAR(50) NOT NULL,
|
|
numero_matricola VARCHAR(100) NOT NULL,
|
|
marca VARCHAR(100),
|
|
modello VARCHAR(100),
|
|
anno_installazione YEAR,
|
|
coefficiente_moltiplicatore DECIMAL(8,4) DEFAULT 1.0000,
|
|
unita_misura ENUM('mc', 'kWh', 'kcal', 'Gj') NOT NULL,
|
|
posizione_contatore VARCHAR(255),
|
|
note VARCHAR(500),
|
|
attivo BOOLEAN DEFAULT TRUE,
|
|
data_installazione DATE,
|
|
data_dismissione DATE NULL,
|
|
creato_da BIGINT UNSIGNED NOT NULL,
|
|
modificato_da BIGINT UNSIGNED NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
-- Indexes
|
|
INDEX idx_stabile_tipo (stabile_id, tipo_contatore),
|
|
INDEX idx_unita_tipo (unita_immobiliare_id, tipo_contatore),
|
|
INDEX idx_codice (codice_contatore),
|
|
INDEX idx_matricola (numero_matricola),
|
|
INDEX idx_attivo (attivo),
|
|
|
|
-- Constraints
|
|
UNIQUE KEY uk_stabile_codice (stabile_id, codice_contatore),
|
|
UNIQUE KEY uk_matricola (numero_matricola),
|
|
|
|
-- Foreign Keys
|
|
FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (creato_da) REFERENCES users(id),
|
|
FOREIGN KEY (modificato_da) REFERENCES users(id)
|
|
);
|
|
```
|
|
|
|
### **2. Tabella Letture Contatori**
|
|
```sql
|
|
-- Migration: create_letture_contatori_table.php
|
|
CREATE TABLE letture_contatori (
|
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
|
contatore_id BIGINT UNSIGNED NOT NULL,
|
|
stabile_id BIGINT UNSIGNED NOT NULL,
|
|
periodo_riferimento VARCHAR(7) NOT NULL, -- YYYY-MM formato
|
|
data_lettura DATE NOT NULL,
|
|
lettura_precedente DECIMAL(10,3) DEFAULT 0.000,
|
|
lettura_attuale DECIMAL(10,3) NOT NULL,
|
|
consumo_calcolato DECIMAL(10,3) AS (lettura_attuale - lettura_precedente) STORED,
|
|
consumo_rettificato DECIMAL(10,3) NULL, -- Per correzioni manuali
|
|
tipo_lettura ENUM('manuale', 'automatica', 'stimata', 'import_ditta') NOT NULL,
|
|
fonte_lettura VARCHAR(100), -- Nome ditta o utente
|
|
note_lettura TEXT,
|
|
validata BOOLEAN DEFAULT FALSE,
|
|
validata_da BIGINT UNSIGNED NULL,
|
|
validata_at TIMESTAMP NULL,
|
|
importo_unitario DECIMAL(8,4) NULL, -- Per collegamento fatture
|
|
importo_totale DECIMAL(10,2) NULL,
|
|
fattura_collegata VARCHAR(255) NULL,
|
|
-- Metadati per import automatico
|
|
import_batch_id VARCHAR(50) NULL,
|
|
import_file_name VARCHAR(255) NULL,
|
|
import_riga_numero INT NULL,
|
|
-- Audit fields
|
|
inserita_da BIGINT UNSIGNED NOT NULL,
|
|
modificata_da BIGINT UNSIGNED NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
-- Indexes
|
|
INDEX idx_contatore_periodo (contatore_id, periodo_riferimento),
|
|
INDEX idx_stabile_periodo (stabile_id, periodo_riferimento),
|
|
INDEX idx_data_lettura (data_lettura),
|
|
INDEX idx_tipo_lettura (tipo_lettura),
|
|
INDEX idx_validata (validata),
|
|
INDEX idx_import_batch (import_batch_id),
|
|
|
|
-- Constraints
|
|
UNIQUE KEY uk_contatore_periodo (contatore_id, periodo_riferimento),
|
|
CHECK (lettura_attuale >= lettura_precedente),
|
|
|
|
-- Foreign Keys
|
|
FOREIGN KEY (contatore_id) REFERENCES contatori(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (validata_da) REFERENCES users(id),
|
|
FOREIGN KEY (inserita_da) REFERENCES users(id),
|
|
FOREIGN KEY (modificata_da) REFERENCES users(id)
|
|
);
|
|
```
|
|
|
|
### **3. Tabella Ripartizione Consumi**
|
|
```sql
|
|
-- Migration: create_ripartizione_consumi_table.php
|
|
CREATE TABLE ripartizione_consumi (
|
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
|
stabile_id BIGINT UNSIGNED NOT NULL,
|
|
periodo_riferimento VARCHAR(7) NOT NULL, -- YYYY-MM
|
|
tipo_consumo ENUM('acqua_fredda', 'acqua_calda', 'riscaldamento', 'gas', 'energia_elettrica') NOT NULL,
|
|
modalita_ripartizione ENUM('contatori_individuali', 'millesimi_standard', 'millesimi_personalizzati', 'misto') NOT NULL,
|
|
tabella_millesimale_id BIGINT UNSIGNED NULL, -- Per ripartizione millesimale
|
|
consumo_totale_periodo DECIMAL(12,3) NOT NULL,
|
|
importo_totale_periodo DECIMAL(12,2) NOT NULL,
|
|
consumo_generale DECIMAL(12,3) DEFAULT 0.000, -- Da contatore generale
|
|
consumo_individuale_totale DECIMAL(12,3) DEFAULT 0.000, -- Somma contatori individuali
|
|
differenza_consumo DECIMAL(12,3) AS (consumo_generale - consumo_individuale_totale) STORED,
|
|
percentuale_perdite DECIMAL(5,2) AS (
|
|
CASE
|
|
WHEN consumo_generale > 0 THEN ((consumo_generale - consumo_individuale_totale) / consumo_generale) * 100
|
|
ELSE 0
|
|
END
|
|
) STORED,
|
|
-- Configurazione ripartizione
|
|
ripartizione_perdite ENUM('proporzionale', 'millesimi', 'fissa', 'escludi') DEFAULT 'proporzionale',
|
|
quota_fissa_percentuale DECIMAL(5,2) DEFAULT 0.00, -- % quota fissa
|
|
quota_variabile_percentuale DECIMAL(5,2) DEFAULT 100.00, -- % quota variabile
|
|
-- Stato processamento
|
|
stato_ripartizione ENUM('bozza', 'calcolata', 'verificata', 'confermata', 'fatturata') DEFAULT 'bozza',
|
|
data_calcolo TIMESTAMP NULL,
|
|
calcolata_da BIGINT UNSIGNED NULL,
|
|
data_conferma TIMESTAMP NULL,
|
|
confermata_da BIGINT UNSIGNED NULL,
|
|
note_ripartizione TEXT,
|
|
-- Audit
|
|
creata_da BIGINT UNSIGNED NOT NULL,
|
|
modificata_da BIGINT UNSIGNED NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
-- Indexes
|
|
INDEX idx_stabile_periodo (stabile_id, periodo_riferimento),
|
|
INDEX idx_tipo_consumo (tipo_consumo),
|
|
INDEX idx_stato (stato_ripartizione),
|
|
INDEX idx_modalita (modalita_ripartizione),
|
|
|
|
-- Constraints
|
|
UNIQUE KEY uk_stabile_periodo_tipo (stabile_id, periodo_riferimento, tipo_consumo),
|
|
CHECK (quota_fissa_percentuale + quota_variabile_percentuale = 100),
|
|
|
|
-- Foreign Keys
|
|
FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (tabella_millesimale_id) REFERENCES tabelle_millesimali(id) ON DELETE SET NULL,
|
|
FOREIGN KEY (calcolata_da) REFERENCES users(id),
|
|
FOREIGN KEY (confermata_da) REFERENCES users(id),
|
|
FOREIGN KEY (creata_da) REFERENCES users(id),
|
|
FOREIGN KEY (modificata_da) REFERENCES users(id)
|
|
);
|
|
```
|
|
|
|
### **4. Tabella Dettaglio Ripartizione Consumi**
|
|
```sql
|
|
-- Migration: create_dettaglio_ripartizione_consumi_table.php
|
|
CREATE TABLE dettaglio_ripartizione_consumi (
|
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
|
ripartizione_consumo_id BIGINT UNSIGNED NOT NULL,
|
|
unita_immobiliare_id BIGINT UNSIGNED NOT NULL,
|
|
contatore_id BIGINT UNSIGNED NULL, -- NULL se ripartizione millesimale
|
|
-- Consumi
|
|
consumo_individuale DECIMAL(10,3) DEFAULT 0.000,
|
|
consumo_quota_fissa DECIMAL(10,3) DEFAULT 0.000,
|
|
consumo_quota_variabile DECIMAL(10,3) DEFAULT 0.000,
|
|
consumo_perdite DECIMAL(10,3) DEFAULT 0.000,
|
|
consumo_totale DECIMAL(10,3) AS (consumo_individuale + consumo_quota_fissa + consumo_quota_variabile + consumo_perdite) STORED,
|
|
-- Importi
|
|
importo_quota_fissa DECIMAL(10,2) DEFAULT 0.00,
|
|
importo_quota_variabile DECIMAL(10,2) DEFAULT 0.00,
|
|
importo_perdite DECIMAL(10,2) DEFAULT 0.00,
|
|
importo_totale DECIMAL(10,2) AS (importo_quota_fissa + importo_quota_variabile + importo_perdite) STORED,
|
|
-- Millesimi utilizzati
|
|
millesimi_quota_fissa DECIMAL(8,4) DEFAULT 0.0000,
|
|
millesimi_quota_variabile DECIMAL(8,4) DEFAULT 0.0000,
|
|
millesimi_perdite DECIMAL(8,4) DEFAULT 0.0000,
|
|
-- Flags
|
|
esentato BOOLEAN DEFAULT FALSE,
|
|
note_esenzione VARCHAR(255) NULL,
|
|
-- Audit
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
-- Indexes
|
|
INDEX idx_ripartizione (ripartizione_consumo_id),
|
|
INDEX idx_unita (unita_immobiliare_id),
|
|
INDEX idx_contatore (contatore_id),
|
|
INDEX idx_esentato (esentato),
|
|
|
|
-- Constraints
|
|
UNIQUE KEY uk_ripartizione_unita (ripartizione_consumo_id, unita_immobiliare_id),
|
|
|
|
-- Foreign Keys
|
|
FOREIGN KEY (ripartizione_consumo_id) REFERENCES ripartizione_consumi(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (contatore_id) REFERENCES contatori(id) ON DELETE SET NULL
|
|
);
|
|
```
|
|
|
|
### **5. Tabella Parametri Calcolo**
|
|
```sql
|
|
-- Migration: create_parametri_calcolo_consumi_table.php
|
|
CREATE TABLE parametri_calcolo_consumi (
|
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
|
stabile_id BIGINT UNSIGNED NOT NULL,
|
|
tipo_consumo ENUM('acqua_fredda', 'acqua_calda', 'riscaldamento', 'gas', 'energia_elettrica') NOT NULL,
|
|
-- Parametri generali
|
|
millesimi_totali DECIMAL(8,4) DEFAULT 1000.0000,
|
|
gestione_perdite ENUM('proporzionale', 'millesimi', 'fissa', 'escludi') DEFAULT 'proporzionale',
|
|
soglia_perdite_percentuale DECIMAL(5,2) DEFAULT 10.00, -- Alert se perdite > soglia
|
|
-- Quote fisse/variabili
|
|
quota_fissa_abilitata BOOLEAN DEFAULT FALSE,
|
|
quota_fissa_percentuale DECIMAL(5,2) DEFAULT 0.00,
|
|
quota_variabile_percentuale DECIMAL(5,2) DEFAULT 100.00,
|
|
-- Tariffe
|
|
tariffa_base DECIMAL(8,4) DEFAULT 0.0000,
|
|
tariffa_scaglioni JSON NULL, -- Array di scaglioni progressivi
|
|
-- Configurazioni avanzate
|
|
arrotondamento_centesimi ENUM('matematico', 'eccesso', 'difetto') DEFAULT 'matematico',
|
|
decimali_consumo TINYINT DEFAULT 3,
|
|
decimali_importo TINYINT DEFAULT 2,
|
|
-- Validità
|
|
valido_da DATE NOT NULL,
|
|
valido_fino DATE NULL,
|
|
attivo BOOLEAN DEFAULT TRUE,
|
|
-- Audit
|
|
creato_da BIGINT UNSIGNED NOT NULL,
|
|
modificato_da BIGINT UNSIGNED NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
|
-- Indexes
|
|
INDEX idx_stabile_tipo (stabile_id, tipo_consumo),
|
|
INDEX idx_validita (valido_da, valido_fino),
|
|
INDEX idx_attivo (attivo),
|
|
|
|
-- Constraints
|
|
UNIQUE KEY uk_stabile_tipo_validita (stabile_id, tipo_consumo, valido_da),
|
|
CHECK (quota_fissa_percentuale + quota_variabile_percentuale = 100),
|
|
|
|
-- Foreign Keys
|
|
FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (creato_da) REFERENCES users(id),
|
|
FOREIGN KEY (modificato_da) REFERENCES users(id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 **MODELLI ELOQUENT**
|
|
|
|
### **1. Modello Contatore**
|
|
```php
|
|
// app/Models/Contatore.php
|
|
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class Contatore extends Model
|
|
{
|
|
protected $table = 'contatori';
|
|
|
|
protected $fillable = [
|
|
'stabile_id', 'unita_immobiliare_id', 'tipo_contatore',
|
|
'codice_contatore', 'numero_matricola', 'marca', 'modello',
|
|
'anno_installazione', 'coefficiente_moltiplicatore', 'unita_misura',
|
|
'posizione_contatore', 'note', 'attivo', 'data_installazione',
|
|
'data_dismissione', 'creato_da'
|
|
];
|
|
|
|
protected $casts = [
|
|
'coefficiente_moltiplicatore' => 'decimal:4',
|
|
'attivo' => 'boolean',
|
|
'data_installazione' => 'date',
|
|
'data_dismissione' => 'date',
|
|
'anno_installazione' => 'integer'
|
|
];
|
|
|
|
// Relazioni
|
|
public function stabile()
|
|
{
|
|
return $this->belongsTo(Stabile::class);
|
|
}
|
|
|
|
public function unitaImmobiliare()
|
|
{
|
|
return $this->belongsTo(UnitaImmobiliare::class);
|
|
}
|
|
|
|
public function lettureContatori()
|
|
{
|
|
return $this->hasMany(LetturaContatore::class);
|
|
}
|
|
|
|
public function creatoDA()
|
|
{
|
|
return $this->belongsTo(User::class, 'creato_da');
|
|
}
|
|
|
|
public function modificatoDa()
|
|
{
|
|
return $this->belongsTo(User::class, 'modificato_da');
|
|
}
|
|
|
|
// Scopes
|
|
public function scopeAttivi($query)
|
|
{
|
|
return $query->where('attivo', true);
|
|
}
|
|
|
|
public function scopePerTipo($query, $tipo)
|
|
{
|
|
return $query->where('tipo_contatore', $tipo);
|
|
}
|
|
|
|
public function scopeGenerali($query)
|
|
{
|
|
return $query->whereNull('unita_immobiliare_id');
|
|
}
|
|
|
|
public function scopeIndividuali($query)
|
|
{
|
|
return $query->whereNotNull('unita_immobiliare_id');
|
|
}
|
|
|
|
// Metodi utilità
|
|
public function getUltimaLettura()
|
|
{
|
|
return $this->lettureContatori()
|
|
->orderBy('data_lettura', 'desc')
|
|
->first();
|
|
}
|
|
|
|
public function getConsumoMedio($mesi = 12)
|
|
{
|
|
$letture = $this->lettureContatori()
|
|
->where('data_lettura', '>=', now()->subMonths($mesi))
|
|
->where('validata', true)
|
|
->get();
|
|
|
|
if ($letture->count() < 2) {
|
|
return 0;
|
|
}
|
|
|
|
$consumoTotale = $letture->sum('consumo_calcolato');
|
|
return $consumoTotale / $letture->count();
|
|
}
|
|
|
|
public function applicaCoefficiente($valore)
|
|
{
|
|
return $valore * $this->coefficiente_moltiplicatore;
|
|
}
|
|
|
|
public function getDescrizioneCompletaAttribute()
|
|
{
|
|
$descrizione = $this->codice_contatore . ' - ' . $this->getTipoContatore();
|
|
|
|
if ($this->unitaImmobiliare) {
|
|
$descrizione .= ' (Unità: ' . $this->unitaImmobiliare->codice_unita . ')';
|
|
} else {
|
|
$descrizione .= ' (Generale)';
|
|
}
|
|
|
|
return $descrizione;
|
|
}
|
|
|
|
public function getTipoContatore()
|
|
{
|
|
$tipi = [
|
|
'acqua_fredda' => 'Acqua Fredda',
|
|
'acqua_calda' => 'Acqua Calda',
|
|
'riscaldamento' => 'Riscaldamento',
|
|
'gas' => 'Gas',
|
|
'energia_elettrica' => 'Energia Elettrica'
|
|
];
|
|
|
|
return $tipi[$this->tipo_contatore] ?? $this->tipo_contatore;
|
|
}
|
|
|
|
public static function getTipiContatore()
|
|
{
|
|
return [
|
|
'acqua_fredda' => 'Acqua Fredda',
|
|
'acqua_calda' => 'Acqua Calda',
|
|
'riscaldamento' => 'Riscaldamento',
|
|
'gas' => 'Gas',
|
|
'energia_elettrica' => 'Energia Elettrica'
|
|
];
|
|
}
|
|
|
|
public static function getUnitaMisura()
|
|
{
|
|
return [
|
|
'mc' => 'Metri Cubi (m³)',
|
|
'kWh' => 'Kilowattora (kWh)',
|
|
'kcal' => 'Kilocalorie (kcal)',
|
|
'Gj' => 'Gigajoule (GJ)'
|
|
];
|
|
}
|
|
|
|
// Boot method per generazione codice automatica
|
|
protected static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
static::creating(function ($model) {
|
|
if (empty($model->codice_contatore)) {
|
|
$model->codice_contatore = self::generateUniqueCode($model->stabile_id, $model->tipo_contatore);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static function generateUniqueCode($stabileId, $tipo)
|
|
{
|
|
$prefisso = strtoupper(substr($tipo, 0, 2));
|
|
$numero = self::where('stabile_id', $stabileId)
|
|
->where('tipo_contatore', $tipo)
|
|
->count() + 1;
|
|
|
|
do {
|
|
$code = $prefisso . str_pad($numero, 3, '0', STR_PAD_LEFT);
|
|
$numero++;
|
|
} while (self::where('stabile_id', $stabileId)->where('codice_contatore', $code)->exists());
|
|
|
|
return $code;
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Modello Lettura Contatore**
|
|
```php
|
|
// app/Models/LetturaContatore.php
|
|
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class LetturaContatore extends Model
|
|
{
|
|
protected $table = 'letture_contatori';
|
|
|
|
protected $fillable = [
|
|
'contatore_id', 'stabile_id', 'periodo_riferimento', 'data_lettura',
|
|
'lettura_precedente', 'lettura_attuale', 'consumo_rettificato',
|
|
'tipo_lettura', 'fonte_lettura', 'note_lettura', 'validata',
|
|
'importo_unitario', 'importo_totale', 'fattura_collegata',
|
|
'import_batch_id', 'import_file_name', 'import_riga_numero',
|
|
'inserita_da'
|
|
];
|
|
|
|
protected $casts = [
|
|
'data_lettura' => 'date',
|
|
'lettura_precedente' => 'decimal:3',
|
|
'lettura_attuale' => 'decimal:3',
|
|
'consumo_rettificato' => 'decimal:3',
|
|
'importo_unitario' => 'decimal:4',
|
|
'importo_totale' => 'decimal:2',
|
|
'validata' => 'boolean',
|
|
'validata_at' => 'datetime',
|
|
'import_riga_numero' => 'integer'
|
|
];
|
|
|
|
// Relazioni
|
|
public function contatore()
|
|
{
|
|
return $this->belongsTo(Contatore::class);
|
|
}
|
|
|
|
public function stabile()
|
|
{
|
|
return $this->belongsTo(Stabile::class);
|
|
}
|
|
|
|
public function inseritaDa()
|
|
{
|
|
return $this->belongsTo(User::class, 'inserita_da');
|
|
}
|
|
|
|
public function validataDa()
|
|
{
|
|
return $this->belongsTo(User::class, 'validata_da');
|
|
}
|
|
|
|
// Scopes
|
|
public function scopeValidate($query)
|
|
{
|
|
return $query->where('validata', true);
|
|
}
|
|
|
|
public function scopePerPeriodo($query, $periodo)
|
|
{
|
|
return $query->where('periodo_riferimento', $periodo);
|
|
}
|
|
|
|
public function scopePerTipo($query, $tipo)
|
|
{
|
|
return $query->where('tipo_lettura', $tipo);
|
|
}
|
|
|
|
public function scopeImportBatch($query, $batchId)
|
|
{
|
|
return $query->where('import_batch_id', $batchId);
|
|
}
|
|
|
|
// Accessors
|
|
public function getConsumoFinaleAttribute()
|
|
{
|
|
return $this->consumo_rettificato ?? $this->consumo_calcolato;
|
|
}
|
|
|
|
public function getConsumoCalcolatoAttribute()
|
|
{
|
|
return $this->lettura_attuale - $this->lettura_precedente;
|
|
}
|
|
|
|
public function getImportoCalcolatoAttribute()
|
|
{
|
|
if ($this->importo_unitario > 0) {
|
|
return $this->consumo_finale * $this->importo_unitario;
|
|
}
|
|
return $this->importo_totale;
|
|
}
|
|
|
|
// Metodi
|
|
public function valida($userId = null)
|
|
{
|
|
$this->update([
|
|
'validata' => true,
|
|
'validata_da' => $userId ?? auth()->id(),
|
|
'validata_at' => now()
|
|
]);
|
|
}
|
|
|
|
public function invalidate()
|
|
{
|
|
$this->update([
|
|
'validata' => false,
|
|
'validata_da' => null,
|
|
'validata_at' => null
|
|
]);
|
|
}
|
|
|
|
public function rettificaConsumo($nuovoConsumo, $motivo = null)
|
|
{
|
|
$this->update([
|
|
'consumo_rettificato' => $nuovoConsumo,
|
|
'note_lettura' => $this->note_lettura . "\n[Rettifica: {$motivo}]"
|
|
]);
|
|
}
|
|
|
|
public function calcolaVariazionePercentuale()
|
|
{
|
|
$letturaPrec = self::where('contatore_id', $this->contatore_id)
|
|
->where('data_lettura', '<', $this->data_lettura)
|
|
->orderBy('data_lettura', 'desc')
|
|
->first();
|
|
|
|
if (!$letturaPrec || $letturaPrec->consumo_finale == 0) {
|
|
return null;
|
|
}
|
|
|
|
return (($this->consumo_finale - $letturaPrec->consumo_finale) / $letturaPrec->consumo_finale) * 100;
|
|
}
|
|
|
|
public static function getTipiLettura()
|
|
{
|
|
return [
|
|
'manuale' => 'Lettura Manuale',
|
|
'automatica' => 'Lettura Automatica',
|
|
'stimata' => 'Lettura Stimata',
|
|
'import_ditta' => 'Import da Ditta'
|
|
];
|
|
}
|
|
|
|
// Observer hooks
|
|
protected static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
static::creating(function ($model) {
|
|
// Calcola lettura precedente automaticamente
|
|
if (empty($model->lettura_precedente)) {
|
|
$ultimaLettura = self::where('contatore_id', $model->contatore_id)
|
|
->where('data_lettura', '<', $model->data_lettura)
|
|
->orderBy('data_lettura', 'desc')
|
|
->first();
|
|
|
|
$model->lettura_precedente = $ultimaLettura ? $ultimaLettura->lettura_attuale : 0;
|
|
}
|
|
});
|
|
|
|
static::created(function ($model) {
|
|
// Aggiorna letture successive se necessario
|
|
$model->aggiornaLettureSuccessive();
|
|
});
|
|
}
|
|
|
|
private function aggiornaLettureSuccessive()
|
|
{
|
|
$lettureSuccessive = self::where('contatore_id', $this->contatore_id)
|
|
->where('data_lettura', '>', $this->data_lettura)
|
|
->orderBy('data_lettura', 'asc')
|
|
->get();
|
|
|
|
$letturaPrec = $this->lettura_attuale;
|
|
|
|
foreach ($lettureSuccessive as $lettura) {
|
|
$lettura->update(['lettura_precedente' => $letturaPrec]);
|
|
$letturaPrec = $lettura->lettura_attuale;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### **3. Modello Ripartizione Consumi**
|
|
```php
|
|
// app/Models/RipartizioneConsumi.php
|
|
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class RipartizioneConsumi extends Model
|
|
{
|
|
protected $table = 'ripartizione_consumi';
|
|
|
|
protected $fillable = [
|
|
'stabile_id', 'periodo_riferimento', 'tipo_consumo', 'modalita_ripartizione',
|
|
'tabella_millesimale_id', 'consumo_totale_periodo', 'importo_totale_periodo',
|
|
'consumo_generale', 'consumo_individuale_totale', 'ripartizione_perdite',
|
|
'quota_fissa_percentuale', 'quota_variabile_percentuale', 'stato_ripartizione',
|
|
'note_ripartizione', 'creata_da'
|
|
];
|
|
|
|
protected $casts = [
|
|
'consumo_totale_periodo' => 'decimal:3',
|
|
'importo_totale_periodo' => 'decimal:2',
|
|
'consumo_generale' => 'decimal:3',
|
|
'consumo_individuale_totale' => 'decimal:3',
|
|
'quota_fissa_percentuale' => 'decimal:2',
|
|
'quota_variabile_percentuale' => 'decimal:2',
|
|
'data_calcolo' => 'datetime',
|
|
'data_conferma' => 'datetime'
|
|
];
|
|
|
|
// Relazioni
|
|
public function stabile()
|
|
{
|
|
return $this->belongsTo(Stabile::class);
|
|
}
|
|
|
|
public function tabellaMillesimale()
|
|
{
|
|
return $this->belongsTo(TabellaMillesimale::class);
|
|
}
|
|
|
|
public function dettagliRipartizione()
|
|
{
|
|
return $this->hasMany(DettaglioRipartizioneConsumi::class);
|
|
}
|
|
|
|
public function calcolataDa()
|
|
{
|
|
return $this->belongsTo(User::class, 'calcolata_da');
|
|
}
|
|
|
|
public function confermataDa()
|
|
{
|
|
return $this->belongsTo(User::class, 'confermata_da');
|
|
}
|
|
|
|
// Scopes
|
|
public function scopePerStabile($query, $stabileId)
|
|
{
|
|
return $query->where('stabile_id', $stabileId);
|
|
}
|
|
|
|
public function scopePerPeriodo($query, $periodo)
|
|
{
|
|
return $query->where('periodo_riferimento', $periodo);
|
|
}
|
|
|
|
public function scopePerTipo($query, $tipo)
|
|
{
|
|
return $query->where('tipo_consumo', $tipo);
|
|
}
|
|
|
|
public function scopePerStato($query, $stato)
|
|
{
|
|
return $query->where('stato_ripartizione', $stato);
|
|
}
|
|
|
|
// Metodi principali
|
|
public function calcolaRipartizione()
|
|
{
|
|
if ($this->stato_ripartizione !== 'bozza') {
|
|
throw new \Exception('La ripartizione non è in stato bozza');
|
|
}
|
|
|
|
switch ($this->modalita_ripartizione) {
|
|
case 'contatori_individuali':
|
|
return $this->calcolaRipartizioneContatori();
|
|
case 'millesimi_standard':
|
|
return $this->calcolaRipartizioneMillesimi();
|
|
case 'millesimi_personalizzati':
|
|
return $this->calcolaRipartizioneMillesimiPersonalizzati();
|
|
case 'misto':
|
|
return $this->calcolaRipartizioneMista();
|
|
default:
|
|
throw new \Exception('Modalità di ripartizione non supportata');
|
|
}
|
|
}
|
|
|
|
private function calcolaRipartizioneContatori()
|
|
{
|
|
$unitaImmobiliari = $this->stabile->unitaImmobiliari()
|
|
->with(['contatori' => function($query) {
|
|
$query->where('tipo_contatore', $this->tipo_consumo)
|
|
->where('attivo', true);
|
|
}])
|
|
->get();
|
|
|
|
$dettagli = [];
|
|
$consumoTotaleIndividuale = 0;
|
|
|
|
foreach ($unitaImmobiliari as $unita) {
|
|
$contatore = $unita->contatori->first();
|
|
$consumoUnita = 0;
|
|
|
|
if ($contatore) {
|
|
$lettura = $contatore->lettureContatori()
|
|
->where('periodo_riferimento', $this->periodo_riferimento)
|
|
->where('validata', true)
|
|
->first();
|
|
|
|
if ($lettura) {
|
|
$consumoUnita = $lettura->consumo_finale;
|
|
$consumoTotaleIndividuale += $consumoUnita;
|
|
}
|
|
}
|
|
|
|
$dettagli[] = [
|
|
'unita_immobiliare_id' => $unita->id,
|
|
'contatore_id' => $contatore?->id,
|
|
'consumo_individuale' => $consumoUnita
|
|
];
|
|
}
|
|
|
|
// Calcola perdite da ripartire
|
|
$perdite = $this->consumo_generale - $consumoTotaleIndividuale;
|
|
|
|
// Ripartisce perdite e quote fisse
|
|
foreach ($dettagli as &$dettaglio) {
|
|
$unita = $unitaImmobiliari->find($dettaglio['unita_immobiliare_id']);
|
|
|
|
// Quota fissa (se abilitata)
|
|
if ($this->quota_fissa_percentuale > 0) {
|
|
$quotaFissa = $this->calcolaQuotaFissa($unita);
|
|
$dettaglio['consumo_quota_fissa'] = $quotaFissa;
|
|
}
|
|
|
|
// Ripartizione perdite
|
|
if ($perdite > 0) {
|
|
$dettaglio['consumo_perdite'] = $this->calcolaPerdite($unita, $perdite, $consumoTotaleIndividuale);
|
|
}
|
|
|
|
// Calcola importi
|
|
$dettaglio['importo_quota_fissa'] = $this->calcolaImporto($dettaglio['consumo_quota_fissa'] ?? 0);
|
|
$dettaglio['importo_quota_variabile'] = $this->calcolaImporto($dettaglio['consumo_individuale']);
|
|
$dettaglio['importo_perdite'] = $this->calcolaImporto($dettaglio['consumo_perdite'] ?? 0);
|
|
}
|
|
|
|
// Salva dettagli
|
|
$this->dettagliRipartizione()->delete();
|
|
foreach ($dettagli as $dettaglio) {
|
|
$this->dettagliRipartizione()->create($dettaglio);
|
|
}
|
|
|
|
$this->update([
|
|
'consumo_individuale_totale' => $consumoTotaleIndividuale,
|
|
'stato_ripartizione' => 'calcolata',
|
|
'data_calcolo' => now(),
|
|
'calcolata_da' => auth()->id()
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
private function calcolaQuotaFissa($unita)
|
|
{
|
|
if (!$this->tabellaMillesimale) {
|
|
return 0;
|
|
}
|
|
|
|
$millesimi = $this->tabellaMillesimale->dettagliTabellaMillesimale()
|
|
->where('unita_immobiliare_id', $unita->id)
|
|
->first();
|
|
|
|
if (!$millesimi) {
|
|
return 0;
|
|
}
|
|
|
|
$consumoQuotaFissa = $this->consumo_totale_periodo * ($this->quota_fissa_percentuale / 100);
|
|
return $consumoQuotaFissa * ($millesimi->millesimi / 1000);
|
|
}
|
|
|
|
private function calcolaPerdite($unita, $perdite, $consumoTotaleIndividuale)
|
|
{
|
|
switch ($this->ripartizione_perdite) {
|
|
case 'proporzionale':
|
|
if ($consumoTotaleIndividuale > 0) {
|
|
$dettaglio = $this->dettagliRipartizione()->where('unita_immobiliare_id', $unita->id)->first();
|
|
return $perdite * ($dettaglio['consumo_individuale'] / $consumoTotaleIndividuale);
|
|
}
|
|
return 0;
|
|
|
|
case 'millesimi':
|
|
if (!$this->tabellaMillesimale) {
|
|
return 0;
|
|
}
|
|
$millesimi = $this->tabellaMillesimale->dettagliTabellaMillesimale()
|
|
->where('unita_immobiliare_id', $unita->id)
|
|
->first();
|
|
return $perdite * ($millesimi->millesimi / 1000);
|
|
|
|
case 'fissa':
|
|
$numeroUnita = $this->stabile->unitaImmobiliari()->count();
|
|
return $perdite / $numeroUnita;
|
|
|
|
case 'escludi':
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function calcolaImporto($consumo)
|
|
{
|
|
if ($this->consumo_totale_periodo > 0) {
|
|
return ($consumo / $this->consumo_totale_periodo) * $this->importo_totale_periodo;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public function confermaRipartizione()
|
|
{
|
|
if ($this->stato_ripartizione !== 'calcolata') {
|
|
throw new \Exception('La ripartizione deve essere calcolata prima di essere confermata');
|
|
}
|
|
|
|
$this->update([
|
|
'stato_ripartizione' => 'confermata',
|
|
'data_conferma' => now(),
|
|
'confermata_da' => auth()->id()
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function annullaRipartizione()
|
|
{
|
|
$this->update([
|
|
'stato_ripartizione' => 'bozza',
|
|
'data_calcolo' => null,
|
|
'calcolata_da' => null,
|
|
'data_conferma' => null,
|
|
'confermata_da' => null
|
|
]);
|
|
|
|
$this->dettagliRipartizione()->delete();
|
|
|
|
return $this;
|
|
}
|
|
|
|
// Accessors
|
|
public function getDifferenzaConsumoAttribute()
|
|
{
|
|
return $this->consumo_generale - $this->consumo_individuale_totale;
|
|
}
|
|
|
|
public function getPercentualePerditeAttribute()
|
|
{
|
|
if ($this->consumo_generale > 0) {
|
|
return (($this->consumo_generale - $this->consumo_individuale_totale) / $this->consumo_generale) * 100;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Metodi statici
|
|
public static function getTipiConsumo()
|
|
{
|
|
return [
|
|
'acqua_fredda' => 'Acqua Fredda',
|
|
'acqua_calda' => 'Acqua Calda',
|
|
'riscaldamento' => 'Riscaldamento',
|
|
'gas' => 'Gas',
|
|
'energia_elettrica' => 'Energia Elettrica'
|
|
];
|
|
}
|
|
|
|
public static function getModalitaRipartizione()
|
|
{
|
|
return [
|
|
'contatori_individuali' => 'Contatori Individuali',
|
|
'millesimi_standard' => 'Millesimi Standard',
|
|
'millesimi_personalizzati' => 'Millesimi Personalizzati',
|
|
'misto' => 'Modalità Mista'
|
|
];
|
|
}
|
|
|
|
public static function getStatiRipartizione()
|
|
{
|
|
return [
|
|
'bozza' => 'Bozza',
|
|
'calcolata' => 'Calcolata',
|
|
'verificata' => 'Verificata',
|
|
'confermata' => 'Confermata',
|
|
'fatturata' => 'Fatturata'
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 **SERVIZI SPECIALIZZATI**
|
|
|
|
### **1. Servizio Import Letture**
|
|
```php
|
|
// app/Services/ImportLettureService.php
|
|
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\LetturaContatore;
|
|
use App\Models\Contatore;
|
|
use Illuminate\Support\Facades\DB;
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
|
|
class ImportLettureService
|
|
{
|
|
public function importaDaExcel($filePath, $stabileId, $periodo)
|
|
{
|
|
$spreadsheet = IOFactory::load($filePath);
|
|
$worksheet = $spreadsheet->getActiveSheet();
|
|
$batchId = uniqid('import_');
|
|
|
|
DB::beginTransaction();
|
|
|
|
try {
|
|
$righe = $worksheet->toArray();
|
|
$header = array_shift($righe); // Rimuove intestazione
|
|
|
|
foreach ($righe as $indice => $riga) {
|
|
if (empty($riga[0])) continue; // Skip righe vuote
|
|
|
|
$this->processaRigaImport($riga, $stabileId, $periodo, $batchId, $indice + 2);
|
|
}
|
|
|
|
DB::commit();
|
|
return ['success' => true, 'batch_id' => $batchId];
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return ['success' => false, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
private function processaRigaImport($riga, $stabileId, $periodo, $batchId, $numeroRiga)
|
|
{
|
|
$codiceContatore = $riga[0];
|
|
$letturaAttuale = (float) $riga[1];
|
|
$dataLettura = $riga[2];
|
|
$note = $riga[3] ?? '';
|
|
|
|
$contatore = Contatore::where('stabile_id', $stabileId)
|
|
->where('codice_contatore', $codiceContatore)
|
|
->first();
|
|
|
|
if (!$contatore) {
|
|
throw new \Exception("Contatore {$codiceContatore} non trovato alla riga {$numeroRiga}");
|
|
}
|
|
|
|
// Verifica se lettura già esistente
|
|
$esistente = LetturaContatore::where('contatore_id', $contatore->id)
|
|
->where('periodo_riferimento', $periodo)
|
|
->first();
|
|
|
|
if ($esistente) {
|
|
// Aggiorna lettura esistente
|
|
$esistente->update([
|
|
'lettura_attuale' => $letturaAttuale,
|
|
'data_lettura' => $dataLettura,
|
|
'tipo_lettura' => 'import_ditta',
|
|
'note_lettura' => $note,
|
|
'import_batch_id' => $batchId,
|
|
'import_file_name' => basename($filePath),
|
|
'import_riga_numero' => $numeroRiga
|
|
]);
|
|
} else {
|
|
// Crea nuova lettura
|
|
LetturaContatore::create([
|
|
'contatore_id' => $contatore->id,
|
|
'stabile_id' => $stabileId,
|
|
'periodo_riferimento' => $periodo,
|
|
'data_lettura' => $dataLettura,
|
|
'lettura_attuale' => $letturaAttuale,
|
|
'tipo_lettura' => 'import_ditta',
|
|
'note_lettura' => $note,
|
|
'import_batch_id' => $batchId,
|
|
'import_file_name' => basename($filePath),
|
|
'import_riga_numero' => $numeroRiga,
|
|
'inserita_da' => auth()->id()
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function validaBatchImport($batchId)
|
|
{
|
|
$letture = LetturaContatore::where('import_batch_id', $batchId)->get();
|
|
|
|
foreach ($letture as $lettura) {
|
|
// Validazioni automatiche
|
|
if ($this->validaLettura($lettura)) {
|
|
$lettura->valida();
|
|
}
|
|
}
|
|
|
|
return $letture->count();
|
|
}
|
|
|
|
private function validaLettura($lettura)
|
|
{
|
|
// Controlla se consumo è ragionevole
|
|
$consumo = $lettura->consumo_calcolato;
|
|
$media = $lettura->contatore->getConsumoMedio();
|
|
|
|
if ($media > 0) {
|
|
$variazione = abs($consumo - $media) / $media;
|
|
if ($variazione > 0.5) { // Variazione > 50%
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Controlla se lettura è crescente
|
|
if ($lettura->lettura_attuale < $lettura->lettura_precedente) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
### **2. Servizio Calcolo Ripartizione**
|
|
```php
|
|
// app/Services/CalcoloRipartizioneService.php
|
|
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\RipartizioneConsumi;
|
|
use App\Models\DettaglioRipartizioneConsumi;
|
|
use App\Models\ParametriCalcoloConsumi;
|
|
|
|
class CalcoloRipartizioneService
|
|
{
|
|
public function creaRipartizione($stabileId, $periodo, $tipoConsumo, $parametri)
|
|
{
|
|
$ripartizione = RipartizioneConsumi::create([
|
|
'stabile_id' => $stabileId,
|
|
'periodo_riferimento' => $periodo,
|
|
'tipo_consumo' => $tipoConsumo,
|
|
'modalita_ripartizione' => $parametri['modalita_ripartizione'],
|
|
'tabella_millesimale_id' => $parametri['tabella_millesimale_id'] ?? null,
|
|
'consumo_totale_periodo' => $parametri['consumo_totale'],
|
|
'importo_totale_periodo' => $parametri['importo_totale'],
|
|
'consumo_generale' => $parametri['consumo_generale'] ?? 0,
|
|
'ripartizione_perdite' => $parametri['ripartizione_perdite'] ?? 'proporzionale',
|
|
'quota_fissa_percentuale' => $parametri['quota_fissa_percentuale'] ?? 0,
|
|
'quota_variabile_percentuale' => $parametri['quota_variabile_percentuale'] ?? 100,
|
|
'creata_da' => auth()->id()
|
|
]);
|
|
|
|
return $ripartizione;
|
|
}
|
|
|
|
public function calcolaAnteprimaRipartizione($stabileId, $periodo, $tipoConsumo, $parametri)
|
|
{
|
|
// Crea ripartizione temporanea senza salvare
|
|
$ripartizione = new RipartizioneConsumi([
|
|
'stabile_id' => $stabileId,
|
|
'periodo_riferimento' => $periodo,
|
|
'tipo_consumo' => $tipoConsumo,
|
|
'modalita_ripartizione' => $parametri['modalita_ripartizione'],
|
|
'tabella_millesimale_id' => $parametri['tabella_millesimale_id'] ?? null,
|
|
'consumo_totale_periodo' => $parametri['consumo_totale'],
|
|
'importo_totale_periodo' => $parametri['importo_totale'],
|
|
'consumo_generale' => $parametri['consumo_generale'] ?? 0,
|
|
'ripartizione_perdite' => $parametri['ripartizione_perdite'] ?? 'proporzionale',
|
|
'quota_fissa_percentuale' => $parametri['quota_fissa_percentuale'] ?? 0,
|
|
'quota_variabile_percentuale' => $parametri['quota_variabile_percentuale'] ?? 100
|
|
]);
|
|
|
|
// Simula calcolo senza salvare
|
|
return $this->simulaCalcoloRipartizione($ripartizione);
|
|
}
|
|
|
|
private function simulaCalcoloRipartizione($ripartizione)
|
|
{
|
|
// Implementa logica di simulazione simile a calcolaRipartizione
|
|
// ma senza salvare i dati, solo per anteprima
|
|
|
|
$anteprima = [];
|
|
// ... logica di calcolo ...
|
|
|
|
return $anteprima;
|
|
}
|
|
|
|
public function verificaCoerenzaRipartizione($ripartizioneId)
|
|
{
|
|
$ripartizione = RipartizioneConsumi::find($ripartizioneId);
|
|
$dettagli = $ripartizione->dettagliRipartizione;
|
|
|
|
$verifiche = [
|
|
'totale_consumo_coerente' => $this->verificaTotaleConsumo($ripartizione, $dettagli),
|
|
'totale_importo_coerente' => $this->verificaTotaleImporto($ripartizione, $dettagli),
|
|
'millesimi_bilanciati' => $this->verificaMillesimi($ripartizione, $dettagli),
|
|
'perdite_ragionevoli' => $this->verificaPerdite($ripartizione)
|
|
];
|
|
|
|
return $verifiche;
|
|
}
|
|
|
|
private function verificaTotaleConsumo($ripartizione, $dettagli)
|
|
{
|
|
$totaleCalcolato = $dettagli->sum('consumo_totale');
|
|
$differenza = abs($totaleCalcolato - $ripartizione->consumo_totale_periodo);
|
|
|
|
return $differenza < 0.01; // Tolleranza 0.01 m³
|
|
}
|
|
|
|
private function verificaTotaleImporto($ripartizione, $dettagli)
|
|
{
|
|
$totaleCalcolato = $dettagli->sum('importo_totale');
|
|
$differenza = abs($totaleCalcolato - $ripartizione->importo_totale_periodo);
|
|
|
|
return $differenza < 0.01; // Tolleranza 1 centesimo
|
|
}
|
|
|
|
private function verificaMillesimi($ripartizione, $dettagli)
|
|
{
|
|
if (!$ripartizione->tabellaMillesimale) {
|
|
return true;
|
|
}
|
|
|
|
$totaleMillesimi = $dettagli->sum('millesimi_quota_fissa') +
|
|
$dettagli->sum('millesimi_quota_variabile') +
|
|
$dettagli->sum('millesimi_perdite');
|
|
|
|
return abs($totaleMillesimi - 1000) < 0.01;
|
|
}
|
|
|
|
private function verificaPerdite($ripartizione)
|
|
{
|
|
$percentualePerdite = $ripartizione->percentuale_perdite;
|
|
|
|
// Considera ragionevoli perdite fino al 15%
|
|
return $percentualePerdite <= 15;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 **DASHBOARD E REPORTING**
|
|
|
|
### **1. Dashboard Consumi**
|
|
```php
|
|
// app/Services/DashboardConsumiService.php
|
|
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Contatore;
|
|
use App\Models\LetturaContatore;
|
|
use App\Models\RipartizioneConsumi;
|
|
use Carbon\Carbon;
|
|
|
|
class DashboardConsumiService
|
|
{
|
|
public function getDashboardData($stabileId, $periodo = null)
|
|
{
|
|
$periodo = $periodo ?? Carbon::now()->format('Y-m');
|
|
|
|
return [
|
|
'riepilogo_consumi' => $this->getRiepilogoConsumi($stabileId, $periodo),
|
|
'grafici_andamento' => $this->getGraficiAndamento($stabileId),
|
|
'alert_anomalie' => $this->getAlertAnomalie($stabileId, $periodo),
|
|
'statistiche_perdite' => $this->getStatistichePerdite($stabileId),
|
|
'scadenze_letture' => $this->getScadenzeLettureAmministratore($stabileId)
|
|
];
|
|
}
|
|
|
|
private function getRiepilogoConsumi($stabileId, $periodo)
|
|
{
|
|
$tipiConsumo = ['acqua_fredda', 'acqua_calda', 'riscaldamento', 'gas', 'energia_elettrica'];
|
|
$riepilogo = [];
|
|
|
|
foreach ($tipiConsumo as $tipo) {
|
|
$ripartizione = RipartizioneConsumi::where('stabile_id', $stabileId)
|
|
->where('periodo_riferimento', $periodo)
|
|
->where('tipo_consumo', $tipo)
|
|
->first();
|
|
|
|
if ($ripartizione) {
|
|
$riepilogo[$tipo] = [
|
|
'consumo_totale' => $ripartizione->consumo_totale_periodo,
|
|
'importo_totale' => $ripartizione->importo_totale_periodo,
|
|
'perdite_percentuale' => $ripartizione->percentuale_perdite,
|
|
'stato' => $ripartizione->stato_ripartizione
|
|
];
|
|
}
|
|
}
|
|
|
|
return $riepilogo;
|
|
}
|
|
|
|
private function getGraficiAndamento($stabileId)
|
|
{
|
|
$ultimi12Mesi = [];
|
|
|
|
for ($i = 11; $i >= 0; $i--) {
|
|
$data = Carbon::now()->subMonths($i);
|
|
$periodo = $data->format('Y-m');
|
|
|
|
$ripartizioni = RipartizioneConsumi::where('stabile_id', $stabileId)
|
|
->where('periodo_riferimento', $periodo)
|
|
->get();
|
|
|
|
$ultimi12Mesi[] = [
|
|
'periodo' => $periodo,
|
|
'mese_nome' => $data->format('M Y'),
|
|
'consumi' => $ripartizioni->mapWithKeys(function($r) {
|
|
return [$r->tipo_consumo => $r->consumo_totale_periodo];
|
|
})->toArray()
|
|
];
|
|
}
|
|
|
|
return $ultimi12Mesi;
|
|
}
|
|
|
|
private function getAlertAnomalie($stabileId, $periodo)
|
|
{
|
|
$alert = [];
|
|
|
|
// Cerca letture con variazioni anomale
|
|
$lettureAnomale = LetturaContatore::whereHas('contatore', function($q) use ($stabileId) {
|
|
$q->where('stabile_id', $stabileId);
|
|
})
|
|
->where('periodo_riferimento', $periodo)
|
|
->get()
|
|
->filter(function($lettura) {
|
|
$variazione = $lettura->calcolaVariazionePercentuale();
|
|
return $variazione !== null && abs($variazione) > 50;
|
|
});
|
|
|
|
foreach ($lettureAnomale as $lettura) {
|
|
$alert[] = [
|
|
'tipo' => 'variazione_anomala',
|
|
'messaggio' => "Variazione anomala contatore {$lettura->contatore->codice_contatore}",
|
|
'dettagli' => "Variazione: " . round($lettura->calcolaVariazionePercentuale(), 1) . "%"
|
|
];
|
|
}
|
|
|
|
// Cerca perdite eccessive
|
|
$ripartizioniPerdite = RipartizioneConsumi::where('stabile_id', $stabileId)
|
|
->where('periodo_riferimento', $periodo)
|
|
->get()
|
|
->filter(function($r) {
|
|
return $r->percentuale_perdite > 15;
|
|
});
|
|
|
|
foreach ($ripartizioniPerdite as $ripartizione) {
|
|
$alert[] = [
|
|
'tipo' => 'perdite_eccessive',
|
|
'messaggio' => "Perdite eccessive per {$ripartizione->getTipoConsumo()}",
|
|
'dettagli' => "Perdite: " . round($ripartizione->percentuale_perdite, 1) . "%"
|
|
];
|
|
}
|
|
|
|
return $alert;
|
|
}
|
|
|
|
private function getStatistichePerdite($stabileId)
|
|
{
|
|
$ultimi6Mesi = Carbon::now()->subMonths(6)->format('Y-m');
|
|
|
|
return RipartizioneConsumi::where('stabile_id', $stabileId)
|
|
->where('periodo_riferimento', '>=', $ultimi6Mesi)
|
|
->selectRaw('tipo_consumo, AVG(CASE WHEN consumo_generale > 0 THEN ((consumo_generale - consumo_individuale_totale) / consumo_generale) * 100 ELSE 0 END) as media_perdite')
|
|
->groupBy('tipo_consumo')
|
|
->get()
|
|
->mapWithKeys(function($item) {
|
|
return [$item->tipo_consumo => round($item->media_perdite, 2)];
|
|
});
|
|
}
|
|
|
|
private function getScadenzeLettureAmministratore($stabileId)
|
|
{
|
|
$prossimoMese = Carbon::now()->addMonth()->format('Y-m');
|
|
|
|
return Contatore::where('stabile_id', $stabileId)
|
|
->where('attivo', true)
|
|
->whereNotExists(function($query) use ($prossimoMese) {
|
|
$query->select('id')
|
|
->from('letture_contatori')
|
|
->whereColumn('contatore_id', 'contatori.id')
|
|
->where('periodo_riferimento', $prossimoMese);
|
|
})
|
|
->count();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎯 **CHECKLIST IMPLEMENTAZIONE**
|
|
|
|
### **Database**
|
|
- [ ] Migration tabelle contatori
|
|
- [ ] Migration letture contatori
|
|
- [ ] Migration ripartizione consumi
|
|
- [ ] Migration dettaglio ripartizione
|
|
- [ ] Migration parametri calcolo
|
|
- [ ] Seed dati di test
|
|
|
|
### **Modelli Eloquent**
|
|
- [ ] Modello Contatore con relazioni
|
|
- [ ] Modello LetturaContatore con metodi calcolo
|
|
- [ ] Modello RipartizioneConsumi con logica business
|
|
- [ ] Modello DettaglioRipartizioneConsumi
|
|
- [ ] Modello ParametriCalcoloConsumi
|
|
|
|
### **Servizi**
|
|
- [ ] ImportLettureService per Excel/CSV
|
|
- [ ] CalcoloRipartizioneService per algoritmi
|
|
- [ ] DashboardConsumiService per report
|
|
- [ ] NotificazioneService per alert
|
|
|
|
### **Controller e API**
|
|
- [ ] Controller per gestione contatori
|
|
- [ ] Controller per inserimento letture
|
|
- [ ] Controller per calcolo ripartizione
|
|
- [ ] API per dashboard e grafici
|
|
|
|
### **Frontend**
|
|
- [ ] Interfaccia gestione contatori
|
|
- [ ] Wizard inserimento letture
|
|
- [ ] Wizard calcolo ripartizione
|
|
- [ ] Dashboard consumi con grafici
|
|
|
|
---
|
|
|
|
**Aggiornato**: 2025-01-27
|
|
**Versione**: 1.0
|
|
**Prossimo step**: Implementazione migration e modelli base per consumi
|