Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.55% covered (warning)
77.55%
38 / 49
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProductionOrderController
77.55% covered (warning)
77.55%
38 / 49
42.86% covered (danger)
42.86%
3 / 7
20.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 index
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 store
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 show
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 complete
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
5.07
 cancel
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace App\Http\Controllers\admin;
4
5use App\Http\Controllers\Controller;
6use App\Models\Bom;
7use App\Models\ProductionOrder;
8use App\Services\Location\BranchContext;
9use App\Services\Manufacturing\BomService;
10use App\Services\Manufacturing\ProductionService;
11use Illuminate\Http\RedirectResponse;
12use Illuminate\Http\Request;
13use Illuminate\View\View;
14
15/**
16 * Settings → Manufacturing → Production Orders. The "Make a batch" flow:
17 * pick a BOM + a quantity, see live material availability, then commit —
18 * App\Services\Manufacturing\ProductionService does the actual stock move.
19 */
20class ProductionOrderController extends Controller
21{
22    public function __construct(
23        private readonly BomService $bomService,
24        private readonly ProductionService $productionService,
25    ) {}
26
27    public function index(): View
28    {
29        $orders = app(BranchContext::class)->scope(ProductionOrder::query())->with('bom.product')->latest()->paginate(20);
30
31        return view('admin.manufacturing.production.index', compact('orders'));
32    }
33
34    public function create(): View
35    {
36        $boms = Bom::query()->with('product')->where('is_active', true)->get();
37
38        $warehouses = feature('multi_warehouse') ? app(BranchContext::class)->warehouses()->active()->with('branch')->get() : collect();
39
40        return view('admin.manufacturing.production.create', compact('boms', 'warehouses'));
41    }
42
43    public function store(Request $request): RedirectResponse
44    {
45        $data = $request->validate([
46            'bom_id' => ['required', 'exists:boms,id'],
47            'planned_qty' => ['required', 'numeric', 'min:0.0001'],
48            'note' => ['nullable', 'string', 'max:1000'],
49            'source_warehouse_id' => feature('multi_warehouse') ? ['required', 'exists:warehouses,id'] : ['nullable'],
50            'output_warehouse_id' => feature('multi_warehouse') ? ['required', 'exists:warehouses,id'] : ['nullable'],
51        ]);
52
53        $bom = Bom::with('items.material')->findOrFail($data['bom_id']);
54        app(BranchContext::class)->ensureWarehouse(isset($data['source_warehouse_id']) ? (int) $data['source_warehouse_id'] : null);
55        app(BranchContext::class)->ensureWarehouse(isset($data['output_warehouse_id']) ? (int) $data['output_warehouse_id'] : null);
56        $order = $this->productionService->create(
57            $bom,
58            (float) $data['planned_qty'],
59            $data['note'] ?? null,
60            $data['source_warehouse_id'] ?? null,
61            $data['output_warehouse_id'] ?? null,
62        );
63
64        return redirect()->route('admin.manufacturing.production.show', $order)->with('success', "Production order {$order->number} created.");
65    }
66
67    public function show(ProductionOrder $productionOrder): View
68    {
69        $productionOrder->load('bom.product', 'consumptions.material', 'outputs.product', 'sourceWarehouse', 'outputWarehouse');
70        $requirements = $this->bomService->requirements($productionOrder->bom, (float) $productionOrder->planned_qty);
71
72        return view('admin.manufacturing.production.show', [
73            'order' => $productionOrder,
74            'requirements' => $requirements,
75            'canProduce' => $requirements->every(fn ($r) => $r['sufficient']),
76        ]);
77    }
78
79    public function complete(Request $request, ProductionOrder $productionOrder): RedirectResponse
80    {
81        $data = $request->validate([
82            'actual_qty' => ['nullable', 'numeric', 'min:0.0001'],
83            'labor_cost' => ['nullable', 'numeric', 'min:0'],
84            'overhead_cost' => ['nullable', 'numeric', 'min:0'],
85        ]);
86
87        try {
88            $this->productionService->complete(
89                $productionOrder,
90                isset($data['actual_qty']) ? (float) $data['actual_qty'] : null,
91                isset($data['labor_cost']) ? (float) $data['labor_cost'] : null,
92                isset($data['overhead_cost']) ? (float) $data['overhead_cost'] : null,
93            );
94        } catch (\Throwable $e) {
95            return back()->with('error', $e->getMessage());
96        }
97
98        return redirect()->route('admin.manufacturing.production.show', $productionOrder)->with('success', "Production order {$productionOrder->number} completed.");
99    }
100
101    public function cancel(ProductionOrder $productionOrder): RedirectResponse
102    {
103        try {
104            $this->productionService->cancel($productionOrder);
105        } catch (\RuntimeException $e) {
106            return back()->with('error', $e->getMessage());
107        }
108
109        return redirect()->route('admin.manufacturing.production.index')->with('success', "Production order {$productionOrder->number} cancelled.");
110    }
111}