feat: [작업지시/작업자화면] items.item 관계 로드, 부서 필터 개선, BD 재질 자동 매칭, 전개도 폭 매칭
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Models\Documents\DocumentTemplate;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Process;
|
||||
@@ -18,6 +17,7 @@
|
||||
use App\Models\Tenants\StockTransaction;
|
||||
use App\Services\Audit\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@@ -62,6 +62,7 @@ public function index(array $params)
|
||||
'salesOrder.client:id,name',
|
||||
'process:id,process_name,process_code,department,options',
|
||||
'items:id,work_order_id,item_id,item_name,specification,quantity,unit,status,options,sort_order,source_order_item_id',
|
||||
'items.item:id,code',
|
||||
'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code',
|
||||
'items.sourceOrderItem.node:id,name,code',
|
||||
'items.materialInputs:id,work_order_id,work_order_item_id,stock_lot_id,item_id,qty,input_by,input_at',
|
||||
@@ -123,14 +124,21 @@ public function index(array $params)
|
||||
->orWhereHas('assignees', fn ($aq) => $aq->where('user_id', $userId));
|
||||
});
|
||||
} else {
|
||||
// 2차: 사용자 소속 부서의 작업지시 필터
|
||||
// 2차: 사용자 소속 부서 + 상위 부서의 작업지시 필터
|
||||
$departmentIds = DB::table('department_user')
|
||||
->where('user_id', $userId)
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('department_id');
|
||||
|
||||
if ($departmentIds->isNotEmpty()) {
|
||||
$query->whereIn('team_id', $departmentIds);
|
||||
// 소속 부서의 상위 부서도 포함 (부서 계층 지원)
|
||||
$parentIds = DB::table('departments')
|
||||
->whereIn('id', $departmentIds)
|
||||
->whereNotNull('parent_id')
|
||||
->pluck('parent_id');
|
||||
|
||||
$allDeptIds = $departmentIds->merge($parentIds)->unique();
|
||||
$query->whereIn('team_id', $allDeptIds);
|
||||
}
|
||||
// 3차: 부서도 없으면 필터 없이 전체 노출
|
||||
}
|
||||
@@ -151,7 +159,37 @@ public function index(array $params)
|
||||
|
||||
$query->orderByDesc('created_at');
|
||||
|
||||
return $query->paginate($size, ['*'], 'page', $page);
|
||||
$result = $query->paginate($size, ['*'], 'page', $page);
|
||||
|
||||
// 작업자 화면: BENDING 카테고리 품목에 전개도 폭(bending_width) 추가
|
||||
if ($workerScreen) {
|
||||
$this->appendBendingWidths($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* BENDING 카테고리 품목에 전개도 폭 추가
|
||||
*/
|
||||
private function appendBendingWidths($paginator): void
|
||||
{
|
||||
$bendingService = app(BendingCodeService::class);
|
||||
|
||||
foreach ($paginator->items() as $workOrder) {
|
||||
foreach ($workOrder->items as $item) {
|
||||
$itemCode = $item->item?->code;
|
||||
if (! $itemCode || ! str_starts_with($itemCode, 'BD-')) {
|
||||
continue;
|
||||
}
|
||||
$width = $bendingService->getBendingWidthByItemCode($itemCode);
|
||||
if ($width !== null) {
|
||||
$options = $item->options ?? [];
|
||||
$options['bending_width'] = $width;
|
||||
$item->setAttribute('options', $options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,6 +253,7 @@ public function show(int $id)
|
||||
'salesOrder.writer:id,name',
|
||||
'process:id,process_name,process_code,work_steps,department,options',
|
||||
'process.steps' => fn ($q) => $q->where('is_active', true)->orderBy('sort_order'),
|
||||
'items.item:id,code',
|
||||
'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code',
|
||||
'items.sourceOrderItem.node:id,name,code',
|
||||
'items.materialInputs:id,work_order_id,work_order_item_id,stock_lot_id,item_id,qty,input_by,input_at',
|
||||
@@ -3806,13 +3845,59 @@ public function getMaterialsForItem(int $workOrderId, int $itemId): array
|
||||
}
|
||||
}
|
||||
|
||||
// BOM이 없으면 품목 자체를 자재로 사용
|
||||
// BOM이 없으면 BD 품목의 재질 정보로 원자재 자동 매칭
|
||||
if (empty($materialItems) && $woItem->item_id && $woItem->item) {
|
||||
$materialItems[] = [
|
||||
'item' => $woItem->item,
|
||||
'bom_qty' => 1,
|
||||
'required_qty' => $woItem->quantity ?? 1,
|
||||
];
|
||||
$itemOptions = $woItem->item->options ?? [];
|
||||
$material = $itemOptions['material'] ?? null;
|
||||
|
||||
$matchedRawItems = [];
|
||||
if ($material && preg_match('/^(\w+)\s*(\d+\.?\d*)/i', $material, $matMatch)) {
|
||||
$matType = $matMatch[1];
|
||||
$matThickness = (float) $matMatch[2];
|
||||
|
||||
// 품목명에서 제품 길이 추출 (예: 1750mm)
|
||||
$productLength = 0;
|
||||
if (preg_match('/(\d{3,5})mm/', $woItem->item->name, $lenMatch)) {
|
||||
$productLength = (int) $lenMatch[1];
|
||||
}
|
||||
|
||||
// 원자재 검색: material_type + thickness 매칭, length >= 제품길이
|
||||
$rawItems = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->where('item_type', 'RM')
|
||||
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(options, '$.attributes.material_type')) = ?", [$matType])
|
||||
->whereRaw('CAST(JSON_EXTRACT(options, \'$.attributes.thickness\') AS DECIMAL(10,2)) = ?', [$matThickness])
|
||||
->get();
|
||||
|
||||
foreach ($rawItems as $rawItem) {
|
||||
$rawAttrs = $rawItem->options['attributes'] ?? [];
|
||||
$rawLength = $rawAttrs['length'] ?? null;
|
||||
|
||||
// 길이 조건: 원자재 길이 >= 제품 길이 (길이 미정이면 통과)
|
||||
if ($rawLength !== null && $productLength > 0 && $rawLength < $productLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchedRawItems[] = $rawItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($matchedRawItems)) {
|
||||
// 매칭된 원자재를 자재 목록으로 추가
|
||||
foreach ($matchedRawItems as $rawItem) {
|
||||
$materialItems[] = [
|
||||
'item' => $rawItem,
|
||||
'bom_qty' => 1,
|
||||
'required_qty' => $woItem->quantity ?? 1,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// 매칭 실패 시 기존 동작 유지 (품목 자체를 자재로 표시)
|
||||
$materialItems[] = [
|
||||
'item' => $woItem->item,
|
||||
'bom_qty' => 1,
|
||||
'required_qty' => $woItem->quantity ?? 1,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 이미 투입된 수량 조회 (item_id별 SUM)
|
||||
|
||||
Reference in New Issue
Block a user