- internal_code DB 저장 → 프론트에서 선택한 제품 그대로 주문 - 기존 장바구니 백업/복구로 사용자 장바구니 보존 - 수인약품 submit_order() 수정 (체크박스 제외 방식) - 테스트 파일 정리 및 문서 추가
862 lines
31 KiB
Python
862 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
주문 관리 DB (SQLite)
|
|
- 다중 도매상 지원 (지오영, 수인, 백제 등)
|
|
- 주문 상태 추적
|
|
- 품목별 결과 관리
|
|
- 자동화 ERP 확장 대비
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
from datetime import datetime
|
|
from typing import Optional, List, Dict
|
|
import json
|
|
|
|
# DB 경로
|
|
DB_PATH = os.path.join(os.path.dirname(__file__), 'db', 'orders.db')
|
|
|
|
|
|
def get_connection():
|
|
"""DB 연결"""
|
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def init_db():
|
|
"""DB 초기화 - 테이블 생성"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 도매상 마스터
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS wholesalers (
|
|
id TEXT PRIMARY KEY, -- 'geoyoung', 'sooin', 'baekje'
|
|
name TEXT NOT NULL, -- '지오영', '수인', '백제'
|
|
api_type TEXT, -- 'playwright', 'api', 'manual'
|
|
base_url TEXT,
|
|
is_active INTEGER DEFAULT 1,
|
|
config_json TEXT, -- 로그인 정보 등 (암호화 권장)
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
|
|
# 기본 도매상 등록
|
|
cursor.execute('''
|
|
INSERT OR IGNORE INTO wholesalers (id, name, api_type, base_url)
|
|
VALUES
|
|
('geoyoung', '지오영', 'playwright', 'https://gwn.geoweb.kr'),
|
|
('sooin', '수인', 'manual', NULL),
|
|
('baekje', '백제', 'manual', NULL)
|
|
''')
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 주문 헤더
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS orders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
order_no TEXT UNIQUE, -- 주문번호 (ORD-20260306-001)
|
|
wholesaler_id TEXT NOT NULL, -- 도매상 ID
|
|
|
|
-- 주문 정보
|
|
order_date TEXT NOT NULL, -- 주문일 (YYYY-MM-DD)
|
|
order_time TEXT, -- 주문시간 (HH:MM:SS)
|
|
order_type TEXT DEFAULT 'manual', -- 'manual', 'auto', 'scheduled'
|
|
order_session TEXT, -- 'morning', 'afternoon', 'evening'
|
|
|
|
-- 상태
|
|
status TEXT DEFAULT 'draft', -- draft, pending, submitted, partial, completed, failed, cancelled
|
|
|
|
-- 집계
|
|
total_items INTEGER DEFAULT 0,
|
|
total_qty INTEGER DEFAULT 0,
|
|
success_items INTEGER DEFAULT 0,
|
|
failed_items INTEGER DEFAULT 0,
|
|
|
|
-- 참조
|
|
parent_order_id INTEGER, -- 재주문 시 원주문 참조
|
|
reference_period TEXT, -- 사용량 조회 기간 (2026-03-01~2026-03-06)
|
|
|
|
-- 메타
|
|
note TEXT,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
submitted_at TEXT, -- 실제 제출 시간
|
|
completed_at TEXT,
|
|
|
|
FOREIGN KEY (wholesaler_id) REFERENCES wholesalers(id),
|
|
FOREIGN KEY (parent_order_id) REFERENCES orders(id)
|
|
)
|
|
''')
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 주문 품목 상세
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS order_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
order_id INTEGER NOT NULL,
|
|
|
|
-- 약품 정보
|
|
drug_code TEXT NOT NULL, -- PIT3000 약품코드
|
|
kd_code TEXT, -- 보험코드 (지오영 검색용)
|
|
internal_code TEXT, -- 🔧 도매상 내부 코드 (장바구니 직접 추가용!)
|
|
product_name TEXT NOT NULL,
|
|
manufacturer TEXT,
|
|
|
|
-- 규격/수량
|
|
specification TEXT, -- '30T', '300T', '500T'
|
|
unit_qty INTEGER, -- 규격당 수량 (30, 300, 500)
|
|
order_qty INTEGER NOT NULL, -- 주문 수량 (단위 개수)
|
|
total_dose INTEGER, -- 총 정제수 (order_qty * unit_qty)
|
|
|
|
-- 주문 근거
|
|
usage_qty INTEGER, -- 사용량 (조회 기간)
|
|
current_stock INTEGER, -- 주문 시점 재고
|
|
|
|
-- 가격 (선택)
|
|
unit_price INTEGER,
|
|
total_price INTEGER,
|
|
|
|
-- 상태
|
|
status TEXT DEFAULT 'pending', -- pending, submitted, success, failed, cancelled
|
|
|
|
-- 결과
|
|
result_code TEXT, -- 'OK', 'OUT_OF_STOCK', 'NOT_FOUND', 'ERROR'
|
|
result_message TEXT,
|
|
wholesaler_order_no TEXT, -- 도매상 측 주문번호
|
|
|
|
-- 메타
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
|
)
|
|
''')
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 주문 로그 (상태 변경 이력)
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS order_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
order_id INTEGER NOT NULL,
|
|
order_item_id INTEGER, -- NULL이면 주문 전체 로그
|
|
|
|
action TEXT NOT NULL, -- 'created', 'submitted', 'success', 'failed', 'cancelled'
|
|
old_status TEXT,
|
|
new_status TEXT,
|
|
|
|
message TEXT,
|
|
detail_json TEXT, -- API 응답 등 상세 정보
|
|
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (order_item_id) REFERENCES order_items(id) ON DELETE CASCADE
|
|
)
|
|
''')
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 주문 컨텍스트 (AI 학습용 스냅샷)
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS order_context (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
order_item_id INTEGER NOT NULL,
|
|
|
|
-- 약품 정보
|
|
drug_code TEXT NOT NULL,
|
|
product_name TEXT,
|
|
|
|
-- 주문 시점 재고
|
|
stock_at_order INTEGER, -- 주문 시점 현재고
|
|
|
|
-- 사용량 분석
|
|
usage_1d INTEGER, -- 최근 1일 사용량
|
|
usage_7d INTEGER, -- 최근 7일 사용량
|
|
usage_30d INTEGER, -- 최근 30일 사용량
|
|
avg_daily_usage REAL, -- 일평균 사용량 (30일 기준)
|
|
|
|
-- 주문 패턴
|
|
ordered_spec TEXT, -- 주문한 규격 (30T, 300T)
|
|
ordered_qty INTEGER, -- 주문 수량 (단위 개수)
|
|
ordered_dose INTEGER, -- 주문 총 정제수
|
|
|
|
-- 규격 선택 이유 (AI 분석용)
|
|
available_specs TEXT, -- 가능한 규격들 JSON ["30T", "300T"]
|
|
spec_stocks TEXT, -- 규격별 도매상 재고 JSON {"30T": 50, "300T": 0}
|
|
selection_reason TEXT, -- 'stock_available', 'best_fit', 'only_option', 'user_choice'
|
|
|
|
-- 예측 vs 실제 (나중에 업데이트)
|
|
days_until_stockout REAL, -- 주문 시점 예상 재고 소진일
|
|
actual_reorder_days INTEGER, -- 실제 재주문까지 일수 (나중에 업데이트)
|
|
|
|
-- 메타
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
FOREIGN KEY (order_item_id) REFERENCES order_items(id) ON DELETE CASCADE
|
|
)
|
|
''')
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 일별 사용량 추적 (시계열 데이터)
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS daily_usage (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
drug_code TEXT NOT NULL,
|
|
usage_date TEXT NOT NULL, -- YYYY-MM-DD
|
|
|
|
-- 처방 데이터
|
|
rx_count INTEGER DEFAULT 0, -- 처방 건수
|
|
rx_qty INTEGER DEFAULT 0, -- 처방 수량 (정제수)
|
|
|
|
-- POS 데이터 (일반약)
|
|
pos_count INTEGER DEFAULT 0,
|
|
pos_qty INTEGER DEFAULT 0,
|
|
|
|
-- 집계
|
|
total_qty INTEGER DEFAULT 0,
|
|
|
|
-- 재고 스냅샷
|
|
stock_start INTEGER, -- 시작 재고
|
|
stock_end INTEGER, -- 종료 재고
|
|
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(drug_code, usage_date)
|
|
)
|
|
''')
|
|
|
|
# ─────────────────────────────────────────────
|
|
# AI 분석 결과/패턴
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS order_patterns (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
drug_code TEXT NOT NULL,
|
|
|
|
-- 분석 기간
|
|
analysis_date TEXT NOT NULL,
|
|
analysis_period_days INTEGER,
|
|
|
|
-- 사용 패턴
|
|
avg_daily_usage REAL,
|
|
usage_stddev REAL, -- 사용량 표준편차 (변동성)
|
|
peak_usage INTEGER, -- 최대 사용량
|
|
|
|
-- 주문 패턴
|
|
typical_order_spec TEXT, -- 주로 주문하는 규격
|
|
typical_order_qty INTEGER, -- 주로 주문하는 수량
|
|
order_frequency_days REAL, -- 평균 주문 주기 (일)
|
|
|
|
-- AI 추천
|
|
recommended_spec TEXT, -- 추천 규격
|
|
recommended_qty INTEGER, -- 추천 수량
|
|
recommended_reorder_point INTEGER,-- 추천 재주문점 (재고가 이 이하면 주문)
|
|
confidence_score REAL, -- 추천 신뢰도 (0-1)
|
|
|
|
-- 모델 정보
|
|
model_version TEXT,
|
|
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
UNIQUE(drug_code, analysis_date)
|
|
)
|
|
''')
|
|
|
|
# ─────────────────────────────────────────────
|
|
# 인덱스
|
|
# ─────────────────────────────────────────────
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_wholesaler ON orders(wholesaler_id)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_order_items_drug ON order_items(drug_code)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_order_items_status ON order_items(status)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_order_context_drug ON order_context(drug_code)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_daily_usage_drug ON daily_usage(drug_code)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_daily_usage_date ON daily_usage(usage_date)')
|
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_order_patterns_drug ON order_patterns(drug_code)')
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return True
|
|
|
|
|
|
def generate_order_no(wholesaler_id: str) -> str:
|
|
"""주문번호 생성 (ORD-GEO-20260306-001)"""
|
|
prefix_map = {
|
|
'geoyoung': 'GEO',
|
|
'sooin': 'SOO',
|
|
'baekje': 'BAK'
|
|
}
|
|
prefix = prefix_map.get(wholesaler_id, 'ORD')
|
|
date_str = datetime.now().strftime('%Y%m%d')
|
|
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# 오늘 해당 도매상 주문 수 카운트
|
|
cursor.execute('''
|
|
SELECT COUNT(*) FROM orders
|
|
WHERE wholesaler_id = ? AND order_date = ?
|
|
''', (wholesaler_id, datetime.now().strftime('%Y-%m-%d')))
|
|
|
|
count = cursor.fetchone()[0] + 1
|
|
conn.close()
|
|
|
|
return f"ORD-{prefix}-{date_str}-{count:03d}"
|
|
|
|
|
|
def create_order(wholesaler_id: str, items: List[Dict],
|
|
order_type: str = 'manual',
|
|
order_session: str = None,
|
|
reference_period: str = None,
|
|
note: str = None) -> Dict:
|
|
"""
|
|
주문 생성 (draft 상태)
|
|
|
|
items: [
|
|
{
|
|
'drug_code': '670400830',
|
|
'kd_code': '670400830',
|
|
'product_name': '레바미피드정 30T',
|
|
'manufacturer': '휴온스',
|
|
'specification': '30T',
|
|
'unit_qty': 30,
|
|
'order_qty': 10,
|
|
'usage_qty': 280,
|
|
'current_stock': 50
|
|
}
|
|
]
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
order_no = generate_order_no(wholesaler_id)
|
|
now = datetime.now()
|
|
|
|
# 주문 헤더 생성
|
|
cursor.execute('''
|
|
INSERT INTO orders (
|
|
order_no, wholesaler_id, order_date, order_time,
|
|
order_type, order_session, reference_period, note,
|
|
total_items, total_qty, status
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'draft')
|
|
''', (
|
|
order_no,
|
|
wholesaler_id,
|
|
now.strftime('%Y-%m-%d'),
|
|
now.strftime('%H:%M:%S'),
|
|
order_type,
|
|
order_session,
|
|
reference_period,
|
|
note,
|
|
len(items),
|
|
sum(item.get('order_qty', 0) for item in items)
|
|
))
|
|
|
|
order_id = cursor.lastrowid
|
|
|
|
# 주문 품목 생성
|
|
for item in items:
|
|
unit_qty = item.get('unit_qty', 1)
|
|
order_qty = item.get('order_qty', 0)
|
|
|
|
cursor.execute('''
|
|
INSERT INTO order_items (
|
|
order_id, drug_code, kd_code, internal_code, product_name, manufacturer,
|
|
specification, unit_qty, order_qty, total_dose,
|
|
usage_qty, current_stock, status
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
|
|
''', (
|
|
order_id,
|
|
item.get('drug_code'),
|
|
item.get('kd_code'),
|
|
item.get('internal_code'), # 🔧 도매상 내부 코드 저장!
|
|
item.get('product_name'),
|
|
item.get('manufacturer'),
|
|
item.get('specification'),
|
|
unit_qty,
|
|
order_qty,
|
|
order_qty * unit_qty,
|
|
item.get('usage_qty'),
|
|
item.get('current_stock')
|
|
))
|
|
|
|
# 로그
|
|
cursor.execute('''
|
|
INSERT INTO order_logs (order_id, action, new_status, message)
|
|
VALUES (?, 'created', 'draft', ?)
|
|
''', (order_id, f'{len(items)}개 품목 주문 생성'))
|
|
|
|
conn.commit()
|
|
|
|
return {
|
|
'success': True,
|
|
'order_id': order_id,
|
|
'order_no': order_no,
|
|
'total_items': len(items)
|
|
}
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
return {'success': False, 'error': str(e)}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def get_order(order_id: int) -> Optional[Dict]:
|
|
"""주문 조회 (품목 포함)"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('SELECT * FROM orders WHERE id = ?', (order_id,))
|
|
order = cursor.fetchone()
|
|
|
|
if not order:
|
|
conn.close()
|
|
return None
|
|
|
|
cursor.execute('SELECT * FROM order_items WHERE order_id = ?', (order_id,))
|
|
items = cursor.fetchall()
|
|
|
|
conn.close()
|
|
|
|
return {
|
|
**dict(order),
|
|
'items': [dict(item) for item in items]
|
|
}
|
|
|
|
|
|
def update_order_status(order_id: int, status: str, message: str = None) -> bool:
|
|
"""주문 상태 업데이트"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
# 현재 상태 조회
|
|
cursor.execute('SELECT status FROM orders WHERE id = ?', (order_id,))
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
return False
|
|
|
|
old_status = row['status']
|
|
|
|
# 상태 업데이트
|
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
update_fields = ['status = ?', 'updated_at = ?']
|
|
params = [status, now]
|
|
|
|
if status == 'submitted':
|
|
update_fields.append('submitted_at = ?')
|
|
params.append(now)
|
|
elif status in ('completed', 'failed'):
|
|
update_fields.append('completed_at = ?')
|
|
params.append(now)
|
|
|
|
params.append(order_id)
|
|
|
|
cursor.execute(f'''
|
|
UPDATE orders SET {', '.join(update_fields)} WHERE id = ?
|
|
''', params)
|
|
|
|
# 로그
|
|
cursor.execute('''
|
|
INSERT INTO order_logs (order_id, action, old_status, new_status, message)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (order_id, status, old_status, status, message))
|
|
|
|
conn.commit()
|
|
return True
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def update_item_result(item_id: int, status: str, result_code: str = None,
|
|
result_message: str = None, wholesaler_order_no: str = None) -> bool:
|
|
"""품목 결과 업데이트"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
cursor.execute('''
|
|
UPDATE order_items SET
|
|
status = ?,
|
|
result_code = ?,
|
|
result_message = ?,
|
|
wholesaler_order_no = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
''', (status, result_code, result_message, wholesaler_order_no, item_id))
|
|
|
|
# 주문 집계 업데이트
|
|
cursor.execute('SELECT order_id FROM order_items WHERE id = ?', (item_id,))
|
|
order_id = cursor.fetchone()['order_id']
|
|
|
|
cursor.execute('''
|
|
UPDATE orders SET
|
|
success_items = (SELECT COUNT(*) FROM order_items WHERE order_id = ? AND status = 'success'),
|
|
failed_items = (SELECT COUNT(*) FROM order_items WHERE order_id = ? AND status = 'failed'),
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
''', (order_id, order_id, order_id))
|
|
|
|
conn.commit()
|
|
return True
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def get_order_history(wholesaler_id: str = None,
|
|
start_date: str = None,
|
|
end_date: str = None,
|
|
status: str = None,
|
|
limit: int = 50) -> List[Dict]:
|
|
"""주문 이력 조회"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
query = 'SELECT * FROM orders WHERE 1=1'
|
|
params = []
|
|
|
|
if wholesaler_id:
|
|
query += ' AND wholesaler_id = ?'
|
|
params.append(wholesaler_id)
|
|
|
|
if start_date:
|
|
query += ' AND order_date >= ?'
|
|
params.append(start_date)
|
|
|
|
if end_date:
|
|
query += ' AND order_date <= ?'
|
|
params.append(end_date)
|
|
|
|
if status:
|
|
query += ' AND status = ?'
|
|
params.append(status)
|
|
|
|
query += ' ORDER BY created_at DESC LIMIT ?'
|
|
params.append(limit)
|
|
|
|
cursor.execute(query, params)
|
|
orders = [dict(row) for row in cursor.fetchall()]
|
|
|
|
conn.close()
|
|
return orders
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# AI 학습용 함수들
|
|
# ─────────────────────────────────────────────
|
|
|
|
def save_order_context(order_item_id: int, context: Dict) -> bool:
|
|
"""
|
|
주문 시점 컨텍스트 저장 (AI 학습용)
|
|
|
|
context: {
|
|
'drug_code': '670400830',
|
|
'product_name': '레바미피드정',
|
|
'stock_at_order': 50,
|
|
'usage_1d': 30,
|
|
'usage_7d': 180,
|
|
'usage_30d': 800,
|
|
'ordered_spec': '30T',
|
|
'ordered_qty': 10,
|
|
'available_specs': ['30T', '300T'],
|
|
'spec_stocks': {'30T': 50, '300T': 0},
|
|
'selection_reason': 'stock_available'
|
|
}
|
|
"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
# 일평균 사용량 계산
|
|
usage_30d = context.get('usage_30d', 0)
|
|
avg_daily = usage_30d / 30.0 if usage_30d else 0
|
|
|
|
# 재고 소진 예상일 계산
|
|
stock = context.get('stock_at_order', 0)
|
|
days_until_stockout = stock / avg_daily if avg_daily > 0 else None
|
|
|
|
# 주문 총 정제수
|
|
ordered_qty = context.get('ordered_qty', 0)
|
|
spec = context.get('ordered_spec', '')
|
|
unit_qty = int(''.join(filter(str.isdigit, spec))) if spec else 1
|
|
ordered_dose = ordered_qty * unit_qty
|
|
|
|
cursor.execute('''
|
|
INSERT INTO order_context (
|
|
order_item_id, drug_code, product_name,
|
|
stock_at_order, usage_1d, usage_7d, usage_30d, avg_daily_usage,
|
|
ordered_spec, ordered_qty, ordered_dose,
|
|
available_specs, spec_stocks, selection_reason,
|
|
days_until_stockout
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
order_item_id,
|
|
context.get('drug_code'),
|
|
context.get('product_name'),
|
|
context.get('stock_at_order'),
|
|
context.get('usage_1d'),
|
|
context.get('usage_7d'),
|
|
context.get('usage_30d'),
|
|
avg_daily,
|
|
context.get('ordered_spec'),
|
|
ordered_qty,
|
|
ordered_dose,
|
|
json.dumps(context.get('available_specs', []), ensure_ascii=False),
|
|
json.dumps(context.get('spec_stocks', {}), ensure_ascii=False),
|
|
context.get('selection_reason'),
|
|
days_until_stockout
|
|
))
|
|
|
|
conn.commit()
|
|
return True
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def update_daily_usage(drug_code: str, usage_date: str,
|
|
rx_count: int = 0, rx_qty: int = 0,
|
|
pos_count: int = 0, pos_qty: int = 0,
|
|
stock_end: int = None) -> bool:
|
|
"""일별 사용량 업데이트 (UPSERT)"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
total_qty = rx_qty + pos_qty
|
|
|
|
cursor.execute('''
|
|
INSERT INTO daily_usage (
|
|
drug_code, usage_date, rx_count, rx_qty, pos_count, pos_qty,
|
|
total_qty, stock_end
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(drug_code, usage_date) DO UPDATE SET
|
|
rx_count = rx_count + excluded.rx_count,
|
|
rx_qty = rx_qty + excluded.rx_qty,
|
|
pos_count = pos_count + excluded.pos_count,
|
|
pos_qty = pos_qty + excluded.pos_qty,
|
|
total_qty = total_qty + excluded.total_qty,
|
|
stock_end = COALESCE(excluded.stock_end, stock_end)
|
|
''', (drug_code, usage_date, rx_count, rx_qty, pos_count, pos_qty,
|
|
total_qty, stock_end))
|
|
|
|
conn.commit()
|
|
return True
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def get_usage_stats(drug_code: str, days: int = 30) -> Dict:
|
|
"""약품 사용량 통계 조회 (AI 분석용)"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
from datetime import datetime, timedelta
|
|
end_date = datetime.now().strftime('%Y-%m-%d')
|
|
start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
|
|
cursor.execute('''
|
|
SELECT
|
|
COUNT(*) as days_with_data,
|
|
SUM(total_qty) as total_usage,
|
|
AVG(total_qty) as avg_daily,
|
|
MAX(total_qty) as max_daily,
|
|
MIN(total_qty) as min_daily
|
|
FROM daily_usage
|
|
WHERE drug_code = ? AND usage_date BETWEEN ? AND ?
|
|
''', (drug_code, start_date, end_date))
|
|
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
if row and row['total_usage']:
|
|
return {
|
|
'drug_code': drug_code,
|
|
'period_days': days,
|
|
'days_with_data': row['days_with_data'],
|
|
'total_usage': row['total_usage'],
|
|
'avg_daily': round(row['avg_daily'], 2) if row['avg_daily'] else 0,
|
|
'max_daily': row['max_daily'],
|
|
'min_daily': row['min_daily']
|
|
}
|
|
|
|
return {
|
|
'drug_code': drug_code,
|
|
'period_days': days,
|
|
'days_with_data': 0,
|
|
'total_usage': 0,
|
|
'avg_daily': 0,
|
|
'max_daily': 0,
|
|
'min_daily': 0
|
|
}
|
|
|
|
|
|
def get_order_pattern(drug_code: str) -> Optional[Dict]:
|
|
"""약품 주문 패턴 조회"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# 최근 주문 이력 분석
|
|
cursor.execute('''
|
|
SELECT
|
|
oi.specification,
|
|
oi.order_qty,
|
|
oi.total_dose,
|
|
o.order_date
|
|
FROM order_items oi
|
|
JOIN orders o ON oi.order_id = o.id
|
|
WHERE oi.drug_code = ? AND oi.status = 'success'
|
|
ORDER BY o.order_date DESC
|
|
LIMIT 10
|
|
''', (drug_code,))
|
|
|
|
orders = [dict(row) for row in cursor.fetchall()]
|
|
|
|
if not orders:
|
|
conn.close()
|
|
return None
|
|
|
|
# 가장 많이 사용된 규격
|
|
spec_counts = {}
|
|
for o in orders:
|
|
spec = o['specification']
|
|
spec_counts[spec] = spec_counts.get(spec, 0) + 1
|
|
|
|
typical_spec = max(spec_counts, key=spec_counts.get)
|
|
|
|
# 평균 주문 수량
|
|
typical_qty = sum(o['order_qty'] for o in orders) // len(orders)
|
|
|
|
# 주문 주기 계산
|
|
if len(orders) >= 2:
|
|
dates = [datetime.strptime(o['order_date'], '%Y-%m-%d') for o in orders]
|
|
intervals = [(dates[i] - dates[i+1]).days for i in range(len(dates)-1)]
|
|
avg_interval = sum(intervals) / len(intervals) if intervals else 0
|
|
else:
|
|
avg_interval = 0
|
|
|
|
conn.close()
|
|
|
|
return {
|
|
'drug_code': drug_code,
|
|
'order_count': len(orders),
|
|
'typical_spec': typical_spec,
|
|
'typical_qty': typical_qty,
|
|
'avg_order_interval_days': round(avg_interval, 1),
|
|
'recent_orders': orders[:5]
|
|
}
|
|
|
|
|
|
def get_ai_training_data(limit: int = 1000) -> List[Dict]:
|
|
"""AI 학습용 데이터 추출"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT
|
|
oc.*,
|
|
oi.status as order_status,
|
|
oi.result_code,
|
|
o.order_date,
|
|
o.wholesaler_id
|
|
FROM order_context oc
|
|
JOIN order_items oi ON oc.order_item_id = oi.id
|
|
JOIN orders o ON oi.order_id = o.id
|
|
ORDER BY oc.created_at DESC
|
|
LIMIT ?
|
|
''', (limit,))
|
|
|
|
data = []
|
|
for row in cursor.fetchall():
|
|
item = dict(row)
|
|
# JSON 필드 파싱
|
|
if item.get('available_specs'):
|
|
item['available_specs'] = json.loads(item['available_specs'])
|
|
if item.get('spec_stocks'):
|
|
item['spec_stocks'] = json.loads(item['spec_stocks'])
|
|
data.append(item)
|
|
|
|
conn.close()
|
|
return data
|
|
|
|
|
|
def save_ai_pattern(drug_code: str, pattern: Dict) -> bool:
|
|
"""AI 분석 결과 저장"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
cursor.execute('''
|
|
INSERT INTO order_patterns (
|
|
drug_code, analysis_date, analysis_period_days,
|
|
avg_daily_usage, usage_stddev, peak_usage,
|
|
typical_order_spec, typical_order_qty, order_frequency_days,
|
|
recommended_spec, recommended_qty, recommended_reorder_point,
|
|
confidence_score, model_version
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(drug_code, analysis_date) DO UPDATE SET
|
|
avg_daily_usage = excluded.avg_daily_usage,
|
|
recommended_spec = excluded.recommended_spec,
|
|
recommended_qty = excluded.recommended_qty,
|
|
confidence_score = excluded.confidence_score
|
|
''', (
|
|
drug_code,
|
|
today,
|
|
pattern.get('period_days', 30),
|
|
pattern.get('avg_daily_usage'),
|
|
pattern.get('usage_stddev'),
|
|
pattern.get('peak_usage'),
|
|
pattern.get('typical_order_spec'),
|
|
pattern.get('typical_order_qty'),
|
|
pattern.get('order_frequency_days'),
|
|
pattern.get('recommended_spec'),
|
|
pattern.get('recommended_qty'),
|
|
pattern.get('recommended_reorder_point'),
|
|
pattern.get('confidence_score'),
|
|
pattern.get('model_version', 'v1')
|
|
))
|
|
|
|
conn.commit()
|
|
return True
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# 초기화 실행
|
|
init_db()
|