feat: 재고량 vs 처방 사용량 이중 Y축 비교 그래프 추가
- /api/stock-analytics/stock-with-usage API 추가 - 일별/주별/월별 분석 기간 선택 - 재고 추세 + 사용량 추세 동시 분석 - 추세 해석 카드 (과잉재고/부족 위험 등 자동 진단)
This commit is contained in:
186
backend/app.py
186
backend/app.py
@@ -9973,6 +9973,192 @@ def api_stock_level():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/stock-analytics/stock-with-usage')
|
||||
def api_stock_with_usage():
|
||||
"""
|
||||
재고 변화 + 처방 기반 사용량 통합 API (이중 Y축 그래프용)
|
||||
GET /api/stock-analytics/stock-with-usage?drug_code=A123456789&start_date=2026-01-01&end_date=2026-03-13&period=daily|weekly|monthly
|
||||
|
||||
반환:
|
||||
- items: [{ date, stock, inbound, outbound, rx_usage }]
|
||||
- rx_usage: PS_sub_pharm 기반 처방 사용량
|
||||
"""
|
||||
conn = None
|
||||
conn_pres = None
|
||||
try:
|
||||
drug_code = request.args.get('drug_code', '').strip()
|
||||
start_date = request.args.get('start_date', '')
|
||||
end_date = request.args.get('end_date', '')
|
||||
period = request.args.get('period', 'daily') # daily, weekly, monthly
|
||||
|
||||
if not drug_code:
|
||||
return jsonify({'success': False, 'error': 'drug_code 파라미터 필요'}), 400
|
||||
|
||||
# 날짜 형식 변환
|
||||
start_date_fmt = start_date.replace('-', '') if start_date else (datetime.now() - timedelta(days=90)).strftime('%Y%m%d')
|
||||
end_date_fmt = end_date.replace('-', '') if end_date else datetime.now().strftime('%Y%m%d')
|
||||
|
||||
import pyodbc
|
||||
conn_str_drug = (
|
||||
'DRIVER={ODBC Driver 17 for SQL Server};'
|
||||
'SERVER=192.168.0.4\\PM2014;'
|
||||
'DATABASE=PM_DRUG;'
|
||||
'UID=sa;'
|
||||
'PWD=tmddls214!%(;'
|
||||
'TrustServerCertificate=yes'
|
||||
)
|
||||
conn_str_pres = (
|
||||
'DRIVER={ODBC Driver 17 for SQL Server};'
|
||||
'SERVER=192.168.0.4\\PM2014;'
|
||||
'DATABASE=PM_PRES;'
|
||||
'UID=sa;'
|
||||
'PWD=tmddls214!%(;'
|
||||
'TrustServerCertificate=yes'
|
||||
)
|
||||
|
||||
conn = pyodbc.connect(conn_str_drug, timeout=10)
|
||||
conn_pres = pyodbc.connect(conn_str_pres, timeout=10)
|
||||
cursor = conn.cursor()
|
||||
cursor_pres = conn_pres.cursor()
|
||||
|
||||
# 1. 현재 재고 및 약품 정보
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
IT.IM_QT_sale_debit as current_stock,
|
||||
G.GoodsName as product_name,
|
||||
G.SplName as supplier
|
||||
FROM IM_total IT
|
||||
LEFT JOIN CD_GOODS G ON IT.DrugCode = G.DrugCode
|
||||
WHERE IT.DrugCode = ?
|
||||
""", (drug_code,))
|
||||
|
||||
stock_row = cursor.fetchone()
|
||||
current_stock = int(stock_row.current_stock or 0) if stock_row else 0
|
||||
product_name = stock_row.product_name if stock_row else drug_code
|
||||
supplier = stock_row.supplier if stock_row else ''
|
||||
|
||||
# 2. 그룹핑 기준 설정
|
||||
if period == 'monthly':
|
||||
date_group_expr = "LEFT(IM_DT_appl, 6)" # YYYYMM
|
||||
rx_date_group_expr = "LEFT(INSUDATETIME, 6)"
|
||||
elif period == 'weekly':
|
||||
# 주차 계산: YYYYWW
|
||||
date_group_expr = "CAST(YEAR(CONVERT(DATE, IM_DT_appl, 112)) AS VARCHAR) + RIGHT('0' + CAST(DATEPART(WEEK, CONVERT(DATE, IM_DT_appl, 112)) AS VARCHAR), 2)"
|
||||
rx_date_group_expr = "CAST(YEAR(CONVERT(DATE, INSUDATETIME, 112)) AS VARCHAR) + RIGHT('0' + CAST(DATEPART(WEEK, CONVERT(DATE, INSUDATETIME, 112)) AS VARCHAR), 2)"
|
||||
else: # daily
|
||||
date_group_expr = "IM_DT_appl"
|
||||
rx_date_group_expr = "LEFT(INSUDATETIME, 8)"
|
||||
|
||||
# 3. 재고 입출고 데이터 (IM_date_total)
|
||||
cursor.execute(f"""
|
||||
SELECT
|
||||
{date_group_expr} as date_group,
|
||||
SUM(ISNULL(IM_QT_sale_debit, 0)) as inbound,
|
||||
SUM(ISNULL(IM_QT_sale_credit, 0)) as outbound
|
||||
FROM IM_date_total
|
||||
WHERE DrugCode = ?
|
||||
AND IM_DT_appl >= ?
|
||||
AND IM_DT_appl <= ?
|
||||
GROUP BY {date_group_expr}
|
||||
ORDER BY {date_group_expr} DESC
|
||||
""", (drug_code, start_date_fmt, end_date_fmt))
|
||||
|
||||
stock_rows = cursor.fetchall()
|
||||
|
||||
# 4. 처방 기반 사용량 (PS_sub_pharm)
|
||||
# 컬럼명: PreSerial, Indate, DrugCode, QUAN, Days
|
||||
if period == 'monthly':
|
||||
rx_date_group_expr = "LEFT(CONVERT(VARCHAR, PSP.Indate, 112), 6)"
|
||||
elif period == 'weekly':
|
||||
rx_date_group_expr = "CAST(YEAR(PSP.Indate) AS VARCHAR) + RIGHT('0' + CAST(DATEPART(WEEK, PSP.Indate) AS VARCHAR), 2)"
|
||||
else: # daily
|
||||
rx_date_group_expr = "CONVERT(VARCHAR, PSP.Indate, 112)"
|
||||
|
||||
cursor_pres.execute(f"""
|
||||
SELECT
|
||||
{rx_date_group_expr} as date_group,
|
||||
SUM(ISNULL(PSP.QUAN * PSP.QUAN_TIME * PSP.Days, 0)) as rx_usage,
|
||||
COUNT(DISTINCT PSP.PreSerial) as rx_count
|
||||
FROM PS_sub_pharm PSP
|
||||
WHERE PSP.DrugCode = ?
|
||||
AND PSP.Indate >= ?
|
||||
AND PSP.Indate <= ?
|
||||
GROUP BY {rx_date_group_expr}
|
||||
ORDER BY {rx_date_group_expr}
|
||||
""", (drug_code, start_date_fmt, end_date_fmt))
|
||||
|
||||
rx_rows = cursor_pres.fetchall()
|
||||
rx_data = {str(row.date_group): {'rx_usage': int(row.rx_usage or 0), 'rx_count': int(row.rx_count or 0)} for row in rx_rows}
|
||||
|
||||
# 5. 역순 누적 재고 계산 + 처방 사용량 병합
|
||||
items = []
|
||||
running_stock = current_stock
|
||||
|
||||
for row in stock_rows:
|
||||
date_group = str(row.date_group) if row.date_group else ''
|
||||
|
||||
# 날짜 포맷팅
|
||||
if period == 'monthly' and len(date_group) == 6:
|
||||
formatted_date = f"{date_group[:4]}-{date_group[4:6]}"
|
||||
elif period == 'weekly':
|
||||
formatted_date = date_group # YYYYWW
|
||||
else: # daily
|
||||
formatted_date = f"{date_group[:4]}-{date_group[4:6]}-{date_group[6:8]}" if len(date_group) == 8 else date_group
|
||||
|
||||
inbound = int(row.inbound or 0)
|
||||
outbound = int(row.outbound or 0)
|
||||
|
||||
rx_info = rx_data.get(date_group, {'rx_usage': 0, 'rx_count': 0})
|
||||
|
||||
items.append({
|
||||
'date': formatted_date,
|
||||
'date_group': date_group,
|
||||
'stock': running_stock,
|
||||
'inbound': inbound,
|
||||
'outbound': outbound,
|
||||
'rx_usage': rx_info['rx_usage'],
|
||||
'rx_count': rx_info['rx_count']
|
||||
})
|
||||
|
||||
running_stock = running_stock - inbound + outbound
|
||||
|
||||
# 시간순 정렬
|
||||
items.reverse()
|
||||
|
||||
# 6. 통계 계산
|
||||
total_rx_usage = sum(item['rx_usage'] for item in items)
|
||||
total_rx_count = sum(item['rx_count'] for item in items)
|
||||
avg_rx_usage = total_rx_usage / len(items) if items else 0
|
||||
|
||||
conn.close()
|
||||
conn_pres.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'drug_code': drug_code,
|
||||
'product_name': product_name,
|
||||
'supplier': supplier,
|
||||
'current_stock': current_stock,
|
||||
'period': period,
|
||||
'items': items,
|
||||
'stats': {
|
||||
'total_rx_usage': total_rx_usage,
|
||||
'total_rx_count': total_rx_count,
|
||||
'avg_rx_usage': round(avg_rx_usage, 1)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.close()
|
||||
if conn_pres:
|
||||
conn_pres.close()
|
||||
logging.error(f"stock-with-usage API 오류: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/stock-analytics/search-drugs')
|
||||
def api_search_drugs():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user