Files
star-erp/app/Modules/Inventory/Services/InventoryService.php

252 lines
10 KiB
PHP
Raw Normal View History

<?php
namespace App\Modules\Inventory\Services;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use App\Modules\Inventory\Models\Inventory;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\Product;
use Illuminate\Support\Facades\DB;
class InventoryService implements InventoryServiceInterface
{
public function getAllWarehouses()
{
return Warehouse::all();
}
public function getAllProducts()
{
return Product::with(['baseUnit', 'largeUnit'])->get();
}
public function getUnits()
{
return \App\Modules\Inventory\Models\Unit::all();
}
public function getInventoriesByIds(array $ids, array $with = [])
{
return Inventory::whereIn('id', $ids)->with($with)->get();
}
public function getProduct(int $id)
{
return Product::with(['baseUnit', 'largeUnit'])->find($id);
}
public function getProductsByIds(array $ids)
{
return Product::whereIn('id', $ids)->with(['baseUnit', 'largeUnit'])->get();
}
public function getProductsByName(string $name)
{
return Product::where('name', 'like', "%{$name}%")->with(['baseUnit', 'largeUnit'])->get();
}
public function getWarehouse(int $id)
{
return Warehouse::find($id);
}
public function checkStock(int $productId, int $warehouseId, float $quantity): bool
{
$stock = Inventory::where('product_id', $productId)
->where('warehouse_id', $warehouseId)
->sum('quantity');
return $stock >= $quantity;
}
public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null): void
{
DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason, $force, $slot) {
$query = Inventory::where('product_id', $productId)
->where('warehouse_id', $warehouseId)
->where('quantity', '>', 0);
if ($slot) {
$query->where('location', $slot);
}
$inventories = $query->orderBy('arrival_date', 'asc')
->get();
$remainingToDecrease = $quantity;
foreach ($inventories as $inventory) {
if ($remainingToDecrease <= 0) break;
$decreaseAmount = min($inventory->quantity, $remainingToDecrease);
$this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason);
$remainingToDecrease -= $decreaseAmount;
}
if ($remainingToDecrease > 0) {
if ($force) {
// Find any existing inventory record in this warehouse/slot to subtract from, or create one
$query = Inventory::where('product_id', $productId)
->where('warehouse_id', $warehouseId);
if ($slot) {
$query->where('location', $slot);
}
$inventory = $query->first();
if (!$inventory) {
$inventory = Inventory::create([
'warehouse_id' => $warehouseId,
'product_id' => $productId,
'location' => $slot,
'quantity' => 0,
'unit_cost' => 0,
'total_value' => 0,
'batch_number' => 'POS-AUTO-' . ($slot ? $slot . '-' : '') . time(),
'arrival_date' => now(),
'origin_country' => 'TW',
'quality_status' => 'normal',
]);
}
$this->decreaseInventoryQuantity($inventory->id, $remainingToDecrease, $reason);
} else {
throw new \Exception("庫存不足,無法扣除所有請求的數量。");
}
}
});
}
public function getInventoriesByWarehouse(int $warehouseId)
{
return Inventory::with(['product.baseUnit', 'product.largeUnit'])
->where('warehouse_id', $warehouseId)
->where('quantity', '>', 0)
->orderBy('arrival_date', 'asc')
->get();
}
public function createInventoryRecord(array $data)
{
return DB::transaction(function () use ($data) {
// 嘗試查找是否已有相同批號的庫存
$inventory = Inventory::where('warehouse_id', $data['warehouse_id'])
->where('product_id', $data['product_id'])
->where('batch_number', $data['batch_number'] ?? null)
->first();
$balanceBefore = 0;
if ($inventory) {
// 若存在,則更新數量與相關資訊 (鎖定行以避免併發問題)
$inventory = Inventory::lockForUpdate()->find($inventory->id);
$balanceBefore = $inventory->quantity;
// 加權平均成本計算 (可選,這裡先採簡單邏輯:若有新成本則更新,否則沿用)
// 若本次入庫有指定成本,則更新該批次單價 (假設同批號成本相同)
if (isset($data['unit_cost'])) {
$inventory->unit_cost = $data['unit_cost'];
}
$inventory->quantity += $data['quantity'];
// 更新總價值
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
// 更新其他可能變更的欄位 (如最後入庫日)
$inventory->arrival_date = $data['arrival_date'] ?? $inventory->arrival_date;
$inventory->save();
} else {
// 若不存在,則建立新紀錄
$unitCost = $data['unit_cost'] ?? 0;
$inventory = Inventory::create([
'warehouse_id' => $data['warehouse_id'],
'product_id' => $data['product_id'],
'quantity' => $data['quantity'],
'unit_cost' => $unitCost,
'total_value' => $data['quantity'] * $unitCost,
'batch_number' => $data['batch_number'] ?? null,
'box_number' => $data['box_number'] ?? null,
'origin_country' => $data['origin_country'] ?? 'TW',
'arrival_date' => $data['arrival_date'] ?? now(),
'expiry_date' => $data['expiry_date'] ?? null,
'quality_status' => $data['quality_status'] ?? 'normal',
'source_purchase_order_id' => $data['source_purchase_order_id'] ?? null,
]);
}
\App\Modules\Inventory\Models\InventoryTransaction::create([
'inventory_id' => $inventory->id,
'type' => '入庫',
'quantity' => $data['quantity'],
'unit_cost' => $inventory->unit_cost, // 記錄當下成本
'balance_before' => $balanceBefore,
'balance_after' => $inventory->quantity,
'reason' => $data['reason'] ?? '手動入庫',
'reference_type' => $data['reference_type'] ?? null,
'reference_id' => $data['reference_id'] ?? null,
'user_id' => auth()->id(),
'actual_time' => now(),
]);
return $inventory;
});
}
public function decreaseInventoryQuantity(int $inventoryId, float $quantity, ?string $reason = null, ?string $referenceType = null, $referenceId = null): void
{
DB::transaction(function () use ($inventoryId, $quantity, $reason, $referenceType, $referenceId) {
$inventory = Inventory::lockForUpdate()->findOrFail($inventoryId);
$balanceBefore = $inventory->quantity;
$inventory->decrement('quantity', $quantity); // decrement 不會自動觸發 total_value 更新
// 需要手動更新總價值
$inventory->refresh();
$inventory->total_value = $inventory->quantity * $inventory->unit_cost;
$inventory->save();
\App\Modules\Inventory\Models\InventoryTransaction::create([
'inventory_id' => $inventory->id,
'type' => '出庫',
'quantity' => -$quantity,
'unit_cost' => $inventory->unit_cost, // 記錄出庫時的成本
'balance_before' => $balanceBefore,
'balance_after' => $inventory->quantity,
'reason' => $reason ?? '庫存扣減',
'reference_type' => $referenceType,
'reference_id' => $referenceId,
'user_id' => auth()->id(),
'actual_time' => now(),
]);
});
}
public function findInventoryByBatch(int $warehouseId, int $productId, ?string $batchNumber)
{
return Inventory::where('warehouse_id', $warehouseId)
->where('product_id', $productId)
->where('batch_number', $batchNumber)
->first();
}
public function getDashboardStats(): array
{
// 庫存總表 join 安全庫存表,計算低庫存
$lowStockCount = DB::table('warehouse_product_safety_stocks as ss')
->join(DB::raw('(SELECT warehouse_id, product_id, SUM(quantity) as total_qty FROM inventories WHERE deleted_at IS NULL GROUP BY warehouse_id, product_id) as inv'),
function ($join) {
$join->on('ss.warehouse_id', '=', 'inv.warehouse_id')
->on('ss.product_id', '=', 'inv.product_id');
})
->whereRaw('inv.total_qty <= ss.safety_stock')
->count();
return [
'productsCount' => Product::count(),
'warehousesCount' => Warehouse::count(),
'lowStockCount' => $lowStockCount,
'totalInventoryQuantity' => Inventory::sum('quantity'),
];
}
}