Browse Source

fatture in cloud

Roberto Santini 3 weeks ago
parent
commit
bbcfa20583

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

8
 use App\Models\Azienda;
8
 use App\Models\Azienda;
9
 use App\Models\Config;
9
 use App\Models\Config;
10
 use App\Models\ConfiguraSegnalazione;
10
 use App\Models\ConfiguraSegnalazione;
11
+use App\Services\FattureInCloudClient;
12
+use RuntimeException;
11
 use Illuminate\Support\Facades\Auth;
13
 use Illuminate\Support\Facades\Auth;
12
 use Illuminate\Support\Facades\Log;
14
 use Illuminate\Support\Facades\Log;
13
 use Illuminate\Support\Facades\Storage;
15
 use Illuminate\Support\Facades\Storage;
26
   {
28
   {
27
     return [
29
     return [
28
       new Middleware('permission:view-config', only: ['index', 'admin_index']),
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
           continue;
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
         $config->value = $this->normalizeValue($config->type, $values[$key] ?? null);
104
         $config->value = $this->normalizeValue($config->type, $values[$key] ?? null);
98
         $config->save();
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
   protected function normalizeValue(?string $type, mixed $value): ?string
134
   protected function normalizeValue(?string $type, mixed $value): ?string

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

110
   public function admin_update(Request $request, $id)
110
   public function admin_update(Request $request, $id)
111
   {
111
   {
112
     $fattura = Fattura::findOrFail($id);
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
     $validated = $request->validate([
117
     $validated = $request->validate([
115
       'numero' => 'nullable|integer|min:1',
118
       'numero' => 'nullable|integer|min:1',
127
       'telefono' => 'nullable|string|max:255',
130
       'telefono' => 'nullable|string|max:255',
128
       'comune_id' => 'nullable|exists:comune,id',
131
       'comune_id' => 'nullable|exists:comune,id',
129
       'note' => 'nullable|string',
132
       'note' => 'nullable|string',
130
-      'iva_percentuale' => 'nullable|numeric|min:0|max:100',
133
+      'fic_vat_id' => 'nullable|integer',
131
       'imponibile' => 'nullable|numeric',
134
       'imponibile' => 'nullable|numeric',
132
       'iva' => 'nullable|numeric',
135
       'iva' => 'nullable|numeric',
133
       'totale' => 'nullable|numeric',
136
       'totale' => 'nullable|numeric',
144
       'righe.*.nota' => 'nullable|string',
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
       $fattura->numero = $validated['numero'] ?? null;
160
       $fattura->numero = $validated['numero'] ?? null;
149
       $fattura->stato = $validated['stato'];
161
       $fattura->stato = $validated['stato'];
150
       $fattura->data = $validated['data'] ?? null;
162
       $fattura->data = $validated['data'] ?? null;
160
       $fattura->telefono = $validated['telefono'] ?? null;
172
       $fattura->telefono = $validated['telefono'] ?? null;
161
       $fattura->comune_id = $validated['comune_id'] ?? null;
173
       $fattura->comune_id = $validated['comune_id'] ?? null;
162
       $fattura->note = $validated['note'] ?? null;
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
       $fattura->imponibile = $validated['imponibile'] ?? null;
179
       $fattura->imponibile = $validated['imponibile'] ?? null;
165
       $fattura->iva = $validated['iva'] ?? null;
180
       $fattura->iva = $validated['iva'] ?? null;
166
       $fattura->totale = $validated['totale'] ?? null;
181
       $fattura->totale = $validated['totale'] ?? null;

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

32
 
32
 
33
   const FIC_COMPANY_ID = 'FIC_COMPANY_ID';
33
   const FIC_COMPANY_ID = 'FIC_COMPANY_ID';
34
   const FIC_ACCESS_TOKEN = 'FIC_ACCESS_TOKEN';
34
   const FIC_ACCESS_TOKEN = 'FIC_ACCESS_TOKEN';
35
+  const FIC_ALIQUOTE = 'FIC_ALIQUOTE';
35
 
36
 
36
   public static function getConfig(){
37
   public static function getConfig(){
37
     $config = [];
38
     $config = [];
70
 
71
 
71
     $config['Fatturazione'] = [
72
     $config['Fatturazione'] = [
72
       ['key' => self::FATTURAZIONE_TARIFFA_ORARIA, 'value' => '40', 'display_name' => 'Tariffa oraria default (€/ora)', 'type' => 'number'],
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
       ['key' => self::FATTURAZIONE_COSTO_USCITA, 'value' => '30', 'display_name' => 'Costo d\'uscita (€ / intervento)', 'type' => 'number'],
75
       ['key' => self::FATTURAZIONE_COSTO_USCITA, 'value' => '30', 'display_name' => 'Costo d\'uscita (€ / intervento)', 'type' => 'number'],
75
       ['key' => self::FATTURAZIONE_RINNOVI_DA, 'value' => '2026-09-01', 'display_name' => 'Fattura i rinnovi a partire da', 'type' => 'date'],
76
       ['key' => self::FATTURAZIONE_RINNOVI_DA, 'value' => '2026-09-01', 'display_name' => 'Fattura i rinnovi a partire da', 'type' => 'date'],
76
     ];
77
     ];
78
     $config['Fatture in Cloud'] = [
79
     $config['Fatture in Cloud'] = [
79
       ['key' => self::FIC_COMPANY_ID, 'value' => null, 'display_name' => 'Company ID', 'type' => 'text'],
80
       ['key' => self::FIC_COMPANY_ID, 'value' => null, 'display_name' => 'Company ID', 'type' => 'text'],
80
       ['key' => self::FIC_ACCESS_TOKEN, 'value' => null, 'display_name' => 'Access token', 'type' => 'password'],
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
     return $config;
85
     return $config;
232
 
234
 
233
   public static function ivaPercentualeDefault(): float
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
     $value = self::valueOf(self::FATTURAZIONE_IVA_PERCENTUALE, '22');
242
     $value = self::valueOf(self::FATTURAZIONE_IVA_PERCENTUALE, '22');
236
     if ($value === null || ! is_numeric($value)) {
243
     if ($value === null || ! is_numeric($value)) {
237
       return 22.0;
244
       return 22.0;
240
     return max(0, (float) $value);
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
   public static function costoUscitaDefault(): float
440
   public static function costoUscitaDefault(): float
244
   {
441
   {
245
     $value = self::valueOf(self::FATTURAZIONE_COSTO_USCITA, '30');
442
     $value = self::valueOf(self::FATTURAZIONE_COSTO_USCITA, '30');

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

92
         return $id ? (int) $id : null;
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
     public function scopeNonAnnullate($query)
114
     public function scopeNonAnnullate($query)
96
     {
115
     {
97
         return $query->where('stato', '!=', self::STATO_ANNULLATA);
116
         return $query->where('stato', '!=', self::STATO_ANNULLATA);
106
     {
125
     {
107
         $this->loadMissing('righe');
126
         $this->loadMissing('righe');
108
         $imponibile = round((float) $this->righe->sum(fn (FatturaRiga $r) => (float) ($r->importo ?? 0)), 2);
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
         $iva = round($imponibile * $pct / 100, 2);
132
         $iva = round($imponibile * $pct / 100, 2);
111
 
133
 
112
         $this->imponibile = $imponibile;
134
         $this->imponibile = $imponibile;

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

36
             throw new RuntimeException('Collega il cliente a Fatture in Cloud dalla scheda azienda, poi riprova.');
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
         try {
40
         try {
40
             $client = $this->fic->getClient((int) $collegamento->fic_client_id);
41
             $client = $this->fic->getClient((int) $collegamento->fic_client_id);
41
-            $vatId = $this->vatId((float) ($fattura->iva_percentuale ?? Config::ivaPercentualeDefault()));
42
             $anagrafica = User::find($collegamento->user_id);
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
             if (! isset($document['number']) || $document['number'] === '' || $document['number'] === null) {
44
             if (! isset($document['number']) || $document['number'] === '' || $document['number'] === null) {
45
                 $document = $this->fic->getIssuedDocument((int) $document['id']);
45
                 $document = $this->fic->getIssuedDocument((int) $document['id']);
46
             }
46
             }
54
         }
54
         }
55
 
55
 
56
         $meta = is_array($fattura->meta) ? $fattura->meta : [];
56
         $meta = is_array($fattura->meta) ? $fattura->meta : [];
57
+        $meta['fic_vat_id'] = (int) $aliquota['id'];
57
         $meta['fic'] = [
58
         $meta['fic'] = [
58
             'document_id' => (int) $document['id'],
59
             'document_id' => (int) $document['id'],
59
             'number' => $numeroFic,
60
             'number' => $numeroFic,
107
      * @param  array<string, mixed>  $client
108
      * @param  array<string, mixed>  $client
108
      * @return array<string, mixed>
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
         $data = $fattura->data ? Carbon::parse($fattura->data)->toDateString() : now()->toDateString();
113
         $data = $fattura->data ? Carbon::parse($fattura->data)->toDateString() : now()->toDateString();
113
         $entity = ['id' => (int) $client['id']];
114
         $entity = ['id' => (int) $client['id']];
156
             $netto += round(((float) $item['qty']) * ((float) $item['net_price']), 2);
157
             $netto += round(((float) $item['qty']) * ((float) $item['net_price']), 2);
157
         }
158
         }
158
         $netto = round($netto, 2);
159
         $netto = round($netto, 2);
159
-        $pct = (float) ($fattura->iva_percentuale ?? Config::ivaPercentualeDefault());
160
+        $pct = $ivaPercentuale;
160
         $lordo = round($netto + round($netto * $pct / 100, 2), 2);
161
         $lordo = round($netto + round($netto * $pct / 100, 2), 2);
161
         $scadenza = $fattura->data_scadenza
162
         $scadenza = $fattura->data_scadenza
162
             ? Carbon::parse($fattura->data_scadenza)->toDateString()
163
             ? Carbon::parse($fattura->data_scadenza)->toDateString()
251
         return (int) round(round($qty * $price, 2) * 100);
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
     protected function conScope(string $message): string
275
     protected function conScope(string $message): string

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

149
             $fattura->periodo_da = $from->toDateString();
149
             $fattura->periodo_da = $from->toDateString();
150
             $fattura->periodo_a = $to->toDateString();
150
             $fattura->periodo_a = $to->toDateString();
151
             $fattura->oggetto = 'Periodo '.$from->format('d/m/Y').' – '.$to->format('d/m/Y');
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
             $fattura->snapshotDaUser($user);
156
             $fattura->snapshotDaUser($user);
154
             $fattura->meta = [
157
             $fattura->meta = [
155
                 'annotazione_ids' => $annotazioneIds,
158
                 'annotazione_ids' => $annotazioneIds,
156
                 'dettaglio_ticket' => $dettaglio,
159
                 'dettaglio_ticket' => $dettaglio,
157
                 'accorpa_segnalazioni' => $accorpaSegnalazioni,
160
                 'accorpa_segnalazioni' => $accorpaSegnalazioni,
161
+                'fic_vat_id' => $aliquota['id'] ?? null,
158
             ];
162
             ];
159
             $fattura->save();
163
             $fattura->save();
160
 
164
 

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

9
   'Fatture in Cloud' => 'bx-cloud',
9
   'Fatture in Cloud' => 'bx-cloud',
10
 ];
10
 ];
11
 $groupKeys = array_keys($groups);
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
 @endphp
20
 @endphp
13
 
21
 
14
 @extends('layouts/layoutMaster')
22
 @extends('layouts/layoutMaster')
50
 
58
 
51
 <form method="POST" action="{{ route('admin.config.save') }}" autocomplete="off">
59
 <form method="POST" action="{{ route('admin.config.save') }}" autocomplete="off">
52
   @csrf
60
   @csrf
61
+  <input type="hidden" name="config_group" id="config_group" value="{{ $groupKeys[$activeIndex] ?? '' }}">
53
 
62
 
54
   <div class="card mb-4">
63
   <div class="card mb-4">
55
     <div class="card-header border-bottom-0 pb-0">
64
     <div class="card-header border-bottom-0 pb-0">
57
         @foreach($groupKeys as $ti => $group)
66
         @foreach($groupKeys as $ti => $group)
58
         <li class="nav-item" role="presentation">
67
         <li class="nav-item" role="presentation">
59
           <button type="button"
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
             id="config-tab-{{ $ti }}-tab"
70
             id="config-tab-{{ $ti }}-tab"
62
             data-bs-toggle="tab"
71
             data-bs-toggle="tab"
63
             data-bs-target="#config-tab-{{ $ti }}"
72
             data-bs-target="#config-tab-{{ $ti }}"
64
             role="tab"
73
             role="tab"
65
             aria-controls="config-tab-{{ $ti }}"
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
             <i class="icon-base bx {{ $tabIcons[$group] ?? 'bx-slider-alt' }}" aria-hidden="true"></i>
76
             <i class="icon-base bx {{ $tabIcons[$group] ?? 'bx-slider-alt' }}" aria-hidden="true"></i>
68
             <span>{{ $group }}</span>
77
             <span>{{ $group }}</span>
69
           </button>
78
           </button>
75
       <div class="tab-content">
84
       <div class="tab-content">
76
         @foreach($groupKeys as $ti => $group)
85
         @foreach($groupKeys as $ti => $group)
77
         @php $items = $groups[$group]; @endphp
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
           @forelse($items as $config)
88
           @forelse($items as $config)
80
           @php
89
           @php
81
             $fid = 'cfg-'.preg_replace('/[^a-zA-Z0-9_-]/', '-', $config->key);
90
             $fid = 'cfg-'.preg_replace('/[^a-zA-Z0-9_-]/', '-', $config->key);
119
               @if($config->key === Config::FATTURAZIONE_COSTO_USCITA)
128
               @if($config->key === Config::FATTURAZIONE_COSTO_USCITA)
120
               <div class="form-text">Importo aggiunto in fattura per ogni registrazione tempo con «Costo d'uscita».</div>
129
               <div class="form-text">Importo aggiunto in fattura per ogni registrazione tempo con «Costo d'uscita».</div>
121
               @endif
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
               @break
134
               @break
123
 
135
 
124
               @case('date')
136
               @case('date')
183
               </div>
195
               </div>
184
               @break
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
               @case('password')
236
               @case('password')
187
               <input type="password" class="form-control" name="value[{{ $config->key }}]" id="{{ $fid }}-control" value="" autocomplete="new-password" placeholder="{{ $config->value ? 'Token già salvato' : '' }}" {{ $disabled }}>
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
               @if($config->key === Config::FIC_ACCESS_TOKEN)
238
               @if($config->key === Config::FIC_ACCESS_TOKEN)
217
   @endif
267
   @endif
218
 </form>
268
 </form>
219
 @endsection
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
     }
20
     }
21
     return rtrim(rtrim(number_format((float) $value, 2, '.', ''), '0'), '.');
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
 @endphp
29
 @endphp
24
 
30
 
25
 <div class="d-flex flex-wrap align-items-center gap-2 mb-4 pb-3 border-bottom">
31
 <div class="d-flex flex-wrap align-items-center gap-2 mb-4 pb-3 border-bottom">
26
   @can('edit-fattura')
32
   @can('edit-fattura')
33
+  @if($fattura->isBozza())
27
   <button type="submit" class="btn btn-primary btn-sm" form="formFattura">
34
   <button type="submit" class="btn btn-primary btn-sm" form="formFattura">
28
     <i class="bx bx-save me-1"></i>Salva
35
     <i class="bx bx-save me-1"></i>Salva
29
   </button>
36
   </button>
37
+  @endif
30
   @if($fattura->isBozza())
38
   @if($fattura->isBozza())
31
   <form method="POST" action="{{ route('admin.fatturazione.emetti', $fattura->id) }}" class="d-inline">
39
   <form method="POST" action="{{ route('admin.fatturazione.emetti', $fattura->id) }}" class="d-inline">
32
     @csrf
40
     @csrf
99
 </div>
107
 </div>
100
 @endif
108
 @endif
101
 
109
 
102
-@can('edit-fattura')
110
+@if(auth()->user()->can('edit-fattura') && $fattura->isBozza())
103
 @php
111
 @php
104
   $righeForm = old('righe');
112
   $righeForm = old('righe');
105
   if ($righeForm === null) {
113
   if ($righeForm === null) {
200
           <label class="form-label" for="telefono">Telefono</label>
208
           <label class="form-label" for="telefono">Telefono</label>
201
           <input type="text" name="telefono" id="telefono" class="form-control" value="{{ old('telefono', $fattura->telefono) }}">
209
           <input type="text" name="telefono" id="telefono" class="form-control" value="{{ old('telefono', $fattura->telefono) }}">
202
         </div>
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
         </div>
226
         </div>
207
         <div class="col-12">
227
         <div class="col-12">
208
           <label class="form-label" for="note">Note</label>
228
           <label class="form-label" for="note">Note</label>
348
         @if($fattura->oggettoFic() !== '')
368
         @if($fattura->oggettoFic() !== '')
349
         <p class="mb-1"><strong>Oggetto:</strong> {{ $fattura->oggettoFic() }}</p>
369
         <p class="mb-1"><strong>Oggetto:</strong> {{ $fattura->oggettoFic() }}</p>
350
         @endif
370
         @endif
371
+        <p class="mb-1"><strong>Aliquota IVA:</strong> {{ $fattura->aliquotaLabel() }}</p>
351
         <p class="mb-0"><strong>Periodo:</strong>
372
         <p class="mb-0"><strong>Periodo:</strong>
352
           {{ $fattura->periodo_da ? \Carbon\Carbon::parse($fattura->periodo_da)->format('d/m/Y') : '—' }}
373
           {{ $fattura->periodo_da ? \Carbon\Carbon::parse($fattura->periodo_da)->format('d/m/Y') : '—' }}
353
           –
374
           –
410
               <th class="text-end">{{ \App\Models\Config::formatCosto($fattura->imponibile ?? 0) }}</th>
431
               <th class="text-end">{{ \App\Models\Config::formatCosto($fattura->imponibile ?? 0) }}</th>
411
             </tr>
432
             </tr>
412
             <tr>
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
               <th class="text-end">{{ \App\Models\Config::formatCosto($fattura->iva ?? 0) }}</th>
435
               <th class="text-end">{{ \App\Models\Config::formatCosto($fattura->iva ?? 0) }}</th>
415
             </tr>
436
             </tr>
416
             <tr>
437
             <tr>
423
     </div>
444
     </div>
424
   </div>
445
   </div>
425
 </div>
446
 </div>
426
-@endcan
447
+@endif
427
 @endsection
448
 @endsection
428
 
449
 
429
-@can('edit-fattura')
450
+@if(auth()->user()->can('edit-fattura') && $fattura->isBozza())
430
 @section('page-script')
451
 @section('page-script')
431
 <script type="module">
452
 <script type="module">
432
 $(function () {
453
 $(function () {
472
     let imponibile = 0;
493
     let imponibile = 0;
473
     tbody.querySelectorAll('.importo-input').forEach(el => { imponibile += num(el); });
494
     tbody.querySelectorAll('.importo-input').forEach(el => { imponibile += num(el); });
474
     imponibile = round2(imponibile);
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
     const iva = round2(imponibile * pct / 100);
499
     const iva = round2(imponibile * pct / 100);
477
     document.getElementById('imponibile').value = imponibile.toFixed(2);
500
     document.getElementById('imponibile').value = imponibile.toFixed(2);
478
     document.getElementById('iva').value = iva.toFixed(2);
501
     document.getElementById('iva').value = iva.toFixed(2);
509
     }
532
     }
510
   });
533
   });
511
 
534
 
512
-  document.getElementById('iva_percentuale').addEventListener('input', function () {
535
+  document.getElementById('fic_vat_id')?.addEventListener('change', function () {
513
     if (form.dataset.totaliManual !== '1') scriviTotali();
536
     if (form.dataset.totaliManual !== '1') scriviTotali();
514
   });
537
   });
515
 
538
 
526
 });
549
 });
527
 </script>
550
 </script>
528
 @endsection
551
 @endsection
529
-@endcan
552
+@endif

+ 1
- 0
routes/web.php View File

188
     // Configurazione
188
     // Configurazione
189
     Route::get('config', [ConfigController::class, 'admin_index'])->name('admin.config.index');
189
     Route::get('config', [ConfigController::class, 'admin_index'])->name('admin.config.index');
190
     Route::post('config', [ConfigController::class, 'admin_save'])->name('admin.config.save');
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
     // Configura Segnalazione
193
     // Configura Segnalazione
193
     Route::get('configura-segnalazione', [ConfiguraSegnalazioneController::class, 'admin_index'])->name('admin.configura-segnalazione.index');
194
     Route::get('configura-segnalazione', [ConfiguraSegnalazioneController::class, 'admin_index'])->name('admin.configura-segnalazione.index');

Loading…
Cancel
Save