feat: 판매관리 시스템 Phase 1 및 마일리지 시스템 구현
- 판매 관리 기능 추가 - compounds 테이블에 판매 관련 컬럼 추가 (payment_method, discount_rate, delivery_method 등) - 판매 상태 관리 (조제완료→결제대기→결제완료→배송대기→배송완료) - 판매 처리 모달 UI 구현 - 9가지 상태별 뱃지 표시 - 마일리지 시스템 구축 - patients 테이블에 마일리지 컬럼 추가 (balance, earned, used) - mileage_transactions 테이블 생성 (거래 이력 관리) - 마일리지 사용/적립 기능 구현 - 복합 결제 기능 - 할인율(%) / 할인액(원) 직접 입력 선택 가능 - 마일리지 + 현금 + 카드 + 계좌이체 복합 결제 - 결제 금액 자동 검증 - 결제 방법 자동 분류 (복합결제 지원) - API 엔드포인트 추가 - POST /api/compounds/<id>/status (상태 업데이트) - PUT /api/compounds/<id>/price (가격 조정) - GET /api/sales/statistics (판매 통계) - 데이터베이스 설정 통합 - config.py 생성하여 DB 경로 중앙화 TODO: 처방별 기본가격 정책 시스템 (price_policies 테이블 활용) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ad9ac396e2
commit
f3f1efd8c2
194
add_mileage_system.py
Normal file
194
add_mileage_system.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
마일리지 시스템 구축 - patients 테이블에 마일리지 컬럼 추가 및 이력 테이블 생성
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from config import DATABASE_PATH
|
||||||
|
|
||||||
|
def add_mileage_system():
|
||||||
|
"""환자 테이블에 마일리지 컬럼 추가 및 이력 테이블 생성"""
|
||||||
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 1. patients 테이블에 마일리지 관련 컬럼 추가
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE patients
|
||||||
|
ADD COLUMN mileage_balance INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ mileage_balance 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- mileage_balance 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE patients
|
||||||
|
ADD COLUMN total_mileage_earned INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ total_mileage_earned 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- total_mileage_earned 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE patients
|
||||||
|
ADD COLUMN total_mileage_used INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ total_mileage_used 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- total_mileage_used 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 2. 마일리지 거래 이력 테이블 생성
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS mileage_transactions (
|
||||||
|
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
patient_id INTEGER REFERENCES patients(patient_id),
|
||||||
|
compound_id INTEGER REFERENCES compounds(compound_id),
|
||||||
|
transaction_type TEXT NOT NULL, -- EARNED, USED, EXPIRED, ADMIN_ADJUST
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
balance_after INTEGER NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ mileage_transactions 테이블 생성 완료")
|
||||||
|
|
||||||
|
# 인덱스 생성
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mileage_transactions_patient
|
||||||
|
ON mileage_transactions(patient_id)
|
||||||
|
""")
|
||||||
|
print("✓ 인덱스 생성 완료")
|
||||||
|
|
||||||
|
# 3. compounds 테이블에 마일리지 사용 컬럼 추가
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN mileage_used INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ compounds.mileage_used 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- compounds.mileage_used 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN mileage_earned INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ compounds.mileage_earned 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- compounds.mileage_earned 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 4. 복합 결제를 위한 컬럼 추가
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN payment_cash INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ payment_cash 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- payment_cash 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN payment_card INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ payment_card 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- payment_card 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN payment_transfer INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ payment_transfer 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- payment_transfer 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN discount_amount INTEGER DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ discount_amount 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- discount_amount 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 5. 테스트용 마일리지 데이터 추가 (박주호 회원)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT patient_id, name FROM patients
|
||||||
|
WHERE name LIKE '%박주호%' OR name LIKE '%주호%'
|
||||||
|
""")
|
||||||
|
patients = cursor.fetchall()
|
||||||
|
|
||||||
|
if patients:
|
||||||
|
for patient in patients:
|
||||||
|
print(f"\n박주호 회원 발견: ID={patient[0]}, 이름={patient[1]}")
|
||||||
|
# 초기 마일리지 부여
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE patients
|
||||||
|
SET mileage_balance = 50000,
|
||||||
|
total_mileage_earned = 50000,
|
||||||
|
total_mileage_used = 0
|
||||||
|
WHERE patient_id = ?
|
||||||
|
""", (patient[0],))
|
||||||
|
|
||||||
|
# 마일리지 이력 추가
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO mileage_transactions
|
||||||
|
(patient_id, transaction_type, amount, balance_after, description, created_by)
|
||||||
|
VALUES (?, 'ADMIN_ADJUST', 50000, 50000, '초기 마일리지 부여', 'system')
|
||||||
|
""", (patient[0],))
|
||||||
|
print(f" → 50,000 마일리지 부여 완료")
|
||||||
|
else:
|
||||||
|
print("\n박주호 회원을 찾을 수 없습니다.")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 현재 patients 테이블 구조 확인
|
||||||
|
cursor.execute("PRAGMA table_info(patients)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
print("\n현재 patients 테이블 구조 (마일리지 관련):")
|
||||||
|
for col in columns:
|
||||||
|
if 'mileage' in col[1].lower():
|
||||||
|
print(f" - {col[1]}: {col[2]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\n마일리지 시스템 구축 완료!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
add_mileage_system()
|
||||||
131
add_sales_columns.py
Normal file
131
add_sales_columns.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
판매 관리 시스템 Phase 1 - compounds 테이블에 판매 관련 컬럼 추가
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def add_sales_columns():
|
||||||
|
"""compounds 테이블에 판매 관련 컬럼 추가"""
|
||||||
|
conn = sqlite3.connect('database/kdrug.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. payment_method 컬럼 추가
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN payment_method TEXT
|
||||||
|
""")
|
||||||
|
print("✓ payment_method 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- payment_method 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 2. payment_date 컬럼 추가
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN payment_date DATETIME
|
||||||
|
""")
|
||||||
|
print("✓ payment_date 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- payment_date 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 3. discount_rate 컬럼 추가
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN discount_rate REAL DEFAULT 0
|
||||||
|
""")
|
||||||
|
print("✓ discount_rate 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- discount_rate 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 4. discount_reason 컬럼 추가
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN discount_reason TEXT
|
||||||
|
""")
|
||||||
|
print("✓ discount_reason 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- discount_reason 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 5. delivery_method 컬럼 추가
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN delivery_method TEXT
|
||||||
|
""")
|
||||||
|
print("✓ delivery_method 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- delivery_method 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 6. delivery_date 컬럼 추가
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN delivery_date DATETIME
|
||||||
|
""")
|
||||||
|
print("✓ delivery_date 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- delivery_date 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 7. invoice_number 컬럼 추가
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN invoice_number TEXT
|
||||||
|
""")
|
||||||
|
print("✓ invoice_number 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- invoice_number 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 8. actual_payment_amount 컬럼 추가 (실제 결제 금액)
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE compounds
|
||||||
|
ADD COLUMN actual_payment_amount REAL
|
||||||
|
""")
|
||||||
|
print("✓ actual_payment_amount 컬럼 추가 완료")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("- actual_payment_amount 컬럼 이미 존재")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("\n판매 관련 컬럼 추가 완료!")
|
||||||
|
|
||||||
|
# 현재 compounds 테이블 구조 확인
|
||||||
|
cursor.execute("PRAGMA table_info(compounds)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
print("\n현재 compounds 테이블 구조:")
|
||||||
|
for col in columns:
|
||||||
|
print(f" - {col[1]}: {col[2]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
add_sales_columns()
|
||||||
322
app.py
322
app.py
@ -15,11 +15,12 @@ from werkzeug.utils import secure_filename
|
|||||||
import json
|
import json
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from excel_processor import ExcelProcessor
|
from excel_processor import ExcelProcessor
|
||||||
|
from config import DATABASE_PATH, STATIC_PATH, TEMPLATES_PATH
|
||||||
|
|
||||||
# Flask 앱 초기화
|
# Flask 앱 초기화
|
||||||
app = Flask(__name__, static_folder='static', template_folder='templates')
|
app = Flask(__name__, static_folder='static', template_folder='templates')
|
||||||
app.config['SECRET_KEY'] = 'your-secret-key-change-in-production'
|
app.config['SECRET_KEY'] = 'your-secret-key-change-in-production'
|
||||||
app.config['DATABASE'] = 'database/kdrug.db'
|
app.config['DATABASE'] = str(DATABASE_PATH) # config.py의 경로 사용
|
||||||
app.config['UPLOAD_FOLDER'] = 'uploads'
|
app.config['UPLOAD_FOLDER'] = 'uploads'
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
|
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
|
||||||
|
|
||||||
@ -2735,6 +2736,325 @@ def get_all_efficacy_tags():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
# ==================== 판매 관리 API ====================
|
||||||
|
|
||||||
|
@app.route('/api/compounds/<int:compound_id>/status', methods=['POST'])
|
||||||
|
def update_compound_status(compound_id):
|
||||||
|
"""조제 상태 업데이트"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
new_status = data.get('status')
|
||||||
|
|
||||||
|
valid_statuses = ['PREPARED', 'PENDING_PAYMENT', 'PAID', 'PENDING_DELIVERY',
|
||||||
|
'DELIVERED', 'COMPLETED', 'OTC_CONVERTED', 'CANCELLED', 'REFUNDED']
|
||||||
|
|
||||||
|
if new_status not in valid_statuses:
|
||||||
|
return jsonify({'error': '유효하지 않은 상태입니다'}), 400
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 현재 상태 조회
|
||||||
|
cursor.execute("SELECT status FROM compounds WHERE compound_id = ?", (compound_id,))
|
||||||
|
current = cursor.fetchone()
|
||||||
|
if not current:
|
||||||
|
return jsonify({'error': '조제 정보를 찾을 수 없습니다'}), 404
|
||||||
|
|
||||||
|
old_status = current['status']
|
||||||
|
|
||||||
|
# 상태 업데이트
|
||||||
|
update_fields = ['status = ?']
|
||||||
|
update_values = [new_status]
|
||||||
|
|
||||||
|
# 결제 관련 정보
|
||||||
|
if new_status == 'PAID':
|
||||||
|
if 'payment_method' in data:
|
||||||
|
update_fields.append('payment_method = ?')
|
||||||
|
update_values.append(data['payment_method'])
|
||||||
|
if 'payment_date' in data:
|
||||||
|
update_fields.append('payment_date = ?')
|
||||||
|
update_values.append(data['payment_date'])
|
||||||
|
if 'actual_payment_amount' in data:
|
||||||
|
update_fields.append('actual_payment_amount = ?')
|
||||||
|
update_values.append(data['actual_payment_amount'])
|
||||||
|
|
||||||
|
# 배송 관련 정보
|
||||||
|
elif new_status in ['PENDING_DELIVERY', 'DELIVERED']:
|
||||||
|
if 'delivery_method' in data:
|
||||||
|
update_fields.append('delivery_method = ?')
|
||||||
|
update_values.append(data['delivery_method'])
|
||||||
|
if 'delivery_date' in data:
|
||||||
|
update_fields.append('delivery_date = ?')
|
||||||
|
update_values.append(data['delivery_date'])
|
||||||
|
if 'invoice_number' in data:
|
||||||
|
update_fields.append('invoice_number = ?')
|
||||||
|
update_values.append(data['invoice_number'])
|
||||||
|
|
||||||
|
update_values.append(compound_id)
|
||||||
|
|
||||||
|
cursor.execute(f"""
|
||||||
|
UPDATE compounds
|
||||||
|
SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE compound_id = ?
|
||||||
|
""", update_values)
|
||||||
|
|
||||||
|
# 상태 변경 이력 저장
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO sales_status_history (compound_id, old_status, new_status, changed_by, change_reason)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", (compound_id, old_status, new_status,
|
||||||
|
data.get('changed_by', 'system'), data.get('reason', '')))
|
||||||
|
|
||||||
|
# 판매 거래 기록 (결제 완료시)
|
||||||
|
if new_status == 'PAID' and 'actual_payment_amount' in data:
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO sales_transactions (compound_id, transaction_date, transaction_type,
|
||||||
|
amount, payment_method, payment_status, created_by)
|
||||||
|
VALUES (?, ?, 'SALE', ?, ?, 'COMPLETED', ?)
|
||||||
|
""", (compound_id, data.get('payment_date', datetime.now()),
|
||||||
|
data['actual_payment_amount'], data.get('payment_method'),
|
||||||
|
data.get('changed_by', 'system')))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({'success': True, 'message': '상태가 업데이트되었습니다'})
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""조제 가격 조정 (복합결제 지원)"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 현재 정보 조회
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT sell_price_total, cost_total, patient_id
|
||||||
|
FROM compounds WHERE compound_id = ?
|
||||||
|
""", (compound_id,))
|
||||||
|
current = cursor.fetchone()
|
||||||
|
if not current:
|
||||||
|
return jsonify({'error': '조제 정보를 찾을 수 없습니다'}), 404
|
||||||
|
|
||||||
|
update_fields = []
|
||||||
|
update_values = []
|
||||||
|
|
||||||
|
# 판매가격
|
||||||
|
if 'sell_price_total' in data:
|
||||||
|
update_fields.append('sell_price_total = ?')
|
||||||
|
update_values.append(data['sell_price_total'])
|
||||||
|
|
||||||
|
# 할인 정보
|
||||||
|
if 'discount_rate' in data:
|
||||||
|
update_fields.append('discount_rate = ?')
|
||||||
|
update_values.append(data['discount_rate'])
|
||||||
|
|
||||||
|
if 'discount_amount' in data:
|
||||||
|
update_fields.append('discount_amount = ?')
|
||||||
|
update_values.append(data['discount_amount'])
|
||||||
|
|
||||||
|
if 'discount_reason' in data:
|
||||||
|
update_fields.append('discount_reason = ?')
|
||||||
|
update_values.append(data['discount_reason'])
|
||||||
|
|
||||||
|
# 복합 결제 정보
|
||||||
|
if 'payment_cash' in data:
|
||||||
|
update_fields.append('payment_cash = ?')
|
||||||
|
update_values.append(data['payment_cash'])
|
||||||
|
|
||||||
|
if 'payment_card' in data:
|
||||||
|
update_fields.append('payment_card = ?')
|
||||||
|
update_values.append(data['payment_card'])
|
||||||
|
|
||||||
|
if 'payment_transfer' in data:
|
||||||
|
update_fields.append('payment_transfer = ?')
|
||||||
|
update_values.append(data['payment_transfer'])
|
||||||
|
|
||||||
|
# 마일리지 사용
|
||||||
|
if 'mileage_used' in data and data['mileage_used'] > 0:
|
||||||
|
update_fields.append('mileage_used = ?')
|
||||||
|
update_values.append(data['mileage_used'])
|
||||||
|
|
||||||
|
# 환자 마일리지 차감
|
||||||
|
if current['patient_id']:
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE patients
|
||||||
|
SET mileage_balance = mileage_balance - ?,
|
||||||
|
total_mileage_used = total_mileage_used + ?
|
||||||
|
WHERE patient_id = ?
|
||||||
|
""", (data['mileage_used'], data['mileage_used'], current['patient_id']))
|
||||||
|
|
||||||
|
# 마일리지 거래 이력 추가
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO mileage_transactions
|
||||||
|
(patient_id, compound_id, transaction_type, amount, balance_after, description, created_by)
|
||||||
|
SELECT patient_id, ?, 'USED', ?, mileage_balance, '한약 결제 사용', 'system'
|
||||||
|
FROM patients WHERE patient_id = ?
|
||||||
|
""", (compound_id, data['mileage_used'], current['patient_id']))
|
||||||
|
|
||||||
|
# 실제 결제 금액 계산
|
||||||
|
total_payment = (data.get('mileage_used', 0) +
|
||||||
|
data.get('payment_cash', 0) +
|
||||||
|
data.get('payment_card', 0) +
|
||||||
|
data.get('payment_transfer', 0))
|
||||||
|
|
||||||
|
if total_payment > 0:
|
||||||
|
update_fields.append('actual_payment_amount = ?')
|
||||||
|
update_values.append(total_payment)
|
||||||
|
|
||||||
|
update_values.append(compound_id)
|
||||||
|
|
||||||
|
if update_fields:
|
||||||
|
cursor.execute(f"""
|
||||||
|
UPDATE compounds
|
||||||
|
SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE compound_id = ?
|
||||||
|
""", update_values)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({'success': True, 'message': '가격이 조정되었습니다'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/sales/statistics', methods=['GET'])
|
||||||
|
def get_sales_statistics():
|
||||||
|
"""판매 통계 조회"""
|
||||||
|
try:
|
||||||
|
start_date = request.args.get('start_date')
|
||||||
|
end_date = request.args.get('end_date')
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 기간 내 매출 통계
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_count,
|
||||||
|
SUM(COALESCE(actual_payment_amount, sell_price_total)) as total_sales,
|
||||||
|
AVG(COALESCE(actual_payment_amount, sell_price_total)) as avg_price,
|
||||||
|
COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END) as completed_count,
|
||||||
|
COUNT(CASE WHEN status = 'CANCELLED' THEN 1 END) as cancelled_count,
|
||||||
|
COUNT(CASE WHEN status = 'REFUNDED' THEN 1 END) as refunded_count
|
||||||
|
FROM compounds
|
||||||
|
WHERE status IN ('PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED')
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = []
|
||||||
|
if start_date:
|
||||||
|
query += " AND compound_date >= ?"
|
||||||
|
params.append(start_date)
|
||||||
|
if end_date:
|
||||||
|
query += " AND compound_date <= ?"
|
||||||
|
params.append(end_date)
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
stats = cursor.fetchone()
|
||||||
|
|
||||||
|
# 일별 매출 추이
|
||||||
|
daily_query = """
|
||||||
|
SELECT
|
||||||
|
DATE(compound_date) as sale_date,
|
||||||
|
COUNT(*) as count,
|
||||||
|
SUM(COALESCE(actual_payment_amount, sell_price_total)) as daily_total
|
||||||
|
FROM compounds
|
||||||
|
WHERE status IN ('PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED')
|
||||||
|
"""
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
daily_query += " AND compound_date >= ?"
|
||||||
|
if end_date:
|
||||||
|
daily_query += " AND compound_date <= ?"
|
||||||
|
|
||||||
|
daily_query += " GROUP BY DATE(compound_date) ORDER BY sale_date"
|
||||||
|
|
||||||
|
cursor.execute(daily_query, params)
|
||||||
|
daily_sales = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
daily_sales.append({
|
||||||
|
'date': row['sale_date'],
|
||||||
|
'count': row['count'],
|
||||||
|
'total': row['daily_total']
|
||||||
|
})
|
||||||
|
|
||||||
|
# 처방별 매출
|
||||||
|
formula_query = """
|
||||||
|
SELECT
|
||||||
|
f.formula_name,
|
||||||
|
COUNT(*) as count,
|
||||||
|
SUM(COALESCE(c.actual_payment_amount, c.sell_price_total)) as total
|
||||||
|
FROM compounds c
|
||||||
|
LEFT JOIN formulas f ON c.formula_id = f.formula_id
|
||||||
|
WHERE c.status IN ('PAID', 'PENDING_DELIVERY', 'DELIVERED', 'COMPLETED')
|
||||||
|
"""
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
formula_query += " AND c.compound_date >= ?"
|
||||||
|
if end_date:
|
||||||
|
formula_query += " AND c.compound_date <= ?"
|
||||||
|
|
||||||
|
formula_query += " GROUP BY f.formula_name ORDER BY total DESC LIMIT 10"
|
||||||
|
|
||||||
|
cursor.execute(formula_query, params)
|
||||||
|
formula_sales = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
formula_sales.append({
|
||||||
|
'formula_name': row['formula_name'] or '직접조제',
|
||||||
|
'count': row['count'],
|
||||||
|
'total': row['total']
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'summary': {
|
||||||
|
'total_count': stats['total_count'],
|
||||||
|
'total_sales': stats['total_sales'],
|
||||||
|
'avg_price': stats['avg_price'],
|
||||||
|
'completed_count': stats['completed_count'],
|
||||||
|
'cancelled_count': stats['cancelled_count'],
|
||||||
|
'refunded_count': stats['refunded_count']
|
||||||
|
},
|
||||||
|
'daily_sales': daily_sales,
|
||||||
|
'formula_sales': formula_sales
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/compounds/<int:compound_id>/convert-to-otc', methods=['POST'])
|
||||||
|
def convert_to_otc(compound_id):
|
||||||
|
"""OTC 전환"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 조제 정보 업데이트
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE compounds
|
||||||
|
SET status = 'OTC_CONVERTED',
|
||||||
|
notes = COALESCE(notes || ' | ', '') || 'OTC 전환: ' || ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE compound_id = ?
|
||||||
|
""", (data.get('reason', ''), compound_id))
|
||||||
|
|
||||||
|
# 상태 변경 이력
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO sales_status_history (compound_id, old_status, new_status,
|
||||||
|
changed_by, change_reason)
|
||||||
|
SELECT compound_id, status, 'OTC_CONVERTED', ?, ?
|
||||||
|
FROM compounds WHERE compound_id = ?
|
||||||
|
""", (data.get('changed_by', 'system'), data.get('reason', ''), compound_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({'success': True, 'message': 'OTC로 전환되었습니다'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 데이터베이스 초기화
|
# 데이터베이스 초기화
|
||||||
if not os.path.exists(app.config['DATABASE']):
|
if not os.path.exists(app.config['DATABASE']):
|
||||||
|
|||||||
102
check_compound_detail.py
Normal file
102
check_compound_detail.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
조제 데이터 상세 확인 - formula_id가 NULL인 케이스 분석
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def check_compound_details():
|
||||||
|
conn = sqlite3.connect('database/kdrug.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("조제 데이터 상세 분석")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# formula_id가 NULL인 조제건 확인
|
||||||
|
print("\n1. Formula ID가 NULL인 조제건:")
|
||||||
|
print("-" * 80)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT compound_id, patient_id, formula_id, is_custom, custom_type,
|
||||||
|
custom_summary, compound_date, status
|
||||||
|
FROM compounds
|
||||||
|
WHERE formula_id IS NULL
|
||||||
|
ORDER BY compound_id DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
null_formulas = cursor.fetchall()
|
||||||
|
for comp in null_formulas:
|
||||||
|
print(f" ID: {comp[0]}")
|
||||||
|
print(f" 환자ID: {comp[1]}, is_custom: {comp[3]}, custom_type: {comp[4]}")
|
||||||
|
print(f" custom_summary: {comp[5]}")
|
||||||
|
print(f" 조제일: {comp[6]}, 상태: {comp[7]}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 전체 조제 데이터 요약
|
||||||
|
print("\n2. 전체 조제 데이터 요약:")
|
||||||
|
print("-" * 80)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
COUNT(formula_id) as with_formula,
|
||||||
|
COUNT(*) - COUNT(formula_id) as without_formula,
|
||||||
|
SUM(CASE WHEN is_custom = 1 THEN 1 ELSE 0 END) as custom_count
|
||||||
|
FROM compounds
|
||||||
|
""")
|
||||||
|
|
||||||
|
summary = cursor.fetchone()
|
||||||
|
print(f" 총 조제건수: {summary[0]}")
|
||||||
|
print(f" 처방 있음: {summary[1]}")
|
||||||
|
print(f" 처방 없음(직접조제): {summary[2]}")
|
||||||
|
print(f" 커스텀 플래그: {summary[3]}")
|
||||||
|
|
||||||
|
# compound_items 테이블 확인
|
||||||
|
print("\n3. Compound_items 테이블 구조 및 데이터:")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
# 테이블 구조 확인
|
||||||
|
cursor.execute("PRAGMA table_info(compound_items)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
if columns:
|
||||||
|
print(" 테이블 구조:")
|
||||||
|
for col in columns:
|
||||||
|
print(f" {col[1]:20s} {col[2]:15s}")
|
||||||
|
|
||||||
|
# 샘플 데이터
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT * FROM compound_items
|
||||||
|
LIMIT 5
|
||||||
|
""")
|
||||||
|
items = cursor.fetchall()
|
||||||
|
if items:
|
||||||
|
print("\n 샘플 데이터:")
|
||||||
|
for item in items:
|
||||||
|
print(f" {item}")
|
||||||
|
else:
|
||||||
|
print(" compound_items 테이블이 비어있거나 없습니다.")
|
||||||
|
|
||||||
|
# 직접조제의 약재 구성 확인 (compound_items와 연결)
|
||||||
|
print("\n4. 직접조제(formula_id=NULL) 약재 구성:")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
for comp_id in [10, 8, 7]: # NULL인 compound_id들
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT ci.*, h.herb_name
|
||||||
|
FROM compound_items ci
|
||||||
|
LEFT JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
|
||||||
|
WHERE ci.compound_id = ?
|
||||||
|
LIMIT 5
|
||||||
|
""", (comp_id,))
|
||||||
|
|
||||||
|
items = cursor.fetchall()
|
||||||
|
if items:
|
||||||
|
print(f" Compound ID {comp_id}의 약재:")
|
||||||
|
for item in items:
|
||||||
|
print(f" {item}")
|
||||||
|
else:
|
||||||
|
print(f" Compound ID {comp_id}: 약재 데이터 없음")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_compound_details()
|
||||||
103
check_compound_ingredients.py
Normal file
103
check_compound_ingredients.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
compound_ingredients 테이블 확인 - 직접조제 데이터 분석
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def analyze_compound_ingredients():
|
||||||
|
conn = sqlite3.connect('database/kdrug.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("Compound_ingredients 테이블 분석")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# 테이블 구조
|
||||||
|
print("\n1. 테이블 구조:")
|
||||||
|
print("-" * 80)
|
||||||
|
cursor.execute("PRAGMA table_info(compound_ingredients)")
|
||||||
|
for col in cursor.fetchall():
|
||||||
|
print(f" {col[1]:25s} {col[2]:15s} {'NOT NULL' if col[3] else 'NULL':10s}")
|
||||||
|
|
||||||
|
# formula_id가 NULL인 compound의 약재 구성
|
||||||
|
print("\n2. Formula_id가 NULL인 조제건의 약재 구성:")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
c.compound_id,
|
||||||
|
c.formula_id,
|
||||||
|
c.is_custom,
|
||||||
|
c.custom_type,
|
||||||
|
COUNT(ci.compound_ingredient_id) as ingredient_count,
|
||||||
|
GROUP_CONCAT(h.herb_name || ':' || ci.grams_per_cheop || 'g', ', ') as ingredients
|
||||||
|
FROM compounds c
|
||||||
|
LEFT JOIN compound_ingredients ci ON c.compound_id = ci.compound_id
|
||||||
|
LEFT JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
|
||||||
|
WHERE c.formula_id IS NULL
|
||||||
|
GROUP BY c.compound_id
|
||||||
|
ORDER BY c.compound_id DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
results = cursor.fetchall()
|
||||||
|
for row in results:
|
||||||
|
print(f"\n Compound ID: {row[0]}")
|
||||||
|
print(f" Formula ID: {row[1]}, is_custom: {row[2]}, custom_type: {row[3]}")
|
||||||
|
print(f" 약재 수: {row[4]}개")
|
||||||
|
print(f" 구성: {row[5]}")
|
||||||
|
|
||||||
|
# 전체 통계
|
||||||
|
print("\n3. 전체 조제 통계:")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
c.formula_id IS NOT NULL as has_formula,
|
||||||
|
c.is_custom,
|
||||||
|
COUNT(DISTINCT c.compound_id) as compound_count,
|
||||||
|
COUNT(ci.compound_ingredient_id) as total_ingredients
|
||||||
|
FROM compounds c
|
||||||
|
LEFT JOIN compound_ingredients ci ON c.compound_id = ci.compound_id
|
||||||
|
GROUP BY has_formula, is_custom
|
||||||
|
""")
|
||||||
|
|
||||||
|
print(f" {'처방있음':10s} {'커스텀':8s} {'조제수':8s} {'총약재수':10s}")
|
||||||
|
print(" " + "-" * 40)
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
has_formula = "예" if row[0] else "아니오"
|
||||||
|
is_custom = "예" if row[1] else "아니오"
|
||||||
|
print(f" {has_formula:10s} {is_custom:8s} {row[2]:8d} {row[3]:10d}")
|
||||||
|
|
||||||
|
# 특정 조제건 상세
|
||||||
|
print("\n4. Compound ID 10번 상세 (formula_id=NULL):")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
ci.herb_item_id,
|
||||||
|
h.herb_name,
|
||||||
|
ci.grams_per_cheop,
|
||||||
|
c.cheop_total,
|
||||||
|
ci.total_grams,
|
||||||
|
ci.notes
|
||||||
|
FROM compound_ingredients ci
|
||||||
|
LEFT JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
|
||||||
|
LEFT JOIN compounds c ON ci.compound_id = c.compound_id
|
||||||
|
WHERE ci.compound_id = 10
|
||||||
|
ORDER BY ci.compound_ingredient_id
|
||||||
|
""")
|
||||||
|
|
||||||
|
ingredients = cursor.fetchall()
|
||||||
|
if ingredients:
|
||||||
|
print(f" {'약재코드':15s} {'약재명':15s} {'1첩용량':10s} {'첩수':8s} {'총용량':10s}")
|
||||||
|
print(" " + "-" * 60)
|
||||||
|
for ing in ingredients:
|
||||||
|
print(f" {ing[0]:15s} {ing[1] or 'Unknown':15s} {ing[2]:10.1f}g {ing[3]:8.0f} {ing[4]:10.1f}g")
|
||||||
|
else:
|
||||||
|
print(" 약재 데이터 없음")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
analyze_compound_ingredients()
|
||||||
65
check_compound_schema.py
Normal file
65
check_compound_schema.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
조제(compound) 관련 테이블 스키마 확인
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def check_compound_tables():
|
||||||
|
conn = sqlite3.connect('database/kdrug.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# compounds 테이블 구조 확인
|
||||||
|
print("=" * 80)
|
||||||
|
print("1. COMPOUNDS 테이블 구조:")
|
||||||
|
print("=" * 80)
|
||||||
|
cursor.execute("PRAGMA table_info(compounds)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
for col in columns:
|
||||||
|
print(f" {col[1]:25s} {col[2]:15s} {'NOT NULL' if col[3] else 'NULL':10s} {col[4] or ''}")
|
||||||
|
|
||||||
|
# compound_items 테이블 구조 확인
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("2. COMPOUND_ITEMS 테이블 구조:")
|
||||||
|
print("=" * 80)
|
||||||
|
cursor.execute("PRAGMA table_info(compound_items)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
for col in columns:
|
||||||
|
print(f" {col[1]:25s} {col[2]:15s} {'NOT NULL' if col[3] else 'NULL':10s} {col[4] or ''}")
|
||||||
|
|
||||||
|
# 관련 테이블 확인
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("3. 관련 테이블 목록:")
|
||||||
|
print("=" * 80)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type='table' AND (name LIKE '%sale%' OR name LIKE '%payment%' OR name LIKE '%invoice%')
|
||||||
|
ORDER BY name
|
||||||
|
""")
|
||||||
|
related_tables = cursor.fetchall()
|
||||||
|
if related_tables:
|
||||||
|
for table in related_tables:
|
||||||
|
print(f" - {table[0]}")
|
||||||
|
else:
|
||||||
|
print(" 판매/결제 관련 테이블 없음")
|
||||||
|
|
||||||
|
# 샘플 데이터 확인
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("4. 최근 조제 데이터 샘플:")
|
||||||
|
print("=" * 80)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT c.compound_id, p.name, f.formula_name, c.compound_date, c.created_at, c.status
|
||||||
|
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
|
||||||
|
ORDER BY c.created_at DESC
|
||||||
|
LIMIT 5
|
||||||
|
""")
|
||||||
|
compounds = cursor.fetchall()
|
||||||
|
for comp in compounds:
|
||||||
|
print(f" ID:{comp[0]} | 환자:{comp[1]} | 처방:{comp[2]} | 조제일:{comp[3]} | 상태:{comp[5]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
check_compound_tables()
|
||||||
42
config.py
Normal file
42
config.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
kdrug 프로젝트 공통 설정 파일
|
||||||
|
모든 Python 스크립트에서 이 설정을 import하여 사용
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 프로젝트 루트 디렉토리
|
||||||
|
PROJECT_ROOT = Path(__file__).parent
|
||||||
|
|
||||||
|
# 데이터베이스 경로 - 항상 절대 경로 사용
|
||||||
|
DATABASE_PATH = PROJECT_ROOT / 'database' / 'kdrug.db'
|
||||||
|
|
||||||
|
# 기타 자주 사용하는 경로들
|
||||||
|
STATIC_PATH = PROJECT_ROOT / 'static'
|
||||||
|
TEMPLATES_PATH = PROJECT_ROOT / 'templates'
|
||||||
|
DOCS_PATH = PROJECT_ROOT / 'docs'
|
||||||
|
BACKUP_PATH = PROJECT_ROOT / 'backups'
|
||||||
|
|
||||||
|
# 데이터베이스 연결 헬퍼 함수
|
||||||
|
def get_db_connection():
|
||||||
|
"""표준 데이터베이스 연결 반환"""
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect(str(DATABASE_PATH))
|
||||||
|
conn.row_factory = sqlite3.Row # 컬럼명으로 접근 가능하도록 설정
|
||||||
|
return conn
|
||||||
|
|
||||||
|
# 설정 확인용 (디버그)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"프로젝트 루트: {PROJECT_ROOT}")
|
||||||
|
print(f"데이터베이스 경로: {DATABASE_PATH}")
|
||||||
|
print(f"데이터베이스 존재: {DATABASE_PATH.exists()}")
|
||||||
|
|
||||||
|
if DATABASE_PATH.exists():
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'")
|
||||||
|
table_count = cursor.fetchone()[0]
|
||||||
|
print(f"테이블 개수: {table_count}")
|
||||||
|
conn.close()
|
||||||
94
create_sales_tables.py
Normal file
94
create_sales_tables.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
판매 관리 시스템 - 신규 테이블 생성
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def create_sales_tables():
|
||||||
|
"""판매 관련 신규 테이블 생성"""
|
||||||
|
conn = sqlite3.connect('database/kdrug.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 1. sales_transactions 테이블 생성
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS sales_transactions (
|
||||||
|
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
compound_id INTEGER REFERENCES compounds(compound_id),
|
||||||
|
transaction_date DATETIME NOT NULL,
|
||||||
|
transaction_type TEXT NOT NULL, -- SALE, REFUND, CANCEL
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
payment_method TEXT,
|
||||||
|
payment_status TEXT, -- PENDING, COMPLETED, FAILED
|
||||||
|
notes TEXT,
|
||||||
|
created_by TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ sales_transactions 테이블 생성 완료")
|
||||||
|
|
||||||
|
# 2. price_policies 테이블 생성
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS price_policies (
|
||||||
|
policy_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
formula_id INTEGER REFERENCES formulas(formula_id),
|
||||||
|
base_price REAL NOT NULL,
|
||||||
|
dispensing_fee REAL DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT 1,
|
||||||
|
effective_date DATE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ price_policies 테이블 생성 완료")
|
||||||
|
|
||||||
|
# 3. sales_status_history 테이블 생성
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS sales_status_history (
|
||||||
|
history_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
compound_id INTEGER REFERENCES compounds(compound_id),
|
||||||
|
old_status TEXT,
|
||||||
|
new_status TEXT NOT NULL,
|
||||||
|
changed_by TEXT,
|
||||||
|
change_reason TEXT,
|
||||||
|
changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ sales_status_history 테이블 생성 완료")
|
||||||
|
|
||||||
|
# 인덱스 생성
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sales_transactions_compound
|
||||||
|
ON sales_transactions(compound_id)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_price_policies_formula
|
||||||
|
ON price_policies(formula_id)
|
||||||
|
""")
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sales_status_history_compound
|
||||||
|
ON sales_status_history(compound_id)
|
||||||
|
""")
|
||||||
|
print("✓ 인덱스 생성 완료")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 생성된 테이블 확인
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type='table'
|
||||||
|
AND name IN ('sales_transactions', 'price_policies', 'sales_status_history')
|
||||||
|
""")
|
||||||
|
tables = cursor.fetchall()
|
||||||
|
print("\n생성된 테이블:")
|
||||||
|
for table in tables:
|
||||||
|
print(f" - {table[0]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\n판매 관련 테이블 생성 완료!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_sales_tables()
|
||||||
317
docs/직접조제_고도화_기획문서.md
Normal file
317
docs/직접조제_고도화_기획문서.md
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
# 직접조제(Custom Compound) 고도화 기획 문서
|
||||||
|
|
||||||
|
## 1. 현황 분석
|
||||||
|
|
||||||
|
### 1.1 현재 직접조제 데이터 구조
|
||||||
|
|
||||||
|
#### 실제 데이터 사례
|
||||||
|
```
|
||||||
|
Compound ID 10: 휴먼건강 3.0g (단품)
|
||||||
|
Compound ID 8: 휴먼건강 2.0g (단품)
|
||||||
|
Compound ID 7: 휴먼일당귀 100.0g (단품)
|
||||||
|
Compound ID 6: 휴먼일당귀 100.0g (단품)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 현재 저장 방식
|
||||||
|
- formula_id: NULL
|
||||||
|
- is_custom: 0 (잘못된 설정)
|
||||||
|
- custom_type: 'standard' (잘못된 설정)
|
||||||
|
- custom_summary: NULL
|
||||||
|
- 약재 정보: compound_ingredients에 단일 약재로 저장
|
||||||
|
|
||||||
|
### 1.2 문제점
|
||||||
|
|
||||||
|
1. **분류 체계 미비**
|
||||||
|
- 직접조제임에도 is_custom=0으로 저장
|
||||||
|
- custom_type이 'standard'로 잘못 설정
|
||||||
|
- 표준처방과 직접조제 구분 불명확
|
||||||
|
|
||||||
|
2. **데이터 무결성**
|
||||||
|
- formula_id NULL이지만 custom 플래그 미설정
|
||||||
|
- 직접조제 사유/목적 미기록
|
||||||
|
- 처방명 없음 (custom_summary 미사용)
|
||||||
|
|
||||||
|
3. **관리 기능 부족**
|
||||||
|
- 직접조제 이력 추적 어려움
|
||||||
|
- 가격 책정 기준 불명확
|
||||||
|
- 재조제시 참조 데이터 부재
|
||||||
|
|
||||||
|
## 2. 직접조제 유형 분류
|
||||||
|
|
||||||
|
### 2.1 단품 판매 (Single Item)
|
||||||
|
- **특징**: 단일 약재 판매
|
||||||
|
- **사례**: 녹용 100g, 홍삼 50g
|
||||||
|
- **용도**: 환자 요청에 의한 약재 구매
|
||||||
|
|
||||||
|
### 2.2 맞춤 조제 (Custom Formula)
|
||||||
|
- **특징**: 여러 약재 조합하여 맞춤 처방
|
||||||
|
- **사례**: 기존 처방 가감방, 한약사 임의 조제
|
||||||
|
- **용도**: 환자 체질/증상에 맞춘 개별화 처방
|
||||||
|
|
||||||
|
### 2.3 OTC 조제 (Over The Counter)
|
||||||
|
- **특징**: 처방전 없이 판매 가능한 제품
|
||||||
|
- **사례**: 쌍화탕 파우치, 공진단
|
||||||
|
- **용도**: 일반 판매용 제품
|
||||||
|
|
||||||
|
### 2.4 테스트/샘플 (Test/Sample)
|
||||||
|
- **특징**: 시음용, 테스트용 소량 조제
|
||||||
|
- **사례**: 처방 샘플 1일분
|
||||||
|
- **용도**: 환자 시음, 품질 테스트
|
||||||
|
|
||||||
|
## 3. 개선 방안
|
||||||
|
|
||||||
|
### 3.1 데이터 구조 개선
|
||||||
|
|
||||||
|
#### 3.1.1 compounds 테이블 수정
|
||||||
|
```sql
|
||||||
|
-- 직접조제 분류 강화
|
||||||
|
ALTER TABLE compounds ADD COLUMN custom_category TEXT;
|
||||||
|
-- 'SINGLE_ITEM', 'CUSTOM_FORMULA', 'OTC', 'SAMPLE'
|
||||||
|
|
||||||
|
ALTER TABLE compounds ADD COLUMN custom_name TEXT;
|
||||||
|
-- 직접조제명 (예: '감기 맞춤처방', '홍삼 단품')
|
||||||
|
|
||||||
|
ALTER TABLE compounds ADD COLUMN custom_purpose TEXT;
|
||||||
|
-- 조제 목적/사유
|
||||||
|
|
||||||
|
ALTER TABLE compounds ADD COLUMN reference_compound_id INTEGER;
|
||||||
|
-- 재조제시 참조할 이전 조제 ID
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.1.2 custom_formulas 테이블 신규
|
||||||
|
```sql
|
||||||
|
CREATE TABLE custom_formulas (
|
||||||
|
custom_formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
compound_id INTEGER REFERENCES compounds(compound_id),
|
||||||
|
formula_name TEXT NOT NULL,
|
||||||
|
formula_description TEXT,
|
||||||
|
base_formula_id INTEGER REFERENCES formulas(formula_id),
|
||||||
|
-- 기본이 된 표준처방 (가감방인 경우)
|
||||||
|
|
||||||
|
modification_summary TEXT,
|
||||||
|
-- 가감 내용 요약
|
||||||
|
|
||||||
|
symptoms TEXT,
|
||||||
|
-- 대상 증상
|
||||||
|
|
||||||
|
contraindications TEXT,
|
||||||
|
-- 금기사항
|
||||||
|
|
||||||
|
created_by TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
is_reusable BOOLEAN DEFAULT 0,
|
||||||
|
-- 재사용 가능 여부 (다른 환자에게도 적용 가능)
|
||||||
|
|
||||||
|
reuse_count INTEGER DEFAULT 0
|
||||||
|
-- 재사용 횟수
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 비즈니스 로직 개선
|
||||||
|
|
||||||
|
#### 3.2.1 직접조제 생성 프로세스
|
||||||
|
```
|
||||||
|
1. 조제 유형 선택
|
||||||
|
└─ 단품/맞춤/OTC/샘플
|
||||||
|
|
||||||
|
2. 유형별 정보 입력
|
||||||
|
├─ 단품: 약재, 용량, 판매사유
|
||||||
|
├─ 맞춤: 처방명, 약재구성, 증상, 기반처방
|
||||||
|
├─ OTC: 제품명, 수량
|
||||||
|
└─ 샘플: 목적, 대상처방, 용량
|
||||||
|
|
||||||
|
3. 가격 책정
|
||||||
|
├─ 자동계산: 약재원가 + 조제료
|
||||||
|
└─ 수동입력: 특별가 적용
|
||||||
|
|
||||||
|
4. 데이터 저장
|
||||||
|
├─ compounds: is_custom=1, custom_category 설정
|
||||||
|
├─ compound_ingredients: 약재 구성
|
||||||
|
└─ custom_formulas: 맞춤처방 상세정보
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.2 직접조제 관리 기능
|
||||||
|
1. **템플릿 관리**
|
||||||
|
- 자주 사용하는 직접조제 템플릿 저장
|
||||||
|
- 템플릿에서 빠른 조제 생성
|
||||||
|
|
||||||
|
2. **이력 관리**
|
||||||
|
- 환자별 직접조제 이력 조회
|
||||||
|
- 동일 처방 재조제 기능
|
||||||
|
|
||||||
|
3. **가격 정책**
|
||||||
|
- 직접조제 유형별 조제료 설정
|
||||||
|
- 단품 판매 마진율 관리
|
||||||
|
|
||||||
|
### 3.3 UI/UX 개선
|
||||||
|
|
||||||
|
#### 3.3.1 직접조제 입력 화면
|
||||||
|
```
|
||||||
|
┌─────────────── 직접조제 등록 ──────────────┐
|
||||||
|
│ │
|
||||||
|
│ 조제 유형: [단품 판매 ▼] │
|
||||||
|
│ │
|
||||||
|
│ ─────── 기본 정보 ───────── │
|
||||||
|
│ 조제명: [________________] │
|
||||||
|
│ 환자: [환자선택 ▼] 또는 [비회원] │
|
||||||
|
│ │
|
||||||
|
│ ─────── 약재 구성 ───────── │
|
||||||
|
│ [+ 약재 추가] │
|
||||||
|
│ ┌────────┬──────┬──────┬────┐ │
|
||||||
|
│ │약재명 │용량 │단가 │삭제 │ │
|
||||||
|
│ ├────────┼──────┼──────┼────┤ │
|
||||||
|
│ │홍삼 │100g │500원/g│ X │ │
|
||||||
|
│ └────────┴──────┴──────┴────┘ │
|
||||||
|
│ │
|
||||||
|
│ ─────── 가격 정보 ───────── │
|
||||||
|
│ 약재비: 50,000원 │
|
||||||
|
│ 조제료: [10,000원] │
|
||||||
|
│ 판매가: [60,000원] │
|
||||||
|
│ │
|
||||||
|
│ 조제 목적: [________________] │
|
||||||
|
│ 비고: [____________________] │
|
||||||
|
│ │
|
||||||
|
│ [템플릿 저장] [취소] [조제 등록] │
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3.2 직접조제 목록 화면
|
||||||
|
```
|
||||||
|
[직접조제 관리]
|
||||||
|
|
||||||
|
필터: [전체 ▼] [2024-02-01] ~ [2024-02-29] [검색]
|
||||||
|
|
||||||
|
┌────┬──────┬────────┬──────┬──────┬────────┬──────┐
|
||||||
|
│번호│유형 │조제명 │환자 │약재수 │판매가 │작업 │
|
||||||
|
├────┼──────┼────────┼──────┼──────┼────────┼──────┤
|
||||||
|
│ 1 │단품 │홍삼100g │홍길동 │ 1 │ 60,000 │[상세] │
|
||||||
|
│ 2 │맞춤 │감기처방 │김철수 │ 8 │120,000 │[재조제]│
|
||||||
|
│ 3 │OTC │공진단3환 │비회원 │ 3 │ 90,000 │[상세] │
|
||||||
|
│ 4 │샘플 │시음1일분 │이영희 │ 5 │ 0 │[상세] │
|
||||||
|
└────┴──────┴────────┴──────┴──────┴────────┴──────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 데이터 마이그레이션
|
||||||
|
|
||||||
|
#### 3.4.1 기존 데이터 정리
|
||||||
|
```sql
|
||||||
|
-- formula_id가 NULL인 조제 데이터 업데이트
|
||||||
|
UPDATE compounds
|
||||||
|
SET is_custom = 1,
|
||||||
|
custom_type = 'custom',
|
||||||
|
custom_category = 'SINGLE_ITEM',
|
||||||
|
custom_name = (
|
||||||
|
SELECT h.herb_name || ' ' || ci.total_grams || 'g'
|
||||||
|
FROM compound_ingredients ci
|
||||||
|
JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
|
||||||
|
WHERE ci.compound_id = compounds.compound_id
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE formula_id IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.4.2 데이터 검증
|
||||||
|
- 모든 직접조제 데이터의 무결성 확인
|
||||||
|
- is_custom 플래그와 formula_id NULL 일치 여부 검증
|
||||||
|
- custom_category 분류 정확성 확인
|
||||||
|
|
||||||
|
## 4. API 설계
|
||||||
|
|
||||||
|
### 4.1 직접조제 API
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 직접조제 생성
|
||||||
|
POST /api/compounds/custom
|
||||||
|
{
|
||||||
|
"patient_id": 1, # nullable for OTC
|
||||||
|
"custom_category": "SINGLE_ITEM",
|
||||||
|
"custom_name": "홍삼 100g",
|
||||||
|
"custom_purpose": "면역력 증진",
|
||||||
|
"ingredients": [
|
||||||
|
{
|
||||||
|
"herb_item_id": 123,
|
||||||
|
"grams": 100,
|
||||||
|
"unit_price": 500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dispensing_fee": 10000,
|
||||||
|
"sell_price": 60000
|
||||||
|
}
|
||||||
|
|
||||||
|
# 직접조제 템플릿 저장
|
||||||
|
POST /api/custom-templates
|
||||||
|
{
|
||||||
|
"template_name": "감기 기본방",
|
||||||
|
"category": "CUSTOM_FORMULA",
|
||||||
|
"ingredients": [...],
|
||||||
|
"default_price": 80000
|
||||||
|
}
|
||||||
|
|
||||||
|
# 직접조제 이력 조회
|
||||||
|
GET /api/compounds/custom?patient_id=1&category=SINGLE_ITEM
|
||||||
|
|
||||||
|
# 재조제
|
||||||
|
POST /api/compounds/{compound_id}/reorder
|
||||||
|
{
|
||||||
|
"patient_id": 1,
|
||||||
|
"quantity_multiplier": 1.5 # 1.5배 용량
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 구현 로드맵
|
||||||
|
|
||||||
|
### Phase 1: 기반 구축 (1주)
|
||||||
|
- [ ] DB 스키마 수정 (custom_category, custom_name 등 추가)
|
||||||
|
- [ ] 기존 데이터 마이그레이션
|
||||||
|
- [ ] 직접조제 분류 체계 구현
|
||||||
|
|
||||||
|
### Phase 2: 핵심 기능 (2주)
|
||||||
|
- [ ] 직접조제 생성 UI/API
|
||||||
|
- [ ] 유형별 입력 폼 구현
|
||||||
|
- [ ] 가격 자동계산 로직
|
||||||
|
|
||||||
|
### Phase 3: 관리 기능 (1주)
|
||||||
|
- [ ] 직접조제 목록/검색
|
||||||
|
- [ ] 템플릿 관리
|
||||||
|
- [ ] 재조제 기능
|
||||||
|
|
||||||
|
### Phase 4: 고도화 (2주)
|
||||||
|
- [ ] 통계 및 리포트
|
||||||
|
- [ ] 환자별 구매 패턴 분석
|
||||||
|
- [ ] 인기 직접조제 랭킹
|
||||||
|
|
||||||
|
## 6. 주의사항
|
||||||
|
|
||||||
|
### 6.1 규제 준수
|
||||||
|
- 의약품 판매 관련 법규 확인
|
||||||
|
- 처방전 필요 여부 명확히 구분
|
||||||
|
- OTC 판매 가능 품목 관리
|
||||||
|
|
||||||
|
### 6.2 데이터 정합성
|
||||||
|
- formula_id NULL과 is_custom=1 일치 유지
|
||||||
|
- 직접조제는 반드시 custom_category 설정
|
||||||
|
- 가격 정보 필수 입력
|
||||||
|
|
||||||
|
### 6.3 사용자 교육
|
||||||
|
- 직접조제 유형별 사용 가이드
|
||||||
|
- 가격 책정 기준 안내
|
||||||
|
- 템플릿 활용 방법
|
||||||
|
|
||||||
|
## 7. 기대 효과
|
||||||
|
|
||||||
|
1. **체계적 관리**
|
||||||
|
- 직접조제 데이터 일관성 확보
|
||||||
|
- 판매 이력 추적 가능
|
||||||
|
|
||||||
|
2. **업무 효율성**
|
||||||
|
- 템플릿으로 빠른 조제
|
||||||
|
- 재조제 간소화
|
||||||
|
|
||||||
|
3. **매출 증대**
|
||||||
|
- 단품 판매 활성화
|
||||||
|
- 맞춤 처방 서비스 확대
|
||||||
|
|
||||||
|
4. **고객 만족**
|
||||||
|
- 개인 맞춤 서비스
|
||||||
|
- 투명한 가격 정책
|
||||||
298
docs/판매관리_기획문서.md
Normal file
298
docs/판매관리_기획문서.md
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
# 한약 판매 관리 시스템 기획 문서
|
||||||
|
|
||||||
|
## 1. 개요
|
||||||
|
|
||||||
|
### 1.1 배경
|
||||||
|
- 한약사가 조제한 한약은 재고 차감과 별개로 판매 프로세스를 거쳐야 함
|
||||||
|
- 조제 완료 후 판매 상태 관리 및 가격 책정이 필요
|
||||||
|
- 환자 관리 및 마케팅을 위한 카카오 채널 연동 계획
|
||||||
|
|
||||||
|
### 1.2 목적
|
||||||
|
- 조제된 한약의 판매 상태 관리
|
||||||
|
- 유연한 가격 정책 적용
|
||||||
|
- 판매 이력 추적 및 매출 관리
|
||||||
|
- 환자 정보 연계 강화
|
||||||
|
|
||||||
|
## 2. 현재 시스템 분석
|
||||||
|
|
||||||
|
### 2.1 기존 구조
|
||||||
|
```
|
||||||
|
compounds 테이블:
|
||||||
|
- compound_id: 조제 고유번호
|
||||||
|
- status: 현재 'PREPARED'로 고정
|
||||||
|
- sell_price_total: 판매가격 필드 존재하나 미활용
|
||||||
|
- cost_total: 원가 필드 존재
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 현재 문제점
|
||||||
|
- 조제 후 판매 상태 추적 불가
|
||||||
|
- 가격 정책 유연성 부족
|
||||||
|
- 판매 완료/취소/반품 등 프로세스 미비
|
||||||
|
- 매출 통계 기능 없음
|
||||||
|
|
||||||
|
## 3. 기능 요구사항
|
||||||
|
|
||||||
|
### 3.1 판매 상태 관리
|
||||||
|
|
||||||
|
#### 3.1.1 상태 유형
|
||||||
|
```
|
||||||
|
PREPARED (조제완료) → 현재 기본값
|
||||||
|
PENDING_PAYMENT (결제대기)
|
||||||
|
PAID (결제완료)
|
||||||
|
PENDING_DELIVERY (배송대기)
|
||||||
|
DELIVERED (배송완료)
|
||||||
|
COMPLETED (판매완료)
|
||||||
|
OTC_CONVERTED (OTC전환)
|
||||||
|
CANCELLED (취소)
|
||||||
|
REFUNDED (환불)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.1.2 상태 전이 규칙
|
||||||
|
```
|
||||||
|
조제완료 → 결제대기 → 결제완료 → 배송대기 → 배송완료 → 판매완료
|
||||||
|
↓ ↓
|
||||||
|
OTC전환 취소/환불
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 가격 관리
|
||||||
|
|
||||||
|
#### 3.2.1 가격 정책
|
||||||
|
1. **기본가격 산정**
|
||||||
|
- 표준처방: 처방별 기본가격 테이블 참조
|
||||||
|
- 직접조제: 약재별 단가 × 용량 × 첩수 자동 계산
|
||||||
|
|
||||||
|
2. **가격 조정**
|
||||||
|
- 가감방 적용시 자동 재계산
|
||||||
|
- 수동 가격 조정 가능
|
||||||
|
- 할인율 적용 기능
|
||||||
|
|
||||||
|
3. **가격 구성**
|
||||||
|
```
|
||||||
|
약재원가 + 조제료 + 배송비 = 기본가격
|
||||||
|
기본가격 × (1 - 할인율) = 최종판매가
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 판매 처리 기능
|
||||||
|
|
||||||
|
#### 3.3.1 판매 정보 입력
|
||||||
|
- 결제 방법 (현금/카드/계좌이체/카카오페이)
|
||||||
|
- 결제 일시
|
||||||
|
- 실제 판매가격
|
||||||
|
- 할인 사유 및 금액
|
||||||
|
- 배송 정보 (택배/직접수령/퀵서비스)
|
||||||
|
- 영수증 발행 여부
|
||||||
|
|
||||||
|
#### 3.3.2 OTC 전환
|
||||||
|
- 처방전 없이 판매 가능한 경우
|
||||||
|
- 환자 정보 없이도 판매 처리
|
||||||
|
- 별도 재고 관리 필요
|
||||||
|
|
||||||
|
## 4. 데이터베이스 설계
|
||||||
|
|
||||||
|
### 4.1 기존 테이블 수정
|
||||||
|
|
||||||
|
#### compounds 테이블 수정사항
|
||||||
|
```sql
|
||||||
|
ALTER TABLE compounds ADD COLUMN payment_method TEXT;
|
||||||
|
ALTER TABLE compounds ADD COLUMN payment_date DATETIME;
|
||||||
|
ALTER TABLE compounds ADD COLUMN discount_rate REAL DEFAULT 0;
|
||||||
|
ALTER TABLE compounds ADD COLUMN discount_reason TEXT;
|
||||||
|
ALTER TABLE compounds ADD COLUMN delivery_method TEXT;
|
||||||
|
ALTER TABLE compounds ADD COLUMN delivery_date DATETIME;
|
||||||
|
ALTER TABLE compounds ADD COLUMN invoice_number TEXT;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 신규 테이블
|
||||||
|
|
||||||
|
#### sales_transactions (판매 거래)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sales_transactions (
|
||||||
|
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
compound_id INTEGER REFERENCES compounds(compound_id),
|
||||||
|
transaction_date DATETIME NOT NULL,
|
||||||
|
transaction_type TEXT NOT NULL, -- SALE, REFUND, CANCEL
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
payment_method TEXT,
|
||||||
|
payment_status TEXT, -- PENDING, COMPLETED, FAILED
|
||||||
|
notes TEXT,
|
||||||
|
created_by TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### price_policies (가격 정책)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE price_policies (
|
||||||
|
policy_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
formula_id INTEGER REFERENCES formulas(formula_id),
|
||||||
|
base_price REAL NOT NULL,
|
||||||
|
dispensing_fee REAL DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT 1,
|
||||||
|
effective_date DATE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### sales_status_history (판매 상태 이력)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sales_status_history (
|
||||||
|
history_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
compound_id INTEGER REFERENCES compounds(compound_id),
|
||||||
|
old_status TEXT,
|
||||||
|
new_status TEXT NOT NULL,
|
||||||
|
changed_by TEXT,
|
||||||
|
change_reason TEXT,
|
||||||
|
changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 사용자 인터페이스
|
||||||
|
|
||||||
|
### 5.1 판매 관리 화면
|
||||||
|
|
||||||
|
#### 5.1.1 조제 목록 화면 개선
|
||||||
|
```
|
||||||
|
[조제 목록]
|
||||||
|
┌────┬──────┬──────┬────────┬────────┬────────┬──────────┐
|
||||||
|
│번호│환자명│처방명│조제일자│ 상태 │판매가격│ 작업 │
|
||||||
|
├────┼──────┼──────┼────────┼────────┼────────┼──────────┤
|
||||||
|
│ 1 │홍길동│갈근탕│02-18 │조제완료│ 80,000 │[판매처리]│
|
||||||
|
│ 2 │김철수│쌍화탕│02-18 │결제대기│ 60,000 │[결제확인]│
|
||||||
|
│ 3 │이영희│십전대│02-17 │배송대기│120,000 │[배송처리]│
|
||||||
|
└────┴──────┴──────┴────────┴────────┴────────┴──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.2 판매 처리 모달
|
||||||
|
```
|
||||||
|
┌─────────────── 판매 처리 ──────────────┐
|
||||||
|
│ │
|
||||||
|
│ 처방명: 갈근탕 │
|
||||||
|
│ 환자명: 홍길동 │
|
||||||
|
│ 조제일: 2026-02-18 │
|
||||||
|
│ │
|
||||||
|
│ ─────── 가격 정보 ───────── │
|
||||||
|
│ 약재원가: 45,000원 │
|
||||||
|
│ 조제료: 20,000원 │
|
||||||
|
│ 기본가격: 65,000원 │
|
||||||
|
│ │
|
||||||
|
│ 할인율: [10%▼] │
|
||||||
|
│ 할인사유: [_______________] │
|
||||||
|
│ 최종가격: 58,500원 │
|
||||||
|
│ │
|
||||||
|
│ ─────── 결제 정보 ───────── │
|
||||||
|
│ 결제방법: [카드▼] │
|
||||||
|
│ 결제일시: [2026-02-18 14:30] │
|
||||||
|
│ │
|
||||||
|
│ ─────── 배송 정보 ───────── │
|
||||||
|
│ 배송방법: [택배▼] │
|
||||||
|
│ 배송예정: [2026-02-19] │
|
||||||
|
│ │
|
||||||
|
│ [취소] [판매확정] │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 매출 통계 화면
|
||||||
|
|
||||||
|
```
|
||||||
|
[매출 현황]
|
||||||
|
기간: [2026-02-01] ~ [2026-02-29]
|
||||||
|
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 총 매출: 3,540,000원 │
|
||||||
|
│ 총 건수: 42건 │
|
||||||
|
│ 평균 단가: 84,286원 │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
|
||||||
|
[일별 매출 추이 그래프]
|
||||||
|
[처방별 매출 비중 차트]
|
||||||
|
[결제 방법별 통계]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. API 설계
|
||||||
|
|
||||||
|
### 6.1 판매 관련 API
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 판매 상태 업데이트
|
||||||
|
POST /api/compounds/{compound_id}/status
|
||||||
|
{
|
||||||
|
"status": "PAID",
|
||||||
|
"payment_method": "CARD",
|
||||||
|
"payment_date": "2026-02-18T14:30:00",
|
||||||
|
"amount": 58500
|
||||||
|
}
|
||||||
|
|
||||||
|
# 가격 조정
|
||||||
|
PUT /api/compounds/{compound_id}/price
|
||||||
|
{
|
||||||
|
"sell_price_total": 58500,
|
||||||
|
"discount_rate": 10,
|
||||||
|
"discount_reason": "단골 할인"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 판매 통계 조회
|
||||||
|
GET /api/sales/statistics?start_date=2026-02-01&end_date=2026-02-29
|
||||||
|
|
||||||
|
# OTC 전환
|
||||||
|
POST /api/compounds/{compound_id}/convert-to-otc
|
||||||
|
{
|
||||||
|
"reason": "처방전 미제출",
|
||||||
|
"notes": "환자 요청"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 구현 우선순위
|
||||||
|
|
||||||
|
### Phase 1 (1주차)
|
||||||
|
1. compounds 테이블 칼럼 추가
|
||||||
|
2. 판매 상태 변경 기능
|
||||||
|
3. 기본 가격 입력/수정 기능
|
||||||
|
4. 판매 처리 UI 구현
|
||||||
|
|
||||||
|
### Phase 2 (2주차)
|
||||||
|
1. sales_transactions 테이블 생성
|
||||||
|
2. 판매 이력 관리 기능
|
||||||
|
3. 매출 통계 API
|
||||||
|
4. 통계 화면 구현
|
||||||
|
|
||||||
|
### Phase 3 (3주차)
|
||||||
|
1. 가격 정책 테이블 구현
|
||||||
|
2. 자동 가격 계산 로직
|
||||||
|
3. OTC 전환 기능
|
||||||
|
4. 영수증 발행 기능
|
||||||
|
|
||||||
|
## 8. 추후 확장 계획
|
||||||
|
|
||||||
|
### 8.1 카카오 채널 연동
|
||||||
|
- QR 코드 생성 및 출력
|
||||||
|
- 카카오 회원가입 유도
|
||||||
|
- 알림톡 발송 (조제완료, 배송안내)
|
||||||
|
|
||||||
|
### 8.2 고객 관리
|
||||||
|
- 구매 이력 관리
|
||||||
|
- 재구매 주기 분석
|
||||||
|
- 맞춤 처방 추천
|
||||||
|
|
||||||
|
### 8.3 재고 연계
|
||||||
|
- OTC 전환시 별도 재고 관리
|
||||||
|
- 유통기한 관리
|
||||||
|
- 재고 부족 알림
|
||||||
|
|
||||||
|
## 9. 기대 효과
|
||||||
|
|
||||||
|
1. **업무 효율성 향상**
|
||||||
|
- 조제부터 판매까지 일원화된 관리
|
||||||
|
- 자동 가격 계산으로 실수 방지
|
||||||
|
|
||||||
|
2. **매출 관리 개선**
|
||||||
|
- 실시간 매출 현황 파악
|
||||||
|
- 처방별/기간별 분석 가능
|
||||||
|
|
||||||
|
3. **고객 서비스 향상**
|
||||||
|
- 체계적인 배송 관리
|
||||||
|
- 투명한 가격 정책
|
||||||
|
|
||||||
|
4. **의사결정 지원**
|
||||||
|
- 데이터 기반 가격 정책 수립
|
||||||
|
- 인기 처방 파악 및 재고 관리 최적화
|
||||||
318
static/app.js
318
static/app.js
@ -1267,14 +1267,32 @@ $(document).ready(function() {
|
|||||||
let statusBadge = '';
|
let statusBadge = '';
|
||||||
switch(compound.status) {
|
switch(compound.status) {
|
||||||
case 'PREPARED':
|
case 'PREPARED':
|
||||||
statusBadge = '<span class="badge bg-success">조제완료</span>';
|
statusBadge = '<span class="badge bg-primary">조제완료</span>';
|
||||||
break;
|
break;
|
||||||
case 'DISPENSED':
|
case 'PENDING_PAYMENT':
|
||||||
statusBadge = '<span class="badge bg-primary">출고완료</span>';
|
statusBadge = '<span class="badge bg-warning">결제대기</span>';
|
||||||
|
break;
|
||||||
|
case 'PAID':
|
||||||
|
statusBadge = '<span class="badge bg-success">결제완료</span>';
|
||||||
|
break;
|
||||||
|
case 'PENDING_DELIVERY':
|
||||||
|
statusBadge = '<span class="badge bg-info">배송대기</span>';
|
||||||
|
break;
|
||||||
|
case 'DELIVERED':
|
||||||
|
statusBadge = '<span class="badge bg-secondary">배송완료</span>';
|
||||||
|
break;
|
||||||
|
case 'COMPLETED':
|
||||||
|
statusBadge = '<span class="badge bg-dark">판매완료</span>';
|
||||||
|
break;
|
||||||
|
case 'OTC_CONVERTED':
|
||||||
|
statusBadge = '<span class="badge bg-purple">OTC전환</span>';
|
||||||
break;
|
break;
|
||||||
case 'CANCELLED':
|
case 'CANCELLED':
|
||||||
statusBadge = '<span class="badge bg-danger">취소</span>';
|
statusBadge = '<span class="badge bg-danger">취소</span>';
|
||||||
break;
|
break;
|
||||||
|
case 'REFUNDED':
|
||||||
|
statusBadge = '<span class="badge bg-danger">환불</span>';
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
statusBadge = '<span class="badge bg-secondary">대기</span>';
|
statusBadge = '<span class="badge bg-secondary">대기</span>';
|
||||||
}
|
}
|
||||||
@ -1297,6 +1315,20 @@ $(document).ready(function() {
|
|||||||
<button class="btn btn-sm btn-outline-info view-compound-detail" data-id="${compound.compound_id}">
|
<button class="btn btn-sm btn-outline-info view-compound-detail" data-id="${compound.compound_id}">
|
||||||
<i class="bi bi-eye"></i> 상세
|
<i class="bi bi-eye"></i> 상세
|
||||||
</button>
|
</button>
|
||||||
|
${compound.status === 'PREPARED' ? `
|
||||||
|
<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 || '직접조제'}"
|
||||||
|
data-cost="${compound.cost_total || 0}"
|
||||||
|
data-price="${compound.sell_price_total || 0}">
|
||||||
|
<i class="bi bi-cash-coin"></i> 판매
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
${compound.status === 'PAID' ? `
|
||||||
|
<button class="btn btn-sm btn-outline-primary process-delivery" data-id="${compound.compound_id}">
|
||||||
|
<i class="bi bi-truck"></i> 배송
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
@ -1312,6 +1344,23 @@ $(document).ready(function() {
|
|||||||
const compoundId = $(this).data('id');
|
const compoundId = $(this).data('id');
|
||||||
viewCompoundDetail(compoundId);
|
viewCompoundDetail(compoundId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 판매 처리 버튼 이벤트
|
||||||
|
$('.process-sale').on('click', function() {
|
||||||
|
const compoundId = $(this).data('id');
|
||||||
|
const formulaName = $(this).data('formula');
|
||||||
|
const patientName = $(this).data('patient');
|
||||||
|
const costTotal = $(this).data('cost');
|
||||||
|
const priceTotal = $(this).data('price');
|
||||||
|
|
||||||
|
openSalesModal(compoundId, formulaName, patientName, costTotal, priceTotal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 배송 처리 버튼 이벤트
|
||||||
|
$('.process-delivery').on('click', function() {
|
||||||
|
const compoundId = $(this).data('id');
|
||||||
|
processDelivery(compoundId);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
tbody.html('<tr><td colspan="13" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
|
tbody.html('<tr><td colspan="13" class="text-center text-muted">조제 내역이 없습니다.</td></tr>');
|
||||||
$('#todayCompoundCount').text(0);
|
$('#todayCompoundCount').text(0);
|
||||||
@ -3439,4 +3488,267 @@ $(document).ready(function() {
|
|||||||
alert('약재 정보 수정 기능은 준비 중입니다.');
|
alert('약재 정보 수정 기능은 준비 중입니다.');
|
||||||
// TODO: 정보 수정 폼 구현
|
// TODO: 정보 수정 폼 구현
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 판매 관리 기능 ====================
|
||||||
|
|
||||||
|
// 판매 모달 열기
|
||||||
|
function openSalesModal(compoundId, formulaName, patientName, costTotal, priceTotal) {
|
||||||
|
$('#salesCompoundId').val(compoundId);
|
||||||
|
$('#salesFormulaName').val(formulaName);
|
||||||
|
$('#salesPatientName').val(patientName);
|
||||||
|
$('#salesCostTotal').val(costTotal);
|
||||||
|
|
||||||
|
// 환자 마일리지 조회
|
||||||
|
loadPatientMileage(patientName);
|
||||||
|
|
||||||
|
// 기본 가격 계산 (원가 + 조제료)
|
||||||
|
const dispensingFee = parseFloat($('#salesDispensingFee').val()) || 20000;
|
||||||
|
const basePrice = costTotal + dispensingFee;
|
||||||
|
$('#salesBasePrice').val(basePrice);
|
||||||
|
|
||||||
|
// 판매가가 없으면 기본가격으로 설정
|
||||||
|
if (!priceTotal || priceTotal === 0) {
|
||||||
|
priceTotal = basePrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기화
|
||||||
|
$('#salesDiscountValue').val(0);
|
||||||
|
$('#salesPriceAfterDiscount').val(priceTotal);
|
||||||
|
$('#salesMileageUse').val(0);
|
||||||
|
$('#salesPaymentCash').val(0);
|
||||||
|
$('#salesPaymentCard').val(0);
|
||||||
|
$('#salesPaymentTransfer').val(0);
|
||||||
|
updatePaymentSummary();
|
||||||
|
|
||||||
|
// 현재 시간 설정
|
||||||
|
const now = new Date();
|
||||||
|
const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
|
||||||
|
$('#salesPaymentDate').val(localDateTime);
|
||||||
|
|
||||||
|
// 내일 날짜 설정 (배송예정일)
|
||||||
|
const tomorrow = new Date(now);
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
$('#salesDeliveryDate').val(tomorrow.toISOString().slice(0, 10));
|
||||||
|
|
||||||
|
$('#salesModal').modal('show');
|
||||||
|
|
||||||
|
// 가격 자동 계산 이벤트
|
||||||
|
calculateSalesPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 환자 마일리지 조회
|
||||||
|
function loadPatientMileage(patientName) {
|
||||||
|
if (patientName && patientName !== '직접조제') {
|
||||||
|
$.get('/api/patients/search', { name: patientName }, function(response) {
|
||||||
|
if (response.success && response.data.length > 0) {
|
||||||
|
const patient = response.data[0];
|
||||||
|
const mileage = patient.mileage_balance || 0;
|
||||||
|
$('#patientMileageBalance').text(mileage.toLocaleString());
|
||||||
|
$('#salesMileageUse').attr('max', mileage);
|
||||||
|
} else {
|
||||||
|
$('#patientMileageBalance').text('0');
|
||||||
|
$('#salesMileageUse').attr('max', 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$('#patientMileageBalance').text('0');
|
||||||
|
$('#salesMileageUse').attr('max', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 가격 자동 계산
|
||||||
|
function calculateSalesPrice() {
|
||||||
|
const costTotal = parseFloat($('#salesCostTotal').val()) || 0;
|
||||||
|
const dispensingFee = parseFloat($('#salesDispensingFee').val()) || 0;
|
||||||
|
const basePrice = costTotal + dispensingFee;
|
||||||
|
$('#salesBasePrice').val(basePrice);
|
||||||
|
|
||||||
|
const discountType = $('input[name="discountType"]:checked').val();
|
||||||
|
const discountValue = parseFloat($('#salesDiscountValue').val()) || 0;
|
||||||
|
|
||||||
|
let discountAmount = 0;
|
||||||
|
if (discountType === 'rate') {
|
||||||
|
discountAmount = basePrice * (discountValue / 100);
|
||||||
|
} else {
|
||||||
|
discountAmount = discountValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceAfterDiscount = Math.max(0, basePrice - discountAmount);
|
||||||
|
$('#salesPriceAfterDiscount').val(Math.round(priceAfterDiscount));
|
||||||
|
|
||||||
|
updatePaymentSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결제 요약 업데이트
|
||||||
|
function updatePaymentSummary() {
|
||||||
|
const priceAfterDiscount = parseFloat($('#salesPriceAfterDiscount').val()) || 0;
|
||||||
|
const mileageUse = parseFloat($('#salesMileageUse').val()) || 0;
|
||||||
|
const cashPayment = parseFloat($('#salesPaymentCash').val()) || 0;
|
||||||
|
const cardPayment = parseFloat($('#salesPaymentCard').val()) || 0;
|
||||||
|
const transferPayment = parseFloat($('#salesPaymentTransfer').val()) || 0;
|
||||||
|
|
||||||
|
const totalPayment = mileageUse + cashPayment + cardPayment + transferPayment;
|
||||||
|
const needToPay = Math.max(0, priceAfterDiscount - totalPayment);
|
||||||
|
|
||||||
|
$('#salesNeedToPay').text(needToPay.toLocaleString());
|
||||||
|
$('#salesTotalPayment').text(totalPayment.toLocaleString());
|
||||||
|
|
||||||
|
// 결제 방법 자동 설정
|
||||||
|
if (totalPayment > 0) {
|
||||||
|
let methodCount = 0;
|
||||||
|
if (mileageUse > 0) methodCount++;
|
||||||
|
if (cashPayment > 0) methodCount++;
|
||||||
|
if (cardPayment > 0) methodCount++;
|
||||||
|
if (transferPayment > 0) methodCount++;
|
||||||
|
|
||||||
|
if (methodCount > 1) {
|
||||||
|
$('#salesPaymentMethod').val('COMPLEX');
|
||||||
|
} else if (cashPayment > 0) {
|
||||||
|
$('#salesPaymentMethod').val('CASH');
|
||||||
|
} else if (cardPayment > 0) {
|
||||||
|
$('#salesPaymentMethod').val('CARD');
|
||||||
|
} else if (transferPayment > 0) {
|
||||||
|
$('#salesPaymentMethod').val('TRANSFER');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 할인 방식 변경 이벤트
|
||||||
|
$('input[name="discountType"]').on('change', function() {
|
||||||
|
const discountType = $(this).val();
|
||||||
|
if (discountType === 'rate') {
|
||||||
|
$('#discountUnit').text('%');
|
||||||
|
$('#salesDiscountValue').attr('max', 100);
|
||||||
|
} else {
|
||||||
|
$('#discountUnit').text('원');
|
||||||
|
$('#salesDiscountValue').removeAttr('max');
|
||||||
|
}
|
||||||
|
calculateSalesPrice();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 가격 계산 이벤트 핸들러
|
||||||
|
$('#salesDispensingFee, #salesDiscountValue').on('input', calculateSalesPrice);
|
||||||
|
$('#salesMileageUse, #salesPaymentCash, #salesPaymentCard, #salesPaymentTransfer').on('input', updatePaymentSummary);
|
||||||
|
|
||||||
|
// 마일리지 전액 사용 버튼
|
||||||
|
$('#applyMaxMileage').on('click', function() {
|
||||||
|
const maxMileage = parseFloat($('#salesMileageUse').attr('max')) || 0;
|
||||||
|
const priceAfterDiscount = parseFloat($('#salesPriceAfterDiscount').val()) || 0;
|
||||||
|
const useAmount = Math.min(maxMileage, priceAfterDiscount);
|
||||||
|
$('#salesMileageUse').val(useAmount);
|
||||||
|
updatePaymentSummary();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 판매 저장
|
||||||
|
$('#saveSalesBtn').on('click', function() {
|
||||||
|
const compoundId = $('#salesCompoundId').val();
|
||||||
|
const priceAfterDiscount = parseFloat($('#salesPriceAfterDiscount').val()) || 0;
|
||||||
|
const discountType = $('input[name="discountType"]:checked').val();
|
||||||
|
const discountValue = parseFloat($('#salesDiscountValue').val()) || 0;
|
||||||
|
const discountReason = $('#salesDiscountReason').val();
|
||||||
|
|
||||||
|
// 복합 결제 정보
|
||||||
|
const mileageUse = parseFloat($('#salesMileageUse').val()) || 0;
|
||||||
|
const cashPayment = parseFloat($('#salesPaymentCash').val()) || 0;
|
||||||
|
const cardPayment = parseFloat($('#salesPaymentCard').val()) || 0;
|
||||||
|
const transferPayment = parseFloat($('#salesPaymentTransfer').val()) || 0;
|
||||||
|
|
||||||
|
const totalPayment = mileageUse + cashPayment + cardPayment + transferPayment;
|
||||||
|
|
||||||
|
// 결제 금액 검증
|
||||||
|
if (Math.abs(totalPayment - priceAfterDiscount) > 1) {
|
||||||
|
alert(`결제 금액이 일치하지 않습니다.\n필요금액: ${priceAfterDiscount.toLocaleString()}원\n결제금액: ${totalPayment.toLocaleString()}원`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentMethod = $('#salesPaymentMethod').val();
|
||||||
|
const paymentDate = $('#salesPaymentDate').val();
|
||||||
|
const deliveryMethod = $('#salesDeliveryMethod').val();
|
||||||
|
const deliveryDate = $('#salesDeliveryDate').val();
|
||||||
|
|
||||||
|
if (!paymentMethod) {
|
||||||
|
alert('결제방법을 선택해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 할인액 계산
|
||||||
|
const basePrice = parseFloat($('#salesBasePrice').val()) || 0;
|
||||||
|
let discountAmount = 0;
|
||||||
|
if (discountType === 'rate') {
|
||||||
|
discountAmount = basePrice * (discountValue / 100);
|
||||||
|
} else {
|
||||||
|
discountAmount = discountValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 가격 및 결제 정보 업데이트
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/compounds/${compoundId}/price`,
|
||||||
|
type: 'PUT',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
sell_price_total: priceAfterDiscount,
|
||||||
|
discount_rate: discountType === 'rate' ? discountValue : 0,
|
||||||
|
discount_amount: Math.round(discountAmount),
|
||||||
|
discount_reason: discountReason,
|
||||||
|
mileage_used: mileageUse,
|
||||||
|
payment_cash: cashPayment,
|
||||||
|
payment_card: cardPayment,
|
||||||
|
payment_transfer: transferPayment
|
||||||
|
}),
|
||||||
|
success: function() {
|
||||||
|
// 2. 상태를 PAID로 변경
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/compounds/${compoundId}/status`,
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
status: 'PAID',
|
||||||
|
payment_method: paymentMethod,
|
||||||
|
payment_date: paymentDate,
|
||||||
|
actual_payment_amount: priceAfterDiscount,
|
||||||
|
delivery_method: deliveryMethod,
|
||||||
|
delivery_date: deliveryDate,
|
||||||
|
mileage_used: mileageUse,
|
||||||
|
changed_by: 'user'
|
||||||
|
}),
|
||||||
|
success: function() {
|
||||||
|
alert('판매 처리가 완료되었습니다.');
|
||||||
|
$('#salesModal').modal('hide');
|
||||||
|
loadCompounds(); // 목록 새로고침
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
alert('상태 업데이트 실패: ' + (xhr.responseJSON?.error || '알 수 없는 오류'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
alert('가격 업데이트 실패: ' + (xhr.responseJSON?.error || '알 수 없는 오류'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 배송 처리
|
||||||
|
function processDelivery(compoundId) {
|
||||||
|
if (!confirm('배송 처리를 하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/api/compounds/${compoundId}/status`,
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
status: 'DELIVERED',
|
||||||
|
delivery_date: new Date().toISOString(),
|
||||||
|
changed_by: 'user'
|
||||||
|
}),
|
||||||
|
success: function() {
|
||||||
|
alert('배송 처리가 완료되었습니다.');
|
||||||
|
loadCompounds(); // 목록 새로고침
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
alert('배송 처리 실패: ' + (xhr.responseJSON?.error || '알 수 없는 오류'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@ -478,6 +478,178 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 판매 처리 모달 -->
|
||||||
|
<div class="modal fade" id="salesModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-cash-coin"></i> 판매 처리</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="salesForm">
|
||||||
|
<input type="hidden" id="salesCompoundId">
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">처방명</label>
|
||||||
|
<input type="text" class="form-control" id="salesFormulaName" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">환자명</label>
|
||||||
|
<input type="text" class="form-control" id="salesPatientName" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-calculator"></i> 가격 정보</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">약재원가</label>
|
||||||
|
<input type="number" class="form-control" id="salesCostTotal" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">조제료</label>
|
||||||
|
<input type="number" class="form-control" id="salesDispensingFee" value="20000">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">판매가격 (원가+조제료)</label>
|
||||||
|
<input type="number" class="form-control fw-bold" id="salesBasePrice" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label">할인 방식 선택</label>
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<input type="radio" class="btn-check" name="discountType" id="discountTypeRate" value="rate" checked>
|
||||||
|
<label class="btn btn-outline-primary" for="discountTypeRate">할인율 (%)</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="discountType" id="discountTypeAmount" value="amount">
|
||||||
|
<label class="btn btn-outline-primary" for="discountTypeAmount">할인액 (원)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">할인 설정</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" class="form-control" id="salesDiscountValue" min="0" value="0">
|
||||||
|
<span class="input-group-text" id="discountUnit">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">할인사유</label>
|
||||||
|
<input type="text" class="form-control" id="salesDiscountReason" placeholder="예: 단골 할인">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">할인 후 가격</label>
|
||||||
|
<input type="number" class="form-control fw-bold text-danger" id="salesPriceAfterDiscount" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-piggy-bank"></i> <strong>보유 마일리지:</strong>
|
||||||
|
<span id="patientMileageBalance">0</span>원
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-info float-end" id="applyMaxMileage">전액 사용</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-credit-card"></i> 복합 결제</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">마일리지 사용</label>
|
||||||
|
<input type="number" class="form-control" id="salesMileageUse" min="0" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">현금 결제</label>
|
||||||
|
<input type="number" class="form-control" id="salesPaymentCash" min="0" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">카드 결제</label>
|
||||||
|
<input type="number" class="form-control" id="salesPaymentCard" min="0" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">계좌이체</label>
|
||||||
|
<input type="number" class="form-control" id="salesPaymentTransfer" min="0" value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
<strong>결제 필요 금액:</strong> <span id="salesNeedToPay" class="fs-5">0</span>원
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="alert alert-success mb-0">
|
||||||
|
<strong>결제 합계:</strong> <span id="salesTotalPayment" class="fs-5">0</span>원
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">주 결제방법</label>
|
||||||
|
<select class="form-control" id="salesPaymentMethod">
|
||||||
|
<option value="">선택하세요</option>
|
||||||
|
<option value="CASH">현금</option>
|
||||||
|
<option value="CARD">카드</option>
|
||||||
|
<option value="TRANSFER">계좌이체</option>
|
||||||
|
<option value="KAKAO">카카오페이</option>
|
||||||
|
<option value="COMPLEX">복합결제</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">결제일시</label>
|
||||||
|
<input type="datetime-local" class="form-control" id="salesPaymentDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-truck"></i> 배송 정보</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">배송방법</label>
|
||||||
|
<select class="form-control" id="salesDeliveryMethod">
|
||||||
|
<option value="">선택하세요</option>
|
||||||
|
<option value="DELIVERY">택배</option>
|
||||||
|
<option value="PICKUP">직접수령</option>
|
||||||
|
<option value="QUICK">퀵서비스</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">배송예정일</label>
|
||||||
|
<input type="date" class="form-control" id="salesDeliveryDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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-success" id="saveSalesBtn">
|
||||||
|
<i class="bi bi-check-circle"></i> 판매 확정
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 조제 상세보기 모달 -->
|
<!-- 조제 상세보기 모달 -->
|
||||||
<div class="modal fade" id="compoundDetailModal" tabindex="-1">
|
<div class="modal fade" id="compoundDetailModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-xl">
|
<div class="modal-dialog modal-xl">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user