feat: 修正庫存與撥補單邏輯並整合文件
1. 修復倉庫統計數據加總與樣式。 2. 修正可用庫存計算邏輯(排除不可銷售倉庫)。 3. 撥補單商品列表加入批號與效期顯示。 4. 修正撥補單儲存邏輯以支援精確批號轉移。 5. 整合 FEATURES.md 至 README.md。
This commit is contained in:
168
app/Modules/Inventory/Services/InventoryService.php
Normal file
168
app/Modules/Inventory/Services/InventoryService.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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'])->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::find($id);
|
||||
}
|
||||
|
||||
public function getProductsByIds(array $ids)
|
||||
{
|
||||
return Product::whereIn('id', $ids)->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): void
|
||||
{
|
||||
DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason) {
|
||||
$inventories = Inventory::where('product_id', $productId)
|
||||
->where('warehouse_id', $warehouseId)
|
||||
->where('quantity', '>', 0)
|
||||
->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) {
|
||||
// 這裡可以選擇報錯或允許負庫存,目前為了嚴謹拋出異常
|
||||
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;
|
||||
|
||||
$inventory->quantity += $data['quantity'];
|
||||
// 更新其他可能變更的欄位 (如最後入庫日)
|
||||
$inventory->arrival_date = $data['arrival_date'] ?? $inventory->arrival_date;
|
||||
$inventory->save();
|
||||
} else {
|
||||
// 若不存在,則建立新紀錄
|
||||
$inventory = Inventory::create([
|
||||
'warehouse_id' => $data['warehouse_id'],
|
||||
'product_id' => $data['product_id'],
|
||||
'quantity' => $data['quantity'],
|
||||
'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'],
|
||||
'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);
|
||||
$inventory->refresh();
|
||||
|
||||
\App\Modules\Inventory\Models\InventoryTransaction::create([
|
||||
'inventory_id' => $inventory->id,
|
||||
'type' => '出庫',
|
||||
'quantity' => -$quantity,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $inventory->quantity,
|
||||
'reason' => $reason ?? '庫存扣減',
|
||||
'reference_type' => $referenceType,
|
||||
'reference_id' => $referenceId,
|
||||
'user_id' => auth()->id(),
|
||||
'actual_time' => now(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user