2026-01-19 20:23:30 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models\Bidding;
|
|
|
|
|
|
|
|
|
|
use App\Models\Members\User;
|
|
|
|
|
use App\Models\Orders\Client;
|
|
|
|
|
use App\Models\Quote\Quote;
|
2026-01-29 15:33:54 +09:00
|
|
|
use App\Traits\Auditable;
|
2026-01-19 20:23:30 +09:00
|
|
|
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\SoftDeletes;
|
|
|
|
|
|
|
|
|
|
class Bidding extends Model
|
|
|
|
|
{
|
2026-01-29 15:33:54 +09:00
|
|
|
use Auditable, BelongsToTenant, HasFactory, SoftDeletes;
|
2026-01-19 20:23:30 +09:00
|
|
|
|
|
|
|
|
protected $fillable = [
|
|
|
|
|
'tenant_id',
|
|
|
|
|
'bidding_code',
|
|
|
|
|
'quote_id',
|
|
|
|
|
// 거래처/현장
|
|
|
|
|
'client_id',
|
|
|
|
|
'client_name',
|
|
|
|
|
'project_name',
|
|
|
|
|
// 입찰 정보
|
|
|
|
|
'bidding_date',
|
|
|
|
|
'bid_date',
|
|
|
|
|
'submission_date',
|
|
|
|
|
'confirm_date',
|
|
|
|
|
'total_count',
|
|
|
|
|
'bidding_amount',
|
|
|
|
|
// 상태
|
|
|
|
|
'status',
|
|
|
|
|
// 입찰자
|
|
|
|
|
'bidder_id',
|
|
|
|
|
'bidder_name',
|
|
|
|
|
// 공사기간
|
|
|
|
|
'construction_start_date',
|
|
|
|
|
'construction_end_date',
|
|
|
|
|
'vat_type',
|
|
|
|
|
// 비고
|
|
|
|
|
'remarks',
|
|
|
|
|
// 견적 데이터 스냅샷
|
|
|
|
|
'expense_items',
|
|
|
|
|
'estimate_detail_items',
|
|
|
|
|
// 감사
|
|
|
|
|
'created_by',
|
|
|
|
|
'updated_by',
|
|
|
|
|
'deleted_by',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
protected $casts = [
|
|
|
|
|
'bidding_date' => 'date',
|
|
|
|
|
'bid_date' => 'date',
|
|
|
|
|
'submission_date' => 'date',
|
|
|
|
|
'confirm_date' => 'date',
|
|
|
|
|
'construction_start_date' => 'date',
|
|
|
|
|
'construction_end_date' => 'date',
|
|
|
|
|
'bidding_amount' => 'decimal:2',
|
|
|
|
|
'expense_items' => 'array',
|
|
|
|
|
'estimate_detail_items' => 'array',
|
|
|
|
|
'created_at' => 'datetime',
|
|
|
|
|
'updated_at' => 'datetime',
|
|
|
|
|
'deleted_at' => 'datetime',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 상태 상수
|
|
|
|
|
*/
|
|
|
|
|
public const STATUS_WAITING = 'waiting'; // 입찰대기
|
|
|
|
|
|
|
|
|
|
public const STATUS_SUBMITTED = 'submitted'; // 투찰
|
|
|
|
|
|
|
|
|
|
public const STATUS_FAILED = 'failed'; // 탈락
|
|
|
|
|
|
|
|
|
|
public const STATUS_INVALID = 'invalid'; // 유찰
|
|
|
|
|
|
|
|
|
|
public const STATUS_AWARDED = 'awarded'; // 낙찰
|
|
|
|
|
|
|
|
|
|
public const STATUS_HOLD = 'hold'; // 보류
|
|
|
|
|
|
|
|
|
|
public const STATUSES = [
|
|
|
|
|
self::STATUS_WAITING,
|
|
|
|
|
self::STATUS_SUBMITTED,
|
|
|
|
|
self::STATUS_FAILED,
|
|
|
|
|
self::STATUS_INVALID,
|
|
|
|
|
self::STATUS_AWARDED,
|
|
|
|
|
self::STATUS_HOLD,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 부가세 유형 상수
|
|
|
|
|
*/
|
|
|
|
|
public const VAT_INCLUDED = 'included';
|
|
|
|
|
|
|
|
|
|
public const VAT_EXCLUDED = 'excluded';
|
|
|
|
|
|
|
|
|
|
public const VAT_TYPES = [
|
|
|
|
|
self::VAT_INCLUDED,
|
|
|
|
|
self::VAT_EXCLUDED,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 연결된 견적
|
|
|
|
|
*/
|
|
|
|
|
public function quote(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Quote::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 거래처
|
|
|
|
|
*/
|
|
|
|
|
public function client(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Client::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 입찰자
|
|
|
|
|
*/
|
|
|
|
|
public function bidder(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(User::class, 'bidder_id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 생성자
|
|
|
|
|
*/
|
|
|
|
|
public function creator(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(User::class, 'created_by');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 수정자
|
|
|
|
|
*/
|
|
|
|
|
public function updater(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(User::class, 'updated_by');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 상태별 스코프
|
|
|
|
|
*/
|
|
|
|
|
public function scopeWaiting($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', self::STATUS_WAITING);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopeSubmitted($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', self::STATUS_SUBMITTED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopeFailed($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', self::STATUS_FAILED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopeInvalid($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', self::STATUS_INVALID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopeAwarded($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', self::STATUS_AWARDED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function scopeHold($query)
|
|
|
|
|
{
|
|
|
|
|
return $query->where('status', self::STATUS_HOLD);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 상태 필터 스코프
|
|
|
|
|
*/
|
|
|
|
|
public function scopeOfStatus($query, ?string $status)
|
|
|
|
|
{
|
|
|
|
|
if ($status) {
|
|
|
|
|
return $query->where('status', $status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 날짜 범위 스코프
|
|
|
|
|
*/
|
|
|
|
|
public function scopeDateRange($query, ?string $from, ?string $to)
|
|
|
|
|
{
|
|
|
|
|
if ($from) {
|
|
|
|
|
$query->where('bidding_date', '>=', $from);
|
|
|
|
|
}
|
|
|
|
|
if ($to) {
|
|
|
|
|
$query->where('bidding_date', '<=', $to);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 검색 스코프
|
|
|
|
|
*/
|
|
|
|
|
public function scopeSearch($query, ?string $keyword)
|
|
|
|
|
{
|
|
|
|
|
if (! $keyword) {
|
|
|
|
|
return $query;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $query->where(function ($q) use ($keyword) {
|
|
|
|
|
$q->where('bidding_code', 'like', "%{$keyword}%")
|
|
|
|
|
->orWhere('client_name', 'like', "%{$keyword}%")
|
|
|
|
|
->orWhere('project_name', 'like', "%{$keyword}%")
|
|
|
|
|
->orWhere('bidder_name', 'like', "%{$keyword}%");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 수정 가능 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
public function isEditable(): bool
|
|
|
|
|
{
|
|
|
|
|
return ! in_array($this->status, [self::STATUS_AWARDED, self::STATUS_FAILED, self::STATUS_INVALID]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 삭제 가능 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
public function isDeletable(): bool
|
|
|
|
|
{
|
|
|
|
|
return ! in_array($this->status, [self::STATUS_AWARDED]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 낙찰 가능 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
public function isAwardable(): bool
|
|
|
|
|
{
|
|
|
|
|
return in_array($this->status, [self::STATUS_WAITING, self::STATUS_SUBMITTED, self::STATUS_HOLD]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 계약 전환 가능 여부 확인
|
|
|
|
|
*/
|
|
|
|
|
public function isConvertibleToContract(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->status === self::STATUS_AWARDED;
|
|
|
|
|
}
|
|
|
|
|
}
|