feat:테넌트설정 API 및 다수 서비스 개선
- TenantSetting CRUD API 추가 - Calendar, Entertainment, VAT 서비스 개선 - 5130 BOM 계산 로직 수정 - quote_items에 item_type 컬럼 추가 - tenant_settings 테이블 마이그레이션 - Swagger 문서 업데이트
This commit is contained in:
@@ -1025,21 +1025,20 @@ public function updateItemStatus(int $workOrderId, int $itemId, string $status)
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업지시에 필요한 자재 목록 조회 (BOM 기반)
|
||||
* 작업지시에 필요한 자재 목록 조회 (BOM 기반 + 실제 재고 연동)
|
||||
*
|
||||
* 작업지시의 품목에 연결된 BOM 자재 목록을 반환합니다.
|
||||
* 현재는 품목 정보 기반으로 Mock 데이터를 반환하며,
|
||||
* 향후 자재 관리 기능 구현 시 실제 데이터로 연동됩니다.
|
||||
* 작업지시의 품목에 연결된 BOM 자재 목록과 실제 재고 정보를 반환합니다.
|
||||
* 품목의 BOM 정보를 기반으로 필요 자재를 추출하고, 각 자재의 실제 재고를 조회합니다.
|
||||
*
|
||||
* @param int $workOrderId 작업지시 ID
|
||||
* @return array 자재 목록 (id, material_code, material_name, unit, current_stock, fifo_rank)
|
||||
* @return array 자재 목록 (item_id, material_code, material_name, unit, required_qty, current_stock, available_qty, fifo_rank)
|
||||
*/
|
||||
public function getMaterials(int $workOrderId): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$workOrder = WorkOrder::where('tenant_id', $tenantId)
|
||||
->with(['items.item', 'salesOrder.items'])
|
||||
->with(['items.item'])
|
||||
->find($workOrderId);
|
||||
|
||||
if (! $workOrder) {
|
||||
@@ -1048,29 +1047,75 @@ public function getMaterials(int $workOrderId): array
|
||||
|
||||
$materials = [];
|
||||
$rank = 1;
|
||||
$stockService = app(StockService::class);
|
||||
|
||||
// 1. WorkOrder 자체 items가 있으면 사용
|
||||
if ($workOrder->items && $workOrder->items->count() > 0) {
|
||||
foreach ($workOrder->items as $item) {
|
||||
$materials[] = [
|
||||
'id' => $item->id,
|
||||
'material_code' => $item->item_id ? "MAT-{$item->item_id}" : "MAT-{$item->id}",
|
||||
'material_name' => $item->item_name ?? '자재 '.$item->id,
|
||||
'unit' => $item->unit ?? 'EA',
|
||||
'current_stock' => 100, // Mock: 실제 재고 데이터 연동 필요
|
||||
'fifo_rank' => $rank++,
|
||||
];
|
||||
// 작업지시 품목들의 BOM에서 자재 추출
|
||||
foreach ($workOrder->items as $woItem) {
|
||||
// item_id가 있으면 해당 Item의 BOM 조회
|
||||
if ($woItem->item_id) {
|
||||
$item = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->find($woItem->item_id);
|
||||
|
||||
if ($item && ! empty($item->bom)) {
|
||||
// BOM의 각 자재 처리
|
||||
foreach ($item->bom as $bomItem) {
|
||||
$childItemId = $bomItem['child_item_id'] ?? null;
|
||||
$bomQty = (float) ($bomItem['qty'] ?? 1);
|
||||
|
||||
if (! $childItemId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 자재(자식 품목) 정보 조회
|
||||
$childItem = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->find($childItemId);
|
||||
|
||||
if (! $childItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 필요 수량 계산 (BOM 수량 × 작업지시 수량)
|
||||
$requiredQty = $bomQty * ($woItem->quantity ?? 1);
|
||||
|
||||
// 실제 재고 조회
|
||||
$stockInfo = $stockService->getAvailableStock($childItemId);
|
||||
|
||||
$materials[] = [
|
||||
'item_id' => $childItemId,
|
||||
'work_order_item_id' => $woItem->id,
|
||||
'material_code' => $childItem->code,
|
||||
'material_name' => $childItem->name,
|
||||
'specification' => $childItem->specification,
|
||||
'unit' => $childItem->unit ?? 'EA',
|
||||
'bom_qty' => $bomQty,
|
||||
'required_qty' => $requiredQty,
|
||||
'current_stock' => $stockInfo['stock_qty'] ?? 0,
|
||||
'available_qty' => $stockInfo['available_qty'] ?? 0,
|
||||
'reserved_qty' => $stockInfo['reserved_qty'] ?? 0,
|
||||
'is_sufficient' => ($stockInfo['available_qty'] ?? 0) >= $requiredQty,
|
||||
'fifo_rank' => $rank++,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. WorkOrder items가 없으면 SalesOrder items 사용
|
||||
elseif ($workOrder->salesOrder && $workOrder->salesOrder->items && $workOrder->salesOrder->items->count() > 0) {
|
||||
foreach ($workOrder->salesOrder->items as $item) {
|
||||
|
||||
// BOM이 없는 경우, 품목 자체를 자재로 취급 (Fallback)
|
||||
if (empty($materials) && $woItem->item_id) {
|
||||
$stockInfo = $stockService->getAvailableStock($woItem->item_id);
|
||||
|
||||
$materials[] = [
|
||||
'id' => $item->id,
|
||||
'material_code' => $item->item_id ? "MAT-{$item->item_id}" : "SO-{$item->id}",
|
||||
'material_name' => $item->item_name ?? '자재 '.$item->id,
|
||||
'unit' => $item->unit ?? 'EA',
|
||||
'current_stock' => 100, // Mock: 실제 재고 데이터 연동 필요
|
||||
'item_id' => $woItem->item_id,
|
||||
'work_order_item_id' => $woItem->id,
|
||||
'material_code' => $woItem->item_id ? "ITEM-{$woItem->item_id}" : null,
|
||||
'material_name' => $woItem->item_name,
|
||||
'specification' => $woItem->specification,
|
||||
'unit' => $woItem->unit ?? 'EA',
|
||||
'bom_qty' => 1,
|
||||
'required_qty' => $woItem->quantity ?? 1,
|
||||
'current_stock' => $stockInfo['stock_qty'] ?? 0,
|
||||
'available_qty' => $stockInfo['available_qty'] ?? 0,
|
||||
'reserved_qty' => $stockInfo['reserved_qty'] ?? 0,
|
||||
'is_sufficient' => ($stockInfo['available_qty'] ?? 0) >= ($woItem->quantity ?? 1),
|
||||
'fifo_rank' => $rank++,
|
||||
];
|
||||
}
|
||||
@@ -1080,16 +1125,18 @@ public function getMaterials(int $workOrderId): array
|
||||
}
|
||||
|
||||
/**
|
||||
* 자재 투입 등록
|
||||
* 자재 투입 등록 (재고 차감 포함)
|
||||
*
|
||||
* 작업지시에 자재 투입을 등록합니다.
|
||||
* 현재는 감사 로그만 기록하며, 향후 재고 차감 로직 추가 필요.
|
||||
* 작업지시에 자재 투입을 등록하고 재고를 차감합니다.
|
||||
* FIFO 기반으로 가장 오래된 LOT부터 차감합니다.
|
||||
*
|
||||
* @param int $workOrderId 작업지시 ID
|
||||
* @param array $materialIds 투입할 자재 ID 목록
|
||||
* @param array $materials 투입할 자재 목록 [['item_id' => int, 'qty' => float], ...]
|
||||
* @return array 투입 결과
|
||||
*
|
||||
* @throws \Exception 재고 부족 시
|
||||
*/
|
||||
public function registerMaterialInput(int $workOrderId, array $materialIds): array
|
||||
public function registerMaterialInput(int $workOrderId, array $materials): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
@@ -1099,25 +1146,61 @@ public function registerMaterialInput(int $workOrderId, array $materialIds): arr
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 자재 투입 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrderId,
|
||||
'material_input',
|
||||
null,
|
||||
[
|
||||
'material_ids' => $materialIds,
|
||||
'input_by' => $userId,
|
||||
'input_at' => now()->toDateTimeString(),
|
||||
]
|
||||
);
|
||||
return DB::transaction(function () use ($materials, $tenantId, $userId, $workOrderId) {
|
||||
$stockService = app(StockService::class);
|
||||
$inputResults = [];
|
||||
|
||||
return [
|
||||
'work_order_id' => $workOrderId,
|
||||
'material_count' => count($materialIds),
|
||||
'input_at' => now()->toDateTimeString(),
|
||||
];
|
||||
foreach ($materials as $material) {
|
||||
$itemId = $material['item_id'] ?? null;
|
||||
$qty = (float) ($material['qty'] ?? 0);
|
||||
|
||||
if (! $itemId || $qty <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIFO 기반 재고 차감
|
||||
try {
|
||||
$deductedLots = $stockService->decreaseFIFO(
|
||||
itemId: $itemId,
|
||||
qty: $qty,
|
||||
reason: 'work_order_input',
|
||||
referenceId: $workOrderId
|
||||
);
|
||||
|
||||
$inputResults[] = [
|
||||
'item_id' => $itemId,
|
||||
'qty' => $qty,
|
||||
'status' => 'success',
|
||||
'deducted_lots' => $deductedLots,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
// 재고 부족 등의 오류는 전체 트랜잭션 롤백
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// 자재 투입 감사 로그
|
||||
$this->auditLogger->log(
|
||||
$tenantId,
|
||||
self::AUDIT_TARGET,
|
||||
$workOrderId,
|
||||
'material_input',
|
||||
null,
|
||||
[
|
||||
'materials' => $materials,
|
||||
'input_results' => $inputResults,
|
||||
'input_by' => $userId,
|
||||
'input_at' => now()->toDateTimeString(),
|
||||
]
|
||||
);
|
||||
|
||||
return [
|
||||
'work_order_id' => $workOrderId,
|
||||
'material_count' => count($inputResults),
|
||||
'input_results' => $inputResults,
|
||||
'input_at' => now()->toDateTimeString(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user