feat: 생산지시 생성 시 공정 자동 분류 및 아이템 연결

- OrderService: 생산지시 생성 로직 개선
  - order_items.item_id → process_items 테이블에서 공정 자동 조회
  - 공정별로 아이템 그룹화 (미지정 아이템은 별도 그룹)
  - 각 공정별 작업지시 생성
  - work_order_items에 해당 공정의 아이템들 자동 추가

- WorkOrderService: 목록 조회 시 관계 추가
  - items 관계 추가 (틀수 계산용)
  - process.department 필드 추가 (부서 표시용)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-02-06 10:28:30 +09:00
parent f640a837e9
commit 6bc766411b
2 changed files with 86 additions and 33 deletions

View File

@@ -612,29 +612,29 @@ public function syncFromQuote(Quote $quote, int $revision): ?Order
$floorCode = null;
$symbolCode = null;
// formula_source에서 제품 인덱스 추출
$productIndex = 0;
$formulaSource = $quoteItem->formula_source ?? '';
if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
$productIndex = (int) $matches[1];
// 1순위: note에서 floor/code 파싱 (가장 정확한 정보)
// note 형식: "4F FSS-01" (공백으로 구분)
$note = trim($quoteItem->note ?? '');
if ($note !== '') {
$parts = preg_split('/\s+/', $note, 2);
$floorCode = $parts[0] ?? null;
$symbolCode = $parts[1] ?? null;
}
// calculation_inputs에서 floor/code 가져오기
if (isset($productItems[$productIndex])) {
$floorCode = $productItems[$productIndex]['floor'] ?? null;
$symbolCode = $productItems[$productIndex]['code'] ?? null;
} elseif (count($productItems) === 1) {
$floorCode = $productItems[0]['floor'] ?? null;
$symbolCode = $productItems[0]['code'] ?? null;
}
// note에서 파싱 시도
// 2순위: formula_source에서 제품 인덱스 추출하여 calculation_inputs에서 가져오기
if (empty($floorCode) && empty($symbolCode)) {
$note = trim($quoteItem->note ?? '');
if ($note !== '') {
$parts = preg_split('/\s+/', $note, 2);
$floorCode = $parts[0] ?? null;
$symbolCode = $parts[1] ?? null;
$productIndex = 0;
$formulaSource = $quoteItem->formula_source ?? '';
if (preg_match('/product_(\d+)/', $formulaSource, $matches)) {
$productIndex = (int) $matches[1];
}
if (isset($productItems[$productIndex])) {
$floorCode = $productItems[$productIndex]['floor'] ?? null;
$symbolCode = $productItems[$productIndex]['code'] ?? null;
} elseif (count($productItems) === 1) {
$floorCode = $productItems[0]['floor'] ?? null;
$symbolCode = $productItems[0]['code'] ?? null;
}
}
@@ -721,21 +721,47 @@ public function createProductionOrder(int $orderId, array $data)
throw new BadRequestHttpException(__('error.order.production_order_already_exists'));
}
// process_ids 배열 또는 단일 process_id 처리
$processIds = $data['process_ids'] ?? [];
if (empty($processIds) && ! empty($data['process_id'])) {
$processIds = [$data['process_id']];
// order_items의 item_id를 기반으로 공정별 자동 분류
$itemIds = $order->items->pluck('item_id')->filter()->unique()->values()->toArray();
// process_items 테이블에서 item_id → process_id 매핑 조회
$itemProcessMap = [];
if (! empty($itemIds)) {
$processItems = DB::table('process_items as pi')
->join('processes as p', 'pi.process_id', '=', 'p.id')
->where('p.tenant_id', $tenantId)
->whereIn('pi.item_id', $itemIds)
->where('pi.is_active', true)
->select('pi.item_id', 'pi.process_id')
->get();
foreach ($processItems as $pi) {
$itemProcessMap[$pi->item_id] = $pi->process_id;
}
}
// 공정이 없으면 null로 하나만 생성
if (empty($processIds)) {
$processIds = [null];
// order_items를 공정별로 그룹화
$itemsByProcess = [];
foreach ($order->items as $orderItem) {
$processId = $itemProcessMap[$orderItem->item_id] ?? null;
$key = $processId ?? 'none'; // null은 'none' 키로 그룹화
if (! isset($itemsByProcess[$key])) {
$itemsByProcess[$key] = [
'process_id' => $processId,
'items' => [],
];
}
$itemsByProcess[$key]['items'][] = $orderItem;
}
return DB::transaction(function () use ($order, $data, $tenantId, $userId, $processIds) {
return DB::transaction(function () use ($order, $data, $tenantId, $userId, $itemsByProcess) {
$workOrders = [];
foreach ($processIds as $processId) {
foreach ($itemsByProcess as $key => $group) {
$processId = $group['process_id'];
$items = $group['items'];
// 작업지시번호 생성
$workOrderNo = $this->generateWorkOrderNo($tenantId);
@@ -756,6 +782,25 @@ public function createProductionOrder(int $orderId, array $data)
'updated_by' => $userId,
]);
// work_order_items에 아이템 추가
$sortOrder = 1;
foreach ($items as $orderItem) {
DB::table('work_order_items')->insert([
'tenant_id' => $tenantId,
'work_order_id' => $workOrder->id,
'source_order_item_id' => $orderItem->id,
'item_id' => $orderItem->item_id,
'item_name' => $orderItem->item_name,
'specification' => $orderItem->specification,
'quantity' => $orderItem->quantity,
'unit' => $orderItem->unit,
'sort_order' => $sortOrder++,
'status' => 'pending',
'created_at' => now(),
'updated_at' => now(),
]);
}
$workOrders[] = $workOrder->load(['assignee:id,name', 'team:id,name', 'process:id,process_name,process_code']);
}

View File

@@ -49,7 +49,8 @@ public function index(array $params)
'team:id,name',
'salesOrder:id,order_no,client_id,client_name',
'salesOrder.client:id,name',
'process:id,process_name,process_code',
'process:id,process_name,process_code,department',
'items:id,work_order_id,item_name,quantity',
]);
// 검색어
@@ -66,8 +67,14 @@ public function index(array $params)
}
// 공정 필터 (process_id)
// - 'none' 또는 '0': 공정 미지정 (process_id IS NULL)
// - 숫자: 해당 공정 ID로 필터
if ($processId !== null) {
$query->where('process_id', $processId);
if ($processId === 'none' || $processId === '0' || $processId === 0) {
$query->whereNull('process_id');
} else {
$query->where('process_id', $processId);
}
}
// 공정 코드 필터 (process_code) - 대시보드용
@@ -143,9 +150,10 @@ public function show(int $id)
'assignee:id,name',
'assignees.user:id,name',
'team:id,name',
'salesOrder:id,order_no,site_name,client_id',
'salesOrder:id,order_no,site_name,client_id,client_contact,received_at,writer_id,created_at,quantity',
'salesOrder.client:id,name',
'process:id,process_name,process_code,work_steps',
'salesOrder.writer:id,name',
'process:id,process_name,process_code,work_steps,department',
'items',
'bendingDetail',
'issues' => fn ($q) => $q->orderByDesc('created_at'),