feat: [재공품] STOCK 작업지시 dynamic_bom 생성 + 자재 매칭 구현

- BendingCodeService: lengthCodeToMm() public static 메서드 추가
- BendingInfoBuilder: buildDynamicBomForStockItem() 신규 메서드
  - bending_lot(prodCode/specCode/lengthCode) + partKey → BD 품목 코드 → dynamic_bom 엔트리 생성
  - PrefixResolver 활용하여 정확한 prefix 매핑
- OrderService: STOCK 확정 시 bending_lot 기반 dynamic_bom 자동 생성
- WorkOrderService: 기존 STOCK 호환 fallback (getMaterials + getMaterialsForItem)
  - dynamic_bom 없는 기존 재공품도 bending_lot.material로 원자재 검색
This commit is contained in:
김보곤
2026-03-22 14:54:11 +09:00
parent af69f2df0c
commit a8b04e15c3
4 changed files with 305 additions and 1 deletions

View File

@@ -1782,6 +1782,40 @@ public function getMaterials(int $workOrderId): array
];
}
// STOCK 호환: item_id 없는 기존 재공품 → bending_lot.material 기반 원자재 검색
if (empty($materialItems) && ! $woItem->item_id) {
$woBendingInfo = $workOrder->options['bending_info'] ?? [];
if (! empty($woBendingInfo['isStockProduction'])) {
$salesOrder = $workOrder->salesOrder ?? \App\Models\Orders\Order::find($workOrder->sales_order_id);
$bendingLot = $salesOrder?->options['bending_lot'] ?? null;
$material = $bendingLot['material'] ?? null;
$lengthCode = $bendingLot['length_code'] ?? null;
$prodCode = $bendingLot['prod_code'] ?? '';
if ($material && $lengthCode) {
$lengthMm = \App\Services\BendingCodeService::lengthCodeToMm($prodCode, $lengthCode);
if (preg_match('/^([A-Za-zㄱ-ㅎ가-힣]+)\s*(\d+\.?\d*)/u', $material, $matMatch)) {
$matName = $matMatch[1];
$matThickness = (float) $matMatch[2];
$rawItems = \App\Models\Items\Item::where('tenant_id', $tenantId)
->where('item_type', 'RM')
->where('name', 'LIKE', "%{$matName}{$matThickness}%")
->get();
foreach ($rawItems as $rawItem) {
if ($lengthMm > 0 && ! str_contains($rawItem->name, (string) $lengthMm)) {
continue;
}
$materialItems[] = [
'item' => $rawItem,
'bom_qty' => 1,
'required_qty' => $woItem->quantity ?? 1,
];
}
}
}
}
}
// 기존 방식: item_id 기준 합산
foreach ($materialItems as $matInfo) {
$itemId = $matInfo['item']->id;
@@ -4072,6 +4106,46 @@ public function getMaterialsForItem(int $workOrderId, int $itemId): array
}
}
// ④ STOCK 호환: dynamic_bom/BOM/item 모두 없는 기존 재공품 → bending_lot.material 기반 원자재 검색
if (empty($materialItems) && ! $woItem->item_id) {
$woBendingInfo = $workOrder->options['bending_info'] ?? [];
if (! empty($woBendingInfo['isStockProduction'])) {
$salesOrder = \App\Models\Orders\Order::find($workOrder->sales_order_id);
$bendingLot = $salesOrder?->options['bending_lot'] ?? null;
$material = $bendingLot['material'] ?? null;
$lengthCode = $bendingLot['length_code'] ?? null;
$prodCode = $bendingLot['prod_code'] ?? '';
if ($material && $lengthCode) {
$lengthMm = \App\Services\BendingCodeService::lengthCodeToMm($prodCode, $lengthCode);
// material "SUS 1.2T" 또는 "EGI 1.55T" → 재질명 + 두께 파싱
if (preg_match('/^([A-Za-zㄱ-ㅎ가-힣]+)\s*(\d+\.?\d*)/u', $material, $matMatch)) {
$matName = $matMatch[1];
$matThickness = (float) $matMatch[2];
// items 테이블에서 RM(원자재) 검색: 이름에 재질명 + 두께 포함
$rawItems = \App\Models\Items\Item::where('tenant_id', $tenantId)
->where('item_type', 'RM')
->where('name', 'LIKE', "%{$matName}{$matThickness}%")
->get();
// 길이 조건: 원자재 이름에 길이(mm) 포함 여부
foreach ($rawItems as $rawItem) {
if ($lengthMm > 0 && ! str_contains($rawItem->name, (string) $lengthMm)) {
continue;
}
$materialItems[] = [
'item' => $rawItem,
'bom_qty' => 1,
'required_qty' => $woItem->quantity ?? 1,
];
}
}
}
}
}
// 이미 투입된 수량 조회 (item_id별 SUM)
$inputtedQties = WorkOrderMaterialInput::where('tenant_id', $tenantId)
->where('work_order_id', $workOrderId)