Roberto Santini 1 vecka sedan
förälder
incheckning
3243f52a11

+ 2
- 0
GestioneManutenzioni.skipper Visa fil

165
         </orm-attributes>
165
         </orm-attributes>
166
       </field>
166
       </field>
167
       <field name="costo_descrizione" type="text" uuid="2d89a9fb-9229-412e-b6df-9b35a19f14a5"/>
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
       <field name="riservata" type="boolean" default="1" uuid="c48733ff-ec02-4aa0-be7c-25be727c3c1e"/>
170
       <field name="riservata" type="boolean" default="1" uuid="c48733ff-ec02-4aa0-be7c-25be727c3c1e"/>
169
       <field name="riferimento" type="string" uuid="dca8d620-9b45-4a1f-96e4-0d76392ac410"/>
171
       <field name="riferimento" type="string" uuid="dca8d620-9b45-4a1f-96e4-0d76392ac410"/>
170
       <field name="email" type="string" uuid="e2ca0f54-426d-4977-b729-fd007dee6f64"/>
172
       <field name="email" type="string" uuid="e2ca0f54-426d-4977-b729-fd007dee6f64"/>

+ 23
- 1
app/DataTables/TempoImpiegatoDataTable.php Visa fil

44
     ->addColumn('tempo_minuti_label', function($entity){
44
     ->addColumn('tempo_minuti_label', function($entity){
45
       return Config::formatTempo($entity->tempo_minuti);
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
     ->filterColumn('tempo_data', function ($query, $keyword) {
59
     ->filterColumn('tempo_data', function ($query, $keyword) {
48
       CostoSegnalazioneDataTable::applyDateTimeColumnFilter($query, $keyword, 'annotazione.tempo_data');
60
       CostoSegnalazioneDataTable::applyDateTimeColumnFilter($query, $keyword, 'annotazione.tempo_data');
49
     })
61
     })
89
       $query->where('annotazione.text', 'like', '%' . $keyword . '%');
101
       $query->where('annotazione.text', 'like', '%' . $keyword . '%');
90
     })
102
     })
91
     ->setRowId('id')
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
                            }']),
167
                            }']),
156
           Fields\Text::make('tempo_minuti_label', 'Tempo impiegato (1,50 o 1h30m)'),
168
           Fields\Text::make('tempo_minuti_label', 'Tempo impiegato (1,50 o 1h30m)'),
157
           Fields\DateTime::make('tempo_data', 'Data/ora')->format('DD/MM/YYYY HH:mm')->default(Carbon::now()->format('d/m/Y H:i')),
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
           Fields\TextArea::make('text', 'Note')->type('quill'),
178
           Fields\TextArea::make('text', 'Note')->type('quill'),
159
           Fields\Select2::make('segnalazione_id', 'Segnalazione')
179
           Fields\Select2::make('segnalazione_id', 'Segnalazione')
160
           ->default($this->segnalazione ? $this->segnalazione->id : 0)
180
           ->default($this->segnalazione ? $this->segnalazione->id : 0)
202
           Column::make('user_id')->title('Utente')->data('user_label')->responsivePriority(3),
222
           Column::make('user_id')->title('Utente')->data('user_label')->responsivePriority(3),
203
           Column::make('segnalazione_id')->title('Segnalazione')->data('segnalazione_label')->responsivePriority(3)->visible($this->segnalazione == null),
223
           Column::make('segnalazione_id')->title('Segnalazione')->data('segnalazione_label')->responsivePriority(3)->visible($this->segnalazione == null),
204
           Column::make('tempo_minuti')->title('Tempo impiegato')->responsivePriority(3)->data('tempo_minuti_label'),
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
           Column::make('text')->title('Note')->responsivePriority(3)->data('text_excerpt'),
227
           Column::make('text')->title('Note')->responsivePriority(3)->data('text_excerpt'),
206
           Column::computed('action')
228
           Column::computed('action')
207
           ->title(' ')
229
           ->title(' ')

+ 6
- 0
app/DataTables/TempoImpiegatoDataTableEditor.php Visa fil

87
   public function creating(Model $model, array $data): array
87
   public function creating(Model $model, array $data): array
88
   {
88
   {
89
     $data['tempo_minuti'] = Config::parseTempoToMinuti($data['tempo_minuti_label']);
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
     return $data;
93
     return $data;
91
   }
94
   }
92
 
95
 
93
   public function updating(Model $model, array $data): array
96
   public function updating(Model $model, array $data): array
94
   {
97
   {
95
     $data['tempo_minuti'] = Config::parseTempoToMinuti($data['tempo_minuti_label']);
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
     return $data;
102
     return $data;
97
   }
103
   }
98
 
104
 

+ 3
- 0
app/Http/Controllers/SegnalazioneController.php Visa fil

268
       $annotazione->costo_descrizione = $request->costo_descrizione;
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
     $annotazione->save();
274
     $annotazione->save();
272
 
275
 
273
     if ($request->hasFile('allegato')) {
276
     if ($request->hasFile('allegato')) {

+ 6
- 0
app/Models/AbstractModels/AbstractAnnotazione.php Visa fil

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

+ 26
- 0
app/Models/Config.php Visa fil

27
 
27
 
28
   const FATTURAZIONE_TARIFFA_ORARIA = 'FATTURAZIONE_TARIFFA_ORARIA';
28
   const FATTURAZIONE_TARIFFA_ORARIA = 'FATTURAZIONE_TARIFFA_ORARIA';
29
   const FATTURAZIONE_IVA_PERCENTUALE = 'FATTURAZIONE_IVA_PERCENTUALE';
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
   public static function getConfig(){
33
   public static function getConfig(){
32
     $config = [];
34
     $config = [];
66
     $config['Fatturazione'] = [
68
     $config['Fatturazione'] = [
67
       ['key' => self::FATTURAZIONE_TARIFFA_ORARIA, 'value' => '40', 'display_name' => 'Tariffa oraria default (€/ora)', 'type' => 'number'],
69
       ['key' => self::FATTURAZIONE_TARIFFA_ORARIA, 'value' => '40', 'display_name' => 'Tariffa oraria default (€/ora)', 'type' => 'number'],
68
       ['key' => self::FATTURAZIONE_IVA_PERCENTUALE, 'value' => '22', 'display_name' => 'Aliquota IVA default (%)', 'type' => 'number'],
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
     return $config;
75
     return $config;
228
     return max(0, (float) $value);
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
   public static function formatCosto($value, $decimals = 2){
257
   public static function formatCosto($value, $decimals = 2){
232
     return number_format($value, $decimals, ",",".",)." €";
258
     return number_format($value, $decimals, ",",".",)." €";
233
   }
259
   }

+ 2
- 0
app/Models/FatturaRiga.php Visa fil

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

+ 94
- 0
app/Models/FatturazioneEsclusione.php Visa fil

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 Visa fil

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

+ 42
- 0
database/migrations/2026_09_07_234702_skipper_migrations_2026090723470210.php Visa fil

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 Visa fil

114
 
114
 
115
               @case('number')
115
               @case('number')
116
               <input type="number" step="0.01" class="form-control" name="value[{{ $config->key }}]" id="{{ $fid }}-control" value="{{ $config->value }}" {{ $disabled }}>
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
               @break
127
               @break
118
 
128
 
119
               @case('azienda')
129
               @case('azienda')

+ 2
- 0
resources/views/fatturazione/genera.blade.php Visa fil

103
   Tariffa default {{ number_format($esito['tariffa_default'], 2, ',', '.') }} €/ora.
103
   Tariffa default {{ number_format($esito['tariffa_default'], 2, ',', '.') }} €/ora.
104
   Le ore coperte da contratto o ordine non entrano nel totale; extra e ticket liberi sì.
104
   Le ore coperte da contratto o ordine non entrano nel totale; extra e ticket liberi sì.
105
   Le scadenze sono segnalate ma non sommate (importo suggerito a parte).
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
 </p>
108
 </p>
107
 
109
 
108
 <div class="card mb-4">
110
 <div class="card mb-4">

+ 4
- 1
resources/views/fatturazione/partials/cliente.blade.php Visa fil

117
                 @if($s['importo_spesa'] > 0)
117
                 @if($s['importo_spesa'] > 0)
118
                 <div class="text-muted small">spese {{ Config::formatCosto($s['importo_spesa']) }}</div>
118
                 <div class="text-muted small">spese {{ Config::formatCosto($s['importo_spesa']) }}</div>
119
                 @endif
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
               </td>
123
               </td>
121
             </tr>
124
             </tr>
122
             @endforeach
125
             @endforeach
177
       </div>
180
       </div>
178
       @endif
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
       <p class="text-muted mb-0">Nessuna voce nel periodo.</p>
184
       <p class="text-muted mb-0">Nessuna voce nel periodo.</p>
182
       @endif
185
       @endif
183
     </div>
186
     </div>

+ 30
- 2
resources/views/segnalazione/admin/show.blade.php Visa fil

190
       $hasText = $annotazione->text && trim($annotazione->text) !== '';
190
       $hasText = $annotazione->text && trim($annotazione->text) !== '';
191
       $hasCosto = $annotazione->costo !== null && $annotazione->costo != 0;
191
       $hasCosto = $annotazione->costo !== null && $annotazione->costo != 0;
192
       $hasTempo = $annotazione->tempo_minuti !== null && $annotazione->tempo_minuti > 0;
192
       $hasTempo = $annotazione->tempo_minuti !== null && $annotazione->tempo_minuti > 0;
193
+      $hasUscita = (bool) $annotazione->costo_uscita;
194
+      $hasNonFatturare = (bool) $annotazione->non_fatturare;
193
       $hasAllegati = $annotazione->allegatiAnnotazione->count() > 0;
195
       $hasAllegati = $annotazione->allegatiAnnotazione->count() > 0;
194
 
196
 
195
-      if (! ($hasText || $hasCosto || $hasTempo || $hasAllegati)) {
197
+      if (! ($hasText || $hasCosto || $hasTempo || $hasUscita || $hasNonFatturare || $hasAllegati)) {
196
         continue;
198
         continue;
197
       }
199
       }
198
 
200
 
1061
                     </div>
1063
                     </div>
1062
                     @endif
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
                     @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->allegatiAnnotazione->count() > 0)
1081
                     @if($evento['dati']['annotazione'] && $evento['dati']['annotazione']->allegatiAnnotazione->count() > 0)
1065
                     <div class="col-auto">
1082
                     <div class="col-auto">
1066
                       <small class="text-muted">Allegati:</small>
1083
                       <small class="text-muted">Allegati:</small>
1407
                   <label for="ore_tempo_card" class="form-label">Registra ore</label>
1424
                   <label for="ore_tempo_card" class="form-label">Registra ore</label>
1408
                   <input type="text" class="form-control" id="ore_tempo_card" name="tempo_ore" placeholder="Es. 1,5 o 1h30m">
1425
                   <input type="text" class="form-control" id="ore_tempo_card" name="tempo_ore" placeholder="Es. 1,5 o 1h30m">
1409
                 </div>
1426
                 </div>
1410
-                <div class="mb-0">
1427
+                <div class="mb-3">
1411
                   <label for="tempo_data" class="form-label">Data e ora</label>
1428
                   <label for="tempo_data" class="form-label">Data e ora</label>
1412
                   <input type="text" class="form-control flatpickr-input" id="tempo_data" name="tempo_data" value="{{ \Carbon\Carbon::now()->format('d/m/Y H:i') }}">
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
                 </div>
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
               </div>
1442
               </div>
1415
             </div>
1443
             </div>
1416
             <div class="col-md-4">
1444
             <div class="col-md-4">

Laddar…
Avbryt
Spara