From 0ae4ae66f0c62f42c3255dcd12d27cecf9771eef Mon Sep 17 00:00:00 2001 From: thug0bin Date: Sat, 7 Mar 2026 21:29:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(baekje):=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EB=8B=B4=EA=B8=B0=20=EC=8B=9C=20internal=5Fcode=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kd_code 대신 internal_code로 장바구니 추가 - internal_code 없으면 검색 후 규격 매칭으로 찾기 - 백제 장바구니 담기 정상 작동 확인 --- backend/baekje_api.py | 142 ++++++++++++++++++++++++++ backend/check_basen.py | 36 +++++++ backend/check_basen_detail.py | 39 +++++++ backend/check_basen_html.py | 46 +++++++++ backend/check_lasix.py | 87 ++++++++-------- backend/check_lasix_spec.py | 10 ++ backend/order_api.py | 44 +++++++- backend/templates/admin_rx_usage.html | 25 ++++- backend/test_all_orders.py | 26 +++++ 9 files changed, 402 insertions(+), 53 deletions(-) create mode 100644 backend/check_basen.py create mode 100644 backend/check_basen_detail.py create mode 100644 backend/check_basen_html.py create mode 100644 backend/check_lasix_spec.py create mode 100644 backend/test_all_orders.py diff --git a/backend/baekje_api.py b/backend/baekje_api.py index 8539291..71b6e47 100644 --- a/backend/baekje_api.py +++ b/backend/baekje_api.py @@ -262,6 +262,148 @@ def api_get_balance(): 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() + + # 1. 주문 목록 조회 + orders_result = session.get_order_list(start_date, end_date) + + 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 + }) + + # 2. 주문 상세 정보 배치 조회 (items 포함) + details_result = session.get_order_details_batch(orders=orders) + + if not details_result.get('success'): + logger.warning(f"백제 주문 상세 조회 실패: {details_result.get('error')}") + + details = details_result.get('details', {}) + + # KD코드별 집계 + kd_summary = {} + + for order_num, detail in details.items(): + for item in detail.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 + + logger.info(f"백제 주문량 집계: {start_date}~{end_date}, {len(orders)}건 주문, {len(kd_summary)}개 품목") + + return jsonify({ + 'success': True, + 'order_count': len(orders), + '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(): """ diff --git a/backend/check_basen.py b/backend/check_basen.py new file mode 100644 index 0000000..aa82b1c --- /dev/null +++ b/backend/check_basen.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import requests + +print("=== 어제 베이슨 주문 (지오영 + 수인) ===\n") + +# 지오영 +geo = requests.get('http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date=2026-03-06&end_date=2026-03-06', timeout=120).json() +print("【지오영】") +found = False +for kd, info in geo.get('by_kd_code', {}).items(): + if '베이슨' in info['product_name']: + print(f" KD: {kd}") + print(f" 제품명: {info['product_name']}") + print(f" spec: {info['spec']}") + print(f" boxes: {info['boxes']}") + print(f" units: {info['units']}") + found = True +if not found: + print(" (없음)") + +print() + +# 수인 +sooin = requests.get('http://localhost:7001/api/sooin/orders/summary-by-kd?start_date=2026-03-06&end_date=2026-03-06', timeout=120).json() +print("【수인약품】") +found = False +for kd, info in sooin.get('by_kd_code', {}).items(): + if '베이슨' in info['product_name']: + print(f" KD: {kd}") + print(f" 제품명: {info['product_name']}") + print(f" spec: {info['spec']}") + print(f" boxes: {info['boxes']}") + print(f" units: {info['units']}") + found = True +if not found: + print(" (없음)") diff --git a/backend/check_basen_detail.py b/backend/check_basen_detail.py new file mode 100644 index 0000000..5e356d0 --- /dev/null +++ b/backend/check_basen_detail.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +import requests +import sys +sys.path.insert(0, 'c:/Users/청춘약국/source/pharmacy-wholesale-api') +from dotenv import load_dotenv +load_dotenv('c:/Users/청춘약국/source/pharmacy-wholesale-api/.env') +from wholesale import GeoYoungSession + +# 지오영 베이슨 검색 +print("=== 지오영 베이슨 검색 결과 ===") +res = requests.get('http://localhost:7001/api/geoyoung/stock?keyword=베이슨', timeout=30).json() +for item in res.get('items', []): + internal = item.get('internal_code', '') + spec = item.get('specification', '') + name = item.get('product_name', '') + print(f" 내부: {internal} | spec: {spec:8} | {name}") + +print() + +# 어제 주문 상세 +print("=== 어제 베이슨 주문 상세 ===") +session = GeoYoungSession() +session.login() +result = session.get_order_list('2026-03-06', '2026-03-06') + +if result.get('success'): + for order in result.get('orders', []): + for item in order.get('items', []): + name = item.get('product_name', '') + if '베이슨' in name: + internal = item.get('product_code', '') + qty = item.get('quantity', 0) + status = item.get('status', '') + print(f" 주문번호: {order.get('order_num')}") + print(f" 제품명: {name}") + print(f" 내부코드: {internal}") + print(f" 수량: {qty}박스") + print(f" 상태: {status}") + print() diff --git a/backend/check_basen_html.py b/backend/check_basen_html.py new file mode 100644 index 0000000..d972621 --- /dev/null +++ b/backend/check_basen_html.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import sys +sys.path.insert(0, 'c:/Users/청춘약국/source/pharmacy-wholesale-api') +from dotenv import load_dotenv +load_dotenv('c:/Users/청춘약국/source/pharmacy-wholesale-api/.env') +from wholesale import GeoYoungSession +from bs4 import BeautifulSoup + +session = GeoYoungSession() +session.login() + +# MyPage HTML 직접 확인 +resp = session.session.get( + f"{session.BASE_URL}/MyPage", + params={'dtpFrom': '2026-03-06', 'dtpTo': '2026-03-06'}, + timeout=30 +) + +soup = BeautifulSoup(resp.text, 'html.parser') +table = soup.find('table') +tbody = table.find('tbody') or table +rows = tbody.find_all('tr') + +print("=== 베이슨 행 HTML 분석 ===\n") +for row in rows: + cells = row.find_all('td') + if not cells or len(cells) < 10: + continue + + name = cells[1].get_text(strip=True) + if '베이슨' not in name: + continue + + status = cells[9].get_text(strip=True) if len(cells) > 9 else '' + print(f"제품명: {name}") + print(f"상태: {status}") + + # 모든 버튼의 onclick 확인 + print("버튼들:") + for cell in cells: + for btn in cell.find_all('button'): + onclick = btn.get('onclick', '') + btn_text = btn.get_text(strip=True) + print(f" [{btn_text}] onclick: {onclick[:80]}...") + + print() diff --git a/backend/check_lasix.py b/backend/check_lasix.py index b116669..4cf0298 100644 --- a/backend/check_lasix.py +++ b/backend/check_lasix.py @@ -1,48 +1,47 @@ # -*- coding: utf-8 -*- -import pyodbc +import sys +sys.path.insert(0, 'c:/Users/청춘약국/source/pharmacy-wholesale-api') +from dotenv import load_dotenv +load_dotenv('c:/Users/청춘약국/source/pharmacy-wholesale-api/.env') +from wholesale import GeoYoungSession -conn_str = ( - 'DRIVER={ODBC Driver 17 for SQL Server};' - 'SERVER=192.168.0.4\\PM2014;' - 'DATABASE=PM_DRUG;' - 'UID=sa;' - 'PWD=tmddls214!%(;' - 'TrustServerCertificate=yes;' - 'Connection Timeout=10' -) +session = GeoYoungSession() +session.login() -conn = pyodbc.connect(conn_str, timeout=10) -cur = conn.cursor() +# 어제 (3월 6일) 주문 조회 +result = session.get_order_list('2026-03-06', '2026-03-06') -# 라식스 약품 정보 조회 (전체 컬럼) -cur.execute(""" - SELECT TOP 1 * - FROM CD_GOODS - WHERE DrugCode = '652100200' -""") - -row = cur.fetchone() -if row: - columns = [desc[0] for desc in cur.description] - print("=== 라식스 약품 정보 ===") - for i, col in enumerate(columns): - if 'price' in col.lower() or 'cost' in col.lower() or 'amount' in col.lower(): - print(f"{col}: {row[i]}") - -# 처방전에서 라식스 DRUPRICE 확인 -conn2 = pyodbc.connect(conn_str.replace('PM_DRUG', 'PM_PRES'), timeout=10) -cur2 = conn2.cursor() - -cur2.execute(""" - SELECT TOP 5 DrugCode, QUAN, Days, DRUPRICE - FROM PS_sub_pharm - WHERE DrugCode = '652100200' - ORDER BY Indate DESC -""") - -print("\n=== 최근 처방 라식스 DRUPRICE ===") -for row in cur2.fetchall(): - print(f"DrugCode: {row.DrugCode}, QUAN: {row.QUAN}, Days: {row.Days}, DRUPRICE: {row.DRUPRICE}") - dose = row.QUAN * row.Days - amount = row.DRUPRICE * row.QUAN * row.Days - print(f" → 투약량: {dose}, 매출액: {amount:,}") +if result.get('success'): + # KD코드 매핑 + for order in result.get('orders', []): + items = order.get('items', []) + if items: + session._enrich_kd_codes(items) + + print("=== 어제 라식스 주문 상세 ===") + total_boxes = 0 + for order in result.get('orders', []): + for item in order.get('items', []): + name = item.get('product_name', '') + if '라식스' in name: + status = item.get('status', '') + qty = item.get('quantity', 0) + spec = item.get('spec', '') + internal = item.get('product_code', '') + kd = item.get('kd_code', '') + order_num = order.get('order_num', '') + print(f" 주문번호: {order_num}") + print(f" 제품명: {name}") + print(f" 내부코드: {internal}") + print(f" KD코드: {kd}") + print(f" spec: {spec}") + print(f" 수량: {qty}박스") + print(f" 상태: {status}") + print() + + if '취소' not in status and '삭제' not in status: + total_boxes += qty + + print(f"=== 합계 (취소 제외): {total_boxes}박스 ===") +else: + print(f"오류: {result.get('error')}") diff --git a/backend/check_lasix_spec.py b/backend/check_lasix_spec.py new file mode 100644 index 0000000..c6f32a7 --- /dev/null +++ b/backend/check_lasix_spec.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +import requests + +res = requests.get('http://localhost:7001/api/geoyoung/stock?keyword=라식스', timeout=30).json() +print("=== 지오영 라식스 검색 ===") +for item in res.get('items', []): + internal = item.get('internal_code', '') + spec = item.get('specification', '') + name = item.get('product_name', '') + print(f" 내부: {internal} | spec: {spec} | {name}") diff --git a/backend/order_api.py b/backend/order_api.py index 5b5822f..6336305 100644 --- a/backend/order_api.py +++ b/backend/order_api.py @@ -898,12 +898,45 @@ def submit_baekje_order(order: dict, dry_run: bool, cart_only: bool = True) -> d # ───────────────────────────────────────── for item in items: kd_code = item.get('kd_code') or item.get('drug_code') + internal_code = item.get('internal_code') # 프론트엔드에서 전달된 internal_code order_qty = item['order_qty'] spec = item.get('specification', '') + cart_result = {} + + # 🔍 디버그: 백제 주문 파라미터 확인 + logger.info(f"[BAEKJE DEBUG] kd_code={kd_code}, internal_code={internal_code}, qty={order_qty}, spec={spec}") + logger.info(f"[BAEKJE DEBUG] full item: {item}") try: - # 장바구니 추가 - cart_result = baekje_session.add_to_cart(kd_code, order_qty) + if internal_code: + # internal_code가 있으면 바로 장바구니 추가! + logger.info(f"[BAEKJE DEBUG] Using internal_code directly: {internal_code}") + cart_result = baekje_session.add_to_cart(internal_code, order_qty) + logger.info(f"[BAEKJE DEBUG] add_to_cart result: {cart_result}") + else: + # internal_code가 없으면 검색 후 장바구니 추가 + logger.info(f"[BAEKJE DEBUG] No internal_code, searching by kd_code={kd_code}") + search_result = baekje_session.search_products(kd_code) + + if search_result.get('success') and search_result.get('items'): + # 규격 매칭 (재고 있는 것 우선) + matched_item = None + for baekje_item in search_result.get('items', []): + item_spec = baekje_item.get('spec', '') + # 규격이 지정되어 있으면 매칭, 없으면 첫번째 재고 있는 것 + if not spec or spec in item_spec or item_spec in spec: + if matched_item is None or baekje_item.get('stock', 0) > matched_item.get('stock', 0): + matched_item = baekje_item + + if matched_item: + found_internal_code = matched_item.get('internal_code') + logger.info(f"[BAEKJE DEBUG] Found internal_code via search: {found_internal_code}") + cart_result = baekje_session.add_to_cart(found_internal_code, order_qty) + internal_code = found_internal_code # 컨텍스트 저장용 + else: + cart_result = {'success': False, 'error': 'NO_MATCHING_SPEC', 'message': f'규격 {spec} 미발견'} + else: + cart_result = {'success': False, 'error': 'PRODUCT_NOT_FOUND', 'message': '제품 검색 결과 없음'} if cart_result.get('success'): status = 'success' @@ -921,6 +954,7 @@ def submit_baekje_order(order: dict, dry_run: bool, cart_only: bool = True) -> d result_code = 'ERROR' result_message = str(e) failed_count += 1 + logger.error(f"[BAEKJE DEBUG] Exception: {e}") update_item_result(item['id'], status, result_code, result_message) @@ -932,7 +966,8 @@ def submit_baekje_order(order: dict, dry_run: bool, cart_only: bool = True) -> d 'ordered_spec': spec, 'ordered_qty': order_qty, 'selection_reason': 'user_order', - 'wholesaler_id': 'baekje' + 'wholesaler_id': 'baekje', + 'internal_code': internal_code }) results.append({ @@ -943,7 +978,8 @@ def submit_baekje_order(order: dict, dry_run: bool, cart_only: bool = True) -> d 'order_qty': order_qty, 'status': status, 'result_code': result_code, - 'result_message': result_message + 'result_message': result_message, + 'internal_code': internal_code }) # cart_only=False면 주문 확정까지 진행 diff --git a/backend/templates/admin_rx_usage.html b/backend/templates/admin_rx_usage.html index 59484c2..4f06f6b 100644 --- a/backend/templates/admin_rx_usage.html +++ b/backend/templates/admin_rx_usage.html @@ -907,7 +907,7 @@ loadOrderData(); // 수인약품 주문량 로드 }); - // ──────────────── 도매상 주문량 조회 (지오영 + 수인 합산) ──────────────── + // ──────────────── 도매상 주문량 조회 (지오영 + 수인 + 백제 합산) ──────────────── async function loadOrderData() { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; @@ -916,10 +916,11 @@ orderDataByKd = {}; try { - // 지오영 + 수인 병렬 조회 - const [geoRes, sooinRes] = await Promise.all([ + // 지오영 + 수인 + 백제 병렬 조회 + const [geoRes, sooinRes, baekjeRes] = await Promise.all([ fetch(`/api/geoyoung/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })), - fetch(`/api/sooin/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })) + fetch(`/api/sooin/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })), + fetch(`/api/baekje/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })) ]); let totalOrders = 0; @@ -952,7 +953,21 @@ console.log('💜 수인 주문량:', Object.keys(sooinRes.by_kd_code).length, '품목,', sooinRes.order_count, '건'); } - console.log('📦 합산 주문량:', Object.keys(orderDataByKd).length, '품목,', totalOrders, '건 주문'); + // 백제 데이터 합산 + if (baekjeRes.success && baekjeRes.by_kd_code) { + for (const [kd, data] of Object.entries(baekjeRes.by_kd_code)) { + if (!orderDataByKd[kd]) { + orderDataByKd[kd] = { product_name: data.product_name, spec: data.spec, boxes: 0, units: 0, sources: [] }; + } + orderDataByKd[kd].boxes += data.boxes || 0; + orderDataByKd[kd].units += data.units || 0; + orderDataByKd[kd].sources.push('백제'); + } + totalOrders += baekjeRes.order_count || 0; + console.log('💉 백제 주문량:', Object.keys(baekjeRes.by_kd_code).length, '품목,', baekjeRes.order_count, '건'); + } + + console.log('📦 3사 합산 주문량:', Object.keys(orderDataByKd).length, '품목,', totalOrders, '건 주문'); } catch(err) { console.warn('주문량 조회 실패:', err); diff --git a/backend/test_all_orders.py b/backend/test_all_orders.py new file mode 100644 index 0000000..1a1ebbd --- /dev/null +++ b/backend/test_all_orders.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +import requests + +print('=== 주문량 API 테스트 (지오영 + 수인 + 백제) ===') + +date = '2026-03-07' + +# 지오영 +geo = requests.get(f'http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date={date}&end_date={date}', timeout=60).json() +geo_count = len(geo.get('by_kd_code', {})) +print(f'지오영: {"OK" if geo.get("success") else "FAIL"} - {geo_count}개 품목') + +# 수인 +sooin = requests.get(f'http://localhost:7001/api/sooin/orders/summary-by-kd?start_date={date}&end_date={date}', timeout=60).json() +sooin_count = len(sooin.get('by_kd_code', {})) +print(f'수인: {"OK" if sooin.get("success") else "FAIL"} - {sooin_count}개 품목') + +# 백제 +baekje = requests.get(f'http://localhost:7001/api/baekje/orders/summary-by-kd?start_date={date}&end_date={date}', timeout=60).json() +baekje_count = len(baekje.get('by_kd_code', {})) +print(f'백제: {"OK" if baekje.get("success") else "FAIL"} - {baekje_count}개 품목') +if baekje.get('message'): + print(f' 메시지: {baekje.get("message")}') + +print() +print(f'총 품목: {geo_count + sooin_count + baekje_count}개')