Roberto Santini 1 小時之前
父節點
當前提交
df23586b35

+ 36
- 3
app/DataTables/TempoImpiegatoDataTable.php 查看文件

@@ -38,6 +38,11 @@ class TempoImpiegatoDataTable extends DataTable
38 38
     ->addColumn('user_label', function($entity){
39 39
       return $entity->user ? $entity->user->full_name : 'N/A';
40 40
     })
41
+    ->addColumn('azienda_label', function($entity){
42
+      $azienda = $entity->segnalazione?->azienda;
43
+
44
+      return $azienda ? $azienda->full_name : '—';
45
+    })
41 46
     ->addColumn('text_excerpt', function($entity){
42 47
       return $entity->text ? Str::limit($entity->text, 50) : 'N/A';
43 48
     })
@@ -64,6 +69,11 @@ class TempoImpiegatoDataTable extends DataTable
64 69
       if ($keyword === '') {
65 70
         return;
66 71
       }
72
+      if (ctype_digit($keyword)) {
73
+        $query->where('annotazione.user_id', (int) $keyword);
74
+
75
+        return;
76
+      }
67 77
       $query->whereHas('user', function ($q) use ($keyword) {
68 78
         $q->where(function ($sub) use ($keyword) {
69 79
           $sub->where('nome', 'like', '%' . $keyword . '%')
@@ -72,6 +82,24 @@ class TempoImpiegatoDataTable extends DataTable
72 82
         });
73 83
       });
74 84
     })
85
+    ->filterColumn('azienda_id', function ($query, $keyword) {
86
+      $keyword = trim((string) $keyword);
87
+      if ($keyword === '') {
88
+        return;
89
+      }
90
+      if (ctype_digit($keyword)) {
91
+        $query->where('segnalazione.azienda_id', (int) $keyword);
92
+
93
+        return;
94
+      }
95
+      $query->where(function ($q) use ($keyword) {
96
+        $q->where('azienda_user.ragione_sociale', 'like', '%' . $keyword . '%')
97
+          ->orWhere('azienda_user.nome', 'like', '%' . $keyword . '%')
98
+          ->orWhere('azienda_user.cognome', 'like', '%' . $keyword . '%');
99
+      });
100
+    })
101
+    ->orderColumn('user_id', 'annotazione.user_id $1')
102
+    ->orderColumn('azienda_id', "COALESCE(NULLIF(azienda_user.ragione_sociale, ''), azienda_user.nome) $1")
75 103
     ->filterColumn('segnalazione_id', function ($query, $keyword) {
76 104
       $keyword = trim((string) $keyword);
77 105
       if ($keyword === '') {
@@ -111,11 +139,15 @@ class TempoImpiegatoDataTable extends DataTable
111 139
   */
112 140
   public function query(Annotazione $model): QueryBuilder
113 141
   {
114
-    $builder = $model->newQuery()->where('tempo_minuti', '>', 0);
142
+    $builder = $model->newQuery()
143
+      ->select('annotazione.*')
144
+      ->where('annotazione.tempo_minuti', '>', 0)
145
+      ->leftJoin('segnalazione', 'annotazione.segnalazione_id', '=', 'segnalazione.id')
146
+      ->leftJoin('users as azienda_user', 'segnalazione.azienda_id', '=', 'azienda_user.id');
115 147
     if($this->segnalazione){
116
-      $builder->where('segnalazione_id', $this->segnalazione->id);
148
+      $builder->where('annotazione.segnalazione_id', $this->segnalazione->id);
117 149
     }
118
-    return $builder->with(['user', 'segnalazione']);
150
+    return $builder->with(['user', 'segnalazione.azienda']);
119 151
   }
120 152
 
121 153
   /**
@@ -220,6 +252,7 @@ class TempoImpiegatoDataTable extends DataTable
220 252
         return [
221 253
           Column::make('tempo_data')->title('Data/ora')->responsivePriority(1),
222 254
           Column::make('user_id')->title('Utente')->data('user_label')->responsivePriority(3),
255
+          Column::make('azienda_id')->title('Azienda')->data('azienda_label')->responsivePriority(3),
223 256
           Column::make('segnalazione_id')->title('Segnalazione')->data('segnalazione_label')->responsivePriority(3)->visible($this->segnalazione == null),
224 257
           Column::make('tempo_minuti')->title('Tempo impiegato')->responsivePriority(3)->data('tempo_minuti_label'),
225 258
           Column::make('non_fatturare')->title('Fatturazione')->data('non_fatturare_label')->responsivePriority(4),

+ 27
- 1
app/Http/Controllers/ConfigController.php 查看文件

@@ -30,7 +30,7 @@ class ConfigController extends Controller implements HasMiddleware
30 30
   {
31 31
     return [
32 32
       new Middleware('permission:view-config', only: ['index', 'admin_index']),
33
-      new Middleware('permission:edit-config', only: ['store', 'admin_save', 'admin_sync_aliquote', 'admin_telegram_webhook']),
33
+      new Middleware('permission:edit-config', only: ['store', 'admin_save', 'admin_sync_aliquote', 'admin_sync_metodi_pagamento', 'admin_telegram_webhook']),
34 34
     ];
35 35
   }
36 36
 
@@ -103,6 +103,11 @@ class ConfigController extends Controller implements HasMiddleware
103 103
           continue;
104 104
         }
105 105
 
106
+        if ($config->type === 'metodi_pagamento') {
107
+          Config::impostaMetodoPagamentoDefault((int) $request->input('fic_metodo_pagamento_default', 0));
108
+          continue;
109
+        }
110
+
106 111
         $config->value = $this->normalizeValue($config->type, $values[$key] ?? null);
107 112
         $config->save();
108 113
       }
@@ -133,6 +138,26 @@ class ConfigController extends Controller implements HasMiddleware
133 138
       ->with('success', 'Aliquote IVA aggiornate da Fatture in Cloud ('.$n.'). Segna quella da usare di default e salva.');
134 139
   }
135 140
 
141
+  public function admin_sync_metodi_pagamento(Request $request, FattureInCloudClient $fic)
142
+  {
143
+    try {
144
+      $catalogo = Config::salvaMetodiPagamentoDaFic(
145
+        $fic->paymentMethods(),
146
+        (int) $request->input('fic_metodo_pagamento_default', 0)
147
+      );
148
+    } catch (RuntimeException $e) {
149
+      return redirect()->route('admin.config.index')
150
+        ->with('config_group', 'Fatture in Cloud')
151
+        ->withErrors(['fic' => $e->getMessage()]);
152
+    }
153
+
154
+    $n = count($catalogo['items']);
155
+
156
+    return redirect()->route('admin.config.index')
157
+      ->with('config_group', 'Fatture in Cloud')
158
+      ->with('success', 'Metodi di pagamento aggiornati da Fatture in Cloud ('.$n.'). Segna quello da usare di default e salva.');
159
+  }
160
+
136 161
   public function admin_telegram_webhook(Request $request, TelegramBotService $telegram)
137 162
   {
138 163
     Config::syncDefinitions();
@@ -188,6 +213,7 @@ class ConfigController extends Controller implements HasMiddleware
188 213
       'stato' => $value === null || $value === '' ? null : (string) $value,
189 214
       'stati' => json_encode($this->normalizeStati($value), JSON_UNESCAPED_UNICODE),
190 215
       'tipo' => $value === null || $value === '' ? null : (string) $value,
216
+      'termini_pagamento' => Config::parseTermini($value)['key'],
191 217
       'lines' => json_encode($this->normalizeLines($value), JSON_UNESCAPED_UNICODE),
192 218
       default => $value === null ? null : (string) $value,
193 219
     };

+ 201
- 0
app/Models/Config.php 查看文件

@@ -33,6 +33,8 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
33 33
   const FIC_COMPANY_ID = 'FIC_COMPANY_ID';
34 34
   const FIC_ACCESS_TOKEN = 'FIC_ACCESS_TOKEN';
35 35
   const FIC_ALIQUOTE = 'FIC_ALIQUOTE';
36
+  const FIC_METODI_PAGAMENTO = 'FIC_METODI_PAGAMENTO';
37
+  const FIC_TERMINI_PAGAMENTO = 'FIC_TERMINI_PAGAMENTO';
36 38
 
37 39
   const TELEGRAM_BOT_TOKEN = 'TELEGRAM_BOT_TOKEN';
38 40
   const TELEGRAM_WEBHOOK_SECRET = 'TELEGRAM_WEBHOOK_SECRET';
@@ -86,6 +88,8 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
86 88
       ['key' => self::FIC_COMPANY_ID, 'value' => null, 'display_name' => 'Company ID', 'type' => 'text'],
87 89
       ['key' => self::FIC_ACCESS_TOKEN, 'value' => null, 'display_name' => 'Access token', 'type' => 'password'],
88 90
       ['key' => self::FIC_ALIQUOTE, 'value' => null, 'display_name' => 'Aliquote IVA', 'type' => 'aliquote'],
91
+      ['key' => self::FIC_METODI_PAGAMENTO, 'value' => null, 'display_name' => 'Metodo di pagamento', 'type' => 'metodi_pagamento'],
92
+      ['key' => self::FIC_TERMINI_PAGAMENTO, 'value' => '30|standard', 'display_name' => 'Termini di pagamento', 'type' => 'termini_pagamento'],
89 93
     ];
90 94
 
91 95
     $config['Task'] = [
@@ -436,6 +440,203 @@ class Config extends \App\Models\AbstractModels\AbstractConfig
436 440
     return $label;
437 441
   }
438 442
 
443
+  /**
444
+   * @return array<string, string>
445
+   */
446
+  public static function terminiPagamentoOpzioni(): array
447
+  {
448
+    return [
449
+      '0|standard' => 'Pagamento a vista',
450
+      '8|standard' => '8 giorni data fattura',
451
+      '15|standard' => '15 giorni data fattura',
452
+      '30|standard' => '30 giorni data fattura',
453
+      '30|end_of_month' => '30 giorni fine mese',
454
+      '45|standard' => '45 giorni data fattura',
455
+      '60|standard' => '60 giorni data fattura',
456
+      '60|end_of_month' => '60 giorni fine mese',
457
+      '90|standard' => '90 giorni data fattura',
458
+      '90|end_of_month' => '90 giorni fine mese',
459
+      '120|standard' => '120 giorni data fattura',
460
+      '120|end_of_month' => '120 giorni fine mese',
461
+    ];
462
+  }
463
+
464
+  /**
465
+   * @return array{days: int, type: string, key: string}
466
+   */
467
+  public static function parseTermini(mixed $value): array
468
+  {
469
+    $raw = trim((string) $value);
470
+    if (! preg_match('/^(\d{1,3})\|(standard|end_of_month)$/', $raw, $match)) {
471
+      return ['days' => 30, 'type' => 'standard', 'key' => '30|standard'];
472
+    }
473
+
474
+    $days = min(365, (int) $match[1]);
475
+    $type = $match[2];
476
+
477
+    return ['days' => $days, 'type' => $type, 'key' => $days.'|'.$type];
478
+  }
479
+
480
+  /**
481
+   * @return array{days: int, type: string, key: string}
482
+   */
483
+  public static function terminiPagamentoFic(): array
484
+  {
485
+    return self::parseTermini(self::valueOf(self::FIC_TERMINI_PAGAMENTO, '30|standard'));
486
+  }
487
+
488
+  /**
489
+   * Scadenza a partire dalla data fattura: i giorni si contano dalla data, oppure dalla fine del mese.
490
+   */
491
+  public static function dataScadenza(\Carbon\Carbon|string $data, ?array $termini = null): string
492
+  {
493
+    $termini = $termini ?? self::terminiPagamentoFic();
494
+    $days = max(0, (int) ($termini['days'] ?? 30));
495
+    $date = \Carbon\Carbon::parse($data)->startOfDay();
496
+    if (($termini['type'] ?? 'standard') === 'end_of_month') {
497
+      return $date->copy()->endOfMonth()->startOfDay()->addDays($days)->toDateString();
498
+    }
499
+
500
+    return $date->copy()->addDays($days)->toDateString();
501
+  }
502
+
503
+  /**
504
+   * @return array{default_id: ?int, items: array<int, array<string, mixed>>}
505
+   */
506
+  public static function metodiPagamentoFic(): array
507
+  {
508
+    $json = json_decode((string) self::valueOf(self::FIC_METODI_PAGAMENTO, ''), true);
509
+    if (! is_array($json)) {
510
+      return ['default_id' => null, 'items' => []];
511
+    }
512
+
513
+    $items = [];
514
+    foreach ($json['items'] ?? [] as $item) {
515
+      if (! is_array($item) || ! isset($item['id'])) {
516
+        continue;
517
+      }
518
+      $items[] = self::normalizzaMetodoPagamento($item);
519
+    }
520
+
521
+    $defaultId = isset($json['default_id']) ? (int) $json['default_id'] : 0;
522
+    if (! in_array($defaultId, array_column($items, 'id'), true)) {
523
+      $defaultId = 0;
524
+    }
525
+
526
+    return [
527
+      'default_id' => $defaultId ?: null,
528
+      'items' => $items,
529
+    ];
530
+  }
531
+
532
+  /**
533
+   * @return array<string, mixed>|null
534
+   */
535
+  public static function metodoPagamentoFic(?int $id): ?array
536
+  {
537
+    if (! $id) {
538
+      return null;
539
+    }
540
+    foreach (self::metodiPagamentoFic()['items'] as $item) {
541
+      if ((int) $item['id'] === $id) {
542
+        return $item;
543
+      }
544
+    }
545
+
546
+    return null;
547
+  }
548
+
549
+  /**
550
+   * @return array<string, mixed>|null
551
+   */
552
+  public static function metodoPagamentoFicDefault(): ?array
553
+  {
554
+    $catalogo = self::metodiPagamentoFic();
555
+
556
+    return self::metodoPagamentoFic($catalogo['default_id']);
557
+  }
558
+
559
+  /**
560
+   * @param  array<int, array<string, mixed>>  $methods
561
+   * @return array{default_id: ?int, items: array<int, array<string, mixed>>}
562
+   */
563
+  public static function salvaMetodiPagamentoDaFic(array $methods, int $requestedDefault = 0): array
564
+  {
565
+    $previous = self::metodiPagamentoFic();
566
+    $items = [];
567
+    foreach ($methods as $method) {
568
+      if (! is_array($method) || ! isset($method['id'])) {
569
+        continue;
570
+      }
571
+      $items[] = self::normalizzaMetodoPagamento($method);
572
+    }
573
+
574
+    $ids = array_column($items, 'id');
575
+    $defaultId = in_array($requestedDefault, $ids, true) ? $requestedDefault : 0;
576
+    if (! $defaultId && in_array((int) ($previous['default_id'] ?? 0), $ids, true)) {
577
+      $defaultId = (int) $previous['default_id'];
578
+    }
579
+    if (! $defaultId) {
580
+      foreach ($items as $item) {
581
+        if (! empty($item['is_default'])) {
582
+          $defaultId = (int) $item['id'];
583
+          break;
584
+        }
585
+      }
586
+    }
587
+    if (! $defaultId && $items) {
588
+      $defaultId = (int) $items[0]['id'];
589
+    }
590
+
591
+    $payload = [
592
+      'default_id' => $defaultId ?: null,
593
+      'items' => $items,
594
+    ];
595
+
596
+    $row = self::find(self::FIC_METODI_PAGAMENTO);
597
+    if (! $row) {
598
+      self::syncDefinitions();
599
+      $row = self::find(self::FIC_METODI_PAGAMENTO);
600
+    }
601
+    if ($row) {
602
+      $row->value = json_encode($payload, JSON_UNESCAPED_UNICODE);
603
+      $row->save();
604
+    }
605
+
606
+    return $payload;
607
+  }
608
+
609
+  public static function impostaMetodoPagamentoDefault(int $id): void
610
+  {
611
+    $catalogo = self::metodiPagamentoFic();
612
+    if (! in_array($id, array_column($catalogo['items'], 'id'), true)) {
613
+      return;
614
+    }
615
+    $row = self::find(self::FIC_METODI_PAGAMENTO);
616
+    if (! $row) {
617
+      return;
618
+    }
619
+    $catalogo['default_id'] = $id;
620
+    $row->value = json_encode($catalogo, JSON_UNESCAPED_UNICODE);
621
+    $row->save();
622
+  }
623
+
624
+  /**
625
+   * @param  array<string, mixed>  $item
626
+   * @return array<string, mixed>
627
+   */
628
+  protected static function normalizzaMetodoPagamento(array $item): array
629
+  {
630
+    $ei = strtoupper(trim((string) ($item['ei_payment_method'] ?? '')));
631
+
632
+    return [
633
+      'id' => (int) $item['id'],
634
+      'name' => trim((string) ($item['name'] ?? '')),
635
+      'is_default' => (bool) ($item['is_default'] ?? false),
636
+      'ei_payment_method' => $ei,
637
+    ];
638
+  }
639
+
439 640
   /**
440 641
    * @param  array<string, mixed>  $item
441 642
    * @return array<string, mixed>

+ 96
- 0
app/Models/User.php 查看文件

@@ -123,6 +123,102 @@ class User extends \App\Models\AbstractModels\AbstractUser
123 123
       ->first(fn (self $azienda) => self::normalizeDominioEmail($azienda->dominio_email) === $dominio);
124 124
   }
125 125
 
126
+  /**
127
+   * Indirizzo email utilizzabile per il match in anagrafica.
128
+   */
129
+  public static function normalizeEmailAddress(?string $value): ?string
130
+  {
131
+    if ($value === null) {
132
+      return null;
133
+    }
134
+
135
+    $value = trim($value);
136
+    if ($value === '') {
137
+      return null;
138
+    }
139
+
140
+    if (preg_match('/<([^<>]+)>/', $value, $matches)) {
141
+      $value = $matches[1];
142
+    }
143
+
144
+    $value = strtolower(trim($value, " \t\n\r\0\x0B<>\"'"));
145
+    if (str_starts_with($value, 'mailto:')) {
146
+      $value = substr($value, 7);
147
+    }
148
+    $value = trim($value);
149
+
150
+    return filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : null;
151
+  }
152
+
153
+  /**
154
+   * Utente reale del mittente. Se non esiste, lo crea e lo collega all'azienda.
155
+   */
156
+  public static function findOrCreateContattoFromEmail(string $email, ?string $displayName = null, ?int $aziendaId = null): ?self
157
+  {
158
+    $email = self::normalizeEmailAddress($email);
159
+    if ($email === null) {
160
+      return null;
161
+    }
162
+
163
+    $user = self::query()
164
+      ->soloUtenti()
165
+      ->where(function ($query) use ($email) {
166
+        $query->whereRaw('LOWER(TRIM(email)) = ?', [$email])
167
+          ->orWhereRaw('LOWER(TRIM(pec)) = ?', [$email]);
168
+      })
169
+      ->orderBy('id')
170
+      ->first();
171
+
172
+    if ($user) {
173
+      if ($aziendaId && ! $user->azienda_id) {
174
+        $user->azienda_id = $aziendaId;
175
+        $user->save();
176
+      }
177
+
178
+      return $user;
179
+    }
180
+
181
+    [$nome, $cognome] = self::splitDisplayName($displayName, $email);
182
+
183
+    $user = new self();
184
+    $user->is_azienda = false;
185
+    $user->is_gruppo = false;
186
+    $user->nome = $nome;
187
+    $user->cognome = $cognome;
188
+    $user->email = $email;
189
+    $user->azienda_id = $aziendaId;
190
+    $user->password = null;
191
+    $user->save();
192
+
193
+    $role = \Spatie\Permission\Models\Role::where('name', 'user')->first();
194
+    if ($role) {
195
+      $user->syncRoles([$role->name]);
196
+    }
197
+
198
+    return $user;
199
+  }
200
+
201
+  /**
202
+   * @return array{0:string,1:?string}
203
+   */
204
+  protected static function splitDisplayName(?string $displayName, string $email): array
205
+  {
206
+    $displayName = trim((string) $displayName);
207
+    if ($displayName === '' || strcasecmp($displayName, $email) === 0 || str_contains($displayName, '@')) {
208
+      $local = strstr($email, '@', true) ?: $email;
209
+      $displayName = trim(str_replace(['.', '_', '-'], ' ', $local));
210
+    }
211
+
212
+    $displayName = trim(preg_replace('/\s+/', ' ', $displayName) ?? '');
213
+    if ($displayName === '') {
214
+      return ['Contatto', null];
215
+    }
216
+
217
+    $parts = explode(' ', $displayName, 2);
218
+
219
+    return [$parts[0], $parts[1] ?? null];
220
+  }
221
+
126 222
   /**
127 223
    * Evita conflitti se una colonna/attributo omonimo maschera la relazione.
128 224
    */

+ 46
- 5
app/Services/FatturaFicService.php 查看文件

@@ -159,24 +159,28 @@ class FatturaFicService
159 159
         $netto = round($netto, 2);
160 160
         $pct = $ivaPercentuale;
161 161
         $lordo = round($netto + round($netto * $pct / 100, 2), 2);
162
-        $scadenza = $fattura->data_scadenza
163
-            ? Carbon::parse($fattura->data_scadenza)->toDateString()
164
-            : Carbon::parse($data)->addDays(30)->toDateString();
162
+        $pagamento = $this->pagamento($fattura, $data);
165 163
 
166 164
         $payload = [
167 165
             'type' => 'invoice',
168 166
             'entity' => $entity,
169 167
             'date' => $data,
170 168
             'items_list' => $items,
169
+            'payment_method' => ['id' => $pagamento['method_id']],
170
+            'show_payment_method' => true,
171 171
             'payments_list' => [[
172 172
                 'amount' => $lordo,
173
-                'due_date' => $scadenza,
173
+                'due_date' => $pagamento['due_date'],
174 174
                 'status' => 'not_paid',
175
+                'payment_terms' => [
176
+                    'days' => $pagamento['days'],
177
+                    'type' => $pagamento['type'],
178
+                ],
175 179
             ]],
176 180
             'e_invoice' => true,
177 181
             'ei_data' => [
178 182
                 'vat_kind' => 'I',
179
-                'payment_method' => 'MP05',
183
+                'payment_method' => $pagamento['ei_payment_method'],
180 184
             ],
181 185
         ];
182 186
 
@@ -188,6 +192,43 @@ class FatturaFicService
188 192
         return $payload;
189 193
     }
190 194
 
195
+    /**
196
+     * Metodo e termini scelti in configurazione, quelli salvati sulla fattura in generazione se ci sono.
197
+     *
198
+     * @return array{method_id: int, ei_payment_method: string, days: int, type: string, due_date: string}
199
+     */
200
+    protected function pagamento(Fattura $fattura, string $data): array
201
+    {
202
+        $meta = is_array($fattura->meta) ? $fattura->meta : [];
203
+        $metodo = Config::metodoPagamentoFic((int) ($meta['fic_payment_method_id'] ?? 0))
204
+            ?: Config::metodoPagamentoFicDefault();
205
+        if (! $metodo) {
206
+            throw new RuntimeException('Scegli il metodo di pagamento predefinito nella configurazione Fatture in Cloud.');
207
+        }
208
+        $ei = trim((string) ($metodo['ei_payment_method'] ?? ''));
209
+        if ($ei === '') {
210
+            $nome = trim((string) ($metodo['name'] ?? ''));
211
+            throw new RuntimeException('Il metodo'.($nome !== '' ? ' «'.$nome.'»' : '').' non ha la modalità di pagamento elettronica. Impostala su Fatture in Cloud e aggiorna l\'elenco.');
212
+        }
213
+
214
+        $salvati = is_array($meta['fic_payment_terms'] ?? null) ? $meta['fic_payment_terms'] : null;
215
+        $termini = $salvati
216
+            ? Config::parseTermini(((int) ($salvati['days'] ?? 30)).'|'.($salvati['type'] ?? 'standard'))
217
+            : Config::terminiPagamentoFic();
218
+
219
+        $scadenza = $fattura->data_scadenza
220
+            ? Carbon::parse($fattura->data_scadenza)->toDateString()
221
+            : Config::dataScadenza($data, $termini);
222
+
223
+        return [
224
+            'method_id' => (int) $metodo['id'],
225
+            'ei_payment_method' => $ei,
226
+            'days' => (int) $termini['days'],
227
+            'type' => (string) $termini['type'],
228
+            'due_date' => $scadenza,
229
+        ];
230
+    }
231
+
191 232
     /**
192 233
      * @return array<string, mixed>
193 234
      */

+ 11
- 2
app/Services/FatturazioneGeneraService.php 查看文件

@@ -139,13 +139,17 @@ class FatturazioneGeneraService
139 139
                 $voci = $this->accorpaVociSegnalazioni($voci);
140 140
             }
141 141
 
142
+            $dataFattura = Carbon::today()->toDateString();
143
+            $termini = Config::terminiPagamentoFic();
144
+            $metodo = Config::metodoPagamentoFicDefault();
145
+
142 146
             $fattura = new Fattura();
143 147
             $fattura->user_id = $user->id;
144 148
             $fattura->created_by_id = $author->id;
145 149
             $fattura->numero = Fattura::nextNumero();
146 150
             $fattura->stato = Fattura::STATO_BOZZA;
147
-            $fattura->data = Carbon::today()->toDateString();
148
-            $fattura->data_scadenza = Carbon::today()->addDays(30)->toDateString();
151
+            $fattura->data = $dataFattura;
152
+            $fattura->data_scadenza = Config::dataScadenza($dataFattura, $termini);
149 153
             $fattura->periodo_da = $from->toDateString();
150 154
             $fattura->periodo_a = $to->toDateString();
151 155
             $fattura->oggetto = 'Periodo '.$from->format('d/m/Y').' – '.$to->format('d/m/Y');
@@ -159,6 +163,11 @@ class FatturazioneGeneraService
159 163
                 'dettaglio_ticket' => $dettaglio,
160 164
                 'accorpa_segnalazioni' => $accorpaSegnalazioni,
161 165
                 'fic_vat_id' => $aliquota['id'] ?? null,
166
+                'fic_payment_method_id' => $metodo['id'] ?? null,
167
+                'fic_payment_terms' => [
168
+                    'days' => $termini['days'],
169
+                    'type' => $termini['type'],
170
+                ],
162 171
             ];
163 172
             $fattura->save();
164 173
 

+ 10
- 0
app/Services/FattureInCloudClient.php 查看文件

@@ -414,6 +414,16 @@ class FattureInCloudClient
414 414
         return is_array($json['data'] ?? null) ? $json['data'] : [];
415 415
     }
416 416
 
417
+    /**
418
+     * @return array<int, array<string, mixed>>
419
+     */
420
+    public function paymentMethods(): array
421
+    {
422
+        $json = $this->decode($this->http()->get($this->url('/info/payment_methods')));
423
+
424
+        return is_array($json['data'] ?? null) ? $json['data'] : [];
425
+    }
426
+
417 427
     /**
418 428
      * @param  array<string, mixed>  $data
419 429
      * @return array<string, mixed>

+ 43
- 14
app/Services/SegnalazioneEmailInboxService.php 查看文件

@@ -248,14 +248,19 @@ class SegnalazioneEmailInboxService
248 248
             }
249 249
         }
250 250
 
251
+        $azienda = $fromAddress ? User::findAziendaByDominioEmail($fromAddress) : null;
252
+        $sender = $fromAddress
253
+            ? User::findOrCreateContattoFromEmail($fromAddress, $this->extractFromName($message), $azienda?->id)
254
+            : null;
255
+
251 256
         if (! $segnalazione) {
252
-            $segnalazione = $this->createSegnalazioneFromMessage($message, $subject, $fromAddress);
257
+            $segnalazione = $this->createSegnalazioneFromMessage($message, $subject, $sender, $azienda);
253 258
             $created = true;
254 259
         }
255 260
 
256 261
         $this->syncCcFromMessage($segnalazione, $message, $fromAddress);
257 262
 
258
-        $annotazione = $this->createAnnotazioneFromMessage($segnalazione, $message, $fromAddress, ! $created);
263
+        $annotazione = $this->createAnnotazioneFromMessage($segnalazione, $message, $fromAddress, $sender, ! $created);
259 264
         $this->storeAttachments($segnalazione, $annotazione, $message);
260 265
         $this->embedCidImages($annotazione);
261 266
 
@@ -333,14 +338,13 @@ class SegnalazioneEmailInboxService
333 338
         }
334 339
     }
335 340
 
336
-    protected function createSegnalazioneFromMessage(Message $message, string $subject, ?string $fromAddress): Segnalazione
337
-    {
341
+    protected function createSegnalazioneFromMessage(
342
+        Message $message,
343
+        string $subject,
344
+        ?User $user,
345
+        ?User $azienda,
346
+    ): Segnalazione {
338 347
         $titolo = $this->cleanSubjectAsTitle($subject);
339
-        $user = $fromAddress
340
-            ? User::soloUtenti()->whereRaw('LOWER(email) = ?', [strtolower($fromAddress)])->first()
341
-            : null;
342
-
343
-        $azienda = $fromAddress ? User::findAziendaByDominioEmail($fromAddress) : null;
344 348
         if ($azienda && $user && (int) $user->azienda_id !== (int) $azienda->id) {
345 349
             $user->azienda_id = $azienda->id;
346 350
             $user->save();
@@ -370,6 +374,7 @@ class SegnalazioneEmailInboxService
370 374
         Segnalazione $segnalazione,
371 375
         Message $message,
372 376
         ?string $fromAddress,
377
+        ?User $user,
373 378
         bool $stripQuotedReply = false,
374 379
     ): Annotazione {
375 380
         $html = (string) ($message->getHTMLBody() ?: '');
@@ -388,10 +393,6 @@ class SegnalazioneEmailInboxService
388 393
             $body = '<p><em>(Messaggio senza testo)</em></p>';
389 394
         }
390 395
 
391
-        $user = $fromAddress
392
-            ? User::soloUtenti()->whereRaw('LOWER(email) = ?', [strtolower($fromAddress)])->first()
393
-            : null;
394
-
395 396
         $annotazione = new Annotazione();
396 397
         $annotazione->segnalazione_id = $segnalazione->id;
397 398
         $annotazione->user_id = $user?->id;
@@ -680,8 +681,36 @@ class SegnalazioneEmailInboxService
680 681
             return null;
681 682
         }
682 683
         $first = $from->first();
684
+        if (! $first) {
685
+            return null;
686
+        }
683 687
 
684
-        return $first && ! empty($first->mail) ? (string) $first->mail : null;
688
+        $candidates = [];
689
+        if (is_object($first)) {
690
+            if (! empty($first->mail)) {
691
+                $candidates[] = (string) $first->mail;
692
+            }
693
+            $mailbox = trim((string) ($first->mailbox ?? ''));
694
+            $host = trim((string) ($first->host ?? ''));
695
+            if ($mailbox !== '' && $host !== '') {
696
+                $candidates[] = $mailbox.'@'.$host;
697
+            }
698
+            if ($mailbox !== '') {
699
+                $candidates[] = $mailbox;
700
+            }
701
+            $candidates[] = (string) $first;
702
+        } else {
703
+            $candidates[] = (string) $first;
704
+        }
705
+
706
+        foreach ($candidates as $candidate) {
707
+            $email = User::normalizeEmailAddress($candidate);
708
+            if ($email) {
709
+                return $email;
710
+            }
711
+        }
712
+
713
+        return null;
685 714
     }
686 715
 
687 716
     protected function extractFromName(Message $message): ?string

+ 52
- 0
resources/views/config/index.blade.php 查看文件

@@ -239,6 +239,58 @@ if (is_string($activeGroup)) {
239 239
               @endif
240 240
               @break
241 241
 
242
+              @case('metodi_pagamento')
243
+              @php $catalogoPagamento = Config::metodiPagamentoFic(); @endphp
244
+              <div class="table-responsive mb-3">
245
+                <table class="table table-sm align-middle mb-0">
246
+                  <thead>
247
+                    <tr>
248
+                      <th style="width: 110px;">Predefinito</th>
249
+                      <th>Metodo</th>
250
+                      <th style="width: 140px;">Fattura elettronica</th>
251
+                    </tr>
252
+                  </thead>
253
+                  <tbody>
254
+                    @forelse($catalogoPagamento['items'] as $metodo)
255
+                    <tr>
256
+                      <td>
257
+                        <input class="form-check-input" type="radio" name="fic_metodo_pagamento_default" value="{{ $metodo['id'] }}" @checked((int) ($catalogoPagamento['default_id'] ?? 0) === (int) $metodo['id']) {{ $disabled }}>
258
+                      </td>
259
+                      <td>{{ $metodo['name'] !== '' ? $metodo['name'] : 'Metodo #'.$metodo['id'] }}</td>
260
+                      <td class="text-muted">{{ $metodo['ei_payment_method'] !== '' ? $metodo['ei_payment_method'] : '—' }}</td>
261
+                    </tr>
262
+                    @empty
263
+                    <tr>
264
+                      <td colspan="3" class="text-muted">Nessun metodo salvato. Aggiorna da Fatture in Cloud.</td>
265
+                    </tr>
266
+                    @endforelse
267
+                  </tbody>
268
+                </table>
269
+              </div>
270
+              <div class="form-text mb-2">Il metodo segnato viene scritto sulla fattura quando la generi e quando la invii a Fatture in Cloud. Serve anche la modalità elettronica (MP05, MP01, …) impostata sul metodo.</div>
271
+              @if($canEdit)
272
+              <button type="submit" class="btn btn-sm btn-outline-primary" formaction="{{ route('admin.config.fic_metodi_pagamento') }}">
273
+                <i class="bx bx-cloud-download me-1"></i>Aggiorna da Fatture in Cloud
274
+              </button>
275
+              @endif
276
+              @break
277
+
278
+              @case('termini_pagamento')
279
+              @php
280
+                $termini = Config::terminiPagamentoFic();
281
+                $opzioniTermini = Config::terminiPagamentoOpzioni();
282
+                if (! isset($opzioniTermini[$termini['key']])) {
283
+                  $opzioniTermini = [$termini['key'] => $termini['days'].' giorni'] + $opzioniTermini;
284
+                }
285
+              @endphp
286
+              <select class="form-select" name="value[{{ $config->key }}]" id="{{ $fid }}-control" {{ $disabled }}>
287
+                @foreach($opzioniTermini as $chiave => $etichetta)
288
+                <option value="{{ $chiave }}" @selected($termini['key'] === $chiave)>{{ $etichetta }}</option>
289
+                @endforeach
290
+              </select>
291
+              <div class="form-text">Usati per la scadenza in generazione e, insieme al metodo, nell'invio a Fatture in Cloud. «Fine mese» conta i giorni dall'ultimo giorno del mese della fattura.</div>
292
+              @break
293
+
242 294
               @case('password')
243 295
               <input type="password" class="form-control" name="value[{{ $config->key }}]" id="{{ $fid }}-control" value="" autocomplete="new-password" placeholder="{{ $config->value ? 'Token già salvato' : '' }}" {{ $disabled }}>
244 296
               @if($config->key === Config::FIC_ACCESS_TOKEN)

+ 32
- 7
resources/views/tempo_impiegato/index.blade.php 查看文件

@@ -1,5 +1,21 @@
1 1
 <?php
2
+use App\Models\Annotazione;
3
+use App\Models\Azienda;
4
+use App\Models\User;
2 5
 use Illuminate\Support\Facades\Auth;
6
+
7
+$aziendaOptions = Azienda::query()
8
+    ->orderBy('ragione_sociale')
9
+    ->orderBy('nome')
10
+    ->get()
11
+    ->mapWithKeys(fn ($azienda) => [$azienda->id => $azienda->full_name]);
12
+$userOptions = User::query()
13
+    ->soloUtenti()
14
+    ->whereIn('id', Annotazione::query()->where('tempo_minuti', '>', 0)->select('user_id'))
15
+    ->orderBy('cognome')
16
+    ->orderBy('nome')
17
+    ->get()
18
+    ->mapWithKeys(fn ($user) => [$user->id => $user->full_name]);
3 19
 ?>
4 20
 @php
5 21
 $configData = Helper::appClasses();
@@ -56,27 +72,36 @@ $configData = Helper::appClasses();
56 72
 
57 73
 <x-datatable-filters-offcanvas id="dt_filters_tempo_impiegato" title="Filtri tempi" toggle-label="Filtri tempi">
58 74
   <p class="text-body-secondary small mb-3">
59
-    Filtra per data/ora, utente, segnalazione, minuti impiegati e note.
75
+    Filtra per periodo, azienda e utente.
60 76
   </p>
61 77
   <x-datatable-filter-date-range
62
-    label="Data/ora"
78
+    label="Periodo"
63 79
     table-id="dataTable_tempo_impiegato"
64 80
     column="0"
65 81
     start-label="Dal"
66 82
     end-label="Al"
67 83
   />
84
+  <x-datatable-filter-field
85
+    label="Azienda"
86
+    table-id="dataTable_tempo_impiegato"
87
+    column="2"
88
+    type="select"
89
+    :options="$aziendaOptions"
90
+    empty-label="Tutte"
91
+  />
68 92
   <x-datatable-filter-field
69 93
     label="Utente"
70 94
     table-id="dataTable_tempo_impiegato"
71 95
     column="1"
72
-    type="text"
73
-    placeholder="Nome, cognome o email"
96
+    type="select"
97
+    :options="$userOptions"
98
+    empty-label="Tutti"
74 99
   />
75 100
   @if(!isset($segnalazione) || $segnalazione == null)
76 101
   <x-datatable-filter-field
77 102
     label="Segnalazione"
78 103
     table-id="dataTable_tempo_impiegato"
79
-    column="2"
104
+    column="3"
80 105
     type="text"
81 106
     placeholder="Titolo o numero segnalazione"
82 107
   />
@@ -84,7 +109,7 @@ $configData = Helper::appClasses();
84 109
   <x-datatable-filter-field
85 110
     label="Tempo (minuti)"
86 111
     table-id="dataTable_tempo_impiegato"
87
-    column="3"
112
+    column="4"
88 113
     type="text"
89 114
     placeholder="Es. 90"
90 115
     inputmode="numeric"
@@ -92,7 +117,7 @@ $configData = Helper::appClasses();
92 117
   <x-datatable-filter-field
93 118
     label="Note"
94 119
     table-id="dataTable_tempo_impiegato"
95
-    column="4"
120
+    column="7"
96 121
     type="text"
97 122
     placeholder="Testo nelle note"
98 123
   />

+ 1
- 0
routes/web.php 查看文件

@@ -201,6 +201,7 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session')])->prefix('a
201 201
     Route::get('config', [ConfigController::class, 'admin_index'])->name('admin.config.index');
202 202
     Route::post('config', [ConfigController::class, 'admin_save'])->name('admin.config.save');
203 203
     Route::post('config/fic-aliquote', [ConfigController::class, 'admin_sync_aliquote'])->name('admin.config.fic_aliquote');
204
+    Route::post('config/fic-metodi-pagamento', [ConfigController::class, 'admin_sync_metodi_pagamento'])->name('admin.config.fic_metodi_pagamento');
204 205
     Route::post('config/telegram', [ConfigController::class, 'admin_telegram_webhook'])->name('admin.config.telegram');
205 206
 
206 207
     // Configura Segnalazione

Loading…
取消
儲存