2025-07-29 23:22:31 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models\Orders;
|
|
|
|
|
|
2025-12-14 01:20:18 +09:00
|
|
|
use App\Models\Items\Item;
|
2026-01-15 16:13:34 +09:00
|
|
|
use App\Models\Production\WorkOrder;
|
2026-01-23 21:32:23 +09:00
|
|
|
use App\Models\Products\CommonCode;
|
2026-01-05 15:56:46 +09:00
|
|
|
use App\Models\Quote\Quote;
|
2026-01-22 17:23:36 +09:00
|
|
|
use App\Models\Tenants\Sale;
|
2026-01-16 21:59:06 +09:00
|
|
|
use App\Models\Tenants\Shipment;
|
2026-01-05 15:56:46 +09:00
|
|
|
use App\Traits\BelongsToTenant;
|
2025-07-29 23:22:31 +09:00
|
|
|
use Illuminate\Database\Eloquent\Model;
|
2026-01-05 15:56:46 +09:00
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
2025-07-29 23:22:31 +09:00
|
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
|
|
2025-08-21 09:50:15 +09:00
|
|
|
/**
|
2026-01-05 15:56:46 +09:00
|
|
|
* 수주 마스터 (Orders)
|
|
|
|
|
*
|
2025-08-21 09:50:15 +09:00
|
|
|
* @mixin IdeHelperOrder
|
|
|
|
|
*/
|
2025-07-29 23:22:31 +09:00
|
|
|
class Order extends Model
|
|
|
|
|
{
|
2026-01-05 15:56:46 +09:00
|
|
|
use BelongsToTenant, SoftDeletes;
|
|
|
|
|
|
|
|
|
|
// 상태 코드
|
|
|
|
|
public const STATUS_DRAFT = 'DRAFT';
|
|
|
|
|
|
|
|
|
|
public const STATUS_CONFIRMED = 'CONFIRMED';
|
|
|
|
|
|
|
|
|
|
public const STATUS_IN_PROGRESS = 'IN_PROGRESS';
|
|
|
|
|
|
2026-01-16 21:59:06 +09:00
|
|
|
public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION'; // 생산중
|
|
|
|
|
|
|
|
|
|
public const STATUS_PRODUCED = 'PRODUCED'; // 생산완료
|
|
|
|
|
|
|
|
|
|
public const STATUS_SHIPPING = 'SHIPPING'; // 출하중
|
|
|
|
|
|
|
|
|
|
public const STATUS_SHIPPED = 'SHIPPED'; // 출하완료
|
|
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
public const STATUS_COMPLETED = 'COMPLETED';
|
|
|
|
|
|
|
|
|
|
public const STATUS_CANCELLED = 'CANCELLED';
|
|
|
|
|
|
2026-01-16 21:59:06 +09:00
|
|
|
/**
|
|
|
|
|
* 전체 상태 목록
|
|
|
|
|
*/
|
|
|
|
|
public const STATUSES = [
|
|
|
|
|
self::STATUS_DRAFT,
|
|
|
|
|
self::STATUS_CONFIRMED,
|
|
|
|
|
self::STATUS_IN_PROGRESS,
|
|
|
|
|
self::STATUS_IN_PRODUCTION,
|
|
|
|
|
self::STATUS_PRODUCED,
|
|
|
|
|
self::STATUS_SHIPPING,
|
|
|
|
|
self::STATUS_SHIPPED,
|
|
|
|
|
self::STATUS_COMPLETED,
|
|
|
|
|
self::STATUS_CANCELLED,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 상태 라벨
|
|
|
|
|
*/
|
|
|
|
|
public const STATUS_LABELS = [
|
|
|
|
|
self::STATUS_DRAFT => '임시저장',
|
|
|
|
|
self::STATUS_CONFIRMED => '확정',
|
|
|
|
|
self::STATUS_IN_PROGRESS => '진행중',
|
|
|
|
|
self::STATUS_IN_PRODUCTION => '생산중',
|
|
|
|
|
self::STATUS_PRODUCED => '생산완료',
|
|
|
|
|
self::STATUS_SHIPPING => '출하중',
|
|
|
|
|
self::STATUS_SHIPPED => '출하완료',
|
|
|
|
|
self::STATUS_COMPLETED => '완료',
|
|
|
|
|
self::STATUS_CANCELLED => '취소',
|
|
|
|
|
];
|
|
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
// 주문 유형
|
|
|
|
|
public const TYPE_ORDER = 'ORDER'; // 수주
|
|
|
|
|
|
|
|
|
|
public const TYPE_PURCHASE = 'PURCHASE'; // 발주
|
2025-07-29 23:22:31 +09:00
|
|
|
|
2026-01-22 17:23:36 +09:00
|
|
|
// 매출 인식 시점
|
|
|
|
|
public const SALES_ON_ORDER_CONFIRM = 'on_order_confirm'; // 수주확정 시
|
|
|
|
|
|
|
|
|
|
public const SALES_ON_SHIPMENT = 'on_shipment'; // 출하완료 시
|
|
|
|
|
|
|
|
|
|
public const SALES_MANUAL = 'manual'; // 수동 등록
|
|
|
|
|
|
|
|
|
|
public const SALES_RECOGNITION_TYPES = [
|
|
|
|
|
self::SALES_ON_ORDER_CONFIRM => '수주확정 시',
|
|
|
|
|
self::SALES_ON_SHIPMENT => '출하완료 시',
|
|
|
|
|
self::SALES_MANUAL => '수동 등록',
|
|
|
|
|
];
|
|
|
|
|
|
2025-07-29 23:22:31 +09:00
|
|
|
protected $table = 'orders';
|
|
|
|
|
|
|
|
|
|
protected $fillable = [
|
2026-01-05 15:56:46 +09:00
|
|
|
'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',
|
2026-01-16 21:58:57 +09:00
|
|
|
'options',
|
2026-01-22 17:23:36 +09:00
|
|
|
// 매출 연동
|
|
|
|
|
'sales_recognition',
|
|
|
|
|
'sale_id',
|
2026-01-05 15:56:46 +09:00
|
|
|
// 감사
|
|
|
|
|
'created_by',
|
|
|
|
|
'updated_by',
|
|
|
|
|
'deleted_by',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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',
|
2026-01-16 21:58:57 +09:00
|
|
|
'options' => 'array',
|
2026-01-05 15:56:46 +09:00
|
|
|
'created_at' => 'datetime',
|
|
|
|
|
'updated_at' => 'datetime',
|
|
|
|
|
'deleted_at' => 'datetime',
|
2025-07-29 23:22:31 +09:00
|
|
|
];
|
|
|
|
|
|
2026-01-23 21:32:23 +09:00
|
|
|
/**
|
|
|
|
|
* JSON 응답에 자동 포함할 accessor
|
|
|
|
|
*/
|
|
|
|
|
protected $appends = [
|
|
|
|
|
'delivery_method_label',
|
2026-01-28 22:24:21 +09:00
|
|
|
'shipping_cost_label',
|
2026-01-23 21:32:23 +09:00
|
|
|
];
|
|
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
/**
|
|
|
|
|
* 수주 상세 품목
|
|
|
|
|
*/
|
|
|
|
|
public function items(): HasMany
|
2025-07-29 23:22:31 +09:00
|
|
|
{
|
2026-01-05 15:56:46 +09:00
|
|
|
return $this->hasMany(OrderItem::class)->orderBy('sort_order');
|
2025-07-29 23:22:31 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
/**
|
|
|
|
|
* 수주 이력
|
|
|
|
|
*/
|
|
|
|
|
public function histories(): HasMany
|
2025-07-29 23:22:31 +09:00
|
|
|
{
|
|
|
|
|
return $this->hasMany(OrderHistory::class);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
/**
|
|
|
|
|
* 수주 버전
|
|
|
|
|
*/
|
|
|
|
|
public function versions(): HasMany
|
2025-07-29 23:22:31 +09:00
|
|
|
{
|
|
|
|
|
return $this->hasMany(OrderVersion::class);
|
|
|
|
|
}
|
2025-12-14 01:20:18 +09:00
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
/**
|
|
|
|
|
* 원본 견적
|
|
|
|
|
*/
|
|
|
|
|
public function quote(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Quote::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 거래처
|
|
|
|
|
*/
|
|
|
|
|
public function client(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Client::class);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 21:59:06 +09:00
|
|
|
/**
|
|
|
|
|
* 작성자 (담당자)
|
|
|
|
|
*/
|
|
|
|
|
public function writer(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(\App\Models\Members\User::class, 'writer_id');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
/**
|
|
|
|
|
* 품목 (통합 items 테이블)
|
|
|
|
|
*/
|
|
|
|
|
public function item(): BelongsTo
|
2025-12-14 01:20:18 +09:00
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Item::class, 'item_id');
|
|
|
|
|
}
|
2026-01-05 15:56:46 +09:00
|
|
|
|
2026-01-15 16:13:34 +09:00
|
|
|
/**
|
|
|
|
|
* 작업지시 목록
|
|
|
|
|
*/
|
|
|
|
|
public function workOrders(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(WorkOrder::class, 'sales_order_id');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 21:59:06 +09:00
|
|
|
/**
|
|
|
|
|
* 출하 목록
|
|
|
|
|
*/
|
|
|
|
|
public function shipments(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(Shipment::class, 'order_id');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 17:23:36 +09:00
|
|
|
/**
|
|
|
|
|
* 연결된 매출
|
|
|
|
|
*/
|
|
|
|
|
public function sale(): BelongsTo
|
|
|
|
|
{
|
|
|
|
|
return $this->belongsTo(Sale::class);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 이 수주에서 생성된 모든 매출 (출하별 포함)
|
|
|
|
|
*/
|
|
|
|
|
public function sales(): HasMany
|
|
|
|
|
{
|
|
|
|
|
return $this->hasMany(Sale::class, 'order_id');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 매출 인식 시점 라벨
|
|
|
|
|
*/
|
|
|
|
|
public function getSalesRecognitionLabelAttribute(): string
|
|
|
|
|
{
|
|
|
|
|
return self::SALES_RECOGNITION_TYPES[$this->sales_recognition] ?? '출하완료 시';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 21:32:23 +09:00
|
|
|
/**
|
|
|
|
|
* 배송방식 라벨 (common_codes 테이블에서 조회)
|
|
|
|
|
*/
|
|
|
|
|
public function getDeliveryMethodLabelAttribute(): string
|
|
|
|
|
{
|
|
|
|
|
return CommonCode::getLabel('delivery_method', $this->delivery_method_code);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 22:24:21 +09:00
|
|
|
/**
|
|
|
|
|
* 운임비용 라벨 (common_codes 테이블에서 조회)
|
|
|
|
|
*/
|
|
|
|
|
public function getShippingCostLabelAttribute(): string
|
|
|
|
|
{
|
|
|
|
|
$shippingCostCode = $this->options['shipping_cost_code'] ?? null;
|
|
|
|
|
|
|
|
|
|
return $shippingCostCode ? CommonCode::getLabel('shipping_cost', $shippingCostCode) : '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 17:23:36 +09:00
|
|
|
/**
|
|
|
|
|
* 수주확정 시 매출 생성 여부
|
|
|
|
|
*/
|
|
|
|
|
public function shouldCreateSaleOnConfirm(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->sales_recognition === self::SALES_ON_ORDER_CONFIRM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 출하완료 시 매출 생성 여부
|
|
|
|
|
*/
|
|
|
|
|
public function shouldCreateSaleOnShipment(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->sales_recognition === self::SALES_ON_SHIPMENT;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 15:56:46 +09:00
|
|
|
/**
|
|
|
|
|
* 품목들로부터 금액 합계 재계산
|
|
|
|
|
*/
|
|
|
|
|
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,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2025-07-29 23:22:31 +09:00
|
|
|
}
|