fix(API): 자재투입 모달 중복 로트 버그 수정

동일 자재가 여러 작업지시 품목에 걸쳐 있을 때 StockLot이 중복 표시되던 문제 수정.
Phase 1(유니크 자재 수집) → Phase 2(로트 조회) 구조로 변경하여 중복 제거 및 필요수량 합산.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 16:47:50 +09:00
parent f9de25257f
commit 5eaa5f036b

View File

@@ -1092,7 +1092,7 @@ public function updateItemStatus(int $workOrderId, int $itemId, string $status)
* 작업지시에 필요한 자재 목록 조회 (BOM 기반 + 로트별 재고)
*
* 작업지시 품목의 BOM 자재별로 StockLot(입고 로트)를 FIFO 순서로 반환합니다.
* 로트번호는 입고관리(Receiving)에서 생성된 실제 로트번호입니다.
* 동일 자재가 여러 작업지시 품목에 걸쳐 있으면 필요수량을 합산하고 로트는 중복 없이 반환합니다.
*
* @param int $workOrderId 작업지시 ID
* @return array 자재 목록 (로트 단위)
@@ -1109,8 +1109,8 @@ public function getMaterials(int $workOrderId): array
throw new NotFoundHttpException(__('error.not_found'));
}
$materials = [];
$rank = 1;
// Phase 1: 작업지시 품목들에서 유니크 자재 목록 수집 (item_id 기준 합산)
$uniqueMaterials = [];
foreach ($workOrder->items as $woItem) {
$materialItems = [];
@@ -1140,7 +1140,6 @@ public function getMaterials(int $workOrderId): array
'item' => $childItem,
'bom_qty' => $bomQty,
'required_qty' => $bomQty * ($woItem->quantity ?? 1),
'work_order_item_id' => $woItem->id,
];
}
}
@@ -1152,73 +1151,83 @@ public function getMaterials(int $workOrderId): array
'item' => $woItem->item,
'bom_qty' => 1,
'required_qty' => $woItem->quantity ?? 1,
'work_order_item_id' => $woItem->id,
];
}
// 각 자재별로 StockLot(입고 로트) 조회
// 유니크 자재 수집 (같은 item_id면 required_qty 합산)
foreach ($materialItems as $matInfo) {
$materialItem = $matInfo['item'];
// Stock 조회
$stock = \App\Models\Tenants\Stock::where('tenant_id', $tenantId)
->where('item_id', $materialItem->id)
->first();
if ($stock) {
// 가용 로트를 FIFO 순서로 조회
$lots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId)
->where('stock_id', $stock->id)
->where('status', 'available')
->where('available_qty', '>', 0)
->orderBy('fifo_order', 'asc')
->get();
foreach ($lots as $lot) {
$materials[] = [
'stock_lot_id' => $lot->id,
'item_id' => $materialItem->id,
'work_order_item_id' => $matInfo['work_order_item_id'],
'lot_no' => $lot->lot_no,
'material_code' => $materialItem->code,
'material_name' => $materialItem->name,
'specification' => $materialItem->specification,
'unit' => $lot->unit ?? $materialItem->unit ?? 'EA',
'bom_qty' => $matInfo['bom_qty'],
'required_qty' => $matInfo['required_qty'],
'lot_qty' => (float) $lot->qty,
'lot_available_qty' => (float) $lot->available_qty,
'lot_reserved_qty' => (float) $lot->reserved_qty,
'receipt_date' => $lot->receipt_date,
'supplier' => $lot->supplier,
'fifo_rank' => $rank++,
];
}
$itemId = $matInfo['item']->id;
if (isset($uniqueMaterials[$itemId])) {
$uniqueMaterials[$itemId]['required_qty'] += $matInfo['required_qty'];
} else {
$uniqueMaterials[$itemId] = $matInfo;
}
}
}
// 가용 로트가 없는 경우 자재 정보만 반환 (재고 없음 표시)
$hasLots = collect($materials)->where('item_id', $materialItem->id)->isNotEmpty();
if (! $hasLots) {
// Phase 2: 유니크 자재별로 StockLot 조회
$materials = [];
$rank = 1;
foreach ($uniqueMaterials as $matInfo) {
$materialItem = $matInfo['item'];
$stock = \App\Models\Tenants\Stock::where('tenant_id', $tenantId)
->where('item_id', $materialItem->id)
->first();
$lotsFound = false;
if ($stock) {
$lots = \App\Models\Tenants\StockLot::where('tenant_id', $tenantId)
->where('stock_id', $stock->id)
->where('status', 'available')
->where('available_qty', '>', 0)
->orderBy('fifo_order', 'asc')
->get();
foreach ($lots as $lot) {
$lotsFound = true;
$materials[] = [
'stock_lot_id' => null,
'stock_lot_id' => $lot->id,
'item_id' => $materialItem->id,
'work_order_item_id' => $matInfo['work_order_item_id'],
'lot_no' => null,
'lot_no' => $lot->lot_no,
'material_code' => $materialItem->code,
'material_name' => $materialItem->name,
'specification' => $materialItem->specification,
'unit' => $materialItem->unit ?? 'EA',
'unit' => $lot->unit ?? $materialItem->unit ?? 'EA',
'bom_qty' => $matInfo['bom_qty'],
'required_qty' => $matInfo['required_qty'],
'lot_qty' => 0,
'lot_available_qty' => 0,
'lot_reserved_qty' => 0,
'receipt_date' => null,
'supplier' => null,
'lot_qty' => (float) $lot->qty,
'lot_available_qty' => (float) $lot->available_qty,
'lot_reserved_qty' => (float) $lot->reserved_qty,
'receipt_date' => $lot->receipt_date,
'supplier' => $lot->supplier,
'fifo_rank' => $rank++,
];
}
}
// 가용 로트가 없는 경우 자재 정보만 반환 (재고 없음 표시)
if (! $lotsFound) {
$materials[] = [
'stock_lot_id' => null,
'item_id' => $materialItem->id,
'lot_no' => null,
'material_code' => $materialItem->code,
'material_name' => $materialItem->name,
'specification' => $materialItem->specification,
'unit' => $materialItem->unit ?? 'EA',
'bom_qty' => $matInfo['bom_qty'],
'required_qty' => $matInfo['required_qty'],
'lot_qty' => 0,
'lot_available_qty' => 0,
'lot_reserved_qty' => 0,
'receipt_date' => null,
'supplier' => null,
'fifo_rank' => $rank++,
];
}
}
return $materials;