feat(API): 부실채권, 재고, 입고 기능 개선
- BadDebt 컨트롤러/서비스 기능 확장 - StockService 재고 조회 로직 개선 - ProcessReceivingRequest 검증 규칙 수정 - Item, Order, CommonCode, Shipment 모델 업데이트 - TodayIssueObserverService 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,50 +2,77 @@
|
||||
|
||||
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();
|
||||
|
||||
$query = Stock::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with('item');
|
||||
// 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('item_code', 'like', "%{$search}%")
|
||||
->orWhere('item_name', 'like', "%{$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('item_type', $params['item_type']);
|
||||
$query->where('items.item_type', strtoupper($params['item_type']));
|
||||
}
|
||||
|
||||
// 재고 상태 필터
|
||||
// 재고 상태 필터 (Stock.status)
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
$query->whereHas('stock', function ($q) use ($params) {
|
||||
$q->where('status', $params['status']);
|
||||
});
|
||||
}
|
||||
|
||||
// 위치 필터
|
||||
// 위치 필터 (Stock.location)
|
||||
if (! empty($params['location'])) {
|
||||
$query->where('location', 'like', "%{$params['location']}%");
|
||||
$query->whereHas('stock', function ($q) use ($params) {
|
||||
$q->where('location', 'like', "%{$params['location']}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'item_code';
|
||||
$sortBy = $params['sort_by'] ?? 'code';
|
||||
$sortDir = $params['sort_dir'] ?? 'asc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 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;
|
||||
@@ -54,24 +81,43 @@ public function index(array $params): LengthAwarePaginator
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 통계 조회
|
||||
* 재고 통계 조회 (Item 기준)
|
||||
*/
|
||||
public function stats(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$totalItems = Stock::where('tenant_id', $tenantId)->count();
|
||||
|
||||
$normalCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'normal')
|
||||
// 전체 자재 품목 수 (Item 기준)
|
||||
$totalItems = Item::where('tenant_id', $tenantId)
|
||||
->materials()
|
||||
->count();
|
||||
|
||||
$lowCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'low')
|
||||
// 재고 상태별 카운트 (Stock이 있는 Item 기준)
|
||||
$normalCount = Item::where('items.tenant_id', $tenantId)
|
||||
->materials()
|
||||
->whereHas('stock', function ($q) {
|
||||
$q->where('status', 'normal');
|
||||
})
|
||||
->count();
|
||||
|
||||
$outCount = Stock::where('tenant_id', $tenantId)
|
||||
->where('status', 'out')
|
||||
$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 [
|
||||
@@ -79,57 +125,73 @@ public function stats(): array
|
||||
'normal_count' => $normalCount,
|
||||
'low_count' => $lowCount,
|
||||
'out_count' => $outCount,
|
||||
'no_stock_count' => $noStockCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 재고 상세 조회 (LOT 포함)
|
||||
* 재고 상세 조회 (Item 기준, LOT 포함)
|
||||
*/
|
||||
public function show(int $id): Stock
|
||||
public function show(int $id): Item
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Stock::query()
|
||||
return Item::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['item', 'lots' => function ($query) {
|
||||
->materials()
|
||||
->with(['stock.lots' => function ($query) {
|
||||
$query->orderBy('fifo_order');
|
||||
}])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목코드로 재고 조회
|
||||
* 품목코드로 재고 조회 (Item 기준)
|
||||
*/
|
||||
public function findByItemCode(string $itemCode): ?Stock
|
||||
public function findByItemCode(string $itemCode): ?Item
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return Stock::query()
|
||||
return Item::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_code', $itemCode)
|
||||
->materials()
|
||||
->where('code', $itemCode)
|
||||
->with('stock')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목유형별 통계
|
||||
* 품목유형별 통계 (Item.item_type 기준)
|
||||
*/
|
||||
public function statsByItemType(): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$stats = Stock::where('tenant_id', $tenantId)
|
||||
->selectRaw('item_type, COUNT(*) as count, SUM(stock_qty) as total_qty')
|
||||
// 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 (Stock::ITEM_TYPES as $key => $label) {
|
||||
$data = $stats->get($key);
|
||||
foreach (self::ITEM_TYPE_LABELS as $key => $label) {
|
||||
$itemData = $stats->get($key);
|
||||
$stockData = $stockQtys->get($key);
|
||||
$result[$key] = [
|
||||
'label' => $label,
|
||||
'count' => $data?->count ?? 0,
|
||||
'total_qty' => $data?->total_qty ?? 0,
|
||||
'count' => $itemData?->count ?? 0,
|
||||
'total_qty' => $stockData?->total_qty ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user