feat: 견적 단가 자동 적용 기능 추가

- 고객 그룹별 단가 조정 지원
- 견적 생성 시 자동 단가 조회
- 매출단가만 사용 (매입단가는 경고)
This commit is contained in:
2025-10-13 21:52:34 +09:00
parent be36073282
commit a6b06be61d
17 changed files with 3794 additions and 47 deletions

View File

@@ -2,28 +2,32 @@
namespace App\Services\Estimate;
use App\Models\Commons\Category;
use App\Models\Estimate\Estimate;
use App\Models\Estimate\EstimateItem;
use App\Services\ModelSet\ModelSetService;
use App\Services\Service;
use App\Services\Calculation\CalculationEngine;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use App\Services\ModelSet\ModelSetService;
use App\Services\Pricing\PricingService;
use App\Services\Service;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class EstimateService extends Service
{
protected ModelSetService $modelSetService;
protected CalculationEngine $calculationEngine;
protected PricingService $pricingService;
public function __construct(
ModelSetService $modelSetService,
CalculationEngine $calculationEngine
CalculationEngine $calculationEngine,
PricingService $pricingService
) {
parent::__construct();
$this->modelSetService = $modelSetService;
$this->calculationEngine = $calculationEngine;
$this->pricingService = $pricingService;
}
/**
@@ -35,37 +39,37 @@ public function getEstimates(array $filters = []): LengthAwarePaginator
->where('tenant_id', $this->tenantId());
// 필터링
if (!empty($filters['status'])) {
if (! empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (!empty($filters['customer_name'])) {
$query->where('customer_name', 'like', '%' . $filters['customer_name'] . '%');
if (! empty($filters['customer_name'])) {
$query->where('customer_name', 'like', '%'.$filters['customer_name'].'%');
}
if (!empty($filters['model_set_id'])) {
if (! empty($filters['model_set_id'])) {
$query->where('model_set_id', $filters['model_set_id']);
}
if (!empty($filters['date_from'])) {
if (! empty($filters['date_from'])) {
$query->whereDate('created_at', '>=', $filters['date_from']);
}
if (!empty($filters['date_to'])) {
if (! empty($filters['date_to'])) {
$query->whereDate('created_at', '<=', $filters['date_to']);
}
if (!empty($filters['search'])) {
if (! empty($filters['search'])) {
$searchTerm = $filters['search'];
$query->where(function ($q) use ($searchTerm) {
$q->where('estimate_name', 'like', '%' . $searchTerm . '%')
->orWhere('estimate_no', 'like', '%' . $searchTerm . '%')
->orWhere('project_name', 'like', '%' . $searchTerm . '%');
$q->where('estimate_name', 'like', '%'.$searchTerm.'%')
->orWhere('estimate_no', 'like', '%'.$searchTerm.'%')
->orWhere('project_name', 'like', '%'.$searchTerm.'%');
});
}
return $query->orderBy('created_at', 'desc')
->paginate($filters['per_page'] ?? 20);
->paginate($filters['per_page'] ?? 20);
}
/**
@@ -110,15 +114,22 @@ public function createEstimate(array $data): array
'parameters' => $data['parameters'],
'calculated_results' => $bomCalculation['calculated_values'] ?? [],
'bom_data' => $bomCalculation,
'total_amount' => $bomCalculation['total_amount'] ?? 0,
'total_amount' => 0, // 항목 생성 후 재계산
'notes' => $data['notes'] ?? null,
'valid_until' => now()->addDays(30), // 기본 30일 유효
'created_by' => $this->apiUserId(),
]);
// 견적 항목 생성 (BOM 기반)
if (!empty($bomCalculation['bom_items'])) {
$this->createEstimateItems($estimate, $bomCalculation['bom_items']);
// 견적 항목 생성 (BOM 기반) + 가격 계산
if (! empty($bomCalculation['bom_items'])) {
$totalAmount = $this->createEstimateItems(
$estimate,
$bomCalculation['bom_items'],
$data['client_id'] ?? null
);
// 총액 업데이트
$estimate->update(['total_amount' => $totalAmount]);
}
return $this->getEstimateDetail($estimate->id);
@@ -143,12 +154,16 @@ public function updateEstimate($estimateId, array $data): array
$data['calculated_results'] = $bomCalculation['calculated_values'] ?? [];
$data['bom_data'] = $bomCalculation;
$data['total_amount'] = $bomCalculation['total_amount'] ?? 0;
// 기존 견적 항목 삭제 후 재생성
$estimate->items()->delete();
if (!empty($bomCalculation['bom_items'])) {
$this->createEstimateItems($estimate, $bomCalculation['bom_items']);
if (! empty($bomCalculation['bom_items'])) {
$totalAmount = $this->createEstimateItems(
$estimate,
$bomCalculation['bom_items'],
$data['client_id'] ?? null
);
$data['total_amount'] = $totalAmount;
}
}
@@ -249,13 +264,13 @@ public function changeEstimateStatus($estimateId, string $status, ?string $notes
'EXPIRED' => ['DRAFT'],
];
if (!in_array($status, $validTransitions[$estimate->status] ?? [])) {
if (! in_array($status, $validTransitions[$estimate->status] ?? [])) {
throw new \Exception(__('error.estimate.invalid_status_transition'));
}
$estimate->update([
'status' => $status,
'notes' => $notes ? ($estimate->notes . "\n\n" . $notes) : $estimate->notes,
'notes' => $notes ? ($estimate->notes."\n\n".$notes) : $estimate->notes,
'updated_by' => $this->apiUserId(),
]);
@@ -288,25 +303,63 @@ public function previewCalculation($modelSetId, array $parameters): array
}
/**
* 견적 항목 생성
* 견적 항목 생성 (가격 계산 포함)
*
* @return float 총 견적 금액
*/
protected function createEstimateItems(Estimate $estimate, array $bomItems): void
protected function createEstimateItems(Estimate $estimate, array $bomItems, ?int $clientId = null): float
{
$totalAmount = 0;
$warnings = [];
foreach ($bomItems as $index => $bomItem) {
$quantity = $bomItem['quantity'] ?? 1;
$unitPrice = 0;
// 가격 조회 (item_id와 item_type이 있는 경우)
if (isset($bomItem['item_id']) && isset($bomItem['item_type'])) {
$priceResult = $this->pricingService->getItemPrice(
$bomItem['item_type'], // 'PRODUCT' or 'MATERIAL'
$bomItem['item_id'],
$clientId,
now()->format('Y-m-d')
);
$unitPrice = $priceResult['price'] ?? 0;
if ($priceResult['warning']) {
$warnings[] = $priceResult['warning'];
}
}
$totalPrice = $unitPrice * $quantity;
$totalAmount += $totalPrice;
EstimateItem::create([
'tenant_id' => $this->tenantId(),
'estimate_id' => $estimate->id,
'sequence' => $index + 1,
'item_name' => $bomItem['name'] ?? '견적 항목 ' . ($index + 1),
'item_name' => $bomItem['name'] ?? '견적 항목 '.($index + 1),
'item_description' => $bomItem['description'] ?? '',
'parameters' => $bomItem['parameters'] ?? [],
'calculated_values' => $bomItem['calculated_values'] ?? [],
'unit_price' => $bomItem['unit_price'] ?? 0,
'quantity' => $bomItem['quantity'] ?? 1,
'unit_price' => $unitPrice,
'quantity' => $quantity,
'total_price' => $totalPrice,
'bom_components' => $bomItem['components'] ?? [],
'created_by' => $this->apiUserId(),
]);
}
// 가격 경고가 있으면 로그 기록
if (! empty($warnings)) {
\Log::warning('견적 가격 조회 경고', [
'estimate_id' => $estimate->id,
'warnings' => $warnings,
]);
}
return $totalAmount;
}
/**
@@ -321,19 +374,19 @@ protected function summarizeCalculations(Estimate $estimate): array
];
// 주요 계산 결과 추출
if (!empty($estimate->calculated_results)) {
if (! empty($estimate->calculated_results)) {
$results = $estimate->calculated_results;
if (isset($results['W1'], $results['H1'])) {
$summary['key_calculations']['제작사이즈'] = $results['W1'] . ' × ' . $results['H1'] . ' mm';
$summary['key_calculations']['제작사이즈'] = $results['W1'].' × '.$results['H1'].' mm';
}
if (isset($results['weight'])) {
$summary['key_calculations']['중량'] = $results['weight'] . ' kg';
$summary['key_calculations']['중량'] = $results['weight'].' kg';
}
if (isset($results['area'])) {
$summary['key_calculations']['면적'] = $results['area'] . ' ㎡';
$summary['key_calculations']['면적'] = $results['area'].' ㎡';
}
if (isset($results['bracket_size'])) {
@@ -343,4 +396,4 @@ protected function summarizeCalculations(Estimate $estimate): array
return $summary;
}
}
}