feat: 입찰(Bidding) 관리 기능 구현
- Bidding 모델, 서비스, 컨트롤러, FormRequest 추가 - 마이그레이션 및 시더 추가 - Swagger API 문서 추가 - 견적에서 입찰 전환 시 중복 체크 로직 추가 - per_page 파라미터 100 초과 시 자동 클램핑 처리 - error.bidding.already_registered 에러 메시지 추가
This commit is contained in:
252
app/Models/Bidding/Bidding.php
Normal file
252
app/Models/Bidding/Bidding.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Bidding;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Orders\Client;
|
||||
use App\Models\Quote\Quote;
|
||||
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
|
||||
{
|
||||
use BelongsToTenant, HasFactory, SoftDeletes;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user