feat: 제품 검색 페이지 및 QR 라벨 인쇄 기능
- /admin/products: 전체 제품 검색 페이지 (OTC) - /api/products: 제품 검색 API (세트상품 바코드 포함) - qr_printer.py: Brother QL-710W 프린터 연동 - /api/qr-print, /api/qr-preview: QR 라벨 인쇄/미리보기 API - 판매상세 페이지에 QR 인쇄 버튼 추가 - 수량 선택 UI (+/- 버튼, 최대 10장) - 세트상품 제조사 표시 개선 - 대시보드 헤더에 제품검색/판매조회 탭 추가
This commit is contained in:
177
backend/app.py
177
backend/app.py
@@ -2461,6 +2461,93 @@ def admin_sales_detail():
|
||||
return render_template('admin_sales_detail.html')
|
||||
|
||||
|
||||
# ===== 제품 검색 페이지 =====
|
||||
|
||||
@app.route('/admin/products')
|
||||
def admin_products():
|
||||
"""제품 검색 페이지 (전체 재고에서 검색, QR 인쇄)"""
|
||||
return render_template('admin_products.html')
|
||||
|
||||
|
||||
@app.route('/api/products')
|
||||
def api_products():
|
||||
"""
|
||||
제품 검색 API
|
||||
- 상품명, 상품코드, 바코드로 검색
|
||||
- 세트상품 바코드도 CD_ITEM_UNIT_MEMBER에서 조회
|
||||
"""
|
||||
search = request.args.get('search', '').strip()
|
||||
limit = int(request.args.get('limit', 100))
|
||||
|
||||
if not search or len(search) < 2:
|
||||
return jsonify({'success': False, 'error': '검색어는 2글자 이상 입력하세요'})
|
||||
|
||||
try:
|
||||
drug_session = db_manager.get_session('PM_DRUG')
|
||||
|
||||
# 제품 검색 쿼리
|
||||
# CD_GOODS.BARCODE가 없으면 CD_ITEM_UNIT_MEMBER.CD_CD_BARCODE 사용 (세트상품)
|
||||
# 세트상품 여부 확인: CD_item_set에 SetCode로 존재하면 세트상품
|
||||
products_query = text(f"""
|
||||
SELECT TOP {limit}
|
||||
G.DrugCode as drug_code,
|
||||
G.GoodsName as product_name,
|
||||
COALESCE(NULLIF(G.BARCODE, ''), U.CD_CD_BARCODE, '') as barcode,
|
||||
G.Saleprice as sale_price,
|
||||
G.Price as cost_price,
|
||||
CASE
|
||||
WHEN G.SplName IS NOT NULL AND G.SplName != '' THEN G.SplName
|
||||
WHEN SET_CHK.is_set = 1 THEN N'세트상품'
|
||||
ELSE ''
|
||||
END as supplier,
|
||||
CASE WHEN SET_CHK.is_set = 1 THEN 1 ELSE 0 END as is_set
|
||||
FROM CD_GOODS G
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 CD_CD_BARCODE
|
||||
FROM CD_ITEM_UNIT_MEMBER
|
||||
WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != ''
|
||||
) U
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 1 as is_set
|
||||
FROM CD_item_set
|
||||
WHERE SetCode = G.DrugCode AND DrugCode = 'SET0000'
|
||||
) SET_CHK
|
||||
WHERE
|
||||
G.GoodsName LIKE :search_like
|
||||
OR G.DrugCode LIKE :search_like
|
||||
OR G.BARCODE LIKE :search_like
|
||||
OR U.CD_CD_BARCODE LIKE :search_like
|
||||
ORDER BY G.GoodsName
|
||||
""")
|
||||
|
||||
search_like = f'%{search}%'
|
||||
rows = drug_session.execute(products_query, {
|
||||
'search_like': search_like
|
||||
}).fetchall()
|
||||
|
||||
items = []
|
||||
for row in rows:
|
||||
items.append({
|
||||
'drug_code': row.drug_code or '',
|
||||
'product_name': row.product_name or '',
|
||||
'barcode': row.barcode or '',
|
||||
'sale_price': float(row.sale_price) if row.sale_price else 0,
|
||||
'cost_price': float(row.cost_price) if row.cost_price else 0,
|
||||
'supplier': row.supplier or '',
|
||||
'is_set': bool(row.is_set)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': items,
|
||||
'count': len(items)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"제품 검색 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/admin/sales')
|
||||
def admin_sales_pos():
|
||||
"""판매 내역 페이지 (POS 스타일, 거래별 그룹핑)"""
|
||||
@@ -2483,6 +2570,7 @@ def api_sales_detail():
|
||||
|
||||
# 판매 내역 조회 (최근 N일)
|
||||
# CD_GOODS.BARCODE가 없으면 CD_ITEM_UNIT_MEMBER.CD_CD_BARCODE 사용 (세트상품/자체등록 바코드)
|
||||
# 세트상품 여부 확인: CD_item_set에 SetCode로 존재하면 세트상품
|
||||
sales_query = text("""
|
||||
SELECT
|
||||
S.SL_DT_appl as sale_date,
|
||||
@@ -2490,7 +2578,11 @@ def api_sales_detail():
|
||||
S.DrugCode as drug_code,
|
||||
ISNULL(G.GoodsName, '알 수 없음') as product_name,
|
||||
COALESCE(NULLIF(G.BARCODE, ''), U.CD_CD_BARCODE, '') as barcode,
|
||||
ISNULL(G.SplName, '') as supplier,
|
||||
CASE
|
||||
WHEN G.SplName IS NOT NULL AND G.SplName != '' THEN G.SplName
|
||||
WHEN SET_CHK.is_set = 1 THEN '세트상품'
|
||||
ELSE ''
|
||||
END as supplier,
|
||||
ISNULL(S.QUAN, 1) as quantity,
|
||||
ISNULL(S.SL_TOTAL_PRICE, 0) as total_price_db,
|
||||
ISNULL(G.Saleprice, 0) as unit_price
|
||||
@@ -2501,6 +2593,11 @@ def api_sales_detail():
|
||||
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 >= CONVERT(VARCHAR(8), DATEADD(DAY, -:days, GETDATE()), 112)
|
||||
ORDER BY S.SL_DT_appl DESC, S.SL_NO_order DESC
|
||||
""")
|
||||
@@ -2745,6 +2842,84 @@ def api_claude_status():
|
||||
}), 500
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# QR 라벨 인쇄 API
|
||||
# =============================================================================
|
||||
|
||||
@app.route('/api/qr-print', methods=['POST'])
|
||||
def api_qr_print():
|
||||
"""QR 라벨 인쇄 API"""
|
||||
try:
|
||||
from qr_printer import print_drug_qr_label
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': '데이터가 없습니다'}), 400
|
||||
|
||||
drug_name = data.get('drug_name', '')
|
||||
barcode = data.get('barcode', '')
|
||||
drug_code = data.get('drug_code', '')
|
||||
sale_price = data.get('sale_price', 0)
|
||||
|
||||
if not drug_name:
|
||||
return jsonify({'success': False, 'error': '상품명이 필요합니다'}), 400
|
||||
|
||||
# 바코드가 없으면 drug_code 사용
|
||||
qr_data = barcode if barcode else drug_code
|
||||
|
||||
result = print_drug_qr_label(
|
||||
drug_name=drug_name,
|
||||
barcode=qr_data,
|
||||
sale_price=sale_price,
|
||||
drug_code=drug_code,
|
||||
pharmacy_name='청춘약국'
|
||||
)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
except ImportError as e:
|
||||
logging.error(f"QR 프린터 모듈 로드 실패: {e}")
|
||||
return jsonify({'success': False, 'error': 'QR 프린터 모듈이 없습니다'}), 500
|
||||
except Exception as e:
|
||||
logging.error(f"QR 인쇄 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/qr-preview', methods=['POST'])
|
||||
def api_qr_preview():
|
||||
"""QR 라벨 미리보기 API (base64 이미지 반환)"""
|
||||
try:
|
||||
from qr_printer import preview_qr_label
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'success': False, 'error': '데이터가 없습니다'}), 400
|
||||
|
||||
drug_name = data.get('drug_name', '')
|
||||
barcode = data.get('barcode', '')
|
||||
drug_code = data.get('drug_code', '')
|
||||
sale_price = data.get('sale_price', 0)
|
||||
|
||||
if not drug_name:
|
||||
return jsonify({'success': False, 'error': '상품명이 필요합니다'}), 400
|
||||
|
||||
qr_data = barcode if barcode else drug_code
|
||||
|
||||
image_data = preview_qr_label(
|
||||
drug_name=drug_name,
|
||||
barcode=qr_data,
|
||||
sale_price=sale_price,
|
||||
drug_code=drug_code,
|
||||
pharmacy_name='청춘약국'
|
||||
)
|
||||
|
||||
return jsonify({'success': True, 'image': image_data})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"QR 미리보기 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 개발 모드로 실행
|
||||
app.run(host='0.0.0.0', port=7001, debug=True)
|
||||
|
||||
Reference in New Issue
Block a user