feat: 견적 산출 서비스 prices 테이블 연동
- Price 모델에 getCurrentPrice(), getSalesPriceByItemCode() 메서드 추가 - Price 모델에 STATUS_*, ITEM_TYPE_* 상수 추가 - QuoteCalculationService에 setTenantId(), getUnitPrice() 메서드 추가 - 스크린 품목 단가: 원단, 케이스, 브라켓, 인건비 prices 조회로 변경 - 철재 품목 단가: 철판, 용접, 표면처리, 가공비 prices 조회로 변경 - 모터 용량별 단가: 50W~300W prices 조회로 변경 - 모든 단가는 prices 조회 실패 시 기존 하드코딩 값을 fallback으로 사용
This commit is contained in:
@@ -16,6 +16,24 @@ class Price extends Model
|
||||
|
||||
protected $table = 'prices';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Constants
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
// 상태
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
|
||||
public const STATUS_INACTIVE = 'inactive';
|
||||
|
||||
public const STATUS_FINALIZED = 'finalized';
|
||||
|
||||
// 품목 유형
|
||||
public const ITEM_TYPE_PRODUCT = 'PRODUCT';
|
||||
|
||||
public const ITEM_TYPE_MATERIAL = 'MATERIAL';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'item_type_code',
|
||||
@@ -218,4 +236,93 @@ public function toSnapshot(): array
|
||||
'is_final' => $this->is_final,
|
||||
];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Static Query Methods (견적 산출용)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 특정 품목의 현재 유효 단가 조회
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @param string $itemTypeCode 품목 유형 (PRODUCT/MATERIAL)
|
||||
* @param int $itemId 품목 ID
|
||||
* @param int|null $clientGroupId 고객 그룹 ID (NULL = 기본가)
|
||||
*/
|
||||
public static function getCurrentPrice(
|
||||
int $tenantId,
|
||||
string $itemTypeCode,
|
||||
int $itemId,
|
||||
?int $clientGroupId = null
|
||||
): ?self {
|
||||
$today = now()->toDateString();
|
||||
|
||||
$query = static::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('item_type_code', $itemTypeCode)
|
||||
->where('item_id', $itemId)
|
||||
->whereIn('status', [self::STATUS_ACTIVE, self::STATUS_FINALIZED])
|
||||
->where('effective_from', '<=', $today)
|
||||
->where(function ($q) use ($today) {
|
||||
$q->whereNull('effective_to')
|
||||
->orWhere('effective_to', '>=', $today);
|
||||
});
|
||||
|
||||
// 고객그룹 지정된 가격 우선, 없으면 기본가
|
||||
if ($clientGroupId) {
|
||||
$groupPrice = (clone $query)
|
||||
->where('client_group_id', $clientGroupId)
|
||||
->orderByDesc('effective_from')
|
||||
->first();
|
||||
|
||||
if ($groupPrice) {
|
||||
return $groupPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// 기본가 (client_group_id = NULL)
|
||||
return $query
|
||||
->whereNull('client_group_id')
|
||||
->orderByDesc('effective_from')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 코드로 현재 유효 판매단가 조회
|
||||
* (quote_formula_items.item_code와 연동용)
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @param string $itemCode 품목 코드
|
||||
* @return float 판매단가 (없으면 0)
|
||||
*/
|
||||
public static function getSalesPriceByItemCode(int $tenantId, string $itemCode): float
|
||||
{
|
||||
// products 테이블에서 품목 코드로 검색
|
||||
$product = \Illuminate\Support\Facades\DB::table('products')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $itemCode)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($product) {
|
||||
$price = static::getCurrentPrice($tenantId, self::ITEM_TYPE_PRODUCT, $product->id);
|
||||
|
||||
return (float) ($price?->sales_price ?? 0);
|
||||
}
|
||||
|
||||
// materials 테이블에서도 검색
|
||||
$material = \Illuminate\Support\Facades\DB::table('materials')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $itemCode)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($material) {
|
||||
$price = static::getCurrentPrice($tenantId, self::ITEM_TYPE_MATERIAL, $material->id);
|
||||
|
||||
return (float) ($price?->sales_price ?? 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Quote;
|
||||
|
||||
use App\Models\Products\Price;
|
||||
use App\Models\Quote\Quote;
|
||||
use App\Services\Service;
|
||||
|
||||
@@ -13,10 +14,39 @@
|
||||
*/
|
||||
class QuoteCalculationService extends Service
|
||||
{
|
||||
private ?int $tenantId = null;
|
||||
|
||||
public function __construct(
|
||||
private FormulaEvaluatorService $formulaEvaluator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 테넌트 ID 설정
|
||||
*/
|
||||
public function setTenantId(int $tenantId): self
|
||||
{
|
||||
$this->tenantId = $tenantId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 코드로 단가 조회 (prices 테이블 연동)
|
||||
*
|
||||
* @param string $itemCode 품목 코드
|
||||
* @param float $fallback 조회 실패 시 기본값
|
||||
*/
|
||||
private function getUnitPrice(string $itemCode, float $fallback = 0): float
|
||||
{
|
||||
if (! $this->tenantId) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$price = Price::getSalesPriceByItemCode($this->tenantId, $itemCode);
|
||||
|
||||
return $price > 0 ? $price : $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 자동산출 실행
|
||||
*
|
||||
@@ -209,6 +239,7 @@ private function generateScreenItems(array $inputs, array $outputs, int $qty): a
|
||||
$items = [];
|
||||
|
||||
// 1. 스크린 원단
|
||||
$fabricPrice = $this->getUnitPrice('SCR-FABRIC-001', 25000);
|
||||
$items[] = [
|
||||
'item_code' => 'SCR-FABRIC-001',
|
||||
'item_name' => '스크린 원단',
|
||||
@@ -216,13 +247,14 @@ private function generateScreenItems(array $inputs, array $outputs, int $qty): a
|
||||
'unit' => 'm²',
|
||||
'base_quantity' => 1,
|
||||
'calculated_quantity' => $outputs['AREA'] * $qty,
|
||||
'unit_price' => 25000,
|
||||
'total_price' => $outputs['AREA'] * $qty * 25000,
|
||||
'unit_price' => $fabricPrice,
|
||||
'total_price' => $outputs['AREA'] * $qty * $fabricPrice,
|
||||
'formula' => 'AREA * QTY',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
|
||||
// 2. 케이스
|
||||
$casePrice = $this->getUnitPrice('SCR-CASE-001', 85000);
|
||||
$items[] = [
|
||||
'item_code' => 'SCR-CASE-001',
|
||||
'item_name' => '알루미늄 케이스',
|
||||
@@ -230,8 +262,8 @@ private function generateScreenItems(array $inputs, array $outputs, int $qty): a
|
||||
'unit' => 'EA',
|
||||
'base_quantity' => 1,
|
||||
'calculated_quantity' => $qty,
|
||||
'unit_price' => 85000,
|
||||
'total_price' => $qty * 85000,
|
||||
'unit_price' => $casePrice,
|
||||
'total_price' => $qty * $casePrice,
|
||||
'formula' => 'QTY',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
@@ -252,6 +284,7 @@ private function generateScreenItems(array $inputs, array $outputs, int $qty): a
|
||||
];
|
||||
|
||||
// 4. 브라켓
|
||||
$bracketPrice = $this->getUnitPrice('SCR-BRACKET-001', 15000);
|
||||
$items[] = [
|
||||
'item_code' => 'SCR-BRACKET-001',
|
||||
'item_name' => '설치 브라켓',
|
||||
@@ -259,8 +292,8 @@ private function generateScreenItems(array $inputs, array $outputs, int $qty): a
|
||||
'unit' => 'SET',
|
||||
'base_quantity' => 2,
|
||||
'calculated_quantity' => 2 * $qty,
|
||||
'unit_price' => 15000,
|
||||
'total_price' => 2 * $qty * 15000,
|
||||
'unit_price' => $bracketPrice,
|
||||
'total_price' => 2 * $qty * $bracketPrice,
|
||||
'formula' => '2 * QTY',
|
||||
'formula_category' => 'material',
|
||||
];
|
||||
@@ -272,6 +305,7 @@ private function generateScreenItems(array $inputs, array $outputs, int $qty): a
|
||||
['min' => 10, 'max' => null, 'result' => 4],
|
||||
], 2);
|
||||
|
||||
$laborPrice = $this->getUnitPrice('LAB-INSTALL-001', 50000);
|
||||
$items[] = [
|
||||
'item_code' => 'LAB-INSTALL-001',
|
||||
'item_name' => '설치 인건비',
|
||||
@@ -279,8 +313,8 @@ private function generateScreenItems(array $inputs, array $outputs, int $qty): a
|
||||
'unit' => 'HR',
|
||||
'base_quantity' => $laborHours,
|
||||
'calculated_quantity' => $laborHours * $qty,
|
||||
'unit_price' => 50000,
|
||||
'total_price' => $laborHours * $qty * 50000,
|
||||
'unit_price' => $laborPrice,
|
||||
'total_price' => $laborHours * $qty * $laborPrice,
|
||||
'formula' => 'LABOR_HOURS * QTY',
|
||||
'formula_category' => 'labor',
|
||||
];
|
||||
@@ -295,16 +329,18 @@ private function generateSteelItems(array $inputs, array $outputs, int $qty): ar
|
||||
{
|
||||
$items = [];
|
||||
|
||||
// 재질별 단가
|
||||
$materialPrice = $this->formulaEvaluator->evaluateMapping($inputs['MATERIAL'], [
|
||||
// 재질별 품목코드 및 단가 조회
|
||||
$materialCode = 'STL-PLATE-'.strtoupper($inputs['MATERIAL']);
|
||||
$fallbackMaterialPrice = $this->formulaEvaluator->evaluateMapping($inputs['MATERIAL'], [
|
||||
['source' => 'ss304', 'result' => 4500],
|
||||
['source' => 'ss316', 'result' => 6500],
|
||||
['source' => 'galvanized', 'result' => 3000],
|
||||
], 4500);
|
||||
$materialPrice = $this->getUnitPrice($materialCode, $fallbackMaterialPrice);
|
||||
|
||||
// 1. 철판
|
||||
$items[] = [
|
||||
'item_code' => 'STL-PLATE-001',
|
||||
'item_code' => $materialCode,
|
||||
'item_name' => '철판 ('.$inputs['MATERIAL'].')',
|
||||
'specification' => sprintf('%.0f x %.0f x %.1f mm', $outputs['W1'], $outputs['H1'], $inputs['THICKNESS']),
|
||||
'unit' => 'kg',
|
||||
@@ -318,6 +354,7 @@ private function generateSteelItems(array $inputs, array $outputs, int $qty): ar
|
||||
|
||||
// 2. 용접
|
||||
$weldLength = ($outputs['W1'] + $outputs['H1']) * 2 / 1000; // m
|
||||
$weldPrice = $this->getUnitPrice('STL-WELD-001', 15000);
|
||||
$items[] = [
|
||||
'item_code' => 'STL-WELD-001',
|
||||
'item_name' => '용접 ('.$inputs['WELDING'].')',
|
||||
@@ -325,21 +362,23 @@ private function generateSteelItems(array $inputs, array $outputs, int $qty): ar
|
||||
'unit' => 'm',
|
||||
'base_quantity' => $weldLength,
|
||||
'calculated_quantity' => $weldLength * $qty,
|
||||
'unit_price' => 15000,
|
||||
'total_price' => $weldLength * $qty * 15000,
|
||||
'unit_price' => $weldPrice,
|
||||
'total_price' => $weldLength * $qty * $weldPrice,
|
||||
'formula' => 'WELD_LENGTH * QTY',
|
||||
'formula_category' => 'labor',
|
||||
];
|
||||
|
||||
// 3. 표면처리
|
||||
$finishPrice = $this->formulaEvaluator->evaluateMapping($inputs['FINISH'], [
|
||||
$finishCode = 'STL-FINISH-'.strtoupper($inputs['FINISH']);
|
||||
$fallbackFinishPrice = $this->formulaEvaluator->evaluateMapping($inputs['FINISH'], [
|
||||
['source' => 'hairline', 'result' => 8000],
|
||||
['source' => 'mirror', 'result' => 15000],
|
||||
['source' => 'matte', 'result' => 5000],
|
||||
], 8000);
|
||||
$finishPrice = $this->getUnitPrice($finishCode, $fallbackFinishPrice);
|
||||
|
||||
$items[] = [
|
||||
'item_code' => 'STL-FINISH-001',
|
||||
'item_code' => $finishCode,
|
||||
'item_name' => '표면처리 ('.$inputs['FINISH'].')',
|
||||
'specification' => sprintf('%.2f m²', $outputs['AREA'] * $qty),
|
||||
'unit' => 'm²',
|
||||
@@ -352,6 +391,7 @@ private function generateSteelItems(array $inputs, array $outputs, int $qty): ar
|
||||
];
|
||||
|
||||
// 4. 가공비
|
||||
$processPrice = $this->getUnitPrice('STL-PROCESS-001', 50000);
|
||||
$items[] = [
|
||||
'item_code' => 'STL-PROCESS-001',
|
||||
'item_name' => '가공비',
|
||||
@@ -359,8 +399,8 @@ private function generateSteelItems(array $inputs, array $outputs, int $qty): ar
|
||||
'unit' => 'EA',
|
||||
'base_quantity' => 1,
|
||||
'calculated_quantity' => $qty,
|
||||
'unit_price' => 50000,
|
||||
'total_price' => $qty * 50000,
|
||||
'unit_price' => $processPrice,
|
||||
'total_price' => $qty * $processPrice,
|
||||
'formula' => 'QTY',
|
||||
'formula_category' => 'labor',
|
||||
];
|
||||
@@ -400,17 +440,21 @@ private function calculateCosts(array $items): array
|
||||
}
|
||||
|
||||
/**
|
||||
* 모터 단가 조회
|
||||
* 모터 단가 조회 (prices 테이블 연동)
|
||||
*/
|
||||
private function getMotorPrice(string $capacity): int
|
||||
private function getMotorPrice(string $capacity): float
|
||||
{
|
||||
return match ($capacity) {
|
||||
// 용량별 품목코드 및 기본 단가
|
||||
$motorCode = 'SCR-MOTOR-'.$capacity;
|
||||
$fallbackPrice = match ($capacity) {
|
||||
'50W' => 120000,
|
||||
'100W' => 150000,
|
||||
'200W' => 200000,
|
||||
'300W' => 280000,
|
||||
default => 150000,
|
||||
};
|
||||
|
||||
return $this->getUnitPrice($motorCode, $fallbackPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user