Files
sam-api/app/Models/Quote/Quote.php

384 lines
8.9 KiB
PHP
Raw Normal View History

<?php
namespace App\Models\Quote;
use App\Models\Items\Item;
use App\Models\Members\User;
use App\Models\Orders\Client;
use App\Models\Orders\Order;
use App\Models\Tenants\SiteBriefing;
use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Quote extends Model
{
use Auditable, BelongsToTenant, HasFactory, SoftDeletes;
protected $fillable = [
'tenant_id',
'quote_type',
'order_id',
'site_briefing_id',
'quote_number',
'registration_date',
'receipt_date',
'author',
// 발주처 정보
'client_id',
'client_name',
'manager',
'contact',
// 현장 정보
'site_id',
'site_name',
'site_code',
// 제품 정보
'product_category',
'item_id',
'product_code',
'product_name',
// 규격 정보
'open_size_width',
'open_size_height',
'quantity',
'unit_symbol',
'floors',
// 금액 정보
'material_cost',
'labor_cost',
'install_cost',
'subtotal',
'discount_rate',
'discount_amount',
'total_amount',
// 상태 관리
'status',
'current_revision',
'is_final',
'finalized_at',
'finalized_by',
// 기타 정보
'completion_date',
'remarks',
'memo',
'notes',
// 자동산출 입력값
'calculation_inputs',
// 견적 옵션 (summary_items, expense_items, price_adjustments)
'options',
// 감사
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'registration_date' => 'date',
'receipt_date' => 'date',
'completion_date' => 'date',
'finalized_at' => 'datetime',
'is_final' => 'boolean',
'calculation_inputs' => 'array',
'options' => 'array',
'material_cost' => 'decimal:2',
'labor_cost' => 'decimal:2',
'install_cost' => 'decimal:2',
'subtotal' => 'decimal:2',
'discount_rate' => 'decimal:2',
'discount_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* 견적 유형 상수
*/
public const TYPE_MANUFACTURING = 'manufacturing';
public const TYPE_CONSTRUCTION = 'construction';
public const TYPES = [
self::TYPE_MANUFACTURING,
self::TYPE_CONSTRUCTION,
];
/**
* 제품 카테고리 상수
*/
public const CATEGORY_SCREEN = 'SCREEN';
public const CATEGORY_STEEL = 'STEEL';
/**
* 상태 상수
*/
public const STATUS_PENDING = 'pending'; // 견적대기 (현장설명회에서 자동생성)
public const STATUS_DRAFT = 'draft';
public const STATUS_SENT = 'sent';
public const STATUS_APPROVED = 'approved';
public const STATUS_REJECTED = 'rejected';
public const STATUS_FINALIZED = 'finalized';
public const STATUS_CONVERTED = 'converted';
public const STATUSES = [
self::STATUS_PENDING,
self::STATUS_DRAFT,
self::STATUS_SENT,
self::STATUS_APPROVED,
self::STATUS_REJECTED,
self::STATUS_FINALIZED,
self::STATUS_CONVERTED,
];
/**
* 견적 품목들
*/
public function items(): HasMany
{
return $this->hasMany(QuoteItem::class)->orderBy('sort_order');
}
/**
* 수정 이력들
*/
public function revisions(): HasMany
{
return $this->hasMany(QuoteRevision::class)->orderByDesc('revision_number');
}
/**
* 거래처
*/
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
/**
* 품목 (통합 items 테이블)
*/
public function item(): BelongsTo
{
return $this->belongsTo(Item::class, 'item_id');
}
/**
* 전환된 수주 (Quote.order_id 기준)
*/
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
/**
* 견적을 참조하는 수주들 (Order.quote_id 기준)
* 수주 전환 여부 확인 사용
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class, 'quote_id');
}
/**
* 현장설명회 (자동생성 연결)
*/
public function siteBriefing(): BelongsTo
{
return $this->belongsTo(SiteBriefing::class);
}
/**
* 확정자
*/
public function finalizer(): BelongsTo
{
return $this->belongsTo(User::class, 'finalized_by');
}
/**
* 생성자
*/
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* 수정자
*/
public function updater(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
/**
* 상태별 스코프
*/
public function scopePending($query)
{
return $query->where('status', self::STATUS_PENDING);
}
public function scopeDraft($query)
{
return $query->where('status', self::STATUS_DRAFT);
}
public function scopeSent($query)
{
return $query->where('status', self::STATUS_SENT);
}
public function scopeApproved($query)
{
return $query->where('status', self::STATUS_APPROVED);
}
public function scopeFinalized($query)
{
return $query->where('status', self::STATUS_FINALIZED);
}
public function scopeConverted($query)
{
return $query->whereNotNull('order_id');
}
/**
* 확정된 견적 스코프
*/
public function scopeIsFinal($query)
{
return $query->where('is_final', true);
}
/**
* 제품 카테고리별 스코프
*/
public function scopeScreen($query)
{
return $query->where('product_category', self::CATEGORY_SCREEN);
}
public function scopeSteel($query)
{
return $query->where('product_category', self::CATEGORY_STEEL);
}
/**
* 견적 유형별 스코프
*/
public function scopeManufacturing($query)
{
return $query->where('quote_type', self::TYPE_MANUFACTURING);
}
public function scopeConstruction($query)
{
return $query->where('quote_type', self::TYPE_CONSTRUCTION);
}
public function scopeOfType($query, string $type)
{
return $query->where('quote_type', $type);
}
/**
* 날짜 범위 스코프
*/
public function scopeDateRange($query, ?string $from, ?string $to)
{
if ($from) {
$query->where('registration_date', '>=', $from);
}
if ($to) {
$query->where('registration_date', '<=', $to);
}
return $query;
}
/**
* 검색 스코프
*/
public function scopeSearch($query, ?string $keyword)
{
if (! $keyword) {
return $query;
}
return $query->where(function ($q) use ($keyword) {
$q->where('quote_number', 'like', "%{$keyword}%")
->orWhere('client_name', 'like', "%{$keyword}%")
->orWhere('manager', 'like', "%{$keyword}%")
->orWhere('site_name', 'like', "%{$keyword}%")
->orWhere('product_name', 'like', "%{$keyword}%");
});
}
/**
* 수정 가능 여부 확인
* - 모든 상태에서 수정 가능 (finalized, converted 포함)
* - 수주 전환된 견적 수정 연결된 수주도 함께 동기화됨
*/
public function isEditable(): bool
{
return true;
}
/**
* 상태 접근자: order_id가 존재하면 자동으로 'converted' 반환
* DB에 status='converted' 저장하지 않고, 수주 존재 여부로 판별
*/
public function getStatusAttribute($value): string
{
if ($this->order_id) {
return self::STATUS_CONVERTED;
}
return $value;
}
/**
* 삭제 가능 여부 확인
*/
public function isDeletable(): bool
{
if ($this->order_id) {
return false;
}
return $this->getRawOriginal('status') !== self::STATUS_FINALIZED;
}
/**
* 확정 가능 여부 확인
*/
public function isFinalizable(): bool
{
return in_array($this->status, [self::STATUS_DRAFT, self::STATUS_SENT, self::STATUS_APPROVED])
&& ! $this->is_final;
}
/**
* 수주 전환 가능 여부 확인
*/
public function isConvertible(): bool
{
return $this->status === self::STATUS_FINALIZED && $this->is_final;
}
}