Files
sam-api/app/Services/StockService.php
권혁성 09db0da43b feat(API): 부실채권, 재고, 입고 기능 개선
- BadDebt 컨트롤러/서비스 기능 확장
- StockService 재고 조회 로직 개선
- ProcessReceivingRequest 검증 규칙 수정
- Item, Order, CommonCode, Shipment 모델 업데이트
- TodayIssueObserverService 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 21:32:23 +09:00

201 lines
5.8 KiB
PHP

<?php
namespace App\Services;
use App\Models\Items\Item;
use App\Models\Tenants\Stock;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class StockService extends Service
{
/**
* Item 타입 → 재고관리 라벨 매핑
*/
public const ITEM_TYPE_LABELS = [
'RM' => '원자재',
'SM' => '부자재',
'CS' => '소모품',
];
/**
* 재고 목록 조회 (Item 메인 + Stock LEFT JOIN)
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
// Item 테이블이 메인 (materials 타입만: SM, RM, CS)
$query = Item::query()
->where('items.tenant_id', $tenantId)
->materials() // SM, RM, CS만
->with('stock');
// 검색어 필터 (Item 기준)
if (! empty($params['search'])) {
$search = $params['search'];
$query->where(function ($q) use ($search) {
$q->where('items.code', 'like', "%{$search}%")
->orWhere('items.name', 'like', "%{$search}%");
});
}
// 품목유형 필터 (Item.item_type 기준: RM, SM, CS)
if (! empty($params['item_type'])) {
$query->where('items.item_type', strtoupper($params['item_type']));
}
// 재고 상태 필터 (Stock.status)
if (! empty($params['status'])) {
$query->whereHas('stock', function ($q) use ($params) {
$q->where('status', $params['status']);
});
}
// 위치 필터 (Stock.location)
if (! empty($params['location'])) {
$query->whereHas('stock', function ($q) use ($params) {
$q->where('location', 'like', "%{$params['location']}%");
});
}
// 정렬
$sortBy = $params['sort_by'] ?? 'code';
$sortDir = $params['sort_dir'] ?? 'asc';
// Item 테이블 기준 정렬 필드 매핑
$sortMapping = [
'item_code' => 'items.code',
'item_name' => 'items.name',
'item_type' => 'items.item_type',
'code' => 'items.code',
'name' => 'items.name',
];
$sortColumn = $sortMapping[$sortBy] ?? 'items.code';
$query->orderBy($sortColumn, $sortDir);
// 페이지네이션
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 재고 통계 조회 (Item 기준)
*/
public function stats(): array
{
$tenantId = $this->tenantId();
// 전체 자재 품목 수 (Item 기준)
$totalItems = Item::where('tenant_id', $tenantId)
->materials()
->count();
// 재고 상태별 카운트 (Stock이 있는 Item 기준)
$normalCount = Item::where('items.tenant_id', $tenantId)
->materials()
->whereHas('stock', function ($q) {
$q->where('status', 'normal');
})
->count();
$lowCount = Item::where('items.tenant_id', $tenantId)
->materials()
->whereHas('stock', function ($q) {
$q->where('status', 'low');
})
->count();
$outCount = Item::where('items.tenant_id', $tenantId)
->materials()
->whereHas('stock', function ($q) {
$q->where('status', 'out');
})
->count();
// 재고 정보가 없는 Item 수
$noStockCount = Item::where('items.tenant_id', $tenantId)
->materials()
->whereDoesntHave('stock')
->count();
return [
'total_items' => $totalItems,
'normal_count' => $normalCount,
'low_count' => $lowCount,
'out_count' => $outCount,
'no_stock_count' => $noStockCount,
];
}
/**
* 재고 상세 조회 (Item 기준, LOT 포함)
*/
public function show(int $id): Item
{
$tenantId = $this->tenantId();
return Item::query()
->where('tenant_id', $tenantId)
->materials()
->with(['stock.lots' => function ($query) {
$query->orderBy('fifo_order');
}])
->findOrFail($id);
}
/**
* 품목코드로 재고 조회 (Item 기준)
*/
public function findByItemCode(string $itemCode): ?Item
{
$tenantId = $this->tenantId();
return Item::query()
->where('tenant_id', $tenantId)
->materials()
->where('code', $itemCode)
->with('stock')
->first();
}
/**
* 품목유형별 통계 (Item.item_type 기준)
*/
public function statsByItemType(): array
{
$tenantId = $this->tenantId();
// Item 기준으로 통계 (materials 타입만)
$stats = Item::where('tenant_id', $tenantId)
->materials()
->selectRaw('item_type, COUNT(*) as count')
->groupBy('item_type')
->get()
->keyBy('item_type');
// 재고 수량 합계 (Stock이 있는 경우)
$stockQtys = Item::where('items.tenant_id', $tenantId)
->materials()
->join('stocks', 'items.id', '=', 'stocks.item_id')
->selectRaw('items.item_type, SUM(stocks.stock_qty) as total_qty')
->groupBy('items.item_type')
->get()
->keyBy('item_type');
$result = [];
foreach (self::ITEM_TYPE_LABELS as $key => $label) {
$itemData = $stats->get($key);
$stockData = $stockQtys->get($key);
$result[$key] = [
'label' => $label,
'count' => $itemData?->count ?? 0,
'total_qty' => $stockData?->total_qty ?? 0,
];
}
return $result;
}
}