feat(API): 수주 서비스 기능 개선
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Orders\OrderHistory;
|
||||
use App\Models\Orders\OrderNode;
|
||||
@@ -165,12 +166,86 @@ public function store(array $data)
|
||||
|
||||
$order = Order::create($data);
|
||||
|
||||
// quote_id가 있으면 OrderNode 생성 (개소별 사이즈 정보)
|
||||
$nodeMap = [];
|
||||
$productItems = [];
|
||||
if ($order->quote_id) {
|
||||
$quote = Quote::withoutGlobalScopes()->find($order->quote_id);
|
||||
if ($quote) {
|
||||
$ci = $quote->calculation_inputs ?? [];
|
||||
$productItems = $ci['items'] ?? [];
|
||||
$bomResults = $ci['bomResults'] ?? [];
|
||||
|
||||
foreach ($productItems as $idx => $locItem) {
|
||||
$bomResult = $bomResults[$idx] ?? null;
|
||||
$grandTotal = $bomResult['grand_total'] ?? 0;
|
||||
$bomVars = $bomResult['variables'] ?? [];
|
||||
$qty = (int) ($locItem['quantity'] ?? 1);
|
||||
$floor = $locItem['floor'] ?? '';
|
||||
$symbol = $locItem['code'] ?? '';
|
||||
|
||||
$nodeMap[$idx] = OrderNode::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'order_id' => $order->id,
|
||||
'parent_id' => null,
|
||||
'node_type' => 'location',
|
||||
'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}",
|
||||
'name' => trim("{$floor} {$symbol}") ?: '개소 '.($idx + 1),
|
||||
'status_code' => OrderNode::STATUS_PENDING,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $grandTotal,
|
||||
'total_price' => $grandTotal * $qty,
|
||||
'options' => [
|
||||
'floor' => $floor,
|
||||
'symbol' => $symbol,
|
||||
'product_code' => $locItem['productCode'] ?? null,
|
||||
'product_name' => $locItem['productName'] ?? null,
|
||||
'open_width' => $locItem['openWidth'] ?? null,
|
||||
'open_height' => $locItem['openHeight'] ?? null,
|
||||
'width' => $bomVars['W1'] ?? $locItem['openWidth'] ?? null,
|
||||
'height' => $bomVars['H1'] ?? $locItem['openHeight'] ?? null,
|
||||
'bom_result' => $bomResult,
|
||||
],
|
||||
'depth' => 0,
|
||||
'sort_order' => $idx,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 품목 저장
|
||||
foreach ($items as $index => $item) {
|
||||
$item['tenant_id'] = $tenantId;
|
||||
$item['serial_no'] = $index + 1; // 1부터 시작하는 순번
|
||||
$item['sort_order'] = $index;
|
||||
$this->calculateItemAmounts($item);
|
||||
|
||||
// item_id가 없고 item_code가 있으면 item_code로 조회하여 보완
|
||||
if (empty($item['item_id']) && ! empty($item['item_code'])) {
|
||||
$foundItem = Item::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $item['item_code'])
|
||||
->first();
|
||||
if ($foundItem) {
|
||||
$item['item_id'] = $foundItem->id;
|
||||
}
|
||||
}
|
||||
|
||||
// floor_code/symbol_code로 노드 매칭
|
||||
if (! empty($nodeMap) && ! empty($productItems)) {
|
||||
$floorCode = $item['floor_code'] ?? null;
|
||||
$symbolCode = $item['symbol_code'] ?? null;
|
||||
if ($floorCode && $symbolCode) {
|
||||
foreach ($productItems as $pidx => $pItem) {
|
||||
if (($pItem['floor'] ?? '') === $floorCode && ($pItem['code'] ?? '') === $symbolCode) {
|
||||
$item['order_node_id'] = $nodeMap[$pidx]->id ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$order->items()->create($item);
|
||||
}
|
||||
|
||||
@@ -546,33 +621,71 @@ public function createFromQuote(int $quoteId, array $data = [])
|
||||
|
||||
$order->save();
|
||||
|
||||
// calculation_inputs에서 제품 정보 추출 (floor, code)
|
||||
// 단일 제품인 경우 모든 BOM 품목에 동일한 floor_code/symbol_code 적용
|
||||
// calculation_inputs에서 제품 정보 추출
|
||||
$calculationInputs = $quote->calculation_inputs ?? [];
|
||||
$productItems = $calculationInputs['items'] ?? [];
|
||||
$bomResults = $calculationInputs['bomResults'] ?? [];
|
||||
|
||||
// 견적 품목을 수주 품목으로 변환
|
||||
// OrderNode 생성 (개소별)
|
||||
$nodeMap = [];
|
||||
foreach ($productItems as $idx => $locItem) {
|
||||
$bomResult = $bomResults[$idx] ?? null;
|
||||
$bomVars = $bomResult['variables'] ?? [];
|
||||
$grandTotal = $bomResult['grand_total'] ?? 0;
|
||||
$qty = (int) ($locItem['quantity'] ?? 1);
|
||||
$floor = $locItem['floor'] ?? '';
|
||||
$symbol = $locItem['code'] ?? '';
|
||||
|
||||
$nodeMap[$idx] = OrderNode::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'order_id' => $order->id,
|
||||
'parent_id' => null,
|
||||
'node_type' => 'location',
|
||||
'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}",
|
||||
'name' => trim("{$floor} {$symbol}") ?: '개소 '.($idx + 1),
|
||||
'status_code' => OrderNode::STATUS_PENDING,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $grandTotal,
|
||||
'total_price' => $grandTotal * $qty,
|
||||
'options' => [
|
||||
'floor' => $floor,
|
||||
'symbol' => $symbol,
|
||||
'product_code' => $locItem['productCode'] ?? null,
|
||||
'product_name' => $locItem['productName'] ?? null,
|
||||
'open_width' => $locItem['openWidth'] ?? null,
|
||||
'open_height' => $locItem['openHeight'] ?? null,
|
||||
'width' => $bomVars['W1'] ?? $locItem['openWidth'] ?? null,
|
||||
'height' => $bomVars['H1'] ?? $locItem['openHeight'] ?? null,
|
||||
'guide_rail_type' => $locItem['guideRailType'] ?? null,
|
||||
'motor_power' => $locItem['motorPower'] ?? null,
|
||||
'controller' => $locItem['controller'] ?? null,
|
||||
'wing_size' => $locItem['wingSize'] ?? null,
|
||||
'inspection_fee' => $locItem['inspectionFee'] ?? null,
|
||||
'bom_result' => $bomResult,
|
||||
],
|
||||
'depth' => 0,
|
||||
'sort_order' => $idx,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
// 견적 품목을 수주 품목으로 변환 (노드 연결 포함)
|
||||
foreach ($quote->items as $index => $quoteItem) {
|
||||
// floor_code/symbol_code 추출:
|
||||
// 1순위: calculation_inputs.items[].floor, code (제품 정보)
|
||||
// 2순위: quoteItem->note에서 파싱 (형식: "4F DS-01" → floor=4F, symbol=DS-01)
|
||||
// 3순위: NULL
|
||||
$floorCode = null;
|
||||
$symbolCode = null;
|
||||
$locIdx = 0;
|
||||
|
||||
// formula_source에서 제품 인덱스 추출 시도 (예: "product_0" → 0)
|
||||
$productIndex = 0;
|
||||
// 1순위: formula_source에서 인덱스 추출
|
||||
$formulaSource = $quoteItem->formula_source ?? '';
|
||||
if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
|
||||
$productIndex = (int) $matches[1];
|
||||
$locIdx = (int) $matches[1];
|
||||
}
|
||||
|
||||
// calculation_inputs에서 floor/code 가져오기
|
||||
if (isset($productItems[$productIndex])) {
|
||||
$floorCode = $productItems[$productIndex]['floor'] ?? null;
|
||||
$symbolCode = $productItems[$productIndex]['code'] ?? null;
|
||||
if (isset($productItems[$locIdx])) {
|
||||
$floorCode = $productItems[$locIdx]['floor'] ?? null;
|
||||
$symbolCode = $productItems[$locIdx]['code'] ?? null;
|
||||
} elseif (count($productItems) === 1) {
|
||||
// 단일 제품인 경우 첫 번째 제품 사용
|
||||
$floorCode = $productItems[0]['floor'] ?? null;
|
||||
$symbolCode = $productItems[0]['code'] ?? null;
|
||||
}
|
||||
@@ -587,8 +700,19 @@ public function createFromQuote(int $quoteId, array $data = [])
|
||||
}
|
||||
}
|
||||
|
||||
// note 파싱으로 locIdx 결정 (formula_source 없는 경우)
|
||||
if ($locIdx === 0 && ! empty($floorCode) && ! empty($symbolCode)) {
|
||||
foreach ($productItems as $pidx => $pItem) {
|
||||
if (($pItem['floor'] ?? '') === $floorCode && ($pItem['code'] ?? '') === $symbolCode) {
|
||||
$locIdx = $pidx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$order->items()->create([
|
||||
'tenant_id' => $tenantId,
|
||||
'order_node_id' => $nodeMap[$locIdx]->id ?? null,
|
||||
'serial_no' => $index + 1,
|
||||
'item_id' => $quoteItem->item_id,
|
||||
'item_code' => $quoteItem->item_code,
|
||||
@@ -688,6 +812,7 @@ public function syncFromQuote(Quote $quote, int $revision): ?Order
|
||||
$nodeMap = [];
|
||||
foreach ($productItems as $idx => $locItem) {
|
||||
$bomResult = $bomResults[$idx] ?? null;
|
||||
$bomVars = $bomResult['variables'] ?? [];
|
||||
$grandTotal = $bomResult['grand_total'] ?? 0;
|
||||
$qty = (int) ($locItem['quantity'] ?? 1);
|
||||
$floor = $locItem['floor'] ?? '';
|
||||
@@ -711,6 +836,8 @@ public function syncFromQuote(Quote $quote, int $revision): ?Order
|
||||
'product_name' => $locItem['productName'] ?? null,
|
||||
'open_width' => $locItem['openWidth'] ?? null,
|
||||
'open_height' => $locItem['openHeight'] ?? null,
|
||||
'width' => $bomVars['W1'] ?? $locItem['openWidth'] ?? null,
|
||||
'height' => $bomVars['H1'] ?? $locItem['openHeight'] ?? null,
|
||||
'guide_rail_type' => $locItem['guideRailType'] ?? null,
|
||||
'motor_power' => $locItem['motorPower'] ?? null,
|
||||
'controller' => $locItem['controller'] ?? null,
|
||||
@@ -984,13 +1111,37 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
// work_order_items에 아이템 추가
|
||||
$sortOrder = 1;
|
||||
foreach ($items as $orderItem) {
|
||||
// item_id 결정: order_item에 있으면 사용, 없으면 BOM에서 가져오기
|
||||
// item_id 결정: order_item에 있으면 사용, 없으면 item_code로 조회, 최후에 BOM에서 가져오기
|
||||
$itemId = $orderItem->item_id;
|
||||
if (! $itemId && $orderItem->item_code) {
|
||||
$foundItem = Item::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $orderItem->item_code)
|
||||
->first();
|
||||
$itemId = $foundItem?->id;
|
||||
}
|
||||
if (! $itemId && $orderItem->order_node_id && isset($nodesBomMap[$orderItem->order_node_id])) {
|
||||
$bomItem = $nodesBomMap[$orderItem->order_node_id][$orderItem->item_name] ?? null;
|
||||
$itemId = $bomItem['item_id'] ?? null;
|
||||
}
|
||||
|
||||
// 수주 품목의 노드에서 options(사이즈 등) 조합
|
||||
$nodeOptions = [];
|
||||
if ($orderItem->order_node_id) {
|
||||
$node = $order->rootNodes->firstWhere('id', $orderItem->order_node_id);
|
||||
$nodeOptions = $node ? ($node->options ?? []) : [];
|
||||
}
|
||||
$woItemOptions = array_filter([
|
||||
'floor' => $orderItem->floor_code,
|
||||
'code' => $orderItem->symbol_code,
|
||||
'width' => $nodeOptions['width'] ?? $nodeOptions['open_width'] ?? null,
|
||||
'height' => $nodeOptions['height'] ?? $nodeOptions['open_height'] ?? null,
|
||||
'cutting_info' => $nodeOptions['cutting_info'] ?? null,
|
||||
'slat_info' => $nodeOptions['slat_info'] ?? null,
|
||||
'bending_info' => $nodeOptions['bending_info'] ?? null,
|
||||
'wip_info' => $nodeOptions['wip_info'] ?? null,
|
||||
], fn ($v) => $v !== null);
|
||||
|
||||
DB::table('work_order_items')->insert([
|
||||
'tenant_id' => $tenantId,
|
||||
'work_order_id' => $workOrder->id,
|
||||
@@ -1002,6 +1153,7 @@ public function createProductionOrder(int $orderId, array $data)
|
||||
'unit' => $orderItem->unit,
|
||||
'sort_order' => $sortOrder++,
|
||||
'status' => 'pending',
|
||||
'options' => ! empty($woItemOptions) ? json_encode($woItemOptions) : null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user