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:
thug0bin
2026-03-08 10:33:21 +09:00
parent d6cf4c2cc1
commit 91f8dea5b4
31 changed files with 516 additions and 1036 deletions

View File

@@ -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
}
})