diff --git a/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php b/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php index 45f489c..2f5168d 100644 --- a/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreInvoiceRequest.php @@ -22,7 +22,7 @@ class StoreInvoiceRequest extends FormRequest public function rules(): array { return [ - 'client_id' => 'required|exists:clients,id', + 'client_id' => 'nullable|exists:clients,id', 'group_id' => 'nullable|exists:client_groups,id', 'source_quote_id' => 'nullable|exists:quotes,id', 'status' => 'required|in:brouillon,emise,envoyee,partiellement_payee,payee,echue,annulee,avoir', @@ -49,10 +49,31 @@ class StoreInvoiceRequest extends FormRequest ]; } + public function withValidator($validator): void + { + $validator->after(function ($validator) { + $hasClient = filled($this->input('client_id')); + $hasGroup = filled($this->input('group_id')); + + if (! $hasClient && ! $hasGroup) { + $message = 'Un client ou un groupe de clients est obligatoire.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + + if ($hasClient && $hasGroup) { + $message = 'Selectionnez soit un client, soit un groupe de clients.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + }); + } + public function messages(): array { return [ - 'client_id.required' => 'Le client est obligatoire.', 'client_id.exists' => 'Le client sélectionné est invalide.', 'group_id.exists' => 'Le groupe sélectionné est invalide.', 'status.required' => 'Le statut est obligatoire.', diff --git a/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php b/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php index 6bba608..a996b5a 100644 --- a/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateInvoiceRequest.php @@ -24,7 +24,7 @@ class UpdateInvoiceRequest extends FormRequest $invoiceId = $this->route('invoice'); return [ - 'client_id' => 'sometimes|exists:clients,id', + 'client_id' => 'nullable|exists:clients,id', 'group_id' => 'nullable|exists:client_groups,id', 'source_quote_id' => 'nullable|exists:quotes,id', 'invoice_number' => 'sometimes|string|max:191|unique:invoices,invoice_number,' . $invoiceId, @@ -64,4 +64,30 @@ class UpdateInvoiceRequest extends FormRequest 'e_invoice_status.in' => 'Le statut de facturation électronique est invalide.', ]; } + + public function withValidator($validator): void + { + $validator->after(function ($validator) { + if (! $this->hasAny(['client_id', 'group_id'])) { + return; + } + + $hasClient = filled($this->input('client_id')); + $hasGroup = filled($this->input('group_id')); + + if (! $hasClient && ! $hasGroup) { + $message = 'Un client ou un groupe de clients est obligatoire.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + + if ($hasClient && $hasGroup) { + $message = 'Selectionnez soit un client, soit un groupe de clients.'; + + $validator->errors()->add('client_id', $message); + $validator->errors()->add('group_id', $message); + } + }); + } } diff --git a/thanasoft-back/app/Repositories/InvoiceRepository.php b/thanasoft-back/app/Repositories/InvoiceRepository.php index b6c1539..80ed82e 100644 --- a/thanasoft-back/app/Repositories/InvoiceRepository.php +++ b/thanasoft-back/app/Repositories/InvoiceRepository.php @@ -25,12 +25,17 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter { return DB::transaction(function () use ($quoteId) { // Resolve Quote directly to avoid circular dependency - $quote = Quote::with(['client', 'lines'])->find($quoteId); + $quote = Quote::with(['client', 'group', 'lines'])->find($quoteId); if (!$quote) { throw new \Exception("Quote not found"); } + $existingInvoice = Invoice::where('source_quote_id', $quote->id)->first(); + if ($existingInvoice) { + return $existingInvoice; + } + // Create Invoice $invoiceData = [ 'client_id' => $quote->client_id, @@ -68,17 +73,19 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter $this->recordHistory($invoice->id, null, 'brouillon', 'Created from Quote ' . $quote->reference); try { - $this->timelineRepository->logActivity([ - 'client_id' => $invoice->client_id, - 'actor_type' => 'user', - 'actor_user_id' => auth()->id(), - 'event_type' => 'invoice_created', - 'entity_type' => 'invoice', - 'entity_id' => $invoice->id, - 'title' => 'Nouvelle facture créée', - 'description' => "Une facture a été créée à partir du devis #{$quote->id}.", - 'created_at' => now(), - ]); + if ($invoice->client_id !== null) { + $this->timelineRepository->logActivity([ + 'client_id' => $invoice->client_id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'invoice_created', + 'entity_type' => 'invoice', + 'entity_id' => $invoice->id, + 'title' => 'Nouvelle facture créée', + 'description' => "Une facture a été créée à partir du devis #{$quote->id}.", + 'created_at' => now(), + ]); + } } catch (\Exception $e) { Log::error("Failed to log invoice creation activity: " . $e->getMessage()); } @@ -91,7 +98,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter public function all(array $columns = ['*']): \Illuminate\Support\Collection { - return $this->model->with(['client', 'lines.product'])->get($columns); + return $this->model->with(['client', 'group', 'lines.product'])->get($columns); } public function create(array $data): Invoice @@ -113,17 +120,19 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter $this->recordHistory($invoice->id, null, $invoice->status, 'Invoice created'); try { - $this->timelineRepository->logActivity([ - 'client_id' => $invoice->client_id, - 'actor_type' => 'user', - 'actor_user_id' => auth()->id(), - 'event_type' => 'invoice_created', - 'entity_type' => 'invoice', - 'entity_id' => $invoice->id, - 'title' => 'Nouvelle facture créée', - 'description' => "La facture #{$invoice->id} a été créée.", - 'created_at' => now(), - ]); + if ($invoice->client_id !== null) { + $this->timelineRepository->logActivity([ + 'client_id' => $invoice->client_id, + 'actor_type' => 'user', + 'actor_user_id' => auth()->id(), + 'event_type' => 'invoice_created', + 'entity_type' => 'invoice', + 'entity_id' => $invoice->id, + 'title' => 'Nouvelle facture créée', + 'description' => "La facture #{$invoice->id} a été créée.", + 'created_at' => now(), + ]); + } } catch (\Exception $e) { Log::error("Failed to log manual invoice creation activity: " . $e->getMessage()); } @@ -176,7 +185,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter public function find(int|string $id, array $columns = ['*']): ?Invoice { - return $this->model->with(['client', 'lines.product', 'history.user'])->find($id, $columns); + return $this->model->with(['client', 'group', 'lines.product', 'history.user'])->find($id, $columns); } private function recordHistory(int $invoiceId, ?string $oldStatus, string $newStatus, ?string $comment = null): void diff --git a/thanasoft-back/app/Repositories/QuoteRepository.php b/thanasoft-back/app/Repositories/QuoteRepository.php index c1544cc..9fec950 100644 --- a/thanasoft-back/app/Repositories/QuoteRepository.php +++ b/thanasoft-back/app/Repositories/QuoteRepository.php @@ -95,18 +95,10 @@ class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface // Auto-create invoice when status changes to 'accepte' if ($newStatus === 'accepte' && $oldStatus !== 'accepte') { - try { - $this->invoiceRepository->createFromQuote($id); - Log::info('Invoice auto-created from quote', ['quote_id' => $id]); - } catch (\Exception $e) { - Log::error('Failed to auto-create invoice from quote: ' . $e->getMessage(), [ - 'quote_id' => $id, - 'exception' => $e, - ]); - // Don't throw - quote update should still succeed - } + $this->invoiceRepository->createFromQuote($id); + Log::info('Invoice auto-created from quote', ['quote_id' => $id]); } - } + } } return $updated; diff --git a/thanasoft-back/database/migrations/2026_04_02_151000_make_invoice_client_nullable.php b/thanasoft-back/database/migrations/2026_04_02_151000_make_invoice_client_nullable.php new file mode 100644 index 0000000..8e25eb4 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_04_02_151000_make_invoice_client_nullable.php @@ -0,0 +1,28 @@ +unsignedBigInteger('client_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->unsignedBigInteger('client_id')->nullable(false)->change(); + }); + } +}; diff --git a/thanasoft-front/src/components/Organism/Invoice/InvoiceDetailPresentation.vue b/thanasoft-front/src/components/Organism/Invoice/InvoiceDetailPresentation.vue index d692b54..dd269c6 100644 --- a/thanasoft-front/src/components/Organism/Invoice/InvoiceDetailPresentation.vue +++ b/thanasoft-front/src/components/Organism/Invoice/InvoiceDetailPresentation.vue @@ -56,7 +56,7 @@
{{ invoice.lines?.length || 0 }} ligne(s) dans cette facture. @@ -109,18 +109,18 @@ >