'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 const STATUS_DRAFT = 'draft'; public const STATUS_ACTIVE = 'active'; public const STATUS_INACTIVE = 'inactive'; public const STATUS_FINALIZED = 'finalized'; /** * 품목 유형 상수 */ public const ITEM_TYPE_PRODUCT = 'PRODUCT'; public const ITEM_TYPE_MATERIAL = 'MATERIAL'; /** * 반올림 규칙 상수 */ public const ROUNDING_ROUND = 'round'; public const ROUNDING_CEIL = 'ceil'; public const ROUNDING_FLOOR = 'floor'; /** * 특정 품목의 현재 유효 단가 조회 */ public static function getCurrentPrice( int $tenantId, string $itemTypeCode, int $itemId, ?int $clientGroupId = null ): ?self { return static::where('tenant_id', $tenantId) ->where('item_type_code', $itemTypeCode) ->where('item_id', $itemId) ->where(function ($query) use ($clientGroupId) { if ($clientGroupId) { $query->where('client_group_id', $clientGroupId) ->orWhereNull('client_group_id'); } else { $query->whereNull('client_group_id'); } }) ->where('status', self::STATUS_ACTIVE) ->where('effective_from', '<=', now()) ->where(function ($query) { $query->whereNull('effective_to') ->orWhere('effective_to', '>=', now()); }) ->orderByRaw('client_group_id IS NULL') // 고객그룹 지정된 것 우선 ->orderBy('effective_from', 'desc') ->first(); } /** * 품목 코드로 현재 유효 판매단가 조회 * (quote_formula_items.item_code와 연동용) */ public static function getSalesPriceByItemCode(int $tenantId, string $itemCode): float { // items 테이블에서 품목 코드로 검색 (products + materials 통합 테이블) $item = DB::table('items') ->where('tenant_id', $tenantId) ->where('code', $itemCode) ->whereNull('deleted_at') ->first(); if (! $item) { return 0; } $price = static::getCurrentPrice($tenantId, $item->item_type, $item->id); return (float) ($price?->sales_price ?? 0); } }