|
|
@@ -105,7 +105,7 @@ class FatturazioneGeneraService
|
|
105
|
105
|
];
|
|
106
|
106
|
}
|
|
107
|
107
|
|
|
108
|
|
- public function creaFattura(string $clienteKey, Carbon $from, Carbon $to, User $author): Fattura
|
|
|
108
|
+ public function creaFattura(string $clienteKey, Carbon $from, Carbon $to, User $author, bool $accorpaSegnalazioni = false): Fattura
|
|
109
|
109
|
{
|
|
110
|
110
|
$esito = $this->genera($from, $to);
|
|
111
|
111
|
$cliente = collect(array_merge($esito['aziende'], $esito['privati']))
|
|
|
@@ -126,7 +126,14 @@ class FatturazioneGeneraService
|
|
126
|
126
|
throw new RuntimeException('Cliente non trovato.');
|
|
127
|
127
|
}
|
|
128
|
128
|
|
|
129
|
|
- return DB::transaction(function () use ($cliente, $from, $to, $author, $user) {
|
|
|
129
|
+ return DB::transaction(function () use ($cliente, $from, $to, $author, $user, $accorpaSegnalazioni) {
|
|
|
130
|
+ $voci = $cliente['voci'];
|
|
|
131
|
+ $dettaglio = $this->dettaglioTicket($voci);
|
|
|
132
|
+ $annotazioneIds = collect($voci)->pluck('annotazione_id')->filter()->map(fn ($id) => (int) $id)->unique()->values()->all();
|
|
|
133
|
+ if ($accorpaSegnalazioni) {
|
|
|
134
|
+ $voci = $this->accorpaVociSegnalazioni($voci);
|
|
|
135
|
+ }
|
|
|
136
|
+
|
|
130
|
137
|
$fattura = new Fattura();
|
|
131
|
138
|
$fattura->user_id = $user->id;
|
|
132
|
139
|
$fattura->created_by_id = $author->id;
|
|
|
@@ -138,10 +145,15 @@ class FatturazioneGeneraService
|
|
138
|
145
|
$fattura->periodo_a = $to->toDateString();
|
|
139
|
146
|
$fattura->iva_percentuale = Config::ivaPercentualeDefault();
|
|
140
|
147
|
$fattura->snapshotDaUser($user);
|
|
|
148
|
+ $fattura->meta = [
|
|
|
149
|
+ 'annotazione_ids' => $annotazioneIds,
|
|
|
150
|
+ 'dettaglio_ticket' => $dettaglio,
|
|
|
151
|
+ 'accorpa_segnalazioni' => $accorpaSegnalazioni,
|
|
|
152
|
+ ];
|
|
141
|
153
|
$fattura->save();
|
|
142
|
154
|
|
|
143
|
155
|
$ordine = 0;
|
|
144
|
|
- foreach ($cliente['voci'] as $voce) {
|
|
|
156
|
+ foreach ($voci as $voce) {
|
|
145
|
157
|
$ordine++;
|
|
146
|
158
|
FatturaRiga::create([
|
|
147
|
159
|
'fattura_id' => $fattura->id,
|
|
|
@@ -1102,14 +1114,133 @@ class FatturazioneGeneraService
|
|
1102
|
1114
|
*/
|
|
1103
|
1115
|
protected function annotazioneIdsGiaFatturate(): array
|
|
1104
|
1116
|
{
|
|
1105
|
|
- return FatturaRiga::query()
|
|
|
1117
|
+ $daRighe = FatturaRiga::query()
|
|
1106
|
1118
|
->suFattureAttive()
|
|
1107
|
1119
|
->whereNotNull('annotazione_id')
|
|
1108
|
1120
|
->pluck('annotazione_id')
|
|
1109
|
|
- ->map(fn ($id) => (int) $id)
|
|
1110
|
|
- ->unique()
|
|
1111
|
|
- ->values()
|
|
1112
|
|
- ->all();
|
|
|
1121
|
+ ->map(fn ($id) => (int) $id);
|
|
|
1122
|
+
|
|
|
1123
|
+ $daMeta = Fattura::query()
|
|
|
1124
|
+ ->nonAnnullate()
|
|
|
1125
|
+ ->whereNotNull('meta')
|
|
|
1126
|
+ ->pluck('meta')
|
|
|
1127
|
+ ->flatMap(function ($meta) {
|
|
|
1128
|
+ $ids = is_array($meta) ? ($meta['annotazione_ids'] ?? []) : [];
|
|
|
1129
|
+
|
|
|
1130
|
+ return collect($ids)->map(fn ($id) => (int) $id);
|
|
|
1131
|
+ });
|
|
|
1132
|
+
|
|
|
1133
|
+ return $daRighe->merge($daMeta)->unique()->values()->all();
|
|
|
1134
|
+ }
|
|
|
1135
|
+
|
|
|
1136
|
+ /**
|
|
|
1137
|
+ * Una riga fattura per tipo (ore, extra, spese, uscite) al posto di una riga per ticket.
|
|
|
1138
|
+ *
|
|
|
1139
|
+ * @param array<int, array<string, mixed>> $voci
|
|
|
1140
|
+ * @return array<int, array<string, mixed>>
|
|
|
1141
|
+ */
|
|
|
1142
|
+ protected function accorpaVociSegnalazioni(array $voci): array
|
|
|
1143
|
+ {
|
|
|
1144
|
+ $accorpabili = [
|
|
|
1145
|
+ FatturaRiga::TIPO_TEMPO,
|
|
|
1146
|
+ FatturaRiga::TIPO_EXTRA_CONTRATTO,
|
|
|
1147
|
+ FatturaRiga::TIPO_EXTRA_ORDINE,
|
|
|
1148
|
+ FatturaRiga::TIPO_SPESA,
|
|
|
1149
|
+ FatturaRiga::TIPO_USCITA,
|
|
|
1150
|
+ ];
|
|
|
1151
|
+ $gruppi = [];
|
|
|
1152
|
+ $resto = [];
|
|
|
1153
|
+ foreach ($voci as $voce) {
|
|
|
1154
|
+ $tipo = (string) ($voce['tipo'] ?? '');
|
|
|
1155
|
+ if (empty($voce['segnalazione_id']) || ! in_array($tipo, $accorpabili, true)) {
|
|
|
1156
|
+ $resto[] = $voce;
|
|
|
1157
|
+ continue;
|
|
|
1158
|
+ }
|
|
|
1159
|
+ $gruppi[$tipo][] = $voce;
|
|
|
1160
|
+ }
|
|
|
1161
|
+
|
|
|
1162
|
+ foreach ($gruppi as $tipo => $items) {
|
|
|
1163
|
+ $importo = round(collect($items)->sum(fn ($v) => (float) ($v['importo'] ?? 0)), 2);
|
|
|
1164
|
+ $minuti = (int) collect($items)->sum(fn ($v) => (int) ($v['minuti'] ?? 0));
|
|
|
1165
|
+ $ore = in_array($tipo, [FatturaRiga::TIPO_TEMPO, FatturaRiga::TIPO_EXTRA_CONTRATTO, FatturaRiga::TIPO_EXTRA_ORDINE], true);
|
|
|
1166
|
+ $qty = $ore ? round($minuti / 60, 2) : (float) count($items);
|
|
|
1167
|
+ if ($qty <= 0) {
|
|
|
1168
|
+ $qty = 1;
|
|
|
1169
|
+ }
|
|
|
1170
|
+ $resto[] = [
|
|
|
1171
|
+ 'tipo' => $tipo,
|
|
|
1172
|
+ 'descrizione' => FatturaRiga::TIPI[$tipo] ?? $tipo,
|
|
|
1173
|
+ 'quantita' => $qty,
|
|
|
1174
|
+ 'unita_misura' => $ore ? 'ora' : 'forfait',
|
|
|
1175
|
+ 'prezzo_unitario' => round($importo / $qty, 2),
|
|
|
1176
|
+ 'importo' => $importo,
|
|
|
1177
|
+ 'minuti' => $minuti > 0 ? $minuti : null,
|
|
|
1178
|
+ 'annotazione_id' => null,
|
|
|
1179
|
+ 'segnalazione_id' => null,
|
|
|
1180
|
+ 'contratto_servizio_id' => null,
|
|
|
1181
|
+ 'contratto_servizio_riga_id' => null,
|
|
|
1182
|
+ 'ordine_lavoro_id' => null,
|
|
|
1183
|
+ 'prodotto_id' => null,
|
|
|
1184
|
+ 'periodo_da' => null,
|
|
|
1185
|
+ 'periodo_a' => null,
|
|
|
1186
|
+ 'nota' => null,
|
|
|
1187
|
+ ];
|
|
|
1188
|
+ }
|
|
|
1189
|
+
|
|
|
1190
|
+ return $resto;
|
|
|
1191
|
+ }
|
|
|
1192
|
+
|
|
|
1193
|
+ /**
|
|
|
1194
|
+ * @param array<int, array<string, mixed>> $voci
|
|
|
1195
|
+ * @return array<int, array{titolo: string, data: string, ore: float, uscita: ?float}>
|
|
|
1196
|
+ */
|
|
|
1197
|
+ protected function dettaglioTicket(array $voci): array
|
|
|
1198
|
+ {
|
|
|
1199
|
+ $ids = collect($voci)->pluck('annotazione_id')->filter()->map(fn ($id) => (int) $id)->unique()->values();
|
|
|
1200
|
+ if ($ids->isEmpty()) {
|
|
|
1201
|
+ return [];
|
|
|
1202
|
+ }
|
|
|
1203
|
+
|
|
|
1204
|
+ $uscite = [];
|
|
|
1205
|
+ foreach ($voci as $voce) {
|
|
|
1206
|
+ if (($voce['tipo'] ?? '') !== FatturaRiga::TIPO_USCITA || empty($voce['annotazione_id'])) {
|
|
|
1207
|
+ continue;
|
|
|
1208
|
+ }
|
|
|
1209
|
+ $id = (int) $voce['annotazione_id'];
|
|
|
1210
|
+ $uscite[$id] = round(($uscite[$id] ?? 0) + (float) ($voce['importo'] ?? 0), 2);
|
|
|
1211
|
+ }
|
|
|
1212
|
+
|
|
|
1213
|
+ $annotazioni = Annotazione::query()
|
|
|
1214
|
+ ->with('segnalazione')
|
|
|
1215
|
+ ->whereIn('id', $ids)
|
|
|
1216
|
+ ->get()
|
|
|
1217
|
+ ->keyBy('id');
|
|
|
1218
|
+
|
|
|
1219
|
+ $righe = [];
|
|
|
1220
|
+ foreach ($ids as $id) {
|
|
|
1221
|
+ $annotazione = $annotazioni->get($id);
|
|
|
1222
|
+ if (! $annotazione) {
|
|
|
1223
|
+ continue;
|
|
|
1224
|
+ }
|
|
|
1225
|
+ $quando = $annotazione->tempo_data
|
|
|
1226
|
+ ? Carbon::parse($annotazione->tempo_data)
|
|
|
1227
|
+ : Carbon::parse($annotazione->created_at);
|
|
|
1228
|
+ $ore = round(((int) $annotazione->tempo_minuti) / 60, 2);
|
|
|
1229
|
+ $uscita = $uscite[$id] ?? null;
|
|
|
1230
|
+ if ($ore <= 0 && ! $uscita) {
|
|
|
1231
|
+ continue;
|
|
|
1232
|
+ }
|
|
|
1233
|
+ $righe[] = [
|
|
|
1234
|
+ 'titolo' => $annotazione->segnalazione?->titolo ?: 'Ticket',
|
|
|
1235
|
+ 'data' => $quando->format('Y-m-d H:i'),
|
|
|
1236
|
+ 'ore' => $ore,
|
|
|
1237
|
+ 'uscita' => $uscita,
|
|
|
1238
|
+ ];
|
|
|
1239
|
+ }
|
|
|
1240
|
+
|
|
|
1241
|
+ usort($righe, fn ($a, $b) => strcmp($a['data'], $b['data']));
|
|
|
1242
|
+
|
|
|
1243
|
+ return $righe;
|
|
1113
|
1244
|
}
|
|
1114
|
1245
|
|
|
1115
|
1246
|
protected function canoneGiaFatturato(int $contrattoId, ?int $rigaId, Carbon $pDa, Carbon $pA): bool
|