259 lines
6.7 KiB
PHP
259 lines
6.7 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Models\Products;
|
|||
|
|
|
|||
|
|
use App\Models\Materials\Material;
|
|||
|
|
use App\Models\Orders\ClientGroup;
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 단가 마스터 모델
|
|||
|
|
*
|
|||
|
|
* @property int $id
|
|||
|
|
* @property int $tenant_id
|
|||
|
|
* @property string $item_type_code
|
|||
|
|
* @property int $item_id
|
|||
|
|
* @property int|null $client_group_id
|
|||
|
|
* @property float|null $purchase_price
|
|||
|
|
* @property float|null $processing_cost
|
|||
|
|
* @property float|null $loss_rate
|
|||
|
|
* @property float|null $margin_rate
|
|||
|
|
* @property float|null $sales_price
|
|||
|
|
* @property string $rounding_rule
|
|||
|
|
* @property int $rounding_unit
|
|||
|
|
* @property string|null $supplier
|
|||
|
|
* @property \Carbon\Carbon $effective_from
|
|||
|
|
* @property \Carbon\Carbon|null $effective_to
|
|||
|
|
* @property string|null $note
|
|||
|
|
* @property string $status
|
|||
|
|
* @property bool $is_final
|
|||
|
|
* @property \Carbon\Carbon|null $finalized_at
|
|||
|
|
* @property int|null $finalized_by
|
|||
|
|
*/
|
|||
|
|
class Price extends Model
|
|||
|
|
{
|
|||
|
|
use BelongsToTenant, SoftDeletes;
|
|||
|
|
|
|||
|
|
protected $fillable = [
|
|||
|
|
'tenant_id',
|
|||
|
|
'item_type_code',
|
|||
|
|
'item_id',
|
|||
|
|
'client_group_id',
|
|||
|
|
'purchase_price',
|
|||
|
|
'processing_cost',
|
|||
|
|
'loss_rate',
|
|||
|
|
'margin_rate',
|
|||
|
|
'sales_price',
|
|||
|
|
'rounding_rule',
|
|||
|
|
'rounding_unit',
|
|||
|
|
'supplier',
|
|||
|
|
'effective_from',
|
|||
|
|
'effective_to',
|
|||
|
|
'note',
|
|||
|
|
'status',
|
|||
|
|
'is_final',
|
|||
|
|
'finalized_at',
|
|||
|
|
'finalized_by',
|
|||
|
|
'created_by',
|
|||
|
|
'updated_by',
|
|||
|
|
'deleted_by',
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
protected $casts = [
|
|||
|
|
'purchase_price' => 'decimal:4',
|
|||
|
|
'processing_cost' => 'decimal:4',
|
|||
|
|
'loss_rate' => 'decimal:2',
|
|||
|
|
'margin_rate' => 'decimal:2',
|
|||
|
|
'sales_price' => 'decimal:4',
|
|||
|
|
'rounding_unit' => 'integer',
|
|||
|
|
'effective_from' => 'date',
|
|||
|
|
'effective_to' => 'date',
|
|||
|
|
'is_final' => 'boolean',
|
|||
|
|
'finalized_at' => 'datetime',
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 고객 그룹 관계
|
|||
|
|
*/
|
|||
|
|
public function clientGroup(): BelongsTo
|
|||
|
|
{
|
|||
|
|
return $this->belongsTo(ClientGroup::class, 'client_group_id');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 리비전 이력 관계
|
|||
|
|
*/
|
|||
|
|
public function revisions(): HasMany
|
|||
|
|
{
|
|||
|
|
return $this->hasMany(PriceRevision::class, 'price_id')->orderBy('revision_number', 'desc');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 품목 관계 (Polymorphic - item_type_code에 따라)
|
|||
|
|
*/
|
|||
|
|
public function item()
|
|||
|
|
{
|
|||
|
|
if ($this->item_type_code === 'PRODUCT') {
|
|||
|
|
return $this->belongsTo(Product::class, 'item_id');
|
|||
|
|
} elseif ($this->item_type_code === 'MATERIAL') {
|
|||
|
|
return $this->belongsTo(Material::class, 'item_id');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 제품 관계 (item_type_code = PRODUCT인 경우)
|
|||
|
|
*/
|
|||
|
|
public function product(): BelongsTo
|
|||
|
|
{
|
|||
|
|
return $this->belongsTo(Product::class, 'item_id');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 자재 관계 (item_type_code = MATERIAL인 경우)
|
|||
|
|
*/
|
|||
|
|
public function material(): BelongsTo
|
|||
|
|
{
|
|||
|
|
return $this->belongsTo(Material::class, 'item_id');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 스코프 ==========
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 특정 품목 필터
|
|||
|
|
*/
|
|||
|
|
public function scopeForItem($query, string $itemType, int $itemId)
|
|||
|
|
{
|
|||
|
|
return $query->where('item_type_code', $itemType)
|
|||
|
|
->where('item_id', $itemId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 고객 그룹 필터
|
|||
|
|
*/
|
|||
|
|
public function scopeForClientGroup($query, ?int $clientGroupId)
|
|||
|
|
{
|
|||
|
|
return $query->where('client_group_id', $clientGroupId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 특정 일자에 유효한 단가
|
|||
|
|
*/
|
|||
|
|
public function scopeValidAt($query, $date)
|
|||
|
|
{
|
|||
|
|
return $query->where('effective_from', '<=', $date)
|
|||
|
|
->where(function ($q) use ($date) {
|
|||
|
|
$q->whereNull('effective_to')
|
|||
|
|
->orWhere('effective_to', '>=', $date);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 상태 필터
|
|||
|
|
*/
|
|||
|
|
public function scopeStatus($query, string $status)
|
|||
|
|
{
|
|||
|
|
return $query->where('status', $status);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 활성 단가만
|
|||
|
|
*/
|
|||
|
|
public function scopeActive($query)
|
|||
|
|
{
|
|||
|
|
return $query->where('status', 'active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 확정된 단가만
|
|||
|
|
*/
|
|||
|
|
public function scopeFinalized($query)
|
|||
|
|
{
|
|||
|
|
return $query->where('is_final', true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== 계산 메서드 ==========
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 총원가 계산
|
|||
|
|
* 총원가 = (매입단가 + 가공비) × (1 + LOSS율/100)
|
|||
|
|
*/
|
|||
|
|
public function calculateTotalCost(): float
|
|||
|
|
{
|
|||
|
|
$baseCost = ($this->purchase_price ?? 0) + ($this->processing_cost ?? 0);
|
|||
|
|
$lossMultiplier = 1 + (($this->loss_rate ?? 0) / 100);
|
|||
|
|
|
|||
|
|
return $baseCost * $lossMultiplier;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 판매단가 계산 (마진율 기반)
|
|||
|
|
* 판매단가 = 반올림(총원가 × (1 + 마진율/100), 반올림단위, 반올림규칙)
|
|||
|
|
*/
|
|||
|
|
public function calculateSalesPrice(): float
|
|||
|
|
{
|
|||
|
|
$totalCost = $this->calculateTotalCost();
|
|||
|
|
$marginMultiplier = 1 + (($this->margin_rate ?? 0) / 100);
|
|||
|
|
$rawPrice = $totalCost * $marginMultiplier;
|
|||
|
|
|
|||
|
|
return $this->applyRounding($rawPrice);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 반올림 적용
|
|||
|
|
*/
|
|||
|
|
private function applyRounding(float $value): float
|
|||
|
|
{
|
|||
|
|
$unit = $this->rounding_unit ?: 1;
|
|||
|
|
|
|||
|
|
return match ($this->rounding_rule) {
|
|||
|
|
'ceil' => ceil($value / $unit) * $unit,
|
|||
|
|
'floor' => floor($value / $unit) * $unit,
|
|||
|
|
default => round($value / $unit) * $unit, // 'round'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 확정 가능 여부
|
|||
|
|
*/
|
|||
|
|
public function canFinalize(): bool
|
|||
|
|
{
|
|||
|
|
return ! $this->is_final && in_array($this->status, ['draft', 'active']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 수정 가능 여부
|
|||
|
|
*/
|
|||
|
|
public function canEdit(): bool
|
|||
|
|
{
|
|||
|
|
return ! $this->is_final;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 스냅샷 생성 (리비전용)
|
|||
|
|
*/
|
|||
|
|
public function toSnapshot(): array
|
|||
|
|
{
|
|||
|
|
return [
|
|||
|
|
'purchase_price' => $this->purchase_price,
|
|||
|
|
'processing_cost' => $this->processing_cost,
|
|||
|
|
'loss_rate' => $this->loss_rate,
|
|||
|
|
'margin_rate' => $this->margin_rate,
|
|||
|
|
'sales_price' => $this->sales_price,
|
|||
|
|
'rounding_rule' => $this->rounding_rule,
|
|||
|
|
'rounding_unit' => $this->rounding_unit,
|
|||
|
|
'supplier' => $this->supplier,
|
|||
|
|
'effective_from' => $this->effective_from?->format('Y-m-d'),
|
|||
|
|
'effective_to' => $this->effective_to?->format('Y-m-d'),
|
|||
|
|
'status' => $this->status,
|
|||
|
|
'is_final' => $this->is_final,
|
|||
|
|
'note' => $this->note,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
}
|