2026-01-26 14:59:24 +08:00
< ? 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 ()
{
2026-01-27 17:23:31 +08:00
return Product :: with ([ 'baseUnit' , 'largeUnit' ]) -> get ();
2026-01-26 14:59:24 +08:00
}
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 )
{
2026-01-27 17:23:31 +08:00
return Product :: with ([ 'baseUnit' , 'largeUnit' ]) -> find ( $id );
2026-01-26 14:59:24 +08:00
}
public function getProductsByIds ( array $ids )
{
2026-01-27 17:23:31 +08:00
return Product :: whereIn ( 'id' , $ids ) -> with ([ 'baseUnit' , 'largeUnit' ]) -> get ();
2026-01-26 14:59:24 +08:00
}
2026-01-27 08:59:45 +08:00
public function getProductsByName ( string $name )
{
2026-01-27 17:23:31 +08:00
return Product :: where ( 'name' , 'like' , " % { $name } % " ) -> with ([ 'baseUnit' , 'largeUnit' ]) -> get ();
2026-01-27 08:59:45 +08:00
}
2026-01-26 14:59:24 +08:00
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 ;
}
2026-02-09 14:36:47 +08:00
public function decreaseStock ( int $productId , int $warehouseId , float $quantity , ? string $reason = null , bool $force = false , ? string $slot = null ) : void
2026-01-26 14:59:24 +08:00
{
2026-02-09 14:36:47 +08:00
DB :: transaction ( function () use ( $productId , $warehouseId , $quantity , $reason , $force , $slot ) {
$query = Inventory :: where ( 'product_id' , $productId )
2026-01-26 14:59:24 +08:00
-> where ( 'warehouse_id' , $warehouseId )
2026-02-09 14:36:47 +08:00
-> where ( 'quantity' , '>' , 0 );
if ( $slot ) {
$query -> where ( 'location' , $slot );
}
$inventories = $query -> orderBy ( 'arrival_date' , 'asc' )
2026-01-26 14:59:24 +08:00
-> 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 ) {
2026-02-06 11:56:29 +08:00
if ( $force ) {
2026-02-09 14:36:47 +08:00
// 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 ();
2026-02-06 11:56:29 +08:00
if ( ! $inventory ) {
$inventory = Inventory :: create ([
'warehouse_id' => $warehouseId ,
'product_id' => $productId ,
2026-02-09 14:36:47 +08:00
'location' => $slot ,
2026-02-06 11:56:29 +08:00
'quantity' => 0 ,
'unit_cost' => 0 ,
'total_value' => 0 ,
2026-02-09 14:36:47 +08:00
'batch_number' => 'POS-AUTO-' . ( $slot ? $slot . '-' : '' ) . time (),
2026-02-06 11:56:29 +08:00
'arrival_date' => now (),
'origin_country' => 'TW' ,
'quality_status' => 'normal' ,
]);
}
$this -> decreaseInventoryQuantity ( $inventory -> id , $remainingToDecrease , $reason );
} else {
throw new \Exception ( " 庫存不足,無法扣除所有請求的數量。 " );
}
2026-01-26 14:59:24 +08:00
}
});
}
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 ;
2026-01-26 17:27:34 +08:00
// 加權平均成本計算 (可選,這裡先採簡單邏輯:若有新成本則更新,否則沿用)
// 若本次入庫有指定成本,則更新該批次單價 (假設同批號成本相同)
if ( isset ( $data [ 'unit_cost' ])) {
$inventory -> unit_cost = $data [ 'unit_cost' ];
}
2026-01-26 14:59:24 +08:00
$inventory -> quantity += $data [ 'quantity' ];
2026-01-26 17:27:34 +08:00
// 更新總價值
$inventory -> total_value = $inventory -> quantity * $inventory -> unit_cost ;
2026-01-26 14:59:24 +08:00
// 更新其他可能變更的欄位 (如最後入庫日)
$inventory -> arrival_date = $data [ 'arrival_date' ] ? ? $inventory -> arrival_date ;
$inventory -> save ();
} else {
// 若不存在,則建立新紀錄
2026-01-26 17:27:34 +08:00
$unitCost = $data [ 'unit_cost' ] ? ? 0 ;
2026-01-26 14:59:24 +08:00
$inventory = Inventory :: create ([
'warehouse_id' => $data [ 'warehouse_id' ],
'product_id' => $data [ 'product_id' ],
'quantity' => $data [ 'quantity' ],
2026-01-26 17:27:34 +08:00
'unit_cost' => $unitCost ,
'total_value' => $data [ 'quantity' ] * $unitCost ,
2026-01-26 14:59:24 +08:00
'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' ],
2026-01-26 17:27:34 +08:00
'unit_cost' => $inventory -> unit_cost , // 記錄當下成本
2026-01-26 14:59:24 +08:00
'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 ;
2026-01-26 17:27:34 +08:00
$inventory -> decrement ( 'quantity' , $quantity ); // decrement 不會自動觸發 total_value 更新
// 需要手動更新總價值
2026-01-26 14:59:24 +08:00
$inventory -> refresh ();
2026-01-26 17:27:34 +08:00
$inventory -> total_value = $inventory -> quantity * $inventory -> unit_cost ;
$inventory -> save ();
2026-01-26 14:59:24 +08:00
\App\Modules\Inventory\Models\InventoryTransaction :: create ([
'inventory_id' => $inventory -> id ,
'type' => '出庫' ,
'quantity' => - $quantity ,
2026-01-26 17:27:34 +08:00
'unit_cost' => $inventory -> unit_cost , // 記錄出庫時的成本
2026-01-26 14:59:24 +08:00
'balance_before' => $balanceBefore ,
'balance_after' => $inventory -> quantity ,
'reason' => $reason ? ? '庫存扣減' ,
'reference_type' => $referenceType ,
'reference_id' => $referenceId ,
'user_id' => auth () -> id (),
'actual_time' => now (),
]);
});
}
2026-01-27 08:59:45 +08:00
2026-02-05 09:33:36 +08:00
public function findInventoryByBatch ( int $warehouseId , int $productId , ? string $batchNumber )
{
return Inventory :: where ( 'warehouse_id' , $warehouseId )
-> where ( 'product_id' , $productId )
-> where ( 'batch_number' , $batchNumber )
-> first ();
}
2026-01-27 08:59:45 +08:00
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' ),
];
}
2026-01-26 14:59:24 +08:00
}