From ac54bcaeba671df995d2dcc9cdc5512934f26279 Mon Sep 17 00:00:00 2001 From: "sweep-ai[bot]" <128439645+sweep-ai[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 10:19:12 +0000 Subject: [PATCH] Implement Advanced Inventory Valuation Methods and Cost --- app/Services/InventoryService.php | 45 +++++++- app/Services/InventoryValuationService.php | 102 ++++++++++++++++++ ...14_000001_create_inventory_items_table.php | 2 + ...003_create_inventory_cost_layers_table.php | 27 +++++ 4 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 app/Services/InventoryValuationService.php create mode 100644 database/migrations/2024_02_14_000003_create_inventory_cost_layers_table.php diff --git a/app/Services/InventoryService.php b/app/Services/InventoryService.php index 0de6206f..e897c5c7 100644 --- a/app/Services/InventoryService.php +++ b/app/Services/InventoryService.php @@ -7,9 +7,17 @@ use App\Models\InventoryItem; use App\Models\Transaction; use App\Models\InventoryTransaction; +use App\Models\InventoryCostLayer; class InventoryService { + protected $valuationService; + + public function __construct(InventoryValuationService $valuationService) + { + $this->valuationService = $valuationService; + } + public function createInventoryTransaction( Transaction $transaction, InventoryItem $item, @@ -25,6 +33,22 @@ public function createInventoryTransaction( 'transaction_type' => $type ]); + if ($type === 'purchase') { + InventoryCostLayer::create([ + 'inventory_item_id' => $item->id, + 'quantity' => $quantity, + 'unit_cost' => $unitPrice, + 'purchase_date' => now() + ]); + + if ($item->valuation_method === 'average') { + $this->valuationService->updateAverageCost($item, $quantity, $unitPrice); + } + } else if ($type === 'sale') { + $costOfGoodsSold = $this->valuationService->calculateCostOfGoodsSold($inventoryTx); + $inventoryTx->update(['cost_of_goods_sold' => $costOfGoodsSold]); + } + $change = $type === 'sale' ? -$quantity : $quantity; $item->updateQuantity($change); @@ -41,6 +65,25 @@ public function checkLowStock() public function getInventoryValue() { return InventoryItem::where('is_active', true) - ->sum(\DB::raw('unit_price * current_quantity')); + ->get() + ->sum(function ($item) { + return $this->valuationService->getInventoryValuation($item); + }); + } + + public function getInventoryReport() + { + return InventoryItem::where('is_active', true) + ->get() + ->map(function ($item) { + return [ + 'id' => $item->id, + 'name' => $item->name, + 'sku' => $item->sku, + 'quantity' => $item->current_quantity, + 'valuation_method' => $item->valuation_method, + 'total_value' => $this->valuationService->getInventoryValuation($item) + ]; + }); } } \ No newline at end of file diff --git a/app/Services/InventoryValuationService.php b/app/Services/InventoryValuationService.php new file mode 100644 index 00000000..a64dda76 --- /dev/null +++ b/app/Services/InventoryValuationService.php @@ -0,0 +1,102 @@ + + +inventoryItem; + + switch ($item->valuation_method) { + case 'fifo': + return $this->calculateFIFOCost($transaction); + case 'lifo': + return $this->calculateLIFOCost($transaction); + case 'average': + return $this->calculateAverageCost($transaction); + } + } + + private function calculateFIFOCost(InventoryTransaction $transaction) + { + $remainingQty = $transaction->quantity; + $totalCost = 0; + + $costLayers = InventoryCostLayer::where('inventory_item_id', $transaction->inventory_item_id) + ->where('quantity', '>', 0) + ->orderBy('purchase_date') + ->get(); + + foreach ($costLayers as $layer) { + $qtyFromLayer = min($remainingQty, $layer->quantity); + $totalCost += $qtyFromLayer * $layer->unit_cost; + $layer->quantity -= $qtyFromLayer; + $layer->save(); + + $remainingQty -= $qtyFromLayer; + if ($remainingQty <= 0) break; + } + + return $totalCost; + } + + private function calculateLIFOCost(InventoryTransaction $transaction) + { + $remainingQty = $transaction->quantity; + $totalCost = 0; + + $costLayers = InventoryCostLayer::where('inventory_item_id', $transaction->inventory_item_id) + ->where('quantity', '>', 0) + ->orderBy('purchase_date', 'desc') + ->get(); + + foreach ($costLayers as $layer) { + $qtyFromLayer = min($remainingQty, $layer->quantity); + $totalCost += $qtyFromLayer * $layer->unit_cost; + $layer->quantity -= $qtyFromLayer; + $layer->save(); + + $remainingQty -= $qtyFromLayer; + if ($remainingQty <= 0) break; + } + + return $totalCost; + } + + private function calculateAverageCost(InventoryTransaction $transaction) + { + $item = $transaction->inventoryItem; + return $transaction->quantity * $item->average_cost; + } + + public function updateAverageCost(InventoryItem $item, $purchaseQty, $purchaseCost) + { + $totalValue = ($item->current_quantity * $item->average_cost) + + ($purchaseQty * $purchaseCost); + $totalQty = $item->current_quantity + $purchaseQty; + + $item->average_cost = $totalValue / $totalQty; + $item->save(); + } + + public function getInventoryValuation(InventoryItem $item) + { + switch ($item->valuation_method) { + case 'fifo': + case 'lifo': + return InventoryCostLayer::where('inventory_item_id', $item->id) + ->where('quantity', '>', 0) + ->sum(DB::raw('quantity * unit_cost')); + case 'average': + return $item->current_quantity * $item->average_cost; + } + } +} \ No newline at end of file diff --git a/database/migrations/2024_02_14_000001_create_inventory_items_table.php b/database/migrations/2024_02_14_000001_create_inventory_items_table.php index d8fd790b..bbbf8627 100644 --- a/database/migrations/2024_02_14_000001_create_inventory_items_table.php +++ b/database/migrations/2024_02_14_000001_create_inventory_items_table.php @@ -20,6 +20,8 @@ public function up() $table->integer('reorder_point')->default(0); $table->foreignId('account_id')->constrained('accounts'); $table->foreignId('category_id')->nullable()->constrained('categories'); + $table->enum('valuation_method', ['fifo', 'lifo', 'average'])->default('fifo'); + $table->decimal('average_cost', 15, 2)->nullable(); $table->boolean('is_active')->default(true); $table->timestamps(); }); diff --git a/database/migrations/2024_02_14_000003_create_inventory_cost_layers_table.php b/database/migrations/2024_02_14_000003_create_inventory_cost_layers_table.php new file mode 100644 index 00000000..aef15703 --- /dev/null +++ b/database/migrations/2024_02_14_000003_create_inventory_cost_layers_table.php @@ -0,0 +1,27 @@ + + +id(); + $table->foreignId('inventory_item_id')->constrained('inventory_items'); + $table->integer('quantity'); + $table->decimal('unit_cost', 15, 2); + $table->date('purchase_date'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('inventory_cost_layers'); + } +}; \ No newline at end of file