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

@@ -201,11 +201,31 @@ public function update(int $id, array $data)
// 품목 교체 (있는 경우)
if ($items !== null) {
// 기존 품목의 floor_code/symbol_code 매핑 저장 (item_name + specification → floor_code/symbol_code)
$existingMappings = [];
foreach ($order->items as $existingItem) {
$key = ($existingItem->item_name ?? '').'|'.($existingItem->specification ?? '');
$existingMappings[$key] = [
'floor_code' => $existingItem->floor_code,
'symbol_code' => $existingItem->symbol_code,
];
}
$order->items()->delete();
foreach ($items as $index => $item) {
$item['tenant_id'] = $tenantId;
$item['serial_no'] = $index + 1; // 1부터 시작하는 순번
$item['sort_order'] = $index;
// floor_code/symbol_code 보존: 프론트엔드에서 전달되지 않으면 기존 값 사용
if (empty($item['floor_code']) || empty($item['symbol_code'])) {
$key = ($item['item_name'] ?? '').'|'.($item['specification'] ?? '');
if (isset($existingMappings[$key])) {
$item['floor_code'] = $item['floor_code'] ?? $existingMappings[$key]['floor_code'];
$item['symbol_code'] = $item['symbol_code'] ?? $existingMappings[$key]['symbol_code'];
}
}
$this->calculateItemAmounts($item);
$order->items()->create($item);
}
@@ -277,6 +297,7 @@ public function updateStatus(int $id, string $status)
return DB::transaction(function () use ($order, $status, $userId) {
$createdSale = null;
$previousStatus = $order->status_code;
// 수주확정 시 매출 자동 생성 (sales_recognition = on_order_confirm인 경우)
if ($status === Order::STATUS_CONFIRMED && $order->shouldCreateSaleOnConfirm()) {
@@ -284,6 +305,18 @@ public function updateStatus(int $id, string $status)
$order->sale_id = $createdSale->id;
}
// 🆕 수주확정 시 재고 예약
if ($status === Order::STATUS_CONFIRMED && $previousStatus !== Order::STATUS_CONFIRMED) {
$order->load('items');
app(StockService::class)->reserveForOrder($order->items, $order->id);
}
// 🆕 수주취소 시 재고 예약 해제
if ($status === Order::STATUS_CANCELLED && $previousStatus === Order::STATUS_CONFIRMED) {
$order->load('items');
app(StockService::class)->releaseReservationForOrder($order->items, $order->id);
}
$order->status_code = $status;
$order->updated_by = $userId;
$order->save();
@@ -437,14 +470,45 @@ public function createFromQuote(int $quoteId, array $data = [])
$order->save();
// calculation_inputs에서 제품-부품 매핑 정보 추출
$calculationInputs = $quote->calculation_inputs ?? [];
$calcInputItems = $calculationInputs['items'] ?? [];
// 견적 품목을 수주 품목으로 변환
foreach ($quote->items as $index => $quoteItem) {
// calculation_inputs.items에서 해당 품목의 floor/code 정보 찾기
// 1. item_index로 매칭 시도
// 2. 없으면 배열 인덱스로 fallback
$floorCode = null;
$symbolCode = null;
$itemIndex = $quoteItem->item_index ?? null;
if ($itemIndex !== null) {
// item_index로 매칭
foreach ($calcInputItems as $calcItem) {
if (($calcItem['index'] ?? null) === $itemIndex) {
$floorCode = $calcItem['floor'] ?? null;
$symbolCode = $calcItem['code'] ?? null;
break;
}
}
}
// item_index로 못 찾으면 배열 인덱스로 fallback
if ($floorCode === null && $symbolCode === null && isset($calcInputItems[$index])) {
$floorCode = $calcInputItems[$index]['floor'] ?? null;
$symbolCode = $calcInputItems[$index]['code'] ?? null;
}
$order->items()->create([
'tenant_id' => $tenantId,
'serial_no' => $index + 1, // 1부터 시작하는 순번
'item_id' => $quoteItem->item_id,
'item_code' => $quoteItem->item_code,
'item_name' => $quoteItem->item_name,
'specification' => $quoteItem->specification,
'floor_code' => $floorCode,
'symbol_code' => $symbolCode,
'quantity' => $quoteItem->calculated_quantity,
'unit' => $quoteItem->unit,
'unit_price' => $quoteItem->unit_price,