feat:테넌트설정 API 및 다수 서비스 개선

- TenantSetting CRUD API 추가
- Calendar, Entertainment, VAT 서비스 개선
- 5130 BOM 계산 로직 수정
- quote_items에 item_type 컬럼 추가
- tenant_settings 테이블 마이그레이션
- Swagger 문서 업데이트
This commit is contained in:
2026-01-26 20:29:22 +09:00
parent f2da990771
commit 6d05ab815f
54 changed files with 2090 additions and 110 deletions

View File

@@ -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(),
];
});
}
/**