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:
thug0bin 2026-03-06 01:07:04 +09:00
parent 0d9f4c9a23
commit 0460085791
3 changed files with 2531 additions and 0 deletions

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff