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-02-10 10:47:31 +08:00
/**
* 即時庫存查詢:統計卡片 + 分頁明細
*/
public function getStockQueryData ( array $filters = [], int $perPage = 10 ) : array
{
$today = now () -> toDateString ();
$expiryThreshold = now () -> addDays ( 30 ) -> toDateString ();
// 基礎查詢
$query = Inventory :: query ()
-> join ( 'products' , 'inventories.product_id' , '=' , 'products.id' )
-> join ( 'warehouses' , 'inventories.warehouse_id' , '=' , 'warehouses.id' )
-> leftJoin ( 'categories' , 'products.category_id' , '=' , 'categories.id' )
-> leftJoin ( 'warehouse_product_safety_stocks as ss' , function ( $join ) {
$join -> on ( 'inventories.warehouse_id' , '=' , 'ss.warehouse_id' )
-> on ( 'inventories.product_id' , '=' , 'ss.product_id' );
})
2026-02-10 11:15:08 +08:00
-> whereNull ( 'inventories.deleted_at' )
-> select ([
2026-02-10 10:47:31 +08:00
'inventories.id' ,
'inventories.warehouse_id' ,
'inventories.product_id' ,
'inventories.quantity' ,
'inventories.batch_number' ,
'inventories.expiry_date' ,
'inventories.location' ,
'inventories.quality_status' ,
'products.code as product_code' ,
'products.name as product_name' ,
'categories.name as category_name' ,
'warehouses.name as warehouse_name' ,
'ss.safety_stock' ,
]);
// 篩選:倉庫
if ( ! empty ( $filters [ 'warehouse_id' ])) {
$query -> where ( 'inventories.warehouse_id' , $filters [ 'warehouse_id' ]);
}
// 篩選:分類
if ( ! empty ( $filters [ 'category_id' ])) {
$query -> where ( 'products.category_id' , $filters [ 'category_id' ]);
}
// 篩選:關鍵字(商品代碼或名稱)
if ( ! empty ( $filters [ 'search' ])) {
$search = $filters [ 'search' ];
$query -> where ( function ( $q ) use ( $search ) {
$q -> where ( 'products.code' , 'like' , " % { $search } % " )
-> orWhere ( 'products.name' , 'like' , " % { $search } % " );
});
}
// 篩選:狀態 (改為對齊聚合統計的判斷標準)
if ( ! empty ( $filters [ 'status' ])) {
switch ( $filters [ 'status' ]) {
case 'low_stock' :
$query -> whereIn ( DB :: raw ( '(inventories.warehouse_id, inventories.product_id)' ), function ( $sub ) {
$sub -> select ( 'i2.warehouse_id' , 'i2.product_id' )
-> from ( 'inventories as i2' )
-> join ( 'warehouse_product_safety_stocks as ss2' , function ( $join ) {
$join -> on ( 'i2.warehouse_id' , '=' , 'ss2.warehouse_id' )
-> on ( 'i2.product_id' , '=' , 'ss2.product_id' );
})
-> whereNull ( 'i2.deleted_at' )
-> groupBy ( 'i2.warehouse_id' , 'i2.product_id' , 'ss2.safety_stock' )
-> havingRaw ( 'SUM(i2.quantity) <= ss2.safety_stock' );
});
break ;
case 'negative' :
$query -> whereIn ( DB :: raw ( '(inventories.warehouse_id, inventories.product_id)' ), function ( $sub ) {
$sub -> select ( 'i2.warehouse_id' , 'i2.product_id' )
-> from ( 'inventories as i2' )
-> whereNull ( 'i2.deleted_at' )
-> groupBy ( 'i2.warehouse_id' , 'i2.product_id' )
-> havingRaw ( 'SUM(i2.quantity) < 0' );
});
break ;
case 'expiring' :
$query -> whereNotNull ( 'inventories.expiry_date' )
-> where ( 'inventories.expiry_date' , '>' , $today )
-> where ( 'inventories.expiry_date' , '<=' , $expiryThreshold );
break ;
case 'expired' :
$query -> whereNotNull ( 'inventories.expiry_date' )
-> where ( 'inventories.expiry_date' , '<=' , $today );
break ;
case 'abnormal' :
// 只要該「倉庫-品項」對應的總庫存有低庫存、負庫存,或該批次已過期/即將過期
$query -> where ( function ( $q ) use ( $today , $expiryThreshold ) {
// 1. 低庫存或負庫存 (依聚合判斷)
$q -> whereIn ( DB :: raw ( '(inventories.warehouse_id, inventories.product_id)' ), function ( $sub ) {
$sub -> select ( 'i3.warehouse_id' , 'i3.product_id' )
-> from ( 'inventories as i3' )
-> leftJoin ( 'warehouse_product_safety_stocks as ss3' , function ( $join ) {
$join -> on ( 'i3.warehouse_id' , '=' , 'ss3.warehouse_id' )
-> on ( 'i3.product_id' , '=' , 'ss3.product_id' );
})
-> whereNull ( 'i3.deleted_at' )
-> groupBy ( 'i3.warehouse_id' , 'i3.product_id' , 'ss3.safety_stock' )
-> havingRaw ( 'SUM(i3.quantity) < 0 OR (ss3.safety_stock IS NOT NULL AND SUM(i3.quantity) <= ss3.safety_stock)' );
})
// 2. 或該批次效期異常
-> orWhere ( function ( $q_batch ) use ( $expiryThreshold ) {
$q_batch -> whereNotNull ( 'inventories.expiry_date' )
-> where ( 'inventories.expiry_date' , '<=' , $expiryThreshold );
});
});
break ;
}
}
// 排序
$sortBy = $filters [ 'sort_by' ] ? ? 'products.code' ;
$sortOrder = $filters [ 'sort_order' ] ? ? 'asc' ;
$allowedSorts = [ 'products.code' , 'products.name' , 'warehouses.name' , 'inventories.quantity' , 'inventories.expiry_date' ];
if ( in_array ( $sortBy , $allowedSorts )) {
$query -> orderBy ( $sortBy , $sortOrder );
} else {
$query -> orderBy ( 'products.code' , 'asc' );
}
2026-02-10 11:15:08 +08:00
// 統計卡片(預設無篩選條件下的全域統計,改為明細筆數計數以對齊顯示)
// 1. 庫存明細總數
2026-02-10 10:47:31 +08:00
$totalItems = DB :: table ( 'inventories' )
-> whereNull ( 'deleted_at' )
-> count ();
2026-02-10 11:15:08 +08:00
// 2. 低庫存明細數:只要該明細所屬的「倉庫+商品」總量低於安全庫存,則所有相關明細都計入
$lowStockCount = DB :: table ( 'inventories as i' )
-> join ( 'warehouse_product_safety_stocks as ss' , function ( $join ) {
$join -> on ( 'i.warehouse_id' , '=' , 'ss.warehouse_id' )
-> on ( 'i.product_id' , '=' , 'ss.product_id' );
})
-> whereNull ( 'i.deleted_at' )
-> whereIn ( DB :: raw ( '(i.warehouse_id, i.product_id)' ), function ( $sub ) {
$sub -> select ( 'i2.warehouse_id' , 'i2.product_id' )
-> from ( 'inventories as i2' )
-> whereNull ( 'i2.deleted_at' )
-> groupBy ( 'i2.warehouse_id' , 'i2.product_id' )
-> havingRaw ( 'SUM(i2.quantity) <= (SELECT safety_stock FROM warehouse_product_safety_stocks WHERE warehouse_id = i2.warehouse_id AND product_id = i2.product_id LIMIT 1)' );
})
-> count ();
// 3. 負庫存明細數
$negativeCount = DB :: table ( 'inventories as i' )
-> whereNull ( 'i.deleted_at' )
-> whereIn ( DB :: raw ( '(i.warehouse_id, i.product_id)' ), function ( $sub ) {
$sub -> select ( 'i2.warehouse_id' , 'i2.product_id' )
-> from ( 'inventories as i2' )
-> whereNull ( 'i2.deleted_at' )
-> groupBy ( 'i2.warehouse_id' , 'i2.product_id' )
-> havingRaw ( 'SUM(i2.quantity) < 0' );
})
2026-02-10 10:47:31 +08:00
-> count ();
2026-02-10 11:15:08 +08:00
// 4. 即將過期明細數
2026-02-10 10:47:31 +08:00
$expiringCount = DB :: table ( 'inventories' )
-> whereNull ( 'deleted_at' )
-> whereNotNull ( 'expiry_date' )
-> where ( 'expiry_date' , '<=' , $expiryThreshold )
2026-02-10 11:15:08 +08:00
-> count ();
2026-02-10 10:47:31 +08:00
// 分頁
$paginated = $query -> paginate ( $perPage ) -> withQueryString ();
// 為每筆紀錄附加最後入庫/出庫時間 + 狀態
2026-02-10 11:15:08 +08:00
$items = collect ( $paginated -> items ()) -> map ( function ( $item ) use ( $today , $expiryThreshold ) {
$lastIn = \App\Modules\Inventory\Models\InventoryTransaction :: where ( 'inventory_id' , $item -> id )
-> where ( 'type' , '入庫' )
-> orderByDesc ( 'actual_time' )
-> value ( 'actual_time' );
$lastOut = \App\Modules\Inventory\Models\InventoryTransaction :: where ( 'inventory_id' , $item -> id )
-> where ( 'type' , '出庫' )
-> orderByDesc ( 'actual_time' )
-> value ( 'actual_time' );
2026-02-10 11:09:22 +08:00
// 計算狀態
2026-02-10 10:47:31 +08:00
$statuses = [];
if ( $item -> quantity < 0 ) {
$statuses [] = 'negative' ;
}
if ( $item -> safety_stock !== null && $item -> quantity <= $item -> safety_stock && $item -> quantity >= 0 ) {
$statuses [] = 'low_stock' ;
}
if ( $item -> expiry_date ) {
if ( $item -> expiry_date <= $today ) {
$statuses [] = 'expired' ;
} elseif ( $item -> expiry_date <= $expiryThreshold ) {
$statuses [] = 'expiring' ;
}
}
if ( empty ( $statuses )) {
$statuses [] = 'normal' ;
}
return [
'id' => $item -> id ,
'product_code' => $item -> product_code ,
'product_name' => $item -> product_name ,
'category_name' => $item -> category_name ,
'warehouse_name' => $item -> warehouse_name ,
'batch_number' => $item -> batch_number ,
'quantity' => $item -> quantity ,
'safety_stock' => $item -> safety_stock ,
'expiry_date' => $item -> expiry_date ? \Carbon\Carbon :: parse ( $item -> expiry_date ) -> toDateString () : null ,
'location' => $item -> location ,
2026-02-10 11:09:22 +08:00
'quality_status' => $item -> quality_status ? ? null ,
2026-02-10 10:47:31 +08:00
'last_inbound' => $lastIn ? \Carbon\Carbon :: parse ( $lastIn ) -> toDateString () : null ,
'last_outbound' => $lastOut ? \Carbon\Carbon :: parse ( $lastOut ) -> toDateString () : null ,
'statuses' => $statuses ,
];
});
return [
'summary' => [
'totalItems' => $totalItems ,
'lowStockCount' => $lowStockCount ,
'negativeCount' => $negativeCount ,
'expiringCount' => $expiringCount ,
],
'data' => $items -> toArray (),
'pagination' => [
'total' => $paginated -> total (),
'per_page' => $paginated -> perPage (),
'current_page' => $paginated -> currentPage (),
'last_page' => $paginated -> lastPage (),
'links' => $paginated -> linkCollection () -> toArray (),
],
];
}
2026-01-27 08:59:45 +08:00
public function getDashboardStats () : array
{
2026-02-10 10:47:31 +08:00
$today = now () -> toDateString ();
$expiryThreshold = now () -> addDays ( 30 ) -> toDateString ();
// 1. 庫存品項數 (Unique Product-Warehouse)
$totalItems = DB :: table ( 'inventories' )
-> whereNull ( 'deleted_at' )
-> distinct ()
-> count ( DB :: raw ( 'CONCAT(warehouse_id, "-", product_id)' ));
// 2. 低庫存 (品項計數)
2026-01-27 08:59:45 +08:00
$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 ();
2026-02-10 10:47:31 +08:00
// 3. 負庫存 (品項計數)
$negativeCount = DB :: table ( 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' ))
-> where ( 'total_qty' , '<' , 0 )
-> count ();
// 4. 即將過期 (品項計數)
$expiringCount = DB :: table ( 'inventories' )
-> whereNull ( 'deleted_at' )
-> whereNotNull ( 'expiry_date' )
-> where ( 'expiry_date' , '<=' , $expiryThreshold )
-> distinct ()
-> count ( DB :: raw ( 'CONCAT(warehouse_id, "-", product_id)' ));
// 異常庫存前 10 筆 (明細面依然以個別批次為主,供快速跳轉)
$abnormalItems = Inventory :: query ()
-> join ( 'products' , 'inventories.product_id' , '=' , 'products.id' )
-> join ( 'warehouses' , 'inventories.warehouse_id' , '=' , 'warehouses.id' )
-> leftJoin ( 'warehouse_product_safety_stocks as ss' , function ( $join ) {
$join -> on ( 'inventories.warehouse_id' , '=' , 'ss.warehouse_id' )
-> on ( 'inventories.product_id' , '=' , 'ss.product_id' );
})
-> whereNull ( 'inventories.deleted_at' )
-> where ( function ( $q ) use ( $today , $expiryThreshold ) {
// 1. 屬於低庫存或負庫存品項的批次
$q -> whereIn ( DB :: raw ( '(inventories.warehouse_id, inventories.product_id)' ), function ( $sub ) {
$sub -> select ( 'i3.warehouse_id' , 'i3.product_id' )
-> from ( 'inventories as i3' )
-> leftJoin ( 'warehouse_product_safety_stocks as ss3' , function ( $join ) {
$join -> on ( 'i3.warehouse_id' , '=' , 'ss3.warehouse_id' )
-> on ( 'i3.product_id' , '=' , 'ss3.product_id' );
})
-> whereNull ( 'i3.deleted_at' )
-> groupBy ( 'i3.warehouse_id' , 'i3.product_id' , 'ss3.safety_stock' )
-> havingRaw ( 'SUM(i3.quantity) < 0 OR (ss3.safety_stock IS NOT NULL AND SUM(i3.quantity) <= ss3.safety_stock)' );
})
// 2. 或單一批次效期異常
-> orWhere ( function ( $q2 ) use ( $expiryThreshold ) {
$q2 -> whereNotNull ( 'inventories.expiry_date' )
-> where ( 'inventories.expiry_date' , '<=' , $expiryThreshold );
});
})
-> select ([
'inventories.id' ,
'inventories.quantity' ,
'inventories.expiry_date' ,
'products.code as product_code' ,
'products.name as product_name' ,
'warehouses.name as warehouse_name' ,
'ss.safety_stock' ,
])
-> orderBy ( 'inventories.id' , 'desc' )
-> limit ( 10 )
-> get ()
-> map ( function ( $item ) use ( $today , $expiryThreshold ) {
$statuses = [];
if ( $item -> quantity < 0 ) {
$statuses [] = 'negative' ;
}
if ( $item -> safety_stock !== null && $item -> quantity <= $item -> safety_stock && $item -> quantity >= 0 ) {
$statuses [] = 'low_stock' ;
}
if ( $item -> expiry_date ) {
if ( $item -> expiry_date <= $today ) {
$statuses [] = 'expired' ;
} elseif ( $item -> expiry_date <= $expiryThreshold ) {
$statuses [] = 'expiring' ;
}
}
return [
'id' => $item -> id ,
'product_code' => $item -> product_code ,
'product_name' => $item -> product_name ,
'warehouse_name' => $item -> warehouse_name ,
'quantity' => $item -> quantity ,
'safety_stock' => $item -> safety_stock ,
'expiry_date' => $item -> expiry_date ,
'statuses' => $statuses ,
];
})
-> toArray ();
2026-01-27 08:59:45 +08:00
return [
2026-02-10 10:47:31 +08:00
'productsCount' => $totalItems ,
2026-01-27 08:59:45 +08:00
'warehousesCount' => Warehouse :: count (),
'lowStockCount' => $lowStockCount ,
2026-02-10 10:47:31 +08:00
'negativeCount' => $negativeCount ,
'expiringCount' => $expiringCount ,
2026-01-27 08:59:45 +08:00
'totalInventoryQuantity' => Inventory :: sum ( 'quantity' ),
2026-02-10 10:47:31 +08:00
'abnormalItems' => $abnormalItems ,
2026-01-27 08:59:45 +08:00
];
}
2026-01-26 14:59:24 +08:00
}
2026-02-10 10:47:31 +08:00