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>
This commit is contained in:
2026-01-05 15:56:46 +09:00
parent 02e268e49a
commit 1410cf725a
9 changed files with 566 additions and 32 deletions

View File

@@ -2,47 +2,172 @@
namespace App\Models\Orders;
use App\Models\Clients\Client;
use App\Models\Items\Item;
use App\Models\Quote\Quote;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 수주 마스터 (Orders)
*
* @mixin IdeHelperOrder
*/
class Order extends Model
{
use SoftDeletes;
use BelongsToTenant, SoftDeletes;
// 상태 코드
public const STATUS_DRAFT = 'DRAFT';
public const STATUS_CONFIRMED = 'CONFIRMED';
public const STATUS_IN_PROGRESS = 'IN_PROGRESS';
public const STATUS_COMPLETED = 'COMPLETED';
public const STATUS_CANCELLED = 'CANCELLED';
// 주문 유형
public const TYPE_ORDER = 'ORDER'; // 수주
public const TYPE_PURCHASE = 'PURCHASE'; // 발주
// 주문(견적/수주/발주 메인)
protected $table = 'orders';
protected $fillable = [
'tenant_id', 'order_no', 'order_type_code', 'status_code', 'category_code', 'item_id',
'received_at', 'writer_id', 'client_id', 'client_contact', 'site_name', 'quantity', 'delivery_date',
'delivery_method_code', 'memo',
'tenant_id',
'quote_id',
'order_no',
'order_type_code',
'status_code',
'category_code',
'item_id',
'received_at',
'writer_id',
'client_id',
'client_name',
'client_contact',
'site_name',
'quantity',
// 금액 정보
'supply_amount',
'tax_amount',
'total_amount',
'discount_rate',
'discount_amount',
// 기타
'delivery_date',
'delivery_method_code',
'memo',
'remarks',
'note',
// 감사
'created_by',
'updated_by',
'deleted_by',
];
// 상세(라인)
public function items()
protected $casts = [
'quantity' => 'decimal:4',
'supply_amount' => 'decimal:2',
'tax_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
'discount_rate' => 'decimal:2',
'discount_amount' => 'decimal:2',
'received_at' => 'datetime',
'delivery_date' => 'date',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* 수주 상세 품목
*/
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
return $this->hasMany(OrderItem::class)->orderBy('sort_order');
}
// 이력
public function histories()
/**
* 수주 이력
*/
public function histories(): HasMany
{
return $this->hasMany(OrderHistory::class);
}
// 버전관리
public function versions()
/**
* 수주 버전
*/
public function versions(): HasMany
{
return $this->hasMany(OrderVersion::class);
}
// 품목 (통합 items 테이블)
public function item()
/**
* 원본 견적
*/
public function quote(): BelongsTo
{
return $this->belongsTo(Quote::class);
}
/**
* 거래처
*/
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
/**
* 품목 (통합 items 테이블)
*/
public function item(): BelongsTo
{
return $this->belongsTo(Item::class, 'item_id');
}
/**
* 품목들로부터 금액 합계 재계산
*/
public function recalculateTotals(): self
{
$this->supply_amount = $this->items->sum('supply_amount');
$this->tax_amount = $this->items->sum('tax_amount');
$this->total_amount = $this->items->sum('total_amount');
$this->quantity = $this->items->sum('quantity');
return $this;
}
/**
* 견적에서 수주 생성
*/
public static function createFromQuote(Quote $quote, string $orderNo): self
{
return new self([
'tenant_id' => $quote->tenant_id,
'quote_id' => $quote->id,
'order_no' => $orderNo,
'order_type_code' => self::TYPE_ORDER,
'status_code' => self::STATUS_DRAFT,
'client_id' => $quote->client_id,
'client_name' => $quote->client?->name,
'client_contact' => $quote->contact_person,
'site_name' => $quote->site_name,
'quantity' => $quote->items->sum('calculated_quantity'),
'supply_amount' => $quote->total_amount,
'tax_amount' => round($quote->total_amount * 0.1, 2),
'total_amount' => round($quote->total_amount * 1.1, 2),
'delivery_date' => $quote->delivery_date,
'memo' => $quote->remarks,
'remarks' => $quote->internal_notes,
]);
}
}

View File

@@ -3,38 +3,181 @@
namespace App\Models\Orders;
use App\Models\Items\Item;
use App\Models\Quote\Quote;
use App\Models\Quote\QuoteItem;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 수주 상세 (Order Items)
*
* @mixin IdeHelperOrderItem
*/
class OrderItem extends Model
{
use SoftDeletes;
use BelongsToTenant, SoftDeletes;
// 주문 상세
protected $table = 'order_items';
protected $fillable = [
'tenant_id', 'order_id', 'serial_no', 'item_id', 'quantity',
'status_code', 'design_code', 'remarks', 'attributes',
'tenant_id',
'order_id',
'quote_id',
'quote_item_id',
'serial_no',
// 품목 정보
'item_id',
'item_code',
'item_name',
'specification',
'unit',
// 수량/금액
'quantity',
'unit_price',
'supply_amount',
'tax_amount',
'total_amount',
// 할인
'discount_rate',
'discount_amount',
// 기타
'status_code',
'design_code',
'remarks',
'note',
'sort_order',
'attributes',
// 감사
'created_by',
'updated_by',
'deleted_by',
];
// 투입 구성(자재/BOM 등)
public function components()
{
return $this->hasMany(OrderItemComponent::class);
}
protected $casts = [
'quantity' => 'decimal:4',
'unit_price' => 'decimal:2',
'supply_amount' => 'decimal:2',
'tax_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
'discount_rate' => 'decimal:2',
'discount_amount' => 'decimal:2',
'attributes' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
public function order()
/**
* 수주 마스터
*/
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
// 품목 (통합 items 테이블)
public function item()
/**
* 품목 마스터
*/
public function item(): BelongsTo
{
return $this->belongsTo(Item::class, 'item_id');
}
/**
* 원본 견적
*/
public function quote(): BelongsTo
{
return $this->belongsTo(Quote::class);
}
/**
* 원본 견적 품목
*/
public function quoteItem(): BelongsTo
{
return $this->belongsTo(QuoteItem::class);
}
/**
* 투입 구성 (자재/BOM 등)
*/
public function components(): HasMany
{
return $this->hasMany(OrderItemComponent::class);
}
/**
* 공급가액 계산 (수량 × 단가 - 할인)
*/
public function calculateSupplyAmount(): float
{
$gross = $this->quantity * $this->unit_price;
$discount = $this->discount_amount ?: ($gross * ($this->discount_rate / 100));
return round($gross - $discount, 2);
}
/**
* 세액 계산 (공급가액 × 10%)
*/
public function calculateTaxAmount(): float
{
return round($this->supply_amount * 0.1, 2);
}
/**
* 총액 계산 (공급가액 + 세액)
*/
public function calculateTotalAmount(): float
{
return round($this->supply_amount + $this->tax_amount, 2);
}
/**
* 금액 재계산 및 저장
*/
public function recalculateAmounts(): self
{
$this->supply_amount = $this->calculateSupplyAmount();
$this->tax_amount = $this->calculateTaxAmount();
$this->total_amount = $this->calculateTotalAmount();
return $this;
}
/**
* 견적 품목에서 수주 품목 생성
*
* @param int $serialIndex 품목 순번 (1부터 시작)
*/
public static function createFromQuoteItem(QuoteItem $quoteItem, int $orderId, int $serialIndex = 1): self
{
$qty = $quoteItem->calculated_quantity ?? 1;
$supplyAmount = $quoteItem->unit_price * $qty;
$taxAmount = round($supplyAmount * 0.1, 2);
return new self([
'tenant_id' => $quoteItem->tenant_id,
'order_id' => $orderId,
'quote_id' => $quoteItem->quote_id,
'quote_item_id' => $quoteItem->id,
'serial_no' => str_pad($serialIndex, 3, '0', STR_PAD_LEFT),
'item_id' => $quoteItem->item_id,
'item_code' => $quoteItem->item_code,
'item_name' => $quoteItem->item_name,
'specification' => $quoteItem->specification,
'unit' => $quoteItem->unit ?? 'EA',
'quantity' => $qty,
'unit_price' => $quoteItem->unit_price,
'supply_amount' => $supplyAmount,
'tax_amount' => $taxAmount,
'total_amount' => $supplyAmount + $taxAmount,
'note' => $quoteItem->note,
'sort_order' => $quoteItem->sort_order ?? 0,
]);
}
}

View File

@@ -5,6 +5,7 @@
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;
@@ -18,6 +19,7 @@ class Quote extends Model
protected $fillable = [
'tenant_id',
'order_id',
'quote_number',
'registration_date',
'receipt_date',
@@ -142,6 +144,14 @@ public function item(): BelongsTo
return $this->belongsTo(Item::class, 'item_id');
}
/**
* 전환된 수주
*/
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
/**
* 확정자
*/