Feat: Stat stock+

This commit is contained in:
kevin 2026-05-13 10:07:03 +03:00
parent 050a38c6bd
commit 39c21d3d09
2 changed files with 137 additions and 1 deletions

View File

@ -6,6 +6,7 @@ namespace App\Repositories;
use App\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class ProductRepository extends BaseRepository implements ProductRepositoryInterface
{
@ -132,15 +133,148 @@ class ProductRepository extends BaseRepository implements ProductRepositoryInter
$expiringProducts = $this->model->where('date_expiration', '<=', now()->addDays(30)->toDateString())
->where('date_expiration', '>=', now()->toDateString())
->count();
$totalValue = $this->model->sum(\DB::raw('stock_actuel * prix_unitaire'));
$stockItems = DB::table('stock_items')
->join('products', 'stock_items.product_id', '=', 'products.id');
$totalValue = $stockItems
->selectRaw('COALESCE(SUM(stock_items.qty_on_hand_base * products.prix_unitaire), 0) as total_value')
->value('total_value');
$stockAlertCount = $stockItems
->whereRaw('stock_items.qty_on_hand_base <= stock_items.safety_stock_base')
->count();
$stockRotation = $this->getRotationByProduct();
$warehouseMovements = $this->getWarehouseMovements();
$stockByWarehouse = $this->getStockByWarehouse();
return [
'total_products' => $totalProducts,
'low_stock_products' => $lowStockProducts,
'expiring_products' => $expiringProducts,
'total_value' => $totalValue,
'stock_alerts_count' => $stockAlertCount,
'stock_rotation_by_product' => $stockRotation,
'warehouse_movements' => $warehouseMovements,
'stock_by_warehouse' => $stockByWarehouse,
];
}
private function getRotationByProduct(): array
{
$oneYearAgo = now()->subYear();
$movementSubquery = DB::table('stock_moves')
->selectRaw('product_id, SUM(qty_base) as moved_qty')
->where('moved_at', '>=', $oneYearAgo)
->groupBy('product_id');
$stockOnHandSubquery = DB::table('stock_items')
->selectRaw('product_id, SUM(qty_on_hand_base) as qty_on_hand')
->groupBy('product_id');
$rotation = DB::table('products')
->leftJoinSub($movementSubquery, 'movement', function ($join) {
$join->on('products.id', '=', 'movement.product_id');
})
->leftJoinSub($stockOnHandSubquery, 'stock', function ($join) {
$join->on('products.id', '=', 'stock.product_id');
})
->whereNotNull('movement.moved_qty')
->selectRaw(
'products.id as product_id, products.nom as product_name, ' .
'movement.moved_qty, COALESCE(stock.qty_on_hand, 0) as qty_on_hand, ' .
'CASE WHEN COALESCE(stock.qty_on_hand, 0) > 0 THEN ROUND(movement.moved_qty / stock.qty_on_hand, 2) ELSE null END as rotation_rate'
)
->orderByDesc('rotation_rate')
->limit(10)
->get()
->map(function ($row) {
return [
'product_id' => (int) $row->product_id,
'product_name' => $row->product_name,
'moved_qty_last_12_months' => (float) $row->moved_qty,
'qty_on_hand' => (float) $row->qty_on_hand,
'rotation_rate' => $row->rotation_rate !== null ? (float) $row->rotation_rate : null,
];
})
->toArray();
return $rotation;
}
private function getWarehouseMovements(): array
{
$incoming = DB::table('stock_moves')
->selectRaw('to_warehouse_id as warehouse_id, COUNT(*) as incoming_moves, SUM(qty_base) as incoming_qty')
->whereNotNull('to_warehouse_id')
->groupBy('to_warehouse_id');
$outgoing = DB::table('stock_moves')
->selectRaw('from_warehouse_id as warehouse_id, COUNT(*) as outgoing_moves, SUM(qty_base) as outgoing_qty')
->whereNotNull('from_warehouse_id')
->groupBy('from_warehouse_id');
$warehouses = DB::table('warehouses')
->leftJoinSub($incoming, 'incoming', function ($join) {
$join->on('warehouses.id', '=', 'incoming.warehouse_id');
})
->leftJoinSub($outgoing, 'outgoing', function ($join) {
$join->on('warehouses.id', '=', 'outgoing.warehouse_id');
})
->select([
'warehouses.id as warehouse_id',
'warehouses.name as warehouse_name',
DB::raw('COALESCE(incoming.incoming_moves, 0) as incoming_moves'),
DB::raw('COALESCE(incoming.incoming_qty, 0) as incoming_qty'),
DB::raw('COALESCE(outgoing.outgoing_moves, 0) as outgoing_moves'),
DB::raw('COALESCE(outgoing.outgoing_qty, 0) as outgoing_qty'),
DB::raw('COALESCE(COALESCE(incoming.incoming_moves, 0) + COALESCE(outgoing.outgoing_moves, 0), 0) as total_moves'),
])
->get()
->map(function ($row) {
return [
'warehouse_id' => (int) $row->warehouse_id,
'warehouse_name' => $row->warehouse_name,
'incoming_moves' => (int) $row->incoming_moves,
'incoming_qty' => (float) $row->incoming_qty,
'outgoing_moves' => (int) $row->outgoing_moves,
'outgoing_qty' => (float) $row->outgoing_qty,
'total_moves' => (int) $row->total_moves,
];
})
->toArray();
return $warehouses;
}
private function getStockByWarehouse(): array
{
$warehouses = DB::table('warehouses')
->leftJoin('stock_items', 'warehouses.id', '=', 'stock_items.warehouse_id')
->leftJoin('products', 'stock_items.product_id', '=', 'products.id')
->selectRaw(
'warehouses.id as warehouse_id, warehouses.name as warehouse_name, ' .
'COALESCE(SUM(stock_items.qty_on_hand_base), 0) as qty_on_hand, ' .
'COALESCE(SUM(stock_items.qty_on_hand_base * products.prix_unitaire), 0) as stock_value, ' .
'SUM(CASE WHEN stock_items.qty_on_hand_base <= stock_items.safety_stock_base THEN 1 ELSE 0 END) as alert_count'
)
->groupBy('warehouses.id', 'warehouses.name')
->get()
->map(function ($row) {
return [
'warehouse_id' => (int) $row->warehouse_id,
'warehouse_name' => $row->warehouse_name,
'qty_on_hand' => (float) $row->qty_on_hand,
'stock_value' => (float) $row->stock_value,
'alert_count' => (int) $row->alert_count,
];
})
->toArray();
return $warehouses;
}
/**
* Find a default intervention product (where category has intervention=true)
*/

View File

@ -18,5 +18,7 @@ interface ProductRepositoryInterface extends BaseRepositoryInterface
public function getProductsByFournisseur(int $fournisseurId): LengthAwarePaginator;
public function getStatistics(): array;
public function findInterventionProduct(): ?\App\Models\Product;
}