feat(재고): 약품 더블클릭 시 입고이력 모달 추가
- 새 API: GET /api/drugs/<drug_code>/purchase-history - WH_sub + WH_main + PM_BASE.CD_custom 조인 - 도매상명, 입고일, 수량, 단가, 전화번호 반환 - admin_products.html 업데이트: - tr ondblclick → openPurchaseModal() - 입고이력 모달 UI/스타일 추가 - 도매상 전화번호 클릭 시 복사 기능 - 결과 카운트 옆에 더블클릭 힌트 추가 - 기타 onclick에 event.stopPropagation() 추가 (충돌 방지)
This commit is contained in:
144
backend/app.py
144
backend/app.py
@@ -3639,6 +3639,69 @@ def api_products():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 입고이력 API ====================
|
||||
|
||||
@app.route('/api/drugs/<drug_code>/purchase-history')
|
||||
def api_drug_purchase_history(drug_code):
|
||||
"""
|
||||
약품 입고이력 조회 API
|
||||
- WH_sub: 입고 상세 (약품코드, 수량, 단가)
|
||||
- WH_main: 입고 마스터 (입고일, 도매상코드)
|
||||
- PM_BASE.CD_custom: 도매상명
|
||||
"""
|
||||
try:
|
||||
drug_session = db_manager.get_session('PM_DRUG')
|
||||
|
||||
# 입고이력 조회 (최근 100건)
|
||||
result = drug_session.execute(text("""
|
||||
SELECT TOP 100
|
||||
m.WH_DT_appl as purchase_date,
|
||||
COALESCE(c.CD_NM_custom, m.WH_BUSINAME, '미확인') as supplier_name,
|
||||
CAST(COALESCE(s.WH_NM_item_a, 0) AS INT) as quantity,
|
||||
CAST(COALESCE(s.WH_MY_unit_a, 0) AS INT) as unit_price,
|
||||
c.CD_TEL_charge1 as supplier_tel
|
||||
FROM WH_sub s
|
||||
JOIN WH_main m ON m.WH_NO_stock = s.WH_SR_stock AND m.WH_DT_appl = s.WH_DT_appl
|
||||
LEFT JOIN PM_BASE.dbo.CD_custom c ON m.WH_CD_cust_sale = c.CD_CD_custom
|
||||
WHERE s.DrugCode = :drug_code
|
||||
ORDER BY m.WH_DT_appl DESC
|
||||
"""), {'drug_code': drug_code})
|
||||
|
||||
history = []
|
||||
for row in result.fetchall():
|
||||
# 날짜 포맷팅 (YYYYMMDD -> YYYY-MM-DD)
|
||||
date_str = row.purchase_date or ''
|
||||
if len(date_str) == 8:
|
||||
date_str = f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}"
|
||||
|
||||
history.append({
|
||||
'date': date_str,
|
||||
'supplier': row.supplier_name or '미확인',
|
||||
'quantity': row.quantity or 0,
|
||||
'unit_price': row.unit_price or 0,
|
||||
'supplier_tel': row.supplier_tel or ''
|
||||
})
|
||||
|
||||
# 약품명 조회
|
||||
name_result = drug_session.execute(text("""
|
||||
SELECT GoodsName FROM CD_GOODS WHERE DrugCode = :drug_code
|
||||
"""), {'drug_code': drug_code})
|
||||
name_row = name_result.fetchone()
|
||||
drug_name = name_row[0] if name_row else drug_code
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'drug_code': drug_code,
|
||||
'drug_name': drug_name,
|
||||
'history': history,
|
||||
'count': len(history)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"입고이력 조회 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 위치 정보 API ====================
|
||||
|
||||
@app.route('/api/locations')
|
||||
@@ -5245,7 +5308,9 @@ def admin_return_management():
|
||||
|
||||
@app.route('/api/return-candidates')
|
||||
def api_return_candidates():
|
||||
"""반품 후보 목록 조회 API"""
|
||||
"""반품 후보 목록 조회 API (가격 정보 포함)"""
|
||||
import pyodbc
|
||||
|
||||
status = request.args.get('status', '')
|
||||
urgency = request.args.get('urgency', '')
|
||||
search = request.args.get('search', '')
|
||||
@@ -5292,13 +5357,81 @@ def api_return_candidates():
|
||||
cursor.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# 약품코드 목록 추출
|
||||
drug_codes = [row['drug_code'] for row in rows]
|
||||
|
||||
# MSSQL에서 가격 정보 조회 (한 번에)
|
||||
price_map = {}
|
||||
if drug_codes:
|
||||
try:
|
||||
mssql_conn_str = (
|
||||
'DRIVER={ODBC Driver 17 for SQL Server};'
|
||||
'SERVER=192.168.0.4\\PM2014;'
|
||||
'DATABASE=PM_DRUG;'
|
||||
'UID=sa;'
|
||||
'PWD=tmddls214!%(;'
|
||||
'TrustServerCertificate=yes;'
|
||||
'Connection Timeout=5'
|
||||
)
|
||||
mssql_conn = pyodbc.connect(mssql_conn_str, timeout=5)
|
||||
mssql_cursor = mssql_conn.cursor()
|
||||
|
||||
# IN 절 생성 (SQL Injection 방지를 위해 파라미터화)
|
||||
# Price가 없으면 Saleprice, Topprice 순으로 fallback
|
||||
placeholders = ','.join(['?' for _ in drug_codes])
|
||||
mssql_cursor.execute(f"""
|
||||
SELECT DrugCode,
|
||||
COALESCE(NULLIF(Price, 0), NULLIF(Saleprice, 0), NULLIF(Topprice, 0), 0) as BestPrice
|
||||
FROM CD_GOODS
|
||||
WHERE DrugCode IN ({placeholders})
|
||||
""", drug_codes)
|
||||
|
||||
for row in mssql_cursor.fetchall():
|
||||
price_map[row[0]] = float(row[1]) if row[1] else 0
|
||||
|
||||
mssql_conn.close()
|
||||
except Exception as e:
|
||||
logging.warning(f"MSSQL 가격 조회 실패: {e}")
|
||||
|
||||
# 전체 데이터 조회 (통계용)
|
||||
cursor.execute("SELECT drug_code, current_stock, months_since_use, months_since_purchase FROM return_candidates")
|
||||
all_rows = cursor.fetchall()
|
||||
|
||||
# 긴급도별 금액 합계 계산
|
||||
total_amount = 0
|
||||
critical_amount = 0
|
||||
warning_amount = 0
|
||||
|
||||
for row in all_rows:
|
||||
code = row['drug_code']
|
||||
stock = row['current_stock'] or 0
|
||||
price = price_map.get(code, 0)
|
||||
amount = stock * price
|
||||
|
||||
months_use = row['months_since_use'] or 0
|
||||
months_purchase = row['months_since_purchase'] or 0
|
||||
max_months = max(months_use, months_purchase)
|
||||
|
||||
total_amount += amount
|
||||
if max_months >= 36:
|
||||
critical_amount += amount
|
||||
elif max_months >= 24:
|
||||
warning_amount += amount
|
||||
|
||||
items = []
|
||||
for row in rows:
|
||||
code = row['drug_code']
|
||||
stock = row['current_stock'] or 0
|
||||
price = price_map.get(code, 0)
|
||||
recoverable = stock * price
|
||||
|
||||
items.append({
|
||||
'id': row['id'],
|
||||
'drug_code': row['drug_code'],
|
||||
'drug_code': code,
|
||||
'drug_name': row['drug_name'],
|
||||
'current_stock': row['current_stock'],
|
||||
'current_stock': stock,
|
||||
'unit_price': price,
|
||||
'recoverable_amount': recoverable,
|
||||
'last_prescription_date': row['last_prescription_date'],
|
||||
'months_since_use': row['months_since_use'],
|
||||
'last_purchase_date': row['last_purchase_date'],
|
||||
@@ -5337,7 +5470,10 @@ def api_return_candidates():
|
||||
'warning': warning,
|
||||
'pending': pending,
|
||||
'processed': processed,
|
||||
'total': total
|
||||
'total': total,
|
||||
'total_amount': total_amount,
|
||||
'critical_amount': critical_amount,
|
||||
'warning_amount': warning_amount
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user