|
|
@@ -13,34 +13,26 @@ use Throwable;
|
|
13
|
13
|
class ImportaRedmine extends Command
|
|
14
|
14
|
{
|
|
15
|
15
|
protected $signature = 'redmine:import
|
|
16
|
|
- {project? : ID o identifier del progetto Redmine}
|
|
17
|
|
- {azienda? : ID, ragione sociale o email dell\'azienda locale}
|
|
|
16
|
+ {project? : ID o identifier del progetto Redmine (se omesso, importa tutti i progetti)}
|
|
|
17
|
+ {azienda? : ID, ragione sociale o email dell\'azienda locale (se omesso, viene creata/riusata dal progetto)}
|
|
18
|
18
|
{--list-projects : Elenca i progetti visibili in Redmine}
|
|
19
|
19
|
{--url= : URL Redmine (override REDMINE_URL)}
|
|
20
|
20
|
{--api-key= : API key (override REDMINE_API_KEY)}
|
|
21
|
21
|
{--dry-run : Mostra cosa verrebbe importato, senza scrivere}
|
|
22
|
22
|
{--update : Aggiorna segnalazioni già importate da questo progetto}
|
|
23
|
|
- {--con-sottoprogetti : Include anche i sotto-progetti}
|
|
|
23
|
+ {--con-sottoprogetti : Include anche i sotto-progetti (solo import di un singolo progetto)}
|
|
24
|
24
|
{--no-attachments : Non scaricare gli allegati}
|
|
25
|
25
|
{--no-tempi : Non importare i time entry}
|
|
26
|
26
|
{--crea-utenti : Crea anche gli autori di journal/tempi se non esistono (contatti e assegnatari vengono sempre creati)}
|
|
27
|
27
|
{--user-fallback= : ID utente locale se non c\'è match}
|
|
28
|
28
|
{--tariffa= : €/ora per calcolare il costo dai tempi}
|
|
29
|
29
|
{--contratto= : ID del contratto servizio da assegnare alle segnalazioni}
|
|
30
|
|
- {--limit= : Importa al massimo N issue}';
|
|
|
30
|
+ {--limit= : Importa al massimo N issue (per progetto)}';
|
|
31
|
31
|
|
|
32
|
|
- protected $description = 'Importa issue, tempi, costi e allegati da un progetto Redmine 6.x verso un\'azienda locale';
|
|
|
32
|
+ protected $description = 'Importa issue, tempi, costi e allegati da Redmine 6.x. Senza progetto importa tutti i progetti e crea le aziende mancanti.';
|
|
33
|
33
|
|
|
34
|
34
|
public function handle(): int
|
|
35
|
35
|
{
|
|
36
|
|
- if (! $this->option('list-projects') && (! $this->argument('project') || ! $this->argument('azienda'))) {
|
|
37
|
|
- $this->error('Specifica progetto Redmine e azienda locale, oppure usa --list-projects.');
|
|
38
|
|
- $this->line('Esempio: php artisan redmine:import NOME_PROGETTO 12');
|
|
39
|
|
- $this->line(' php artisan redmine:import 42 "ElephanTech" --dry-run');
|
|
40
|
|
-
|
|
41
|
|
- return self::INVALID;
|
|
42
|
|
- }
|
|
43
|
|
-
|
|
44
|
36
|
try {
|
|
45
|
37
|
$client = RedmineClient::fromConfig(
|
|
46
|
38
|
$this->option('url') ?: null,
|
|
|
@@ -59,9 +51,20 @@ class ImportaRedmine extends Command
|
|
59
|
51
|
$projectArg = $this->argument('project');
|
|
60
|
52
|
$aziendaArg = $this->argument('azienda');
|
|
61
|
53
|
|
|
|
54
|
+ if ($projectArg) {
|
|
|
55
|
+ return $this->importSingleProject($client, (string) $projectArg, $aziendaArg ? (string) $aziendaArg : null);
|
|
|
56
|
+ }
|
|
|
57
|
+
|
|
|
58
|
+ if ($aziendaArg) {
|
|
|
59
|
+ $this->warn('Senza progetto l\'azienda indicata viene ignorata: ogni progetto Redmine ha la propria azienda.');
|
|
|
60
|
+ }
|
|
|
61
|
+
|
|
|
62
|
+ return $this->importAllProjects($client);
|
|
|
63
|
+ }
|
|
|
64
|
+
|
|
|
65
|
+ protected function importSingleProject(RedmineClient $client, string $projectArg, ?string $aziendaArg): int
|
|
|
66
|
+ {
|
|
62
|
67
|
try {
|
|
63
|
|
- $azienda = $this->resolveAzienda((string) $aziendaArg);
|
|
64
|
|
- $contratto = $this->resolveContratto($this->option('contratto'), $azienda);
|
|
65
|
68
|
$project = $client->project($projectArg);
|
|
66
|
69
|
} catch (Throwable $e) {
|
|
67
|
70
|
$this->error($e->getMessage());
|
|
|
@@ -75,24 +78,185 @@ class ImportaRedmine extends Command
|
|
75
|
78
|
return self::FAILURE;
|
|
76
|
79
|
}
|
|
77
|
80
|
|
|
78
|
|
- $this->info('Redmine: '.$client->baseUrl());
|
|
79
|
|
- $this->info('Progetto: #'.$project['id'].' '.$project['name'].' ('.$project['identifier'].')');
|
|
80
|
|
- $this->info('Azienda: #'.$azienda->id.' '.($azienda->ragione_sociale ?: $azienda->nome));
|
|
81
|
|
- if ($contratto) {
|
|
82
|
|
- $this->info('Contratto: #'.$contratto->id.' '.$contratto->label);
|
|
|
81
|
+ try {
|
|
|
82
|
+ $azienda = $aziendaArg !== null
|
|
|
83
|
+ ? $this->resolveAzienda($aziendaArg)
|
|
|
84
|
+ : $this->matchAziendaForProject(
|
|
|
85
|
+ trim((string) ($project['identifier'] ?? '')),
|
|
|
86
|
+ trim((string) ($project['name'] ?? $project['identifier'] ?? '')),
|
|
|
87
|
+ );
|
|
|
88
|
+ $contratto = ($aziendaArg !== null && $azienda)
|
|
|
89
|
+ ? $this->resolveContratto($this->option('contratto'), $azienda)
|
|
|
90
|
+ : null;
|
|
|
91
|
+ } catch (Throwable $e) {
|
|
|
92
|
+ $this->error($e->getMessage());
|
|
|
93
|
+
|
|
|
94
|
+ return self::FAILURE;
|
|
|
95
|
+ }
|
|
|
96
|
+
|
|
|
97
|
+ if ($aziendaArg === null && $this->option('contratto')) {
|
|
|
98
|
+ $this->warn('--contratto è ignorato se l\'azienda non è indicata esplicitamente.');
|
|
|
99
|
+ }
|
|
|
100
|
+
|
|
|
101
|
+ $this->printHeader($client, $project, $azienda, $contratto);
|
|
|
102
|
+
|
|
|
103
|
+ if (! $this->option('no-interaction') && ! $this->confirm('Procedo con l\'import?', true)) {
|
|
|
104
|
+ $this->comment('Annullato.');
|
|
|
105
|
+
|
|
|
106
|
+ return self::SUCCESS;
|
|
|
107
|
+ }
|
|
|
108
|
+
|
|
|
109
|
+ if ($azienda === null) {
|
|
|
110
|
+ try {
|
|
|
111
|
+ $azienda = $this->findOrCreateAzienda($project);
|
|
|
112
|
+ } catch (Throwable $e) {
|
|
|
113
|
+ $this->error($e->getMessage());
|
|
|
114
|
+
|
|
|
115
|
+ return self::FAILURE;
|
|
|
116
|
+ }
|
|
|
117
|
+ } else {
|
|
|
118
|
+ $this->rememberRedmineProject($azienda, $project);
|
|
|
119
|
+ }
|
|
|
120
|
+
|
|
|
121
|
+ $result = $this->runImport($client, $project, $azienda, $contratto?->id, (bool) $this->option('con-sottoprogetti'));
|
|
|
122
|
+ $this->printStats($result['stats']);
|
|
|
123
|
+
|
|
|
124
|
+ return $result['ok'] ? self::SUCCESS : self::FAILURE;
|
|
|
125
|
+ }
|
|
|
126
|
+
|
|
|
127
|
+ protected function importAllProjects(RedmineClient $client): int
|
|
|
128
|
+ {
|
|
|
129
|
+ try {
|
|
|
130
|
+ $projects = $client->projects();
|
|
|
131
|
+ } catch (Throwable $e) {
|
|
|
132
|
+ $this->error($e->getMessage());
|
|
|
133
|
+
|
|
|
134
|
+ return self::FAILURE;
|
|
83
|
135
|
}
|
|
|
136
|
+
|
|
|
137
|
+ if ($projects === []) {
|
|
|
138
|
+ $this->warn('Nessun progetto visibile su '.$client->baseUrl());
|
|
|
139
|
+
|
|
|
140
|
+ return self::SUCCESS;
|
|
|
141
|
+ }
|
|
|
142
|
+
|
|
|
143
|
+ if ($this->option('con-sottoprogetti')) {
|
|
|
144
|
+ $this->warn('--con-sottoprogetti è ignorato nell\'import di tutti i progetti: ogni progetto (anche i sotto-progetti) viene importato nella propria azienda.');
|
|
|
145
|
+ }
|
|
|
146
|
+ if ($this->option('contratto')) {
|
|
|
147
|
+ $this->warn('--contratto è ignorato nell\'import di tutti i progetti.');
|
|
|
148
|
+ }
|
|
|
149
|
+
|
|
|
150
|
+ $preview = [];
|
|
|
151
|
+ $jobs = [];
|
|
|
152
|
+ foreach ($projects as $project) {
|
|
|
153
|
+ if (empty($project['id'])) {
|
|
|
154
|
+ continue;
|
|
|
155
|
+ }
|
|
|
156
|
+ $identifier = trim((string) ($project['identifier'] ?? ''));
|
|
|
157
|
+ $name = trim((string) ($project['name'] ?? $identifier));
|
|
|
158
|
+ try {
|
|
|
159
|
+ $azienda = $this->matchAziendaForProject($identifier, $name);
|
|
|
160
|
+ } catch (Throwable $e) {
|
|
|
161
|
+ $this->error('#'.($project['id'] ?? '?').' '.($project['name'] ?? '').': '.$e->getMessage());
|
|
|
162
|
+
|
|
|
163
|
+ return self::FAILURE;
|
|
|
164
|
+ }
|
|
|
165
|
+ $jobs[] = ['project' => $project, 'azienda' => $azienda];
|
|
|
166
|
+ $preview[] = [
|
|
|
167
|
+ $project['id'] ?? '',
|
|
|
168
|
+ $project['identifier'] ?? '',
|
|
|
169
|
+ $project['name'] ?? '',
|
|
|
170
|
+ $azienda?->id ? '#'.$azienda->id : '—',
|
|
|
171
|
+ $azienda ? ($azienda->ragione_sociale ?: $azienda->nome) : $name,
|
|
|
172
|
+ $azienda ? 'esistente' : 'da creare',
|
|
|
173
|
+ ];
|
|
|
174
|
+ }
|
|
|
175
|
+
|
|
|
176
|
+ $this->info('Redmine: '.$client->baseUrl());
|
|
|
177
|
+ $this->info('Progetti da importare: '.count($jobs));
|
|
84
|
178
|
if ($this->option('dry-run')) {
|
|
85
|
179
|
$this->warn('Modalità dry-run: nessuna scrittura sul database.');
|
|
86
|
180
|
}
|
|
|
181
|
+ $this->table(['ID', 'Identifier', 'Progetto', 'Azienda ID', 'Azienda', 'Stato'], $preview);
|
|
87
|
182
|
|
|
88
|
|
- if (! $this->option('no-interaction') && ! $this->confirm('Procedo con l\'import?', true)) {
|
|
|
183
|
+ if (! $this->option('no-interaction') && ! $this->confirm('Procedo con l\'import di '.count($jobs).' progetti?', true)) {
|
|
89
|
184
|
$this->comment('Annullato.');
|
|
90
|
185
|
|
|
91
|
186
|
return self::SUCCESS;
|
|
92
|
187
|
}
|
|
93
|
188
|
|
|
|
189
|
+ $totals = $this->emptyStats();
|
|
|
190
|
+ $hadErrors = false;
|
|
|
191
|
+ $projectRows = [];
|
|
|
192
|
+
|
|
|
193
|
+ foreach ($jobs as $index => $job) {
|
|
|
194
|
+ $project = $job['project'];
|
|
|
195
|
+ try {
|
|
|
196
|
+ $azienda = $job['azienda'] ?? $this->findOrCreateAzienda($project);
|
|
|
197
|
+ if ($job['azienda']) {
|
|
|
198
|
+ $this->rememberRedmineProject($azienda, $project);
|
|
|
199
|
+ }
|
|
|
200
|
+ } catch (Throwable $e) {
|
|
|
201
|
+ $this->error($e->getMessage());
|
|
|
202
|
+ $hadErrors = true;
|
|
|
203
|
+ $projectRows[] = [
|
|
|
204
|
+ $project['identifier'] ?? $project['id'],
|
|
|
205
|
+ 0, 0, 0, 0, 'errore azienda',
|
|
|
206
|
+ ];
|
|
|
207
|
+ continue;
|
|
|
208
|
+ }
|
|
|
209
|
+ $this->newLine();
|
|
|
210
|
+ $this->info(sprintf(
|
|
|
211
|
+ '[%d/%d] #%s %s → azienda #%s %s',
|
|
|
212
|
+ $index + 1,
|
|
|
213
|
+ count($jobs),
|
|
|
214
|
+ $project['id'],
|
|
|
215
|
+ $project['name'] ?? $project['identifier'],
|
|
|
216
|
+ $azienda->id ?: '—',
|
|
|
217
|
+ $azienda->ragione_sociale ?: $azienda->nome,
|
|
|
218
|
+ ));
|
|
|
219
|
+
|
|
|
220
|
+ $result = $this->runImport($client, $project, $azienda, null, false);
|
|
|
221
|
+ $stats = $result['stats'];
|
|
|
222
|
+ $this->mergeStats($totals, $stats);
|
|
|
223
|
+ if (! $result['ok']) {
|
|
|
224
|
+ $hadErrors = true;
|
|
|
225
|
+ }
|
|
|
226
|
+ $projectRows[] = [
|
|
|
227
|
+ $project['identifier'] ?? $project['id'],
|
|
|
228
|
+ $stats['issues'],
|
|
|
229
|
+ $stats['created'],
|
|
|
230
|
+ $stats['updated'],
|
|
|
231
|
+ $stats['skipped'],
|
|
|
232
|
+ $stats['errors'] === [] ? 'ok' : count($stats['errors']).' errori',
|
|
|
233
|
+ ];
|
|
|
234
|
+ }
|
|
|
235
|
+
|
|
|
236
|
+ $this->newLine();
|
|
|
237
|
+ $this->info('Riepilogo per progetto');
|
|
|
238
|
+ $this->table(['Progetto', 'Issue', 'Create', 'Aggiornate', 'Saltate', 'Esito'], $projectRows);
|
|
|
239
|
+ $this->info('Totale');
|
|
|
240
|
+ $this->printStats($totals);
|
|
|
241
|
+
|
|
|
242
|
+ return $hadErrors || $totals['errors'] !== [] ? self::FAILURE : self::SUCCESS;
|
|
|
243
|
+ }
|
|
|
244
|
+
|
|
|
245
|
+ /**
|
|
|
246
|
+ * @param array<string, mixed> $project
|
|
|
247
|
+ * @return array{ok: bool, stats: array<string, mixed>}
|
|
|
248
|
+ */
|
|
|
249
|
+ protected function runImport(
|
|
|
250
|
+ RedmineClient $client,
|
|
|
251
|
+ array $project,
|
|
|
252
|
+ Azienda $azienda,
|
|
|
253
|
+ ?int $contrattoServizioId,
|
|
|
254
|
+ bool $includeSubprojects,
|
|
|
255
|
+ ): array {
|
|
94
|
256
|
$tariffa = $this->option('tariffa');
|
|
95
|
257
|
$limit = $this->option('limit');
|
|
|
258
|
+ $empty = $this->emptyStats();
|
|
|
259
|
+ $empty['project'] = $project;
|
|
96
|
260
|
|
|
97
|
261
|
$service = new RedmineImportService(
|
|
98
|
262
|
client: $client,
|
|
|
@@ -104,7 +268,7 @@ class ImportaRedmine extends Command
|
|
104
|
268
|
creaUtenti: (bool) $this->option('crea-utenti'),
|
|
105
|
269
|
hourlyRate: $tariffa !== null && $tariffa !== '' ? (float) $tariffa : null,
|
|
106
|
270
|
userFallbackId: $this->option('user-fallback') ? (int) $this->option('user-fallback') : null,
|
|
107
|
|
- contrattoServizioId: $contratto?->id,
|
|
|
271
|
+ contrattoServizioId: $contrattoServizioId,
|
|
108
|
272
|
onProgress: function (int $current, int $total, int $issueId) {
|
|
109
|
273
|
if ($current === 1) {
|
|
110
|
274
|
$this->output->progressStart($total);
|
|
|
@@ -119,7 +283,7 @@ class ImportaRedmine extends Command
|
|
119
|
283
|
try {
|
|
120
|
284
|
$stats = $service->import(
|
|
121
|
285
|
$project['id'],
|
|
122
|
|
- (bool) $this->option('con-sottoprogetti'),
|
|
|
286
|
+ $includeSubprojects,
|
|
123
|
287
|
$limit !== null && $limit !== '' ? (int) $limit : null,
|
|
124
|
288
|
);
|
|
125
|
289
|
} catch (Throwable $e) {
|
|
|
@@ -127,39 +291,215 @@ class ImportaRedmine extends Command
|
|
127
|
291
|
$this->error($e->getTraceAsString());
|
|
128
|
292
|
}
|
|
129
|
293
|
$this->error($e->getMessage());
|
|
|
294
|
+ $empty['errors'][] = $e->getMessage();
|
|
130
|
295
|
|
|
131
|
|
- return self::FAILURE;
|
|
|
296
|
+ return ['ok' => false, 'stats' => $empty];
|
|
|
297
|
+ }
|
|
|
298
|
+
|
|
|
299
|
+ return ['ok' => $stats['errors'] === [], 'stats' => $stats];
|
|
|
300
|
+ }
|
|
|
301
|
+
|
|
|
302
|
+ /**
|
|
|
303
|
+ * @param array<string, mixed> $project
|
|
|
304
|
+ */
|
|
|
305
|
+ protected function findOrCreateAzienda(array $project): Azienda
|
|
|
306
|
+ {
|
|
|
307
|
+ $identifier = trim((string) ($project['identifier'] ?? ''));
|
|
|
308
|
+ $name = trim((string) ($project['name'] ?? $identifier));
|
|
|
309
|
+ if ($name === '') {
|
|
|
310
|
+ throw new RuntimeException('Progetto Redmine #'.($project['id'] ?? '?').' senza nome né identifier.');
|
|
132
|
311
|
}
|
|
133
|
312
|
|
|
|
313
|
+ $existing = $this->matchAziendaForProject($identifier, $name);
|
|
|
314
|
+ if ($existing) {
|
|
|
315
|
+ $this->rememberRedmineProject($existing, $project);
|
|
|
316
|
+
|
|
|
317
|
+ return $existing;
|
|
|
318
|
+ }
|
|
|
319
|
+
|
|
|
320
|
+ if ($this->option('dry-run')) {
|
|
|
321
|
+ $azienda = new Azienda();
|
|
|
322
|
+ $azienda->ragione_sociale = $name;
|
|
|
323
|
+ $azienda->nome = $name;
|
|
|
324
|
+ $azienda->note = $this->redmineNote($project);
|
|
|
325
|
+
|
|
|
326
|
+ return $azienda;
|
|
|
327
|
+ }
|
|
|
328
|
+
|
|
|
329
|
+ $azienda = new Azienda();
|
|
|
330
|
+ $azienda->ragione_sociale = $name;
|
|
|
331
|
+ $azienda->nome = $name;
|
|
|
332
|
+ $azienda->note = $this->redmineNote($project);
|
|
|
333
|
+ $azienda->save();
|
|
|
334
|
+
|
|
|
335
|
+ $this->info(' Azienda creata: #'.$azienda->id.' '.$name.' ('.$identifier.')');
|
|
|
336
|
+
|
|
|
337
|
+ return $azienda;
|
|
|
338
|
+ }
|
|
|
339
|
+
|
|
|
340
|
+ protected function matchAziendaForProject(string $identifier, string $name): ?Azienda
|
|
|
341
|
+ {
|
|
|
342
|
+ if ($identifier !== '') {
|
|
|
343
|
+ $byToken = Azienda::query()
|
|
|
344
|
+ ->where('note', 'like', '%'.$this->redmineIdentifierToken($identifier).'%')
|
|
|
345
|
+ ->first();
|
|
|
346
|
+ if ($byToken) {
|
|
|
347
|
+ return $byToken;
|
|
|
348
|
+ }
|
|
|
349
|
+ }
|
|
|
350
|
+
|
|
|
351
|
+ $matches = Azienda::query()
|
|
|
352
|
+ ->where(function ($q) use ($name, $identifier) {
|
|
|
353
|
+ $q->where('ragione_sociale', $name)
|
|
|
354
|
+ ->orWhere('nome', $name);
|
|
|
355
|
+ if ($identifier !== '' && $identifier !== $name) {
|
|
|
356
|
+ $q->orWhere('ragione_sociale', $identifier)
|
|
|
357
|
+ ->orWhere('nome', $identifier);
|
|
|
358
|
+ }
|
|
|
359
|
+ })
|
|
|
360
|
+ ->get();
|
|
|
361
|
+
|
|
|
362
|
+ if ($matches->count() === 1) {
|
|
|
363
|
+ return $matches->first();
|
|
|
364
|
+ }
|
|
|
365
|
+
|
|
|
366
|
+ if ($matches->count() > 1) {
|
|
|
367
|
+ $elenco = $matches->map(fn (Azienda $a) => '#'.$a->id.' '.($a->ragione_sociale ?: $a->nome))->implode(', ');
|
|
|
368
|
+ throw new RuntimeException('Azienda ambigua per il progetto "'.$name.'". Specifica l\'ID. Trovate: '.$elenco);
|
|
|
369
|
+ }
|
|
|
370
|
+
|
|
|
371
|
+ return null;
|
|
|
372
|
+ }
|
|
|
373
|
+
|
|
|
374
|
+ /**
|
|
|
375
|
+ * @param array<string, mixed> $project
|
|
|
376
|
+ */
|
|
|
377
|
+ protected function rememberRedmineProject(Azienda $azienda, array $project): void
|
|
|
378
|
+ {
|
|
|
379
|
+ $token = $this->redmineIdentifierToken((string) ($project['identifier'] ?? ''));
|
|
|
380
|
+ if ($token === '' || str_contains((string) $azienda->note, $token)) {
|
|
|
381
|
+ return;
|
|
|
382
|
+ }
|
|
|
383
|
+ if ($this->option('dry-run') || ! $azienda->exists) {
|
|
|
384
|
+ return;
|
|
|
385
|
+ }
|
|
|
386
|
+
|
|
|
387
|
+ $note = trim((string) $azienda->note);
|
|
|
388
|
+ $azienda->note = ($note !== '' ? $note."\n" : '').$this->redmineNote($project);
|
|
|
389
|
+ $azienda->save();
|
|
|
390
|
+ }
|
|
|
391
|
+
|
|
|
392
|
+ /**
|
|
|
393
|
+ * @param array<string, mixed> $project
|
|
|
394
|
+ */
|
|
|
395
|
+ protected function redmineNote(array $project): string
|
|
|
396
|
+ {
|
|
|
397
|
+ $identifier = (string) ($project['identifier'] ?? '');
|
|
|
398
|
+
|
|
|
399
|
+ return sprintf(
|
|
|
400
|
+ '[redmine-import] project_id=%s %s',
|
|
|
401
|
+ $project['id'] ?? '',
|
|
|
402
|
+ $this->redmineIdentifierToken($identifier),
|
|
|
403
|
+ );
|
|
|
404
|
+ }
|
|
|
405
|
+
|
|
|
406
|
+ protected function redmineIdentifierToken(string $identifier): string
|
|
|
407
|
+ {
|
|
|
408
|
+ $identifier = trim($identifier);
|
|
|
409
|
+
|
|
|
410
|
+ return $identifier === '' ? '' : 'identifier="'.$identifier.'"';
|
|
|
411
|
+ }
|
|
|
412
|
+
|
|
|
413
|
+ /**
|
|
|
414
|
+ * @param array<string, mixed> $project
|
|
|
415
|
+ */
|
|
|
416
|
+ protected function printHeader(RedmineClient $client, array $project, ?Azienda $azienda, ?ContrattoServizio $contratto): void
|
|
|
417
|
+ {
|
|
|
418
|
+ $this->info('Redmine: '.$client->baseUrl());
|
|
|
419
|
+ $this->info('Progetto: #'.$project['id'].' '.$project['name'].' ('.$project['identifier'].')');
|
|
|
420
|
+ if ($azienda) {
|
|
|
421
|
+ $this->info('Azienda: #'.$azienda->id.' '.($azienda->ragione_sociale ?: $azienda->nome));
|
|
|
422
|
+ } else {
|
|
|
423
|
+ $this->info('Azienda: verrà creata da «'.($project['name'] ?? $project['identifier']).'»');
|
|
|
424
|
+ }
|
|
|
425
|
+ if ($contratto) {
|
|
|
426
|
+ $this->info('Contratto: #'.$contratto->id.' '.$contratto->label);
|
|
|
427
|
+ }
|
|
|
428
|
+ if ($this->option('dry-run')) {
|
|
|
429
|
+ $this->warn('Modalità dry-run: nessuna scrittura sul database.');
|
|
|
430
|
+ }
|
|
|
431
|
+ }
|
|
|
432
|
+
|
|
|
433
|
+ /**
|
|
|
434
|
+ * @param array<string, mixed> $stats
|
|
|
435
|
+ */
|
|
|
436
|
+ protected function printStats(array $stats): void
|
|
|
437
|
+ {
|
|
134
|
438
|
$this->newLine();
|
|
135
|
439
|
$this->table(
|
|
136
|
440
|
['Voce', 'Valore'],
|
|
137
|
441
|
[
|
|
138
|
|
- ['Issue Redmine', $stats['issues']],
|
|
139
|
|
- ['Segnalazioni create', $stats['created']],
|
|
140
|
|
- ['Segnalazioni aggiornate', $stats['updated']],
|
|
141
|
|
- ['Segnalazioni saltate (già importate)', $stats['skipped']],
|
|
142
|
|
- ['Annotazioni', $stats['annotazioni']],
|
|
143
|
|
- ['Time entry', $stats['tempi']],
|
|
144
|
|
- ['Costi', $stats['costi']],
|
|
145
|
|
- ['Allegati', $stats['allegati']],
|
|
146
|
|
- ['Todo / checklist', $stats['todos']],
|
|
147
|
|
- ['Utenti creati', $stats['utenti_creati']],
|
|
|
442
|
+ ['Issue Redmine', $stats['issues'] ?? 0],
|
|
|
443
|
+ ['Segnalazioni create', $stats['created'] ?? 0],
|
|
|
444
|
+ ['Segnalazioni aggiornate', $stats['updated'] ?? 0],
|
|
|
445
|
+ ['Segnalazioni saltate (già importate)', $stats['skipped'] ?? 0],
|
|
|
446
|
+ ['Annotazioni', $stats['annotazioni'] ?? 0],
|
|
|
447
|
+ ['Time entry', $stats['tempi'] ?? 0],
|
|
|
448
|
+ ['Costi', $stats['costi'] ?? 0],
|
|
|
449
|
+ ['Allegati', $stats['allegati'] ?? 0],
|
|
|
450
|
+ ['Todo / checklist', $stats['todos'] ?? 0],
|
|
|
451
|
+ ['Utenti creati', $stats['utenti_creati'] ?? 0],
|
|
148
|
452
|
],
|
|
149
|
453
|
);
|
|
150
|
454
|
|
|
151
|
|
- foreach ($stats['warnings'] as $warning) {
|
|
|
455
|
+ foreach ($stats['warnings'] ?? [] as $warning) {
|
|
152
|
456
|
$this->warn($warning);
|
|
153
|
457
|
}
|
|
154
|
|
- foreach ($stats['errors'] as $error) {
|
|
|
458
|
+ foreach ($stats['errors'] ?? [] as $error) {
|
|
155
|
459
|
$this->error($error);
|
|
156
|
460
|
}
|
|
157
|
461
|
|
|
158
|
|
- if ($stats['skipped'] > 0 && ! $this->option('update')) {
|
|
|
462
|
+ if (($stats['skipped'] ?? 0) > 0 && ! $this->option('update')) {
|
|
159
|
463
|
$this->comment('Le issue già importate sono state saltate. Usa --update per riallinearle.');
|
|
160
|
464
|
}
|
|
|
465
|
+ }
|
|
|
466
|
+
|
|
|
467
|
+ /**
|
|
|
468
|
+ * @return array<string, mixed>
|
|
|
469
|
+ */
|
|
|
470
|
+ protected function emptyStats(): array
|
|
|
471
|
+ {
|
|
|
472
|
+ return [
|
|
|
473
|
+ 'issues' => 0,
|
|
|
474
|
+ 'created' => 0,
|
|
|
475
|
+ 'updated' => 0,
|
|
|
476
|
+ 'skipped' => 0,
|
|
|
477
|
+ 'annotazioni' => 0,
|
|
|
478
|
+ 'tempi' => 0,
|
|
|
479
|
+ 'costi' => 0,
|
|
|
480
|
+ 'allegati' => 0,
|
|
|
481
|
+ 'todos' => 0,
|
|
|
482
|
+ 'utenti_creati' => 0,
|
|
|
483
|
+ 'warnings' => [],
|
|
|
484
|
+ 'errors' => [],
|
|
|
485
|
+ ];
|
|
|
486
|
+ }
|
|
161
|
487
|
|
|
162
|
|
- return $stats['errors'] === [] ? self::SUCCESS : self::FAILURE;
|
|
|
488
|
+ /**
|
|
|
489
|
+ * @param array<string, mixed> $totals
|
|
|
490
|
+ * @param array<string, mixed> $stats
|
|
|
491
|
+ */
|
|
|
492
|
+ protected function mergeStats(array &$totals, array $stats): void
|
|
|
493
|
+ {
|
|
|
494
|
+ foreach (['issues', 'created', 'updated', 'skipped', 'annotazioni', 'tempi', 'costi', 'allegati', 'todos', 'utenti_creati'] as $key) {
|
|
|
495
|
+ $totals[$key] += (int) ($stats[$key] ?? 0);
|
|
|
496
|
+ }
|
|
|
497
|
+ foreach ($stats['warnings'] ?? [] as $warning) {
|
|
|
498
|
+ $totals['warnings'][] = $warning;
|
|
|
499
|
+ }
|
|
|
500
|
+ foreach ($stats['errors'] ?? [] as $error) {
|
|
|
501
|
+ $totals['errors'][] = $error;
|
|
|
502
|
+ }
|
|
163
|
503
|
}
|
|
164
|
504
|
|
|
165
|
505
|
protected function listProjects(RedmineClient $client): int
|
|
|
@@ -185,7 +525,9 @@ class ImportaRedmine extends Command
|
|
185
|
525
|
}
|
|
186
|
526
|
|
|
187
|
527
|
$this->table(['ID', 'Identifier', 'Nome', 'Stato'], $rows);
|
|
188
|
|
- $this->comment('Usa l\'ID o l\'identifier come primo argomento di redmine:import.');
|
|
|
528
|
+ $this->comment('php artisan redmine:import importa tutti i progetti e crea le aziende');
|
|
|
529
|
+ $this->comment('php artisan redmine:import IDENTIFIER importa un progetto (crea l\'azienda se manca)');
|
|
|
530
|
+ $this->comment('php artisan redmine:import IDENTIFIER 12 importa un progetto verso un\'azienda esistente');
|
|
189
|
531
|
|
|
190
|
532
|
return self::SUCCESS;
|
|
191
|
533
|
}
|