Roberto Santini 1 hafta önce
ebeveyn
işleme
3243f52a11

+ 2
- 0
GestioneManutenzioni.skipper Dosyayı Görüntüle

@@ -165,6 +165,8 @@
165 165
         </orm-attributes>
166 166
       </field>
167 167
       <field name="costo_descrizione" type="text" uuid="2d89a9fb-9229-412e-b6df-9b35a19f14a5"/>
168
+      <field name="non_fatturare" type="boolean" default="0" uuid="d0ffea66-3d82-495a-bbc8-f7cc443bae31"/>
169
+      <field name="costo_uscita" type="decimal" default="0" uuid="c7582fe6-bc2e-4eee-ad56-9593b156d48c"/>
168 170
       <field name="riservata" type="boolean" default="1" uuid="c48733ff-ec02-4aa0-be7c-25be727c3c1e"/>
169 171
       <field name="riferimento" type="string" uuid="dca8d620-9b45-4a1f-96e4-0d76392ac410"/>
170 172
       <field name="email" type="string" uuid="e2ca0f54-426d-4977-b729-fd007dee6f64"/>

+ 23
- 1
app/DataTables/TempoImpiegatoDataTable.php Dosyayı Görüntüle

@@ -44,6 +44,18 @@ class TempoImpiegatoDataTable extends DataTable
44 44
     ->addColumn('tempo_minuti_label', function($entity){
45 45
       return Config::formatTempo($entity->tempo_minuti);
46 46
     })
47
+    ->addColumn('non_fatturare_label', function($entity){
48
+      return $entity->non_fatturare
49
+        ? '<span class="badge bg-label-warning">Non fatturare</span>'
50
+        : '—';
51
+    })
52
+    ->addColumn('costo_uscita_label', function($entity){
53
+      return $entity->costo_uscita
54
+        ? '<span class="badge bg-label-info">'.e(Config::formatCosto(Config::costoUscitaDefault())).'</span>'
55
+        : '—';
56
+    })
57
+    ->editColumn('non_fatturare', fn ($entity) => $entity->non_fatturare ? '1' : '0')
58
+    ->editColumn('costo_uscita', fn ($entity) => $entity->costo_uscita ? '1' : '0')
47 59
     ->filterColumn('tempo_data', function ($query, $keyword) {
48 60
       CostoSegnalazioneDataTable::applyDateTimeColumnFilter($query, $keyword, 'annotazione.tempo_data');
49 61
     })
@@ -89,7 +101,7 @@ class TempoImpiegatoDataTable extends DataTable
89 101
       $query->where('annotazione.text', 'like', '%' . $keyword . '%');
90 102
     })
91 103
     ->setRowId('id')
92
-    ->rawColumns(['segnalazione_label', 'text_excerpt']);
104
+    ->rawColumns(['segnalazione_label', 'text_excerpt', 'non_fatturare_label', 'costo_uscita_label']);
93 105
   }
94 106
 
95 107
   /**
@@ -155,6 +167,14 @@ class TempoImpiegatoDataTable extends DataTable
155 167
                            }']),
156 168
           Fields\Text::make('tempo_minuti_label', 'Tempo impiegato (1,50 o 1h30m)'),
157 169
           Fields\DateTime::make('tempo_data', 'Data/ora')->format('DD/MM/YYYY HH:mm')->default(Carbon::now()->format('d/m/Y H:i')),
170
+          Fields\Select::make('non_fatturare', 'Fatturazione')->options([
171
+            '0' => 'Fatturare',
172
+            '1' => 'Non fatturare',
173
+          ])->default('0'),
174
+          Fields\Select::make('costo_uscita', 'Costo d\'uscita ('.Config::formatCosto(Config::costoUscitaDefault()).')')->options([
175
+            '0' => 'No',
176
+            '1' => 'Sì',
177
+          ])->default('0'),
158 178
           Fields\TextArea::make('text', 'Note')->type('quill'),
159 179
           Fields\Select2::make('segnalazione_id', 'Segnalazione')
160 180
           ->default($this->segnalazione ? $this->segnalazione->id : 0)
@@ -202,6 +222,8 @@ class TempoImpiegatoDataTable extends DataTable
202 222
           Column::make('user_id')->title('Utente')->data('user_label')->responsivePriority(3),
203 223
           Column::make('segnalazione_id')->title('Segnalazione')->data('segnalazione_label')->responsivePriority(3)->visible($this->segnalazione == null),
204 224
           Column::make('tempo_minuti')->title('Tempo impiegato')->responsivePriority(3)->data('tempo_minuti_label'),
225
+          Column::make('non_fatturare')->title('Fatturazione')->data('non_fatturare_label')->responsivePriority(4),
226
+          Column::make('costo_uscita')->title('Uscita')->data('costo_uscita_label')->responsivePriority(4),
205 227
           Column::make('text')->title('Note')->responsivePriority(3)->data('text_excerpt'),
206 228
           Column::computed('action')
207 229
           ->title(' ')

+ 6
- 0
app/DataTables/TempoImpiegatoDataTableEditor.php Dosyayı Görüntüle

@@ -87,12 +87,18 @@ class TempoImpiegatoDataTableEditor extends DataTablesEditor
87 87
   public function creating(Model $model, array $data): array
88 88
   {
89 89
     $data['tempo_minuti'] = Config::parseTempoToMinuti($data['tempo_minuti_label']);
90
+    $data['non_fatturare'] = (int) ($data['non_fatturare'] ?? 0) === 1;
91
+    $data['costo_uscita'] = (int) ($data['costo_uscita'] ?? 0) === 1;
92
+
90 93
     return $data;
91 94
   }
92 95
 
93 96
   public function updating(Model $model, array $data): array
94 97
   {
95 98
     $data['tempo_minuti'] = Config::parseTempoToMinuti($data['tempo_minuti_label']);
99
+    $data['non_fatturare'] = (int) ($data['non_fatturare'] ?? 0) === 1;
100
+    $data['costo_uscita'] = (int) ($data['costo_uscita'] ?? 0) === 1;
101
+
96 102
     return $data;
97 103
   }
98 104
 

+ 3
- 0
app/Http/Controllers/SegnalazioneController.php Dosyayı Görüntüle

@@ -268,6 +268,9 @@ class SegnalazioneController extends Controller implements HasMiddleware
268 268
       $annotazione->costo_descrizione = $request->costo_descrizione;
269 269
     }
270 270
 
271
+    $annotazione->non_fatturare = $request->boolean('non_fatturare');
272
+    $annotazione->costo_uscita = $request->boolean('costo_uscita');
273
+
271 274
     $annotazione->save();
272 275
 
273 276
     if ($request->hasFile('allegato')) {

+ 6
- 0
app/Models/AbstractModels/AbstractAnnotazione.php Dosyayı Görüntüle

@@ -31,6 +31,8 @@ abstract class AbstractAnnotazione extends Model
31 31
      */
32 32
     protected $attributes = [
33 33
         'tempo_minuti' => 0,
34
+        'non_fatturare' => 0,
35
+        'costo_uscita' => 0,
34 36
         'riservata' => 1
35 37
     ];
36 38
     
@@ -48,6 +50,8 @@ abstract class AbstractAnnotazione extends Model
48 50
         'tempo_data' => 'datetime:d/m/Y H:i',
49 51
         'costo' => 'decimal:2',
50 52
         'costo_descrizione' => 'string',
53
+        'non_fatturare' => 'boolean',
54
+        'costo_uscita' => 'decimal:2',
51 55
         'riservata' => 'boolean',
52 56
         'riferimento' => 'string',
53 57
         'email' => 'string',
@@ -70,6 +74,8 @@ abstract class AbstractAnnotazione extends Model
70 74
         'tempo_data',
71 75
         'costo',
72 76
         'costo_descrizione',
77
+        'non_fatturare',
78
+        'costo_uscita',
73 79
         'riservata',
74 80
         'riferimento',
75 81
         'email',

+ 26
- 0
app/Models/Config.php Dosyayı Görüntüle

@@ -27,6 +27,8 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
27 27
 
28 28
   const FATTURAZIONE_TARIFFA_ORARIA = 'FATTURAZIONE_TARIFFA_ORARIA';
29 29
   const FATTURAZIONE_IVA_PERCENTUALE = 'FATTURAZIONE_IVA_PERCENTUALE';
30
+  const FATTURAZIONE_COSTO_USCITA = 'FATTURAZIONE_COSTO_USCITA';
31
+  const FATTURAZIONE_RINNOVI_DA = 'FATTURAZIONE_RINNOVI_DA';
30 32
 
31 33
   public static function getConfig(){
32 34
     $config = [];
@@ -66,6 +68,8 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
66 68
     $config['Fatturazione'] = [
67 69
       ['key' => self::FATTURAZIONE_TARIFFA_ORARIA, 'value' => '40', 'display_name' => 'Tariffa oraria default (€/ora)', 'type' => 'number'],
68 70
       ['key' => self::FATTURAZIONE_IVA_PERCENTUALE, 'value' => '22', 'display_name' => 'Aliquota IVA default (%)', 'type' => 'number'],
71
+      ['key' => self::FATTURAZIONE_COSTO_USCITA, 'value' => '30', 'display_name' => 'Costo d\'uscita (€ / intervento)', 'type' => 'number'],
72
+      ['key' => self::FATTURAZIONE_RINNOVI_DA, 'value' => '2026-09-01', 'display_name' => 'Fattura i rinnovi a partire da', 'type' => 'date'],
69 73
     ];
70 74
 
71 75
     return $config;
@@ -228,6 +232,28 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
228 232
     return max(0, (float) $value);
229 233
   }
230 234
 
235
+  public static function costoUscitaDefault(): float
236
+  {
237
+    $value = self::valueOf(self::FATTURAZIONE_COSTO_USCITA, '30');
238
+    if ($value === null || ! is_numeric($value)) {
239
+      return 30.0;
240
+    }
241
+
242
+    return max(0, (float) $value);
243
+  }
244
+
245
+  public static function rinnoviDa(): \Carbon\Carbon
246
+  {
247
+    $value = trim((string) self::valueOf(self::FATTURAZIONE_RINNOVI_DA, '2026-09-01'));
248
+    try {
249
+      $date = \Carbon\Carbon::parse($value ?: '2026-09-01')->startOfDay();
250
+    } catch (\Throwable $e) {
251
+      $date = \Carbon\Carbon::parse('2026-09-01')->startOfDay();
252
+    }
253
+
254
+    return $date;
255
+  }
256
+
231 257
   public static function formatCosto($value, $decimals = 2){
232 258
     return number_format($value, $decimals, ",",".",)." €";
233 259
   }

+ 2
- 0
app/Models/FatturaRiga.php Dosyayı Görüntüle

@@ -7,6 +7,7 @@ class FatturaRiga extends \App\Models\AbstractModels\AbstractFatturaRiga
7 7
     public const TIPO_EXTRA_CONTRATTO = 'extra_contratto';
8 8
     public const TIPO_EXTRA_ORDINE = 'extra_ordine';
9 9
     public const TIPO_SPESA = 'spesa';
10
+    public const TIPO_USCITA = 'uscita';
10 11
     public const TIPO_CANONE = 'canone';
11 12
     public const TIPO_MANUALE = 'manuale';
12 13
 
@@ -15,6 +16,7 @@ class FatturaRiga extends \App\Models\AbstractModels\AbstractFatturaRiga
15 16
         self::TIPO_EXTRA_CONTRATTO => 'Ore extra contratto',
16 17
         self::TIPO_EXTRA_ORDINE => 'Ore extra ordine',
17 18
         self::TIPO_SPESA => 'Spesa',
19
+        self::TIPO_USCITA => 'Costo d\'uscita',
18 20
         self::TIPO_CANONE => 'Canone / rinnovo',
19 21
         self::TIPO_MANUALE => 'Manuale',
20 22
     ];

+ 94
- 0
app/Models/FatturazioneEsclusione.php Dosyayı Görüntüle

@@ -0,0 +1,94 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Carbon\Carbon;
6
+use Illuminate\Database\Eloquent\Model;
7
+
8
+class FatturazioneEsclusione extends Model
9
+{
10
+    protected $table = 'fatturazione_esclusione';
11
+
12
+    public const TIPO_SEGNALAZIONE = 'segnalazione';
13
+    public const TIPO_CANONE = 'canone';
14
+
15
+    public const MODO_TEMPORANEO = 'temporaneo';
16
+    public const MODO_DEFINITIVO = 'definitivo';
17
+
18
+    protected $fillable = [
19
+        'chiave',
20
+        'tipo',
21
+        'modo',
22
+        'segnalazione_id',
23
+        'contratto_servizio_id',
24
+        'contratto_servizio_riga_id',
25
+        'ciclo_da',
26
+        'ciclo_a',
27
+        'skip_da',
28
+        'skip_a',
29
+        'note',
30
+        'created_by_id',
31
+    ];
32
+
33
+    protected $casts = [
34
+        'ciclo_da' => 'date',
35
+        'ciclo_a' => 'date',
36
+        'skip_da' => 'date',
37
+        'skip_a' => 'date',
38
+    ];
39
+
40
+    public static function chiaveSegnalazione(int $segnalazioneId): string
41
+    {
42
+        return 'segnalazione:'.$segnalazioneId;
43
+    }
44
+
45
+    public static function chiaveCanone(int $contrattoId, ?int $rigaId, $cicloDa, $cicloA): string
46
+    {
47
+        $da = Carbon::parse($cicloDa)->toDateString();
48
+        $a = Carbon::parse($cicloA)->toDateString();
49
+
50
+        return 'canone:'.$contrattoId.':'.((int) $rigaId).':'.$da.':'.$a;
51
+    }
52
+
53
+    public function isDefinitivo(): bool
54
+    {
55
+        return $this->modo === self::MODO_DEFINITIVO;
56
+    }
57
+
58
+    public function isTemporaneo(): bool
59
+    {
60
+        return $this->modo === self::MODO_TEMPORANEO;
61
+    }
62
+
63
+    public function getModoLabelAttribute(): string
64
+    {
65
+        return $this->isDefinitivo() ? 'Non fatturare' : 'Prossimo mese';
66
+    }
67
+
68
+    /**
69
+     * Ancora da saltare in questa generazione (temporaneo non scaduto, o definitivo).
70
+     */
71
+    public function isAttivaAl(Carbon $from): bool
72
+    {
73
+        if ($this->isDefinitivo()) {
74
+            return true;
75
+        }
76
+
77
+        return $this->skip_a && $from->copy()->startOfDay()->lte($this->skip_a->copy()->startOfDay());
78
+    }
79
+
80
+    public function createdBy()
81
+    {
82
+        return $this->belongsTo(User::class, 'created_by_id');
83
+    }
84
+
85
+    public function segnalazione()
86
+    {
87
+        return $this->belongsTo(Segnalazione::class, 'segnalazione_id');
88
+    }
89
+
90
+    public function contrattoServizio()
91
+    {
92
+        return $this->belongsTo(ContrattoServizio::class, 'contratto_servizio_id');
93
+    }
94
+}

+ 70
- 17
app/Services/FatturazioneGeneraService.php Dosyayı Görüntüle

@@ -170,9 +170,13 @@ class FatturazioneGeneraService
170 170
                 'segnalazione.ordineLavoro.righe',
171 171
                 'segnalazione.acquistoServizio',
172 172
             ])
173
+            ->where(function ($q) {
174
+                $q->whereNull('non_fatturare')->orWhere('non_fatturare', false);
175
+            })
173 176
             ->where(function ($q) {
174 177
                 $q->where('tempo_minuti', '>', 0)
175
-                    ->orWhere('costo', '>', 0);
178
+                    ->orWhere('costo', '>', 0)
179
+                    ->orWhere('costo_uscita', true);
176 180
             })
177 181
             ->where(function ($q) use ($from, $to) {
178 182
                 $q->whereBetween('tempo_data', [$from, $to])
@@ -379,7 +383,7 @@ class FatturazioneGeneraService
379 383
         $data = $this->dataAnnotazione($annotazione);
380 384
 
381 385
         if ($segnalazione->acquisto_servizio_id) {
382
-            return [
386
+            return $this->conCostoUscita([
383 387
                 'copertura' => 'acquisto',
384 388
                 'minuti_coperti' => $minuti,
385 389
                 'minuti_fatturare' => 0,
@@ -388,7 +392,7 @@ class FatturazioneGeneraService
388 392
                 'importo_spesa' => 0.0,
389 393
                 'tariffa_mancante' => false,
390 394
                 'note' => 'Già pagato (acquisto servizio)',
391
-            ];
395
+            ], $annotazione, $segnalazione);
392 396
         }
393 397
 
394 398
         $fatturaSpesa = $costo > 0 ? round($costo, 2) : 0.0;
@@ -415,7 +419,7 @@ class FatturazioneGeneraService
415 419
                 ? 'contratto_misto'
416 420
                 : ($extra > 0 ? 'contratto_extra' : 'contratto');
417 421
 
418
-            return [
422
+            return $this->conCostoUscita([
419 423
                 'copertura' => $copertura,
420 424
                 'minuti_coperti' => $coperti,
421 425
                 'minuti_fatturare' => $extra,
@@ -424,7 +428,7 @@ class FatturazioneGeneraService
424 428
                 'importo_spesa' => $fatturaSpesa,
425 429
                 'tariffa_mancante' => $mancante,
426 430
                 'note' => $this->noteContratto($contratto, $coperti, $extra, $pools[$poolKey]['ore_totali']),
427
-            ];
431
+            ], $annotazione, $segnalazione);
428 432
         }
429 433
 
430 434
         if ($segnalazione->ordine_lavoro_id && $segnalazione->ordineLavoro) {
@@ -451,7 +455,7 @@ class FatturazioneGeneraService
451 455
                 ? 'ordine_misto'
452 456
                 : ($extra > 0 ? 'ordine_extra' : 'ordine');
453 457
 
454
-            return [
458
+            return $this->conCostoUscita([
455 459
                 'copertura' => $copertura,
456 460
                 'minuti_coperti' => $coperti,
457 461
                 'minuti_fatturare' => $extra,
@@ -462,12 +466,12 @@ class FatturazioneGeneraService
462 466
                 'note' => $forfait
463 467
                     ? 'Incluso in ordine (a corpo)'
464 468
                     : $this->noteOrdine($ordine, $coperti, $extra),
465
-            ];
469
+            ], $annotazione, $segnalazione);
466 470
         }
467 471
 
468 472
         $mancante = $minuti > 0 && $tariffaDefault <= 0;
469 473
 
470
-        return [
474
+        return $this->conCostoUscita([
471 475
             'copertura' => 'libero',
472 476
             'minuti_coperti' => 0,
473 477
             'minuti_fatturare' => $minuti,
@@ -476,12 +480,23 @@ class FatturazioneGeneraService
476 480
             'importo_spesa' => $fatturaSpesa,
477 481
             'tariffa_mancante' => $mancante,
478 482
             'note' => 'Senza contratto né ordine',
479
-        ];
483
+        ], $annotazione, $segnalazione);
484
+    }
485
+
486
+    protected function conCostoUscita(array $allocazione, Annotazione $annotazione, $segnalazione): array
487
+    {
488
+        $allocazione['importo_uscita'] = 0.0;
489
+        if (! $segnalazione->acquisto_servizio_id && $annotazione->costo_uscita) {
490
+            $allocazione['importo_uscita'] = round(Config::costoUscitaDefault(), 2);
491
+        }
492
+
493
+        return $allocazione;
480 494
     }
481 495
 
482 496
     protected function accumulaTempo(array &$cliente, $segnalazione, Annotazione $annotazione, array $allocazione): void
483 497
     {
484 498
         $sid = (int) $segnalazione->id;
499
+        $importoUscita = (float) ($allocazione['importo_uscita'] ?? 0);
485 500
         if (! isset($cliente['segnalazioni'][$sid])) {
486 501
             $cliente['segnalazioni'][$sid] = [
487 502
                 'id' => $sid,
@@ -498,6 +513,7 @@ class FatturazioneGeneraService
498 513
                 'minuti_fatturare' => 0,
499 514
                 'importo_tempo' => 0.0,
500 515
                 'importo_spesa' => 0.0,
516
+                'importo_uscita' => 0.0,
501 517
                 'importo' => 0.0,
502 518
                 'tariffa' => $allocazione['tariffa'],
503 519
                 'tariffa_mancante' => false,
@@ -511,7 +527,8 @@ class FatturazioneGeneraService
511 527
         $row['minuti_fatturare'] += $allocazione['minuti_fatturare'];
512 528
         $row['importo_tempo'] = round($row['importo_tempo'] + $allocazione['importo_tempo'], 2);
513 529
         $row['importo_spesa'] = round($row['importo_spesa'] + $allocazione['importo_spesa'], 2);
514
-        $row['importo'] = round($row['importo_tempo'] + $row['importo_spesa'], 2);
530
+        $row['importo_uscita'] = round($row['importo_uscita'] + $importoUscita, 2);
531
+        $row['importo'] = round($row['importo_tempo'] + $row['importo_spesa'] + $row['importo_uscita'], 2);
515 532
         $row['tariffa_mancante'] = $row['tariffa_mancante'] || $allocazione['tariffa_mancante'];
516 533
         if ($allocazione['tariffa'] !== null) {
517 534
             $row['tariffa'] = $allocazione['tariffa'];
@@ -560,18 +577,39 @@ class FatturazioneGeneraService
560 577
                 'nota' => null,
561 578
             ];
562 579
         }
580
+        if ($importoUscita > 0) {
581
+            $cliente['voci'][] = [
582
+                'tipo' => FatturaRiga::TIPO_USCITA,
583
+                'descrizione' => 'Costo d\'uscita '.$segnalazione->getNumeroLabel().' — '.$segnalazione->titolo,
584
+                'quantita' => 1,
585
+                'unita_misura' => 'forfait',
586
+                'prezzo_unitario' => $importoUscita,
587
+                'importo' => $importoUscita,
588
+                'minuti' => null,
589
+                'annotazione_id' => $annotazione->id,
590
+                'segnalazione_id' => $segnalazione->id,
591
+                'contratto_servizio_id' => $segnalazione->contratto_servizio_id,
592
+                'contratto_servizio_riga_id' => null,
593
+                'ordine_lavoro_id' => $segnalazione->ordine_lavoro_id,
594
+                'prodotto_id' => null,
595
+                'periodo_da' => null,
596
+                'periodo_a' => null,
597
+                'nota' => 'Costo d\'uscita',
598
+            ];
599
+        }
563 600
 
564 601
         $cliente['minuti_totali'] += (int) $annotazione->tempo_minuti;
565 602
         $cliente['minuti_coperti'] += $allocazione['minuti_coperti'];
566 603
         $cliente['minuti_fatturare'] += $allocazione['minuti_fatturare'];
567 604
         $cliente['importo_tempi'] = round($cliente['importo_tempi'] + $allocazione['importo_tempo'], 2);
568
-        $cliente['importo_spese'] = round($cliente['importo_spese'] + $allocazione['importo_spesa'], 2);
605
+        $cliente['importo_spese'] = round($cliente['importo_spese'] + $allocazione['importo_spesa'] + $importoUscita, 2);
569 606
         $cliente['totale'] = round($cliente['importo_tempi'] + $cliente['importo_spese'] + $cliente['importo_rinnovi'], 2);
570 607
         $cliente['ha_tariffa_mancante'] = $cliente['ha_tariffa_mancante'] || $allocazione['tariffa_mancante'];
571 608
     }
572 609
 
573 610
     protected function accumulaRinnovi(array &$clienti, Carbon $from, Carbon $to, ?int $privatiAziendaId): void
574 611
     {
612
+        $rinnoviDa = Config::rinnoviDa();
575 613
         $contratti = ContrattoServizio::query()
576 614
             ->with(['user.azienda', 'righe.prodotto'])
577 615
             ->where('attivo', true)
@@ -595,6 +633,9 @@ class FatturazioneGeneraService
595 633
 
596 634
             foreach ($contratto->righe as $riga) {
597 635
                 foreach ($this->dateOccorrenzeRigaNelPeriodo($contratto, $riga, $from, $to) as $occurrence) {
636
+                    if ($occurrence->copy()->startOfDay()->lt($rinnoviDa)) {
637
+                        continue;
638
+                    }
598 639
                     $periodoKey = (string) ($riga->periodo ?: $contratto->periodo ?: 'una_tantum');
599 640
                     [$pDa, $pA] = $this->finestraCiclo($periodoKey, $occurrence);
600 641
                     if ($this->canoneGiaFatturato($contratto->id, $riga->id, $pDa, $pA)) {
@@ -618,6 +659,9 @@ class FatturazioneGeneraService
618 659
                 ]);
619 660
                 $fake->setRelation('contrattoServizio', $contratto);
620 661
                 foreach ($this->dateOccorrenzeRigaNelPeriodo($contratto, $fake, $from, $to) as $occurrence) {
662
+                    if ($occurrence->copy()->startOfDay()->lt($rinnoviDa)) {
663
+                        continue;
664
+                    }
621 665
                     [$pDa, $pA] = $this->finestraCiclo($periodo, $occurrence);
622 666
                     if ($this->canoneGiaFatturato($contratto->id, null, $pDa, $pA)) {
623 667
                         continue;
@@ -669,9 +713,6 @@ class FatturazioneGeneraService
669 713
         }
670 714
     }
671 715
 
672
-    /**
673
-     * @return Carbon[]
674
-     */
675 716
     protected function dateOccorrenzeRigaNelPeriodo(ContrattoServizio $contratto, ContrattoServizioRiga $riga, Carbon $from, Carbon $to): array
676 717
     {
677 718
         if ($riga->isAConsumo()) {
@@ -773,7 +814,12 @@ class FatturazioneGeneraService
773 814
     {
774 815
         $query = Annotazione::query()
775 816
             ->where('tempo_minuti', '>', 0)
776
-            ->whereHas('segnalazione', fn ($q) => $q->where('contratto_servizio_id', $contrattoId));
817
+            ->where(function ($q) {
818
+                $q->whereNull('non_fatturare')->orWhere('non_fatturare', false);
819
+            })
820
+            ->whereHas('segnalazione', function ($q) use ($contrattoId) {
821
+                $q->where('contratto_servizio_id', $contrattoId);
822
+            });
777 823
 
778 824
         $this->applicaLimiteTemporale($query, $windowFrom, $billingFrom->copy()->subSecond());
779 825
 
@@ -784,7 +830,12 @@ class FatturazioneGeneraService
784 830
     {
785 831
         $query = Annotazione::query()
786 832
             ->where('tempo_minuti', '>', 0)
787
-            ->whereHas('segnalazione', fn ($q) => $q->where('ordine_lavoro_id', $ordineId));
833
+            ->where(function ($q) {
834
+                $q->whereNull('non_fatturare')->orWhere('non_fatturare', false);
835
+            })
836
+            ->whereHas('segnalazione', function ($q) use ($ordineId) {
837
+                $q->where('ordine_lavoro_id', $ordineId);
838
+            });
788 839
 
789 840
         $this->applicaLimiteTemporale($query, null, $billingFrom->copy()->subSecond());
790 841
 
@@ -1003,13 +1054,15 @@ class FatturazioneGeneraService
1003 1054
         $labelRiga = $riga ? $riga->label : 'Canone contratto';
1004 1055
         $periodoLabel = $riga ? $riga->periodo_label : $contratto->periodo_label;
1005 1056
         $descrizione = $contratto->label.' — '.$labelRiga.' ('.$pDa->format('d/m/Y').' – '.$pA->format('d/m/Y').')';
1006
-
1007 1057
         $cliente['rinnovi'][] = [
1008 1058
             'tipo' => 'canone',
1009 1059
             'contratto_id' => $contratto->id,
1010 1060
             'contratto' => $contratto->label,
1061
+            'contratto_servizio_riga_id' => $riga?->id,
1011 1062
             'riga' => $labelRiga,
1012 1063
             'periodo' => $periodoLabel,
1064
+            'periodo_da' => $pDa->toDateString(),
1065
+            'periodo_a' => $pA->toDateString(),
1013 1066
             'occorrenze' => 1,
1014 1067
             'importo' => $importo,
1015 1068
             'scadenza' => null,

+ 42
- 0
database/migrations/2026_09_07_234702_skipper_migrations_2026090723470210.php Dosyayı Görüntüle

@@ -0,0 +1,42 @@
1
+<?php
2
+/* 
3
+ * Migrations generated by: Skipper (http://www.skipper18.com)
4
+ * Migration id: dc6bbfda-98f8-4d97-b0e8-e16f3f9bdbbf
5
+ * Migration datetime: 2026-09-07 23:47:02.102868
6
+ */ 
7
+
8
+use Illuminate\Support\Facades\Schema;
9
+use Illuminate\Database\Schema\Blueprint;
10
+use Illuminate\Database\Migrations\Migration;
11
+
12
+class SkipperMigrations2026090723470210 extends Migration
13
+{
14
+    /**
15
+     * Run the migrations.
16
+     *
17
+     * @return void
18
+     */
19
+    public function up()
20
+    {
21
+        Schema::table('annotazione', function (Blueprint $table) {
22
+            $table->boolean('non_fatturare')->nullable(true)->default(0)->after('costo_descrizione');
23
+        });
24
+        Schema::table('annotazione', function (Blueprint $table) {
25
+            $table->decimal('costo_uscita', 8, 2)->nullable(true)->default(0)->after('non_fatturare');
26
+        });
27
+    }
28
+    /**
29
+     * Reverse the migrations.
30
+     *
31
+     * @return void
32
+     */
33
+    public function down()
34
+    {
35
+        Schema::table('annotazione', function (Blueprint $table) {
36
+            $table->dropColumn('costo_uscita');
37
+        });
38
+        Schema::table('annotazione', function (Blueprint $table) {
39
+            $table->dropColumn('non_fatturare');
40
+        });
41
+    }
42
+}

+ 10
- 0
resources/views/config/index.blade.php Dosyayı Görüntüle

@@ -114,6 +114,16 @@ $groupKeys = array_keys($groups);
114 114
 
115 115
               @case('number')
116 116
               <input type="number" step="0.01" class="form-control" name="value[{{ $config->key }}]" id="{{ $fid }}-control" value="{{ $config->value }}" {{ $disabled }}>
117
+              @if($config->key === Config::FATTURAZIONE_COSTO_USCITA)
118
+              <div class="form-text">Importo aggiunto in fattura per ogni registrazione tempo con «Costo d'uscita».</div>
119
+              @endif
120
+              @break
121
+
122
+              @case('date')
123
+              <input type="date" class="form-control" name="value[{{ $config->key }}]" id="{{ $fid }}-control" value="{{ $config->value }}" {{ $disabled }}>
124
+              @if($config->key === Config::FATTURAZIONE_RINNOVI_DA)
125
+              <div class="form-text">I canoni con data di rinnovo precedente non vengono fatturati (già gestiti sulla piattaforma precedente).</div>
126
+              @endif
117 127
               @break
118 128
 
119 129
               @case('azienda')

+ 2
- 0
resources/views/fatturazione/genera.blade.php Dosyayı Görüntüle

@@ -103,6 +103,8 @@
103 103
   Tariffa default {{ number_format($esito['tariffa_default'], 2, ',', '.') }} €/ora.
104 104
   Le ore coperte da contratto o ordine non entrano nel totale; extra e ticket liberi sì.
105 105
   Le scadenze sono segnalate ma non sommate (importo suggerito a parte).
106
+  I rinnovi si fatturano solo dal {{ Config::rinnoviDa()->format('d/m/Y') }} in poi.
107
+  Il costo d'uscita ({{ Config::formatCosto(Config::costoUscitaDefault()) }}) si aggiunge se spuntato in registrazione tempo.
106 108
 </p>
107 109
 
108 110
 <div class="card mb-4">

+ 4
- 1
resources/views/fatturazione/partials/cliente.blade.php Dosyayı Görüntüle

@@ -117,6 +117,9 @@
117 117
                 @if($s['importo_spesa'] > 0)
118 118
                 <div class="text-muted small">spese {{ Config::formatCosto($s['importo_spesa']) }}</div>
119 119
                 @endif
120
+                @if(($s['importo_uscita'] ?? 0) > 0)
121
+                <div class="text-muted small">uscite {{ Config::formatCosto($s['importo_uscita']) }}</div>
122
+                @endif
120 123
               </td>
121 124
             </tr>
122 125
             @endforeach
@@ -177,7 +180,7 @@
177 180
       </div>
178 181
       @endif
179 182
 
180
-      @if(count($cliente['segnalazioni']) === 0 && count($cliente['rinnovi']) === 0)
183
+      @if(count($cliente['segnalazioni'] ?? []) === 0 && count($cliente['rinnovi'] ?? []) === 0)
181 184
       <p class="text-muted mb-0">Nessuna voce nel periodo.</p>
182 185
       @endif
183 186
     </div>

+ 30
- 2
resources/views/segnalazione/admin/show.blade.php Dosyayı Görüntüle

@@ -190,9 +190,11 @@ use App\Models\Config;
190 190
       $hasText = $annotazione->text && trim($annotazione->text) !== '';
191 191
       $hasCosto = $annotazione->costo !== null && $annotazione->costo != 0;
192 192
       $hasTempo = $annotazione->tempo_minuti !== null && $annotazione->tempo_minuti > 0;
193
+      $hasUscita = (bool) $annotazione->costo_uscita;
194
+      $hasNonFatturare = (bool) $annotazione->non_fatturare;
193 195
       $hasAllegati = $annotazione->allegatiAnnotazione->count() > 0;
194 196
 
195
-      if (! ($hasText || $hasCosto || $hasTempo || $hasAllegati)) {
197
+      if (! ($hasText || $hasCosto || $hasTempo || $hasUscita || $hasNonFatturare || $hasAllegati)) {
196 198
         continue;
197 199
       }
198 200
 
@@ -1061,6 +1063,21 @@ use App\Models\Config;
1061 1063
                     </div>
1062 1064
                     @endif
1063 1065
 
1066
+                    @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->non_fatturare)
1067
+                    <div class="col-auto">
1068
+                      <span class="badge bg-label-warning">Non fatturare</span>
1069
+                    </div>
1070
+                    @endif
1071
+
1072
+                    @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->costo_uscita)
1073
+                    <div class="col-auto">
1074
+                      <small class="text-muted">Uscita:</small>
1075
+                      <span class="badge bg-label-info ms-2">
1076
+                        <i class="bx bx-car me-1"></i>{{ Config::formatCosto(Config::costoUscitaDefault()) }}
1077
+                      </span>
1078
+                    </div>
1079
+                    @endif
1080
+
1064 1081
                     @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->allegatiAnnotazione->count() > 0)
1065 1082
                     <div class="col-auto">
1066 1083
                       <small class="text-muted">Allegati:</small>
@@ -1407,10 +1424,21 @@ use App\Models\Config;
1407 1424
                   <label for="ore_tempo_card" class="form-label">Registra ore</label>
1408 1425
                   <input type="text" class="form-control" id="ore_tempo_card" name="tempo_ore" placeholder="Es. 1,5 o 1h30m">
1409 1426
                 </div>
1410
-                <div class="mb-0">
1427
+                <div class="mb-3">
1411 1428
                   <label for="tempo_data" class="form-label">Data e ora</label>
1412 1429
                   <input type="text" class="form-control flatpickr-input" id="tempo_data" name="tempo_data" value="{{ \Carbon\Carbon::now()->format('d/m/Y H:i') }}">
1413 1430
                 </div>
1431
+                <div class="form-check mb-2">
1432
+                  <input type="hidden" name="non_fatturare" value="0">
1433
+                  <input class="form-check-input" type="checkbox" name="non_fatturare" id="non_fatturare" value="1" @checked(old('non_fatturare'))>
1434
+                  <label class="form-check-label" for="non_fatturare">Non fatturare</label>
1435
+                  <div class="form-text">Questo tempo non entra in fattura.</div>
1436
+                </div>
1437
+                <div class="form-check mb-0">
1438
+                  <input type="hidden" name="costo_uscita" value="0">
1439
+                  <input class="form-check-input" type="checkbox" name="costo_uscita" id="costo_uscita" value="1" @checked(old('costo_uscita'))>
1440
+                  <label class="form-check-label" for="costo_uscita">Costo d'uscita ({{ Config::formatCosto(Config::costoUscitaDefault()) }})</label>
1441
+                </div>
1414 1442
               </div>
1415 1443
             </div>
1416 1444
             <div class="col-md-4">

Loading…
İptal
Kaydet