Files
sam-api/app/Models/Quote/Quote.php
kent 1410cf725a feat(API): 견적-주문 연동 필드 및 마이그레이션 추가
- Order, OrderItem 모델에 견적 연동 필드 추가
- Quote 모델에 order_id 관계 추가
- QuoteService 개선
- 관련 마이그레이션 파일 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 15:56:46 +09:00

294 lines
6.7 KiB
PHP

<?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\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 BelongsToTenant, HasFactory, SoftDeletes;
protected $fillable = [
'tenant_id',
'order_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',
// 감사
'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',
'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 CATEGORY_SCREEN = 'SCREEN';
public const CATEGORY_STEEL = 'STEEL';
/**
* 상태 상수
*/
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 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');
}
/**
* 전환된 수주
*/
public function order(): BelongsTo
{
return $this->belongsTo(Order::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 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->where('status', self::STATUS_CONVERTED);
}
/**
* 확정된 견적 스코프
*/
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 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}%");
});
}
/**
* 수정 가능 여부 확인
*/
public function isEditable(): bool
{
return ! in_array($this->status, [self::STATUS_FINALIZED, self::STATUS_CONVERTED]);
}
/**
* 삭제 가능 여부 확인
*/
public function isDeletable(): bool
{
return ! in_array($this->status, [self::STATUS_FINALIZED, self::STATUS_CONVERTED]);
}
/**
* 확정 가능 여부 확인
*/
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;
}
}