Skip to content

Commit 0c41f8d

Browse files
chore: store stock out and stock in after transfer
1 parent 0fe1c2a commit 0c41f8d

19 files changed

Lines changed: 277 additions & 36 deletions

app/Http/Requests/Ingredients/IngredientStockOut/StoreIngredientStockOutRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public function rules(): array
2424
'items.*.ingredient_id' => ['required', 'integer', 'exists:ingredients,id'],
2525
'items.*.ingredient_batch_id' => ['nullable', 'integer', 'exists:ingredient_batches,id'],
2626
'items.*.quantity' => ['required', 'numeric', 'min:0.0001'],
27+
'items.*.unit_cost' => ['nullable', 'numeric', 'min:0'],
2728
];
2829
}
2930
}

app/Http/Requests/Ingredients/IngredientStockOut/UpdateIngredientStockOutRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public function rules(): array
2424
'items.*.ingredient_id' => ['required', 'integer', 'exists:ingredients,id'],
2525
'items.*.ingredient_batch_id' => ['nullable', 'integer', 'exists:ingredient_batches,id'],
2626
'items.*.quantity' => ['required', 'numeric', 'min:0.0001'],
27+
'items.*.unit_cost' => ['nullable', 'numeric', 'min:0'],
2728
];
2829
}
2930
}

app/Http/Requests/Ingredients/IngredientWastage/StoreIngredientWastageRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public function rules(): array
2424
'items.*.ingredient_id' => ['required', 'integer', 'exists:ingredients,id'],
2525
'items.*.ingredient_batch_id' => ['nullable', 'integer', 'exists:ingredient_batches,id'],
2626
'items.*.quantity' => ['required', 'numeric', 'min:0.0001'],
27+
'items.*.unit_cost' => ['nullable', 'numeric', 'min:0'],
2728
];
2829
}
2930
}

app/Http/Requests/Ingredients/IngredientWastage/UpdateIngredientWastageRequest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public function rules(): array
2424
'items.*.ingredient_id' => ['required', 'integer', 'exists:ingredients,id'],
2525
'items.*.ingredient_batch_id' => ['nullable', 'integer', 'exists:ingredient_batches,id'],
2626
'items.*.quantity' => ['required', 'numeric', 'min:0.0001'],
27+
'items.*.unit_cost' => ['nullable', 'numeric', 'min:0'],
2728
];
2829
}
2930
}

app/Services/IngredientStockOutService.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ public function approve(IngredientStockOut $stockOut, int $userId): void
154154

155155
foreach ($stockOut->items as $item) {
156156
$stock = $this->inventoryService->getCurrentStock($warehouse, $item->ingredient);
157-
$unitCost = (float) $stock->average_cost;
157+
$unitCost = ($item->unit_cost !== null && (float) $item->unit_cost > 0)
158+
? (float) $item->unit_cost
159+
: (float) $stock->average_cost;
158160

159161
$this->inventoryService->decreaseStock($warehouse, $item->ingredient, [
160162
'transaction_type' => 'stock_out',
@@ -199,6 +201,7 @@ private function syncItems(IngredientStockOut $stockOut, array $items): void
199201
'ingredient_id' => $item['ingredient_id'],
200202
'ingredient_batch_id' => $item['ingredient_batch_id'] ?? null,
201203
'quantity' => $item['quantity'],
204+
'unit_cost' => isset($item['unit_cost']) && $item['unit_cost'] !== '' ? (float) $item['unit_cost'] : null,
202205
]);
203206
}
204207
}

app/Services/IngredientStockTransferService.php

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
namespace App\Services;
44

55
use App\Models\Ingredient;
6-
use App\Models\IngredientBatch;
6+
use App\Models\IngredientStockIn;
7+
use App\Models\IngredientStockInItem;
8+
use App\Models\IngredientStockOut;
9+
use App\Models\IngredientStockOutItem;
710
use App\Models\IngredientStockTransfer;
811
use App\Models\IngredientStockTransferItem;
912
use App\Models\Warehouse;
@@ -188,6 +191,9 @@ public function dispatch(IngredientStockTransfer $transfer, array $data, int $us
188191

189192
DB::transaction(function () use ($transfer, $data, $userId, $itemsById) {
190193
$fromWarehouse = $transfer->fromWarehouse ?? Warehouse::findOrFail($transfer->from_warehouse_id);
194+
$toWarehouse = $transfer->toWarehouse ?? Warehouse::findOrFail($transfer->to_warehouse_id);
195+
196+
$stockOutItems = [];
191197

192198
foreach ($data['items'] as $inputItem) {
193199
$item = $itemsById->get($inputItem['id'])
@@ -196,10 +202,7 @@ public function dispatch(IngredientStockTransfer $transfer, array $data, int $us
196202
$dispatchedQty = (float) $inputItem['dispatched_quantity'];
197203
$batchId = $inputItem['ingredient_batch_id'] ?? $item->ingredient_batch_id;
198204

199-
$stock = $this->inventoryService->getCurrentStock(
200-
$fromWarehouse,
201-
$item->ingredient,
202-
);
205+
$stock = $this->inventoryService->getCurrentStock($fromWarehouse, $item->ingredient);
203206

204207
$unitCost = (float) $stock->average_cost;
205208
$totalCost = $dispatchedQty * $unitCost;
@@ -220,12 +223,41 @@ public function dispatch(IngredientStockTransfer $transfer, array $data, int $us
220223
'unit_cost' => $unitCost,
221224
'total_cost' => $totalCost,
222225
]);
226+
227+
$stockOutItems[] = [
228+
'ingredient_id' => $item->ingredient_id,
229+
'quantity' => $dispatchedQty,
230+
'unit_cost' => $unitCost,
231+
'total_cost' => $totalCost,
232+
];
233+
}
234+
235+
$stockOut = IngredientStockOut::create([
236+
'stock_out_no' => $this->generateStockOutNo(),
237+
'warehouse_id' => $fromWarehouse->id,
238+
'stock_out_date' => now()->toDateString(),
239+
'purpose' => 'transfer',
240+
'status' => 'approved',
241+
'remarks' => "Transfer {$transfer->transfer_no} dispatched to {$toWarehouse->name}",
242+
'created_by' => $userId,
243+
'approved_by' => $userId,
244+
'approved_at' => now(),
245+
]);
246+
247+
foreach ($stockOutItems as $si) {
248+
IngredientStockOutItem::create([
249+
'ingredient_stock_out_id' => $stockOut->id,
250+
'ingredient_id' => $si['ingredient_id'],
251+
'quantity' => $si['quantity'],
252+
'unit_cost' => $si['unit_cost'],
253+
'total_cost' => $si['total_cost'],
254+
]);
223255
}
224256

225257
$transfer->update([
226-
'status' => 'dispatched',
227-
'dispatched_by' => $userId,
228-
'dispatched_at' => now(),
258+
'status' => 'dispatched',
259+
'dispatched_by' => $userId,
260+
'dispatched_at' => now(),
229261
]);
230262
});
231263
}
@@ -236,26 +268,30 @@ public function receive(IngredientStockTransfer $transfer, array $data, int $use
236268
throw new RuntimeException('Only dispatched transfers can be received.');
237269
}
238270

239-
$transfer->load('items.ingredient');
271+
$transfer->load(['items.ingredient', 'toWarehouse', 'fromWarehouse']);
240272

241273
$itemsById = $transfer->items->keyBy('id');
242274

243275
DB::transaction(function () use ($transfer, $data, $userId, $itemsById) {
244276
$toWarehouse = $transfer->toWarehouse ?? Warehouse::findOrFail($transfer->to_warehouse_id);
245277

278+
$stockInItems = [];
279+
246280
foreach ($data['items'] as $inputItem) {
247-
$item = $itemsById->get($inputItem['id'])
281+
$item = $itemsById->get($inputItem['id'])
248282
?? throw new RuntimeException("Transfer item [{$inputItem['id']}] not found.");
249-
$receivedQty = (float) $inputItem['received_quantity'];
283+
$receivedQty = (float) $inputItem['received_quantity'];
250284

251285
if ($receivedQty <= 0) {
252286
continue;
253287
}
254288

289+
$unitCost = (float) $item->unit_cost;
290+
255291
$batch = $this->inventoryService->createBatch($toWarehouse, $item->ingredient, [
256292
'batch_no' => $inputItem['batch_no'] ?? null,
257293
'received_quantity' => $receivedQty,
258-
'unit_cost' => (float) $item->unit_cost,
294+
'unit_cost' => $unitCost,
259295
'expiry_date' => $inputItem['expiry_date'] ?? null,
260296
'source_type' => IngredientStockTransfer::class,
261297
'source_id' => $transfer->id,
@@ -264,7 +300,7 @@ public function receive(IngredientStockTransfer $transfer, array $data, int $use
264300
$this->inventoryService->increaseStock($toWarehouse, $item->ingredient, [
265301
'transaction_type' => 'transfer_in',
266302
'quantity' => $receivedQty,
267-
'unit_cost' => (float) $item->unit_cost,
303+
'unit_cost' => $unitCost,
268304
'batch_id' => $batch->id,
269305
'reference_type' => IngredientStockTransfer::class,
270306
'reference_id' => $transfer->id,
@@ -274,6 +310,14 @@ public function receive(IngredientStockTransfer $transfer, array $data, int $use
274310
$item->update([
275311
'received_quantity' => (float) $item->received_quantity + $receivedQty,
276312
]);
313+
314+
$stockInItems[] = [
315+
'ingredient_id' => $item->ingredient_id,
316+
'ingredient_batch_id' => $batch->id,
317+
'quantity' => $receivedQty,
318+
'unit_cost' => $unitCost,
319+
'total_cost' => round($receivedQty * $unitCost, 4),
320+
];
277321
}
278322

279323
$transfer->refresh();
@@ -287,6 +331,32 @@ public function receive(IngredientStockTransfer $transfer, array $data, int $use
287331
'received_by' => $userId,
288332
'received_at' => now(),
289333
]);
334+
335+
if (! empty($stockInItems)) {
336+
$fromName = $transfer->fromWarehouse->name ?? "Transfer {$transfer->transfer_no}";
337+
$stockIn = IngredientStockIn::create([
338+
'stock_in_no' => $this->generateStockInNo(),
339+
'warehouse_id' => $toWarehouse->id,
340+
'stock_in_date' => now()->toDateString(),
341+
'source' => 'transfer',
342+
'status' => 'approved',
343+
'remarks' => "Transfer {$transfer->transfer_no} received from {$fromName}",
344+
'created_by' => $userId,
345+
'approved_by' => $userId,
346+
'approved_at' => now(),
347+
]);
348+
349+
foreach ($stockInItems as $si) {
350+
IngredientStockInItem::create([
351+
'ingredient_stock_in_id' => $stockIn->id,
352+
'ingredient_id' => $si['ingredient_id'],
353+
'ingredient_batch_id' => $si['ingredient_batch_id'],
354+
'quantity' => $si['quantity'],
355+
'unit_cost' => $si['unit_cost'],
356+
'total_cost' => $si['total_cost'],
357+
]);
358+
}
359+
}
290360
});
291361
}
292362

@@ -332,4 +402,28 @@ private function generateTransferNo(): string
332402

333403
return $prefix.str_pad($sequence, 5, '0', STR_PAD_LEFT);
334404
}
405+
406+
private function generateStockOutNo(): string
407+
{
408+
$prefix = 'STO-'.now()->format('Ym').'-';
409+
$last = IngredientStockOut::where('stock_out_no', 'like', $prefix.'%')
410+
->orderByDesc('id')
411+
->value('stock_out_no');
412+
413+
$sequence = $last ? ((int) substr($last, -5)) + 1 : 1;
414+
415+
return $prefix.str_pad($sequence, 5, '0', STR_PAD_LEFT);
416+
}
417+
418+
private function generateStockInNo(): string
419+
{
420+
$prefix = 'STI-'.now()->format('Ym').'-';
421+
$last = IngredientStockIn::where('stock_in_no', 'like', $prefix.'%')
422+
->orderByDesc('id')
423+
->value('stock_in_no');
424+
425+
$sequence = $last ? ((int) substr($last, -5)) + 1 : 1;
426+
427+
return $prefix.str_pad($sequence, 5, '0', STR_PAD_LEFT);
428+
}
335429
}

app/Services/IngredientWastageService.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ public function approve(IngredientWastage $wastage, int $userId): void
154154

155155
foreach ($wastage->items as $item) {
156156
$stock = $this->inventoryService->getCurrentStock($warehouse, $item->ingredient);
157-
$unitCost = (float) $stock->average_cost;
157+
$unitCost = ($item->unit_cost !== null && (float) $item->unit_cost > 0)
158+
? (float) $item->unit_cost
159+
: (float) $stock->average_cost;
158160

159161
$this->inventoryService->decreaseStock($warehouse, $item->ingredient, [
160162
'transaction_type' => 'wastage',
@@ -199,6 +201,7 @@ private function syncItems(IngredientWastage $wastage, array $items): void
199201
'ingredient_id' => $item['ingredient_id'],
200202
'ingredient_batch_id' => $item['ingredient_batch_id'] ?? null,
201203
'quantity' => $item['quantity'],
204+
'unit_cost' => isset($item['unit_cost']) && $item['unit_cost'] !== '' ? (float) $item['unit_cost'] : null,
202205
]);
203206
}
204207
}

app/Services/OrderService.php

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use App\Models\FoodVariant;
1010
use App\Models\Ingredient;
1111
use App\Models\IngredientInventoryTransaction;
12+
use App\Models\IngredientStockIn;
13+
use App\Models\IngredientStockInItem;
1214
use App\Models\IngredientStockOut;
1315
use App\Models\IngredientStockOutItem;
1416
use App\Models\IngredientStockTransfer;
@@ -975,10 +977,7 @@ private function consumeOrderItemStock(OrderItem $item): void
975977
$this->decrementReservedQuantity($warehouse, $ingredient, $reservedQuantity);
976978

977979
if ($saleQuantity > 0) {
978-
$this->recordIngredientConsumption($item, $warehouse, $ingredient, 'sale_consume', $saleQuantity, 'Consumed');
979-
980-
$stock = $this->inventoryService->getCurrentStock($warehouse, $ingredient);
981-
$unitCost = (float) $stock->average_cost;
980+
$unitCost = $this->recordIngredientConsumption($item, $warehouse, $ingredient, 'sale_consume', $saleQuantity, 'Consumed');
982981

983982
$stockOutByWarehouse[$warehouse->id]['warehouse'] = $warehouse;
984983
$stockOutByWarehouse[$warehouse->id]['items'][] = [
@@ -990,10 +989,7 @@ private function consumeOrderItemStock(OrderItem $item): void
990989
}
991990

992991
if ($wastageQuantity > 0) {
993-
$this->recordIngredientConsumption($item, $warehouse, $ingredient, 'wastage', $wastageQuantity, 'Recipe wastage');
994-
995-
$stock = $this->inventoryService->getCurrentStock($warehouse, $ingredient);
996-
$unitCost = (float) $stock->average_cost;
992+
$unitCost = $this->recordIngredientConsumption($item, $warehouse, $ingredient, 'wastage', $wastageQuantity, 'Recipe wastage');
997993

998994
$wastageByWarehouse[$warehouse->id]['warehouse'] = $warehouse;
999995
$wastageByWarehouse[$warehouse->id]['items'][] = [
@@ -1148,7 +1144,7 @@ private function recordIngredientConsumption(
11481144
string $transactionType,
11491145
float $quantity,
11501146
string $label,
1151-
): void {
1147+
): float {
11521148
$stock = $this->inventoryService->getCurrentStock($warehouse, $ingredient);
11531149
$unitCost = (float) $stock->average_cost;
11541150

@@ -1161,6 +1157,8 @@ private function recordIngredientConsumption(
11611157
'remarks' => "{$label} for order item #{$item->id}",
11621158
'created_by' => Auth::id(),
11631159
]);
1160+
1161+
return $unitCost;
11641162
}
11651163

11661164
private function stockAlreadyConsumed(OrderItem $item): bool
@@ -1357,6 +1355,9 @@ private function autoTransferIngredients(
13571355
'received_at' => now(),
13581356
]);
13591357

1358+
$stockOutItems = [];
1359+
$stockInItems = [];
1360+
13601361
foreach ($deductions as $ingredientId => $deduction) {
13611362
$ingredient = Ingredient::findOrFail((int) $ingredientId);
13621363
$qty = (float) $deduction['total_quantity'];
@@ -1395,6 +1396,41 @@ private function autoTransferIngredients(
13951396
'unit_cost' => $unitCost,
13961397
'total_cost' => round($qty * $unitCost, 4),
13971398
]);
1399+
1400+
$stockOutItems[] = ['ingredient_id' => $ingredient->id, 'quantity' => $qty, 'unit_cost' => $unitCost, 'total_cost' => round($qty * $unitCost, 4)];
1401+
$stockInItems[] = ['ingredient_id' => $ingredient->id, 'quantity' => $qty, 'unit_cost' => $unitCost, 'total_cost' => round($qty * $unitCost, 4)];
1402+
}
1403+
1404+
$remarks = "Auto-transfer for order {$order->order_number}";
1405+
1406+
$stockOut = IngredientStockOut::create([
1407+
'stock_out_no' => $this->generateStockOutNo(),
1408+
'warehouse_id' => $from->id,
1409+
'stock_out_date' => now()->toDateString(),
1410+
'purpose' => 'transfer',
1411+
'status' => 'approved',
1412+
'remarks' => $remarks,
1413+
'created_by' => Auth::id(),
1414+
'approved_by' => Auth::id(),
1415+
'approved_at' => now(),
1416+
]);
1417+
foreach ($stockOutItems as $si) {
1418+
IngredientStockOutItem::create(['ingredient_stock_out_id' => $stockOut->id, 'ingredient_id' => $si['ingredient_id'], 'quantity' => $si['quantity'], 'unit_cost' => $si['unit_cost'], 'total_cost' => $si['total_cost']]);
1419+
}
1420+
1421+
$stockIn = IngredientStockIn::create([
1422+
'stock_in_no' => $this->generateStockInNo(),
1423+
'warehouse_id' => $to->id,
1424+
'stock_in_date' => now()->toDateString(),
1425+
'source' => 'transfer',
1426+
'status' => 'approved',
1427+
'remarks' => $remarks,
1428+
'created_by' => Auth::id(),
1429+
'approved_by' => Auth::id(),
1430+
'approved_at' => now(),
1431+
]);
1432+
foreach ($stockInItems as $si) {
1433+
IngredientStockInItem::create(['ingredient_stock_in_id' => $stockIn->id, 'ingredient_id' => $si['ingredient_id'], 'quantity' => $si['quantity'], 'unit_cost' => $si['unit_cost'], 'total_cost' => $si['total_cost']]);
13981434
}
13991435
}
14001436

@@ -1658,6 +1694,17 @@ private function generateWastageNo(): string
16581694
return $prefix.str_pad($sequence, 5, '0', STR_PAD_LEFT);
16591695
}
16601696

1697+
private function generateStockInNo(): string
1698+
{
1699+
$prefix = 'STI-'.now()->format('Ym').'-';
1700+
$last = IngredientStockIn::where('stock_in_no', 'like', $prefix.'%')
1701+
->orderByDesc('id')
1702+
->value('stock_in_no');
1703+
$sequence = $last ? ((int) substr($last, -5)) + 1 : 1;
1704+
1705+
return $prefix.str_pad($sequence, 5, '0', STR_PAD_LEFT);
1706+
}
1707+
16611708
private function scopeOutlets(array $scope): Collection
16621709
{
16631710
$outletId = $this->outletIdFromScope($scope);

database/migrations/2026_05_17_000009_create_ingredient_stock_outs_table.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function up(): void
2525
'sample',
2626
'distribution',
2727
'other',
28+
'transfer',
2829
])->default('other');
2930

3031
$table->enum('status', [

0 commit comments

Comments
 (0)