feat: 한퓨어 엑셀 형식 지원 및 조제 용도 구분(usage_type) 추가
한퓨어 엑셀: - ExcelProcessor에 hanpure 형식 자동 감지 및 처리 추가 - 옵션항목에서 중량 파싱 (600g*5개 → 3000g 등) - 주문번호에서 입고일 추출, ingredient_code 직접 활용 조제 용도 구분: - compounds.usage_type 컬럼 추가 (SALE/SELF_USE/SAMPLE/DISPOSAL) - 조제 실행 시 용도 선택 드롭다운 - 조제 목록에서 용도 뱃지 클릭으로 사후 변경 가능 - 비판매 용도 시 sell_price_total=0, 매출 통계 제외 - PUT /api/compounds/:id/usage-type API 추가 - 용도 구분 설계 문서 (docs/조제_용도구분_usage_type.md) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
69be63d00d
commit
974ce5f655
83
app.py
83
app.py
@ -908,7 +908,7 @@ def create_supplier():
|
||||
|
||||
@app.route('/api/upload/purchase', methods=['POST'])
|
||||
def upload_purchase_excel():
|
||||
"""Excel 파일 업로드 및 입고 처리 (한의사랑/한의정보 형식 자동 감지)"""
|
||||
"""Excel 파일 업로드 및 입고 처리 (한의사랑/한의정보/한퓨어 형식 자동 감지)"""
|
||||
try:
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'success': False, 'error': '파일이 없습니다'}), 400
|
||||
@ -1056,7 +1056,9 @@ def upload_purchase_excel():
|
||||
WHERE herb_item_id = ?
|
||||
""", (ingredient_code, company_name, herb_item_id))
|
||||
else:
|
||||
# herb_products에 없는 경우 기존 로직
|
||||
# herb_products에 없는 경우 — Excel에서 제공한 ingredient_code 활용
|
||||
row_ingredient_code = row.get('ingredient_code') if pd.notna(row.get('ingredient_code')) else None
|
||||
|
||||
cursor.execute("""
|
||||
SELECT herb_item_id FROM herb_items
|
||||
WHERE insurance_code = ? OR herb_name = ?
|
||||
@ -1065,12 +1067,18 @@ def upload_purchase_excel():
|
||||
|
||||
if not herb:
|
||||
cursor.execute("""
|
||||
INSERT INTO herb_items (insurance_code, herb_name)
|
||||
VALUES (?, ?)
|
||||
""", (insurance_code, row['herb_name']))
|
||||
INSERT INTO herb_items (ingredient_code, insurance_code, herb_name)
|
||||
VALUES (?, ?, ?)
|
||||
""", (row_ingredient_code, insurance_code, row['herb_name']))
|
||||
herb_item_id = cursor.lastrowid
|
||||
else:
|
||||
herb_item_id = herb[0]
|
||||
if row_ingredient_code:
|
||||
cursor.execute("""
|
||||
UPDATE herb_items
|
||||
SET ingredient_code = COALESCE(ingredient_code, ?)
|
||||
WHERE herb_item_id = ?
|
||||
""", (row_ingredient_code, herb_item_id))
|
||||
else:
|
||||
# 보험코드가 없는 경우 약재명으로만 처리
|
||||
cursor.execute("""
|
||||
@ -1126,7 +1134,8 @@ def upload_purchase_excel():
|
||||
# 응답 메시지 생성
|
||||
format_name = {
|
||||
'hanisarang': '한의사랑',
|
||||
'haninfo': '한의정보'
|
||||
'haninfo': '한의정보',
|
||||
'hanpure': '한퓨어'
|
||||
}.get(summary['format_type'], '알 수 없음')
|
||||
|
||||
return jsonify({
|
||||
@ -1771,7 +1780,8 @@ def get_compounds():
|
||||
c.status,
|
||||
c.notes,
|
||||
c.created_at,
|
||||
c.created_by
|
||||
c.created_by,
|
||||
c.usage_type
|
||||
FROM compounds c
|
||||
LEFT JOIN patients p ON c.patient_id = p.patient_id
|
||||
LEFT JOIN formulas f ON c.formula_id = f.formula_id
|
||||
@ -2002,13 +2012,17 @@ def create_compound():
|
||||
|
||||
custom_summary = " | ".join(summary_parts) if summary_parts else ""
|
||||
|
||||
# 용도 구분 (SALE: 판매, SELF_USE: 자가소비, SAMPLE: 샘플, DISPOSAL: 폐기)
|
||||
usage_type = data.get('usage_type', 'SALE')
|
||||
|
||||
# 조제 마스터 생성 (커스텀 정보 포함)
|
||||
cursor.execute("""
|
||||
INSERT INTO compounds (patient_id, formula_id, compound_date,
|
||||
je_count, cheop_total, pouch_total,
|
||||
prescription_no, notes, created_by,
|
||||
is_custom, custom_summary, custom_type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
is_custom, custom_summary, custom_type,
|
||||
usage_type, sell_price_total)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
data.get('patient_id'),
|
||||
formula_id,
|
||||
@ -2021,7 +2035,9 @@ def create_compound():
|
||||
data.get('created_by', 'system'),
|
||||
1 if is_custom else 0,
|
||||
custom_summary if is_custom else None,
|
||||
'custom' if is_custom else 'standard'
|
||||
'custom' if is_custom else 'standard',
|
||||
usage_type,
|
||||
0 if usage_type != 'SALE' else data.get('sell_price_total')
|
||||
))
|
||||
compound_id = cursor.lastrowid
|
||||
|
||||
@ -3471,6 +3487,50 @@ def update_compound_status(compound_id):
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/compounds/<int:compound_id>/usage-type', methods=['PUT'])
|
||||
def update_compound_usage_type(compound_id):
|
||||
"""조제 용도 변경 (SALE, SELF_USE, SAMPLE, DISPOSAL)"""
|
||||
try:
|
||||
data = request.json
|
||||
new_usage = data.get('usage_type')
|
||||
valid_types = ['SALE', 'SELF_USE', 'SAMPLE', 'DISPOSAL']
|
||||
|
||||
if new_usage not in valid_types:
|
||||
return jsonify({'error': f'유효하지 않은 용도입니다: {new_usage}'}), 400
|
||||
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT usage_type, status FROM compounds WHERE compound_id = ?", (compound_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return jsonify({'error': '조제를 찾을 수 없습니다'}), 404
|
||||
|
||||
old_usage = row['usage_type'] or 'SALE'
|
||||
|
||||
# 용도 변경
|
||||
cursor.execute("""
|
||||
UPDATE compounds
|
||||
SET usage_type = ?,
|
||||
sell_price_total = CASE WHEN ? != 'SALE' THEN 0 ELSE sell_price_total END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE compound_id = ?
|
||||
""", (new_usage, new_usage, compound_id))
|
||||
|
||||
# 이력 기록
|
||||
cursor.execute("""
|
||||
INSERT INTO sales_status_history (compound_id, old_status, new_status, changed_by, change_reason)
|
||||
VALUES (?, ?, ?, 'system', ?)
|
||||
""", (compound_id, f'usage:{old_usage}', f'usage:{new_usage}',
|
||||
f'용도 변경: {old_usage} → {new_usage}'))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': f'용도가 변경되었습니다: {new_usage}'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/compounds/<int:compound_id>/price', methods=['PUT'])
|
||||
def update_compound_price(compound_id):
|
||||
"""조제 가격 조정 (복합결제 지원)"""
|
||||
@ -3591,6 +3651,7 @@ def get_sales_statistics():
|
||||
COUNT(CASE WHEN status = 'REFUNDED' THEN 1 END) as refunded_count
|
||||
FROM compounds
|
||||
WHERE status IN ('PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED')
|
||||
AND COALESCE(usage_type, 'SALE') = 'SALE'
|
||||
"""
|
||||
|
||||
params = []
|
||||
@ -3612,6 +3673,7 @@ def get_sales_statistics():
|
||||
SUM(COALESCE(actual_payment_amount, sell_price_total)) as daily_total
|
||||
FROM compounds
|
||||
WHERE status IN ('PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED')
|
||||
AND COALESCE(usage_type, 'SALE') = 'SALE'
|
||||
"""
|
||||
|
||||
if start_date:
|
||||
@ -3639,6 +3701,7 @@ def get_sales_statistics():
|
||||
FROM compounds c
|
||||
LEFT JOIN formulas f ON c.formula_id = f.formula_id
|
||||
WHERE c.status IN ('PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED')
|
||||
AND COALESCE(c.usage_type, 'SALE') = 'SALE'
|
||||
"""
|
||||
|
||||
if start_date:
|
||||
|
||||
122
docs/조제_용도구분_usage_type.md
Normal file
122
docs/조제_용도구분_usage_type.md
Normal file
@ -0,0 +1,122 @@
|
||||
# 조제 용도 구분 (usage_type)
|
||||
|
||||
> 조제된 약은 반드시 판매 목적이 아닐 수 있다.
|
||||
> 자가소비, 샘플 제공, 폐기 등 **재고는 차감되지만 매출이 아닌 경우**를 구분한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 기존 status와의 차이
|
||||
|
||||
| 구분 | `status` | `usage_type` |
|
||||
|------|----------|-------------|
|
||||
| 역할 | 판매 진행 **흐름 상태** | 조제의 **용도 분류** |
|
||||
| 변화 | 시간에 따라 전이 (PREPARED → PAID → COMPLETED) | 조제 시점에 결정, 사후 변경 가능 |
|
||||
| 예시 | "이 조제는 결제 완료 상태" | "이 조제는 자가소비 용도" |
|
||||
|
||||
**핵심**: `status`와 `usage_type`은 서로 다른 차원. 자가소비도 `PREPARED → COMPLETED` 상태 흐름을 탈 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## 2. usage_type 값 정의
|
||||
|
||||
| 값 | 한글명 | 설명 | 매출 포함 | 판매가 |
|
||||
|----|--------|------|:---------:|--------|
|
||||
| `SALE` | 판매 | 환자에게 판매 (기본값) | O | 설정 가격 |
|
||||
| `SELF_USE` | 자가소비 | 약국 자체 사용 | X | 0원 |
|
||||
| `SAMPLE` | 샘플 | 시음/샘플 제공 | X | 0원 |
|
||||
| `DISPOSAL` | 폐기 | 유통기한 초과 등 폐기 처리 | X | 0원 |
|
||||
|
||||
---
|
||||
|
||||
## 3. DB 스키마
|
||||
|
||||
```sql
|
||||
-- compounds 테이블에 추가된 컬럼
|
||||
ALTER TABLE compounds ADD COLUMN usage_type TEXT DEFAULT 'SALE';
|
||||
```
|
||||
|
||||
- 기본값 `'SALE'` — 기존 데이터 호환
|
||||
- `COALESCE(usage_type, 'SALE')` 패턴으로 NULL 안전 처리
|
||||
|
||||
---
|
||||
|
||||
## 4. 동작 규칙
|
||||
|
||||
### 4-1. 조제 생성 시
|
||||
- 조제 실행 폼에서 **용도** 드롭다운 선택 (기본: 판매)
|
||||
- `SALE`이 아닌 용도 → `sell_price_total = 0` 자동 설정
|
||||
- 재고 차감은 용도에 관계없이 **항상 발생**
|
||||
|
||||
### 4-2. 사후 변경
|
||||
- 조제 목록의 **용도 뱃지 클릭** → 번호 입력으로 변경
|
||||
- API: `PUT /api/compounds/:id/usage-type`
|
||||
- 판매 → 자가소비 변경 시 `sell_price_total = 0`으로 자동 변경
|
||||
- 변경 이력이 `sales_status_history`에 기록됨
|
||||
|
||||
### 4-3. 매출 통계 제외
|
||||
- 대시보드 월매출: `usage_type = 'SALE'`만 집계
|
||||
- 판매 통계 API: `COALESCE(usage_type, 'SALE') = 'SALE'` 조건
|
||||
- 자가소비/샘플/폐기는 매출에서 완전 제외
|
||||
|
||||
### 4-4. UI 표시
|
||||
|
||||
| 용도 | 뱃지 색상 | 판매 버튼 | 판매가 표시 |
|
||||
|------|----------|:---------:|:----------:|
|
||||
| 판매 | 초록 | O | 금액 표시 |
|
||||
| 자가소비 | 노랑 | X | `-` |
|
||||
| 샘플 | 파랑 | X | `-` |
|
||||
| 폐기 | 회색 | X | `-` |
|
||||
|
||||
---
|
||||
|
||||
## 5. API 명세
|
||||
|
||||
### 용도 변경
|
||||
```
|
||||
PUT /api/compounds/:compound_id/usage-type
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"usage_type": "SELF_USE" // SALE, SELF_USE, SAMPLE, DISPOSAL
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true,
|
||||
"message": "용도가 변경되었습니다: SELF_USE"
|
||||
}
|
||||
```
|
||||
|
||||
### 조제 생성 시 용도 지정
|
||||
```
|
||||
POST /api/compounds
|
||||
{
|
||||
"patient_id": 1,
|
||||
"formula_id": 5,
|
||||
"je_count": 1,
|
||||
"cheop_total": 30,
|
||||
"pouch_total": 30,
|
||||
"usage_type": "SELF_USE", // 생략 시 기본 SALE
|
||||
"ingredients": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 활용 시나리오
|
||||
|
||||
### 자가소비 조제
|
||||
1. 약사가 본인/가족용으로 쌍화탕 1제 조제
|
||||
2. 조제 실행 시 용도 → "자가소비" 선택
|
||||
3. 재고 차감됨, 판매가 = 0, 매출 통계 제외
|
||||
4. 원가만 기록 → 비용 관리 목적
|
||||
|
||||
### 기존 조제를 자가소비로 변경
|
||||
1. 실수로 판매로 조제했는데 실제로는 자가소비
|
||||
2. 조제 내역 목록에서 "판매" 뱃지 클릭
|
||||
3. "2" (자가소비) 입력
|
||||
4. 즉시 변경 → 매출에서 제외
|
||||
|
||||
---
|
||||
|
||||
*최종 수정: 2026-02-19*
|
||||
@ -36,6 +36,18 @@ class ExcelProcessor:
|
||||
'비고': 'notes'
|
||||
}
|
||||
|
||||
# 한퓨어 형식 컬럼 매핑
|
||||
HANPURE_MAPPING = {
|
||||
'상품명': 'herb_name',
|
||||
'제조사코드': 'insurance_code',
|
||||
'주성분코드': 'ingredient_code',
|
||||
'제조사명': 'supplier_name',
|
||||
'원산지': 'origin_country',
|
||||
'소계': 'total_amount',
|
||||
'옵션항목': 'option_detail',
|
||||
'주문번호': 'order_number',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.format_type = None
|
||||
self.df_original = None
|
||||
@ -45,6 +57,11 @@ class ExcelProcessor:
|
||||
"""Excel 형식 자동 감지"""
|
||||
columns = df.columns.tolist()
|
||||
|
||||
# 한퓨어 형식 체크 (주문번호, 주성분코드, 제조사코드, 옵션항목이 특징)
|
||||
hanpure_cols = ['주문번호', '주성분코드', '제조사코드', '상품명', '옵션항목']
|
||||
if all(col in columns for col in hanpure_cols):
|
||||
return 'hanpure'
|
||||
|
||||
# 한의사랑 형식 체크
|
||||
hanisarang_cols = ['품목명', '제품코드', '일그램당단가', '총구입량', '총구입단가']
|
||||
if all(col in columns for col in hanisarang_cols):
|
||||
@ -64,8 +81,10 @@ class ExcelProcessor:
|
||||
def read_excel(self, file_path):
|
||||
"""Excel 파일 읽기"""
|
||||
try:
|
||||
# 제품코드를 문자열로 읽기 위한 dtype 설정
|
||||
self.df_original = pd.read_excel(file_path, dtype={'제품코드': str})
|
||||
# 코드 컬럼을 문자열로 읽기 위한 dtype 설정
|
||||
self.df_original = pd.read_excel(file_path, dtype={
|
||||
'제품코드': str, '제조사코드': str, '주성분코드': str, '대표코드': str
|
||||
})
|
||||
self.format_type = self.detect_format(self.df_original)
|
||||
return True
|
||||
except Exception as e:
|
||||
@ -137,12 +156,96 @@ class ExcelProcessor:
|
||||
self.df_processed = df_mapped
|
||||
return df_mapped
|
||||
|
||||
@staticmethod
|
||||
def parse_option_quantity_g(option_text):
|
||||
"""옵션항목에서 총 중량(g) 파싱
|
||||
예: '인삼 특A (4~5년근) 600g*5개' → 3000
|
||||
'감초 1kg' → 1000
|
||||
'복령 500g' → 500
|
||||
'백출 300g*3개' → 900
|
||||
"""
|
||||
if not option_text or pd.isna(option_text):
|
||||
return None
|
||||
|
||||
text = str(option_text)
|
||||
|
||||
# 패턴1: NNNg*N개 또는 NNNg×N개
|
||||
m = re.search(r'(\d+(?:\.\d+)?)\s*g\s*[*×x]\s*(\d+)', text, re.IGNORECASE)
|
||||
if m:
|
||||
return float(m.group(1)) * int(m.group(2))
|
||||
|
||||
# 패턴2: N kg*N개
|
||||
m = re.search(r'(\d+(?:\.\d+)?)\s*kg\s*[*×x]\s*(\d+)', text, re.IGNORECASE)
|
||||
if m:
|
||||
return float(m.group(1)) * 1000 * int(m.group(2))
|
||||
|
||||
# 패턴3: NNNg (단독)
|
||||
m = re.search(r'(\d+(?:\.\d+)?)\s*g(?!\w)', text, re.IGNORECASE)
|
||||
if m:
|
||||
return float(m.group(1))
|
||||
|
||||
# 패턴4: Nkg (단독)
|
||||
m = re.search(r'(\d+(?:\.\d+)?)\s*kg(?!\w)', text, re.IGNORECASE)
|
||||
if m:
|
||||
return float(m.group(1)) * 1000
|
||||
|
||||
return None
|
||||
|
||||
def process_hanpure(self):
|
||||
"""한퓨어 형식 처리"""
|
||||
df = self.df_original.copy()
|
||||
|
||||
df_mapped = pd.DataFrame()
|
||||
|
||||
for old_col, new_col in self.HANPURE_MAPPING.items():
|
||||
if old_col in df.columns:
|
||||
df_mapped[new_col] = df[old_col]
|
||||
|
||||
# 보험코드 9자리 패딩 처리
|
||||
if 'insurance_code' in df_mapped.columns:
|
||||
df_mapped['insurance_code'] = df_mapped['insurance_code'].apply(
|
||||
lambda x: str(x).zfill(9) if pd.notna(x) and str(x).strip().isdigit() else str(x).strip() if pd.notna(x) else None
|
||||
)
|
||||
|
||||
# 주문번호에서 날짜 추출 (20260211-22511888 → 20260211)
|
||||
if 'order_number' in df_mapped.columns:
|
||||
df_mapped['receipt_date'] = df_mapped['order_number'].apply(
|
||||
lambda x: str(x).split('-')[0] if pd.notna(x) else None
|
||||
)
|
||||
|
||||
# 옵션항목에서 중량(g) 파싱
|
||||
if 'option_detail' in df_mapped.columns:
|
||||
df_mapped['quantity'] = df_mapped['option_detail'].apply(self.parse_option_quantity_g)
|
||||
|
||||
# 업체명 기본값
|
||||
if 'supplier_name' not in df_mapped.columns or df_mapped['supplier_name'].isnull().all():
|
||||
df_mapped['supplier_name'] = '한퓨어'
|
||||
|
||||
# 단가 계산 (소계 / 중량g)
|
||||
if 'total_amount' in df_mapped.columns and 'quantity' in df_mapped.columns:
|
||||
df_mapped['unit_price'] = df_mapped.apply(
|
||||
lambda row: round(row['total_amount'] / row['quantity'], 2)
|
||||
if pd.notna(row.get('quantity')) and row.get('quantity', 0) > 0
|
||||
else None, axis=1
|
||||
)
|
||||
|
||||
# 비고에 옵션항목 원문 저장
|
||||
df_mapped['notes'] = df_mapped.get('option_detail', '')
|
||||
|
||||
# 임시 컬럼 제거
|
||||
df_mapped.drop(columns=['order_number', 'option_detail'], errors='ignore', inplace=True)
|
||||
|
||||
self.df_processed = df_mapped
|
||||
return df_mapped
|
||||
|
||||
def process(self):
|
||||
"""형식에 따라 자동 처리"""
|
||||
if self.format_type == 'hanisarang':
|
||||
return self.process_hanisarang()
|
||||
elif self.format_type == 'haninfo':
|
||||
return self.process_haninfo()
|
||||
elif self.format_type == 'hanpure':
|
||||
return self.process_hanpure()
|
||||
else:
|
||||
raise ValueError(f"지원하지 않는 형식: {self.format_type}")
|
||||
|
||||
@ -221,7 +324,8 @@ class ExcelProcessor:
|
||||
standard_columns = [
|
||||
'insurance_code', 'supplier_name', 'herb_name',
|
||||
'receipt_date', 'quantity', 'total_amount',
|
||||
'unit_price', 'origin_country', 'notes'
|
||||
'unit_price', 'origin_country', 'notes',
|
||||
'ingredient_code'
|
||||
]
|
||||
|
||||
# 있는 컬럼만 선택
|
||||
|
||||
@ -141,10 +141,11 @@ $(document).ready(function() {
|
||||
const todayCompounds = response.data.filter(c => c.compound_date === today);
|
||||
$('#todayCompounds').text(todayCompounds.length);
|
||||
|
||||
// 이번달 매출/마진 계산
|
||||
// 이번달 매출/마진 계산 (자가소비/샘플/폐기 제외)
|
||||
const monthData = response.data.filter(c =>
|
||||
c.compound_date && c.compound_date.startsWith(currentMonth) &&
|
||||
['PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED'].includes(c.status)
|
||||
['PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED'].includes(c.status) &&
|
||||
(!c.usage_type || c.usage_type === 'SALE')
|
||||
);
|
||||
const monthSales = monthData.reduce((sum, c) => sum + (c.actual_payment_amount || c.sell_price_total || 0), 0);
|
||||
const monthCost = monthData.reduce((sum, c) => sum + (c.cost_total || 0), 0);
|
||||
@ -1627,6 +1628,7 @@ $(document).ready(function() {
|
||||
je_count: parseFloat($('#jeCount').val()),
|
||||
cheop_total: parseFloat($('#cheopTotal').val()),
|
||||
pouch_total: parseFloat($('#pouchTotal').val()),
|
||||
usage_type: $('#compoundUsageType').val() || 'SALE',
|
||||
ingredients: ingredients
|
||||
};
|
||||
|
||||
@ -1637,8 +1639,11 @@ $(document).ready(function() {
|
||||
data: JSON.stringify(compoundData),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert(`조제가 완료되었습니다.\n원가: ${formatCurrency(response.total_cost)}`);
|
||||
const usageType = $('#compoundUsageType').val();
|
||||
const usageLabel = {SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'}[usageType] || '판매';
|
||||
alert(`조제가 완료되었습니다. [${usageLabel}]\n원가: ${formatCurrency(response.total_cost)}`);
|
||||
$('#compoundForm').hide();
|
||||
$('#compoundUsageType').val('SALE');
|
||||
loadCompounds();
|
||||
}
|
||||
},
|
||||
@ -1701,6 +1706,13 @@ $(document).ready(function() {
|
||||
statusBadge = '<span class="badge bg-secondary">대기</span>';
|
||||
}
|
||||
|
||||
// 용도 뱃지 (클릭으로 변경 가능)
|
||||
const usageLabels = {SALE: '판매', SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'};
|
||||
const usageColors = {SALE: 'success', SELF_USE: 'warning text-dark', SAMPLE: 'info', DISPOSAL: 'secondary'};
|
||||
const curUsage = compound.usage_type || 'SALE';
|
||||
const usageBadge = `<span class="badge bg-${usageColors[curUsage]} change-usage" style="cursor:pointer" data-id="${compound.compound_id}" data-current="${curUsage}" title="클릭하여 용도 변경">${usageLabels[curUsage]}</span>`;
|
||||
const isSale = curUsage === 'SALE';
|
||||
|
||||
const row = $(`
|
||||
<tr>
|
||||
<td>${response.data.length - index}</td>
|
||||
@ -1712,14 +1724,14 @@ $(document).ready(function() {
|
||||
<td>${compound.cheop_total || 0}</td>
|
||||
<td>${compound.pouch_total || 0}</td>
|
||||
<td>${formatCurrency(compound.cost_total || 0)}</td>
|
||||
<td>${formatCurrency(compound.sell_price_total || 0)}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${isSale ? formatCurrency(compound.sell_price_total || 0) : '-'}</td>
|
||||
<td>${usageBadge} ${statusBadge}</td>
|
||||
<td>${compound.prescription_no || '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-info view-compound-detail" data-id="${compound.compound_id}">
|
||||
<i class="bi bi-eye"></i> 상세
|
||||
</button>
|
||||
${compound.status === 'PREPARED' ? `
|
||||
${compound.status === 'PREPARED' && isSale ? `
|
||||
<button class="btn btn-sm btn-outline-success process-sale" data-id="${compound.compound_id}"
|
||||
data-formula="${compound.formula_name || '직접조제'}"
|
||||
data-patient="${compound.patient_name || '직접조제'}"
|
||||
@ -1728,6 +1740,8 @@ $(document).ready(function() {
|
||||
data-price="${compound.sell_price_total || 0}">
|
||||
<i class="bi bi-cash-coin"></i> 판매
|
||||
</button>
|
||||
` : ''}
|
||||
${compound.status === 'PREPARED' ? `
|
||||
<button class="btn btn-sm btn-outline-danger cancel-compound" data-id="${compound.compound_id}">
|
||||
<i class="bi bi-x-circle"></i> 취소
|
||||
</button>
|
||||
@ -1795,6 +1809,40 @@ $(document).ready(function() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 용도 변경 뱃지 클릭 이벤트
|
||||
$('.change-usage').on('click', function() {
|
||||
const compoundId = $(this).data('id');
|
||||
const current = $(this).data('current');
|
||||
const options = {SALE: '판매', SELF_USE: '자가소비', SAMPLE: '샘플', DISPOSAL: '폐기'};
|
||||
const choices = Object.entries(options)
|
||||
.map(([k, v]) => `${k === current ? '● ' : ' '}${v}`)
|
||||
.join('\n');
|
||||
const input = prompt(`용도를 선택하세요 (현재: ${options[current]})\n\n1: 판매\n2: 자가소비\n3: 샘플\n4: 폐기`, current === 'SALE' ? '1' : current === 'SELF_USE' ? '2' : current === 'SAMPLE' ? '3' : '4');
|
||||
if (!input) return;
|
||||
const typeMap = {'1': 'SALE', '2': 'SELF_USE', '3': 'SAMPLE', '4': 'DISPOSAL'};
|
||||
const newType = typeMap[input.trim()];
|
||||
if (!newType) { alert('잘못된 입력입니다.'); return; }
|
||||
if (newType === current) return;
|
||||
|
||||
$.ajax({
|
||||
url: `/api/compounds/${compoundId}/usage-type`,
|
||||
method: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ usage_type: newType }),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
loadCompounds();
|
||||
loadDashboard();
|
||||
} else {
|
||||
alert(response.error || '변경 실패');
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
alert(xhr.responseJSON?.error || '변경 실패');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
tbody.html('<tr><td colspan="13" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
|
||||
$('#todayCompoundCount').text(0);
|
||||
|
||||
@ -443,18 +443,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">제수</label>
|
||||
<input type="number" class="form-control" id="jeCount" value="1" min="0.5" step="0.5" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">총 첩수</label>
|
||||
<input type="number" class="form-control" id="cheopTotal" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">총 파우치수</label>
|
||||
<input type="number" class="form-control" id="pouchTotal" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">용도</label>
|
||||
<select class="form-control" id="compoundUsageType">
|
||||
<option value="SALE">판매</option>
|
||||
<option value="SELF_USE">자가소비</option>
|
||||
<option value="SAMPLE">샘플</option>
|
||||
<option value="DISPOSAL">폐기</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h6>약재 구성 (가감 가능)</h6>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user