diff --git a/backend/app.py b/backend/app.py index 5180f00..18a9bbd 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4096,7 +4096,7 @@ def api_rx_usage(): ISNULL(G.SplName, '') as supplier, SUM(ISNULL(P.QUAN, 1)) as total_qty, SUM(ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) as total_dose, - SUM(ISNULL(P.DRUPRICE, 0) * ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) as total_amount, + SUM(ISNULL(P.DRUPRICE, 0)) as total_amount, -- DRUPRICE 합계 COUNT(DISTINCT P.PreSerial) as prescription_count, COALESCE(NULLIF(G.BARCODE, ''), '') as barcode, ISNULL(IT.IM_QT_sale_debit, 0) as current_stock, diff --git a/backend/order_api.py b/backend/order_api.py index ea0be5f..5b5822f 100644 --- a/backend/order_api.py +++ b/backend/order_api.py @@ -1056,3 +1056,125 @@ def api_ai_order_pattern(drug_code): 'pattern': None, 'message': '주문 이력이 없습니다' }) + + +# ───────────────────────────────────────────── +# 도매상 한도 관리 API +# ───────────────────────────────────────────── + +@order_bp.route('/wholesaler/limits', methods=['GET']) +def api_wholesaler_limits(): + """ + 전체 도매상 한도 조회 (현재 월 사용량 포함) + + GET /api/order/wholesaler/limits + """ + import sqlite3 + from datetime import datetime + + # 절대 경로 사용 + db_path = r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend\db\orders.db' + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cur = conn.cursor() + + # 현재 월 + year_month = datetime.now().strftime('%Y-%m') + + # 한도 정보 조회 + cur.execute('SELECT * FROM wholesaler_limits WHERE is_active = 1 ORDER BY priority') + limits = cur.fetchall() + + result = [] + for row in limits: + ws_id = row['wholesaler_id'] + monthly_limit = row['monthly_limit'] + + # 이번 달 실제 주문 금액 조회 (성공한 것만) + cur.execute(''' + SELECT COALESCE(SUM(oi.unit_price * oi.order_qty), 0) as total_amount + FROM order_items oi + JOIN orders o ON oi.order_id = o.id + WHERE o.wholesaler_id = ? + AND strftime('%Y-%m', o.order_date) = ? + AND o.status IN ('submitted', 'success', 'confirmed') + ''', (ws_id, year_month)) + + usage_row = cur.fetchone() + current_usage = usage_row['total_amount'] if usage_row else 0 + + usage_percent = (current_usage / monthly_limit * 100) if monthly_limit > 0 else 0 + remaining = monthly_limit - current_usage + + result.append({ + 'wholesaler_id': ws_id, + 'monthly_limit': monthly_limit, + 'current_usage': current_usage, + 'remaining': remaining, + 'usage_percent': round(usage_percent, 1), + 'warning_threshold': row['warning_threshold'], + 'is_warning': usage_percent >= (row['warning_threshold'] * 100), + 'priority': row['priority'] + }) + + conn.close() + + return jsonify({ + 'success': True, + 'year_month': year_month, + 'limits': result + }) + + +@order_bp.route('/wholesaler/limits/', methods=['PUT']) +def api_update_wholesaler_limit(wholesaler_id): + """ + 도매상 한도 수정 + + PUT /api/order/wholesaler/limits/geoyoung + { + "monthly_limit": 30000000, + "warning_threshold": 0.85 + } + """ + import sqlite3 + + data = request.get_json() + + db_path = r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend\db\orders.db' + conn = sqlite3.connect(db_path) + cur = conn.cursor() + + updates = [] + params = [] + + if 'monthly_limit' in data: + updates.append('monthly_limit = ?') + params.append(data['monthly_limit']) + + if 'warning_threshold' in data: + updates.append('warning_threshold = ?') + params.append(data['warning_threshold']) + + if 'priority' in data: + updates.append('priority = ?') + params.append(data['priority']) + + if updates: + updates.append("updated_at = datetime('now')") + params.append(wholesaler_id) + + cur.execute(f''' + UPDATE wholesaler_limits + SET {', '.join(updates)} + WHERE wholesaler_id = ? + ''', params) + + conn.commit() + + conn.close() + + return jsonify({ + 'success': True, + 'message': f'{wholesaler_id} 한도 업데이트 완료' + }) diff --git a/backend/templates/admin_rx_usage.html b/backend/templates/admin_rx_usage.html index 54e9fd7..66b0bc8 100644 --- a/backend/templates/admin_rx_usage.html +++ b/backend/templates/admin_rx_usage.html @@ -774,7 +774,7 @@
💰
-
-
총 약가
+
총 매출액
🛒
@@ -807,7 +807,7 @@ 현재고 처방횟수 투약량 - 약가 + 매출액 주문수량 @@ -1156,14 +1156,55 @@ // 다중 도매상 선택을 위한 전역 변수 let pendingWholesalerItems = {}; let pendingOtherItems = []; + let wholesalerLimits = {}; // 도매상 한도 캐시 - function openWholesalerSelectModal(itemsByWholesaler, otherItems) { + async function openWholesalerSelectModal(itemsByWholesaler, otherItems) { pendingWholesalerItems = itemsByWholesaler; pendingOtherItems = otherItems; const modal = document.getElementById('multiWholesalerModal'); const body = document.getElementById('multiWholesalerBody'); + // 로딩 표시 + body.innerHTML = '
📊 한도 및 월 매출 조회 중...
'; + modal.classList.add('show'); + + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; + + // 1. 도매상 한도 정보 가져오기 + try { + const res = await fetch('/api/order/wholesaler/limits'); + const data = await res.json(); + if (data.success) { + data.limits.forEach(l => { + wholesalerLimits[l.wholesaler_id] = l; + }); + } + } catch (e) { + console.warn('한도 조회 실패:', e); + } + + // 2. 실제 월 매출 가져오기 (도매상 API 호출) + const wholesalerConfigs = WHOLESALER_ORDER.map(id => WHOLESALER_CONFIG[id]); + await Promise.all(wholesalerConfigs.map(async ws => { + try { + const salesRes = await fetch(`${ws.salesApi}?year=${year}&month=${month}`); + const salesData = await salesRes.json(); + if (salesData.success && wholesalerLimits[ws.id]) { + // 실제 월 매출로 current_usage 업데이트 + wholesalerLimits[ws.id].current_usage = salesData.total_amount || 0; + wholesalerLimits[ws.id].usage_percent = wholesalerLimits[ws.id].monthly_limit > 0 + ? Math.round((salesData.total_amount || 0) / wholesalerLimits[ws.id].monthly_limit * 1000) / 10 + : 0; + wholesalerLimits[ws.id].remaining = wholesalerLimits[ws.id].monthly_limit - (salesData.total_amount || 0); + } + } catch (e) { + console.warn(`${ws.id} 월매출 조회 실패:`, e); + } + })); + const wsIds = Object.keys(itemsByWholesaler); // 전체 총액 계산 @@ -1190,14 +1231,44 @@ wsIds.forEach(wsId => { const ws = WHOLESALERS[wsId]; const items = itemsByWholesaler[wsId]; + const limit = wholesalerLimits[wsId]; // 도매상별 소계 const wsTotal = items.reduce((sum, item) => sum + (item.unit_price || 0) * item.qty, 0); + // 한도 정보 계산 + let limitHtml = ''; + if (limit) { + const afterOrder = limit.current_usage + wsTotal; + const afterPercent = (afterOrder / limit.monthly_limit * 100).toFixed(1); + const isOver = afterOrder > limit.monthly_limit; + const isWarning = afterPercent >= (limit.warning_threshold * 100); + + limitHtml = ` +
+
+ 월 한도 + ${(limit.monthly_limit/10000).toLocaleString()}만원 +
+
+ 이번달 사용 + ${(limit.current_usage/10000).toLocaleString()}만원 (${limit.usage_percent}%) +
+
+ 주문 후 + + ${(afterOrder/10000).toLocaleString()}만원 (${afterPercent}%) + ${isOver ? ' ⚠️ 초과!' : isWarning ? ' ⚠️' : ' ✓'} + +
+
+ `; + } + html += `
- ${ws.icon} + ${ws.name} ${ws.name} ${items.length}개 품목 ${wsTotal > 0 ? `₩${wsTotal.toLocaleString()}` : ''} @@ -1213,6 +1284,7 @@ }).join('')} ${items.length > 3 ? `
... 외 ${items.length - 3}개
` : ''}
+ ${limitHtml}
`; }); @@ -1446,7 +1518,7 @@ const headerDiv = modal.querySelector('.order-modal-header'); // 도매상별 헤더 및 본문 텍스트 변경 - document.getElementById('orderConfirmTitle').innerHTML = `${ws.icon} ${ws.name} 주문 확인`; + document.getElementById('orderConfirmTitle').innerHTML = `${ws.name}${ws.name} 주문 확인`; document.getElementById('orderConfirmWholesaler').textContent = ws.name; headerDiv.style.background = ws.gradient;