feat: 전문의약품(Rx) 사용량 조회 페이지 + OTC 사용량 페이지 추가
- /admin/rx-usage: 전문의약품 사용량 조회 (현재고 포함) - /admin/usage: OTC 일반약 사용량 조회 - /api/rx-usage: 처방전 데이터 기반 품목별 집계 API - /api/usage: POS 판매 데이터 기반 품목별 집계 API - 현재고: IM_total.IM_QT_sale_debit 사용 - 장바구니 + 주문서 생성 기능 - 세로모니터 대응 미디어쿼리 포함
This commit is contained in:
parent
0d9f4c9a23
commit
0460085791
330
backend/app.py
330
backend/app.py
@ -3877,6 +3877,336 @@ def api_sales_detail():
|
||||
}), 500
|
||||
|
||||
|
||||
# ===== 사용량 조회 페이지 및 API =====
|
||||
|
||||
@app.route('/admin/usage')
|
||||
def admin_usage():
|
||||
"""OTC 사용량 조회 · 주문 페이지"""
|
||||
return render_template('admin_usage.html')
|
||||
|
||||
|
||||
@app.route('/admin/rx-usage')
|
||||
def admin_rx_usage():
|
||||
"""전문의약품 사용량 조회 · 주문 페이지"""
|
||||
return render_template('admin_rx_usage.html')
|
||||
|
||||
|
||||
@app.route('/api/usage')
|
||||
def api_usage():
|
||||
"""
|
||||
기간별 품목 사용량 조회 API
|
||||
GET /api/usage?start_date=2026-01-01&end_date=2026-01-31&search=타이레놀&sort=qty_desc
|
||||
"""
|
||||
try:
|
||||
start_date = request.args.get('start_date', '')
|
||||
end_date = request.args.get('end_date', '')
|
||||
search = request.args.get('search', '').strip()
|
||||
sort = request.args.get('sort', 'qty_desc') # qty_desc, qty_asc, name_asc, amount_desc
|
||||
|
||||
# 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD)
|
||||
start_date_fmt = start_date.replace('-', '') if start_date else datetime.now().strftime('%Y%m%d')
|
||||
end_date_fmt = end_date.replace('-', '') if end_date else datetime.now().strftime('%Y%m%d')
|
||||
|
||||
mssql_session = db_manager.get_session('PM_PRES')
|
||||
|
||||
# 품목별 사용량 집계 쿼리
|
||||
usage_query = text("""
|
||||
SELECT
|
||||
S.DrugCode as drug_code,
|
||||
ISNULL(G.GoodsName, '알 수 없음') as product_name,
|
||||
CASE
|
||||
WHEN G.SplName IS NOT NULL AND G.SplName != '' THEN G.SplName
|
||||
WHEN SET_CHK.is_set = 1 THEN '세트상품'
|
||||
ELSE ''
|
||||
END as supplier,
|
||||
SUM(ISNULL(S.QUAN, 1)) as total_qty,
|
||||
SUM(ISNULL(S.SL_TOTAL_PRICE, 0)) as total_amount,
|
||||
COALESCE(NULLIF(G.BARCODE, ''), U.CD_CD_BARCODE, '') as barcode
|
||||
FROM SALE_SUB S
|
||||
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 CD_CD_BARCODE
|
||||
FROM PM_DRUG.dbo.CD_ITEM_UNIT_MEMBER
|
||||
WHERE DRUGCODE = S.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != ''
|
||||
) U
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 1 as is_set
|
||||
FROM PM_DRUG.dbo.CD_item_set
|
||||
WHERE SetCode = S.DrugCode AND DrugCode = 'SET0000'
|
||||
) SET_CHK
|
||||
WHERE S.SL_DT_appl >= :start_date
|
||||
AND S.SL_DT_appl <= :end_date
|
||||
GROUP BY S.DrugCode, G.GoodsName, G.SplName, SET_CHK.is_set, G.BARCODE, U.CD_CD_BARCODE
|
||||
ORDER BY SUM(ISNULL(S.QUAN, 1)) DESC
|
||||
""")
|
||||
|
||||
rows = mssql_session.execute(usage_query, {
|
||||
'start_date': start_date_fmt,
|
||||
'end_date': end_date_fmt
|
||||
}).fetchall()
|
||||
|
||||
items = []
|
||||
total_qty = 0
|
||||
total_amount = 0
|
||||
|
||||
for row in rows:
|
||||
drug_code = row.drug_code or ''
|
||||
product_name = row.product_name or ''
|
||||
|
||||
# 검색 필터
|
||||
if search:
|
||||
search_lower = search.lower()
|
||||
if (search_lower not in product_name.lower() and
|
||||
search_lower not in drug_code.lower()):
|
||||
continue
|
||||
|
||||
qty = int(row.total_qty or 0)
|
||||
amount = float(row.total_amount or 0)
|
||||
|
||||
items.append({
|
||||
'drug_code': drug_code,
|
||||
'product_name': product_name,
|
||||
'supplier': row.supplier or '',
|
||||
'barcode': row.barcode or '',
|
||||
'total_qty': qty,
|
||||
'total_amount': int(amount),
|
||||
'thumbnail': None
|
||||
})
|
||||
|
||||
total_qty += qty
|
||||
total_amount += amount
|
||||
|
||||
# 정렬
|
||||
if sort == 'qty_asc':
|
||||
items.sort(key=lambda x: x['total_qty'])
|
||||
elif sort == 'qty_desc':
|
||||
items.sort(key=lambda x: x['total_qty'], reverse=True)
|
||||
elif sort == 'name_asc':
|
||||
items.sort(key=lambda x: x['product_name'])
|
||||
elif sort == 'amount_desc':
|
||||
items.sort(key=lambda x: x['total_amount'], reverse=True)
|
||||
|
||||
# 제품 이미지 조회
|
||||
try:
|
||||
images_db_path = Path(__file__).parent / 'db' / 'product_images.db'
|
||||
if images_db_path.exists():
|
||||
img_conn = sqlite3.connect(str(images_db_path))
|
||||
img_cursor = img_conn.cursor()
|
||||
|
||||
barcodes = [item['barcode'] for item in items if item['barcode']]
|
||||
drug_codes = [item['drug_code'] for item in items]
|
||||
|
||||
image_map = {}
|
||||
if barcodes:
|
||||
placeholders = ','.join(['?' for _ in barcodes])
|
||||
img_cursor.execute(f'''
|
||||
SELECT barcode, thumbnail_base64
|
||||
FROM product_images
|
||||
WHERE barcode IN ({placeholders}) AND thumbnail_base64 IS NOT NULL
|
||||
''', barcodes)
|
||||
for r in img_cursor.fetchall():
|
||||
image_map[f'bc:{r[0]}'] = r[1]
|
||||
|
||||
if drug_codes:
|
||||
placeholders = ','.join(['?' for _ in drug_codes])
|
||||
img_cursor.execute(f'''
|
||||
SELECT drug_code, thumbnail_base64
|
||||
FROM product_images
|
||||
WHERE drug_code IN ({placeholders}) AND thumbnail_base64 IS NOT NULL
|
||||
''', drug_codes)
|
||||
for r in img_cursor.fetchall():
|
||||
if f'dc:{r[0]}' not in image_map:
|
||||
image_map[f'dc:{r[0]}'] = r[1]
|
||||
|
||||
img_conn.close()
|
||||
|
||||
for item in items:
|
||||
thumb = image_map.get(f'bc:{item["barcode"]}') or image_map.get(f'dc:{item["drug_code"]}')
|
||||
if thumb:
|
||||
item['thumbnail'] = thumb
|
||||
except Exception as img_err:
|
||||
logging.warning(f"제품 이미지 조회 오류: {img_err}")
|
||||
|
||||
# 기간 일수 계산
|
||||
try:
|
||||
from datetime import datetime as dt
|
||||
start_dt = dt.strptime(start_date_fmt, '%Y%m%d')
|
||||
end_dt = dt.strptime(end_date_fmt, '%Y%m%d')
|
||||
period_days = (end_dt - start_dt).days + 1
|
||||
except:
|
||||
period_days = 1
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': items[:500], # 최대 500건
|
||||
'stats': {
|
||||
'period_days': period_days,
|
||||
'product_count': len(items),
|
||||
'total_qty': total_qty,
|
||||
'total_amount': int(total_amount)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"사용량 조회 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/rx-usage')
|
||||
def api_rx_usage():
|
||||
"""
|
||||
전문의약품(처방전) 기간별 사용량 조회 API
|
||||
GET /api/rx-usage?start_date=2026-01-01&end_date=2026-01-31&search=타이레놀&sort=qty_desc
|
||||
"""
|
||||
try:
|
||||
start_date = request.args.get('start_date', '')
|
||||
end_date = request.args.get('end_date', '')
|
||||
search = request.args.get('search', '').strip()
|
||||
sort = request.args.get('sort', 'qty_desc')
|
||||
|
||||
# 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD)
|
||||
start_date_fmt = start_date.replace('-', '') if start_date else datetime.now().strftime('%Y%m%d')
|
||||
end_date_fmt = end_date.replace('-', '') if end_date else datetime.now().strftime('%Y%m%d')
|
||||
|
||||
mssql_session = db_manager.get_session('PM_PRES')
|
||||
|
||||
# 전문의약품 품목별 사용량 집계 쿼리 (현재고: IM_total.IM_QT_sale_debit)
|
||||
rx_query = text("""
|
||||
SELECT
|
||||
P.DrugCode as drug_code,
|
||||
ISNULL(G.GoodsName, '알 수 없음') as product_name,
|
||||
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,
|
||||
COUNT(DISTINCT P.PreSerial) as prescription_count,
|
||||
COALESCE(NULLIF(G.BARCODE, ''), '') as barcode,
|
||||
ISNULL(IT.IM_QT_sale_debit, 0) as current_stock
|
||||
FROM PS_sub_pharm P
|
||||
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON P.DrugCode = G.DrugCode
|
||||
LEFT JOIN PM_DRUG.dbo.IM_total IT ON P.DrugCode = IT.DrugCode
|
||||
WHERE P.Indate >= :start_date
|
||||
AND P.Indate <= :end_date
|
||||
GROUP BY P.DrugCode, G.GoodsName, G.SplName, G.BARCODE, IT.IM_QT_sale_debit
|
||||
ORDER BY SUM(ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) DESC
|
||||
""")
|
||||
|
||||
rows = mssql_session.execute(rx_query, {
|
||||
'start_date': start_date_fmt,
|
||||
'end_date': end_date_fmt
|
||||
}).fetchall()
|
||||
|
||||
items = []
|
||||
total_qty = 0
|
||||
total_dose = 0
|
||||
total_amount = 0
|
||||
total_prescriptions = set()
|
||||
|
||||
for row in rows:
|
||||
drug_code = row.drug_code or ''
|
||||
product_name = row.product_name or ''
|
||||
|
||||
# 검색 필터
|
||||
if search:
|
||||
search_lower = search.lower()
|
||||
if (search_lower not in product_name.lower() and
|
||||
search_lower not in drug_code.lower()):
|
||||
continue
|
||||
|
||||
qty = int(row.total_qty or 0)
|
||||
dose = int(row.total_dose or 0)
|
||||
amount = float(row.total_amount or 0)
|
||||
rx_count = int(row.prescription_count or 0)
|
||||
|
||||
items.append({
|
||||
'drug_code': drug_code,
|
||||
'product_name': product_name,
|
||||
'supplier': row.supplier or '',
|
||||
'barcode': row.barcode or '',
|
||||
'total_qty': qty,
|
||||
'total_dose': dose, # 총 투약량 (수량 x 일수)
|
||||
'total_amount': int(amount),
|
||||
'prescription_count': rx_count,
|
||||
'current_stock': int(row.current_stock or 0), # 현재고
|
||||
'thumbnail': None
|
||||
})
|
||||
|
||||
total_qty += qty
|
||||
total_dose += dose
|
||||
total_amount += amount
|
||||
|
||||
# 정렬
|
||||
if sort == 'qty_asc':
|
||||
items.sort(key=lambda x: x['total_dose'])
|
||||
elif sort == 'qty_desc':
|
||||
items.sort(key=lambda x: x['total_dose'], reverse=True)
|
||||
elif sort == 'name_asc':
|
||||
items.sort(key=lambda x: x['product_name'])
|
||||
elif sort == 'amount_desc':
|
||||
items.sort(key=lambda x: x['total_amount'], reverse=True)
|
||||
elif sort == 'rx_desc':
|
||||
items.sort(key=lambda x: x['prescription_count'], reverse=True)
|
||||
|
||||
# 제품 이미지 조회
|
||||
try:
|
||||
images_db_path = Path(__file__).parent / 'db' / 'product_images.db'
|
||||
if images_db_path.exists():
|
||||
img_conn = sqlite3.connect(str(images_db_path))
|
||||
img_cursor = img_conn.cursor()
|
||||
|
||||
drug_codes = [item['drug_code'] for item in items]
|
||||
|
||||
image_map = {}
|
||||
if drug_codes:
|
||||
placeholders = ','.join(['?' for _ in drug_codes])
|
||||
img_cursor.execute(f'''
|
||||
SELECT drug_code, thumbnail_base64
|
||||
FROM product_images
|
||||
WHERE drug_code IN ({placeholders}) AND thumbnail_base64 IS NOT NULL
|
||||
''', drug_codes)
|
||||
for r in img_cursor.fetchall():
|
||||
image_map[r[0]] = r[1]
|
||||
|
||||
img_conn.close()
|
||||
|
||||
for item in items:
|
||||
if item['drug_code'] in image_map:
|
||||
item['thumbnail'] = image_map[item['drug_code']]
|
||||
except Exception as img_err:
|
||||
logging.warning(f"제품 이미지 조회 오류: {img_err}")
|
||||
|
||||
# 기간 일수 계산
|
||||
try:
|
||||
from datetime import datetime as dt
|
||||
start_dt = dt.strptime(start_date_fmt, '%Y%m%d')
|
||||
end_dt = dt.strptime(end_date_fmt, '%Y%m%d')
|
||||
period_days = (end_dt - start_dt).days + 1
|
||||
except:
|
||||
period_days = 1
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': items[:500],
|
||||
'stats': {
|
||||
'period_days': period_days,
|
||||
'product_count': len(items),
|
||||
'total_qty': total_qty,
|
||||
'total_dose': total_dose,
|
||||
'total_amount': int(total_amount)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"전문의약품 사용량 조회 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
# ===== Claude 상태 API =====
|
||||
|
||||
@app.route('/api/claude-status')
|
||||
|
||||
1121
backend/templates/admin_rx_usage.html
Normal file
1121
backend/templates/admin_rx_usage.html
Normal file
File diff suppressed because it is too large
Load Diff
1080
backend/templates/admin_usage.html
Normal file
1080
backend/templates/admin_usage.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user