- 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>
294 lines
6.7 KiB
PHP
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;
|
|
}
|
|
}
|