Browse Source

fatture in cloud

Roberto Santini 3 weeks ago
parent
commit
bbcfa20583

+ 31
- 2
app/Http/Controllers/ConfigController.php View File

@@ -8,6 +8,8 @@ use Illuminate\Http\Request;
8 8
 use App\Models\Azienda;
9 9
 use App\Models\Config;
10 10
 use App\Models\ConfiguraSegnalazione;
11
+use App\Services\FattureInCloudClient;
12
+use RuntimeException;
11 13
 use Illuminate\Support\Facades\Auth;
12 14
 use Illuminate\Support\Facades\Log;
13 15
 use Illuminate\Support\Facades\Storage;
@@ -26,7 +28,7 @@ class ConfigController extends Controller implements HasMiddleware
26 28
   {
27 29
     return [
28 30
       new Middleware('permission:view-config', only: ['index', 'admin_index']),
29
-      new Middleware('permission:edit-config', only: ['store', 'admin_save']),
31
+      new Middleware('permission:edit-config', only: ['store', 'admin_save', 'admin_sync_aliquote']),
30 32
     ];
31 33
   }
32 34
 
@@ -94,12 +96,39 @@ class ConfigController extends Controller implements HasMiddleware
94 96
           continue;
95 97
         }
96 98
 
99
+        if ($config->type === 'aliquote') {
100
+          Config::impostaAliquotaDefault((int) $request->input('fic_aliquota_default', 0));
101
+          continue;
102
+        }
103
+
97 104
         $config->value = $this->normalizeValue($config->type, $values[$key] ?? null);
98 105
         $config->save();
99 106
       }
100 107
     }
101 108
 
102
-    return redirect()->route('admin.config.index')->with('success', 'Configurazione aggiornata');
109
+    return redirect()->route('admin.config.index')
110
+      ->with('success', 'Configurazione aggiornata')
111
+      ->with('config_group', $request->input('config_group'));
112
+  }
113
+
114
+  public function admin_sync_aliquote(Request $request, FattureInCloudClient $fic)
115
+  {
116
+    try {
117
+      $catalogo = Config::salvaAliquoteDaFic(
118
+        $fic->vatTypes(),
119
+        (int) $request->input('fic_aliquota_default', 0)
120
+      );
121
+    } catch (RuntimeException $e) {
122
+      return redirect()->route('admin.config.index')
123
+        ->with('config_group', 'Fatture in Cloud')
124
+        ->withErrors(['fic' => $e->getMessage()]);
125
+    }
126
+
127
+    $n = count($catalogo['items']);
128
+
129
+    return redirect()->route('admin.config.index')
130
+      ->with('config_group', 'Fatture in Cloud')
131
+      ->with('success', 'Aliquote IVA aggiornate da Fatture in Cloud ('.$n.'). Segna quella da usare di default e salva.');
103 132
   }
104 133
 
105 134
   protected function normalizeValue(?string $type, mixed $value): ?string

+ 18
- 3
app/Http/Controllers/FatturazioneController.php View File

@@ -110,6 +110,9 @@ class FatturazioneController extends Controller implements HasMiddleware
110 110
   public function admin_update(Request $request, $id)
111 111
   {
112 112
     $fattura = Fattura::findOrFail($id);
113
+    if (! $fattura->isBozza()) {
114
+      return redirect()->back()->with('error', 'Una fattura emessa non si modifica.');
115
+    }
113 116
 
114 117
     $validated = $request->validate([
115 118
       'numero' => 'nullable|integer|min:1',
@@ -127,7 +130,7 @@ class FatturazioneController extends Controller implements HasMiddleware
127 130
       'telefono' => 'nullable|string|max:255',
128 131
       'comune_id' => 'nullable|exists:comune,id',
129 132
       'note' => 'nullable|string',
130
-      'iva_percentuale' => 'nullable|numeric|min:0|max:100',
133
+      'fic_vat_id' => 'nullable|integer',
131 134
       'imponibile' => 'nullable|numeric',
132 135
       'iva' => 'nullable|numeric',
133 136
       'totale' => 'nullable|numeric',
@@ -144,7 +147,16 @@ class FatturazioneController extends Controller implements HasMiddleware
144 147
       'righe.*.nota' => 'nullable|string',
145 148
     ]);
146 149
 
147
-    DB::transaction(function () use ($fattura, $validated) {
150
+    $aliquota = Config::aliquotaFic((int) ($validated['fic_vat_id'] ?? 0));
151
+    if (! $aliquota) {
152
+      $messaggio = Config::aliquoteFic()['items']
153
+        ? 'Scegli un\'aliquota IVA tra quelle salvate da Fatture in Cloud.'
154
+        : 'Scarica le aliquote IVA da Fatture in Cloud nella configurazione, poi scegline una.';
155
+
156
+      return redirect()->back()->withInput()->withErrors(['fic_vat_id' => $messaggio]);
157
+    }
158
+
159
+    DB::transaction(function () use ($fattura, $validated, $aliquota) {
148 160
       $fattura->numero = $validated['numero'] ?? null;
149 161
       $fattura->stato = $validated['stato'];
150 162
       $fattura->data = $validated['data'] ?? null;
@@ -160,7 +172,10 @@ class FatturazioneController extends Controller implements HasMiddleware
160 172
       $fattura->telefono = $validated['telefono'] ?? null;
161 173
       $fattura->comune_id = $validated['comune_id'] ?? null;
162 174
       $fattura->note = $validated['note'] ?? null;
163
-      $fattura->iva_percentuale = $validated['iva_percentuale'] ?? null;
175
+      $fattura->iva_percentuale = (float) $aliquota['value'];
176
+      $meta = is_array($fattura->meta) ? $fattura->meta : [];
177
+      $meta['fic_vat_id'] = (int) $aliquota['id'];
178
+      $fattura->meta = $meta;
164 179
       $fattura->imponibile = $validated['imponibile'] ?? null;
165 180
       $fattura->iva = $validated['iva'] ?? null;
166 181
       $fattura->totale = $validated['totale'] ?? null;

+ 198
- 1
app/Models/Config.php View File

@@ -32,6 +32,7 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
32 32
 
33 33
   const FIC_COMPANY_ID = 'FIC_COMPANY_ID';
34 34
   const FIC_ACCESS_TOKEN = 'FIC_ACCESS_TOKEN';
35
+  const FIC_ALIQUOTE = 'FIC_ALIQUOTE';
35 36
 
36 37
   public static function getConfig(){
37 38
     $config = [];
@@ -70,7 +71,7 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
70 71
 
71 72
     $config['Fatturazione'] = [
72 73
       ['key' => self::FATTURAZIONE_TARIFFA_ORARIA, 'value' => '40', 'display_name' => 'Tariffa oraria default (€/ora)', 'type' => 'number'],
73
-      ['key' => self::FATTURAZIONE_IVA_PERCENTUALE, 'value' => '22', 'display_name' => 'Aliquota IVA default (%)', 'type' => 'number'],
74
+      ['key' => self::FATTURAZIONE_IVA_PERCENTUALE, 'value' => '22', 'display_name' => 'Aliquota IVA di riserva (%)', 'type' => 'number'],
74 75
       ['key' => self::FATTURAZIONE_COSTO_USCITA, 'value' => '30', 'display_name' => 'Costo d\'uscita (€ / intervento)', 'type' => 'number'],
75 76
       ['key' => self::FATTURAZIONE_RINNOVI_DA, 'value' => '2026-09-01', 'display_name' => 'Fattura i rinnovi a partire da', 'type' => 'date'],
76 77
     ];
@@ -78,6 +79,7 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
78 79
     $config['Fatture in Cloud'] = [
79 80
       ['key' => self::FIC_COMPANY_ID, 'value' => null, 'display_name' => 'Company ID', 'type' => 'text'],
80 81
       ['key' => self::FIC_ACCESS_TOKEN, 'value' => null, 'display_name' => 'Access token', 'type' => 'password'],
82
+      ['key' => self::FIC_ALIQUOTE, 'value' => null, 'display_name' => 'Aliquote IVA', 'type' => 'aliquote'],
81 83
     ];
82 84
 
83 85
     return $config;
@@ -232,6 +234,11 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
232 234
 
233 235
   public static function ivaPercentualeDefault(): float
234 236
   {
237
+    $aliquota = self::aliquotaFicDefault();
238
+    if ($aliquota) {
239
+      return max(0, (float) $aliquota['value']);
240
+    }
241
+
235 242
     $value = self::valueOf(self::FATTURAZIONE_IVA_PERCENTUALE, '22');
236 243
     if ($value === null || ! is_numeric($value)) {
237 244
       return 22.0;
@@ -240,6 +247,196 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
240 247
     return max(0, (float) $value);
241 248
   }
242 249
 
250
+  /**
251
+   * @return array{default_id: ?int, items: array<int, array<string, mixed>>}
252
+   */
253
+  public static function aliquoteFic(): array
254
+  {
255
+    $json = json_decode((string) self::valueOf(self::FIC_ALIQUOTE, ''), true);
256
+    if (! is_array($json)) {
257
+      return ['default_id' => null, 'items' => []];
258
+    }
259
+
260
+    $items = [];
261
+    foreach ($json['items'] ?? [] as $item) {
262
+      if (! is_array($item) || ! isset($item['id'])) {
263
+        continue;
264
+      }
265
+      $items[] = self::normalizzaAliquota($item);
266
+    }
267
+
268
+    $defaultId = isset($json['default_id']) ? (int) $json['default_id'] : 0;
269
+    $ids = array_column($items, 'id');
270
+    if (! in_array($defaultId, $ids, true)) {
271
+      $defaultId = 0;
272
+    }
273
+
274
+    return [
275
+      'default_id' => $defaultId ?: null,
276
+      'items' => $items,
277
+    ];
278
+  }
279
+
280
+  /**
281
+   * @return array<string, mixed>|null
282
+   */
283
+  public static function aliquotaFic(?int $id): ?array
284
+  {
285
+    if (! $id) {
286
+      return null;
287
+    }
288
+    foreach (self::aliquoteFic()['items'] as $item) {
289
+      if ((int) $item['id'] === $id) {
290
+        return $item;
291
+      }
292
+    }
293
+
294
+    return null;
295
+  }
296
+
297
+  /**
298
+   * @return array<string, mixed>|null
299
+   */
300
+  public static function aliquotaFicDefault(): ?array
301
+  {
302
+    $catalogo = self::aliquoteFic();
303
+
304
+    return self::aliquotaFic($catalogo['default_id']);
305
+  }
306
+
307
+  /**
308
+   * Aliquota della fattura: quella salvata, oppure l'unica (o la predefinita) con la stessa percentuale.
309
+   *
310
+   * @return array<string, mixed>|null
311
+   */
312
+  public static function aliquotaPerFattura(Fattura $fattura): ?array
313
+  {
314
+    $stored = self::aliquotaFic($fattura->ficVatId());
315
+    if ($stored) {
316
+      return $stored;
317
+    }
318
+
319
+    $pct = $fattura->iva_percentuale !== null ? (float) $fattura->iva_percentuale : null;
320
+    if ($pct === null) {
321
+      return self::aliquotaFicDefault();
322
+    }
323
+
324
+    $matches = [];
325
+    foreach (self::aliquoteFic()['items'] as $item) {
326
+      if (abs((float) $item['value'] - $pct) < 0.001) {
327
+        $matches[] = $item;
328
+      }
329
+    }
330
+    $default = self::aliquotaFicDefault();
331
+    if ($default && abs((float) $default['value'] - $pct) < 0.001) {
332
+      return $default;
333
+    }
334
+    if (count($matches) === 1) {
335
+      return $matches[0];
336
+    }
337
+
338
+    return null;
339
+  }
340
+
341
+  /**
342
+   * @param  array<int, array<string, mixed>>  $types
343
+   * @return array{default_id: ?int, items: array<int, array<string, mixed>>}
344
+   */
345
+  public static function salvaAliquoteDaFic(array $types, int $requestedDefault = 0): array
346
+  {
347
+    $previous = self::aliquoteFic();
348
+    $items = [];
349
+    foreach ($types as $type) {
350
+      if (! is_array($type) || ! empty($type['is_disabled']) || ! isset($type['id'])) {
351
+        continue;
352
+      }
353
+      $items[] = self::normalizzaAliquota($type);
354
+    }
355
+
356
+    $ids = array_column($items, 'id');
357
+    $defaultId = in_array($requestedDefault, $ids, true) ? $requestedDefault : 0;
358
+    if (! $defaultId && in_array((int) ($previous['default_id'] ?? 0), $ids, true)) {
359
+      $defaultId = (int) $previous['default_id'];
360
+    }
361
+    if (! $defaultId && $items) {
362
+      $defaultId = (int) $items[0]['id'];
363
+      foreach ($items as $item) {
364
+        $natura = (string) ($item['ei_type'] ?? '');
365
+        if (abs((float) $item['value'] - 22) < 0.001 && ($natura === '' || $natura === '0')) {
366
+          $defaultId = (int) $item['id'];
367
+          break;
368
+        }
369
+      }
370
+    }
371
+
372
+    $payload = [
373
+      'default_id' => $defaultId ?: null,
374
+      'items' => $items,
375
+    ];
376
+
377
+    $row = self::find(self::FIC_ALIQUOTE);
378
+    if (! $row) {
379
+      self::syncDefinitions();
380
+      $row = self::find(self::FIC_ALIQUOTE);
381
+    }
382
+    if ($row) {
383
+      $row->value = json_encode($payload, JSON_UNESCAPED_UNICODE);
384
+      $row->save();
385
+    }
386
+
387
+    return $payload;
388
+  }
389
+
390
+  public static function impostaAliquotaDefault(int $id): void
391
+  {
392
+    $catalogo = self::aliquoteFic();
393
+    if (! in_array($id, array_column($catalogo['items'], 'id'), true)) {
394
+      return;
395
+    }
396
+    $row = self::find(self::FIC_ALIQUOTE);
397
+    if (! $row) {
398
+      return;
399
+    }
400
+    $catalogo['default_id'] = $id;
401
+    $row->value = json_encode($catalogo, JSON_UNESCAPED_UNICODE);
402
+    $row->save();
403
+  }
404
+
405
+  /**
406
+   * @param  array<string, mixed>  $item
407
+   */
408
+  public static function labelAliquota(array $item): string
409
+  {
410
+    $pct = rtrim(rtrim(number_format((float) ($item['value'] ?? 0), 2, ',', ''), '0'), ',');
411
+    $desc = trim((string) ($item['description'] ?? ''));
412
+    $label = $desc !== '' ? $desc : $pct.'%';
413
+    if (! str_contains($label, '%')) {
414
+      $label = $pct.'% — '.$label;
415
+    }
416
+    $natura = trim((string) ($item['ei_type'] ?? ''));
417
+    if ($natura !== '' && $natura !== '0' && ! str_contains($label, $natura)) {
418
+      $label .= ' ('.$natura.')';
419
+    }
420
+
421
+    return $label;
422
+  }
423
+
424
+  /**
425
+   * @param  array<string, mixed>  $item
426
+   * @return array<string, mixed>
427
+   */
428
+  protected static function normalizzaAliquota(array $item): array
429
+  {
430
+    return [
431
+      'id' => (int) $item['id'],
432
+      'value' => round((float) ($item['value'] ?? 0), 2),
433
+      'description' => trim((string) ($item['description'] ?? '')),
434
+      'e_invoice' => (bool) ($item['e_invoice'] ?? false),
435
+      'ei_type' => trim((string) ($item['ei_type'] ?? '')),
436
+      'ei_description' => trim((string) ($item['ei_description'] ?? '')),
437
+    ];
438
+  }
439
+
243 440
   public static function costoUscitaDefault(): float
244 441
   {
245 442
     $value = self::valueOf(self::FATTURAZIONE_COSTO_USCITA, '30');

+ 23
- 1
app/Models/Fattura.php View File

@@ -92,6 +92,25 @@ class Fattura extends \App\Models\AbstractModels\AbstractFattura
92 92
         return $id ? (int) $id : null;
93 93
     }
94 94
 
95
+    public function ficVatId(): ?int
96
+    {
97
+        $id = data_get($this->meta, 'fic_vat_id');
98
+
99
+        return $id ? (int) $id : null;
100
+    }
101
+
102
+    public function aliquotaLabel(): string
103
+    {
104
+        $aliquota = Config::aliquotaPerFattura($this);
105
+        if ($aliquota) {
106
+            return Config::labelAliquota($aliquota);
107
+        }
108
+
109
+        $pct = rtrim(rtrim(number_format((float) $this->iva_percentuale, 2, ',', ''), '0'), ',');
110
+
111
+        return $pct.'%';
112
+    }
113
+
95 114
     public function scopeNonAnnullate($query)
96 115
     {
97 116
         return $query->where('stato', '!=', self::STATO_ANNULLATA);
@@ -106,7 +125,10 @@ class Fattura extends \App\Models\AbstractModels\AbstractFattura
106 125
     {
107 126
         $this->loadMissing('righe');
108 127
         $imponibile = round((float) $this->righe->sum(fn (FatturaRiga $r) => (float) ($r->importo ?? 0)), 2);
109
-        $pct = $this->iva_percentuale !== null ? (float) $this->iva_percentuale : Config::ivaPercentualeDefault();
128
+        $aliquota = Config::aliquotaFic($this->ficVatId());
129
+        $pct = $aliquota
130
+            ? (float) $aliquota['value']
131
+            : ($this->iva_percentuale !== null ? (float) $this->iva_percentuale : Config::ivaPercentualeDefault());
110 132
         $iva = round($imponibile * $pct / 100, 2);
111 133
 
112 134
         $this->imponibile = $imponibile;

+ 19
- 14
app/Services/FatturaFicService.php View File

@@ -36,11 +36,11 @@ class FatturaFicService
36 36
             throw new RuntimeException('Collega il cliente a Fatture in Cloud dalla scheda azienda, poi riprova.');
37 37
         }
38 38
 
39
+        $aliquota = $this->aliquota($fattura);
39 40
         try {
40 41
             $client = $this->fic->getClient((int) $collegamento->fic_client_id);
41
-            $vatId = $this->vatId((float) ($fattura->iva_percentuale ?? Config::ivaPercentualeDefault()));
42 42
             $anagrafica = User::find($collegamento->user_id);
43
-            $document = $this->fic->createIssuedDocument($this->payload($fattura, $client, $vatId, $anagrafica));
43
+            $document = $this->fic->createIssuedDocument($this->payload($fattura, $client, (int) $aliquota['id'], $anagrafica, (float) $aliquota['value']));
44 44
             if (! isset($document['number']) || $document['number'] === '' || $document['number'] === null) {
45 45
                 $document = $this->fic->getIssuedDocument((int) $document['id']);
46 46
             }
@@ -54,6 +54,7 @@ class FatturaFicService
54 54
         }
55 55
 
56 56
         $meta = is_array($fattura->meta) ? $fattura->meta : [];
57
+        $meta['fic_vat_id'] = (int) $aliquota['id'];
57 58
         $meta['fic'] = [
58 59
             'document_id' => (int) $document['id'],
59 60
             'number' => $numeroFic,
@@ -107,7 +108,7 @@ class FatturaFicService
107 108
      * @param  array<string, mixed>  $client
108 109
      * @return array<string, mixed>
109 110
      */
110
-    protected function payload(Fattura $fattura, array $client, int $vatId, ?User $anagrafica = null): array
111
+    protected function payload(Fattura $fattura, array $client, int $vatId, ?User $anagrafica, float $ivaPercentuale): array
111 112
     {
112 113
         $data = $fattura->data ? Carbon::parse($fattura->data)->toDateString() : now()->toDateString();
113 114
         $entity = ['id' => (int) $client['id']];
@@ -156,7 +157,7 @@ class FatturaFicService
156 157
             $netto += round(((float) $item['qty']) * ((float) $item['net_price']), 2);
157 158
         }
158 159
         $netto = round($netto, 2);
159
-        $pct = (float) ($fattura->iva_percentuale ?? Config::ivaPercentualeDefault());
160
+        $pct = $ivaPercentuale;
160 161
         $lordo = round($netto + round($netto * $pct / 100, 2), 2);
161 162
         $scadenza = $fattura->data_scadenza
162 163
             ? Carbon::parse($fattura->data_scadenza)->toDateString()
@@ -251,20 +252,24 @@ class FatturaFicService
251 252
         return (int) round(round($qty * $price, 2) * 100);
252 253
     }
253 254
 
254
-    protected function vatId(float $percent): int
255
+    /**
256
+     * @return array<string, mixed>
257
+     */
258
+    protected function aliquota(Fattura $fattura): array
255 259
     {
256
-        foreach ($this->fic->vatTypes() as $type) {
257
-            if (! is_array($type) || ! empty($type['is_disabled'])) {
258
-                continue;
259
-            }
260
-            if (abs((float) ($type['value'] ?? -1) - $percent) < 0.001 && isset($type['id'])) {
261
-                return (int) $type['id'];
262
-            }
260
+        if (Config::aliquoteFic()['items'] === []) {
261
+            throw new RuntimeException('Scarica le aliquote IVA da Fatture in Cloud nella configurazione e segna quella predefinita.');
263 262
         }
264 263
 
265
-        $label = rtrim(rtrim(number_format($percent, 2, ',', ''), '0'), ',');
264
+        $aliquota = Config::aliquotaPerFattura($fattura);
265
+        if (! $aliquota) {
266
+            throw new RuntimeException('Ci sono più aliquote con la stessa percentuale. Apri la fattura in bozza e scegli quella giusta.');
267
+        }
268
+        if ($fattura->iva_percentuale !== null && abs((float) $fattura->iva_percentuale - (float) $aliquota['value']) >= 0.01) {
269
+            throw new RuntimeException('L\'aliquota '.Config::labelAliquota($aliquota).' non coincide con l\'IVA della fattura. Se è in bozza, selezionala e salva.');
270
+        }
266 271
 
267
-        throw new RuntimeException('Su Fatture in Cloud non c\'è un\'aliquota IVA del '.$label.'%.');
272
+        return $aliquota;
268 273
     }
269 274
 
270 275
     protected function conScope(string $message): string

+ 5
- 1
app/Services/FatturazioneGeneraService.php View File

@@ -149,12 +149,16 @@ class FatturazioneGeneraService
149 149
             $fattura->periodo_da = $from->toDateString();
150 150
             $fattura->periodo_a = $to->toDateString();
151 151
             $fattura->oggetto = 'Periodo '.$from->format('d/m/Y').' – '.$to->format('d/m/Y');
152
-            $fattura->iva_percentuale = Config::ivaPercentualeDefault();
152
+            $aliquota = Config::aliquotaFicDefault();
153
+            $fattura->iva_percentuale = $aliquota
154
+                ? (float) $aliquota['value']
155
+                : Config::ivaPercentualeDefault();
153 156
             $fattura->snapshotDaUser($user);
154 157
             $fattura->meta = [
155 158
                 'annotazione_ids' => $annotazioneIds,
156 159
                 'dettaglio_ticket' => $dettaglio,
157 160
                 'accorpa_segnalazioni' => $accorpaSegnalazioni,
161
+                'fic_vat_id' => $aliquota['id'] ?? null,
158 162
             ];
159 163
             $fattura->save();
160 164
 

+ 65
- 3
resources/views/config/index.blade.php View File

@@ -9,6 +9,14 @@ $tabIcons = [
9 9
   'Fatture in Cloud' => 'bx-cloud',
10 10
 ];
11 11
 $groupKeys = array_keys($groups);
12
+$activeGroup = session('config_group');
13
+$activeIndex = 0;
14
+if (is_string($activeGroup)) {
15
+  $found = array_search($activeGroup, $groupKeys, true);
16
+  if ($found !== false) {
17
+    $activeIndex = $found;
18
+  }
19
+}
12 20
 @endphp
13 21
 
14 22
 @extends('layouts/layoutMaster')
@@ -50,6 +58,7 @@ $groupKeys = array_keys($groups);
50 58
 
51 59
 <form method="POST" action="{{ route('admin.config.save') }}" autocomplete="off">
52 60
   @csrf
61
+  <input type="hidden" name="config_group" id="config_group" value="{{ $groupKeys[$activeIndex] ?? '' }}">
53 62
 
54 63
   <div class="card mb-4">
55 64
     <div class="card-header border-bottom-0 pb-0">
@@ -57,13 +66,13 @@ $groupKeys = array_keys($groups);
57 66
         @foreach($groupKeys as $ti => $group)
58 67
         <li class="nav-item" role="presentation">
59 68
           <button type="button"
60
-            class="nav-link d-inline-flex align-items-center gap-1 @if($ti === 0) active @endif"
69
+            class="nav-link d-inline-flex align-items-center gap-1 @if($ti === $activeIndex) active @endif"
61 70
             id="config-tab-{{ $ti }}-tab"
62 71
             data-bs-toggle="tab"
63 72
             data-bs-target="#config-tab-{{ $ti }}"
64 73
             role="tab"
65 74
             aria-controls="config-tab-{{ $ti }}"
66
-            @if($ti === 0) aria-selected="true" @else aria-selected="false" @endif>
75
+            @if($ti === $activeIndex) aria-selected="true" @else aria-selected="false" @endif>
67 76
             <i class="icon-base bx {{ $tabIcons[$group] ?? 'bx-slider-alt' }}" aria-hidden="true"></i>
68 77
             <span>{{ $group }}</span>
69 78
           </button>
@@ -75,7 +84,7 @@ $groupKeys = array_keys($groups);
75 84
       <div class="tab-content">
76 85
         @foreach($groupKeys as $ti => $group)
77 86
         @php $items = $groups[$group]; @endphp
78
-        <div class="tab-pane fade @if($ti === 0) show active @endif" id="config-tab-{{ $ti }}" role="tabpanel" aria-labelledby="config-tab-{{ $ti }}-tab">
87
+        <div class="tab-pane fade @if($ti === $activeIndex) show active @endif" id="config-tab-{{ $ti }}" role="tabpanel" aria-labelledby="config-tab-{{ $ti }}-tab">
79 88
           @forelse($items as $config)
80 89
           @php
81 90
             $fid = 'cfg-'.preg_replace('/[^a-zA-Z0-9_-]/', '-', $config->key);
@@ -119,6 +128,9 @@ $groupKeys = array_keys($groups);
119 128
               @if($config->key === Config::FATTURAZIONE_COSTO_USCITA)
120 129
               <div class="form-text">Importo aggiunto in fattura per ogni registrazione tempo con «Costo d'uscita».</div>
121 130
               @endif
131
+              @if($config->key === Config::FATTURAZIONE_IVA_PERCENTUALE)
132
+              <div class="form-text">Usata solo se non hai ancora scaricato le aliquote da Fatture in Cloud. Con l'elenco salvato, la fattura usa quella segnata come predefinita.</div>
133
+              @endif
122 134
               @break
123 135
 
124 136
               @case('date')
@@ -183,6 +195,44 @@ $groupKeys = array_keys($groups);
183 195
               </div>
184 196
               @break
185 197
 
198
+              @case('aliquote')
199
+              @php $catalogoIva = Config::aliquoteFic(); @endphp
200
+              <div class="table-responsive mb-3">
201
+                <table class="table table-sm align-middle mb-0">
202
+                  <thead>
203
+                    <tr>
204
+                      <th style="width: 110px;">Predefinita</th>
205
+                      <th>Aliquota</th>
206
+                      <th style="width: 90px;">%</th>
207
+                      <th>Natura</th>
208
+                    </tr>
209
+                  </thead>
210
+                  <tbody>
211
+                    @forelse($catalogoIva['items'] as $aliquota)
212
+                    <tr>
213
+                      <td>
214
+                        <input class="form-check-input" type="radio" name="fic_aliquota_default" value="{{ $aliquota['id'] }}" @checked((int) ($catalogoIva['default_id'] ?? 0) === (int) $aliquota['id']) {{ $disabled }}>
215
+                      </td>
216
+                      <td>{{ $aliquota['description'] !== '' ? $aliquota['description'] : Config::labelAliquota($aliquota) }}</td>
217
+                      <td>{{ rtrim(rtrim(number_format((float) $aliquota['value'], 2, ',', ''), '0'), ',') }}</td>
218
+                      <td class="text-muted">{{ $aliquota['ei_type'] !== '' && $aliquota['ei_type'] !== '0' ? $aliquota['ei_type'] : '—' }}</td>
219
+                    </tr>
220
+                    @empty
221
+                    <tr>
222
+                      <td colspan="4" class="text-muted">Nessuna aliquota salvata. Aggiorna da Fatture in Cloud.</td>
223
+                    </tr>
224
+                    @endforelse
225
+                  </tbody>
226
+                </table>
227
+              </div>
228
+              <div class="form-text mb-2">L'elenco resta in locale: l'invio fattura non interroga più Fatture in Cloud per le aliquote. La predefinita è proposta sulle nuove fatture.</div>
229
+              @if($canEdit)
230
+              <button type="submit" class="btn btn-sm btn-outline-primary" formaction="{{ route('admin.config.fic_aliquote') }}">
231
+                <i class="bx bx-cloud-download me-1"></i>Aggiorna da Fatture in Cloud
232
+              </button>
233
+              @endif
234
+              @break
235
+
186 236
               @case('password')
187 237
               <input type="password" class="form-control" name="value[{{ $config->key }}]" id="{{ $fid }}-control" value="" autocomplete="new-password" placeholder="{{ $config->value ? 'Token già salvato' : '' }}" {{ $disabled }}>
188 238
               @if($config->key === Config::FIC_ACCESS_TOKEN)
@@ -217,3 +267,15 @@ $groupKeys = array_keys($groups);
217 267
   @endif
218 268
 </form>
219 269
 @endsection
270
+
271
+@section('page-script')
272
+<script>
273
+document.querySelectorAll('#config_group').length && document.querySelectorAll('[data-bs-toggle="tab"]').forEach(function (btn) {
274
+  btn.addEventListener('shown.bs.tab', function () {
275
+    var label = btn.querySelector('span');
276
+    var field = document.getElementById('config_group');
277
+    if (field && label) field.value = label.textContent.trim();
278
+  });
279
+});
280
+</script>
281
+@endsection

+ 33
- 10
resources/views/fatturazione/show.blade.php View File

@@ -20,13 +20,21 @@
20 20
     }
21 21
     return rtrim(rtrim(number_format((float) $value, 2, '.', ''), '0'), '.');
22 22
   };
23
+  $catalogoIva = \App\Models\Config::aliquoteFic();
24
+  $vatSelezionato = (int) old('fic_vat_id', 0);
25
+  if (! $vatSelezionato) {
26
+    $salvata = \App\Models\Config::aliquotaPerFattura($fattura);
27
+    $vatSelezionato = (int) ($salvata['id'] ?? 0);
28
+  }
23 29
 @endphp
24 30
 
25 31
 <div class="d-flex flex-wrap align-items-center gap-2 mb-4 pb-3 border-bottom">
26 32
   @can('edit-fattura')
33
+  @if($fattura->isBozza())
27 34
   <button type="submit" class="btn btn-primary btn-sm" form="formFattura">
28 35
     <i class="bx bx-save me-1"></i>Salva
29 36
   </button>
37
+  @endif
30 38
   @if($fattura->isBozza())
31 39
   <form method="POST" action="{{ route('admin.fatturazione.emetti', $fattura->id) }}" class="d-inline">
32 40
     @csrf
@@ -99,7 +107,7 @@
99 107
 </div>
100 108
 @endif
101 109
 
102
-@can('edit-fattura')
110
+@if(auth()->user()->can('edit-fattura') && $fattura->isBozza())
103 111
 @php
104 112
   $righeForm = old('righe');
105 113
   if ($righeForm === null) {
@@ -200,9 +208,21 @@
200 208
           <label class="form-label" for="telefono">Telefono</label>
201 209
           <input type="text" name="telefono" id="telefono" class="form-control" value="{{ old('telefono', $fattura->telefono) }}">
202 210
         </div>
203
-        <div class="col-md-4">
204
-          <label class="form-label" for="iva_percentuale">IVA %</label>
205
-          <input type="number" step="0.01" min="0" max="100" name="iva_percentuale" id="iva_percentuale" class="form-control" value="{{ old('iva_percentuale', $numInput($fattura->iva_percentuale)) }}">
211
+        <div class="col-12">
212
+          <label class="form-label" for="fic_vat_id">Aliquota IVA</label>
213
+          <select name="fic_vat_id" id="fic_vat_id" class="form-select @error('fic_vat_id') is-invalid @enderror">
214
+            <option value="">— Seleziona —</option>
215
+            @foreach($catalogoIva['items'] as $aliquota)
216
+            <option value="{{ $aliquota['id'] }}" data-percentuale="{{ $aliquota['value'] }}" @selected($vatSelezionato === (int) $aliquota['id'])>
217
+              {{ \App\Models\Config::labelAliquota($aliquota) }}@if((int) ($catalogoIva['default_id'] ?? 0) === (int) $aliquota['id']) — predefinita @endif
218
+            </option>
219
+            @endforeach
220
+          </select>
221
+          @if($catalogoIva['items'] === [])
222
+          <div class="form-text">Nessuna aliquota in configurazione. Scaricale da Fatture in Cloud in <a href="{{ route('admin.config.index') }}">Configurazione</a>.</div>
223
+          @else
224
+          <div class="form-text">Elenco salvato da Fatture in Cloud. La predefinita è quella segnata in configurazione.</div>
225
+          @endif
206 226
         </div>
207 227
         <div class="col-12">
208 228
           <label class="form-label" for="note">Note</label>
@@ -348,6 +368,7 @@
348 368
         @if($fattura->oggettoFic() !== '')
349 369
         <p class="mb-1"><strong>Oggetto:</strong> {{ $fattura->oggettoFic() }}</p>
350 370
         @endif
371
+        <p class="mb-1"><strong>Aliquota IVA:</strong> {{ $fattura->aliquotaLabel() }}</p>
351 372
         <p class="mb-0"><strong>Periodo:</strong>
352 373
           {{ $fattura->periodo_da ? \Carbon\Carbon::parse($fattura->periodo_da)->format('d/m/Y') : '—' }}
353 374
           –
@@ -410,7 +431,7 @@
410 431
               <th class="text-end">{{ \App\Models\Config::formatCosto($fattura->imponibile ?? 0) }}</th>
411 432
             </tr>
412 433
             <tr>
413
-              <th colspan="3" class="text-end">IVA {{ rtrim(rtrim(number_format((float) $fattura->iva_percentuale, 2, ',', ''), '0'), ',') }}%</th>
434
+              <th colspan="3" class="text-end">IVA {{ $fattura->aliquotaLabel() }}</th>
414 435
               <th class="text-end">{{ \App\Models\Config::formatCosto($fattura->iva ?? 0) }}</th>
415 436
             </tr>
416 437
             <tr>
@@ -423,10 +444,10 @@
423 444
     </div>
424 445
   </div>
425 446
 </div>
426
-@endcan
447
+@endif
427 448
 @endsection
428 449
 
429
-@can('edit-fattura')
450
+@if(auth()->user()->can('edit-fattura') && $fattura->isBozza())
430 451
 @section('page-script')
431 452
 <script type="module">
432 453
 $(function () {
@@ -472,7 +493,9 @@ $(function () {
472 493
     let imponibile = 0;
473 494
     tbody.querySelectorAll('.importo-input').forEach(el => { imponibile += num(el); });
474 495
     imponibile = round2(imponibile);
475
-    const pct = num(document.getElementById('iva_percentuale'));
496
+    const aliquota = document.getElementById('fic_vat_id');
497
+    const opt = aliquota ? aliquota.selectedOptions[0] : null;
498
+    const pct = opt ? num({ value: opt.dataset.percentuale || '0' }) : 0;
476 499
     const iva = round2(imponibile * pct / 100);
477 500
     document.getElementById('imponibile').value = imponibile.toFixed(2);
478 501
     document.getElementById('iva').value = iva.toFixed(2);
@@ -509,7 +532,7 @@ $(function () {
509 532
     }
510 533
   });
511 534
 
512
-  document.getElementById('iva_percentuale').addEventListener('input', function () {
535
+  document.getElementById('fic_vat_id')?.addEventListener('change', function () {
513 536
     if (form.dataset.totaliManual !== '1') scriviTotali();
514 537
   });
515 538
 
@@ -526,4 +549,4 @@ $(function () {
526 549
 });
527 550
 </script>
528 551
 @endsection
529
-@endcan
552
+@endif

+ 1
- 0
routes/web.php View File

@@ -188,6 +188,7 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session')])->prefix('a
188 188
     // Configurazione
189 189
     Route::get('config', [ConfigController::class, 'admin_index'])->name('admin.config.index');
190 190
     Route::post('config', [ConfigController::class, 'admin_save'])->name('admin.config.save');
191
+    Route::post('config/fic-aliquote', [ConfigController::class, 'admin_sync_aliquote'])->name('admin.config.fic_aliquote');
191 192
 
192 193
     // Configura Segnalazione
193 194
     Route::get('configura-segnalazione', [ConfiguraSegnalazioneController::class, 'admin_index'])->name('admin.configura-segnalazione.index');

Loading…
Cancel
Save