feat: add sous-devis generation from commandes

From a commande, users can now create one or more sub-quotes
(sous-devis) to ventilate purchases across communes for approval.

- Add parent_devis_id and commande_id FKs on devis table
- Order model: sousDevis() hasMany relation
- Devis model: commande() and parent() relations
- DevisController: createFromCommande / storeFromCommande methods
- OrderController: load and pass sous_devis on show
- Commandes/Show: "Créer un sous-devis" button + ventilation panel
- Devis/CreateFromCommande: form pre-loaded with commande context
- Devis/Index: badge "Sous-devis · CMD-XXXX" on linked quotes
- Devis/Show: back-link to originating commande

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mrKamoo
2026-06-19 15:14:45 +02:00
co-authored by Claude Sonnet 4.6
parent 7312c7640b
commit 8d51f4de3e
12 changed files with 874 additions and 15 deletions
+88 -3
View File
@@ -12,9 +12,9 @@ class DevisController extends Controller
{
public function index()
{
$devis = Devis::latest()->get();
$devis = Devis::with(['sousDevis', 'parent'])->latest()->get();
return Inertia::render('Devis/Index', [
'devis' => $devis
'devis' => $devis,
]);
}
@@ -68,12 +68,97 @@ class DevisController extends Controller
'adresse' => "Béziers, France",
]);
$devi->load(['sousDevis', 'parent']);
return Inertia::render('Devis/Show', [
'devis' => $devi,
'settings' => $settings
'settings' => $settings,
]);
}
public function createFromCommande(\App\Models\Order $commande)
{
return Inertia::render('Devis/CreateFromCommande', [
'commande' => [
'id' => $commande->id,
'number' => $commande->number,
'label' => $commande->label,
'supplier' => $commande->supplier,
'quote_number' => $commande->quote_number,
],
]);
}
public function storeFromCommande(Request $request, \App\Models\Order $commande)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'destinataire_type' => 'required|string|in:commune,service_interne',
'destinataire_nom' => 'required|string|max:255',
'referent' => 'nullable|string|max:255',
'date_devis' => 'required|date',
'status' => 'required|string|in:draft,sent,accepted,rejected',
'items' => 'required|array|min:1',
'tva_rate' => 'nullable|numeric|min:0|max:100',
'notes' => 'nullable|string',
]);
$totalHt = 0;
foreach ($validated['items'] as $item) {
$totalHt += floatval($item['quantite'] ?? 0) * floatval($item['prix_unitaire'] ?? 0);
}
$tvaRate = floatval($validated['tva_rate'] ?? 20);
$validated['total_ht'] = $totalHt;
$validated['total_ttc'] = $totalHt * (1 + ($tvaRate / 100));
$validated['commande_id'] = $commande->id;
$validated['commande_number'] = $commande->number;
Devis::create($validated);
return redirect()->route('commandes.show', $commande->id)
->with('success', 'Sous-devis créé avec succès.');
}
public function createSousDevis(Devis $devi)
{
return Inertia::render('Devis/CreateSousDevis', [
'parentDevis' => $devi,
]);
}
public function storeSousDevis(Request $request, Devis $devi)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'destinataire_type' => 'required|string|in:commune,service_interne',
'destinataire_nom' => 'required|string|max:255',
'referent' => 'nullable|string|max:255',
'date_devis' => 'required|date',
'status' => 'required|string|in:draft,sent,accepted,rejected',
'items' => 'required|array|min:1',
'tva_rate' => 'nullable|numeric|min:0|max:100',
'notes' => 'nullable|string',
]);
$totalHt = 0;
foreach ($validated['items'] as $item) {
$totalHt += floatval($item['quantite'] ?? 0) * floatval($item['prix_unitaire'] ?? 0);
}
$tvaRate = floatval($validated['tva_rate'] ?? 20);
$validated['total_ht'] = $totalHt;
$validated['total_ttc'] = $totalHt * (1 + ($tvaRate / 100));
$validated['parent_devis_id'] = $devi->id;
Devis::create($validated);
return redirect()->route('devis.show', $devi->id)
->with('success', 'Sous-devis créé avec succès.');
}
public function edit(Devis $devi)
{
return Inertia::render('Devis/Edit', [
+10 -2
View File
@@ -194,10 +194,18 @@ class OrderController extends Controller
{
Gate::authorize('view', $order);
$order->load(['attachments', 'statusLogs.user']);
$order->load(['attachments', 'statusLogs.user', 'sousDevis']);
return Inertia::render('Commandes/Show', [
'order' => new OrderResource($order),
'order' => new OrderResource($order),
'sousDevis' => $order->sousDevis->map(fn($d) => [
'id' => $d->id,
'number' => $d->number,
'title' => $d->title,
'destinataire_nom' => $d->destinataire_nom,
'total_ttc' => (float) $d->total_ttc,
'status' => $d->status,
]),
]);
}