- get_order_list(include_details=True)로 한 번에 조회 - 접수 상태(확정 전)도 집계에 포함 - pending_count, approved_count 응답에 추가
448 lines
13 KiB
Python
448 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
백제약품 도매상 API - Flask Blueprint
|
|
|
|
핵심 로직은 wholesale 패키지에서 가져옴
|
|
이 파일은 Flask 웹 API 연동만 담당
|
|
"""
|
|
|
|
import time
|
|
import logging
|
|
|
|
from flask import Blueprint, jsonify, request as flask_request
|
|
|
|
# wholesale 패키지 경로 설정
|
|
import wholesale_path
|
|
|
|
# wholesale 패키지에서 핵심 클래스 가져오기
|
|
from wholesale import BaekjeSession
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Blueprint 생성
|
|
baekje_bp = Blueprint('baekje', __name__, url_prefix='/api/baekje')
|
|
|
|
|
|
# ========== 세션 관리 ==========
|
|
|
|
_baekje_session = None
|
|
_init_started = False
|
|
|
|
def get_baekje_session():
|
|
global _baekje_session
|
|
if _baekje_session is None:
|
|
_baekje_session = BaekjeSession()
|
|
return _baekje_session
|
|
|
|
|
|
def init_baekje_session():
|
|
"""앱 시작 시 백그라운드에서 로그인 시작"""
|
|
global _init_started
|
|
if _init_started:
|
|
return
|
|
_init_started = True
|
|
|
|
session = get_baekje_session()
|
|
|
|
# 저장된 토큰이 있으면 즉시 사용 가능
|
|
if session._logged_in:
|
|
logger.info(f"백제약품: 저장된 토큰 사용 중")
|
|
return
|
|
|
|
# 백그라운드 로그인 시작
|
|
session.start_background_login()
|
|
logger.info(f"백제약품: 백그라운드 로그인 시작됨")
|
|
|
|
|
|
# 모듈 로드 시 자동 시작
|
|
try:
|
|
init_baekje_session()
|
|
except Exception as e:
|
|
logger.warning(f"백제약품 초기화 오류: {e}")
|
|
|
|
|
|
def search_baekje_stock(keyword: str):
|
|
"""백제약품 재고 검색"""
|
|
try:
|
|
session = get_baekje_session()
|
|
result = session.search_products(keyword)
|
|
|
|
if result.get('success'):
|
|
return {
|
|
'success': True,
|
|
'keyword': keyword,
|
|
'count': result['total'],
|
|
'items': result['items']
|
|
}
|
|
else:
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"백제약품 검색 오류: {e}")
|
|
return {'success': False, 'error': 'SEARCH_ERROR', 'message': str(e)}
|
|
|
|
|
|
# ========== Flask API Routes ==========
|
|
|
|
@baekje_bp.route('/stock', methods=['GET'])
|
|
def api_baekje_stock():
|
|
"""
|
|
백제약품 재고 조회 API
|
|
|
|
GET /api/baekje/stock?kd_code=672300240
|
|
GET /api/baekje/stock?keyword=타이레놀
|
|
"""
|
|
kd_code = flask_request.args.get('kd_code', '').strip()
|
|
keyword = flask_request.args.get('keyword', '').strip()
|
|
|
|
search_term = kd_code or keyword
|
|
|
|
if not search_term:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'MISSING_PARAM',
|
|
'message': 'kd_code 또는 keyword 파라미터가 필요합니다'
|
|
}), 400
|
|
|
|
try:
|
|
result = search_baekje_stock(search_term)
|
|
return jsonify(result)
|
|
except Exception as e:
|
|
logger.error(f"백제약품 API 오류: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'API_ERROR',
|
|
'message': str(e)
|
|
}), 500
|
|
|
|
|
|
@baekje_bp.route('/session-status', methods=['GET'])
|
|
def api_session_status():
|
|
"""세션 상태 확인"""
|
|
session = get_baekje_session()
|
|
return jsonify({
|
|
'success': True,
|
|
'wholesaler': 'baekje',
|
|
'name': '백제약품',
|
|
'logged_in': session._logged_in,
|
|
'last_login': session._last_login,
|
|
'session_timeout': session.SESSION_TIMEOUT
|
|
})
|
|
|
|
|
|
@baekje_bp.route('/login', methods=['POST'])
|
|
def api_login():
|
|
"""수동 로그인"""
|
|
session = get_baekje_session()
|
|
success = session.login()
|
|
return jsonify({
|
|
'success': success,
|
|
'message': '로그인 성공' if success else '로그인 실패'
|
|
})
|
|
|
|
|
|
@baekje_bp.route('/cart', methods=['GET'])
|
|
def api_get_cart():
|
|
"""장바구니 조회"""
|
|
session = get_baekje_session()
|
|
result = session.get_cart()
|
|
return jsonify(result)
|
|
|
|
|
|
@baekje_bp.route('/cart', methods=['POST'])
|
|
def api_add_to_cart():
|
|
"""
|
|
장바구니 추가
|
|
|
|
POST /api/baekje/cart
|
|
{
|
|
"product_code": "672300240",
|
|
"quantity": 2
|
|
}
|
|
"""
|
|
data = flask_request.get_json() or {}
|
|
product_code = data.get('product_code', '').strip()
|
|
quantity = int(data.get('quantity', 1))
|
|
|
|
if not product_code:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'MISSING_PARAM',
|
|
'message': 'product_code 필요'
|
|
}), 400
|
|
|
|
session = get_baekje_session()
|
|
result = session.add_to_cart(product_code, quantity)
|
|
return jsonify(result)
|
|
|
|
|
|
@baekje_bp.route('/order', methods=['POST'])
|
|
def api_submit_order():
|
|
"""
|
|
주문 등록
|
|
|
|
POST /api/baekje/order
|
|
{
|
|
"memo": "긴급 요청"
|
|
}
|
|
"""
|
|
data = flask_request.get_json() or {}
|
|
memo = data.get('memo', '')
|
|
|
|
session = get_baekje_session()
|
|
result = session.submit_order(memo)
|
|
return jsonify(result)
|
|
|
|
|
|
# ========== 프론트엔드 통합용 ==========
|
|
|
|
@baekje_bp.route('/search-for-order', methods=['POST'])
|
|
def api_search_for_order():
|
|
"""
|
|
발주용 재고 검색 (프론트엔드 통합용)
|
|
|
|
POST /api/baekje/search-for-order
|
|
{
|
|
"kd_code": "672300240",
|
|
"product_name": "타이레놀",
|
|
"specification": "500T"
|
|
}
|
|
"""
|
|
data = flask_request.get_json() or {}
|
|
kd_code = data.get('kd_code', '').strip()
|
|
product_name = data.get('product_name', '').strip()
|
|
specification = data.get('specification', '').strip()
|
|
|
|
search_term = kd_code or product_name
|
|
|
|
if not search_term:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'MISSING_PARAM'
|
|
}), 400
|
|
|
|
result = search_baekje_stock(search_term)
|
|
|
|
if result.get('success') and specification:
|
|
# 규격 필터링
|
|
filtered = [
|
|
item for item in result.get('items', [])
|
|
if specification.lower() in item.get('spec', '').lower()
|
|
]
|
|
result['items'] = filtered
|
|
result['count'] = len(filtered)
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
# ========== 잔고 조회 ==========
|
|
|
|
@baekje_bp.route('/balance', methods=['GET'])
|
|
def api_get_balance():
|
|
"""
|
|
잔고액 조회
|
|
|
|
GET /api/baekje/balance
|
|
GET /api/baekje/balance?year=2026
|
|
|
|
Returns:
|
|
{
|
|
"success": true,
|
|
"balance": 14193234,
|
|
"monthly": [
|
|
{"month": "2026-03", "sales": 6935133, "balance": 14193234, ...},
|
|
{"month": "2026-02", "sales": 18600692, "balance": 7258101, ...}
|
|
]
|
|
}
|
|
"""
|
|
year = flask_request.args.get('year', '').strip()
|
|
|
|
session = get_baekje_session()
|
|
result = session.get_balance(year if year else None)
|
|
return jsonify(result)
|
|
|
|
|
|
@baekje_bp.route('/orders/summary-by-kd', methods=['GET'])
|
|
def api_baekje_orders_by_kd():
|
|
"""
|
|
백제약품 주문량 KD코드별 집계 API
|
|
|
|
GET /api/baekje/orders/summary-by-kd?start_date=2026-03-01&end_date=2026-03-07
|
|
|
|
Returns:
|
|
{
|
|
"success": true,
|
|
"order_count": 4,
|
|
"by_kd_code": {
|
|
"670400830": {
|
|
"product_name": "레바미피드정",
|
|
"spec": "100T",
|
|
"boxes": 2,
|
|
"units": 200
|
|
}
|
|
},
|
|
"total_products": 15
|
|
}
|
|
"""
|
|
import re
|
|
from datetime import datetime
|
|
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
start_date = flask_request.args.get('start_date', today).strip()
|
|
end_date = flask_request.args.get('end_date', today).strip()
|
|
|
|
def parse_spec(spec: str, product_name: str = '') -> int:
|
|
"""
|
|
규격에서 수량 추출 (30T → 30, 100C → 100)
|
|
"""
|
|
combined = f"{spec} {product_name}"
|
|
|
|
# D(도즈) 단위는 박스 단위로 계산 (140D → 1)
|
|
if re.search(r'\d+\s*D\b', combined, re.IGNORECASE):
|
|
return 1
|
|
|
|
# T/C/P 단위가 붙은 숫자 추출 (예: 14T, 100C, 30P)
|
|
qty_match = re.search(r'(\d+)\s*[TCP]\b', combined, re.IGNORECASE)
|
|
if qty_match:
|
|
return int(qty_match.group(1))
|
|
|
|
# 없으면 spec의 첫 번째 숫자
|
|
if spec:
|
|
num_match = re.search(r'(\d+)', spec)
|
|
if num_match:
|
|
val = int(num_match.group(1))
|
|
# mg, ml 같은 용량 단위면 수량 1로 처리
|
|
if re.search(r'\d+\s*(mg|ml|g)\b', spec, re.IGNORECASE):
|
|
return 1
|
|
return val
|
|
|
|
return 1
|
|
|
|
try:
|
|
session = get_baekje_session()
|
|
|
|
# 주문 목록 + 상세를 한 번에 조회 (include_details=True)
|
|
# 접수 상태(확정 전)도 포함됨!
|
|
orders_result = session.get_order_list(start_date, end_date, include_details=True)
|
|
|
|
if not orders_result.get('success'):
|
|
return jsonify({
|
|
'success': False,
|
|
'error': orders_result.get('error', 'ORDERS_FETCH_FAILED'),
|
|
'by_kd_code': {},
|
|
'order_count': 0
|
|
})
|
|
|
|
orders = orders_result.get('orders', [])
|
|
|
|
if not orders:
|
|
return jsonify({
|
|
'success': True,
|
|
'order_count': 0,
|
|
'period': {'start': start_date, 'end': end_date},
|
|
'by_kd_code': {},
|
|
'total_products': 0,
|
|
'pending_count': 0,
|
|
'approved_count': 0
|
|
})
|
|
|
|
# KD코드별 집계 (items가 이미 각 order에 포함됨)
|
|
kd_summary = {}
|
|
|
|
for order in orders:
|
|
for item in order.get('items', []):
|
|
# 취소 상태 제외
|
|
status = item.get('status', '').strip()
|
|
if '취소' in status or '삭제' in status:
|
|
continue
|
|
|
|
# 백제는 kd_code가 insurance_code(BOHUM_CD)에 있음
|
|
kd_code = item.get('kd_code', '') or item.get('insurance_code', '')
|
|
if not kd_code:
|
|
continue
|
|
|
|
product_name = item.get('product_name', '')
|
|
spec = item.get('spec', '')
|
|
quantity = item.get('quantity', 0) or item.get('order_qty', 0)
|
|
per_unit = parse_spec(spec, product_name)
|
|
total_units = quantity * per_unit
|
|
|
|
if kd_code not in kd_summary:
|
|
kd_summary[kd_code] = {
|
|
'product_name': product_name,
|
|
'spec': spec,
|
|
'boxes': 0,
|
|
'units': 0
|
|
}
|
|
|
|
kd_summary[kd_code]['boxes'] += quantity
|
|
kd_summary[kd_code]['units'] += total_units
|
|
|
|
pending_count = orders_result.get('pending_count', 0)
|
|
approved_count = orders_result.get('approved_count', 0)
|
|
|
|
logger.info(f"백제 주문량 집계: {start_date}~{end_date}, {len(orders)}건 (접수:{pending_count}, 승인:{approved_count}), {len(kd_summary)}개 품목")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'order_count': len(orders),
|
|
'pending_count': pending_count, # 접수 상태 (확정 전)
|
|
'approved_count': approved_count, # 승인 상태 (확정됨)
|
|
'period': {'start': start_date, 'end': end_date},
|
|
'by_kd_code': kd_summary,
|
|
'total_products': len(kd_summary)
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"백제 주문량 집계 오류: {e}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'API_ERROR',
|
|
'message': str(e),
|
|
'by_kd_code': {},
|
|
'order_count': 0
|
|
}), 500
|
|
|
|
|
|
@baekje_bp.route('/monthly-sales', methods=['GET'])
|
|
def api_get_monthly_sales():
|
|
"""
|
|
월간 매출(주문) 합계 조회
|
|
|
|
GET /api/baekje/monthly-sales?year=2026&month=3
|
|
|
|
Returns:
|
|
{
|
|
"success": true,
|
|
"total_amount": 7305877, // 월간 매출 합계
|
|
"total_returns": 0, // 월간 반품 합계
|
|
"net_amount": 7305877, // 순매출 (매출 - 반품)
|
|
"total_paid": 0, // 월간 입금 합계
|
|
"ending_balance": 14563978, // 월말 잔액
|
|
"prev_balance": 14565453, // 전월이월금
|
|
"from_date": "2026-03-01",
|
|
"to_date": "2026-03-31",
|
|
"rotate_days": 58.4 // 회전일수
|
|
}
|
|
"""
|
|
from datetime import datetime
|
|
|
|
year = flask_request.args.get('year', '').strip()
|
|
month = flask_request.args.get('month', '').strip()
|
|
|
|
# 기본값: 현재 연월
|
|
now = datetime.now()
|
|
if not year:
|
|
year = now.year
|
|
else:
|
|
year = int(year)
|
|
|
|
if not month:
|
|
month = now.month
|
|
else:
|
|
month = int(month)
|
|
|
|
session = get_baekje_session()
|
|
result = session.get_monthly_sales(year, month)
|
|
return jsonify(result)
|