Compare commits
No commits in common. "2a14af59c322a3447b10037f55c9531d7fc1cea2" and "40be340a63f7fff87e9823b212084c0324abd60a" have entirely different histories.
2a14af59c3
...
40be340a63
@ -1,48 +0,0 @@
|
|||||||
# Flask 프로세스 관리 가이드
|
|
||||||
|
|
||||||
## 🚨 문제 상황
|
|
||||||
- Flask Debug 모드로 실행 시 코드 변경 시마다 자동 재시작
|
|
||||||
- 여러 개의 백그라운드 프로세스가 누적되어 실행됨
|
|
||||||
- 동일한 포트(5001)를 여러 프로세스가 점유하려고 시도
|
|
||||||
|
|
||||||
## ✅ 해결 방법
|
|
||||||
|
|
||||||
### 1. 즉시 해결 명령어
|
|
||||||
```bash
|
|
||||||
# 한 줄로 모든 Flask 프로세스 종료 및 재시작
|
|
||||||
lsof -ti:5001 | xargs -r kill -9 && sleep 2 && source venv/bin/activate && python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. run_server.sh 스크립트 사용 (이미 생성됨)
|
|
||||||
```bash
|
|
||||||
./run_server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 근본적 해결책
|
|
||||||
```python
|
|
||||||
# app.py 수정 - Debug 모드 끄기
|
|
||||||
app.run(debug=False, host='0.0.0.0', port=5001)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 프로세스 확인 명령어
|
|
||||||
```bash
|
|
||||||
# 포트 5001 사용 중인 프로세스 확인
|
|
||||||
lsof -i:5001
|
|
||||||
|
|
||||||
# 모든 Python 프로세스 확인
|
|
||||||
ps aux | grep python
|
|
||||||
|
|
||||||
# 특정 포트 프로세스만 종료
|
|
||||||
lsof -ti:5001 | xargs -r kill -9
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ 주의사항
|
|
||||||
- Debug 모드는 개발 중에만 사용
|
|
||||||
- Production에서는 반드시 debug=False로 설정
|
|
||||||
- 코드 변경이 빈번할 때는 수동으로 재시작 권장
|
|
||||||
|
|
||||||
## 🔄 자동화 스크립트
|
|
||||||
run_server.sh가 이미 생성되어 있음:
|
|
||||||
- 기존 프로세스 자동 종료
|
|
||||||
- 새 프로세스 시작
|
|
||||||
- 단일 프로세스만 실행 보장
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
# Git 사용 가이드라인
|
|
||||||
|
|
||||||
## ⚠️ 중요한 주의사항
|
|
||||||
|
|
||||||
### 1. Git 초기화 금지
|
|
||||||
- **절대로 `git init`를 실행하지 말 것**
|
|
||||||
- 이 프로젝트는 이미 Gitea 서버에 연결되어 있음
|
|
||||||
- 원격 저장소: origin (Gitea 서버)
|
|
||||||
|
|
||||||
### 2. 커밋 전 확인 사항
|
|
||||||
- 항상 `git status`로 현재 상태 확인
|
|
||||||
- `git log --oneline -n 5`로 최근 커밋 이력 확인
|
|
||||||
- `git remote -v`로 원격 저장소 확인
|
|
||||||
|
|
||||||
### 3. 커밋 시 규칙
|
|
||||||
```bash
|
|
||||||
# 상태 확인
|
|
||||||
git status
|
|
||||||
|
|
||||||
# 변경사항 확인
|
|
||||||
git diff
|
|
||||||
|
|
||||||
# 논리적 단위로 나누어서 커밋
|
|
||||||
git add [파일명]
|
|
||||||
git commit -m "커밋 메시지"
|
|
||||||
|
|
||||||
# 원격 저장소에 푸시
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 커밋 메시지 작성 규칙
|
|
||||||
- **기능 추가**: `feat: 효능 태그 시스템 추가`
|
|
||||||
- **버그 수정**: `fix: 총금액 표시 오류 수정`
|
|
||||||
- **문서 작성**: `docs: README 업데이트`
|
|
||||||
- **리팩토링**: `refactor: 코드 구조 개선`
|
|
||||||
- **스타일**: `style: 코드 포맷팅`
|
|
||||||
- **테스트**: `test: 단위 테스트 추가`
|
|
||||||
- **기타**: `chore: 빌드 스크립트 수정`
|
|
||||||
|
|
||||||
### 5. 파일 관리
|
|
||||||
- 테스트 파일들은 `.gitignore`에 추가 고려
|
|
||||||
- 업로드 폴더는 커밋하지 않기 (uploads/)
|
|
||||||
- 데이터베이스 파일은 주의해서 관리
|
|
||||||
|
|
||||||
### 6. 브랜치 전략
|
|
||||||
- 현재 main 브랜치 사용 중
|
|
||||||
- 큰 기능은 별도 브랜치 생성 고려
|
|
||||||
```bash
|
|
||||||
# 브랜치 생성 및 체크아웃
|
|
||||||
git checkout -b feature/기능명
|
|
||||||
|
|
||||||
# 작업 후 main으로 머지
|
|
||||||
git checkout main
|
|
||||||
git merge feature/기능명
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 실수 방지
|
|
||||||
- `git push --force`는 절대 사용 금지
|
|
||||||
- `git reset --hard`는 신중하게 사용
|
|
||||||
- 작업 전 `git pull` 실행 습관화
|
|
||||||
|
|
||||||
## 현재 프로젝트 상태
|
|
||||||
- Repository: kdrug (한약재 재고관리 시스템)
|
|
||||||
- Branch: main
|
|
||||||
- Remote: origin (Gitea 서버)
|
|
||||||
|
|
||||||
## 체크리스트
|
|
||||||
- [ ] git status 확인
|
|
||||||
- [ ] 논리적 단위로 파일 그룹화
|
|
||||||
- [ ] 의미있는 커밋 메시지 작성
|
|
||||||
- [ ] 불필요한 파일 제외 확인
|
|
||||||
- [ ] push 전 최종 검토
|
|
||||||
|
|
||||||
---
|
|
||||||
작성일: 2026-02-15
|
|
||||||
작성자: Claude Assistant
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
# 원산지별 재고 관리 및 조제 시 선택 기능
|
|
||||||
|
|
||||||
## 🎯 완료된 기능 (2026-02-15)
|
|
||||||
|
|
||||||
### 1. 원산지별 재고 구분 표시
|
|
||||||
- **재고 현황 API 개선** (`/api/inventory/summary`)
|
|
||||||
- `origin_count`: 원산지 개수
|
|
||||||
- `min_price`, `max_price`: 가격 범위
|
|
||||||
- 평균 가격 표시
|
|
||||||
|
|
||||||
### 2. 재고 상세 API 추가 (`/api/inventory/detail/<herb_id>`)
|
|
||||||
- 원산지별로 그룹화된 재고 정보
|
|
||||||
- 각 원산지별 로트 목록
|
|
||||||
- 평균 단가, 재고 수량, 재고 가치
|
|
||||||
|
|
||||||
### 3. UI 개선
|
|
||||||
- 재고 목록에서 원산지 개수 배지 표시
|
|
||||||
- 가격 범위 표시 (여러 원산지일 경우)
|
|
||||||
- 클릭 시 상세 모달 표시
|
|
||||||
- 원산지별 재고 카드형 UI
|
|
||||||
|
|
||||||
## 📌 실제 사례: 건강(乾薑) 약재
|
|
||||||
```
|
|
||||||
페루산: 5,000g @ 12.4원/g (총 62,000원)
|
|
||||||
한국산: 1,500g @ 51.4원/g (총 77,100원)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 조제 시 원산지 선택 기능 (설계)
|
|
||||||
|
|
||||||
### 1. 데이터베이스 스키마 변경
|
|
||||||
```sql
|
|
||||||
-- compound_details 테이블에 로트 정보 추가
|
|
||||||
ALTER TABLE compound_details ADD COLUMN lot_id INTEGER;
|
|
||||||
ALTER TABLE compound_details ADD FOREIGN KEY (lot_id) REFERENCES inventory_lots(lot_id);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 조제 프로세스 개선
|
|
||||||
1. **처방 선택 후 약재 목록 표시**
|
|
||||||
- 각 약재에 원산지 선택 옵션 제공
|
|
||||||
- 기본값: 가장 저렴한 원산지
|
|
||||||
- 선택 가능: 고품질(비싼) 원산지
|
|
||||||
|
|
||||||
2. **원산지 선택 UI**
|
|
||||||
```javascript
|
|
||||||
// 예시: 건강 30g 조제 시
|
|
||||||
{
|
|
||||||
herb: "건강",
|
|
||||||
required: 30,
|
|
||||||
options: [
|
|
||||||
{origin: "페루", price: 12.4, available: 5000},
|
|
||||||
{origin: "한국", price: 51.4, available: 1500}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **가격 차이 실시간 표시**
|
|
||||||
- 기본 선택: 페루산 30g = 372원
|
|
||||||
- 프리미엄 선택: 한국산 30g = 1,542원
|
|
||||||
- 차이: +1,170원
|
|
||||||
|
|
||||||
### 3. API 엔드포인트 추가
|
|
||||||
- `GET /api/herbs/<id>/lots` - 약재별 가용 로트 목록
|
|
||||||
- `POST /api/compounds/calculate` - 원산지별 가격 계산
|
|
||||||
- `POST /api/compounds/dispense` - 원산지 지정 조제
|
|
||||||
|
|
||||||
### 4. FIFO 변형 전략
|
|
||||||
- **기본**: 저렴한 원산지 우선 (Cost-Optimized FIFO)
|
|
||||||
- **선택**: 특정 원산지 지정 가능
|
|
||||||
- **혼합**: 일부는 저렴, 일부는 고품질
|
|
||||||
|
|
||||||
## 💡 비즈니스 가치
|
|
||||||
1. **환자 선택권**: 가격 vs 품질 선택 가능
|
|
||||||
2. **투명성**: 원산지별 가격 명시
|
|
||||||
3. **재고 관리**: 원산지별 재고 추적
|
|
||||||
4. **수익성**: 프리미엄 옵션 제공
|
|
||||||
|
|
||||||
## 🚀 구현 우선순위
|
|
||||||
1. ✅ 원산지별 재고 표시 (완료)
|
|
||||||
2. ✅ 재고 상세 모달 (완료)
|
|
||||||
3. ⏳ 조제 시 원산지 선택 UI
|
|
||||||
4. ⏳ 원산지별 가격 계산
|
|
||||||
5. ⏳ 조제 이력에 원산지 기록
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
누락된 약재 추가 스크립트
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
def add_missing_herbs():
|
|
||||||
conn = sqlite3.connect('database/kdrug.db')
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 누락된 약재 추가
|
|
||||||
missing_herbs = [
|
|
||||||
('A001100', '당귀', '보혈'), # 보혈약
|
|
||||||
('A001200', '백작약', '보혈'), # 보혈약
|
|
||||||
('A001300', '인삼', '대보원기'), # 대보원기약
|
|
||||||
('A001400', '생강', '온중'), # 온중약
|
|
||||||
]
|
|
||||||
|
|
||||||
print("=== 누락된 약재 추가 ===")
|
|
||||||
for code, name, efficacy in missing_herbs:
|
|
||||||
# 약재가 이미 있는지 확인
|
|
||||||
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (name,))
|
|
||||||
if cursor.fetchone():
|
|
||||||
print(f" ⚠️ {name} - 이미 존재함")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 약재 추가
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO herb_items (insurance_code, herb_name, specification, default_unit, is_active)
|
|
||||||
VALUES (?, ?, ?, 'g', 1)
|
|
||||||
""", (code, name, ''))
|
|
||||||
|
|
||||||
herb_id = cursor.lastrowid
|
|
||||||
print(f" ✅ {name} (ID: {herb_id}) 추가 완료")
|
|
||||||
|
|
||||||
# 효능 태그 연결
|
|
||||||
cursor.execute("SELECT tag_id FROM herb_efficacy_tags WHERE tag_name = ?", (efficacy,))
|
|
||||||
tag_result = cursor.fetchone()
|
|
||||||
|
|
||||||
if tag_result:
|
|
||||||
tag_id = tag_result[0]
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT OR IGNORE INTO herb_item_tags (herb_item_id, tag_id)
|
|
||||||
VALUES (?, ?)
|
|
||||||
""", (herb_id, tag_id))
|
|
||||||
print(f" → 효능 '{efficacy}' 연결")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# 쌍화탕 처방 재구성 시도
|
|
||||||
print("\n=== 쌍화탕 처방 재구성 ===")
|
|
||||||
|
|
||||||
# 쌍화탕 처방 ID 가져오기
|
|
||||||
cursor.execute("SELECT formula_id FROM formulas WHERE formula_name = '쌍화탕'")
|
|
||||||
formula_result = cursor.fetchone()
|
|
||||||
|
|
||||||
if formula_result:
|
|
||||||
formula_id = formula_result[0]
|
|
||||||
|
|
||||||
# 현재 없는 약재들 다시 추가 시도
|
|
||||||
missing_ingredients = [
|
|
||||||
('당귀', 6.0, '보혈'),
|
|
||||||
('백작약', 6.0, '보혈'),
|
|
||||||
('인삼', 4.0, '대보원기'),
|
|
||||||
('생강', 5.0, '온중'),
|
|
||||||
]
|
|
||||||
|
|
||||||
for herb_name, grams, notes in missing_ingredients:
|
|
||||||
# 이미 등록되었는지 확인
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT fi.ingredient_id
|
|
||||||
FROM formula_ingredients fi
|
|
||||||
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
|
|
||||||
WHERE fi.formula_id = ? AND h.herb_name = ?
|
|
||||||
""", (formula_id, herb_name))
|
|
||||||
|
|
||||||
if cursor.fetchone():
|
|
||||||
print(f" ⚠️ {herb_name} - 이미 처방에 포함됨")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 약재 ID 찾기
|
|
||||||
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (herb_name,))
|
|
||||||
herb_result = cursor.fetchone()
|
|
||||||
|
|
||||||
if herb_result:
|
|
||||||
herb_id = herb_result[0]
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO formula_ingredients (
|
|
||||||
formula_id, herb_item_id, grams_per_cheop, notes
|
|
||||||
) VALUES (?, ?, ?, ?)
|
|
||||||
""", (formula_id, herb_id, grams, notes))
|
|
||||||
print(f" ✅ {herb_name}: {grams}g ({notes}) 추가")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# 최종 쌍화탕 구성 확인
|
|
||||||
print("\n=== 최종 쌍화탕 구성 확인 ===")
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
h.herb_name,
|
|
||||||
fi.grams_per_cheop,
|
|
||||||
fi.notes,
|
|
||||||
GROUP_CONCAT(et.tag_name) as efficacy_tags
|
|
||||||
FROM formula_ingredients fi
|
|
||||||
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
|
|
||||||
LEFT JOIN herb_item_tags hit ON h.herb_item_id = hit.herb_item_id
|
|
||||||
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
|
|
||||||
WHERE fi.formula_id = (SELECT formula_id FROM formulas WHERE formula_name = '쌍화탕')
|
|
||||||
GROUP BY h.herb_name, fi.grams_per_cheop, fi.notes
|
|
||||||
ORDER BY fi.grams_per_cheop DESC
|
|
||||||
""")
|
|
||||||
|
|
||||||
total_grams = 0
|
|
||||||
for herb_name, grams, notes, tags in cursor.fetchall():
|
|
||||||
total_grams += grams
|
|
||||||
tags_display = f" [{tags}]" if tags else ""
|
|
||||||
print(f" - {herb_name}: {grams}g ({notes}){tags_display}")
|
|
||||||
|
|
||||||
print(f"\n 📊 총 {cursor.rowcount}종 약재")
|
|
||||||
print(f" 💊 1첩 총량: {total_grams}g")
|
|
||||||
print(f" 📦 20첩 총량: {total_grams * 20}g")
|
|
||||||
|
|
||||||
print("\n✅ 약재 추가 및 처방 재구성 완료!")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 오류 발생: {e}")
|
|
||||||
conn.rollback()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
add_missing_herbs()
|
|
||||||
320
app.py
320
app.py
@ -125,36 +125,18 @@ def create_patient():
|
|||||||
|
|
||||||
@app.route('/api/herbs', methods=['GET'])
|
@app.route('/api/herbs', methods=['GET'])
|
||||||
def get_herbs():
|
def get_herbs():
|
||||||
"""약재 목록 조회 (효능 태그 포함)"""
|
"""약재 목록 조회"""
|
||||||
try:
|
try:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT h.*, COALESCE(s.total_quantity, 0) as current_stock
|
||||||
h.herb_item_id,
|
|
||||||
h.insurance_code,
|
|
||||||
h.herb_name,
|
|
||||||
h.is_active,
|
|
||||||
COALESCE(SUM(il.quantity_onhand), 0) as current_stock,
|
|
||||||
GROUP_CONCAT(DISTINCT et.tag_name) as efficacy_tags
|
|
||||||
FROM herb_items h
|
FROM herb_items h
|
||||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id
|
LEFT JOIN v_current_stock s ON h.herb_item_id = s.herb_item_id
|
||||||
AND il.is_depleted = 0
|
|
||||||
LEFT JOIN herb_item_tags hit ON h.herb_item_id = hit.herb_item_id
|
|
||||||
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
|
|
||||||
WHERE h.is_active = 1
|
WHERE h.is_active = 1
|
||||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name, h.is_active
|
|
||||||
ORDER BY h.herb_name
|
ORDER BY h.herb_name
|
||||||
""")
|
""")
|
||||||
herbs = [dict(row) for row in cursor.fetchall()]
|
herbs = [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
# 태그를 리스트로 변환
|
|
||||||
for herb in herbs:
|
|
||||||
if herb['efficacy_tags']:
|
|
||||||
herb['efficacy_tags'] = herb['efficacy_tags'].split(',')
|
|
||||||
else:
|
|
||||||
herb['efficacy_tags'] = []
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': herbs})
|
return jsonify({'success': True, 'data': herbs})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
@ -245,53 +227,6 @@ def get_formula_ingredients(formula_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
# ==================== 도매상 관리 API ====================
|
|
||||||
|
|
||||||
@app.route('/api/suppliers', methods=['GET'])
|
|
||||||
def get_suppliers():
|
|
||||||
"""도매상 목록 조회"""
|
|
||||||
try:
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT supplier_id, name, business_no, phone, address, is_active
|
|
||||||
FROM suppliers
|
|
||||||
WHERE is_active = 1
|
|
||||||
ORDER BY name
|
|
||||||
""")
|
|
||||||
suppliers = [dict(row) for row in cursor.fetchall()]
|
|
||||||
return jsonify({'success': True, 'data': suppliers})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/suppliers', methods=['POST'])
|
|
||||||
def create_supplier():
|
|
||||||
"""도매상 등록"""
|
|
||||||
try:
|
|
||||||
data = request.json
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO suppliers (name, business_no, contact_person, phone, address)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
""", (
|
|
||||||
data['name'],
|
|
||||||
data.get('business_no'),
|
|
||||||
data.get('contact_person'),
|
|
||||||
data.get('phone'),
|
|
||||||
data.get('address')
|
|
||||||
))
|
|
||||||
supplier_id = cursor.lastrowid
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'message': '도매상이 등록되었습니다',
|
|
||||||
'supplier_id': supplier_id
|
|
||||||
})
|
|
||||||
except sqlite3.IntegrityError:
|
|
||||||
return jsonify({'success': False, 'error': '이미 등록된 도매상입니다'}), 400
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
# ==================== 입고 관리 API ====================
|
# ==================== 입고 관리 API ====================
|
||||||
|
|
||||||
@app.route('/api/upload/purchase', methods=['POST'])
|
@app.route('/api/upload/purchase', methods=['POST'])
|
||||||
@ -308,11 +243,6 @@ def upload_purchase_excel():
|
|||||||
if not allowed_file(file.filename):
|
if not allowed_file(file.filename):
|
||||||
return jsonify({'success': False, 'error': '허용되지 않는 파일 형식입니다'}), 400
|
return jsonify({'success': False, 'error': '허용되지 않는 파일 형식입니다'}), 400
|
||||||
|
|
||||||
# 도매상 ID 가져오기 (폼 데이터에서)
|
|
||||||
supplier_id = request.form.get('supplier_id')
|
|
||||||
if not supplier_id:
|
|
||||||
return jsonify({'success': False, 'error': '도매상을 선택해주세요'}), 400
|
|
||||||
|
|
||||||
# 파일 저장
|
# 파일 저장
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
@ -351,16 +281,21 @@ def upload_purchase_excel():
|
|||||||
processed_rows = 0
|
processed_rows = 0
|
||||||
processed_items = set()
|
processed_items = set()
|
||||||
|
|
||||||
# 도매상 정보 확인
|
# 날짜별, 업체별로 그룹화
|
||||||
cursor.execute("SELECT name FROM suppliers WHERE supplier_id = ?", (supplier_id,))
|
grouped = df.groupby(['receipt_date', 'supplier_name'])
|
||||||
supplier_info = cursor.fetchone()
|
|
||||||
if not supplier_info:
|
|
||||||
return jsonify({'success': False, 'error': '유효하지 않은 도매상입니다'}), 400
|
|
||||||
|
|
||||||
# 날짜별로 그룹화 (도매상은 이미 선택됨)
|
for (receipt_date, supplier_name), group in grouped:
|
||||||
grouped = df.groupby(['receipt_date'])
|
# 공급업체 확인/생성
|
||||||
|
cursor.execute("SELECT supplier_id FROM suppliers WHERE name = ?", (supplier_name,))
|
||||||
|
supplier = cursor.fetchone()
|
||||||
|
|
||||||
for receipt_date, group in grouped:
|
if not supplier:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO suppliers (name) VALUES (?)
|
||||||
|
""", (supplier_name,))
|
||||||
|
supplier_id = cursor.lastrowid
|
||||||
|
else:
|
||||||
|
supplier_id = supplier[0]
|
||||||
|
|
||||||
# 입고장 헤더 생성
|
# 입고장 헤더 생성
|
||||||
total_amount = group['total_amount'].sum()
|
total_amount = group['total_amount'].sum()
|
||||||
@ -464,13 +399,13 @@ def get_purchase_receipts():
|
|||||||
pr.receipt_id,
|
pr.receipt_id,
|
||||||
pr.receipt_date,
|
pr.receipt_date,
|
||||||
pr.receipt_no,
|
pr.receipt_no,
|
||||||
|
pr.total_amount,
|
||||||
pr.source_file,
|
pr.source_file,
|
||||||
pr.created_at,
|
pr.created_at,
|
||||||
s.name as supplier_name,
|
s.name as supplier_name,
|
||||||
s.supplier_id,
|
s.supplier_id,
|
||||||
COUNT(prl.line_id) as line_count,
|
COUNT(prl.line_id) as line_count,
|
||||||
SUM(prl.quantity_g) as total_quantity,
|
SUM(prl.quantity_g) as total_quantity
|
||||||
SUM(prl.line_total) as total_amount
|
|
||||||
FROM purchase_receipts pr
|
FROM purchase_receipts pr
|
||||||
JOIN suppliers s ON pr.supplier_id = s.supplier_id
|
JOIN suppliers s ON pr.supplier_id = s.supplier_id
|
||||||
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
|
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
|
||||||
@ -690,19 +625,7 @@ def delete_purchase_receipt(receipt_id):
|
|||||||
'error': f'{usage["used_count"]}개 품목에서 {usage["used_quantity"]}g이 이미 사용되어 삭제할 수 없습니다'
|
'error': f'{usage["used_count"]}개 품목에서 {usage["used_quantity"]}g이 이미 사용되어 삭제할 수 없습니다'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# 삭제 순서 중요: 참조하는 테이블부터 삭제
|
# 재고 로트 삭제
|
||||||
# 1. 재고 원장 기록 삭제 (lot_id를 참조)
|
|
||||||
cursor.execute("""
|
|
||||||
DELETE FROM stock_ledger
|
|
||||||
WHERE lot_id IN (
|
|
||||||
SELECT lot_id FROM inventory_lots
|
|
||||||
WHERE receipt_line_id IN (
|
|
||||||
SELECT line_id FROM purchase_receipt_lines WHERE receipt_id = ?
|
|
||||||
)
|
|
||||||
)
|
|
||||||
""", (receipt_id,))
|
|
||||||
|
|
||||||
# 2. 재고 로트 삭제 (receipt_line_id를 참조)
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
DELETE FROM inventory_lots
|
DELETE FROM inventory_lots
|
||||||
WHERE receipt_line_id IN (
|
WHERE receipt_line_id IN (
|
||||||
@ -710,10 +633,16 @@ def delete_purchase_receipt(receipt_id):
|
|||||||
)
|
)
|
||||||
""", (receipt_id,))
|
""", (receipt_id,))
|
||||||
|
|
||||||
# 3. 입고장 라인 삭제 (receipt_id를 참조)
|
# 재고 원장 기록
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM stock_ledger
|
||||||
|
WHERE reference_table = 'purchase_receipts' AND reference_id = ?
|
||||||
|
""", (receipt_id,))
|
||||||
|
|
||||||
|
# 입고장 라인 삭제
|
||||||
cursor.execute("DELETE FROM purchase_receipt_lines WHERE receipt_id = ?", (receipt_id,))
|
cursor.execute("DELETE FROM purchase_receipt_lines WHERE receipt_id = ?", (receipt_id,))
|
||||||
|
|
||||||
# 4. 입고장 헤더 삭제
|
# 입고장 헤더 삭제
|
||||||
cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,))
|
cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,))
|
||||||
|
|
||||||
return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'})
|
return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'})
|
||||||
@ -757,7 +686,6 @@ def create_compound():
|
|||||||
for ingredient in data['ingredients']:
|
for ingredient in data['ingredients']:
|
||||||
herb_item_id = ingredient['herb_item_id']
|
herb_item_id = ingredient['herb_item_id']
|
||||||
total_grams = ingredient['total_grams']
|
total_grams = ingredient['total_grams']
|
||||||
origin_country = ingredient.get('origin_country') # 원산지 선택 정보
|
|
||||||
|
|
||||||
# 조제 약재 구성 기록
|
# 조제 약재 구성 기록
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@ -767,26 +695,14 @@ def create_compound():
|
|||||||
""", (compound_id, herb_item_id,
|
""", (compound_id, herb_item_id,
|
||||||
ingredient['grams_per_cheop'], total_grams))
|
ingredient['grams_per_cheop'], total_grams))
|
||||||
|
|
||||||
# 재고 차감 (FIFO 방식 - 원산지 지정 시 해당 원산지만)
|
# 재고 차감 (FIFO 방식)
|
||||||
remaining_qty = total_grams
|
remaining_qty = total_grams
|
||||||
|
cursor.execute("""
|
||||||
# 원산지가 지정된 경우 해당 원산지만, 아니면 전체에서 FIFO
|
SELECT lot_id, quantity_onhand, unit_price_per_g
|
||||||
if origin_country and origin_country != 'auto':
|
FROM inventory_lots
|
||||||
cursor.execute("""
|
WHERE herb_item_id = ? AND is_depleted = 0 AND quantity_onhand > 0
|
||||||
SELECT lot_id, quantity_onhand, unit_price_per_g
|
ORDER BY received_date, lot_id
|
||||||
FROM inventory_lots
|
""", (herb_item_id,))
|
||||||
WHERE herb_item_id = ? AND is_depleted = 0 AND quantity_onhand > 0
|
|
||||||
AND origin_country = ?
|
|
||||||
ORDER BY unit_price_per_g, received_date, lot_id
|
|
||||||
""", (herb_item_id, origin_country))
|
|
||||||
else:
|
|
||||||
# 자동 선택: 가격이 저렴한 것부터
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT lot_id, quantity_onhand, unit_price_per_g
|
|
||||||
FROM inventory_lots
|
|
||||||
WHERE herb_item_id = ? AND is_depleted = 0 AND quantity_onhand > 0
|
|
||||||
ORDER BY unit_price_per_g, received_date, lot_id
|
|
||||||
""", (herb_item_id,))
|
|
||||||
|
|
||||||
lots = cursor.fetchall()
|
lots = cursor.fetchall()
|
||||||
|
|
||||||
@ -845,90 +761,11 @@ def create_compound():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
# ==================== 조제용 재고 조회 API ====================
|
|
||||||
|
|
||||||
@app.route('/api/herbs/<int:herb_item_id>/available-lots', methods=['GET'])
|
|
||||||
def get_available_lots(herb_item_id):
|
|
||||||
"""조제용 가용 로트 목록 - 원산지별로 그룹화"""
|
|
||||||
try:
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# 약재 정보
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT herb_name, insurance_code
|
|
||||||
FROM herb_items
|
|
||||||
WHERE herb_item_id = ?
|
|
||||||
""", (herb_item_id,))
|
|
||||||
herb = cursor.fetchone()
|
|
||||||
|
|
||||||
if not herb:
|
|
||||||
return jsonify({'success': False, 'error': '약재를 찾을 수 없습니다'}), 404
|
|
||||||
|
|
||||||
# 가용 로트 목록 (소진되지 않은 재고)
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
lot_id,
|
|
||||||
origin_country,
|
|
||||||
quantity_onhand,
|
|
||||||
unit_price_per_g,
|
|
||||||
received_date,
|
|
||||||
supplier_id
|
|
||||||
FROM inventory_lots
|
|
||||||
WHERE herb_item_id = ?
|
|
||||||
AND is_depleted = 0
|
|
||||||
AND quantity_onhand > 0
|
|
||||||
ORDER BY origin_country, unit_price_per_g, received_date
|
|
||||||
""", (herb_item_id,))
|
|
||||||
|
|
||||||
lots = []
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
lots.append({
|
|
||||||
'lot_id': row[0],
|
|
||||||
'origin_country': row[1] or '미지정',
|
|
||||||
'quantity_onhand': row[2],
|
|
||||||
'unit_price_per_g': row[3],
|
|
||||||
'received_date': row[4],
|
|
||||||
'supplier_id': row[5]
|
|
||||||
})
|
|
||||||
|
|
||||||
# 원산지별 요약
|
|
||||||
origin_summary = {}
|
|
||||||
for lot in lots:
|
|
||||||
origin = lot['origin_country']
|
|
||||||
if origin not in origin_summary:
|
|
||||||
origin_summary[origin] = {
|
|
||||||
'origin_country': origin,
|
|
||||||
'total_quantity': 0,
|
|
||||||
'min_price': float('inf'),
|
|
||||||
'max_price': 0,
|
|
||||||
'lot_count': 0,
|
|
||||||
'lots': []
|
|
||||||
}
|
|
||||||
|
|
||||||
origin_summary[origin]['total_quantity'] += lot['quantity_onhand']
|
|
||||||
origin_summary[origin]['min_price'] = min(origin_summary[origin]['min_price'], lot['unit_price_per_g'])
|
|
||||||
origin_summary[origin]['max_price'] = max(origin_summary[origin]['max_price'], lot['unit_price_per_g'])
|
|
||||||
origin_summary[origin]['lot_count'] += 1
|
|
||||||
origin_summary[origin]['lots'].append(lot)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'data': {
|
|
||||||
'herb_name': herb[0],
|
|
||||||
'insurance_code': herb[1],
|
|
||||||
'origins': list(origin_summary.values()),
|
|
||||||
'total_quantity': sum(lot['quantity_onhand'] for lot in lots)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
# ==================== 재고 현황 API ====================
|
# ==================== 재고 현황 API ====================
|
||||||
|
|
||||||
@app.route('/api/inventory/summary', methods=['GET'])
|
@app.route('/api/inventory/summary', methods=['GET'])
|
||||||
def get_inventory_summary():
|
def get_inventory_summary():
|
||||||
"""재고 현황 요약 - 원산지별 구분 표시"""
|
"""재고 현황 요약"""
|
||||||
try:
|
try:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@ -939,29 +776,16 @@ def get_inventory_summary():
|
|||||||
h.herb_name,
|
h.herb_name,
|
||||||
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
|
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
|
||||||
COUNT(DISTINCT il.lot_id) as lot_count,
|
COUNT(DISTINCT il.lot_id) as lot_count,
|
||||||
COUNT(DISTINCT il.origin_country) as origin_count,
|
|
||||||
AVG(il.unit_price_per_g) as avg_price,
|
AVG(il.unit_price_per_g) as avg_price,
|
||||||
MIN(il.unit_price_per_g) as min_price,
|
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
|
||||||
MAX(il.unit_price_per_g) as max_price,
|
|
||||||
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value,
|
|
||||||
GROUP_CONCAT(DISTINCT et.tag_name) as efficacy_tags
|
|
||||||
FROM herb_items h
|
FROM herb_items h
|
||||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
|
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
|
||||||
LEFT JOIN herb_item_tags hit ON h.herb_item_id = hit.herb_item_id
|
|
||||||
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
|
|
||||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name
|
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name
|
||||||
HAVING total_quantity > 0
|
HAVING total_quantity > 0
|
||||||
ORDER BY h.herb_name
|
ORDER BY h.herb_name
|
||||||
""")
|
""")
|
||||||
inventory = [dict(row) for row in cursor.fetchall()]
|
inventory = [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
# 태그를 리스트로 변환
|
|
||||||
for item in inventory:
|
|
||||||
if item['efficacy_tags']:
|
|
||||||
item['efficacy_tags'] = item['efficacy_tags'].split(',')
|
|
||||||
else:
|
|
||||||
item['efficacy_tags'] = []
|
|
||||||
|
|
||||||
# 전체 요약
|
# 전체 요약
|
||||||
total_value = sum(item['total_value'] for item in inventory)
|
total_value = sum(item['total_value'] for item in inventory)
|
||||||
total_items = len(inventory)
|
total_items = len(inventory)
|
||||||
@ -977,78 +801,6 @@ def get_inventory_summary():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/inventory/detail/<int:herb_item_id>', methods=['GET'])
|
|
||||||
def get_inventory_detail(herb_item_id):
|
|
||||||
"""약재별 재고 상세 - 원산지별로 구분"""
|
|
||||||
try:
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# 약재 기본 정보
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT herb_item_id, insurance_code, herb_name
|
|
||||||
FROM herb_items
|
|
||||||
WHERE herb_item_id = ?
|
|
||||||
""", (herb_item_id,))
|
|
||||||
herb = cursor.fetchone()
|
|
||||||
|
|
||||||
if not herb:
|
|
||||||
return jsonify({'success': False, 'error': '약재를 찾을 수 없습니다'}), 404
|
|
||||||
|
|
||||||
herb_data = dict(herb)
|
|
||||||
|
|
||||||
# 원산지별 재고 정보
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
il.lot_id,
|
|
||||||
il.origin_country,
|
|
||||||
il.quantity_onhand,
|
|
||||||
il.unit_price_per_g,
|
|
||||||
il.received_date,
|
|
||||||
il.supplier_id,
|
|
||||||
s.name as supplier_name,
|
|
||||||
il.quantity_onhand * il.unit_price_per_g as lot_value
|
|
||||||
FROM inventory_lots il
|
|
||||||
LEFT JOIN suppliers s ON il.supplier_id = s.supplier_id
|
|
||||||
WHERE il.herb_item_id = ? AND il.is_depleted = 0
|
|
||||||
ORDER BY il.origin_country, il.unit_price_per_g, il.received_date
|
|
||||||
""", (herb_item_id,))
|
|
||||||
|
|
||||||
lots = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
# 원산지별 그룹화
|
|
||||||
by_origin = {}
|
|
||||||
for lot in lots:
|
|
||||||
origin = lot['origin_country'] or '미지정'
|
|
||||||
if origin not in by_origin:
|
|
||||||
by_origin[origin] = {
|
|
||||||
'origin_country': origin,
|
|
||||||
'lots': [],
|
|
||||||
'total_quantity': 0,
|
|
||||||
'total_value': 0,
|
|
||||||
'min_price': float('inf'),
|
|
||||||
'max_price': 0,
|
|
||||||
'avg_price': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
by_origin[origin]['lots'].append(lot)
|
|
||||||
by_origin[origin]['total_quantity'] += lot['quantity_onhand']
|
|
||||||
by_origin[origin]['total_value'] += lot['lot_value']
|
|
||||||
by_origin[origin]['min_price'] = min(by_origin[origin]['min_price'], lot['unit_price_per_g'])
|
|
||||||
by_origin[origin]['max_price'] = max(by_origin[origin]['max_price'], lot['unit_price_per_g'])
|
|
||||||
|
|
||||||
# 평균 단가 계산
|
|
||||||
for origin_data in by_origin.values():
|
|
||||||
if origin_data['total_quantity'] > 0:
|
|
||||||
origin_data['avg_price'] = origin_data['total_value'] / origin_data['total_quantity']
|
|
||||||
|
|
||||||
herb_data['origins'] = list(by_origin.values())
|
|
||||||
herb_data['total_origins'] = len(by_origin)
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': herb_data})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
# 서버 실행
|
# 서버 실행
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 데이터베이스 초기화
|
# 데이터베이스 초기화
|
||||||
|
|||||||
@ -1,158 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
약재 효능 태그 시스템 추가 스크립트
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
def check_and_create_efficacy_system():
|
|
||||||
conn = sqlite3.connect('database/kdrug.db')
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. 현재 테이블 확인
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT name FROM sqlite_master
|
|
||||||
WHERE type='table'
|
|
||||||
ORDER BY name
|
|
||||||
""")
|
|
||||||
print("=== 현재 테이블 목록 ===")
|
|
||||||
for table in cursor.fetchall():
|
|
||||||
print(f" - {table[0]}")
|
|
||||||
|
|
||||||
# 2. herb_items 테이블 스키마 확인
|
|
||||||
print("\n=== herb_items 테이블 구조 ===")
|
|
||||||
cursor.execute("PRAGMA table_info(herb_items)")
|
|
||||||
columns = cursor.fetchall()
|
|
||||||
for col in columns:
|
|
||||||
print(f" {col[1]} ({col[2]})")
|
|
||||||
|
|
||||||
# 3. 효능 태그 테이블 생성
|
|
||||||
print("\n=== 효능 태그 시스템 생성 ===")
|
|
||||||
|
|
||||||
# 효능 마스터 테이블
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS herb_efficacy_tags (
|
|
||||||
tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
tag_name VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
tag_category VARCHAR(50), -- 보(補), 사(瀉), 온(溫), 량(涼) 등
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
print("✅ herb_efficacy_tags 테이블 생성")
|
|
||||||
|
|
||||||
# 약재-효능 연결 테이블 (다대다)
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS herb_item_tags (
|
|
||||||
herb_item_id INTEGER NOT NULL,
|
|
||||||
tag_id INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (herb_item_id, tag_id),
|
|
||||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (tag_id) REFERENCES herb_efficacy_tags(tag_id) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
print("✅ herb_item_tags 테이블 생성")
|
|
||||||
|
|
||||||
# 4. 기본 효능 태그 추가
|
|
||||||
basic_tags = [
|
|
||||||
('보혈', '보', '혈을 보하는 효능'),
|
|
||||||
('보기', '보', '기를 보하는 효능'),
|
|
||||||
('보양', '보', '양기를 보하는 효능'),
|
|
||||||
('보음', '보', '음액을 보하는 효능'),
|
|
||||||
('활혈', '활', '혈액순환을 활발하게 하는 효능'),
|
|
||||||
('거담', '거', '가래를 제거하는 효능'),
|
|
||||||
('온중', '온', '속을 따뜻하게 하는 효능'),
|
|
||||||
('온양', '온', '양기를 따뜻하게 하는 효능'),
|
|
||||||
('청열', '청', '열을 내리는 효능'),
|
|
||||||
('해표', '해', '표증을 해소하는 효능'),
|
|
||||||
('소화', '소', '소화를 돕는 효능'),
|
|
||||||
('이수', '이', '수분대사를 돕는 효능'),
|
|
||||||
('안신', '안', '정신을 안정시키는 효능'),
|
|
||||||
('지혈', '지', '출혈을 멈추는 효능'),
|
|
||||||
('조화제약', '조화', '여러 약재를 조화롭게 하는 효능'),
|
|
||||||
('대보원기', '대보', '원기를 크게 보하는 효능'),
|
|
||||||
('보기건비', '보', '기를 보하고 비장을 건강하게 하는 효능'),
|
|
||||||
('보중익기', '보', '중초를 보하고 기를 증진시키는 효능'),
|
|
||||||
]
|
|
||||||
|
|
||||||
for tag_name, category, description in basic_tags:
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT OR IGNORE INTO herb_efficacy_tags (tag_name, tag_category, description)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
""", (tag_name, category, description))
|
|
||||||
|
|
||||||
print(f"✅ {len(basic_tags)}개 기본 효능 태그 추가")
|
|
||||||
|
|
||||||
# 5. 쌍화탕 약재들에 효능 태그 연결
|
|
||||||
ssanghwa_herbs = [
|
|
||||||
('숙지황', '보혈'),
|
|
||||||
('당귀', '보혈'),
|
|
||||||
('백작약', '보혈'),
|
|
||||||
('천궁', '활혈'),
|
|
||||||
('황기', '보기'),
|
|
||||||
('인삼', '대보원기'),
|
|
||||||
('백출', '보기건비'),
|
|
||||||
('감초', '조화제약'),
|
|
||||||
('생강', '온중'),
|
|
||||||
('대추', '보중익기'),
|
|
||||||
('육계', '온양'),
|
|
||||||
('건강', '온중'),
|
|
||||||
]
|
|
||||||
|
|
||||||
print("\n=== 약재별 효능 태그 연결 ===")
|
|
||||||
for herb_name, tag_name in ssanghwa_herbs:
|
|
||||||
# 약재 ID 찾기
|
|
||||||
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (herb_name,))
|
|
||||||
herb_result = cursor.fetchone()
|
|
||||||
|
|
||||||
# 태그 ID 찾기
|
|
||||||
cursor.execute("SELECT tag_id FROM herb_efficacy_tags WHERE tag_name = ?", (tag_name,))
|
|
||||||
tag_result = cursor.fetchone()
|
|
||||||
|
|
||||||
if herb_result and tag_result:
|
|
||||||
herb_id = herb_result[0]
|
|
||||||
tag_id = tag_result[0]
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT OR IGNORE INTO herb_item_tags (herb_item_id, tag_id)
|
|
||||||
VALUES (?, ?)
|
|
||||||
""", (herb_id, tag_id))
|
|
||||||
print(f" ✅ {herb_name} → {tag_name}")
|
|
||||||
else:
|
|
||||||
if not herb_result:
|
|
||||||
print(f" ⚠️ {herb_name} 약재 없음")
|
|
||||||
if not tag_result:
|
|
||||||
print(f" ⚠️ {tag_name} 태그 없음")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# 6. 결과 확인 - 약재별 태그 조회
|
|
||||||
print("\n=== 약재별 효능 태그 확인 ===")
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
h.herb_name,
|
|
||||||
GROUP_CONCAT(t.tag_name, ', ') as tags
|
|
||||||
FROM herb_items h
|
|
||||||
LEFT JOIN herb_item_tags ht ON h.herb_item_id = ht.herb_item_id
|
|
||||||
LEFT JOIN herb_efficacy_tags t ON ht.tag_id = t.tag_id
|
|
||||||
WHERE ht.tag_id IS NOT NULL
|
|
||||||
GROUP BY h.herb_item_id
|
|
||||||
ORDER BY h.herb_name
|
|
||||||
""")
|
|
||||||
|
|
||||||
results = cursor.fetchall()
|
|
||||||
for herb_name, tags in results:
|
|
||||||
print(f" {herb_name}: {tags}")
|
|
||||||
|
|
||||||
print("\n✅ 효능 태그 시스템 구축 완료!")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 오류 발생: {e}")
|
|
||||||
conn.rollback()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
check_and_create_efficacy_system()
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
쌍화탕 처방 등록 스크립트
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def insert_ssanghwa_formula():
|
|
||||||
conn = sqlite3.connect('database/kdrug.db')
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. 쌍화탕 처방 등록
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO formulas (
|
|
||||||
formula_code,
|
|
||||||
formula_name,
|
|
||||||
formula_type,
|
|
||||||
base_cheop,
|
|
||||||
base_pouches,
|
|
||||||
description
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
""", (
|
|
||||||
'SST001',
|
|
||||||
'쌍화탕',
|
|
||||||
'CUSTOM',
|
|
||||||
20,
|
|
||||||
30,
|
|
||||||
'기혈을 보하고 원기를 회복시키는 대표적인 보약 처방'
|
|
||||||
))
|
|
||||||
|
|
||||||
formula_id = cursor.lastrowid
|
|
||||||
print(f"✅ 쌍화탕 처방 등록 완료 (ID: {formula_id})")
|
|
||||||
|
|
||||||
# 2. 쌍화탕 구성 약재 등록
|
|
||||||
# 전형적인 쌍화탕 구성 (1첩 기준 용량)
|
|
||||||
ingredients = [
|
|
||||||
('숙지황', 8.0, '보혈'),
|
|
||||||
('당귀', 6.0, '보혈'),
|
|
||||||
('백작약', 6.0, '보혈'),
|
|
||||||
('천궁', 4.0, '활혈'),
|
|
||||||
('황기', 6.0, '보기'),
|
|
||||||
('인삼', 4.0, '대보원기'),
|
|
||||||
('백출', 4.0, '보기건비'),
|
|
||||||
('감초', 3.0, '조화제약'),
|
|
||||||
('생강', 5.0, '온중'),
|
|
||||||
('대추', 3.0, '보중익기'),
|
|
||||||
('육계', 2.0, '온양'),
|
|
||||||
('건강', 2.0, '온중') # 건강 포함!
|
|
||||||
]
|
|
||||||
|
|
||||||
for herb_name, grams, notes in ingredients:
|
|
||||||
# 약재 ID 찾기
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT herb_item_id FROM herb_items
|
|
||||||
WHERE herb_name = ?
|
|
||||||
""", (herb_name,))
|
|
||||||
|
|
||||||
result = cursor.fetchone()
|
|
||||||
if result:
|
|
||||||
herb_id = result[0]
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO formula_ingredients (
|
|
||||||
formula_id,
|
|
||||||
herb_item_id,
|
|
||||||
grams_per_cheop,
|
|
||||||
notes
|
|
||||||
) VALUES (?, ?, ?, ?)
|
|
||||||
""", (formula_id, herb_id, grams, notes))
|
|
||||||
print(f" - {herb_name}: {grams}g ({notes})")
|
|
||||||
else:
|
|
||||||
print(f" ⚠️ {herb_name} 약재를 찾을 수 없음")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
print("\n✅ 쌍화탕 처방 구성 완료!")
|
|
||||||
|
|
||||||
# 3. 등록 확인
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
f.formula_name,
|
|
||||||
COUNT(fi.ingredient_id) as ingredient_count,
|
|
||||||
SUM(fi.grams_per_cheop) as total_grams_per_cheop
|
|
||||||
FROM formulas f
|
|
||||||
LEFT JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
|
|
||||||
WHERE f.formula_id = ?
|
|
||||||
GROUP BY f.formula_id
|
|
||||||
""", (formula_id,))
|
|
||||||
|
|
||||||
result = cursor.fetchone()
|
|
||||||
if result:
|
|
||||||
print(f"\n📋 등록 결과:")
|
|
||||||
print(f" 처방명: {result[0]}")
|
|
||||||
print(f" 구성 약재: {result[1]}종")
|
|
||||||
print(f" 1첩 총량: {result[2]}g")
|
|
||||||
print(f" 20첩 총량: {result[2] * 20}g")
|
|
||||||
|
|
||||||
except sqlite3.IntegrityError as e:
|
|
||||||
print(f"⚠️ 이미 등록된 처방이거나 오류 발생: {e}")
|
|
||||||
conn.rollback()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 오류 발생: {e}")
|
|
||||||
conn.rollback()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
insert_ssanghwa_formula()
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Flask 서버 실행 스크립트 - 강화 버전
|
|
||||||
|
|
||||||
echo "=== Flask 서버 관리 스크립트 (강화 버전) ==="
|
|
||||||
|
|
||||||
# 1. 모든 Python Flask 프로세스 종료
|
|
||||||
echo "1. 모든 Flask 프로세스 종료 중..."
|
|
||||||
pkill -9 -f "python app.py" 2>/dev/null
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# 2. 포트 5001 강제 해제
|
|
||||||
echo "2. 포트 5001 강제 해제..."
|
|
||||||
lsof -ti:5001 | xargs -r kill -9 2>/dev/null
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# 3. 잔여 Python 프로세스 확인
|
|
||||||
echo "3. 잔여 프로세스 확인..."
|
|
||||||
REMAINING=$(lsof -ti:5001)
|
|
||||||
if [ ! -z "$REMAINING" ]; then
|
|
||||||
echo " 경고: 아직 포트를 사용 중인 프로세스 발견. 강제 종료..."
|
|
||||||
kill -9 $REMAINING 2>/dev/null
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. 포트 상태 최종 확인
|
|
||||||
if lsof -i:5001 > /dev/null 2>&1; then
|
|
||||||
echo " ⚠️ 포트 5001이 여전히 사용 중입니다. 잠시 후 다시 시도하세요."
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo " ✅ 포트 5001이 비어있습니다."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 5. Flask 서버 시작
|
|
||||||
echo "4. Flask 서버 시작..."
|
|
||||||
cd /root/kdrug
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Debug 모드 비활성화 옵션 (더 안정적)
|
|
||||||
# export FLASK_DEBUG=0
|
|
||||||
|
|
||||||
echo " 서버 시작 중... (http://localhost:5001)"
|
|
||||||
echo " 종료: Ctrl+C"
|
|
||||||
echo "======================================="
|
|
||||||
python app.py
|
|
||||||
348
static/app.js
348
static/app.js
@ -32,7 +32,6 @@ $(document).ready(function() {
|
|||||||
break;
|
break;
|
||||||
case 'purchase':
|
case 'purchase':
|
||||||
loadPurchaseReceipts();
|
loadPurchaseReceipts();
|
||||||
loadSuppliersForSelect();
|
|
||||||
break;
|
break;
|
||||||
case 'formulas':
|
case 'formulas':
|
||||||
loadFormulas();
|
loadFormulas();
|
||||||
@ -288,15 +287,6 @@ $(document).ready(function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 직접조제인 경우
|
|
||||||
if (formulaId === 'custom') {
|
|
||||||
$('#compoundIngredients').empty();
|
|
||||||
// 빈 행 하나 추가
|
|
||||||
addEmptyIngredientRow();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 등록된 처방인 경우
|
|
||||||
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
|
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$('#compoundIngredients').empty();
|
$('#compoundIngredients').empty();
|
||||||
@ -313,11 +303,6 @@ $(document).ready(function() {
|
|||||||
value="${ing.grams_per_cheop}" min="0.1" step="0.1">
|
value="${ing.grams_per_cheop}" min="0.1" step="0.1">
|
||||||
</td>
|
</td>
|
||||||
<td class="total-grams">${totalGrams.toFixed(1)}</td>
|
<td class="total-grams">${totalGrams.toFixed(1)}</td>
|
||||||
<td class="origin-select-cell">
|
|
||||||
<select class="form-control form-control-sm origin-select" disabled>
|
|
||||||
<option value="">로딩중...</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td class="stock-status">확인중...</td>
|
<td class="stock-status">확인중...</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
|
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
|
||||||
@ -326,9 +311,6 @@ $(document).ready(function() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 각 약재별로 원산지별 재고 확인
|
|
||||||
loadOriginOptions(ing.herb_item_id, totalGrams);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 재고 확인
|
// 재고 확인
|
||||||
@ -371,77 +353,6 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 조제 약재 추가
|
// 조제 약재 추가
|
||||||
// 빈 약재 행 추가 함수
|
|
||||||
function addEmptyIngredientRow() {
|
|
||||||
const newRow = $(`
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<select class="form-control form-control-sm herb-select-compound">
|
|
||||||
<option value="">약재 선택</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input type="number" class="form-control form-control-sm grams-per-cheop"
|
|
||||||
min="0.1" step="0.1" placeholder="0.0">
|
|
||||||
</td>
|
|
||||||
<td class="total-grams">0.0</td>
|
|
||||||
<td class="origin-select-cell">
|
|
||||||
<select class="form-control form-control-sm origin-select" disabled>
|
|
||||||
<option value="">약재 선택 후 표시</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td class="stock-status">-</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
|
|
||||||
<i class="bi bi-x"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`);
|
|
||||||
|
|
||||||
$('#compoundIngredients').append(newRow);
|
|
||||||
|
|
||||||
// 약재 목록 로드
|
|
||||||
loadHerbsForSelect(newRow.find('.herb-select-compound'));
|
|
||||||
|
|
||||||
// 약재 선택 시 원산지 옵션 로드
|
|
||||||
newRow.find('.herb-select-compound').on('change', function() {
|
|
||||||
const herbId = $(this).val();
|
|
||||||
if (herbId) {
|
|
||||||
const row = $(this).closest('tr');
|
|
||||||
row.attr('data-herb-id', herbId);
|
|
||||||
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
|
||||||
const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0;
|
|
||||||
const totalGrams = gramsPerCheop * cheopTotal;
|
|
||||||
loadOriginOptions(herbId, totalGrams);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 이벤트 바인딩
|
|
||||||
newRow.find('.grams-per-cheop').on('input', function() {
|
|
||||||
updateIngredientTotals();
|
|
||||||
// 원산지 옵션 다시 로드
|
|
||||||
const herbId = $(this).closest('tr').attr('data-herb-id');
|
|
||||||
if (herbId) {
|
|
||||||
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
|
||||||
const gramsPerCheop = parseFloat($(this).val()) || 0;
|
|
||||||
const totalGrams = gramsPerCheop * cheopTotal;
|
|
||||||
loadOriginOptions(herbId, totalGrams);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newRow.find('.remove-compound-ingredient').on('click', function() {
|
|
||||||
$(this).closest('tr').remove();
|
|
||||||
updateIngredientTotals();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#addIngredientBtn').on('click', function() {
|
|
||||||
addEmptyIngredientRow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 기존 약재 추가 버튼 (기존 코드 삭제)
|
|
||||||
/*
|
|
||||||
$('#addIngredientBtn').on('click', function() {
|
$('#addIngredientBtn').on('click', function() {
|
||||||
const newRow = $(`
|
const newRow = $(`
|
||||||
<tr>
|
<tr>
|
||||||
@ -480,7 +391,6 @@ $(document).ready(function() {
|
|||||||
updateIngredientTotals();
|
updateIngredientTotals();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
|
|
||||||
// 조제 실행
|
// 조제 실행
|
||||||
$('#compoundEntryForm').on('submit', function(e) {
|
$('#compoundEntryForm').on('submit', function(e) {
|
||||||
@ -491,14 +401,12 @@ $(document).ready(function() {
|
|||||||
const herbId = $(this).data('herb-id');
|
const herbId = $(this).data('herb-id');
|
||||||
const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val());
|
const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val());
|
||||||
const totalGrams = parseFloat($(this).find('.total-grams').text());
|
const totalGrams = parseFloat($(this).find('.total-grams').text());
|
||||||
const originCountry = $(this).find('.origin-select').val();
|
|
||||||
|
|
||||||
if (herbId && gramsPerCheop) {
|
if (herbId && gramsPerCheop) {
|
||||||
ingredients.push({
|
ingredients.push({
|
||||||
herb_item_id: parseInt(herbId),
|
herb_item_id: parseInt(herbId),
|
||||||
grams_per_cheop: gramsPerCheop,
|
grams_per_cheop: gramsPerCheop,
|
||||||
total_grams: totalGrams,
|
total_grams: totalGrams
|
||||||
origin_country: originCountry || null // 원산지 선택 정보 추가
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -544,140 +452,17 @@ $(document).ready(function() {
|
|||||||
tbody.empty();
|
tbody.empty();
|
||||||
|
|
||||||
response.data.forEach(item => {
|
response.data.forEach(item => {
|
||||||
// 원산지가 여러 개인 경우 표시
|
|
||||||
const originBadge = item.origin_count > 1
|
|
||||||
? `<span class="badge bg-info ms-2">${item.origin_count}개 원산지</span>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// 효능 태그 표시
|
|
||||||
let efficacyTags = '';
|
|
||||||
if (item.efficacy_tags && item.efficacy_tags.length > 0) {
|
|
||||||
efficacyTags = item.efficacy_tags.map(tag =>
|
|
||||||
`<span class="badge bg-success ms-1">${tag}</span>`
|
|
||||||
).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 가격 범위 표시 (원산지가 여러 개이고 가격차가 있는 경우)
|
|
||||||
let priceDisplay = item.avg_price ? formatCurrency(item.avg_price) : '-';
|
|
||||||
if (item.origin_count > 1 && item.min_price && item.max_price && item.min_price !== item.max_price) {
|
|
||||||
priceDisplay = `${formatCurrency(item.min_price)} ~ ${formatCurrency(item.max_price)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody.append(`
|
tbody.append(`
|
||||||
<tr class="inventory-row" data-herb-id="${item.herb_item_id}" style="cursor: pointer;">
|
<tr>
|
||||||
<td>${item.insurance_code || '-'}</td>
|
<td>${item.insurance_code || '-'}</td>
|
||||||
<td>${item.herb_name}${originBadge}${efficacyTags}</td>
|
<td>${item.herb_name}</td>
|
||||||
<td>${item.total_quantity.toFixed(1)}</td>
|
<td>${item.total_quantity.toFixed(1)}</td>
|
||||||
<td>${item.lot_count}</td>
|
<td>${item.lot_count}</td>
|
||||||
<td>${priceDisplay}</td>
|
<td>${item.avg_price ? formatCurrency(item.avg_price) : '-'}</td>
|
||||||
<td>${formatCurrency(item.total_value)}</td>
|
<td>${formatCurrency(item.total_value)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 클릭 이벤트 바인딩
|
|
||||||
$('.inventory-row').on('click', function() {
|
|
||||||
const herbId = $(this).data('herb-id');
|
|
||||||
showInventoryDetail(herbId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 재고 상세 모달 표시
|
|
||||||
function showInventoryDetail(herbId) {
|
|
||||||
$.get(`/api/inventory/detail/${herbId}`, function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
const data = response.data;
|
|
||||||
|
|
||||||
// 원산지별 재고 정보 HTML 생성
|
|
||||||
let originsHtml = '';
|
|
||||||
data.origins.forEach(origin => {
|
|
||||||
originsHtml += `
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h6 class="mb-0">
|
|
||||||
<i class="bi bi-geo-alt"></i> ${origin.origin_country}
|
|
||||||
<span class="badge bg-primary float-end">${origin.total_quantity.toFixed(1)}g</span>
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-6">
|
|
||||||
<small class="text-muted">평균 단가:</small><br>
|
|
||||||
<strong>${formatCurrency(origin.avg_price)}/g</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<small class="text-muted">재고 가치:</small><br>
|
|
||||||
<strong>${formatCurrency(origin.total_value)}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>로트ID</th>
|
|
||||||
<th>수량</th>
|
|
||||||
<th>단가</th>
|
|
||||||
<th>입고일</th>
|
|
||||||
<th>도매상</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>`;
|
|
||||||
|
|
||||||
origin.lots.forEach(lot => {
|
|
||||||
originsHtml += `
|
|
||||||
<tr>
|
|
||||||
<td>#${lot.lot_id}</td>
|
|
||||||
<td>${lot.quantity_onhand.toFixed(1)}g</td>
|
|
||||||
<td>${formatCurrency(lot.unit_price_per_g)}</td>
|
|
||||||
<td>${lot.received_date}</td>
|
|
||||||
<td>${lot.supplier_name || '-'}</td>
|
|
||||||
</tr>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
originsHtml += `
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 모달 생성 및 표시
|
|
||||||
const modalHtml = `
|
|
||||||
<div class="modal fade" id="inventoryDetailModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">
|
|
||||||
${data.herb_name} 재고 상세
|
|
||||||
<small class="text-muted">(${data.insurance_code})</small>
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
${data.total_origins > 1
|
|
||||||
? `<div class="alert alert-info">
|
|
||||||
<i class="bi bi-info-circle"></i>
|
|
||||||
이 약재는 ${data.total_origins}개 원산지의 재고가 있습니다.
|
|
||||||
조제 시 원산지를 선택할 수 있습니다.
|
|
||||||
</div>`
|
|
||||||
: ''}
|
|
||||||
${originsHtml}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
// 기존 모달 제거
|
|
||||||
$('#inventoryDetailModal').remove();
|
|
||||||
$('body').append(modalHtml);
|
|
||||||
|
|
||||||
// 모달 표시
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('inventoryDetailModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -735,8 +520,8 @@ $(document).ready(function() {
|
|||||||
<td>${receipt.receipt_date}</td>
|
<td>${receipt.receipt_date}</td>
|
||||||
<td>${receipt.supplier_name}</td>
|
<td>${receipt.supplier_name}</td>
|
||||||
<td>${receipt.line_count}개</td>
|
<td>${receipt.line_count}개</td>
|
||||||
<td class="fw-bold text-primary">${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'}</td>
|
<td>${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'}</td>
|
||||||
<td class="text-muted small">${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'}</td>
|
<td>${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'}</td>
|
||||||
<td>${receipt.source_file || '-'}</td>
|
<td>${receipt.source_file || '-'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}">
|
<button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}">
|
||||||
@ -853,71 +638,10 @@ $(document).ready(function() {
|
|||||||
loadPurchaseReceipts();
|
loadPurchaseReceipts();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 도매상 목록 로드 (셀렉트 박스용)
|
|
||||||
function loadSuppliersForSelect() {
|
|
||||||
$.get('/api/suppliers', function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
const select = $('#uploadSupplier');
|
|
||||||
select.empty().append('<option value="">도매상을 선택하세요</option>');
|
|
||||||
|
|
||||||
response.data.forEach(supplier => {
|
|
||||||
select.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 필터용 셀렉트 박스도 업데이트
|
|
||||||
const filterSelect = $('#purchaseSupplier');
|
|
||||||
filterSelect.empty().append('<option value="">전체</option>');
|
|
||||||
response.data.forEach(supplier => {
|
|
||||||
filterSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 도매상 등록
|
|
||||||
$('#saveSupplierBtn').on('click', function() {
|
|
||||||
const supplierData = {
|
|
||||||
name: $('#supplierName').val(),
|
|
||||||
business_no: $('#supplierBusinessNo').val(),
|
|
||||||
contact_person: $('#supplierContactPerson').val(),
|
|
||||||
phone: $('#supplierPhone').val(),
|
|
||||||
address: $('#supplierAddress').val()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!supplierData.name) {
|
|
||||||
alert('도매상명은 필수입니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/suppliers',
|
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(supplierData),
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
alert('도매상이 등록되었습니다.');
|
|
||||||
$('#supplierModal').modal('hide');
|
|
||||||
$('#supplierForm')[0].reset();
|
|
||||||
loadSuppliersForSelect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function(xhr) {
|
|
||||||
alert('오류: ' + xhr.responseJSON.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 입고장 업로드
|
// 입고장 업로드
|
||||||
$('#purchaseUploadForm').on('submit', function(e) {
|
$('#purchaseUploadForm').on('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const supplierId = $('#uploadSupplier').val();
|
|
||||||
if (!supplierId) {
|
|
||||||
alert('도매상을 선택해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const fileInput = $('#purchaseFile')[0];
|
const fileInput = $('#purchaseFile')[0];
|
||||||
|
|
||||||
@ -927,7 +651,6 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formData.append('file', fileInput.files[0]);
|
formData.append('file', fileInput.files[0]);
|
||||||
formData.append('supplier_id', supplierId);
|
|
||||||
|
|
||||||
$('#uploadResult').html('<div class="alert alert-info">업로드 중...</div>');
|
$('#uploadResult').html('<div class="alert alert-info">업로드 중...</div>');
|
||||||
|
|
||||||
@ -1008,17 +731,9 @@ $(document).ready(function() {
|
|||||||
const select = $('#compoundFormula');
|
const select = $('#compoundFormula');
|
||||||
select.empty().append('<option value="">처방을 선택하세요</option>');
|
select.empty().append('<option value="">처방을 선택하세요</option>');
|
||||||
|
|
||||||
// 직접조제 옵션 추가
|
response.data.forEach(formula => {
|
||||||
select.append('<option value="custom">직접조제</option>');
|
select.append(`<option value="${formula.formula_id}">${formula.formula_name}</option>`);
|
||||||
|
});
|
||||||
// 등록된 처방 추가
|
|
||||||
if (response.data.length > 0) {
|
|
||||||
select.append('<optgroup label="등록된 처방">');
|
|
||||||
response.data.forEach(formula => {
|
|
||||||
select.append(`<option value="${formula.formula_id}">${formula.formula_name}</option>`);
|
|
||||||
});
|
|
||||||
select.append('</optgroup>');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1035,51 +750,6 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 원산지별 재고 옵션 로드
|
|
||||||
function loadOriginOptions(herbId, requiredQty) {
|
|
||||||
$.get(`/api/herbs/${herbId}/available-lots`, function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
const selectElement = $(`tr[data-herb-id="${herbId}"] .origin-select`);
|
|
||||||
selectElement.empty();
|
|
||||||
|
|
||||||
const origins = response.data.origins;
|
|
||||||
|
|
||||||
if (origins.length === 0) {
|
|
||||||
selectElement.append('<option value="">재고 없음</option>');
|
|
||||||
selectElement.prop('disabled', true);
|
|
||||||
$(`tr[data-herb-id="${herbId}"] .stock-status`)
|
|
||||||
.html('<span class="text-danger">재고 없음</span>');
|
|
||||||
} else {
|
|
||||||
selectElement.append('<option value="auto">자동 선택 (저렴한 것부터)</option>');
|
|
||||||
|
|
||||||
origins.forEach(origin => {
|
|
||||||
const stockStatus = origin.total_quantity >= requiredQty ? '' : ' (재고 부족)';
|
|
||||||
const priceInfo = `${formatCurrency(origin.min_price)}/g`;
|
|
||||||
const option = `<option value="${origin.origin_country}"
|
|
||||||
data-price="${origin.min_price}"
|
|
||||||
data-available="${origin.total_quantity}"
|
|
||||||
${origin.total_quantity < requiredQty ? 'disabled' : ''}>
|
|
||||||
${origin.origin_country} - ${priceInfo} (재고: ${origin.total_quantity.toFixed(1)}g)${stockStatus}
|
|
||||||
</option>`;
|
|
||||||
selectElement.append(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
selectElement.prop('disabled', false);
|
|
||||||
|
|
||||||
// 재고 상태 업데이트
|
|
||||||
const totalAvailable = response.data.total_quantity;
|
|
||||||
const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`);
|
|
||||||
|
|
||||||
if (totalAvailable >= requiredQty) {
|
|
||||||
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>`);
|
|
||||||
} else {
|
|
||||||
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCurrency(amount) {
|
function formatCurrency(amount) {
|
||||||
if (amount === null || amount === undefined) return '0원';
|
if (amount === null || amount === undefined) return '0원';
|
||||||
return new Intl.NumberFormat('ko-KR', {
|
return new Intl.NumberFormat('ko-KR', {
|
||||||
|
|||||||
@ -257,8 +257,8 @@
|
|||||||
<th>입고일</th>
|
<th>입고일</th>
|
||||||
<th>공급업체</th>
|
<th>공급업체</th>
|
||||||
<th>품목 수</th>
|
<th>품목 수</th>
|
||||||
<th>총 금액</th>
|
|
||||||
<th>총 수량</th>
|
<th>총 수량</th>
|
||||||
|
<th>총 금액</th>
|
||||||
<th>파일명</th>
|
<th>파일명</th>
|
||||||
<th>작업</th>
|
<th>작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -272,31 +272,19 @@
|
|||||||
|
|
||||||
<!-- Excel 업로드 -->
|
<!-- Excel 업로드 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">새 입고 등록 (Excel 업로드)</h5>
|
<h5 class="mb-0">새 입고 등록 (Excel 업로드)</h5>
|
||||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#supplierModal">
|
|
||||||
<i class="bi bi-plus"></i> 도매상 등록
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="purchaseUploadForm" enctype="multipart/form-data">
|
<form id="purchaseUploadForm" enctype="multipart/form-data">
|
||||||
<div class="row">
|
<div class="mb-3">
|
||||||
<div class="col-md-6">
|
<label for="purchaseFile" class="form-label">입고 Excel 파일 선택</label>
|
||||||
<label for="uploadSupplier" class="form-label">도매상 선택 *</label>
|
<input type="file" class="form-control" id="purchaseFile" accept=".xlsx,.xls" required>
|
||||||
<select class="form-control" id="uploadSupplier" required>
|
<div class="form-text">
|
||||||
<option value="">도매상을 선택하세요</option>
|
지원 형식: 한의사랑, 한의정보 (자동 감지)
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="purchaseFile" class="form-label">Excel 파일 선택 *</label>
|
|
||||||
<input type="file" class="form-control" id="purchaseFile" accept=".xlsx,.xls" required>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text mt-2">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="bi bi-info-circle"></i> Excel 형식: 한의사랑, 한의정보 (자동 감지)<br>
|
|
||||||
<i class="bi bi-info-circle"></i> Excel 내 업체명은 제조사(제약사)로 저장됩니다
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary mt-3">
|
|
||||||
<i class="bi bi-upload"></i> 업로드 및 처리
|
<i class="bi bi-upload"></i> 업로드 및 처리
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -382,7 +370,6 @@
|
|||||||
<th>약재명</th>
|
<th>약재명</th>
|
||||||
<th>1첩당 용량(g)</th>
|
<th>1첩당 용량(g)</th>
|
||||||
<th>총 용량(g)</th>
|
<th>총 용량(g)</th>
|
||||||
<th>원산지 선택</th>
|
|
||||||
<th>재고</th>
|
<th>재고</th>
|
||||||
<th>작업</th>
|
<th>작업</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -612,46 +599,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Supplier Modal -->
|
|
||||||
<div class="modal fade" id="supplierModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">도매상 등록</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="supplierForm">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">도매상명 *</label>
|
|
||||||
<input type="text" class="form-control" id="supplierName" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">사업자번호</label>
|
|
||||||
<input type="text" class="form-control" id="supplierBusinessNo" placeholder="000-00-00000">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">담당자</label>
|
|
||||||
<input type="text" class="form-control" id="supplierContactPerson">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">전화번호</label>
|
|
||||||
<input type="tel" class="form-control" id="supplierPhone">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">주소</label>
|
|
||||||
<input type="text" class="form-control" id="supplierAddress">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="saveSupplierBtn">저장</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user