diff --git a/INDEX.md b/INDEX.md index 0413d43..dbe0a16 100644 --- a/INDEX.md +++ b/INDEX.md @@ -20,7 +20,7 @@ | Blade+React | `dev/standards/blade-react-policy.md` | Blade JSX 이중 중괄호 충돌 방지 | | 이메일 연동 | `dev/guides/tenant-email-integration-guide.md` | 테넌트 메일 연동, SMTP 프리셋, MNG 관리 | | 품목관리 | `rules/item-policy.md` | 품목 정책 | -| 단가관리 | `rules/pricing-policy.md` | 원가/판매가, 리비전 | +| 단가관리 | `rules/pricing-policy.md` | 6 Depth 단가 체계, 흐름도, 계산공식, 버전관리 개선방향 | | 견적관리 | `features/quotes/README.md` | 견적 시스템, BOM 계산 | | 급여관리 API | `frontend/api-specs/payroll-api.md` | 급여관리 API 전체 명세 (18개 엔드포인트) | | 바로빌 회계 API | `frontend/api-specs/barobill-api.md` | 카드/은행/홈택스 REST API (42개 엔드포인트) | diff --git a/rules/README.md b/rules/README.md index f847c0b..4773645 100644 --- a/rules/README.md +++ b/rules/README.md @@ -12,7 +12,7 @@ | 문서 | 설명 | |------|------| | [item-policy.md](item-policy.md) | 품목 정책 (유형 체계, 예약어, API 규칙) | -| [pricing-policy.md](pricing-policy.md) | 단가 정책 (원가/판매가 계산, 리비전 관리) | +| [pricing-policy.md](pricing-policy.md) | 단가 정책 (6 Depth 체계, 흐름도, 계산공식, 버전관리 개선방향) | | [customer-pricing.md](customer-pricing.md) | 고객 안내용 서비스 요금표 | | [partner-commission.md](partner-commission.md) | 영업파트너 수당 체계 및 정산 | | [billing-policy.md](billing-policy.md) | 내부용 원가/마진/코드참조 (CONFIDENTIAL) | diff --git a/rules/pricing-policy.md b/rules/pricing-policy.md index 4905354..2381c6f 100644 --- a/rules/pricing-policy.md +++ b/rules/pricing-policy.md @@ -1,263 +1,199 @@ -# 단가 정책 (Pricing Policy) +# SAM 단가 정책 (Pricing Policy) > **작성일**: 2025-12-08 -> **상태**: 설계 확정 -> **관련 요청**: `docs/front/[API-2025-12-08] pricing-api-enhancement-request.md` +> **최종 갱신**: 2026-03-19 +> **상태**: 설계 확정 + 개선방향 기획 (Depth 버전관리) --- ## 1. 개요 ### 1.1 목적 -- 품목(제품/자재)의 **표준단가** 관리 -- **실제 입고단가** 기반 원가 계산 -- 단가 변경 **이력(리비전)** 추적 -- 고객그룹별 **차등 판매가** 지원 + +SAM 시스템 내 모든 단가의 **유형 정의**, **계산 공식**, **흐름 추적**, **버전 관리**를 명확히 한다. ### 1.2 핵심 원칙 | 원칙 | 설명 | |------|------| +| **Depth 기반 흐름** | 매입단가 → 표준원가 → 판매단가 → 견적단가 → 수주단가 → 매출단가 | | **실제원가 우선** | 수입검사 입고단가 > 표준원가 | -| **시점 기반 유효성** | effective_from ~ effective_to 기간 내 유효 | -| **리비전 추적** | 모든 변경 사항 이력 보관 | -| **확정 후 불변** | finalized 상태는 수정 불가 | +| **시점 기반 유효성** | `effective_from` ~ `effective_to` 기간 내 유효 | +| **리비전 추적** | 모든 변경 사항 이력 보관 (`price_revisions`) | +| **확정 후 불변** | `finalized` 상태는 수정/삭제 불가 | +| **단가 복제 원칙** | 하위 단계(견적→수주→매출)로 복제된 단가는 원본과 독립 | ---- - -## 2. 테이블 구조 - -### 2.1 ERD 개요 +### 1.3 단가 유형 정의 (6 Depth) ``` -┌─────────────────┐ ┌─────────────────────┐ -│ prices │──────<│ price_revisions │ -│ (단가 마스터) │ │ (변경 이력) │ -└────────┬────────┘ └─────────────────────┘ - │ - │ item_type_code + item_id - │ - ┌────┴────┐ - │ │ -┌───▼───┐ ┌───▼─────┐ -│products│ │materials│ -└───────┘ └────┬────┘ - │ - │ material_id - ▼ - ┌─────────────────┐ - │material_receipts│ - │ (실제 입고단가) │ - └─────────────────┘ -``` - -### 2.2 prices 테이블 (단가 마스터) - -```sql -CREATE TABLE prices ( - id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', - - -- 품목 연결 -- - item_type_code VARCHAR(20) NOT NULL COMMENT '품목유형 (PRODUCT/MATERIAL)', - item_id BIGINT UNSIGNED NOT NULL COMMENT '품목 ID', - client_group_id BIGINT UNSIGNED NULL COMMENT '고객그룹 ID (NULL=기본가)', - - -- 원가 정보 -- - purchase_price DECIMAL(15,4) NULL COMMENT '매입단가 (표준원가)', - processing_cost DECIMAL(15,4) NULL COMMENT '가공비', - loss_rate DECIMAL(5,2) NULL COMMENT 'LOSS율 (%)', - - -- 판매가 정보 -- - margin_rate DECIMAL(5,2) NULL COMMENT '마진율 (%)', - sales_price DECIMAL(15,4) NULL COMMENT '판매단가', - rounding_rule ENUM('round','ceil','floor') DEFAULT 'round' COMMENT '반올림 규칙', - rounding_unit INT DEFAULT 1 COMMENT '반올림 단위 (1,10,100,1000)', - - -- 메타 정보 -- - supplier VARCHAR(255) NULL COMMENT '공급업체', - effective_from DATE NOT NULL COMMENT '적용 시작일', - effective_to DATE NULL COMMENT '적용 종료일', - note TEXT NULL COMMENT '비고', - - -- 상태 관리 -- - status ENUM('draft','active','inactive','finalized') DEFAULT 'draft' COMMENT '상태', - is_final BOOLEAN DEFAULT FALSE COMMENT '최종 확정 여부', - finalized_at DATETIME NULL COMMENT '확정 일시', - finalized_by BIGINT UNSIGNED NULL COMMENT '확정자 ID', - - -- 감사 컬럼 -- - created_by BIGINT UNSIGNED NULL COMMENT '생성자 ID', - updated_by BIGINT UNSIGNED NULL COMMENT '수정자 ID', - deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자 ID', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL COMMENT 'Soft Delete', - - -- 인덱스 -- - INDEX idx_prices_tenant (tenant_id), - INDEX idx_prices_item (tenant_id, item_type_code, item_id), - INDEX idx_prices_effective (tenant_id, effective_from, effective_to), - INDEX idx_prices_status (tenant_id, status), - UNIQUE idx_prices_unique (tenant_id, item_type_code, item_id, client_group_id, effective_from, deleted_at) -) COMMENT='단가 마스터'; -``` - -### 2.3 price_revisions 테이블 (변경 이력) - -```sql -CREATE TABLE price_revisions ( - id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', - price_id BIGINT UNSIGNED NOT NULL COMMENT '단가 ID', - - -- 리비전 정보 -- - revision_number INT NOT NULL COMMENT '리비전 번호', - changed_at DATETIME NOT NULL COMMENT '변경 일시', - changed_by BIGINT UNSIGNED NOT NULL COMMENT '변경자 ID', - change_reason VARCHAR(500) NULL COMMENT '변경 사유', - - -- 변경 스냅샷 (JSON) -- - before_snapshot JSON NULL COMMENT '변경 전 데이터', - after_snapshot JSON NOT NULL COMMENT '변경 후 데이터', - - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - - -- 인덱스 -- - INDEX idx_revisions_price (price_id), - INDEX idx_revisions_tenant (tenant_id), - UNIQUE idx_revisions_unique (price_id, revision_number), - - FOREIGN KEY (price_id) REFERENCES prices(id) ON DELETE CASCADE -) COMMENT='단가 변경 이력'; -``` - -### 2.4 material_receipts 테이블 (기존 - 실제 입고단가) - -```sql --- 기존 테이블, purchase_price_excl_vat 필드 활용 --- 수입검사 시 실제 입고 단가 입력 +Depth 0 매입단가 prices.purchase_price 공급업체로부터의 구매가 +Depth 1 표준원가 (매입단가 + 가공비) × LOSS 제조 기준 원가 +Depth 2 판매단가 prices.sales_price 표준원가 + 마진, 반올림 적용 +Depth 3 견적단가 quote_items.unit_price BOM 산출 또는 수동 입력 +Depth 4 수주단가 order_items.unit_price 견적에서 복제 (확정가) +Depth 5 매출단가 sales.supply_amount 수주에서 복제 (최종가) ``` --- -## 3. 원가 계산 정책 +## 2. 단가 흐름도 -### 3.1 원가 조회 우선순위 +### 2.1 전체 흐름 ``` -┌─────────────────────────────────────────────────────────┐ -│ 원가 조회 로직 │ -├─────────────────────────────────────────────────────────┤ -│ 1순위: 수입검사 입고단가 │ -│ material_receipts.purchase_price_excl_vat │ -│ (가장 최근 입고 기준) │ -│ │ -│ 2순위: 표준원가 │ -│ prices.purchase_price │ -│ (해당 일자에 유효한 단가) │ -│ │ -│ 3순위: NULL (단가 미등록) │ -│ → 경고 메시지 반환 │ -└─────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────┐ +│ Depth 0: 매입단가 │ +│ prices.purchase_price + prices.processing_cost │ +│ 소스: 공급업체 견적, 수입검사 입고단가 │ +└──────────────────────┬──────────────────────────────────────┘ + ↓ × (1 + LOSS율/100) +┌──────────────────────┴──────────────────────────────────────┐ +│ Depth 1: 표준원가 │ +│ = (매입단가 + 가공비) × (1 + LOSS율/100) │ +└──────────────────────┬──────────────────────────────────────┘ + ↓ × (1 + 마진율/100) + 반올림 +┌──────────────────────┴──────────────────────────────────────┐ +│ Depth 2: 판매단가 │ +│ prices.sales_price (고객그룹별 차등가 가능) │ +└──────────────────────┬──────────────────────────────────────┘ + ↓ BOM 산출 or 수동 입력 +┌──────────────────────┴──────────────────────────────────────┐ +│ Depth 3: 견적단가 │ +│ quote_items.unit_price (BOM 부품별 판매단가 합산 = 완제품가) │ +│ quotes.total_amount = 재료비 + 노무비 + 시공비 - 할인 │ +└──────────────────────┬──────────────────────────────────────┘ + ↓ convertToOrder() 복제 +┌──────────────────────┴──────────────────────────────────────┐ +│ Depth 4: 수주단가 │ +│ order_items.unit_price (견적단가 복제) │ +│ + supply_amount, tax_amount, total_amount 재계산 │ +└──────────────────────┬──────────────────────────────────────┘ + ↓ createFromOrder/Shipment 복제 +┌──────────────────────┴──────────────────────────────────────┐ +│ Depth 5: 매출단가 │ +│ sales.supply_amount, tax_amount, total_amount (수주 복제) │ +└─────────────────────────────────────────────────────────────┘ ``` -### 3.2 원가 계산 공식 +### 2.2 단가 저장 위치 -``` -총원가 = (매입단가 + 가공비) × (1 + LOSS율/100) - -예시: -- 매입단가: 10,000원 -- 가공비: 2,000원 -- LOSS율: 5% -- 총원가 = (10,000 + 2,000) × 1.05 = 12,600원 -``` - -### 3.3 품목 유형별 원가 적용 - -| 품목 유형 | 원가 소스 | 설명 | -|----------|----------|------| -| **MATERIAL** (자재) | material_receipts 우선 | 실제 입고단가 사용 | -| **PRODUCT** (제품) | prices 테이블 | 표준원가 사용 | +| Depth | 테이블 | 단가 컬럼 | 금액 컬럼 | +|:-----:|--------|----------|----------| +| 0 | `prices` | `purchase_price`, `processing_cost` | — | +| 1 | (계산값) | — | `(purchase_price + processing_cost) × (1 + loss_rate)` | +| 2 | `prices` | `sales_price` | — | +| 3 | `quote_items` | `unit_price` | `total_price` | +| 3 | `quotes` | — | `material_cost`, `labor_cost`, `install_cost`, `total_amount` | +| 4 | `order_items` | `unit_price` | `supply_amount`, `tax_amount`, `total_amount` | +| 4 | `orders` | — | `supply_amount`, `tax_amount`, `total_amount` | +| 5 | `sales` | — | `supply_amount`, `tax_amount`, `total_amount` | --- -## 4. 판매가 계산 정책 +## 3. 단가 계산 공식 -### 4.1 판매가 계산 공식 +### 3.1 표준원가 (Depth 1) ``` -판매단가 = 반올림(총원가 × (1 + 마진율/100), 반올림단위, 반올림규칙) +표준원가 = (매입단가 + 가공비) × (1 + LOSS율/100) -예시: -- 총원가: 12,600원 -- 마진율: 25% -- 반올림단위: 100 -- 반올림규칙: round -- 판매단가 = round(12,600 × 1.25, 100) = round(15,750, 100) = 15,800원 +예시: (10,000 + 2,000) × 1.05 = 12,600원 ``` -### 4.2 반올림 규칙 - -| 규칙 | 설명 | 예시 (단위: 100) | -|------|------|-----------------| -| `round` | 반올림 | 15,750 → 15,800 | -| `ceil` | 올림 | 15,701 → 15,800 | -| `floor` | 버림 | 15,799 → 15,700 | - -### 4.3 고객그룹별 차등가 +### 3.2 판매단가 (Depth 2) ``` -┌─────────────────────────────────────────────────────────┐ -│ 판매가 조회 우선순위 │ -├─────────────────────────────────────────────────────────┤ -│ 1순위: 고객그룹별 특별가 │ -│ prices WHERE client_group_id = [고객의 그룹ID] │ -│ │ -│ 2순위: 기본 판매가 │ -│ prices WHERE client_group_id IS NULL │ -└─────────────────────────────────────────────────────────┘ +판매단가 = 반올림(표준원가 × (1 + 마진율/100), 반올림단위, 반올림규칙) + +예시: round(12,600 × 1.25 / 100) × 100 = 15,800원 ``` +| 반올림규칙 | 설명 | 15,750 (단위 100) | +|-----------|------|------------------| +| `round` | 반올림 | 15,800 | +| `ceil` | 올림 | 15,800 | +| `floor` | 버림 | 15,700 | + +### 3.3 원가 조회 우선순위 + +| 순위 | 소스 | 조건 | +|:----:|------|------| +| 1 | `material_receipts.purchase_price_excl_vat` | 자재만, 최근 입고 기준 | +| 2 | `prices.purchase_price` | 해당 일자 유효 단가 | +| 3 | NULL | 단가 미등록 → 경고 반환 | + +### 3.4 견적단가 (Depth 3) — BOM 산출 + +``` +견적 품목 금액 = 계산수량 × 단가 +견적 소계 = 재료비 + 노무비 + 시공비 +견적 총액 = 소계 - 할인금액 +``` + +BOM 자동산출 시 `EstimatePriceService`가 items + item_details + prices 조인으로 판매단가 조회. + +### 3.5 수주단가 (Depth 4) + +``` +공급가액 = 수량 × 단가 - 할인금액 +세액 = 공급가액 × 10% +총액 = 공급가액 + 세액 +``` + +### 3.6 매출단가 (Depth 5) + +수주의 `supply_amount`, `tax_amount`, `total_amount`를 그대로 복제. + +--- + +## 4. 고객그룹 차등가 + +### 4.1 구조 + +`client_groups` 테이블: + +| 필드 | 설명 | +|------|------| +| `group_code` | 그룹 코드 | +| `group_name` | 그룹명 | +| `price_rate` | 가격 배율 (1.0=기본, 0.9=10%할인, 1.1=10%인상) | + +### 4.2 조회 우선순위 + +``` +1순위: prices WHERE client_group_id = [거래처의 그룹ID] → 그룹 전용 단가 +2순위: prices WHERE client_group_id IS NULL → 기본 단가 +``` + +`Price::getCurrentPrice($tenantId, $itemTypeCode, $itemId, $clientGroupId)` 메서드가 이 로직을 처리한다. + --- ## 5. 상태 관리 -### 5.1 단가 상태 (status) +### 5.1 상태 전이 ``` -┌──────────┐ 등록 ┌──────────┐ 활성화 ┌──────────┐ -│ draft │ ──────────> │ active │ ─────────── │ finalized│ -│ (초안) │ │ (활성) │ 확정 │ (확정) │ -└──────────┘ └────┬─────┘ └──────────┘ - │ - │ 비활성화 - ▼ - ┌──────────┐ - │ inactive │ - │ (비활성) │ - └──────────┘ +draft ──→ active ──→ finalized + (초안) (활성) (확정, 불변) + │ + └──→ inactive (비활성) ``` ### 5.2 상태별 권한 | 상태 | 조회 | 수정 | 삭제 | 확정 | -|------|------|------|------|------| +|------|:----:|:----:|:----:|:----:| | `draft` | ✅ | ✅ | ✅ | ✅ | | `active` | ✅ | ✅ | ✅ | ✅ | | `inactive` | ✅ | ✅ | ✅ | ❌ | | `finalized` | ✅ | ❌ | ❌ | ❌ | -### 5.3 확정 (Finalize) 규칙 +### 5.3 확정 규칙 -- **확정 조건**: status가 `draft` 또는 `active`일 때만 가능 -- **확정 효과**: - - `is_final = true` - - `status = 'finalized'` - - `finalized_at = NOW()` - - `finalized_by = 현재 사용자` -- **확정 후**: 수정/삭제 불가, 조회만 가능 +- `sales_price` 또는 `purchase_price` 중 하나 이상 존재해야 확정 가능 +- 확정 시: `is_final = true`, `status = 'finalized'`, `finalized_at/by` 기록 +- 확정 후: 수정 필요 시 **새 단가 레코드 생성** (기존 단가의 `effective_to` 자동 설정) --- @@ -265,138 +201,205 @@ CREATE TABLE price_revisions ( ### 6.1 리비전 생성 시점 -| 이벤트 | 리비전 생성 | 설명 | -|--------|-----------|------| -| 최초 등록 | ✅ | revision_number = 1, before_snapshot = NULL | -| 수정 | ✅ | revision_number++, 변경 전/후 기록 | -| 삭제 | ✅ | soft delete, 삭제 전 상태 기록 | -| 확정 | ✅ | 확정 시점 스냅샷 기록 | +| 이벤트 | 리비전 | 설명 | +|--------|:------:|------| +| 최초 등록 | ✅ | `revision_number = 1`, `before_snapshot = NULL` | +| 수정 | ✅ | `revision_number++`, 변경 전/후 기록 | +| 삭제 | ✅ | soft delete, 삭제 전 스냅샷 | +| 확정 | ✅ | 확정 시점 스냅샷 | -### 6.2 스냅샷 JSON 구조 - -```json -{ - "purchase_price": 10000, - "processing_cost": 2000, - "loss_rate": 5.0, - "margin_rate": 25.0, - "sales_price": 15800, - "effective_from": "2025-01-01", - "effective_to": null, - "status": "active", - "supplier": "ABC공급" -} -``` - ---- - -## 7. API 엔드포인트 - -### 7.1 엔드포인트 목록 - -| Method | Endpoint | 설명 | 우선순위 | -|--------|----------|------|---------| -| GET | `/api/v1/pricing` | 단가 목록 조회 | 🔴 필수 | -| GET | `/api/v1/pricing/{id}` | 단가 상세 조회 | 🔴 필수 | -| POST | `/api/v1/pricing` | 단가 등록 | 🔴 필수 | -| PUT | `/api/v1/pricing/{id}` | 단가 수정 | 🔴 필수 | -| DELETE | `/api/v1/pricing/{id}` | 단가 삭제 | 🔴 필수 | -| GET | `/api/v1/pricing/by-items` | 품목별 단가 현황 | 🔴 필수 | -| GET | `/api/v1/pricing/{id}/revisions` | 변경 이력 조회 | 🟡 중요 | -| POST | `/api/v1/pricing/{id}/finalize` | 단가 확정 | 🟢 권장 | -| GET | `/api/v1/pricing/cost` | 원가 조회 (계산) | 🟡 중요 | - -### 7.2 원가 조회 API - -``` -GET /api/v1/pricing/cost?item_type=MATERIAL&item_id=123&date=2025-01-15 -``` - -**Response:** -```json -{ - "success": true, - "data": { - "item_type": "MATERIAL", - "item_id": 123, - "cost_source": "receipt", // "receipt" | "standard" | "not_found" - "purchase_price": 10500, - "receipt_id": 456, // cost_source가 receipt일 때 - "receipt_date": "2025-01-10", - "price_id": null // cost_source가 standard일 때 - } -} -``` - ---- - -## 8. 비즈니스 규칙 - -### 8.1 검증 규칙 - -| 규칙 | 설명 | -|------|------| -| **R1** | effective_from은 필수 | -| **R2** | effective_from ≤ effective_to (종료일이 있을 경우) | -| **R3** | 동일 품목+고객그룹+적용시작일 조합은 중복 불가 | -| **R4** | 확정된 단가는 수정/삭제 불가 | -| **R5** | 마진율은 0~100% 범위 | -| **R6** | LOSS율은 0~100% 범위 | -| **R7** | 반올림단위는 1, 10, 100, 1000 중 하나 | - -### 8.2 기간 중복 처리 +### 6.2 기간 중복 자동 처리 ``` 기존: 2025-01-01 ~ NULL (무기한) 신규: 2025-06-01 ~ NULL -→ 기존 단가의 effective_to를 2025-05-31로 자동 설정 +→ 기존의 effective_to를 2025-05-31로 자동 설정 → 신규 단가 등록 ``` -### 8.3 삭제 정책 +--- -- **Soft Delete** 적용 -- 삭제 시 `deleted_at`, `deleted_by` 기록 -- 삭제된 단가도 리비전 이력에서 조회 가능 +## 7. 비즈니스 규칙 + +| 규칙 | 설명 | +|------|------| +| **R1** | `effective_from`은 필수 | +| **R2** | `effective_from` ≤ `effective_to` | +| **R3** | 동일 품목+고객그룹+적용시작일 중복 불가 | +| **R4** | 확정된 단가는 수정/삭제 불가 | +| **R5** | 마진율: 0% 이상 (상한 없음) | +| **R6** | LOSS율: 0~100% | +| **R7** | 반올림단위: 1, 10, 100, 1000 중 택1 | +| **R8** | Soft Delete 적용 (삭제 후 리비전에서 조회 가능) | --- -## 9. 기존 시스템 호환 +## 8. API 엔드포인트 -### 9.1 price_histories 테이블 처리 +| Method | Endpoint | 설명 | +|--------|----------|------| +| GET | `/api/v1/pricing` | 단가 목록 | +| GET | `/api/v1/pricing/{id}` | 단가 상세 (리비전 10개 포함) | +| POST | `/api/v1/pricing` | 단가 등록 | +| PUT | `/api/v1/pricing/{id}` | 단가 수정 (`change_reason` 선택) | +| DELETE | `/api/v1/pricing/{id}` | 단가 삭제 | +| POST | `/api/v1/pricing/{id}/finalize` | 단가 확정 | +| POST | `/api/v1/pricing/by-items` | 품목별 단가 현황 | +| GET | `/api/v1/pricing/{id}/revisions` | 변경 이력 | +| POST | `/api/v1/pricing/cost` | 원가 조회 | +| GET | `/api/v1/pricing/stats` | 단가 통계 | +| DELETE | `/api/v1/pricing/bulk` | 일괄 삭제 | -| 상태 | 설명 | +--- + +## 9. 핵심 코드 위치 + +| 구분 | 파일 | 역할 | +|------|------|------| +| 모델 | `api/app/Models/Products/Price.php` | `getCurrentPrice()`, `calculateSalesPrice()`, `getSalesPriceByItemCode()` | +| 모델 | `api/app/Models/Products/PriceRevision.php` | 변경 이력 (Immutable) | +| 서비스 | `api/app/Services/PricingService.php` | 단가 CRUD, 리비전, 확정, 원가 조회 | +| 서비스 | `api/app/Services/Quote/EstimatePriceService.php` | BOM 자동산출 시 부품 단가 조회 | +| 서비스 | `api/app/Services/Quote/QuoteService.php` | 견적→수주 전환 (`convertToOrder`) | +| 모델 | `api/app/Models/Orders/OrderItem.php` | `createFromQuoteItem()`, 금액 재계산 | +| 모델 | `api/app/Models/Tenants/Sale.php` | `createFromOrder()`, `createFromShipment()` | + +--- + +## 10. 개선방향: Depth 기반 단가 버전관리 + +> **상태**: 기획 단계 +> **배경**: 현재 원자재 가격 인상 시 하위 Depth(판매단가, 견적단가)에 대한 영향 분석이 수동으로 이루어지고 있다. Depth 개념을 도입하여 단가 변경의 **상향 전파(Cascade)** 와 **영향 분석(Impact Analysis)** 을 체계화한다. + +### 10.1 현재 한계 + +| 문제 | 설명 | |------|------| -| **현재** | 기존 price_histories 데이터 유지 | -| **전환 기간** | 양쪽 테이블 병행 운영 | -| **전환 완료 후** | price_histories deprecated | +| **단절된 추적** | 매입단가 변경 시 어떤 판매단가/견적에 영향이 있는지 추적 불가 | +| **수동 갱신** | 원자재 인상 → 판매단가 수동 재계산 → 견적 재작성 필요 | +| **스냅샷 부재** | 견적/수주 시점의 Depth 0~2 단가를 역추적할 수 없음 | +| **일괄 변경 불가** | 특정 공급업체 자재 10% 인상 시 관련 판매단가 일괄 갱신 불가 | -### 9.2 데이터 마이그레이션 +### 10.2 개선 방향 -```sql --- 기존 데이터를 prices 테이블로 마이그레이션 -INSERT INTO prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, effective_from, effective_to, - status, created_by, created_at -) -SELECT - tenant_id, item_type_code, item_id, client_group_id, - price as purchase_price, started_at, ended_at, - 'active', created_by, created_at -FROM price_histories -WHERE deleted_at IS NULL; +#### Phase 1: 단가 연쇄 관계 추적 + +`prices` 테이블에 Depth 관계를 기록하여, 하위 Depth 단가가 어떤 상위 Depth 단가에서 파생되었는지 추적한다. + +``` +신규 필드 (prices 테이블): + price_depth TINYINT — 0(매입), 2(판매) 등 + parent_price_id BIGINT — 이 단가의 원천 단가 ID (NULL=최상위) + price_version VARCHAR(20) — 단가 버전 (예: 'v2026-Q1') ``` ---- +**효과**: 매입단가(Depth 0) 변경 시 → 해당 품목의 판매단가(Depth 2) 목록을 즉시 조회 가능. -## 10. 관련 문서 +#### Phase 2: 변경 영향 분석 API -- [API 개발 규칙](../standards/api-rules.md) -- [데이터베이스 스키마](../system/database/README.md) -- [프론트엔드 요청서](../front/[API-2025-12-08]%20pricing-api-enhancement-request.md) +매입단가 변경 전 영향 범위를 분석하는 API를 제공한다. + +``` +POST /api/v1/pricing/impact-analysis +Body: { "price_id": 123, "new_purchase_price": 12000 } + +Response: +{ + "affected_sales_prices": [ + { "id": 456, "item_name": "가이드레일", "current": 15800, "projected": 17400, "diff": "+10.1%" } + ], + "affected_quotes": [ + { "id": 789, "quote_number": "KD-SC-260301-01", "status": "draft", "impact": "+52,000원" } + ], + "affected_orders": [ + { "id": 101, "order_number": "ORD-260215-001", "status": "confirmed", "impact": "변경 불가 (확정)" } + ] +} +``` + +#### Phase 3: 단가 버전 그룹 관리 + +분기/반기 단위로 단가 버전을 묶어 관리한다. + +``` +신규 테이블: price_versions + id, tenant_id + version_code VARCHAR(20) — 'v2026-Q1', 'v2026-H1' + version_name VARCHAR(100) — '2026년 1분기 단가' + base_version_id BIGINT — 이전 버전 ID (NULL=최초) + effective_from DATE — 적용 시작일 + status ENUM — draft/active/archived + change_summary JSON — { total_items: 150, avg_increase: 3.2% } + created_by, created_at +``` + +**워크플로우**: + +``` +1. 신규 버전 생성 (draft) + → 이전 버전의 모든 단가를 복사 +2. 변경 대상 품목의 매입단가 수정 + → 판매단가 자동 재계산 (Cascade) +3. 영향 분석 리포트 생성 + → 변경 전후 비교, 마진 변화 시각화 +4. 승인 → 버전 활성화 (active) + → effective_from 일자부터 신규 단가 적용 +5. 이전 버전 자동 종료 (archived) +``` + +#### Phase 4: 견적/수주 단가 스냅샷 + +견적/수주 생성 시점의 Depth 0~2 단가를 `calculation_inputs` JSON에 보존한다. + +```json +{ + "price_snapshot": { + "version": "v2026-Q1", + "captured_at": "2026-03-15", + "items": [ + { + "item_id": 100, + "item_name": "가이드레일", + "depth_0": { "purchase_price": 10000, "processing_cost": 2000, "loss_rate": 5 }, + "depth_2": { "sales_price": 15800, "margin_rate": 25 }, + "depth_3": { "unit_price": 15800, "quantity": 2.5, "total": 39500 } + } + ] + } +} +``` + +**효과**: 수주 완료 후 원가 변동이 발생해도, 해당 수주 시점의 원가 구조를 완전히 역추적 가능. + +### 10.3 기대 효과 + +| 항목 | 현재 | 개선 후 | +|------|------|--------| +| 원자재 인상 반영 | 수동 (1~2일) | 자동 Cascade (즉시) | +| 영향 범위 파악 | 불가능 | Impact Analysis API | +| 단가 이력 비교 | 개별 리비전만 | 버전 간 일괄 비교 | +| 견적 원가 역추적 | 불가능 | 스냅샷 보존 | +| 분기 단가 갱신 | 품목별 수동 | 버전 복사 + 일괄 변경 | + +### 10.4 구현 우선순위 + +| Phase | 내용 | 난이도 | 우선순위 | +|:-----:|------|:------:|:-------:| +| 1 | 단가 연쇄 관계 (parent_price_id) | 낮음 | 🔴 필수 | +| 2 | 변경 영향 분석 API | 중간 | 🟡 중요 | +| 3 | 단가 버전 그룹 관리 | 높음 | 🟢 권장 | +| 4 | 견적/수주 스냅샷 보존 | 낮음 | 🟡 중요 | --- -**최종 업데이트**: 2025-12-08 \ No newline at end of file +## 11. 관련 문서 + +- [품목 정책](item-policy.md) — 품목 유형 체계 (FG, PT, SM, RM, CS) +- [견적 시스템](../features/quotes/README.md) — BOM 산출, 견적 생성 흐름 +- [API 개발 규칙](../dev/standards/api-rules.md) — Service-First 패턴 +- [DB 스키마](../system/database/README.md) — 테이블 관계 + +--- + +**최종 업데이트**: 2026-03-19