33namespace App \Modules \Inventory \Http \Controllers ;
44
55use App \Http \Controllers \Controller ;
6- use App \Modules \Inventory \Http \Requests \ReceivePurchaseOrderRequest ;
7- use App \Modules \Inventory \Http \Requests \StorePurchaseOrderRequest ;
8- use App \Modules \Inventory \Http \Resources \PurchaseOrderResource ;
96use App \Modules \Inventory \Models \Product ;
107use App \Modules \Inventory \Models \PurchaseOrder ;
8+ use App \Modules \Inventory \Models \PurchaseOrderItem ;
9+ use App \Modules \Inventory \Models \StockMovement ;
1110use App \Modules \Inventory \Models \Supplier ;
12- use App \Modules \Inventory \Models \Warehouse ;
1311use Illuminate \Http \RedirectResponse ;
1412use Illuminate \Http \Request ;
1513use Inertia \Inertia ;
@@ -19,162 +17,187 @@ class PurchaseOrderController extends Controller
1917{
2018 public function index (Request $ request ): Response
2119 {
22- $ orders = PurchaseOrder::with (['supplier ' , 'warehouse ' ])
23- ->when ($ request ->status , fn ($ q ) => $ q ->where ('status ' , $ request ->status ))
20+ $ this ->authorize ('viewAny ' , PurchaseOrder::class);
21+
22+ $ orders = PurchaseOrder::with ('supplier ' )
23+ ->when ($ request ->status , fn ($ q ) => $ q ->where ('status ' , $ request ->status ))
2424 ->when ($ request ->supplier_id , fn ($ q ) => $ q ->where ('supplier_id ' , $ request ->supplier_id ))
2525 ->latest ()
26- ->paginate (25 )
26+ ->paginate (20 )
2727 ->withQueryString ();
2828
2929 return Inertia::render ('Inventory/PurchaseOrders/Index ' , [
30- 'orders ' => PurchaseOrderResource::collection ($ orders ),
31- 'suppliers ' => Supplier::where ('is_active ' , true )->orderBy ('name ' )->get (['id ' , 'name ' ]),
32- 'filters ' => $ request ->only (['status ' , 'supplier_id ' ]),
33- 'breadcrumbs ' => [
34- ['label ' => 'Inventory ' ],
35- ['label ' => 'Purchase Orders ' , 'href ' => route ('inventory.purchase-orders.index ' )],
36- ],
30+ 'orders ' => $ orders ,
31+ 'filters ' => $ request ->only (['status ' , 'supplier_id ' ]),
3732 ]);
3833 }
3934
4035 public function create (): Response
4136 {
37+ $ this ->authorize ('create ' , PurchaseOrder::class);
38+
4239 return Inertia::render ('Inventory/PurchaseOrders/Create ' , [
43- 'suppliers ' => Supplier::where ('is_active ' , true )->orderBy ('name ' )->get (['id ' , 'name ' ]),
44- 'warehouses ' => Warehouse::where ('is_active ' , true )->orderBy ('name ' )->get (['id ' , 'name ' ]),
45- 'products ' => Product::active ()->with ('uom ' )->orderBy ('name ' )
46- ->get (['id ' , 'name ' , 'sku ' , 'cost_price ' , 'uom_id ' ]),
47- 'breadcrumbs ' => [
48- ['label ' => 'Inventory ' ],
49- ['label ' => 'Purchase Orders ' , 'href ' => route ('inventory.purchase-orders.index ' )],
50- ['label ' => 'New Order ' ],
51- ],
40+ 'suppliers ' => Supplier::orderBy ('name ' )->get (['id ' , 'name ' ]),
41+ 'products ' => Product::where ('is_active ' , true )->orderBy ('name ' )->get (['id ' , 'name ' , 'sku ' ]),
5242 ]);
5343 }
5444
55- public function store (StorePurchaseOrderRequest $ request ): RedirectResponse
45+ public function store (Request $ request ): RedirectResponse
5646 {
57- $ data = $ request ->validated ();
47+ $ this ->authorize ('create ' , PurchaseOrder::class);
48+
49+ $ validated = $ request ->validate ([
50+ 'supplier_id ' => 'nullable|exists:suppliers,id ' ,
51+ 'order_date ' => 'required|date ' ,
52+ 'expected_date ' => 'nullable|date ' ,
53+ 'currency ' => 'nullable|string|max:3 ' ,
54+ 'notes ' => 'nullable|string ' ,
55+ 'items ' => 'required|array|min:1 ' ,
56+ 'items.*.description ' => 'required|string|max:255 ' ,
57+ 'items.*.quantity ' => 'required|numeric|min:0.01 ' ,
58+ 'items.*.unit_price ' => 'required|numeric|min:0 ' ,
59+ 'items.*.product_id ' => 'nullable|exists:products,id ' ,
60+ ]);
5861
5962 $ po = PurchaseOrder::create ([
60- 'tenant_id ' => auth ()->user ()->tenant_id ,
61- 'supplier_id ' => $ data ['supplier_id ' ],
62- 'warehouse_id ' => $ data ['warehouse_id ' ],
63- 'expected_date ' => $ data ['expected_date ' ] ?? null ,
64- 'notes ' => $ data ['notes ' ] ?? null ,
65- 'created_by ' => auth ()->id (),
63+ 'tenant_id ' => auth ()->user ()->tenant_id ,
64+ 'po_number ' => PurchaseOrder::generatePoNumber (),
65+ 'supplier_id ' => $ validated ['supplier_id ' ] ?? null ,
66+ 'order_date ' => $ validated ['order_date ' ],
67+ 'expected_date ' => $ validated ['expected_date ' ] ?? null ,
68+ 'currency ' => $ validated ['currency ' ] ?? 'USD ' ,
69+ 'notes ' => $ validated ['notes ' ] ?? null ,
70+ 'created_by ' => auth ()->id (),
71+ 'subtotal ' => 0 ,
72+ 'tax ' => 0 ,
73+ 'total ' => 0 ,
6674 ]);
6775
68- foreach ($ data ['items ' ] as $ item ) {
69- $ po ->items ()->create ([
70- 'product_id ' => $ item ['product_id ' ],
71- 'quantity ' => $ item ['quantity ' ],
72- 'unit_cost ' => $ item ['unit_cost ' ],
76+ foreach ($ validated ['items ' ] as $ item ) {
77+ PurchaseOrderItem::create ([
78+ 'tenant_id ' => auth ()->user ()->tenant_id ,
79+ 'purchase_order_id ' => $ po ->id ,
80+ 'product_id ' => $ item ['product_id ' ] ?? null ,
81+ 'description ' => $ item ['description ' ],
82+ 'quantity ' => $ item ['quantity ' ],
83+ 'unit_price ' => $ item ['unit_price ' ],
84+ 'received_qty ' => 0 ,
7385 ]);
7486 }
7587
76- return redirect ()->route ('inventory.purchase-orders.show ' , $ po )
77- ->with ('success ' , 'Purchase order created. ' );
88+ $ po ->recalculateTotals ();
89+
90+ return redirect ()->route ('inventory.purchase-orders.show ' , $ po );
7891 }
7992
8093 public function show (PurchaseOrder $ purchaseOrder ): Response
8194 {
82- $ purchaseOrder ->load (['supplier ' , 'warehouse ' , 'items.product ' , 'creator ' ]);
95+ $ this ->authorize ('view ' , $ purchaseOrder );
96+ $ purchaseOrder ->load (['supplier ' , 'items.product ' , 'createdBy ' ]);
8397
8498 return Inertia::render ('Inventory/PurchaseOrders/Show ' , [
85- 'order ' => new PurchaseOrderResource ($ purchaseOrder ),
86- 'transitions ' => $ purchaseOrder ->availableTransitions (),
87- 'breadcrumbs ' => [
88- ['label ' => 'Inventory ' ],
89- ['label ' => 'Purchase Orders ' , 'href ' => route ('inventory.purchase-orders.index ' )],
90- ['label ' => "PO # {$ purchaseOrder ->id }" ],
91- ],
99+ 'order ' => $ purchaseOrder ,
92100 ]);
93101 }
94102
95- public function receiveForm (PurchaseOrder $ purchaseOrder ): Response
103+ public function send (PurchaseOrder $ purchaseOrder ): RedirectResponse
96104 {
97- if (! $ purchaseOrder ->canTransitionTo ('received ' )) {
98- return redirect ()->route ('inventory.purchase-orders.show ' , $ purchaseOrder )
99- ->withErrors (['status ' => 'This purchase order cannot be received in its current status. ' ]);
100- }
105+ $ this ->authorize ('update ' , $ purchaseOrder );
106+ $ purchaseOrder ->send ();
101107
102- $ purchaseOrder ->load (['supplier ' , 'warehouse ' , 'items.product ' ]);
108+ return back ()->with ('success ' , 'Purchase order sent. ' );
109+ }
103110
104- return Inertia::render ('Inventory/PurchaseOrders/Receive ' , [
105- 'order ' => new PurchaseOrderResource ($ purchaseOrder ),
106- 'breadcrumbs ' => [
107- ['label ' => 'Inventory ' ],
108- ['label ' => 'Purchase Orders ' , 'href ' => route ('inventory.purchase-orders.index ' )],
109- ['label ' => "PO- " . str_pad ($ purchaseOrder ->id , 4 , '0 ' , STR_PAD_LEFT ), 'href ' => route ('inventory.purchase-orders.show ' , $ purchaseOrder )],
110- ['label ' => 'Receive Items ' ],
111- ],
112- ]);
111+ public function cancel (PurchaseOrder $ purchaseOrder ): RedirectResponse
112+ {
113+ $ this ->authorize ('update ' , $ purchaseOrder );
114+ $ purchaseOrder ->cancel ();
115+
116+ return back ()->with ('success ' , 'Purchase order cancelled. ' );
113117 }
114118
115- public function transition (Request $ request , PurchaseOrder $ purchaseOrder ): RedirectResponse
119+ public function receive (Request $ request , PurchaseOrder $ purchaseOrder ): RedirectResponse
116120 {
117- $ status = $ request ->validate (['status ' => ['required ' , 'string ' ]])['status ' ];
118-
119- try {
120- if ($ status === 'received ' ) {
121- $ lines = $ purchaseOrder ->items ->map (fn ($ i ) => [
122- 'id ' => $ i ->id ,
123- 'received_quantity ' => $ i ->quantity ,
124- ])->all ();
125- $ purchaseOrder ->receive ($ lines );
126- } else {
127- $ purchaseOrder ->transitionTo ($ status );
121+ $ this ->authorize ('update ' , $ purchaseOrder );
122+
123+ // Accept both 'items' and 'lines' key; both 'received_qty' and 'received_quantity'
124+ $ lines = $ request ->input ('items ' ) ?? $ request ->input ('lines ' ) ?? [];
125+
126+ foreach ($ lines as $ data ) {
127+ $ item = PurchaseOrderItem::find ($ data ['id ' ] ?? null );
128+ if (!$ item || $ item ->purchase_order_id !== $ purchaseOrder ->id ) {
129+ continue ;
130+ }
131+ $ qty = (float ) ($ data ['received_qty ' ] ?? $ data ['received_quantity ' ] ?? 0 );
132+ $ prevQty = (float ) $ item ->received_qty ;
133+ $ item ->received_qty = $ qty ;
134+ $ item ->save ();
135+
136+ // Create stock movement for the delta if warehouse is set
137+ $ delta = $ qty - $ prevQty ;
138+ if ($ delta > 0 && $ purchaseOrder ->warehouse_id && $ item ->product_id ) {
139+ StockMovement::record ([
140+ 'product_id ' => $ item ->product_id ,
141+ 'warehouse_id ' => $ purchaseOrder ->warehouse_id ,
142+ 'type ' => 'in ' ,
143+ 'quantity ' => $ delta ,
144+ 'reference ' => $ purchaseOrder ->po_number ?? ('PO- ' . $ purchaseOrder ->id ),
145+ 'notes ' => 'PO receiving ' ,
146+ ]);
128147 }
129- } catch (\DomainException $ e ) {
130- return back ()->withErrors (['status ' => $ e ->getMessage ()]);
131148 }
132149
133- return back ()->with ( ' success ' , " Order { $ status } . " );
134- }
150+ $ allReceived = $ purchaseOrder -> items ()->get ()-> every ( fn ( $ i ) => ( float ) $ i -> received_qty >= ( float ) $ i -> quantity );
151+ $ anyReceived = $ purchaseOrder -> items ()-> where ( ' received_qty ' , ' > ' , 0 )-> exists ();
135152
136- public function submit (PurchaseOrder $ purchaseOrder ): RedirectResponse
137- {
138- try {
139- $ purchaseOrder ->transitionTo ('submitted ' );
140- } catch (\DomainException $ e ) {
141- return back ()->withErrors (['status ' => $ e ->getMessage ()]);
153+ if ($ allReceived ) {
154+ $ purchaseOrder ->markReceived ();
155+ } elseif ($ anyReceived ) {
156+ $ purchaseOrder ->status = 'partial ' ;
157+ $ purchaseOrder ->save ();
142158 }
143159
144- return back ()->with ('success ' , 'Purchase order submitted . ' );
160+ return back ()->with ('success ' , 'Receiving updated . ' );
145161 }
146162
147- public function approve (PurchaseOrder $ purchaseOrder ): RedirectResponse
163+ public function receiveForm (PurchaseOrder $ purchaseOrder ): Response
148164 {
149- try {
150- $ purchaseOrder ->transitionTo ('approved ' );
151- } catch (\DomainException $ e ) {
152- return back ()->withErrors (['status ' => $ e ->getMessage ()]);
153- }
154-
155- return back ()->with ('success ' , 'Purchase order approved. ' );
165+ $ this ->authorize ('view ' , $ purchaseOrder );
166+ $ purchaseOrder ->load (['supplier ' , 'items.product ' ]);
167+ return Inertia::render ('Inventory/PurchaseOrders/Receive ' , ['order ' => $ purchaseOrder ]);
156168 }
157169
158- public function receive ( ReceivePurchaseOrderRequest $ request , PurchaseOrder $ purchaseOrder ): RedirectResponse
170+ public function transition ( Request $ request , PurchaseOrder $ purchaseOrder ): RedirectResponse
159171 {
160- try {
161- $ purchaseOrder ->receive ($ request ->validated ()['lines ' ]);
162- } catch (\DomainException $ e ) {
163- return back ()->withErrors (['status ' => $ e ->getMessage ()]);
172+ $ this ->authorize ('update ' , $ purchaseOrder );
173+ $ status = $ request ->input ('status ' );
174+ if ($ status ) {
175+ $ purchaseOrder ->status = $ status ;
176+ $ purchaseOrder ->save ();
164177 }
178+ return back ()->with ('success ' , 'Status updated. ' );
179+ }
165180
166- return redirect ()->route ('inventory.purchase-orders.show ' , $ purchaseOrder )
167- ->with ('success ' , 'Items received and stock updated. ' );
181+ public function submit (PurchaseOrder $ purchaseOrder ): RedirectResponse
182+ {
183+ $ this ->authorize ('update ' , $ purchaseOrder );
184+ $ purchaseOrder ->send ();
185+ return back ()->with ('success ' , 'Purchase order submitted. ' );
168186 }
169187
170- public function cancel (PurchaseOrder $ purchaseOrder ): RedirectResponse
188+ public function approve (PurchaseOrder $ purchaseOrder ): RedirectResponse
171189 {
172- try {
173- $ purchaseOrder ->transitionTo ( ' cancelled ' ) ;
174- } catch ( \ DomainException $ e ) {
175- return back ()->withErrors ([ ' status ' => $ e -> getMessage ()] );
176- }
190+ $ this -> authorize ( ' update ' , $ purchaseOrder );
191+ $ purchaseOrder ->status = ' sent ' ;
192+ $ purchaseOrder -> save ();
193+ return back ()->with ( ' success ' , ' Purchase order approved. ' );
194+ }
177195
178- return back ()->with ('success ' , 'Purchase order cancelled. ' );
196+ public function destroy (PurchaseOrder $ purchaseOrder ): RedirectResponse
197+ {
198+ $ this ->authorize ('delete ' , $ purchaseOrder );
199+ $ purchaseOrder ->delete ();
200+
201+ return redirect ()->route ('inventory.purchase-orders.index ' );
179202 }
180203}
0 commit comments