chore: 개발 파일 정리 및 구조화
- 개발/테스트 스크립트를 dev_scripts/ 폴더로 이동 - 스크린샷을 screenshots/ 폴더로 이동 - 백업 파일 보존 (.backup) - 처방 관련 추가 스크립트 포함 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
120
dev_scripts/analyze_db_structure.py
Normal file
120
dev_scripts/analyze_db_structure.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
데이터베이스 구조 정확히 분석
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def analyze_structure():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("데이터베이스 구조 완전 분석")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. herb_items 분석
|
||||
print("\n1. herb_items 테이블 (재고 관리):")
|
||||
cursor.execute("SELECT COUNT(*) FROM herb_items")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" - 레코드 수: {count}")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT herb_item_id, insurance_code, herb_name, ingredient_code
|
||||
FROM herb_items
|
||||
WHERE herb_item_id IN (1, 2, 3)
|
||||
ORDER BY herb_item_id
|
||||
""")
|
||||
print(" - 샘플 데이터:")
|
||||
for row in cursor.fetchall():
|
||||
print(f" ID={row[0]}: {row[2]} (보험코드: {row[1]}, 성분코드: {row[3]})")
|
||||
|
||||
# 2. herb_masters 분석
|
||||
print("\n2. herb_masters 테이블 (성분코드 마스터):")
|
||||
cursor.execute("SELECT COUNT(*) FROM herb_masters")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" - 레코드 수: {count}")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name
|
||||
FROM herb_masters
|
||||
WHERE herb_name IN ('인삼', '감초', '당귀')
|
||||
""")
|
||||
print(" - 주요 약재:")
|
||||
for row in cursor.fetchall():
|
||||
print(f" {row[0]}: {row[1]}")
|
||||
|
||||
# 3. herb_master_extended 분석
|
||||
print("\n3. herb_master_extended 테이블 (확장 정보):")
|
||||
cursor.execute("SELECT COUNT(*) FROM herb_master_extended")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f" - 레코드 수: {count}")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT herb_id, ingredient_code, name_korean
|
||||
FROM herb_master_extended
|
||||
WHERE name_korean IN ('인삼', '감초', '당귀')
|
||||
""")
|
||||
print(" - 주요 약재 herb_id:")
|
||||
for row in cursor.fetchall():
|
||||
print(f" herb_id={row[0]}: {row[2]} (성분코드: {row[1]})")
|
||||
|
||||
# 4. 관계 매핑 확인
|
||||
print("\n4. 테이블 간 관계:")
|
||||
print(" herb_items.ingredient_code → herb_masters.ingredient_code")
|
||||
print(" herb_masters.ingredient_code → herb_master_extended.ingredient_code")
|
||||
print(" herb_master_extended.herb_id → herb_item_tags.herb_id")
|
||||
|
||||
# 5. 올바른 JOIN 경로 제시
|
||||
print("\n5. 올바른 JOIN 방법:")
|
||||
print("""
|
||||
방법 1: herb_items에서 시작 (재고 있는 약재만)
|
||||
-----------------------------------------------
|
||||
FROM herb_items hi
|
||||
LEFT JOIN herb_masters hm ON hi.ingredient_code = hm.ingredient_code
|
||||
LEFT JOIN herb_master_extended hme ON hm.ingredient_code = hme.ingredient_code
|
||||
LEFT JOIN herb_item_tags hit ON hme.herb_id = hit.herb_id
|
||||
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
|
||||
|
||||
방법 2: herb_masters에서 시작 (모든 약재)
|
||||
-----------------------------------------------
|
||||
FROM herb_masters hm
|
||||
LEFT JOIN herb_master_extended hme ON hm.ingredient_code = hme.ingredient_code
|
||||
LEFT JOIN herb_item_tags hit ON hme.herb_id = hit.herb_id
|
||||
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
|
||||
LEFT JOIN (재고 서브쿼리) inv ON hm.ingredient_code = inv.ingredient_code
|
||||
""")
|
||||
|
||||
# 6. 실제 JOIN 테스트
|
||||
print("\n6. JOIN 테스트 (인삼 예시):")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
hi.herb_item_id,
|
||||
hi.herb_name as item_name,
|
||||
hi.ingredient_code,
|
||||
hme.herb_id as master_herb_id,
|
||||
hme.name_korean as master_name,
|
||||
GROUP_CONCAT(het.tag_name) as tags
|
||||
FROM herb_items hi
|
||||
LEFT JOIN herb_masters hm ON hi.ingredient_code = hm.ingredient_code
|
||||
LEFT JOIN herb_master_extended hme ON hm.ingredient_code = hme.ingredient_code
|
||||
LEFT JOIN herb_item_tags hit ON hme.herb_id = hit.herb_id
|
||||
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
|
||||
WHERE hi.ingredient_code = '3400H1AHM'
|
||||
GROUP BY hi.herb_item_id
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
print(f" herb_item_id: {result[0]}")
|
||||
print(f" 약재명: {result[1]}")
|
||||
print(f" 성분코드: {result[2]}")
|
||||
print(f" master_herb_id: {result[3]}")
|
||||
print(f" master 약재명: {result[4]}")
|
||||
print(f" 효능 태그: {result[5]}")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_structure()
|
||||
198
dev_scripts/analyze_excel_formats.py
Normal file
198
dev_scripts/analyze_excel_formats.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Excel 파일 형식 분석 도구
|
||||
한의사랑과 한의정보 형식 비교
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def analyze_excel_format(file_path, format_name):
|
||||
"""Excel 파일 형식 분석"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"📊 {format_name} 형식 분석")
|
||||
print(f"파일: {file_path}")
|
||||
print('='*60)
|
||||
|
||||
try:
|
||||
# Excel 파일 읽기
|
||||
df = pd.read_excel(file_path)
|
||||
|
||||
# 기본 정보
|
||||
print(f"\n1️⃣ 기본 정보:")
|
||||
print(f" - 행 개수: {len(df)}")
|
||||
print(f" - 열 개수: {len(df.columns)}")
|
||||
|
||||
# 컬럼 정보
|
||||
print(f"\n2️⃣ 컬럼 목록:")
|
||||
for i, col in enumerate(df.columns, 1):
|
||||
print(f" {i}. {col}")
|
||||
|
||||
# 데이터 타입
|
||||
print(f"\n3️⃣ 데이터 타입:")
|
||||
for col in df.columns:
|
||||
print(f" - {col}: {df[col].dtype}")
|
||||
|
||||
# 샘플 데이터 (처음 3행)
|
||||
print(f"\n4️⃣ 샘플 데이터 (처음 3행):")
|
||||
print(df.head(3).to_string(index=False))
|
||||
|
||||
# 누락 데이터 확인
|
||||
print(f"\n5️⃣ 누락 데이터:")
|
||||
null_counts = df.isnull().sum()
|
||||
for col in df.columns:
|
||||
if null_counts[col] > 0:
|
||||
print(f" - {col}: {null_counts[col]}개 누락")
|
||||
if null_counts.sum() == 0:
|
||||
print(" - 누락 데이터 없음")
|
||||
|
||||
# 고유값 개수 (참고용)
|
||||
print(f"\n6️⃣ 고유값 개수:")
|
||||
for col in df.columns:
|
||||
unique_count = df[col].nunique()
|
||||
print(f" - {col}: {unique_count}개")
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 오류 발생: {str(e)}")
|
||||
return None
|
||||
|
||||
def compare_formats(df1, df2, name1, name2):
|
||||
"""두 형식 비교"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🔄 {name1} vs {name2} 형식 비교")
|
||||
print('='*60)
|
||||
|
||||
if df1 is None or df2 is None:
|
||||
print("비교할 수 없습니다 (데이터 로드 실패)")
|
||||
return
|
||||
|
||||
cols1 = set(df1.columns)
|
||||
cols2 = set(df2.columns)
|
||||
|
||||
# 공통 컬럼
|
||||
common = cols1.intersection(cols2)
|
||||
print(f"\n✅ 공통 컬럼 ({len(common)}개):")
|
||||
for col in sorted(common):
|
||||
print(f" - {col}")
|
||||
|
||||
# 한의사랑에만 있는 컬럼
|
||||
only_in_1 = cols1 - cols2
|
||||
if only_in_1:
|
||||
print(f"\n📌 {name1}에만 있는 컬럼 ({len(only_in_1)}개):")
|
||||
for col in sorted(only_in_1):
|
||||
print(f" - {col}")
|
||||
|
||||
# 한의정보에만 있는 컬럼
|
||||
only_in_2 = cols2 - cols1
|
||||
if only_in_2:
|
||||
print(f"\n📌 {name2}에만 있는 컬럼 ({len(only_in_2)}개):")
|
||||
for col in sorted(only_in_2):
|
||||
print(f" - {col}")
|
||||
|
||||
# 컬럼명 매핑 추천
|
||||
print(f"\n🔗 컬럼 매핑 추천:")
|
||||
|
||||
# 가능한 매핑 찾기
|
||||
mappings = []
|
||||
|
||||
# 날짜 관련
|
||||
date_cols1 = [c for c in cols1 if '일' in c or '날짜' in c or 'date' in c.lower()]
|
||||
date_cols2 = [c for c in cols2 if '일' in c or '날짜' in c or 'date' in c.lower()]
|
||||
if date_cols1 and date_cols2:
|
||||
mappings.append((date_cols1[0], date_cols2[0], "날짜"))
|
||||
|
||||
# 약재명 관련
|
||||
herb_cols1 = [c for c in cols1 if '약재' in c or '품목' in c or '제품' in c]
|
||||
herb_cols2 = [c for c in cols2 if '약재' in c or '품목' in c or '제품' in c]
|
||||
if herb_cols1 and herb_cols2:
|
||||
mappings.append((herb_cols1[0], herb_cols2[0], "약재명"))
|
||||
|
||||
# 수량 관련
|
||||
qty_cols1 = [c for c in cols1 if '수량' in c or '량' in c or '구입량' in c]
|
||||
qty_cols2 = [c for c in cols2 if '수량' in c or '량' in c or '구입량' in c]
|
||||
if qty_cols1 and qty_cols2:
|
||||
mappings.append((qty_cols1[0], qty_cols2[0], "수량"))
|
||||
|
||||
# 금액 관련
|
||||
amt_cols1 = [c for c in cols1 if '금액' in c or '액' in c or '가격' in c]
|
||||
amt_cols2 = [c for c in cols2 if '금액' in c or '액' in c or '가격' in c]
|
||||
if amt_cols1 and amt_cols2:
|
||||
mappings.append((amt_cols1[0], amt_cols2[0], "금액"))
|
||||
|
||||
# 업체 관련
|
||||
supplier_cols1 = [c for c in cols1 if '업체' in c or '도매' in c or '공급' in c]
|
||||
supplier_cols2 = [c for c in cols2 if '업체' in c or '도매' in c or '공급' in c]
|
||||
if supplier_cols1 and supplier_cols2:
|
||||
mappings.append((supplier_cols1[0], supplier_cols2[0], "공급업체"))
|
||||
|
||||
# 원산지 관련
|
||||
origin_cols1 = [c for c in cols1 if '원산지' in c or '산지' in c]
|
||||
origin_cols2 = [c for c in cols2 if '원산지' in c or '산지' in c]
|
||||
if origin_cols1 and origin_cols2:
|
||||
mappings.append((origin_cols1[0], origin_cols2[0], "원산지"))
|
||||
|
||||
for col1, col2, mapping_type in mappings:
|
||||
print(f" - {mapping_type}: [{name1}]{col1} ↔ [{name2}]{col2}")
|
||||
|
||||
def main():
|
||||
"""메인 함수"""
|
||||
print("\n" + "="*60)
|
||||
print("🏥 한약 입고장 Excel 형식 분석기")
|
||||
print("="*60)
|
||||
|
||||
# 파일 경로
|
||||
hanisarang_path = '/root/kdrug/sample/한의사랑.xlsx'
|
||||
haninfo_path = '/root/kdrug/sample/한의정보.xlsx'
|
||||
current_path = '/root/kdrug/sample/order_view_20260215154829.xlsx'
|
||||
|
||||
# 각 형식 분석
|
||||
df_hanisarang = None
|
||||
df_haninfo = None
|
||||
df_current = None
|
||||
|
||||
if os.path.exists(hanisarang_path):
|
||||
df_hanisarang = analyze_excel_format(hanisarang_path, "한의사랑")
|
||||
else:
|
||||
print(f"❌ 한의사랑 파일을 찾을 수 없음: {hanisarang_path}")
|
||||
|
||||
if os.path.exists(haninfo_path):
|
||||
df_haninfo = analyze_excel_format(haninfo_path, "한의정보")
|
||||
else:
|
||||
print(f"❌ 한의정보 파일을 찾을 수 없음: {haninfo_path}")
|
||||
|
||||
# 현재 사용 중인 형식도 분석
|
||||
if os.path.exists(current_path):
|
||||
df_current = analyze_excel_format(current_path, "현재 사용 중")
|
||||
|
||||
# 형식 비교
|
||||
if df_hanisarang is not None and df_haninfo is not None:
|
||||
compare_formats(df_hanisarang, df_haninfo, "한의사랑", "한의정보")
|
||||
|
||||
# 통합 매핑 제안
|
||||
print(f"\n{'='*60}")
|
||||
print("💡 통합 컬럼 매핑 제안")
|
||||
print('='*60)
|
||||
|
||||
print("""
|
||||
시스템에서 사용할 표준 컬럼:
|
||||
1. insurance_code (보험코드/제품코드)
|
||||
2. supplier_name (업체명/도매상)
|
||||
3. herb_name (약재명/품목명)
|
||||
4. receipt_date (구입일자/입고일)
|
||||
5. quantity (구입량/수량) - 그램 단위
|
||||
6. total_amount (구입액/금액)
|
||||
7. origin_country (원산지)
|
||||
8. unit_price (단가) - 계산 가능한 경우
|
||||
|
||||
각 형식별 매핑 규칙을 자동으로 적용하여
|
||||
어떤 형식의 Excel 파일도 처리 가능하도록 구현 가능
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
244
dev_scripts/analyze_inventory_discrepancy.py
Normal file
244
dev_scripts/analyze_inventory_discrepancy.py
Normal file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
재고 자산 금액 불일치 분석 스크립트
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, getcontext
|
||||
|
||||
# Decimal 정밀도 설정
|
||||
getcontext().prec = 10
|
||||
|
||||
def analyze_inventory_discrepancy():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("재고 자산 금액 불일치 분석")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. 현재 inventory_lots 기준 재고 자산 계산
|
||||
print("1. 현재 시스템 재고 자산 계산 (inventory_lots 기준)")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(quantity_onhand * unit_price_per_g) as total_value,
|
||||
COUNT(*) as lot_count,
|
||||
SUM(quantity_onhand) as total_quantity
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
system_total = result['total_value'] or 0
|
||||
|
||||
print(f" 총 재고 자산: ₩{system_total:,.0f}")
|
||||
print(f" 총 LOT 수: {result['lot_count']}개")
|
||||
print(f" 총 재고량: {result['total_quantity']:,.1f}g")
|
||||
print()
|
||||
|
||||
# 2. 원본 입고장 데이터 분석
|
||||
print("2. 입고장 기준 계산")
|
||||
print("-" * 60)
|
||||
|
||||
# 전체 입고 금액
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(total_price) as total_purchase,
|
||||
COUNT(*) as receipt_count,
|
||||
SUM(quantity_g) as total_quantity
|
||||
FROM purchase_receipts
|
||||
""")
|
||||
|
||||
receipts = cursor.fetchone()
|
||||
total_purchase = receipts['total_purchase'] or 0
|
||||
|
||||
print(f" 총 입고 금액: ₩{total_purchase:,.0f}")
|
||||
print(f" 총 입고장 수: {receipts['receipt_count']}건")
|
||||
print(f" 총 입고량: {receipts['total_quantity']:,.1f}g")
|
||||
print()
|
||||
|
||||
# 3. 출고 데이터 분석
|
||||
print("3. 출고 데이터 분석")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(pd.quantity * il.unit_price_per_g) as total_dispensed_value,
|
||||
SUM(pd.quantity) as total_dispensed_quantity,
|
||||
COUNT(DISTINCT p.prescription_id) as prescription_count
|
||||
FROM prescription_details pd
|
||||
JOIN prescriptions p ON pd.prescription_id = p.prescription_id
|
||||
JOIN inventory_lots il ON pd.lot_id = il.lot_id
|
||||
WHERE p.status IN ('completed', 'dispensed')
|
||||
""")
|
||||
|
||||
dispensed = cursor.fetchone()
|
||||
total_dispensed_value = dispensed['total_dispensed_value'] or 0
|
||||
|
||||
print(f" 총 출고 금액: ₩{total_dispensed_value:,.0f}")
|
||||
print(f" 총 출고량: {dispensed['total_dispensed_quantity'] or 0:,.1f}g")
|
||||
print(f" 총 처방전 수: {dispensed['prescription_count']}건")
|
||||
print()
|
||||
|
||||
# 4. 재고 보정 데이터 분석
|
||||
print("4. 재고 보정 데이터 분석")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
adjustment_type,
|
||||
SUM(quantity) as total_quantity,
|
||||
SUM(quantity * unit_price) as total_value,
|
||||
COUNT(*) as count
|
||||
FROM stock_adjustments
|
||||
GROUP BY adjustment_type
|
||||
""")
|
||||
|
||||
adjustments = cursor.fetchall()
|
||||
total_adjustment_value = 0
|
||||
|
||||
for adj in adjustments:
|
||||
adj_type = adj['adjustment_type']
|
||||
value = adj['total_value'] or 0
|
||||
|
||||
# 보정 타입에 따른 금액 계산
|
||||
if adj_type in ['disposal', 'loss', 'decrease']:
|
||||
total_adjustment_value -= value
|
||||
print(f" {adj_type}: -₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
|
||||
else:
|
||||
total_adjustment_value += value
|
||||
print(f" {adj_type}: +₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
|
||||
|
||||
print(f" 순 보정 금액: ₩{total_adjustment_value:,.0f}")
|
||||
print()
|
||||
|
||||
# 5. 예상 재고 자산 계산
|
||||
print("5. 예상 재고 자산 계산")
|
||||
print("-" * 60)
|
||||
|
||||
expected_value = total_purchase - total_dispensed_value + total_adjustment_value
|
||||
|
||||
print(f" 입고 금액: ₩{total_purchase:,.0f}")
|
||||
print(f" - 출고 금액: ₩{total_dispensed_value:,.0f}")
|
||||
print(f" + 보정 금액: ₩{total_adjustment_value:,.0f}")
|
||||
print(f" = 예상 재고 자산: ₩{expected_value:,.0f}")
|
||||
print()
|
||||
|
||||
# 6. 차이 분석
|
||||
print("6. 차이 분석")
|
||||
print("-" * 60)
|
||||
|
||||
discrepancy = system_total - expected_value
|
||||
discrepancy_pct = (discrepancy / expected_value * 100) if expected_value != 0 else 0
|
||||
|
||||
print(f" 시스템 재고 자산: ₩{system_total:,.0f}")
|
||||
print(f" 예상 재고 자산: ₩{expected_value:,.0f}")
|
||||
print(f" 차이: ₩{discrepancy:,.0f} ({discrepancy_pct:+.2f}%)")
|
||||
print()
|
||||
|
||||
# 7. 상세 불일치 원인 분석
|
||||
print("7. 잠재적 불일치 원인 분석")
|
||||
print("-" * 60)
|
||||
|
||||
# 7-1. LOT과 입고장 매칭 확인
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as unmatched_lots
|
||||
FROM inventory_lots il
|
||||
WHERE il.receipt_id IS NULL AND il.is_depleted = 0
|
||||
""")
|
||||
unmatched = cursor.fetchone()
|
||||
|
||||
if unmatched['unmatched_lots'] > 0:
|
||||
print(f" ⚠️ 입고장과 매칭되지 않은 LOT: {unmatched['unmatched_lots']}개")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
herb_name,
|
||||
lot_number,
|
||||
quantity_onhand,
|
||||
unit_price_per_g,
|
||||
quantity_onhand * unit_price_per_g as value
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE il.receipt_id IS NULL AND il.is_depleted = 0
|
||||
ORDER BY value DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
unmatched_lots = cursor.fetchall()
|
||||
for lot in unmatched_lots:
|
||||
print(f" - {lot['herb_name']} (LOT: {lot['lot_number']}): ₩{lot['value']:,.0f}")
|
||||
|
||||
# 7-2. 단가 변동 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
MIN(il.unit_price_per_g) as min_price,
|
||||
MAX(il.unit_price_per_g) as max_price,
|
||||
AVG(il.unit_price_per_g) as avg_price,
|
||||
MAX(il.unit_price_per_g) - MIN(il.unit_price_per_g) as price_diff
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
|
||||
GROUP BY h.herb_item_id, h.herb_name
|
||||
HAVING price_diff > 0
|
||||
ORDER BY price_diff DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
price_variations = cursor.fetchall()
|
||||
if price_variations:
|
||||
print(f"\n ⚠️ 단가 변동이 큰 약재 (동일 약재 다른 단가):")
|
||||
for item in price_variations:
|
||||
print(f" - {item['herb_name']}: ₩{item['min_price']:.2f} ~ ₩{item['max_price']:.2f} (차이: ₩{item['price_diff']:.2f})")
|
||||
|
||||
# 7-3. 입고장 없는 출고 확인
|
||||
cursor.execute("""
|
||||
SELECT COUNT(DISTINCT pd.lot_id) as orphan_dispenses
|
||||
FROM prescription_details pd
|
||||
LEFT JOIN inventory_lots il ON pd.lot_id = il.lot_id
|
||||
WHERE il.lot_id IS NULL
|
||||
""")
|
||||
orphan = cursor.fetchone()
|
||||
|
||||
if orphan['orphan_dispenses'] > 0:
|
||||
print(f"\n ⚠️ LOT 정보 없는 출고: {orphan['orphan_dispenses']}건")
|
||||
|
||||
# 7-4. 음수 재고 확인
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as negative_stock
|
||||
FROM inventory_lots
|
||||
WHERE quantity_onhand < 0
|
||||
""")
|
||||
negative = cursor.fetchone()
|
||||
|
||||
if negative['negative_stock'] > 0:
|
||||
print(f"\n ⚠️ 음수 재고 LOT: {negative['negative_stock']}개")
|
||||
|
||||
# 8. 권장사항
|
||||
print("\n8. 권장사항")
|
||||
print("-" * 60)
|
||||
|
||||
if abs(discrepancy) > 1000:
|
||||
print(" 🔴 상당한 금액 차이가 발생했습니다. 다음 사항을 확인하세요:")
|
||||
print(" 1) 모든 입고장이 inventory_lots에 정확히 반영되었는지 확인")
|
||||
print(" 2) 출고 시 올바른 LOT과 단가가 적용되었는지 확인")
|
||||
print(" 3) 재고 보정 내역이 정확히 기록되었는지 확인")
|
||||
print(" 4) 초기 재고 입력 시 단가가 정확했는지 확인")
|
||||
|
||||
if unmatched['unmatched_lots'] > 0:
|
||||
print(f" 5) 입고장과 매칭되지 않은 {unmatched['unmatched_lots']}개 LOT 확인 필요")
|
||||
else:
|
||||
print(" ✅ 재고 자산이 대체로 일치합니다.")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_inventory_discrepancy()
|
||||
315
dev_scripts/analyze_inventory_full.py
Normal file
315
dev_scripts/analyze_inventory_full.py
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
재고 자산 금액 불일치 상세 분석
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, getcontext
|
||||
|
||||
# Decimal 정밀도 설정
|
||||
getcontext().prec = 10
|
||||
|
||||
def analyze_inventory_discrepancy():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("재고 자산 금액 불일치 상세 분석")
|
||||
print("분석 시간:", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. 현재 inventory_lots 기준 재고 자산
|
||||
print("1. 현재 시스템 재고 자산 (inventory_lots 테이블)")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(quantity_onhand * unit_price_per_g) as total_value,
|
||||
COUNT(*) as lot_count,
|
||||
SUM(quantity_onhand) as total_quantity,
|
||||
COUNT(DISTINCT herb_item_id) as herb_count
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
system_total = result['total_value'] or 0
|
||||
|
||||
print(f" 💰 총 재고 자산: ₩{system_total:,.0f}")
|
||||
print(f" 📦 활성 LOT 수: {result['lot_count']}개")
|
||||
print(f" ⚖️ 총 재고량: {result['total_quantity']:,.1f}g")
|
||||
print(f" 🌿 약재 종류: {result['herb_count']}종")
|
||||
print()
|
||||
|
||||
# 2. 입고장 기준 분석
|
||||
print("2. 입고장 데이터 분석 (purchase_receipts + purchase_receipt_lines)")
|
||||
print("-" * 60)
|
||||
|
||||
# 전체 입고 금액 (purchase_receipt_lines 기준)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(prl.line_total) as total_purchase,
|
||||
COUNT(DISTINCT pr.receipt_id) as receipt_count,
|
||||
COUNT(*) as line_count,
|
||||
SUM(prl.quantity_g) as total_quantity
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
|
||||
""")
|
||||
|
||||
receipts = cursor.fetchone()
|
||||
total_purchase = receipts['total_purchase'] or 0
|
||||
|
||||
print(f" 📋 총 입고 금액: ₩{total_purchase:,.0f}")
|
||||
print(f" 📑 입고장 수: {receipts['receipt_count']}건")
|
||||
print(f" 📝 입고 라인 수: {receipts['line_count']}개")
|
||||
print(f" ⚖️ 총 입고량: {receipts['total_quantity']:,.1f}g")
|
||||
|
||||
# 입고장별 요약도 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
pr.receipt_id,
|
||||
pr.receipt_no,
|
||||
pr.receipt_date,
|
||||
pr.total_amount as receipt_total,
|
||||
SUM(prl.line_total) as lines_sum
|
||||
FROM purchase_receipts pr
|
||||
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
|
||||
GROUP BY pr.receipt_id
|
||||
ORDER BY pr.receipt_date DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
print("\n 최근 입고장 5건:")
|
||||
recent_receipts = cursor.fetchall()
|
||||
for r in recent_receipts:
|
||||
print(f" - {r['receipt_no']} ({r['receipt_date']}): ₩{r['lines_sum']:,.0f}")
|
||||
|
||||
print()
|
||||
|
||||
# 3. inventory_lots와 purchase_receipt_lines 매칭 분석
|
||||
print("3. LOT-입고장 매칭 분석")
|
||||
print("-" * 60)
|
||||
|
||||
# receipt_line_id로 연결된 LOT 분석
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total_lots,
|
||||
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN 1 ELSE 0 END) as matched_lots,
|
||||
SUM(CASE WHEN receipt_line_id IS NULL THEN 1 ELSE 0 END) as unmatched_lots,
|
||||
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN quantity_onhand * unit_price_per_g ELSE 0 END) as matched_value,
|
||||
SUM(CASE WHEN receipt_line_id IS NULL THEN quantity_onhand * unit_price_per_g ELSE 0 END) as unmatched_value
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
matching = cursor.fetchone()
|
||||
|
||||
print(f" ✅ 입고장과 연결된 LOT: {matching['matched_lots']}개 (₩{matching['matched_value']:,.0f})")
|
||||
print(f" ❌ 입고장 없는 LOT: {matching['unmatched_lots']}개 (₩{matching['unmatched_value']:,.0f})")
|
||||
|
||||
if matching['unmatched_lots'] > 0:
|
||||
print("\n 입고장 없는 LOT 상세:")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
il.lot_number,
|
||||
il.quantity_onhand,
|
||||
il.unit_price_per_g,
|
||||
il.quantity_onhand * il.unit_price_per_g as value,
|
||||
il.received_date
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE il.receipt_line_id IS NULL
|
||||
AND il.is_depleted = 0
|
||||
AND il.quantity_onhand > 0
|
||||
ORDER BY value DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
unmatched_lots = cursor.fetchall()
|
||||
for lot in unmatched_lots:
|
||||
print(f" - {lot['herb_name']} (LOT: {lot['lot_number']})")
|
||||
print(f" 재고: {lot['quantity_onhand']:,.0f}g, 단가: ₩{lot['unit_price_per_g']:.2f}, 금액: ₩{lot['value']:,.0f}")
|
||||
|
||||
print()
|
||||
|
||||
# 4. 입고장 라인과 LOT 비교
|
||||
print("4. 입고장 라인별 LOT 생성 확인")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total_lines,
|
||||
SUM(CASE WHEN il.lot_id IS NOT NULL THEN 1 ELSE 0 END) as lines_with_lot,
|
||||
SUM(CASE WHEN il.lot_id IS NULL THEN 1 ELSE 0 END) as lines_without_lot
|
||||
FROM purchase_receipt_lines prl
|
||||
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
""")
|
||||
|
||||
line_matching = cursor.fetchone()
|
||||
|
||||
print(f" 📝 전체 입고 라인: {line_matching['total_lines']}개")
|
||||
print(f" ✅ LOT 생성된 라인: {line_matching['lines_with_lot']}개")
|
||||
print(f" ❌ LOT 없는 라인: {line_matching['lines_without_lot']}개")
|
||||
|
||||
if line_matching['lines_without_lot'] > 0:
|
||||
print("\n ⚠️ LOT이 생성되지 않은 입고 라인이 있습니다!")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
pr.receipt_no,
|
||||
pr.receipt_date,
|
||||
h.herb_name,
|
||||
prl.quantity_g,
|
||||
prl.line_total
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
|
||||
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
|
||||
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
WHERE il.lot_id IS NULL
|
||||
ORDER BY prl.line_total DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
missing_lots = cursor.fetchall()
|
||||
for line in missing_lots:
|
||||
print(f" - {line['receipt_no']} ({line['receipt_date']}): {line['herb_name']}")
|
||||
print(f" 수량: {line['quantity_g']:,.0f}g, 금액: ₩{line['line_total']:,.0f}")
|
||||
|
||||
print()
|
||||
|
||||
# 5. 금액 차이 계산
|
||||
print("5. 재고 자산 차이 분석")
|
||||
print("-" * 60)
|
||||
|
||||
# 입고장 라인별로 생성된 LOT의 현재 재고 가치 합계
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(il.quantity_onhand * il.unit_price_per_g) as current_lot_value,
|
||||
SUM(prl.line_total) as original_purchase_value
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
|
||||
""")
|
||||
|
||||
value_comparison = cursor.fetchone()
|
||||
|
||||
if value_comparison['current_lot_value']:
|
||||
print(f" 💰 현재 LOT 재고 가치: ₩{value_comparison['current_lot_value']:,.0f}")
|
||||
print(f" 📋 원본 입고 금액: ₩{value_comparison['original_purchase_value']:,.0f}")
|
||||
print(f" 📊 차이: ₩{(value_comparison['current_lot_value'] - value_comparison['original_purchase_value']):,.0f}")
|
||||
|
||||
print()
|
||||
|
||||
# 6. 출고 내역 확인
|
||||
print("6. 출고 및 소비 내역")
|
||||
print("-" * 60)
|
||||
|
||||
# 처방전을 통한 출고가 있는지 확인
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name IN ('prescriptions', 'prescription_details')
|
||||
""")
|
||||
prescription_tables = cursor.fetchall()
|
||||
|
||||
if len(prescription_tables) == 2:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(pd.quantity * il.unit_price_per_g) as dispensed_value,
|
||||
SUM(pd.quantity) as dispensed_quantity,
|
||||
COUNT(DISTINCT p.prescription_id) as prescription_count
|
||||
FROM prescription_details pd
|
||||
JOIN prescriptions p ON pd.prescription_id = p.prescription_id
|
||||
JOIN inventory_lots il ON pd.lot_id = il.lot_id
|
||||
WHERE p.status IN ('completed', 'dispensed')
|
||||
""")
|
||||
|
||||
dispensed = cursor.fetchone()
|
||||
if dispensed and dispensed['dispensed_value']:
|
||||
print(f" 💊 처방 출고 금액: ₩{dispensed['dispensed_value']:,.0f}")
|
||||
print(f" ⚖️ 처방 출고량: {dispensed['dispensed_quantity']:,.1f}g")
|
||||
print(f" 📋 처방전 수: {dispensed['prescription_count']}건")
|
||||
else:
|
||||
print(" 처방전 테이블이 없습니다.")
|
||||
|
||||
# 복합제 소비 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(cc.quantity_used * il.unit_price_per_g) as compound_value,
|
||||
SUM(cc.quantity_used) as compound_quantity,
|
||||
COUNT(DISTINCT cc.compound_id) as compound_count
|
||||
FROM compound_consumptions cc
|
||||
JOIN inventory_lots il ON cc.lot_id = il.lot_id
|
||||
""")
|
||||
|
||||
compounds = cursor.fetchone()
|
||||
if compounds and compounds['compound_value']:
|
||||
print(f" 🏭 복합제 소비 금액: ₩{compounds['compound_value']:,.0f}")
|
||||
print(f" ⚖️ 복합제 소비량: {compounds['compound_quantity']:,.1f}g")
|
||||
print(f" 📦 복합제 수: {compounds['compound_count']}개")
|
||||
|
||||
print()
|
||||
|
||||
# 7. 재고 보정 내역
|
||||
print("7. 재고 보정 내역")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
adjustment_type,
|
||||
SUM(quantity) as total_quantity,
|
||||
SUM(quantity * unit_price) as total_value,
|
||||
COUNT(*) as count
|
||||
FROM stock_adjustments
|
||||
GROUP BY adjustment_type
|
||||
""")
|
||||
|
||||
adjustments = cursor.fetchall()
|
||||
total_adjustment = 0
|
||||
|
||||
for adj in adjustments:
|
||||
adj_type = adj['adjustment_type']
|
||||
value = adj['total_value'] or 0
|
||||
|
||||
if adj_type in ['disposal', 'loss', 'decrease']:
|
||||
total_adjustment -= value
|
||||
print(f" ➖ {adj_type}: -₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
|
||||
else:
|
||||
total_adjustment += value
|
||||
print(f" ➕ {adj_type}: +₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
|
||||
|
||||
print(f"\n 📊 순 보정 금액: ₩{total_adjustment:,.0f}")
|
||||
print()
|
||||
|
||||
# 8. 최종 분석 결과
|
||||
print("8. 최종 분석 결과")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n 💰 화면 표시 재고 자산: ₩5,875,708")
|
||||
print(f" 📊 실제 계산 재고 자산: ₩{system_total:,.0f}")
|
||||
print(f" ❗ 차이: ₩{5875708 - system_total:,.0f}")
|
||||
|
||||
print("\n 🔍 불일치 원인:")
|
||||
|
||||
if matching['unmatched_lots'] > 0:
|
||||
print(f" 1) 입고장과 연결되지 않은 LOT {matching['unmatched_lots']}개 (₩{matching['unmatched_value']:,.0f})")
|
||||
|
||||
if line_matching['lines_without_lot'] > 0:
|
||||
print(f" 2) LOT이 생성되지 않은 입고 라인 {line_matching['lines_without_lot']}개")
|
||||
|
||||
print(f" 3) 화면의 ₩5,875,708과 실제 DB의 ₩{system_total:,.0f} 차이")
|
||||
|
||||
# 화면에 표시되는 금액이 어디서 오는지 추가 확인
|
||||
print("\n 💡 추가 확인 필요사항:")
|
||||
print(" - 프론트엔드에서 재고 자산을 계산하는 로직 확인")
|
||||
print(" - 캐시된 데이터나 별도 계산 로직이 있는지 확인")
|
||||
print(" - inventory_lots_v2 테이블 데이터와 비교 필요")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_inventory_discrepancy()
|
||||
186
dev_scripts/analyze_price_difference.py
Normal file
186
dev_scripts/analyze_price_difference.py
Normal file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
입고 단가와 LOT 단가 차이 분석
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def analyze_price_difference():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("입고 단가와 LOT 단가 차이 상세 분석")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. 입고 라인과 LOT의 단가 차이 분석
|
||||
print("1. 입고 라인 vs LOT 단가 비교")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
prl.line_id,
|
||||
prl.quantity_g as purchase_qty,
|
||||
prl.unit_price_per_g as purchase_price,
|
||||
prl.line_total as purchase_total,
|
||||
il.quantity_received as lot_received_qty,
|
||||
il.quantity_onhand as lot_current_qty,
|
||||
il.unit_price_per_g as lot_price,
|
||||
il.quantity_received * il.unit_price_per_g as lot_original_value,
|
||||
il.quantity_onhand * il.unit_price_per_g as lot_current_value,
|
||||
ABS(prl.unit_price_per_g - il.unit_price_per_g) as price_diff,
|
||||
prl.line_total - (il.quantity_received * il.unit_price_per_g) as value_diff
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
|
||||
WHERE ABS(prl.unit_price_per_g - il.unit_price_per_g) > 0.01
|
||||
OR ABS(prl.quantity_g - il.quantity_received) > 0.01
|
||||
ORDER BY ABS(value_diff) DESC
|
||||
""")
|
||||
|
||||
diffs = cursor.fetchall()
|
||||
|
||||
if diffs:
|
||||
print(f" ⚠️ 단가 또는 수량이 다른 항목: {len(diffs)}개\n")
|
||||
|
||||
total_value_diff = 0
|
||||
for i, diff in enumerate(diffs[:10], 1):
|
||||
print(f" {i}. {diff['herb_name']}")
|
||||
print(f" 입고: {diff['purchase_qty']:,.0f}g × ₩{diff['purchase_price']:.2f} = ₩{diff['purchase_total']:,.0f}")
|
||||
print(f" LOT: {diff['lot_received_qty']:,.0f}g × ₩{diff['lot_price']:.2f} = ₩{diff['lot_original_value']:,.0f}")
|
||||
print(f" 차이: ₩{diff['value_diff']:,.0f}")
|
||||
total_value_diff += diff['value_diff']
|
||||
print()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT SUM(prl.line_total - (il.quantity_received * il.unit_price_per_g)) as total_diff
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
""")
|
||||
total_diff = cursor.fetchone()['total_diff'] or 0
|
||||
|
||||
print(f" 총 차이 금액: ₩{total_diff:,.0f}")
|
||||
else:
|
||||
print(" ✅ 모든 입고 라인과 LOT의 단가/수량이 일치합니다.")
|
||||
|
||||
# 2. 입고 총액과 LOT 생성 총액 비교
|
||||
print("\n2. 입고 총액 vs LOT 생성 총액")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(prl.line_total) as purchase_total,
|
||||
SUM(il.quantity_received * il.unit_price_per_g) as lot_creation_total
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
""")
|
||||
|
||||
totals = cursor.fetchone()
|
||||
|
||||
print(f" 입고장 총액: ₩{totals['purchase_total']:,.0f}")
|
||||
print(f" LOT 생성 총액: ₩{totals['lot_creation_total']:,.0f}")
|
||||
print(f" 차이: ₩{totals['purchase_total'] - totals['lot_creation_total']:,.0f}")
|
||||
|
||||
# 3. 소비로 인한 차이 분석
|
||||
print("\n3. 소비 내역 상세 분석")
|
||||
print("-" * 60)
|
||||
|
||||
# 복합제 소비 상세
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
c.compound_name,
|
||||
h.herb_name,
|
||||
cc.quantity_used,
|
||||
il.unit_price_per_g,
|
||||
cc.quantity_used * il.unit_price_per_g as consumption_value,
|
||||
cc.consumption_date
|
||||
FROM compound_consumptions cc
|
||||
JOIN inventory_lots il ON cc.lot_id = il.lot_id
|
||||
JOIN compounds c ON cc.compound_id = c.compound_id
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
ORDER BY consumption_value DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
|
||||
consumptions = cursor.fetchall()
|
||||
|
||||
print(" 복합제 소비 내역 (상위 10개):")
|
||||
total_consumption = 0
|
||||
for cons in consumptions:
|
||||
print(f" - {cons['compound_name']} - {cons['herb_name']}")
|
||||
print(f" {cons['quantity_used']:,.0f}g × ₩{cons['unit_price_per_g']:.2f} = ₩{cons['consumption_value']:,.0f}")
|
||||
total_consumption += cons['consumption_value']
|
||||
|
||||
cursor.execute("""
|
||||
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
|
||||
FROM compound_consumptions cc
|
||||
JOIN inventory_lots il ON cc.lot_id = il.lot_id
|
||||
""")
|
||||
total_consumed = cursor.fetchone()['total'] or 0
|
||||
|
||||
print(f"\n 총 소비 금액: ₩{total_consumed:,.0f}")
|
||||
|
||||
# 4. 재고 자산 흐름 요약
|
||||
print("\n4. 재고 자산 흐름 요약")
|
||||
print("=" * 60)
|
||||
|
||||
# 입고장 기준
|
||||
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
|
||||
receipt_total = cursor.fetchone()['total'] or 0
|
||||
|
||||
# LOT 생성 기준
|
||||
cursor.execute("""
|
||||
SELECT SUM(quantity_received * unit_price_per_g) as total
|
||||
FROM inventory_lots
|
||||
WHERE receipt_line_id IS NOT NULL
|
||||
""")
|
||||
lot_creation = cursor.fetchone()['total'] or 0
|
||||
|
||||
# 현재 LOT 재고
|
||||
cursor.execute("""
|
||||
SELECT SUM(quantity_onhand * unit_price_per_g) as total
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
current_inventory = cursor.fetchone()['total'] or 0
|
||||
|
||||
print(f" 1) 입고장 총액: ₩{receipt_total:,.0f}")
|
||||
print(f" 2) LOT 생성 총액: ₩{lot_creation:,.0f}")
|
||||
print(f" 차이 (1-2): ₩{receipt_total - lot_creation:,.0f}")
|
||||
print()
|
||||
print(f" 3) 복합제 소비: ₩{total_consumed:,.0f}")
|
||||
print(f" 4) 현재 재고: ₩{current_inventory:,.0f}")
|
||||
print()
|
||||
print(f" 예상 재고 (2-3): ₩{lot_creation - total_consumed:,.0f}")
|
||||
print(f" 실제 재고: ₩{current_inventory:,.0f}")
|
||||
print(f" 차이: ₩{current_inventory - (lot_creation - total_consumed):,.0f}")
|
||||
|
||||
# 5. 차이 원인 설명
|
||||
print("\n5. 차이 원인 분석")
|
||||
print("-" * 60)
|
||||
|
||||
price_diff = receipt_total - lot_creation
|
||||
if abs(price_diff) > 1000:
|
||||
print(f"\n 💡 입고장과 LOT 생성 시 ₩{abs(price_diff):,.0f} 차이가 있습니다.")
|
||||
print(" 가능한 원인:")
|
||||
print(" - VAT 포함/제외 계산 차이")
|
||||
print(" - 단가 반올림 차이")
|
||||
print(" - 입고 시점의 환율 적용 차이")
|
||||
|
||||
consumption_diff = current_inventory - (lot_creation - total_consumed)
|
||||
if abs(consumption_diff) > 1000:
|
||||
print(f"\n 💡 예상 재고와 실제 재고 간 ₩{abs(consumption_diff):,.0f} 차이가 있습니다.")
|
||||
print(" 가능한 원인:")
|
||||
print(" - 재고 보정 내역")
|
||||
print(" - 소비 시 반올림 오차 누적")
|
||||
print(" - 초기 데이터 입력 오류")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_price_difference()
|
||||
62
dev_scripts/analyze_product_code.py
Normal file
62
dev_scripts/analyze_product_code.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
한약재 제품 코드 엑셀 파일 분석
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import openpyxl
|
||||
|
||||
def analyze_excel_file():
|
||||
file_path = 'sample/(게시)한약재제품코드_2510.xlsx'
|
||||
|
||||
# 엑셀 파일 열기
|
||||
wb = openpyxl.load_workbook(file_path, read_only=True)
|
||||
|
||||
print("=== 엑셀 파일 시트 목록 ===")
|
||||
for i, sheet_name in enumerate(wb.sheetnames, 1):
|
||||
print(f"{i}. {sheet_name}")
|
||||
|
||||
# 4번째 시트 데이터 읽기
|
||||
if len(wb.sheetnames) >= 4:
|
||||
sheet_name = wb.sheetnames[3] # 0-based index
|
||||
print(f"\n=== 4번째 시트 '{sheet_name}' 분석 ===")
|
||||
|
||||
# pandas로 데이터 읽기
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name)
|
||||
|
||||
print(f"\n데이터 크기: {df.shape[0]}행 x {df.shape[1]}열")
|
||||
print(f"\n컬럼 목록:")
|
||||
for i, col in enumerate(df.columns, 1):
|
||||
# NaN이 아닌 값들의 예시
|
||||
non_null_count = df[col].notna().sum()
|
||||
sample_values = df[col].dropna().head(3).tolist()
|
||||
print(f" {i}. {col} (유효값: {non_null_count}개)")
|
||||
if sample_values:
|
||||
print(f" 예시: {sample_values[:3]}")
|
||||
|
||||
print(f"\n=== 데이터 샘플 (처음 10행) ===")
|
||||
pd.set_option('display.max_columns', None)
|
||||
pd.set_option('display.width', None)
|
||||
pd.set_option('display.max_colwidth', 50)
|
||||
print(df.head(10))
|
||||
|
||||
# 주요 컬럼 분석
|
||||
if '주성분코드' in df.columns:
|
||||
print(f"\n=== 주성분코드 분석 ===")
|
||||
print(f"유일한 주성분코드 수: {df['주성분코드'].nunique()}")
|
||||
print(f"주성분코드 샘플: {df['주성분코드'].unique()[:10].tolist()}")
|
||||
|
||||
if '제품명' in df.columns:
|
||||
print(f"\n=== 제품명 분석 ===")
|
||||
print(f"유일한 제품 수: {df['제품명'].nunique()}")
|
||||
print(f"제품명 샘플: {df['제품명'].head(10).tolist()}")
|
||||
|
||||
# 컬럼 정보를 더 자세히 분석
|
||||
print(f"\n=== 데이터 타입 및 null 값 정보 ===")
|
||||
print(df.info())
|
||||
|
||||
wb.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_excel_file()
|
||||
84
dev_scripts/analyze_product_deep.py
Normal file
84
dev_scripts/analyze_product_deep.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
한약재 제품 코드 심층 분석
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
def deep_analyze():
|
||||
file_path = 'sample/(게시)한약재제품코드_2510.xlsx'
|
||||
sheet_name = '한약재 제품코드_20250930기준(유효코드만 공지)'
|
||||
|
||||
# 데이터 읽기 - 제품코드를 문자열로 읽어서 0 유지
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, dtype={'제품코드': str})
|
||||
|
||||
print("=== 한약재 제품 코드 데이터 심층 분석 ===")
|
||||
print(f"전체 데이터: {len(df):,}개 제품")
|
||||
print(f"유일한 주성분코드: {df['주성분코드'].nunique()}개")
|
||||
print(f"유일한 약재 품목명: {df['한약재 품목명'].nunique()}개")
|
||||
print(f"유일한 업체: {df['업체명'].nunique()}개")
|
||||
|
||||
# 주성분코드별 통계
|
||||
print("\n=== 주성분코드별 제품 수 (상위 20개) ===")
|
||||
ingredient_stats = df.groupby(['주성분코드', '한약재 품목명']).size().reset_index(name='제품수')
|
||||
ingredient_stats = ingredient_stats.sort_values('제품수', ascending=False).head(20)
|
||||
|
||||
for _, row in ingredient_stats.iterrows():
|
||||
print(f" {row['주성분코드']} ({row['한약재 품목명']}): {row['제품수']}개 제품")
|
||||
|
||||
# 업체별 통계
|
||||
print("\n=== 업체별 제품 수 (상위 10개) ===")
|
||||
company_stats = df['업체명'].value_counts().head(10)
|
||||
for company, count in company_stats.items():
|
||||
print(f" {company}: {count}개 제품")
|
||||
|
||||
# 규격별 분석
|
||||
print("\n=== 약품 규격 분석 ===")
|
||||
spec_stats = df['약품규격(단위)'].value_counts()
|
||||
print("규격 단위별 제품 수:")
|
||||
for spec, count in spec_stats.items():
|
||||
print(f" {spec}: {count}개")
|
||||
|
||||
# 특정 약재들 확인
|
||||
print("\n=== 주요 약재 확인 ===")
|
||||
target_herbs = ['건강', '감초', '당귀', '황기', '숙지황', '백출', '천궁', '육계', '백작약', '인삼', '생강', '대추']
|
||||
|
||||
for herb in target_herbs:
|
||||
herb_data = df[df['한약재 품목명'] == herb]
|
||||
if not herb_data.empty:
|
||||
unique_code = herb_data['주성분코드'].iloc[0] if len(herb_data) > 0 else 'N/A'
|
||||
product_count = len(herb_data)
|
||||
company_count = herb_data['업체명'].nunique()
|
||||
print(f" {herb}: 주성분코드={unique_code}, {product_count}개 제품, {company_count}개 업체")
|
||||
else:
|
||||
print(f" {herb}: 데이터 없음")
|
||||
|
||||
# 한 약재에 여러 제품이 있는 예시 - 건강
|
||||
print("\n=== '건강' 약재의 제품 예시 (처음 10개) ===")
|
||||
gangang_data = df[df['한약재 품목명'] == '건강'].head(10)
|
||||
if not gangang_data.empty:
|
||||
for _, row in gangang_data.iterrows():
|
||||
# 제품코드를 9자리로 표시 (0 패딩)
|
||||
product_code = str(row['제품코드']).zfill(9)
|
||||
print(f" 업체: {row['업체명']}, 제품명: {row['제품명']}, 제품코드: {product_code}, 규격: {row['약품규격(숫자)']} {row['약품규격(단위)']}")
|
||||
|
||||
# 현재 시스템과의 비교
|
||||
print("\n=== 현재 DB 설계와의 차이점 ===")
|
||||
print("1. 현재 시스템:")
|
||||
print(" - herb_items: 약재 기본 정보 (예: 건강)")
|
||||
print(" - inventory_lots: 로트별 재고 (원산지, 가격 등)")
|
||||
print("\nㅇ2. 제품코드 시스템:")
|
||||
print(" - 주성분코드: 약재별 고유 코드 (예: 3050H1AHM = 건강)")
|
||||
print(" - 제품코드: 업체별 제품 고유코드")
|
||||
print(" - 표준코드/대표코드: 바코드 시스템")
|
||||
print(" - 규격: 포장 단위 (500g, 1000g 등)")
|
||||
|
||||
print("\n=== 시사점 ===")
|
||||
print("- 54,000개 이상의 유통 제품이 454개 주성분코드로 분류됨")
|
||||
print("- 같은 약재(주성분)라도 업체별로 다른 제품명과 코드를 가짐")
|
||||
print("- 제품별로 다양한 포장 규격 존재 (-, 500g, 600g, 1000g 등)")
|
||||
print("- 표준코드(바코드)를 통한 제품 식별 가능")
|
||||
|
||||
if __name__ == "__main__":
|
||||
deep_analyze()
|
||||
158
dev_scripts/check_and_create_efficacy_tags.py
Normal file
158
dev_scripts/check_and_create_efficacy_tags.py
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
약재 효능 태그 시스템 추가 스크립트
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_and_create_efficacy_system():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 1. 현재 테이블 확인
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table'
|
||||
ORDER BY name
|
||||
""")
|
||||
print("=== 현재 테이블 목록 ===")
|
||||
for table in cursor.fetchall():
|
||||
print(f" - {table[0]}")
|
||||
|
||||
# 2. herb_items 테이블 스키마 확인
|
||||
print("\n=== herb_items 테이블 구조 ===")
|
||||
cursor.execute("PRAGMA table_info(herb_items)")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
print(f" {col[1]} ({col[2]})")
|
||||
|
||||
# 3. 효능 태그 테이블 생성
|
||||
print("\n=== 효능 태그 시스템 생성 ===")
|
||||
|
||||
# 효능 마스터 테이블
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS herb_efficacy_tags (
|
||||
tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tag_name VARCHAR(50) NOT NULL UNIQUE,
|
||||
tag_category VARCHAR(50), -- 보(補), 사(瀉), 온(溫), 량(涼) 등
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
print("✅ herb_efficacy_tags 테이블 생성")
|
||||
|
||||
# 약재-효능 연결 테이블 (다대다)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS herb_item_tags (
|
||||
herb_item_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (herb_item_id, tag_id),
|
||||
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES herb_efficacy_tags(tag_id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
print("✅ herb_item_tags 테이블 생성")
|
||||
|
||||
# 4. 기본 효능 태그 추가
|
||||
basic_tags = [
|
||||
('보혈', '보', '혈을 보하는 효능'),
|
||||
('보기', '보', '기를 보하는 효능'),
|
||||
('보양', '보', '양기를 보하는 효능'),
|
||||
('보음', '보', '음액을 보하는 효능'),
|
||||
('활혈', '활', '혈액순환을 활발하게 하는 효능'),
|
||||
('거담', '거', '가래를 제거하는 효능'),
|
||||
('온중', '온', '속을 따뜻하게 하는 효능'),
|
||||
('온양', '온', '양기를 따뜻하게 하는 효능'),
|
||||
('청열', '청', '열을 내리는 효능'),
|
||||
('해표', '해', '표증을 해소하는 효능'),
|
||||
('소화', '소', '소화를 돕는 효능'),
|
||||
('이수', '이', '수분대사를 돕는 효능'),
|
||||
('안신', '안', '정신을 안정시키는 효능'),
|
||||
('지혈', '지', '출혈을 멈추는 효능'),
|
||||
('조화제약', '조화', '여러 약재를 조화롭게 하는 효능'),
|
||||
('대보원기', '대보', '원기를 크게 보하는 효능'),
|
||||
('보기건비', '보', '기를 보하고 비장을 건강하게 하는 효능'),
|
||||
('보중익기', '보', '중초를 보하고 기를 증진시키는 효능'),
|
||||
]
|
||||
|
||||
for tag_name, category, description in basic_tags:
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO herb_efficacy_tags (tag_name, tag_category, description)
|
||||
VALUES (?, ?, ?)
|
||||
""", (tag_name, category, description))
|
||||
|
||||
print(f"✅ {len(basic_tags)}개 기본 효능 태그 추가")
|
||||
|
||||
# 5. 쌍화탕 약재들에 효능 태그 연결
|
||||
ssanghwa_herbs = [
|
||||
('숙지황', '보혈'),
|
||||
('당귀', '보혈'),
|
||||
('백작약', '보혈'),
|
||||
('천궁', '활혈'),
|
||||
('황기', '보기'),
|
||||
('인삼', '대보원기'),
|
||||
('백출', '보기건비'),
|
||||
('감초', '조화제약'),
|
||||
('생강', '온중'),
|
||||
('대추', '보중익기'),
|
||||
('육계', '온양'),
|
||||
('건강', '온중'),
|
||||
]
|
||||
|
||||
print("\n=== 약재별 효능 태그 연결 ===")
|
||||
for herb_name, tag_name in ssanghwa_herbs:
|
||||
# 약재 ID 찾기
|
||||
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (herb_name,))
|
||||
herb_result = cursor.fetchone()
|
||||
|
||||
# 태그 ID 찾기
|
||||
cursor.execute("SELECT tag_id FROM herb_efficacy_tags WHERE tag_name = ?", (tag_name,))
|
||||
tag_result = cursor.fetchone()
|
||||
|
||||
if herb_result and tag_result:
|
||||
herb_id = herb_result[0]
|
||||
tag_id = tag_result[0]
|
||||
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO herb_item_tags (herb_item_id, tag_id)
|
||||
VALUES (?, ?)
|
||||
""", (herb_id, tag_id))
|
||||
print(f" ✅ {herb_name} → {tag_name}")
|
||||
else:
|
||||
if not herb_result:
|
||||
print(f" ⚠️ {herb_name} 약재 없음")
|
||||
if not tag_result:
|
||||
print(f" ⚠️ {tag_name} 태그 없음")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 6. 결과 확인 - 약재별 태그 조회
|
||||
print("\n=== 약재별 효능 태그 확인 ===")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
GROUP_CONCAT(t.tag_name, ', ') as tags
|
||||
FROM herb_items h
|
||||
LEFT JOIN herb_item_tags ht ON h.herb_item_id = ht.herb_item_id
|
||||
LEFT JOIN herb_efficacy_tags t ON ht.tag_id = t.tag_id
|
||||
WHERE ht.tag_id IS NOT NULL
|
||||
GROUP BY h.herb_item_id
|
||||
ORDER BY h.herb_name
|
||||
""")
|
||||
|
||||
results = cursor.fetchall()
|
||||
for herb_name, tags in results:
|
||||
print(f" {herb_name}: {tags}")
|
||||
|
||||
print("\n✅ 효능 태그 시스템 구축 완료!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 오류 발생: {e}")
|
||||
conn.rollback()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_and_create_efficacy_system()
|
||||
224
dev_scripts/check_custom_prescription.py
Normal file
224
dev_scripts/check_custom_prescription.py
Normal file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
커스텀 처방 감지 유틸리티
|
||||
조제 시 원 처방과 다른 구성인지 확인
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
def get_connection():
|
||||
"""데이터베이스 연결"""
|
||||
return sqlite3.connect('database/kdrug.db')
|
||||
|
||||
def check_custom_prescription(compound_id: int) -> Tuple[bool, Dict]:
|
||||
"""
|
||||
조제가 원 처방과 다른지 확인
|
||||
|
||||
Returns:
|
||||
(is_custom, differences_dict)
|
||||
"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. compound의 formula_id 가져오기
|
||||
cursor.execute("""
|
||||
SELECT c.formula_id, f.formula_name
|
||||
FROM compounds c
|
||||
JOIN formulas f ON c.formula_id = f.formula_id
|
||||
WHERE c.compound_id = ?
|
||||
""", (compound_id,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
conn.close()
|
||||
return False, {"error": "Compound not found"}
|
||||
|
||||
formula_id, formula_name = result
|
||||
|
||||
# 2. 원 처방의 구성 약재
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
fi.herb_item_id,
|
||||
h.herb_name,
|
||||
fi.grams_per_cheop
|
||||
FROM formula_ingredients fi
|
||||
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
|
||||
WHERE fi.formula_id = ?
|
||||
ORDER BY fi.herb_item_id
|
||||
""", (formula_id,))
|
||||
|
||||
original_ingredients = {row[0]: {
|
||||
'herb_name': row[1],
|
||||
'grams_per_cheop': row[2]
|
||||
} for row in cursor.fetchall()}
|
||||
|
||||
# 3. 실제 조제된 구성 약재
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
ci.herb_item_id,
|
||||
h.herb_name,
|
||||
ci.grams_per_cheop
|
||||
FROM compound_ingredients ci
|
||||
JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
|
||||
WHERE ci.compound_id = ?
|
||||
ORDER BY ci.herb_item_id
|
||||
""", (compound_id,))
|
||||
|
||||
actual_ingredients = {row[0]: {
|
||||
'herb_name': row[1],
|
||||
'grams_per_cheop': row[2]
|
||||
} for row in cursor.fetchall()}
|
||||
|
||||
conn.close()
|
||||
|
||||
# 4. 비교 분석
|
||||
differences = {
|
||||
'formula_name': formula_name,
|
||||
'added': [],
|
||||
'removed': [],
|
||||
'modified': [],
|
||||
'is_custom': False
|
||||
}
|
||||
|
||||
# 추가된 약재
|
||||
for herb_id, info in actual_ingredients.items():
|
||||
if herb_id not in original_ingredients:
|
||||
differences['added'].append({
|
||||
'herb_id': herb_id,
|
||||
'herb_name': info['herb_name'],
|
||||
'grams_per_cheop': info['grams_per_cheop']
|
||||
})
|
||||
differences['is_custom'] = True
|
||||
|
||||
# 제거된 약재
|
||||
for herb_id, info in original_ingredients.items():
|
||||
if herb_id not in actual_ingredients:
|
||||
differences['removed'].append({
|
||||
'herb_id': herb_id,
|
||||
'herb_name': info['herb_name'],
|
||||
'grams_per_cheop': info['grams_per_cheop']
|
||||
})
|
||||
differences['is_custom'] = True
|
||||
|
||||
# 용량 변경된 약재
|
||||
for herb_id in set(original_ingredients.keys()) & set(actual_ingredients.keys()):
|
||||
orig_grams = original_ingredients[herb_id]['grams_per_cheop']
|
||||
actual_grams = actual_ingredients[herb_id]['grams_per_cheop']
|
||||
|
||||
if abs(orig_grams - actual_grams) > 0.01: # 부동소수점 오차 고려
|
||||
differences['modified'].append({
|
||||
'herb_id': herb_id,
|
||||
'herb_name': original_ingredients[herb_id]['herb_name'],
|
||||
'original_grams': orig_grams,
|
||||
'actual_grams': actual_grams,
|
||||
'difference': actual_grams - orig_grams
|
||||
})
|
||||
differences['is_custom'] = True
|
||||
|
||||
return differences['is_custom'], differences
|
||||
|
||||
def generate_custom_summary(differences: Dict) -> str:
|
||||
"""커스텀 내역을 요약 문자열로 생성"""
|
||||
summary_parts = []
|
||||
|
||||
# 추가
|
||||
if differences['added']:
|
||||
added_herbs = [f"{item['herb_name']} {item['grams_per_cheop']}g"
|
||||
for item in differences['added']]
|
||||
summary_parts.append(f"추가: {', '.join(added_herbs)}")
|
||||
|
||||
# 제거
|
||||
if differences['removed']:
|
||||
removed_herbs = [item['herb_name'] for item in differences['removed']]
|
||||
summary_parts.append(f"제거: {', '.join(removed_herbs)}")
|
||||
|
||||
# 수정
|
||||
if differences['modified']:
|
||||
modified_herbs = [f"{item['herb_name']} {item['original_grams']}g→{item['actual_grams']}g"
|
||||
for item in differences['modified']]
|
||||
summary_parts.append(f"변경: {', '.join(modified_herbs)}")
|
||||
|
||||
return " | ".join(summary_parts) if summary_parts else "표준 처방"
|
||||
|
||||
def list_all_custom_prescriptions():
|
||||
"""모든 커스텀 처방 찾기"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 모든 조제 목록
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
c.compound_id,
|
||||
c.compound_date,
|
||||
p.name as patient_name,
|
||||
f.formula_name
|
||||
FROM compounds c
|
||||
LEFT JOIN patients p ON c.patient_id = p.patient_id
|
||||
JOIN formulas f ON c.formula_id = f.formula_id
|
||||
ORDER BY c.compound_date DESC
|
||||
""")
|
||||
|
||||
compounds = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
custom_compounds = []
|
||||
|
||||
for compound in compounds:
|
||||
compound_id = compound[0]
|
||||
is_custom, differences = check_custom_prescription(compound_id)
|
||||
|
||||
if is_custom:
|
||||
custom_compounds.append({
|
||||
'compound_id': compound_id,
|
||||
'compound_date': compound[1],
|
||||
'patient_name': compound[2],
|
||||
'formula_name': compound[3],
|
||||
'summary': generate_custom_summary(differences),
|
||||
'differences': differences
|
||||
})
|
||||
|
||||
return custom_compounds
|
||||
|
||||
def demo():
|
||||
"""데모 실행"""
|
||||
print("\n" + "="*80)
|
||||
print("커스텀 처방 감지 시스템")
|
||||
print("="*80)
|
||||
|
||||
# 전체 커스텀 처방 검색
|
||||
custom_prescriptions = list_all_custom_prescriptions()
|
||||
|
||||
if not custom_prescriptions:
|
||||
print("\n조제 내역이 없거나 모든 조제가 표준 처방입니다.")
|
||||
|
||||
# 테스트용 샘플 데이터 표시
|
||||
print("\n[시뮬레이션] 만약 십전대보탕에 구기자를 추가했다면:")
|
||||
print("-" * 60)
|
||||
|
||||
sample_diff = {
|
||||
'formula_name': '십전대보탕',
|
||||
'added': [{'herb_name': '구기자', 'grams_per_cheop': 3}],
|
||||
'removed': [],
|
||||
'modified': [{'herb_name': '인삼', 'original_grams': 5, 'actual_grams': 7}],
|
||||
'is_custom': True
|
||||
}
|
||||
|
||||
summary = generate_custom_summary(sample_diff)
|
||||
print(f"처방: 십전대보탕 (가감방)")
|
||||
print(f"변경 내역: {summary}")
|
||||
print("\n환자 기록 표시:")
|
||||
print(" 2024-02-17 십전대보탕 가감방 20첩")
|
||||
print(f" └─ {summary}")
|
||||
else:
|
||||
print(f"\n총 {len(custom_prescriptions)}개의 커스텀 처방이 발견되었습니다.\n")
|
||||
|
||||
for cp in custom_prescriptions:
|
||||
print(f"조제 #{cp['compound_id']} | {cp['compound_date']} | {cp['patient_name']}")
|
||||
print(f" 처방: {cp['formula_name']} (가감방)")
|
||||
print(f" 변경: {cp['summary']}")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
121
dev_scripts/check_formula_columns.py
Normal file
121
dev_scripts/check_formula_columns.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
formulas 테이블의 칼럼 구조 확인
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_formula_structure():
|
||||
"""formulas 테이블의 전체 구조 확인"""
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("🔍 formulas 테이블 구조 확인")
|
||||
print("="*70)
|
||||
|
||||
# 테이블 구조 확인
|
||||
cursor.execute("PRAGMA table_info(formulas)")
|
||||
columns = cursor.fetchall()
|
||||
|
||||
print("\n📊 formulas 테이블 칼럼 목록:")
|
||||
print("-"*70)
|
||||
print(f"{'번호':>4} | {'칼럼명':20} | {'타입':15} | {'NULL 허용':10} | {'기본값'}")
|
||||
print("-"*70)
|
||||
|
||||
efficacy_columns = []
|
||||
for col in columns:
|
||||
cid, name, type_name, notnull, dflt_value, pk = col
|
||||
null_str = "NOT NULL" if notnull else "NULL"
|
||||
default_str = dflt_value if dflt_value else "-"
|
||||
|
||||
print(f"{cid:4d} | {name:20} | {type_name:15} | {null_str:10} | {default_str}")
|
||||
|
||||
# 효능 관련 칼럼 찾기
|
||||
if 'efficacy' in name.lower() or 'indication' in name.lower() or '효능' in name:
|
||||
efficacy_columns.append(name)
|
||||
|
||||
print("\n" + "="*70)
|
||||
|
||||
if efficacy_columns:
|
||||
print(f"✅ 효능 관련 칼럼 발견: {', '.join(efficacy_columns)}")
|
||||
else:
|
||||
print("❌ 효능 관련 칼럼이 없습니다.")
|
||||
|
||||
# 실제 데이터 예시 확인
|
||||
print("\n📋 십전대보탕 데이터 예시:")
|
||||
print("-"*70)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT * FROM formulas
|
||||
WHERE formula_code = 'SJDB01'
|
||||
""")
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
col_names = [description[0] for description in cursor.description]
|
||||
for i, (col_name, value) in enumerate(zip(col_names, row)):
|
||||
if value and value != 0: # 값이 있는 경우만 표시
|
||||
print(f"{col_name:25}: {str(value)[:100]}")
|
||||
|
||||
# prescription_details 테이블도 확인
|
||||
print("\n\n🔍 prescription_details 테이블 확인 (혹시 여기 있는지)")
|
||||
print("="*70)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='prescription_details'
|
||||
""")
|
||||
|
||||
if cursor.fetchone():
|
||||
cursor.execute("PRAGMA table_info(prescription_details)")
|
||||
columns = cursor.fetchall()
|
||||
|
||||
print("📊 prescription_details 테이블 칼럼:")
|
||||
print("-"*70)
|
||||
|
||||
for col in columns:
|
||||
cid, name, type_name, notnull, dflt_value, pk = col
|
||||
if 'efficacy' in name.lower() or 'indication' in name.lower():
|
||||
print(f" ✅ {name}: {type_name}")
|
||||
|
||||
# formula_details 테이블도 확인
|
||||
print("\n\n🔍 formula_details 테이블 확인")
|
||||
print("="*70)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='formula_details'
|
||||
""")
|
||||
|
||||
if cursor.fetchone():
|
||||
cursor.execute("PRAGMA table_info(formula_details)")
|
||||
columns = cursor.fetchall()
|
||||
|
||||
print("📊 formula_details 테이블 칼럼:")
|
||||
print("-"*70)
|
||||
|
||||
for col in columns:
|
||||
cid, name, type_name, notnull, dflt_value, pk = col
|
||||
print(f" {name}: {type_name}")
|
||||
|
||||
# 실제 데이터 확인
|
||||
cursor.execute("""
|
||||
SELECT * FROM formula_details
|
||||
WHERE formula_id = (SELECT formula_id FROM formulas WHERE formula_code = 'SJDB01')
|
||||
""")
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
print("\n십전대보탕 상세 정보:")
|
||||
col_names = [description[0] for description in cursor.description]
|
||||
for col_name, value in zip(col_names, row):
|
||||
if value:
|
||||
print(f" {col_name}: {str(value)[:100]}")
|
||||
else:
|
||||
print("❌ formula_details 테이블이 없습니다.")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_formula_structure()
|
||||
45
dev_scripts/check_herb_data.py
Normal file
45
dev_scripts/check_herb_data.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""약재 데이터 확인"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect('kdrug.db')
|
||||
cur = conn.cursor()
|
||||
|
||||
# 확장 정보가 있는 약재 확인
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM herb_master_extended
|
||||
WHERE nature IS NOT NULL OR taste IS NOT NULL
|
||||
""")
|
||||
extended_count = cur.fetchone()[0]
|
||||
print(f"확장 정보가 있는 약재: {extended_count}개")
|
||||
|
||||
# 효능 태그가 있는 약재 확인
|
||||
cur.execute("SELECT COUNT(DISTINCT ingredient_code) FROM herb_item_tags")
|
||||
tagged_count = cur.fetchone()[0]
|
||||
print(f"효능 태그가 있는 약재: {tagged_count}개")
|
||||
|
||||
# 구체적인 데이터 확인
|
||||
cur.execute("""
|
||||
SELECT hme.ingredient_code, hme.herb_name, hme.nature, hme.taste
|
||||
FROM herb_master_extended hme
|
||||
WHERE hme.nature IS NOT NULL OR hme.taste IS NOT NULL
|
||||
LIMIT 5
|
||||
""")
|
||||
print("\n확장 정보 샘플:")
|
||||
for row in cur.fetchall():
|
||||
print(f" - {row[1]} ({row[0]}): {row[2]}/{row[3]}")
|
||||
|
||||
# herb_item_tags 데이터 확인
|
||||
cur.execute("""
|
||||
SELECT hit.ingredient_code, het.name, COUNT(*) as count
|
||||
FROM herb_item_tags hit
|
||||
JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
|
||||
GROUP BY hit.ingredient_code
|
||||
LIMIT 5
|
||||
""")
|
||||
print("\n효능 태그 샘플:")
|
||||
for row in cur.fetchall():
|
||||
print(f" - {row[0]}: {row[2]}개 태그")
|
||||
|
||||
conn.close()
|
||||
143
dev_scripts/check_lot_creation_methods.py
Normal file
143
dev_scripts/check_lot_creation_methods.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
LOT 생성 방법 분석 - 입고장 연결 vs 독립 생성
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_lot_creation_methods():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("📦 LOT 생성 방법 분석")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. 전체 LOT 현황
|
||||
print("1. 전체 LOT 현황")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total_lots,
|
||||
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN 1 ELSE 0 END) as with_receipt,
|
||||
SUM(CASE WHEN receipt_line_id IS NULL THEN 1 ELSE 0 END) as without_receipt,
|
||||
SUM(CASE WHEN is_depleted = 0 THEN 1 ELSE 0 END) as active_lots
|
||||
FROM inventory_lots
|
||||
""")
|
||||
|
||||
stats = cursor.fetchone()
|
||||
|
||||
print(f" 전체 LOT 수: {stats['total_lots']}개")
|
||||
print(f" ✅ 입고장 연결: {stats['with_receipt']}개")
|
||||
print(f" ❌ 입고장 없음: {stats['without_receipt']}개")
|
||||
print(f" 활성 LOT: {stats['active_lots']}개")
|
||||
|
||||
# 2. 입고장 없는 LOT 상세
|
||||
if stats['without_receipt'] > 0:
|
||||
print("\n2. 입고장 없이 생성된 LOT 상세")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
il.lot_id,
|
||||
h.herb_name,
|
||||
il.lot_number,
|
||||
il.quantity_received,
|
||||
il.quantity_onhand,
|
||||
il.unit_price_per_g,
|
||||
il.quantity_onhand * il.unit_price_per_g as value,
|
||||
il.received_date,
|
||||
il.created_at
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE il.receipt_line_id IS NULL
|
||||
ORDER BY il.created_at DESC
|
||||
""")
|
||||
|
||||
no_receipt_lots = cursor.fetchall()
|
||||
|
||||
for lot in no_receipt_lots:
|
||||
print(f"\n LOT {lot['lot_id']}: {lot['herb_name']}")
|
||||
print(f" LOT 번호: {lot['lot_number'] or 'None'}")
|
||||
print(f" 수량: {lot['quantity_received']:,.0f}g → {lot['quantity_onhand']:,.0f}g")
|
||||
print(f" 단가: ₩{lot['unit_price_per_g']:.2f}")
|
||||
print(f" 재고 가치: ₩{lot['value']:,.0f}")
|
||||
print(f" 입고일: {lot['received_date']}")
|
||||
print(f" 생성일: {lot['created_at']}")
|
||||
|
||||
# 금액 합계
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(quantity_onhand * unit_price_per_g) as total_value,
|
||||
SUM(quantity_onhand) as total_qty
|
||||
FROM inventory_lots
|
||||
WHERE receipt_line_id IS NULL
|
||||
AND is_depleted = 0
|
||||
AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
no_receipt_total = cursor.fetchone()
|
||||
|
||||
if no_receipt_total['total_value']:
|
||||
print(f"\n 📊 입고장 없는 LOT 합계:")
|
||||
print(f" 총 재고량: {no_receipt_total['total_qty']:,.0f}g")
|
||||
print(f" 총 재고 가치: ₩{no_receipt_total['total_value']:,.0f}")
|
||||
|
||||
# 3. LOT 생성 방법별 재고 자산
|
||||
print("\n3. LOT 생성 방법별 재고 자산")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
CASE
|
||||
WHEN receipt_line_id IS NOT NULL THEN '입고장 연결'
|
||||
ELSE '직접 생성'
|
||||
END as creation_type,
|
||||
COUNT(*) as lot_count,
|
||||
SUM(quantity_onhand) as total_qty,
|
||||
SUM(quantity_onhand * unit_price_per_g) as total_value
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
GROUP BY creation_type
|
||||
""")
|
||||
|
||||
by_type = cursor.fetchall()
|
||||
|
||||
total_value = 0
|
||||
for row in by_type:
|
||||
print(f"\n {row['creation_type']}:")
|
||||
print(f" LOT 수: {row['lot_count']}개")
|
||||
print(f" 재고량: {row['total_qty']:,.0f}g")
|
||||
print(f" 재고 가치: ₩{row['total_value']:,.0f}")
|
||||
total_value += row['total_value']
|
||||
|
||||
print(f"\n 📊 전체 재고 자산: ₩{total_value:,.0f}")
|
||||
|
||||
# 4. 시스템 설계 분석
|
||||
print("\n4. 시스템 설계 분석")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n 💡 현재 시스템은 두 가지 방법으로 LOT 생성 가능:")
|
||||
print(" 1) 입고장 등록 시 자동 생성 (receipt_line_id 연결)")
|
||||
print(" 2) 재고 직접 입력 (receipt_line_id = NULL)")
|
||||
print()
|
||||
print(" 📌 재고 자산 계산 로직:")
|
||||
print(" - 입고장 연결 여부와 관계없이")
|
||||
print(" - 모든 활성 LOT의 (수량 × 단가) 합계")
|
||||
print()
|
||||
|
||||
if stats['without_receipt'] > 0:
|
||||
print(" ⚠️ 주의사항:")
|
||||
print(" - 입고장 없는 LOT이 존재합니다")
|
||||
print(" - 초기 재고 입력이나 재고 조정으로 생성된 것으로 추정")
|
||||
print(" - 회계 추적을 위해서는 입고장 연결 권장")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_lot_creation_methods()
|
||||
179
dev_scripts/check_missing_lots.py
Normal file
179
dev_scripts/check_missing_lots.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
LOT이 생성되지 않은 입고 라인 확인
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_missing_lots():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("LOT이 생성되지 않은 입고 라인 분석")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. 전체 입고 라인과 LOT 매칭 상태
|
||||
print("1. 입고 라인 - LOT 매칭 현황")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total_lines,
|
||||
SUM(CASE WHEN il.lot_id IS NOT NULL THEN 1 ELSE 0 END) as lines_with_lot,
|
||||
SUM(CASE WHEN il.lot_id IS NULL THEN 1 ELSE 0 END) as lines_without_lot,
|
||||
SUM(prl.line_total) as total_purchase_amount,
|
||||
SUM(CASE WHEN il.lot_id IS NOT NULL THEN prl.line_total ELSE 0 END) as amount_with_lot,
|
||||
SUM(CASE WHEN il.lot_id IS NULL THEN prl.line_total ELSE 0 END) as amount_without_lot
|
||||
FROM purchase_receipt_lines prl
|
||||
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
|
||||
print(f" 총 입고 라인: {result['total_lines']}개")
|
||||
print(f" ✅ LOT 생성됨: {result['lines_with_lot']}개 (₩{result['amount_with_lot']:,.0f})")
|
||||
print(f" ❌ LOT 없음: {result['lines_without_lot']}개 (₩{result['amount_without_lot']:,.0f})")
|
||||
print()
|
||||
print(f" 총 입고 금액: ₩{result['total_purchase_amount']:,.0f}")
|
||||
print(f" LOT 없는 금액: ₩{result['amount_without_lot']:,.0f}")
|
||||
|
||||
if result['amount_without_lot'] > 0:
|
||||
print(f"\n ⚠️ LOT이 생성되지 않은 입고 금액이 ₩{result['amount_without_lot']:,.0f} 있습니다!")
|
||||
print(" 이것이 DB 재고와 예상 재고 차이(₩55,500)의 원인일 가능성이 높습니다.")
|
||||
|
||||
# 2. LOT이 없는 입고 라인 상세
|
||||
if result['lines_without_lot'] > 0:
|
||||
print("\n2. LOT이 생성되지 않은 입고 라인 상세")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
pr.receipt_no,
|
||||
pr.receipt_date,
|
||||
h.herb_name,
|
||||
prl.quantity_g,
|
||||
prl.unit_price_per_g,
|
||||
prl.line_total,
|
||||
prl.lot_number,
|
||||
prl.line_id
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
|
||||
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
|
||||
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
WHERE il.lot_id IS NULL
|
||||
ORDER BY prl.line_total DESC
|
||||
""")
|
||||
|
||||
missing_lots = cursor.fetchall()
|
||||
|
||||
total_missing_amount = 0
|
||||
print("\n LOT이 생성되지 않은 입고 라인:")
|
||||
for i, line in enumerate(missing_lots, 1):
|
||||
print(f"\n {i}. {line['herb_name']}")
|
||||
print(f" 입고장: {line['receipt_no']} ({line['receipt_date']})")
|
||||
print(f" 수량: {line['quantity_g']:,.0f}g")
|
||||
print(f" 단가: ₩{line['unit_price_per_g']:.2f}/g")
|
||||
print(f" 금액: ₩{line['line_total']:,.0f}")
|
||||
print(f" LOT번호: {line['lot_number'] or 'None'}")
|
||||
print(f" Line ID: {line['line_id']}")
|
||||
total_missing_amount += line['line_total']
|
||||
|
||||
print(f"\n 총 누락 금액: ₩{total_missing_amount:,.0f}")
|
||||
|
||||
# 3. 반대로 입고 라인 없는 LOT 확인
|
||||
print("\n3. 입고 라인과 연결되지 않은 LOT")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as orphan_lots,
|
||||
SUM(quantity_onhand * unit_price_per_g) as orphan_value,
|
||||
SUM(quantity_onhand) as orphan_quantity
|
||||
FROM inventory_lots
|
||||
WHERE receipt_line_id IS NULL
|
||||
AND is_depleted = 0
|
||||
AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
orphans = cursor.fetchone()
|
||||
|
||||
if orphans['orphan_lots'] > 0:
|
||||
print(f" 입고 라인 없는 LOT: {orphans['orphan_lots']}개")
|
||||
print(f" 해당 재고 가치: ₩{orphans['orphan_value']:,.0f}")
|
||||
print(f" 해당 재고량: {orphans['orphan_quantity']:,.0f}g")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
il.lot_number,
|
||||
il.quantity_onhand,
|
||||
il.unit_price_per_g,
|
||||
il.quantity_onhand * il.unit_price_per_g as value,
|
||||
il.received_date
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE il.receipt_line_id IS NULL
|
||||
AND il.is_depleted = 0
|
||||
AND il.quantity_onhand > 0
|
||||
ORDER BY value DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
orphan_lots = cursor.fetchall()
|
||||
if orphan_lots:
|
||||
print("\n 상위 5개 입고 라인 없는 LOT:")
|
||||
for lot in orphan_lots:
|
||||
print(f" - {lot['herb_name']} (LOT: {lot['lot_number']})")
|
||||
print(f" 재고: {lot['quantity_onhand']:,.0f}g, 금액: ₩{lot['value']:,.0f}")
|
||||
else:
|
||||
print(" ✅ 모든 LOT이 입고 라인과 연결되어 있습니다.")
|
||||
|
||||
# 4. 금액 차이 분석
|
||||
print("\n4. 금액 차이 최종 분석")
|
||||
print("=" * 60)
|
||||
|
||||
# 현재 DB 재고
|
||||
cursor.execute("""
|
||||
SELECT SUM(quantity_onhand * unit_price_per_g) as total
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
db_total = cursor.fetchone()['total'] or 0
|
||||
|
||||
# 총 입고 - 소비
|
||||
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
|
||||
total_in = cursor.fetchone()['total'] or 0
|
||||
|
||||
cursor.execute("""
|
||||
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
|
||||
FROM compound_consumptions cc
|
||||
JOIN inventory_lots il ON cc.lot_id = il.lot_id
|
||||
""")
|
||||
total_out = cursor.fetchone()['total'] or 0
|
||||
|
||||
expected = total_in - total_out
|
||||
|
||||
print(f" DB 재고 자산: ₩{db_total:,.0f}")
|
||||
print(f" 예상 재고 (입고-소비): ₩{expected:,.0f}")
|
||||
print(f" 차이: ₩{expected - db_total:,.0f}")
|
||||
print()
|
||||
|
||||
if result['amount_without_lot'] > 0:
|
||||
print(f" 💡 LOT 없는 입고 금액: ₩{result['amount_without_lot']:,.0f}")
|
||||
adjusted_expected = (total_in - result['amount_without_lot']) - total_out
|
||||
print(f" 📊 조정된 예상 재고: ₩{adjusted_expected:,.0f}")
|
||||
print(f" 조정 후 차이: ₩{adjusted_expected - db_total:,.0f}")
|
||||
|
||||
if abs(adjusted_expected - db_total) < 1000:
|
||||
print("\n ✅ LOT이 생성되지 않은 입고 라인을 제외하면 차이가 거의 없습니다!")
|
||||
print(" 이것이 차이의 주요 원인입니다.")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_missing_lots()
|
||||
51
dev_scripts/check_purchase_structure.py
Normal file
51
dev_scripts/check_purchase_structure.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=== purchase_receipts 테이블 구조 ===")
|
||||
cursor.execute("PRAGMA table_info(purchase_receipts)")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
print(f" {col[1]}: {col[2]}")
|
||||
|
||||
print("\n=== purchase_receipt_lines 테이블 구조 ===")
|
||||
cursor.execute("PRAGMA table_info(purchase_receipt_lines)")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
print(f" {col[1]}: {col[2]}")
|
||||
|
||||
print("\n=== 입고장 데이터 샘플 ===")
|
||||
cursor.execute("""
|
||||
SELECT pr.receipt_id, pr.receipt_number, pr.receipt_date,
|
||||
COUNT(prl.line_id) as line_count,
|
||||
SUM(prl.quantity_g) as total_quantity,
|
||||
SUM(prl.total_price) as total_amount
|
||||
FROM purchase_receipts pr
|
||||
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
|
||||
GROUP BY pr.receipt_id
|
||||
LIMIT 5
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
print(f" 입고장 {row[0]}: {row[1]} ({row[2]})")
|
||||
print(f" - 항목수: {row[3]}개, 총량: {row[4]}g, 총액: ₩{row[5]:,.0f}")
|
||||
|
||||
print("\n=== inventory_lots의 receipt_line_id 연결 확인 ===")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total_lots,
|
||||
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN 1 ELSE 0 END) as matched_lots,
|
||||
SUM(CASE WHEN receipt_line_id IS NULL THEN 1 ELSE 0 END) as unmatched_lots
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
print(f" 전체 LOT: {result[0]}개")
|
||||
print(f" 입고장 연결된 LOT: {result[1]}개")
|
||||
print(f" 입고장 연결 안된 LOT: {result[2]}개")
|
||||
|
||||
conn.close()
|
||||
35
dev_scripts/check_purchase_tables.py
Normal file
35
dev_scripts/check_purchase_tables.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 테이블 목록 확인
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
tables = cursor.fetchall()
|
||||
|
||||
print("=== 전체 테이블 목록 ===")
|
||||
for table in tables:
|
||||
print(f" - {table[0]}")
|
||||
|
||||
print("\n=== inventory_lots 테이블 구조 ===")
|
||||
cursor.execute("PRAGMA table_info(inventory_lots)")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
print(f" {col[1]}: {col[2]}")
|
||||
|
||||
print("\n=== inventory_lots 샘플 데이터 ===")
|
||||
cursor.execute("""
|
||||
SELECT lot_id, lot_number, herb_item_id, quantity_onhand,
|
||||
unit_price_per_g, received_date, receipt_id
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0
|
||||
LIMIT 5
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
print(f" LOT {row[0]}: {row[1]}, 재고:{row[3]}g, 단가:₩{row[4]}, 입고일:{row[5]}, receipt_id:{row[6]}")
|
||||
|
||||
conn.close()
|
||||
109
dev_scripts/check_samsoeun_ingredients.py
Normal file
109
dev_scripts/check_samsoeun_ingredients.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
삼소음에 사용되는 약재들의 성분 코드 확인
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_herb_codes():
|
||||
"""약재 성분 코드 확인"""
|
||||
|
||||
# 삼소음에 사용되는 약재들
|
||||
herbs_to_check = [
|
||||
"인삼",
|
||||
"소엽", # 자소엽
|
||||
"전호",
|
||||
"반하",
|
||||
"갈근",
|
||||
"적복령", # 적복령 또는 복령
|
||||
"대조", # 대추
|
||||
"진피",
|
||||
"길경",
|
||||
"지각",
|
||||
"감초",
|
||||
"건강"
|
||||
]
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
herb_codes = {}
|
||||
|
||||
print("🌿 삼소음 약재 성분 코드 확인")
|
||||
print("="*60)
|
||||
|
||||
for herb in herbs_to_check:
|
||||
# 정확한 이름으로 먼저 검색
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name, herb_name_hanja
|
||||
FROM herb_masters
|
||||
WHERE herb_name = ?
|
||||
""", (herb,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
|
||||
# 정확한 이름이 없으면 포함된 이름으로 검색
|
||||
if not result:
|
||||
# 특수 케이스 처리
|
||||
if herb == "소엽":
|
||||
search_term = "자소엽"
|
||||
elif herb == "대조":
|
||||
search_term = "대추"
|
||||
elif herb == "적복령":
|
||||
search_term = "적복령"
|
||||
else:
|
||||
search_term = herb
|
||||
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name, herb_name_hanja
|
||||
FROM herb_masters
|
||||
WHERE herb_name LIKE ? OR herb_name = ?
|
||||
ORDER BY
|
||||
CASE WHEN herb_name = ? THEN 0 ELSE 1 END,
|
||||
LENGTH(herb_name)
|
||||
LIMIT 1
|
||||
""", (f'%{search_term}%', search_term, search_term))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
herb_codes[herb] = result[0]
|
||||
print(f"✅ {herb}: {result[0]} ({result[1]})")
|
||||
else:
|
||||
print(f"❌ {herb}: 찾을 수 없음")
|
||||
|
||||
# 유사한 이름 검색
|
||||
cursor.execute("""
|
||||
SELECT herb_name
|
||||
FROM herb_masters
|
||||
WHERE herb_name LIKE ?
|
||||
LIMIT 5
|
||||
""", (f'%{herb[:2]}%',))
|
||||
similar = cursor.fetchall()
|
||||
if similar:
|
||||
print(f" 유사한 약재: {', '.join([s[0] for s in similar])}")
|
||||
|
||||
# 복령 관련 추가 확인
|
||||
if "적복령" not in herb_codes or not herb_codes.get("적복령"):
|
||||
print("\n📌 복령 관련 약재 추가 검색:")
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name
|
||||
FROM herb_masters
|
||||
WHERE herb_name LIKE '%복령%'
|
||||
ORDER BY herb_name
|
||||
""")
|
||||
bokryung_list = cursor.fetchall()
|
||||
for code, name in bokryung_list:
|
||||
print(f" - {code}: {name}")
|
||||
|
||||
conn.close()
|
||||
|
||||
return herb_codes
|
||||
|
||||
if __name__ == "__main__":
|
||||
herb_codes = check_herb_codes()
|
||||
|
||||
print("\n📊 약재 코드 매핑 결과:")
|
||||
print("-"*60)
|
||||
for herb, code in herb_codes.items():
|
||||
if code:
|
||||
print(f'"{herb}": "{code}",')
|
||||
119
dev_scripts/check_sipjeondaebotang.py
Normal file
119
dev_scripts/check_sipjeondaebotang.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
십전대보탕 데이터 조회 및 분석
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_sipjeondaebotang():
|
||||
"""십전대보탕 처방 상세 조회"""
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("🔍 십전대보탕 처방 조회")
|
||||
print("="*70)
|
||||
|
||||
# 십전대보탕 처방 찾기
|
||||
cursor.execute("""
|
||||
SELECT formula_id, formula_code, formula_name, formula_type,
|
||||
base_cheop, base_pouches, description, is_active
|
||||
FROM formulas
|
||||
WHERE formula_name LIKE '%십전대보%'
|
||||
OR formula_name LIKE '%십전대보탕%'
|
||||
OR formula_code LIKE '%SJDB%'
|
||||
""")
|
||||
|
||||
formulas = cursor.fetchall()
|
||||
|
||||
if not formulas:
|
||||
print("❌ 십전대보탕 처방을 찾을 수 없습니다.")
|
||||
else:
|
||||
for formula_id, code, name, f_type, cheop, pouches, desc, active in formulas:
|
||||
print(f"\n📋 {name} ({code})")
|
||||
print(f" ID: {formula_id}")
|
||||
print(f" 타입: {f_type if f_type else '❌ 없음'}")
|
||||
print(f" 기본 첩수: {cheop if cheop else '❌ 없음'}")
|
||||
print(f" 기본 포수: {pouches if pouches else '❌ 없음'}")
|
||||
print(f" 설명: {desc if desc else '❌ 없음'}")
|
||||
print(f" 활성 상태: {'활성' if active else '비활성'}")
|
||||
|
||||
# 처방 구성 약재 확인
|
||||
cursor.execute("""
|
||||
SELECT hm.herb_name, hm.ingredient_code, fi.grams_per_cheop, fi.notes
|
||||
FROM formula_ingredients fi
|
||||
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
|
||||
WHERE fi.formula_id = ?
|
||||
ORDER BY fi.sort_order
|
||||
""", (formula_id,))
|
||||
|
||||
ingredients = cursor.fetchall()
|
||||
print(f"\n 구성 약재 ({len(ingredients)}개):")
|
||||
print(" " + "-"*60)
|
||||
print(f" {'약재명':15s} | {'용량(g)':>8s} | {'효능 설명'}")
|
||||
print(" " + "-"*60)
|
||||
|
||||
total_amount = 0
|
||||
for herb_name, code, amount, notes in ingredients:
|
||||
total_amount += amount
|
||||
notes_str = notes if notes else "❌ 효능 설명 없음"
|
||||
print(f" {herb_name:15s} | {amount:8.1f} | {notes_str}")
|
||||
|
||||
print(" " + "-"*60)
|
||||
print(f" {'총 용량':15s} | {total_amount:8.1f} |")
|
||||
|
||||
# 빠진 정보 체크
|
||||
print(f"\n ⚠️ 빠진 정보 체크:")
|
||||
missing = []
|
||||
if not desc:
|
||||
missing.append("처방 설명")
|
||||
if not f_type:
|
||||
missing.append("처방 타입")
|
||||
if not cheop:
|
||||
missing.append("기본 첩수")
|
||||
if not pouches:
|
||||
missing.append("기본 포수")
|
||||
|
||||
# 약재별 효능 설명 체크
|
||||
missing_notes = []
|
||||
for herb_name, code, amount, notes in ingredients:
|
||||
if not notes:
|
||||
missing_notes.append(herb_name)
|
||||
|
||||
if missing:
|
||||
print(f" - 처방 기본 정보: {', '.join(missing)}")
|
||||
if missing_notes:
|
||||
print(f" - 약재 효능 설명 없음: {', '.join(missing_notes)}")
|
||||
|
||||
if not missing and not missing_notes:
|
||||
print(" ✅ 모든 정보가 완비되어 있습니다.")
|
||||
|
||||
# 십전대보탕 표준 구성 확인
|
||||
print(f"\n\n📚 십전대보탕 표준 구성 (참고용):")
|
||||
print("="*70)
|
||||
print("""
|
||||
십전대보탕은 사군자탕(인삼, 백출, 복령, 감초)과
|
||||
사물탕(당귀, 천궁, 백작약, 숙지황)을 합방한 처방으로,
|
||||
황기와 육계를 추가하여 총 10개 약재로 구성됩니다.
|
||||
|
||||
주요 효능: 기혈양허(氣血兩虛)를 치료하는 대표 처방
|
||||
- 대보기혈(大補氣血): 기와 혈을 크게 보함
|
||||
- 병후 회복, 수술 후 회복, 만성 피로에 사용
|
||||
|
||||
표준 구성 (1첩 기준):
|
||||
- 인삼 4g (대보원기)
|
||||
- 황기 4g (보기승양)
|
||||
- 백출 4g (보기건비)
|
||||
- 복령 4g (건비이수)
|
||||
- 감초 2g (조화제약)
|
||||
- 당귀(일당귀) 4g (보혈)
|
||||
- 천궁 4g (활혈)
|
||||
- 백작약 4g (보혈)
|
||||
- 숙지황 4g (보음보혈)
|
||||
- 육계 2g (온양보화)
|
||||
""")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_sipjeondaebotang()
|
||||
81
dev_scripts/check_ssanghwatang.py
Normal file
81
dev_scripts/check_ssanghwatang.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
쌍화탕 처방 및 당귀 약재 확인
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_ssanghwatang():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 쌍화탕 처방 찾기
|
||||
print("🔍 쌍화탕 처방 검색...")
|
||||
print("="*60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT formula_id, formula_code, formula_name
|
||||
FROM formulas
|
||||
WHERE formula_name LIKE '%쌍화%'
|
||||
""")
|
||||
|
||||
formulas = cursor.fetchall()
|
||||
|
||||
if not formulas:
|
||||
print("❌ 쌍화탕 처방을 찾을 수 없습니다.")
|
||||
else:
|
||||
for formula_id, code, name in formulas:
|
||||
print(f"\n📋 {name} ({code})")
|
||||
|
||||
# 처방 구성 약재 확인
|
||||
cursor.execute("""
|
||||
SELECT hm.herb_name, hm.ingredient_code, fi.grams_per_cheop
|
||||
FROM formula_ingredients fi
|
||||
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
|
||||
WHERE fi.formula_id = ?
|
||||
ORDER BY fi.sort_order
|
||||
""", (formula_id,))
|
||||
|
||||
ingredients = cursor.fetchall()
|
||||
print(" 구성 약재:")
|
||||
for herb_name, code, amount in ingredients:
|
||||
if '당귀' in herb_name:
|
||||
print(f" ⚠️ {herb_name} ({code}): {amount}g <-- 당귀 발견!")
|
||||
else:
|
||||
print(f" - {herb_name} ({code}): {amount}g")
|
||||
|
||||
# 당귀 관련 약재 검색
|
||||
print("\n\n🌿 당귀 관련 약재 검색...")
|
||||
print("="*60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name, herb_name_hanja
|
||||
FROM herb_masters
|
||||
WHERE herb_name LIKE '%당귀%'
|
||||
ORDER BY herb_name
|
||||
""")
|
||||
|
||||
danggui_herbs = cursor.fetchall()
|
||||
for code, name, hanja in danggui_herbs:
|
||||
print(f"{code}: {name} ({hanja})")
|
||||
|
||||
# 일당귀 확인
|
||||
print("\n✅ 일당귀 검색:")
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name, herb_name_hanja
|
||||
FROM herb_masters
|
||||
WHERE herb_name = '일당귀'
|
||||
OR herb_name LIKE '%일당귀%'
|
||||
""")
|
||||
|
||||
result = cursor.fetchall()
|
||||
if result:
|
||||
for code, name, hanja in result:
|
||||
print(f" {code}: {name} ({hanja})")
|
||||
else:
|
||||
print(" ❌ 일당귀를 찾을 수 없음")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_ssanghwatang()
|
||||
19
dev_scripts/check_tables.py
Normal file
19
dev_scripts/check_tables.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cur = conn.cursor()
|
||||
|
||||
# herb_item_tags 테이블 구조 확인
|
||||
cur.execute("PRAGMA table_info(herb_item_tags)")
|
||||
print("herb_item_tags 테이블 구조:")
|
||||
for row in cur.fetchall():
|
||||
print(f" {row}")
|
||||
|
||||
# 실제 테이블 목록 확인
|
||||
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'herb%' ORDER BY name")
|
||||
print("\n약재 관련 테이블:")
|
||||
for row in cur.fetchall():
|
||||
print(f" - {row[0]}")
|
||||
|
||||
conn.close()
|
||||
65
dev_scripts/check_totals.py
Normal file
65
dev_scripts/check_totals.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
|
||||
# 데이터베이스 연결
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=== 입고장별 총금액 확인 ===\n")
|
||||
|
||||
# 각 입고장의 라인별 총액 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
pr.receipt_id,
|
||||
pr.receipt_date,
|
||||
s.name as supplier_name,
|
||||
COUNT(prl.line_id) as line_count,
|
||||
SUM(prl.quantity_g) as total_quantity,
|
||||
SUM(prl.line_total) as calculated_total
|
||||
FROM purchase_receipts pr
|
||||
JOIN suppliers s ON pr.supplier_id = s.supplier_id
|
||||
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
|
||||
GROUP BY pr.receipt_id
|
||||
ORDER BY pr.receipt_date DESC
|
||||
""")
|
||||
|
||||
results = cursor.fetchall()
|
||||
|
||||
for row in results:
|
||||
print(f"입고장 ID: {row['receipt_id']}")
|
||||
print(f" 날짜: {row['receipt_date']}")
|
||||
print(f" 도매상: {row['supplier_name']}")
|
||||
print(f" 품목 수: {row['line_count']}개")
|
||||
print(f" 총 수량: {row['total_quantity']}g")
|
||||
print(f" 총 금액: {row['calculated_total']:,.0f}원" if row['calculated_total'] else " 총 금액: 0원")
|
||||
print("-" * 40)
|
||||
|
||||
print("\n=== 입고장 라인 상세 (첫 번째 입고장) ===\n")
|
||||
|
||||
# 첫 번째 입고장의 라인 상세 확인
|
||||
if results:
|
||||
first_receipt_id = results[0]['receipt_id']
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
herb_item_id,
|
||||
quantity_g,
|
||||
unit_price_per_g,
|
||||
line_total
|
||||
FROM purchase_receipt_lines
|
||||
WHERE receipt_id = ?
|
||||
LIMIT 5
|
||||
""", (first_receipt_id,))
|
||||
|
||||
lines = cursor.fetchall()
|
||||
for line in lines:
|
||||
print(f"약재 ID: {line['herb_item_id']}")
|
||||
print(f" 수량: {line['quantity_g']}g")
|
||||
print(f" 단가: {line['unit_price_per_g']}원/g")
|
||||
print(f" 라인 총액: {line['line_total']}원")
|
||||
print(f" 계산 검증: {line['quantity_g']} × {line['unit_price_per_g']} = {line['quantity_g'] * line['unit_price_per_g']}원")
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
78
dev_scripts/check_wolbitang_ingredients.py
Normal file
78
dev_scripts/check_wolbitang_ingredients.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
월비탕에 사용되는 약재들의 성분 코드 확인
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def check_herb_codes():
|
||||
"""약재 성분 코드 확인"""
|
||||
|
||||
# 월비탕에 사용되는 약재들
|
||||
herbs_to_check = [
|
||||
"마황",
|
||||
"석고",
|
||||
"감초",
|
||||
"진피",
|
||||
"복령",
|
||||
"갈근",
|
||||
"건지황",
|
||||
"창출"
|
||||
]
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
herb_codes = {}
|
||||
|
||||
print("🌿 월비탕 약재 성분 코드 확인")
|
||||
print("="*50)
|
||||
|
||||
for herb in herbs_to_check:
|
||||
# 정확한 이름으로 먼저 검색
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name, herb_name_hanja
|
||||
FROM herb_masters
|
||||
WHERE herb_name = ?
|
||||
""", (herb,))
|
||||
|
||||
result = cursor.fetchone()
|
||||
|
||||
# 정확한 이름이 없으면 포함된 이름으로 검색
|
||||
if not result:
|
||||
cursor.execute("""
|
||||
SELECT ingredient_code, herb_name, herb_name_hanja
|
||||
FROM herb_masters
|
||||
WHERE herb_name LIKE ?
|
||||
ORDER BY LENGTH(herb_name)
|
||||
LIMIT 1
|
||||
""", (f'%{herb}%',))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
herb_codes[herb] = result[0]
|
||||
print(f"✅ {herb}: {result[0]} ({result[1]}, {result[2]})")
|
||||
else:
|
||||
print(f"❌ {herb}: 찾을 수 없음")
|
||||
# 비슷한 이름 찾기
|
||||
cursor.execute("""
|
||||
SELECT herb_name
|
||||
FROM herb_masters
|
||||
WHERE herb_name LIKE ?
|
||||
LIMIT 5
|
||||
""", (f'%{herb[:2]}%',))
|
||||
similar = cursor.fetchall()
|
||||
if similar:
|
||||
print(f" 유사한 약재: {', '.join([s[0] for s in similar])}")
|
||||
|
||||
conn.close()
|
||||
|
||||
return herb_codes
|
||||
|
||||
if __name__ == "__main__":
|
||||
herb_codes = check_herb_codes()
|
||||
|
||||
print("\n📊 약재 코드 매핑 결과:")
|
||||
print("-"*50)
|
||||
for herb, code in herb_codes.items():
|
||||
print(f'"{herb}": "{code}",')
|
||||
115
dev_scripts/debug_api_calculation.py
Normal file
115
dev_scripts/debug_api_calculation.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
API 재고 계산 디버깅
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def debug_api_calculation():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("API 재고 계산 디버깅")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# API와 동일한 쿼리 실행
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_item_id,
|
||||
h.insurance_code,
|
||||
h.herb_name,
|
||||
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
|
||||
COUNT(DISTINCT il.lot_id) as lot_count,
|
||||
COUNT(DISTINCT il.origin_country) as origin_count,
|
||||
AVG(il.unit_price_per_g) as avg_price,
|
||||
MIN(il.unit_price_per_g) as min_price,
|
||||
MAX(il.unit_price_per_g) as max_price,
|
||||
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
|
||||
FROM herb_items h
|
||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
|
||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name
|
||||
HAVING total_quantity > 0
|
||||
ORDER BY total_value DESC
|
||||
""")
|
||||
|
||||
items = cursor.fetchall()
|
||||
|
||||
print("상위 10개 약재별 재고 가치:")
|
||||
print("-" * 60)
|
||||
|
||||
total_api_value = 0
|
||||
for i, item in enumerate(items[:10], 1):
|
||||
value = item['total_value']
|
||||
total_api_value += value
|
||||
print(f"{i:2}. {item['herb_name']:15} 재고:{item['total_quantity']:8.0f}g 금액:₩{value:10,.0f}")
|
||||
|
||||
# 전체 합계 계산
|
||||
total_api_value = sum(item['total_value'] for item in items)
|
||||
|
||||
print()
|
||||
print(f"전체 약재 수: {len(items)}개")
|
||||
print(f"API 계산 총액: ₩{total_api_value:,.0f}")
|
||||
print()
|
||||
|
||||
# 직접 inventory_lots에서 계산
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(quantity_onhand * unit_price_per_g) as direct_total
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
direct_total = cursor.fetchone()['direct_total'] or 0
|
||||
|
||||
print(f"직접 계산 총액: ₩{direct_total:,.0f}")
|
||||
print(f"차이: ₩{total_api_value - direct_total:,.0f}")
|
||||
print()
|
||||
|
||||
# 차이 원인 분석
|
||||
if abs(total_api_value - direct_total) > 1:
|
||||
print("차이 원인 분석:")
|
||||
print("-" * 40)
|
||||
|
||||
# 중복 LOT 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
COUNT(*) as lot_count,
|
||||
SUM(il.quantity_onhand * il.unit_price_per_g) as total_value
|
||||
FROM herb_items h
|
||||
JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id
|
||||
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
|
||||
GROUP BY h.herb_item_id
|
||||
HAVING lot_count > 1
|
||||
ORDER BY total_value DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
multi_lots = cursor.fetchall()
|
||||
if multi_lots:
|
||||
print("\n여러 LOT을 가진 약재:")
|
||||
for herb in multi_lots:
|
||||
print(f" - {herb['herb_name']}: {herb['lot_count']}개 LOT, ₩{herb['total_value']:,.0f}")
|
||||
|
||||
# 특이사항 확인 - LEFT JOIN으로 인한 NULL 처리
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as herbs_without_lots
|
||||
FROM herb_items h
|
||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id
|
||||
AND il.is_depleted = 0
|
||||
AND il.quantity_onhand > 0
|
||||
WHERE il.lot_id IS NULL
|
||||
""")
|
||||
no_lots = cursor.fetchone()['herbs_without_lots']
|
||||
|
||||
if no_lots > 0:
|
||||
print(f"\n재고가 없는 약재 수: {no_lots}개")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug_api_calculation()
|
||||
103
dev_scripts/debug_receipt_detail.py
Normal file
103
dev_scripts/debug_receipt_detail.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
입고장 상세보기 오류 디버그
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import traceback
|
||||
|
||||
def debug_receipt_detail():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
receipt_id = 6
|
||||
|
||||
print("=== 1. 입고장 헤더 조회 ===")
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
pr.*,
|
||||
s.name as supplier_name,
|
||||
s.business_no as supplier_business_no,
|
||||
s.phone as supplier_phone
|
||||
FROM purchase_receipts pr
|
||||
JOIN suppliers s ON pr.supplier_id = s.supplier_id
|
||||
WHERE pr.receipt_id = ?
|
||||
""", (receipt_id,))
|
||||
|
||||
receipt = cursor.fetchone()
|
||||
if receipt:
|
||||
receipt_dict = dict(receipt)
|
||||
print("헤더 조회 성공!")
|
||||
for key, value in receipt_dict.items():
|
||||
print(f" {key}: {value} (type: {type(value).__name__})")
|
||||
else:
|
||||
print("입고장을 찾을 수 없습니다.")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"헤더 조회 오류: {e}")
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
print("\n=== 2. 입고장 상세 라인 조회 ===")
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
prl.*,
|
||||
h.herb_name,
|
||||
h.insurance_code,
|
||||
il.lot_id,
|
||||
il.quantity_onhand as current_stock
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
|
||||
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
WHERE prl.receipt_id = ?
|
||||
ORDER BY prl.line_id
|
||||
""", (receipt_id,))
|
||||
|
||||
lines = cursor.fetchall()
|
||||
print(f"라인 수: {len(lines)}개")
|
||||
|
||||
if lines:
|
||||
first_line = dict(lines[0])
|
||||
print("\n첫 번째 라인 데이터:")
|
||||
for key, value in first_line.items():
|
||||
print(f" {key}: {value} (type: {type(value).__name__})")
|
||||
except Exception as e:
|
||||
print(f"라인 조회 오류: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
print("\n=== 3. JSON 변환 테스트 ===")
|
||||
try:
|
||||
import json
|
||||
|
||||
# receipt_data 구성
|
||||
receipt_data = dict(receipt)
|
||||
receipt_data['lines'] = [dict(row) for row in lines]
|
||||
|
||||
# JSON 변환 시도
|
||||
json_str = json.dumps(receipt_data, ensure_ascii=False, default=str)
|
||||
print("JSON 변환 성공!")
|
||||
print(f"JSON 길이: {len(json_str)} 문자")
|
||||
|
||||
except Exception as e:
|
||||
print(f"JSON 변환 오류: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
# 문제가 되는 필드 찾기
|
||||
print("\n각 필드별 JSON 변환 테스트:")
|
||||
for key, value in receipt_data.items():
|
||||
try:
|
||||
json.dumps({key: value}, default=str)
|
||||
print(f" ✓ {key}: OK")
|
||||
except Exception as field_error:
|
||||
print(f" ✗ {key}: {field_error}")
|
||||
print(f" 값: {value}")
|
||||
print(f" 타입: {type(value)}")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug_receipt_detail()
|
||||
193
dev_scripts/final_inventory_analysis.py
Normal file
193
dev_scripts/final_inventory_analysis.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
재고 자산 금액 불일치 최종 분석
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
def final_analysis():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("재고 자산 금액 불일치 최종 분석")
|
||||
print("분석 시간:", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. 현재 DB의 실제 재고 자산
|
||||
print("📊 현재 데이터베이스 상태")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(quantity_onhand * unit_price_per_g) as total_value,
|
||||
COUNT(*) as lot_count,
|
||||
SUM(quantity_onhand) as total_quantity
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
current = cursor.fetchone()
|
||||
db_total = current['total_value'] or 0
|
||||
|
||||
print(f" DB 재고 자산: ₩{db_total:,.0f}")
|
||||
print(f" 활성 LOT: {current['lot_count']}개")
|
||||
print(f" 총 재고량: {current['total_quantity']:,.1f}g")
|
||||
print()
|
||||
|
||||
# 2. 입고와 출고 분석
|
||||
print("💼 입고/출고 분석")
|
||||
print("-" * 60)
|
||||
|
||||
# 입고 총액
|
||||
cursor.execute("""
|
||||
SELECT SUM(line_total) as total_in
|
||||
FROM purchase_receipt_lines
|
||||
""")
|
||||
total_in = cursor.fetchone()['total_in'] or 0
|
||||
|
||||
# 복합제 소비 금액
|
||||
cursor.execute("""
|
||||
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total_out
|
||||
FROM compound_consumptions cc
|
||||
JOIN inventory_lots il ON cc.lot_id = il.lot_id
|
||||
""")
|
||||
total_out = cursor.fetchone()['total_out'] or 0
|
||||
|
||||
print(f" 총 입고 금액: ₩{total_in:,.0f}")
|
||||
print(f" 총 소비 금액: ₩{total_out:,.0f}")
|
||||
print(f" 예상 잔액: ₩{total_in - total_out:,.0f}")
|
||||
print()
|
||||
|
||||
# 3. 차이 분석
|
||||
print("🔍 차이 분석 결과")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
ui_value = 5875708 # 화면에 표시되는 금액
|
||||
expected_value = total_in - total_out
|
||||
|
||||
print(f" 화면 표시 금액: ₩{ui_value:,.0f}")
|
||||
print(f" DB 계산 금액: ₩{db_total:,.0f}")
|
||||
print(f" 예상 금액 (입고-소비): ₩{expected_value:,.0f}")
|
||||
print()
|
||||
|
||||
print(" 차이:")
|
||||
print(f" 화면 vs DB: ₩{ui_value - db_total:,.0f}")
|
||||
print(f" 화면 vs 예상: ₩{ui_value - expected_value:,.0f}")
|
||||
print(f" DB vs 예상: ₩{db_total - expected_value:,.0f}")
|
||||
print()
|
||||
|
||||
# 4. 가능한 원인 분석
|
||||
print("❗ 불일치 원인 분석")
|
||||
print("-" * 60)
|
||||
|
||||
# 4-1. 단가 차이 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
prl.line_id,
|
||||
h.herb_name,
|
||||
prl.quantity_g as purchase_qty,
|
||||
prl.unit_price_per_g as purchase_price,
|
||||
prl.line_total as purchase_total,
|
||||
il.quantity_onhand as current_qty,
|
||||
il.unit_price_per_g as lot_price,
|
||||
il.quantity_onhand * il.unit_price_per_g as current_value,
|
||||
ABS(prl.unit_price_per_g - il.unit_price_per_g) as price_diff
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
|
||||
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
|
||||
AND ABS(prl.unit_price_per_g - il.unit_price_per_g) > 0.01
|
||||
ORDER BY price_diff DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
price_diffs = cursor.fetchall()
|
||||
if price_diffs:
|
||||
print("\n ⚠️ 입고 단가와 LOT 단가가 다른 항목:")
|
||||
for pd in price_diffs:
|
||||
print(f" {pd['herb_name']}:")
|
||||
print(f" 입고 단가: ₩{pd['purchase_price']:.2f}/g")
|
||||
print(f" LOT 단가: ₩{pd['lot_price']:.2f}/g")
|
||||
print(f" 차이: ₩{pd['price_diff']:.2f}/g")
|
||||
|
||||
# 4-2. 소비 후 남은 재고 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
il.lot_number,
|
||||
il.quantity_received as original_qty,
|
||||
il.quantity_onhand as current_qty,
|
||||
il.quantity_received - il.quantity_onhand as consumed_qty,
|
||||
il.unit_price_per_g
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE il.is_depleted = 0
|
||||
AND il.quantity_received > il.quantity_onhand
|
||||
ORDER BY (il.quantity_received - il.quantity_onhand) DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
|
||||
consumed_lots = cursor.fetchall()
|
||||
if consumed_lots:
|
||||
print("\n 📉 소비된 재고가 있는 LOT (상위 5개):")
|
||||
for cl in consumed_lots:
|
||||
print(f" {cl['herb_name']} (LOT: {cl['lot_number']})")
|
||||
print(f" 원래: {cl['original_qty']:,.0f}g → 현재: {cl['current_qty']:,.0f}g")
|
||||
print(f" 소비: {cl['consumed_qty']:,.0f}g (₩{cl['consumed_qty'] * cl['unit_price_per_g']:,.0f})")
|
||||
|
||||
# 4-3. JavaScript 계산 로직 확인 필요
|
||||
print("\n 💡 추가 확인 필요사항:")
|
||||
print(" 1) 프론트엔드 JavaScript에서 재고 자산을 계산하는 로직")
|
||||
print(" 2) 캐시 또는 세션 스토리지에 저장된 이전 값")
|
||||
print(" 3) inventory_lots_v2 테이블 사용 여부")
|
||||
|
||||
# inventory_lots_v2 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(quantity_onhand * unit_price_per_g) as v2_total,
|
||||
COUNT(*) as v2_count
|
||||
FROM inventory_lots_v2
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
v2_result = cursor.fetchone()
|
||||
if v2_result and v2_result['v2_count'] > 0:
|
||||
v2_total = v2_result['v2_total'] or 0
|
||||
print(f"\n ⚠️ inventory_lots_v2 테이블 데이터:")
|
||||
print(f" 재고 자산: ₩{v2_total:,.0f}")
|
||||
print(f" LOT 수: {v2_result['v2_count']}개")
|
||||
|
||||
if abs(v2_total - ui_value) < 100:
|
||||
print(f" → 화면 금액과 일치할 가능성 높음!")
|
||||
|
||||
print()
|
||||
|
||||
# 5. 결론
|
||||
print("📝 결론")
|
||||
print("=" * 60)
|
||||
|
||||
diff = ui_value - db_total
|
||||
if diff > 0:
|
||||
print(f" 화면에 표시되는 금액(₩{ui_value:,.0f})이")
|
||||
print(f" 실제 DB 금액(₩{db_total:,.0f})보다")
|
||||
print(f" ₩{diff:,.0f} 더 많습니다.")
|
||||
print()
|
||||
print(" 가능한 원인:")
|
||||
print(" 1) 프론트엔드에서 별도의 계산 로직 사용")
|
||||
print(" 2) 캐시된 이전 데이터 표시")
|
||||
print(" 3) inventory_lots_v2 테이블 참조")
|
||||
print(" 4) 재고 보정 내역이 즉시 반영되지 않음")
|
||||
else:
|
||||
print(f" 실제 DB 금액이 화면 표시 금액보다 적습니다.")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
final_analysis()
|
||||
183
dev_scripts/final_price_analysis.py
Normal file
183
dev_scripts/final_price_analysis.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
최종 가격 차이 분석
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def final_price_analysis():
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 80)
|
||||
print("📊 재고 자산 차이 최종 분석")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. 핵심 차이 확인
|
||||
print("1. 핵심 금액 차이")
|
||||
print("-" * 60)
|
||||
|
||||
# 입고 라인과 LOT 차이
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
prl.quantity_g as receipt_qty,
|
||||
prl.unit_price_per_g as receipt_price,
|
||||
prl.line_total as receipt_total,
|
||||
il.quantity_received as lot_qty,
|
||||
il.unit_price_per_g as lot_price,
|
||||
il.quantity_received * il.unit_price_per_g as lot_total,
|
||||
prl.line_total - (il.quantity_received * il.unit_price_per_g) as diff
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
|
||||
WHERE ABS(prl.line_total - (il.quantity_received * il.unit_price_per_g)) > 1
|
||||
ORDER BY ABS(prl.line_total - (il.quantity_received * il.unit_price_per_g)) DESC
|
||||
""")
|
||||
|
||||
differences = cursor.fetchall()
|
||||
|
||||
if differences:
|
||||
print(" 입고장과 LOT 생성 시 차이가 있는 항목:")
|
||||
print()
|
||||
total_diff = 0
|
||||
for diff in differences:
|
||||
print(f" 📌 {diff['herb_name']}")
|
||||
print(f" 입고장: {diff['receipt_qty']:,.0f}g × ₩{diff['receipt_price']:.2f} = ₩{diff['receipt_total']:,.0f}")
|
||||
print(f" LOT: {diff['lot_qty']:,.0f}g × ₩{diff['lot_price']:.2f} = ₩{diff['lot_total']:,.0f}")
|
||||
print(f" 차이: ₩{diff['diff']:,.0f}")
|
||||
print()
|
||||
total_diff += diff['diff']
|
||||
|
||||
print(f" 총 차이: ₩{total_diff:,.0f}")
|
||||
|
||||
# 2. 재고 자산 흐름
|
||||
print("\n2. 재고 자산 흐름 정리")
|
||||
print("=" * 60)
|
||||
|
||||
# 각 단계별 금액
|
||||
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
|
||||
receipt_total = cursor.fetchone()['total'] or 0
|
||||
|
||||
cursor.execute("""
|
||||
SELECT SUM(quantity_received * unit_price_per_g) as total
|
||||
FROM inventory_lots
|
||||
""")
|
||||
lot_creation_total = cursor.fetchone()['total'] or 0
|
||||
|
||||
cursor.execute("""
|
||||
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
|
||||
FROM compound_consumptions cc
|
||||
JOIN inventory_lots il ON cc.lot_id = il.lot_id
|
||||
""")
|
||||
consumed_total = cursor.fetchone()['total'] or 0
|
||||
|
||||
cursor.execute("""
|
||||
SELECT SUM(quantity_onhand * unit_price_per_g) as total
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
current_inventory = cursor.fetchone()['total'] or 0
|
||||
|
||||
print(f" 1️⃣ 입고장 총액: ₩{receipt_total:,.0f}")
|
||||
print(f" 2️⃣ LOT 생성 총액: ₩{lot_creation_total:,.0f}")
|
||||
print(f" 차이 (1-2): ₩{receipt_total - lot_creation_total:,.0f}")
|
||||
print()
|
||||
print(f" 3️⃣ 소비 총액: ₩{consumed_total:,.0f}")
|
||||
print(f" 4️⃣ 현재 재고 자산: ₩{current_inventory:,.0f}")
|
||||
print()
|
||||
print(f" 📊 계산식:")
|
||||
print(f" LOT 생성 - 소비 = ₩{lot_creation_total:,.0f} - ₩{consumed_total:,.0f}")
|
||||
print(f" = ₩{lot_creation_total - consumed_total:,.0f} (예상)")
|
||||
print(f" 실제 재고 = ₩{current_inventory:,.0f}")
|
||||
print(f" 차이 = ₩{current_inventory - (lot_creation_total - consumed_total):,.0f}")
|
||||
|
||||
# 3. 차이 원인 분석
|
||||
print("\n3. 차이 원인 설명")
|
||||
print("-" * 60)
|
||||
|
||||
# 휴먼일당귀 특별 케이스 확인
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
prl.quantity_g as receipt_qty,
|
||||
il.quantity_received as lot_received,
|
||||
il.quantity_onhand as lot_current
|
||||
FROM purchase_receipt_lines prl
|
||||
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
|
||||
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
|
||||
WHERE h.herb_name = '휴먼일당귀'
|
||||
""")
|
||||
|
||||
ildan = cursor.fetchone()
|
||||
if ildan:
|
||||
print("\n 💡 휴먼일당귀 케이스:")
|
||||
print(f" 입고장 수량: {ildan['receipt_qty']:,.0f}g")
|
||||
print(f" LOT 생성 수량: {ildan['lot_received']:,.0f}g")
|
||||
print(f" 현재 재고: {ildan['lot_current']:,.0f}g")
|
||||
print(f" → 입고 시 5,000g 중 3,000g만 LOT 생성됨")
|
||||
print(f" → 나머지 2,000g는 별도 처리되었을 가능성")
|
||||
|
||||
print("\n 📝 결론:")
|
||||
print(" 1. 입고장 총액 (₩1,616,400) vs LOT 생성 총액 (₩1,607,400)")
|
||||
print(" → ₩9,000 차이 (휴먼일당귀 수량 차이로 인함)")
|
||||
print()
|
||||
print(" 2. 예상 재고 (₩1,529,434) vs 실제 재고 (₩1,529,434)")
|
||||
print(" → 정확히 일치")
|
||||
print()
|
||||
print(" 3. 입고 기준 예상 (₩1,538,434) vs 실제 재고 (₩1,529,434)")
|
||||
print(" → ₩9,000 차이 (입고와 LOT 생성 차이와 동일)")
|
||||
|
||||
# 4. 추가 LOT 확인
|
||||
print("\n4. 추가 LOT 존재 여부")
|
||||
print("-" * 60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_name,
|
||||
COUNT(*) as lot_count,
|
||||
SUM(il.quantity_received) as total_received,
|
||||
SUM(il.quantity_onhand) as total_onhand
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE h.herb_name = '휴먼일당귀'
|
||||
GROUP BY h.herb_item_id
|
||||
""")
|
||||
|
||||
ildan_lots = cursor.fetchone()
|
||||
if ildan_lots:
|
||||
print(f" 휴먼일당귀 LOT 현황:")
|
||||
print(f" LOT 개수: {ildan_lots['lot_count']}개")
|
||||
print(f" 총 입고량: {ildan_lots['total_received']:,.0f}g")
|
||||
print(f" 현재 재고: {ildan_lots['total_onhand']:,.0f}g")
|
||||
|
||||
# 상세 LOT 정보
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
lot_id,
|
||||
lot_number,
|
||||
quantity_received,
|
||||
quantity_onhand,
|
||||
unit_price_per_g,
|
||||
receipt_line_id
|
||||
FROM inventory_lots il
|
||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
||||
WHERE h.herb_name = '휴먼일당귀'
|
||||
""")
|
||||
|
||||
lots = cursor.fetchall()
|
||||
for lot in lots:
|
||||
print(f"\n LOT {lot['lot_id']}:")
|
||||
print(f" LOT 번호: {lot['lot_number']}")
|
||||
print(f" 입고량: {lot['quantity_received']:,.0f}g")
|
||||
print(f" 현재: {lot['quantity_onhand']:,.0f}g")
|
||||
print(f" 단가: ₩{lot['unit_price_per_g']:.2f}")
|
||||
print(f" 입고라인: {lot['receipt_line_id']}")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
final_price_analysis()
|
||||
145
dev_scripts/final_verification.py
Normal file
145
dev_scripts/final_verification.py
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
최종 검증 - 문제 해결 확인
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
def final_verification():
|
||||
print("=" * 80)
|
||||
print("📊 재고 자산 문제 해결 최종 검증")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# 1. API 호출 결과
|
||||
print("1. API 응답 확인")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen('http://localhost:5001/api/inventory/summary') as response:
|
||||
data = json.loads(response.read())
|
||||
api_value = data['summary']['total_value']
|
||||
total_items = data['summary']['total_items']
|
||||
|
||||
print(f" API 재고 자산: ₩{api_value:,.0f}")
|
||||
print(f" 총 약재 수: {total_items}개")
|
||||
except Exception as e:
|
||||
print(f" API 호출 실패: {e}")
|
||||
api_value = 0
|
||||
|
||||
# 2. 데이터베이스 직접 계산
|
||||
print("\n2. 데이터베이스 직접 계산")
|
||||
print("-" * 60)
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(quantity_onhand * unit_price_per_g) as total_value,
|
||||
COUNT(*) as lot_count,
|
||||
SUM(quantity_onhand) as total_quantity
|
||||
FROM inventory_lots
|
||||
WHERE is_depleted = 0 AND quantity_onhand > 0
|
||||
""")
|
||||
|
||||
db_result = cursor.fetchone()
|
||||
db_value = db_result['total_value'] or 0
|
||||
|
||||
print(f" DB 재고 자산: ₩{db_value:,.0f}")
|
||||
print(f" 활성 LOT: {db_result['lot_count']}개")
|
||||
print(f" 총 재고량: {db_result['total_quantity']:,.1f}g")
|
||||
|
||||
# 3. 입고와 출고 기반 계산
|
||||
print("\n3. 입고/출고 기반 계산")
|
||||
print("-" * 60)
|
||||
|
||||
# 총 입고액
|
||||
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
|
||||
total_in = cursor.fetchone()['total'] or 0
|
||||
|
||||
# 총 소비액
|
||||
cursor.execute("""
|
||||
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
|
||||
FROM compound_consumptions cc
|
||||
JOIN inventory_lots il ON cc.lot_id = il.lot_id
|
||||
""")
|
||||
total_out = cursor.fetchone()['total'] or 0
|
||||
|
||||
expected = total_in - total_out
|
||||
|
||||
print(f" 입고 총액: ₩{total_in:,.0f}")
|
||||
print(f" 소비 총액: ₩{total_out:,.0f}")
|
||||
print(f" 예상 재고: ₩{expected:,.0f}")
|
||||
|
||||
# 4. 결과 비교
|
||||
print("\n4. 결과 비교")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\n 🎯 API 재고 자산: ₩{api_value:,.0f}")
|
||||
print(f" 🎯 DB 직접 계산: ₩{db_value:,.0f}")
|
||||
print(f" 🎯 예상 재고액: ₩{expected:,.0f}")
|
||||
|
||||
# 차이 계산
|
||||
api_db_diff = abs(api_value - db_value)
|
||||
db_expected_diff = abs(db_value - expected)
|
||||
|
||||
print(f"\n API vs DB 차이: ₩{api_db_diff:,.0f}")
|
||||
print(f" DB vs 예상 차이: ₩{db_expected_diff:,.0f}")
|
||||
|
||||
# 5. 결론
|
||||
print("\n5. 결론")
|
||||
print("=" * 60)
|
||||
|
||||
if api_db_diff < 100:
|
||||
print("\n ✅ 문제 해결 완료!")
|
||||
print(" API와 DB 계산이 일치합니다.")
|
||||
print(f" 재고 자산: ₩{api_value:,.0f}")
|
||||
else:
|
||||
print("\n ⚠️ 아직 차이가 있습니다.")
|
||||
print(f" 차이: ₩{api_db_diff:,.0f}")
|
||||
|
||||
if db_expected_diff > 100000:
|
||||
print("\n 📌 참고: DB 재고와 예상 재고 간 차이는")
|
||||
print(" 다음 요인들로 인해 발생할 수 있습니다:")
|
||||
print(" - 입고 시점과 LOT 생성 시점의 단가 차이")
|
||||
print(" - 재고 보정 내역")
|
||||
print(" - 반올림 오차 누적")
|
||||
|
||||
# 6. 효능 태그 확인 (중복 문제가 해결되었는지)
|
||||
print("\n6. 효능 태그 표시 확인")
|
||||
print("-" * 60)
|
||||
|
||||
# API에서 효능 태그가 있는 약재 확인
|
||||
try:
|
||||
with urllib.request.urlopen('http://localhost:5001/api/inventory/summary') as response:
|
||||
data = json.loads(response.read())
|
||||
|
||||
herbs_with_tags = [
|
||||
item for item in data['data']
|
||||
if item.get('efficacy_tags') and len(item['efficacy_tags']) > 0
|
||||
]
|
||||
|
||||
print(f" 효능 태그가 있는 약재: {len(herbs_with_tags)}개")
|
||||
|
||||
if herbs_with_tags:
|
||||
sample = herbs_with_tags[0]
|
||||
print(f"\n 예시: {sample['herb_name']}")
|
||||
print(f" 태그: {', '.join(sample['efficacy_tags'])}")
|
||||
print(f" 재고 가치: ₩{sample['total_value']:,.0f}")
|
||||
except Exception as e:
|
||||
print(f" 효능 태그 확인 실패: {e}")
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("검증 완료")
|
||||
print("=" * 80)
|
||||
|
||||
if __name__ == "__main__":
|
||||
final_verification()
|
||||
200
dev_scripts/test_compound_e2e.py
Normal file
200
dev_scripts/test_compound_e2e.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
E2E 테스트: 조제 화면에서 쌍화탕 선택 후 인삼 선택 가능 확인
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright, expect
|
||||
import time
|
||||
|
||||
def test_compound_ginseng_selection():
|
||||
"""쌍화탕 조제 시 인삼 선택 가능 테스트"""
|
||||
|
||||
with sync_playwright() as p:
|
||||
# 브라우저 실행 (headless 모드)
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
try:
|
||||
print("=" * 80)
|
||||
print("E2E 테스트: 쌍화탕 조제 시 인삼 선택 가능 확인")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. 메인 페이지 접속
|
||||
print("\n[1] 메인 페이지 접속...")
|
||||
page.goto('http://localhost:5001')
|
||||
page.wait_for_load_state('networkidle')
|
||||
print("✓ 페이지 로드 완료")
|
||||
|
||||
# 2. 조제관리 메뉴 클릭
|
||||
print("\n[2] 조제관리 메뉴 클릭...")
|
||||
|
||||
# 사이드바에서 조제 관리 클릭
|
||||
compound_menu = page.locator('text=조제 관리').first
|
||||
compound_menu.click()
|
||||
time.sleep(2)
|
||||
print("✓ 조제관리 화면 진입")
|
||||
|
||||
# 조제 입력 섹션 표시
|
||||
print("\n[2-1] 조제 입력 섹션 표시...")
|
||||
show_compound_entry = page.locator('#showCompoundEntry')
|
||||
if show_compound_entry.count() > 0:
|
||||
show_compound_entry.click()
|
||||
time.sleep(1)
|
||||
print("✓ 조제 입력 섹션 표시")
|
||||
|
||||
# 3. 현재 화면 상태 확인
|
||||
print("\n[3] 화면 상태 확인...")
|
||||
|
||||
# 스크린샷 저장
|
||||
page.screenshot(path='/tmp/compound_screen_after_menu_click.png')
|
||||
print("✓ 스크린샷: /tmp/compound_screen_after_menu_click.png")
|
||||
|
||||
# 페이지에 select 요소가 있는지 확인
|
||||
all_selects = page.locator('select').all()
|
||||
print(f"✓ 페이지 내 select 요소: {len(all_selects)}개")
|
||||
|
||||
for idx, sel in enumerate(all_selects):
|
||||
sel_id = sel.get_attribute('id')
|
||||
sel_name = sel.get_attribute('name')
|
||||
print(f" [{idx}] id={sel_id}, name={sel_name}")
|
||||
|
||||
# 처방 선택 시도
|
||||
print("\n[4] 처방 선택...")
|
||||
|
||||
# compoundFormula select 요소 찾기 (ID로 정확히)
|
||||
formula_select = page.locator('#compoundFormula')
|
||||
|
||||
if formula_select.count() > 0:
|
||||
# select가 visible 될 때까지 기다리기
|
||||
try:
|
||||
formula_select.wait_for(state="visible", timeout=5000)
|
||||
except:
|
||||
print("⚠️ 처방 선택 드롭다운이 보이지 않음")
|
||||
|
||||
# 옵션 확인
|
||||
options = formula_select.locator('option').all()
|
||||
print(f"✓ 드롭다운 옵션: {len(options)}개")
|
||||
for opt in options:
|
||||
print(f" - {opt.text_content()}")
|
||||
|
||||
# 쌍화탕 선택
|
||||
try:
|
||||
formula_select.select_option(label='쌍화탕')
|
||||
time.sleep(3)
|
||||
print("✓ 쌍화탕 선택 완료")
|
||||
except Exception as e:
|
||||
print(f"⚠️ label로 선택 실패: {e}")
|
||||
# index로 시도 (첫 번째 옵션은 보통 placeholder이므로 index=1)
|
||||
try:
|
||||
formula_select.select_option(index=1)
|
||||
time.sleep(3)
|
||||
print("✓ 첫 번째 처방 선택 완료")
|
||||
except Exception as e2:
|
||||
print(f"❌ 처방 선택 실패: {e2}")
|
||||
else:
|
||||
print("❌ 처방 드롭다운을 찾을 수 없음")
|
||||
|
||||
# 5. 약재 추가 버튼 클릭
|
||||
print("\n[5] 약재 추가 버튼 클릭...")
|
||||
|
||||
# 약재 추가 버튼 찾기
|
||||
add_ingredient_btn = page.locator('#addIngredientBtn')
|
||||
|
||||
if add_ingredient_btn.count() > 0:
|
||||
add_ingredient_btn.click()
|
||||
time.sleep(1)
|
||||
print("✓ 약재 추가 버튼 클릭 완료")
|
||||
|
||||
# 6. 새로 추가된 행에서 약재 선택 드롭다운 확인
|
||||
print("\n[6] 약재 선택 드롭다운 확인...")
|
||||
|
||||
# 새로 추가된 행 찾기 (마지막 행)
|
||||
new_row = page.locator('#compoundIngredients tr').last
|
||||
|
||||
# 약재 선택 드롭다운 찾기
|
||||
herb_select = new_row.locator('.herb-select-compound')
|
||||
|
||||
if herb_select.count() > 0:
|
||||
print("✓ 약재 선택 드롭다운 발견")
|
||||
|
||||
# 드롭다운 옵션 확인
|
||||
time.sleep(1) # 드롭다운이 로드될 시간 확보
|
||||
options = herb_select.locator('option').all()
|
||||
print(f"✓ 약재 옵션: {len(options)}개")
|
||||
|
||||
# 처음 10개 옵션 출력
|
||||
for idx, option in enumerate(options[:10]):
|
||||
text = option.text_content()
|
||||
value = option.get_attribute('value')
|
||||
print(f" [{idx}] {text} (value: {value})")
|
||||
|
||||
# 마스터 약재명이 표시되는지 확인
|
||||
has_master_names = False
|
||||
for option in options:
|
||||
text = option.text_content()
|
||||
# ingredient_code 형식의 value와 한글/한자 형식의 텍스트 확인
|
||||
if '(' in text and ')' in text: # 한자 포함 형식
|
||||
has_master_names = True
|
||||
break
|
||||
|
||||
if has_master_names:
|
||||
print("\n✅ 마스터 약재명이 드롭다운에 표시됨!")
|
||||
|
||||
# 인삼 선택 시도
|
||||
try:
|
||||
herb_select.select_option(label='인삼 (人蔘)')
|
||||
print("✓ 인삼 선택 완료")
|
||||
except:
|
||||
# label이 정확히 일치하지 않으면 부분 매칭
|
||||
for idx, option in enumerate(options):
|
||||
if '인삼' in option.text_content():
|
||||
herb_select.select_option(index=idx)
|
||||
print(f"✓ 인삼 선택 완료 (index {idx})")
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 제품 선택 드롭다운 확인
|
||||
product_select = new_row.locator('.product-select')
|
||||
if product_select.count() > 0:
|
||||
print("\n[7] 제품 선택 드롭다운 확인...")
|
||||
time.sleep(1) # 제품 목록 로드 대기
|
||||
|
||||
product_options = product_select.locator('option').all()
|
||||
print(f"✓ 제품 옵션: {len(product_options)}개")
|
||||
for idx, option in enumerate(product_options):
|
||||
print(f" [{idx}] {option.text_content()}")
|
||||
else:
|
||||
print("\n⚠️ 마스터 약재명 대신 제품명이 드롭다운에 표시됨")
|
||||
print("(신흥생강, 신흥작약 등의 제품명이 보임)")
|
||||
else:
|
||||
print("❌ 약재 선택 드롭다운을 찾을 수 없음")
|
||||
else:
|
||||
print("❌ 약재 추가 버튼을 찾을 수 없음")
|
||||
|
||||
# 7. 최종 스크린샷
|
||||
page.screenshot(path='/tmp/compound_screen_final.png')
|
||||
print("\n✓ 최종 스크린샷: /tmp/compound_screen_final.png")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("테스트 완료")
|
||||
print("=" * 80)
|
||||
|
||||
# 완료
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 에러 발생: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# 에러 스크린샷
|
||||
page.screenshot(path='/tmp/compound_error.png')
|
||||
print("에러 스크린샷: /tmp/compound_error.png")
|
||||
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_compound_ginseng_selection()
|
||||
92
dev_scripts/test_compound_page.py
Normal file
92
dev_scripts/test_compound_page.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
조제 페이지 드롭다운 테스트
|
||||
"""
|
||||
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:5001"
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("조제 페이지 기능 테스트")
|
||||
print("="*80)
|
||||
|
||||
# 1. 약재 마스터 목록 확인
|
||||
print("\n1. /api/herbs/masters 테스트:")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs/masters")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 성공: {data['success']}")
|
||||
print(f" 총 약재: {len(data['data'])}개")
|
||||
print(f" 재고 있는 약재: {data['stats']['herbs_with_stock']}개")
|
||||
print(f" 커버리지: {data['stats']['coverage_rate']}%")
|
||||
else:
|
||||
print(f" ❌ 실패: {response.status_code}")
|
||||
|
||||
# 2. 처방 목록 확인
|
||||
print("\n2. /api/formulas 테스트:")
|
||||
response = requests.get(f"{BASE_URL}/api/formulas")
|
||||
if response.status_code == 200:
|
||||
formulas = response.json()
|
||||
print(f" ✅ 성공: {len(formulas)}개 처방")
|
||||
|
||||
# 십전대보탕 찾기
|
||||
for f in formulas:
|
||||
if '십전대보탕' in f.get('formula_name', ''):
|
||||
print(f" 십전대보탕 ID: {f['formula_id']}")
|
||||
|
||||
# 처방 구성 확인
|
||||
response2 = requests.get(f"{BASE_URL}/api/formulas/{f['formula_id']}/ingredients")
|
||||
if response2.status_code == 200:
|
||||
ingredients = response2.json()
|
||||
print(f" 구성 약재: {len(ingredients)}개")
|
||||
for ing in ingredients[:3]:
|
||||
print(f" - {ing['herb_name']} ({ing['ingredient_code']}): {ing['grams_per_cheop']}g")
|
||||
break
|
||||
else:
|
||||
print(f" ❌ 실패: {response.status_code}")
|
||||
|
||||
# 3. 특정 약재(당귀)의 제품 목록 확인
|
||||
print("\n3. /api/herbs/by-ingredient/3400H1ACD (당귀) 테스트:")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs/by-ingredient/3400H1ACD")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 성공: {data['success']}")
|
||||
if data['data']:
|
||||
print(f" 당귀 제품 수: {len(data['data'])}개")
|
||||
for product in data['data'][:3]:
|
||||
print(f" - {product.get('herb_name', '제품명 없음')} ({product.get('insurance_code', '')})")
|
||||
print(f" 재고: {product.get('total_stock', 0)}g, 로트: {product.get('lot_count', 0)}개")
|
||||
else:
|
||||
print(f" ❌ 실패: {response.status_code}")
|
||||
|
||||
# 4. 재고 현황 페이지 API 확인
|
||||
print("\n4. /api/herbs (재고현황 API) 테스트:")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ 성공: {data['success']}")
|
||||
print(f" 약재 수: {len(data['data'])}개")
|
||||
|
||||
# 재고가 있는 약재 필터링
|
||||
herbs_with_stock = [h for h in data['data'] if h.get('current_stock', 0) > 0]
|
||||
print(f" 재고 있는 약재: {len(herbs_with_stock)}개")
|
||||
|
||||
for herb in herbs_with_stock[:3]:
|
||||
print(f" - {herb['herb_name']} ({herb['insurance_code']}): {herb['current_stock']}g")
|
||||
else:
|
||||
print(f" ❌ 실패: {response.status_code}")
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("테스트 완료")
|
||||
print("="*80)
|
||||
print("\n결론:")
|
||||
print("✅ 모든 API가 정상 작동하고 있습니다.")
|
||||
print("✅ 약재 드롭다운이 정상적으로 로드될 것으로 예상됩니다.")
|
||||
print("\n웹 브라우저에서 확인:")
|
||||
print("1. 조제 탭으로 이동")
|
||||
print("2. 처방 선택: 십전대보탕")
|
||||
print("3. '약재 추가' 버튼 클릭")
|
||||
print("4. 드롭다운에 약재 목록이 나타나는지 확인")
|
||||
46
dev_scripts/test_db.py
Normal file
46
dev_scripts/test_db.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
pr.receipt_id,
|
||||
pr.receipt_date,
|
||||
pr.receipt_no,
|
||||
pr.total_amount,
|
||||
pr.source_file,
|
||||
pr.created_at,
|
||||
s.name as supplier_name,
|
||||
s.supplier_id,
|
||||
COUNT(prl.line_id) as line_count,
|
||||
SUM(prl.quantity_g) as total_quantity
|
||||
FROM purchase_receipts pr
|
||||
JOIN suppliers s ON pr.supplier_id = s.supplier_id
|
||||
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
|
||||
GROUP BY pr.receipt_id
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
print("Row keys:", row.keys())
|
||||
receipt = dict(row)
|
||||
for key, value in receipt.items():
|
||||
print(f"{key}: {value} (type: {type(value).__name__})")
|
||||
|
||||
# 타입 변환 시도
|
||||
if isinstance(receipt.get('receipt_date'), bytes):
|
||||
receipt['receipt_date'] = receipt['receipt_date'].decode('utf-8')
|
||||
elif receipt.get('receipt_date'):
|
||||
receipt['receipt_date'] = str(receipt['receipt_date'])
|
||||
|
||||
if isinstance(receipt.get('created_at'), bytes):
|
||||
receipt['created_at'] = receipt['created_at'].decode('utf-8')
|
||||
elif receipt.get('created_at'):
|
||||
receipt['created_at'] = str(receipt['created_at'])
|
||||
|
||||
print("\nAfter conversion:")
|
||||
print(json.dumps(receipt, indent=2))
|
||||
68
dev_scripts/test_direct.py
Normal file
68
dev_scripts/test_direct.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
직접 JavaScript 함수 호출 테스트
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
def test_direct_load():
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
print("페이지 로드 중...")
|
||||
page.goto("http://localhost:5001")
|
||||
page.wait_for_load_state("networkidle")
|
||||
time.sleep(2)
|
||||
|
||||
# 직접 입고 관리 탭 활성화 및 함수 호출
|
||||
print("\n입고 데이터 직접 로드...")
|
||||
page.evaluate("""
|
||||
// 입고 탭 표시
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('show', 'active'));
|
||||
document.querySelector('#purchase').classList.add('show', 'active');
|
||||
|
||||
// 직접 함수 호출
|
||||
if (typeof loadPurchaseReceipts === 'function') {
|
||||
loadPurchaseReceipts();
|
||||
} else {
|
||||
console.error('loadPurchaseReceipts 함수를 찾을 수 없습니다');
|
||||
}
|
||||
""")
|
||||
time.sleep(2)
|
||||
|
||||
# 테이블 내용 확인
|
||||
table_html = page.inner_html('#purchaseReceiptsList')
|
||||
print(f"\n테이블 내용 (처음 500자):\n{table_html[:500]}")
|
||||
|
||||
# 테이블 행 수 확인
|
||||
row_count = page.evaluate("document.querySelectorAll('#purchaseReceiptsList tr').length")
|
||||
print(f"\n테이블 행 수: {row_count}")
|
||||
|
||||
# 첫 번째 행 내용 확인
|
||||
if row_count > 0:
|
||||
first_row = page.evaluate("""
|
||||
const row = document.querySelector('#purchaseReceiptsList tr');
|
||||
if (row) {
|
||||
const cells = row.querySelectorAll('td');
|
||||
return Array.from(cells).map(cell => cell.textContent.trim());
|
||||
}
|
||||
return null;
|
||||
""")
|
||||
if first_row:
|
||||
print(f"\n첫 번째 행 데이터:")
|
||||
headers = ['입고일', '공급업체', '품목 수', '총 금액', '총 수량', '파일명', '작업']
|
||||
for i, value in enumerate(first_row[:-1]): # 마지막 '작업' 열 제외
|
||||
if i < len(headers):
|
||||
print(f" {headers[i]}: {value}")
|
||||
|
||||
# 스크린샷
|
||||
page.screenshot(path="/root/kdrug/direct_test.png")
|
||||
print("\n스크린샷 저장: /root/kdrug/direct_test.png")
|
||||
|
||||
browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_direct_load()
|
||||
84
dev_scripts/test_direct_api.py
Normal file
84
dev_scripts/test_direct_api.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
API 함수 직접 테스트
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
# Flask 앱과 동일한 설정
|
||||
DATABASE = 'database/kdrug.db'
|
||||
|
||||
def get_inventory_summary():
|
||||
"""app.py의 get_inventory_summary 함수와 동일"""
|
||||
conn = sqlite3.connect(DATABASE)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
h.herb_item_id,
|
||||
h.insurance_code,
|
||||
h.herb_name,
|
||||
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
|
||||
COUNT(DISTINCT il.lot_id) as lot_count,
|
||||
COUNT(DISTINCT il.origin_country) as origin_count,
|
||||
AVG(il.unit_price_per_g) as avg_price,
|
||||
MIN(il.unit_price_per_g) as min_price,
|
||||
MAX(il.unit_price_per_g) as max_price,
|
||||
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value,
|
||||
GROUP_CONCAT(DISTINCT et.tag_name) as efficacy_tags
|
||||
FROM herb_items h
|
||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
|
||||
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
|
||||
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
|
||||
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
|
||||
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name
|
||||
HAVING total_quantity > 0
|
||||
ORDER BY h.herb_name
|
||||
""")
|
||||
|
||||
inventory = []
|
||||
for row in cursor.fetchall():
|
||||
item = dict(row)
|
||||
if item['efficacy_tags']:
|
||||
item['efficacy_tags'] = item['efficacy_tags'].split(',')
|
||||
else:
|
||||
item['efficacy_tags'] = []
|
||||
inventory.append(item)
|
||||
|
||||
# 전체 요약
|
||||
total_value = sum(item['total_value'] for item in inventory)
|
||||
total_items = len(inventory)
|
||||
|
||||
print("=" * 60)
|
||||
print("API 함수 직접 실행 결과")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(f"총 약재 수: {total_items}개")
|
||||
print(f"총 재고 자산: ₩{total_value:,.0f}")
|
||||
print()
|
||||
|
||||
# 상세 내역
|
||||
print("약재별 재고 가치 (상위 10개):")
|
||||
print("-" * 40)
|
||||
sorted_items = sorted(inventory, key=lambda x: x['total_value'], reverse=True)
|
||||
for i, item in enumerate(sorted_items[:10], 1):
|
||||
print(f"{i:2}. {item['herb_name']:15} ₩{item['total_value']:10,.0f}")
|
||||
|
||||
conn.close()
|
||||
return total_value
|
||||
|
||||
if __name__ == "__main__":
|
||||
total = get_inventory_summary()
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(f"최종 결과: ₩{total:,.0f}")
|
||||
|
||||
if total == 5875708:
|
||||
print("⚠️ API와 동일한 값이 나옴!")
|
||||
else:
|
||||
print(f"✅ 예상값: ₩1,529,434")
|
||||
print(f" 차이: ₩{total - 1529434:,.0f}")
|
||||
78
dev_scripts/test_frontend.py
Normal file
78
dev_scripts/test_frontend.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Playwright로 프론트엔드 확인
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
import json
|
||||
|
||||
def test_purchase_receipts():
|
||||
with sync_playwright() as p:
|
||||
# 브라우저 시작
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# 페이지 이동
|
||||
print("1. 페이지 로드 중...")
|
||||
page.goto("http://localhost:5001")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# 입고 관리 탭 클릭
|
||||
print("2. 입고 관리 탭으로 이동...")
|
||||
try:
|
||||
page.click('a[href="#purchase"]', timeout=5000)
|
||||
except:
|
||||
# 다른 방법으로 시도
|
||||
page.evaluate("document.querySelector('a[href=\"#purchase\"]').click()")
|
||||
time.sleep(1)
|
||||
|
||||
# API 호출 직접 확인
|
||||
print("3. API 직접 호출 확인...")
|
||||
api_response = page.evaluate("""
|
||||
async () => {
|
||||
const response = await fetch('/api/purchase-receipts');
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
""")
|
||||
|
||||
print("\n=== API 응답 데이터 ===")
|
||||
print(json.dumps(api_response, indent=2, ensure_ascii=False))
|
||||
|
||||
# 테이블 내용 확인
|
||||
print("\n4. 테이블 렌더링 확인...")
|
||||
table_rows = page.query_selector_all('#purchaseReceiptsList tr')
|
||||
|
||||
if len(table_rows) == 0:
|
||||
print(" 테이블에 행이 없습니다.")
|
||||
# "입고장이 없습니다." 메시지 확인
|
||||
empty_message = page.query_selector('#purchaseReceiptsList td')
|
||||
if empty_message:
|
||||
print(f" 메시지: {empty_message.text_content()}")
|
||||
else:
|
||||
print(f" 테이블 행 수: {len(table_rows)}")
|
||||
|
||||
# 첫 번째 행 상세 확인
|
||||
if len(table_rows) > 0:
|
||||
first_row = table_rows[0]
|
||||
cells = first_row.query_selector_all('td')
|
||||
|
||||
print("\n 첫 번째 행 내용:")
|
||||
headers = ['입고일', '공급업체', '품목 수', '총 금액', '총 수량', '파일명', '작업']
|
||||
for i, cell in enumerate(cells[:-1]): # 마지막 '작업' 열 제외
|
||||
print(f" {headers[i]}: {cell.text_content()}")
|
||||
|
||||
# JavaScript 콘솔 에러 확인
|
||||
page.on("console", lambda msg: print(f"콘솔: {msg.text}") if msg.type == "error" else None)
|
||||
|
||||
# 스크린샷 저장
|
||||
print("\n5. 스크린샷 저장...")
|
||||
page.screenshot(path="/root/kdrug/purchase_screenshot.png")
|
||||
print(" /root/kdrug/purchase_screenshot.png 저장 완료")
|
||||
|
||||
browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_purchase_receipts()
|
||||
200
dev_scripts/test_herb_dropdown_bug.py
Normal file
200
dev_scripts/test_herb_dropdown_bug.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
신규 약재 추가 드롭다운 버그 테스트
|
||||
십전대보탕 조제 시 새로운 약재 추가가 안되는 문제 확인
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:5001"
|
||||
|
||||
def test_herb_dropdown_api():
|
||||
"""약재 목록 API 테스트"""
|
||||
print("\n" + "="*80)
|
||||
print("1. 약재 목록 API 테스트")
|
||||
print("="*80)
|
||||
|
||||
# 1. 전체 약재 목록 조회
|
||||
response = requests.get(f"{BASE_URL}/api/herbs")
|
||||
print(f"상태 코드: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
herbs = response.json()
|
||||
print(f"총 약재 수: {len(herbs)}")
|
||||
|
||||
# 처음 5개만 출력
|
||||
print("\n처음 5개 약재:")
|
||||
for herb in herbs[:5]:
|
||||
print(f" - ID: {herb.get('herb_item_id')}, 이름: {herb.get('herb_name')}, 코드: {herb.get('insurance_code')}")
|
||||
else:
|
||||
print(f"오류: {response.text}")
|
||||
|
||||
return response.status_code == 200
|
||||
|
||||
def test_formula_ingredients():
|
||||
"""십전대보탕 처방 구성 테스트"""
|
||||
print("\n" + "="*80)
|
||||
print("2. 십전대보탕 처방 구성 조회")
|
||||
print("="*80)
|
||||
|
||||
# 십전대보탕 ID 찾기
|
||||
response = requests.get(f"{BASE_URL}/api/formulas")
|
||||
formulas = response.json()
|
||||
|
||||
sipjeon_id = None
|
||||
for formula in formulas:
|
||||
if '십전대보탕' in formula.get('formula_name', ''):
|
||||
sipjeon_id = formula['formula_id']
|
||||
print(f"십전대보탕 ID: {sipjeon_id}")
|
||||
break
|
||||
|
||||
if not sipjeon_id:
|
||||
print("십전대보탕을 찾을 수 없습니다")
|
||||
return False
|
||||
|
||||
# 처방 구성 조회
|
||||
response = requests.get(f"{BASE_URL}/api/formulas/{sipjeon_id}/ingredients")
|
||||
if response.status_code == 200:
|
||||
ingredients = response.json()
|
||||
print(f"\n십전대보탕 구성 약재 ({len(ingredients)}개):")
|
||||
|
||||
ingredient_codes = []
|
||||
for ing in ingredients:
|
||||
print(f" - {ing.get('herb_name')} ({ing.get('ingredient_code')}): {ing.get('grams_per_cheop')}g")
|
||||
ingredient_codes.append(ing.get('ingredient_code'))
|
||||
|
||||
return ingredient_codes
|
||||
else:
|
||||
print(f"오류: {response.text}")
|
||||
return []
|
||||
|
||||
def test_available_herbs_for_compound():
|
||||
"""조제 시 사용 가능한 약재 목록 테스트"""
|
||||
print("\n" + "="*80)
|
||||
print("3. 조제용 약재 목록 API 테스트")
|
||||
print("="*80)
|
||||
|
||||
# 재고가 있는 약재만 조회하는 API가 있는지 확인
|
||||
endpoints = [
|
||||
"/api/herbs",
|
||||
"/api/herbs/available",
|
||||
"/api/herbs-with-inventory"
|
||||
]
|
||||
|
||||
for endpoint in endpoints:
|
||||
print(f"\n테스트: {endpoint}")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}{endpoint}")
|
||||
if response.status_code == 200:
|
||||
herbs = response.json()
|
||||
print(f" ✓ 성공 - {len(herbs)}개 약재")
|
||||
|
||||
# 재고 정보 확인
|
||||
if herbs and len(herbs) > 0:
|
||||
sample = herbs[0]
|
||||
print(f" 샘플 데이터: {sample}")
|
||||
if 'quantity_onhand' in sample or 'total_quantity' in sample:
|
||||
print(" → 재고 정보 포함됨")
|
||||
else:
|
||||
print(f" ✗ 실패 - 상태코드: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 오류: {e}")
|
||||
|
||||
def check_frontend_code():
|
||||
"""프론트엔드 코드에서 약재 추가 부분 확인"""
|
||||
print("\n" + "="*80)
|
||||
print("4. 프론트엔드 코드 분석")
|
||||
print("="*80)
|
||||
|
||||
print("""
|
||||
app.js의 약재 추가 관련 주요 함수:
|
||||
1. loadHerbOptions() - 약재 드롭다운 로드
|
||||
2. addIngredientRow() - 약재 행 추가
|
||||
3. loadOriginOptions() - 원산지 옵션 로드
|
||||
|
||||
문제 가능성:
|
||||
- loadHerbOptions() 함수가 제대로 호출되지 않음
|
||||
- API 엔드포인트가 잘못됨
|
||||
- 드롭다운 element 선택자 오류
|
||||
- 이벤트 바인딩 문제
|
||||
""")
|
||||
|
||||
def test_with_playwright():
|
||||
"""Playwright로 실제 UI 테스트"""
|
||||
print("\n" + "="*80)
|
||||
print("5. Playwright UI 테스트 스크립트 생성")
|
||||
print("="*80)
|
||||
|
||||
test_code = '''from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
def test_herb_dropdown():
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
page = browser.new_page()
|
||||
|
||||
# 1. 조제 페이지로 이동
|
||||
page.goto("http://localhost:5001")
|
||||
page.click('a[href="#compound"]')
|
||||
time.sleep(1)
|
||||
|
||||
# 2. 십전대보탕 선택
|
||||
page.select_option('#compoundFormula', label='십전대보탕')
|
||||
time.sleep(1)
|
||||
|
||||
# 3. 새 약재 추가 버튼 클릭
|
||||
page.click('#addIngredientBtn')
|
||||
time.sleep(1)
|
||||
|
||||
# 4. 드롭다운 확인
|
||||
dropdown = page.locator('.herb-select').last
|
||||
options = dropdown.locator('option').all_text_contents()
|
||||
|
||||
print(f"드롭다운 옵션 수: {len(options)}")
|
||||
print(f"처음 5개: {options[:5]}")
|
||||
|
||||
browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_herb_dropdown()
|
||||
'''
|
||||
|
||||
print("Playwright 테스트 코드를 test_ui_dropdown.py 파일로 저장합니다.")
|
||||
|
||||
with open('/root/kdrug/test_ui_dropdown.py', 'w') as f:
|
||||
f.write(test_code)
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""메인 테스트 실행"""
|
||||
print("\n" + "="*80)
|
||||
print("신규 약재 추가 드롭다운 버그 테스트")
|
||||
print("="*80)
|
||||
|
||||
# 1. API 테스트
|
||||
if not test_herb_dropdown_api():
|
||||
print("\n❌ 약재 목록 API에 문제가 있습니다")
|
||||
return
|
||||
|
||||
# 2. 처방 구성 테스트
|
||||
ingredient_codes = test_formula_ingredients()
|
||||
|
||||
# 3. 조제용 약재 테스트
|
||||
test_available_herbs_for_compound()
|
||||
|
||||
# 4. 프론트엔드 코드 분석
|
||||
check_frontend_code()
|
||||
|
||||
# 5. Playwright 테스트 생성
|
||||
test_with_playwright()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("테스트 완료 - app.js 파일을 확인하여 문제를 찾아보겠습니다")
|
||||
print("="*80)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
192
dev_scripts/test_herb_info_page.py
Normal file
192
dev_scripts/test_herb_info_page.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""약재 정보 페이지 테스트 - 렌더링 문제 수정 후 검증"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:5001"
|
||||
|
||||
|
||||
def test_html_structure():
|
||||
"""HTML 구조 검증 - herb-info가 content-area 안에 있는지 확인"""
|
||||
print("1. HTML 구조 검증...")
|
||||
response = requests.get(f"{BASE_URL}/")
|
||||
if response.status_code != 200:
|
||||
print(f" FAIL: 페이지 로드 실패 {response.status_code}")
|
||||
return False
|
||||
|
||||
content = response.text
|
||||
|
||||
# herb-info가 col-md-10 content-area 안에 있는지 확인
|
||||
idx_content = content.find('col-md-10 content-area')
|
||||
idx_herb_info = content.find('id="herb-info"')
|
||||
|
||||
if idx_content < 0:
|
||||
print(" FAIL: col-md-10 content-area 찾을 수 없음")
|
||||
return False
|
||||
if idx_herb_info < 0:
|
||||
print(" FAIL: herb-info div 찾을 수 없음")
|
||||
return False
|
||||
|
||||
if idx_herb_info > idx_content:
|
||||
print(" PASS: herb-info가 content-area 안에 올바르게 위치함")
|
||||
else:
|
||||
print(" FAIL: herb-info가 content-area 밖에 있음!")
|
||||
return False
|
||||
|
||||
# efficacyFilter ID 중복 검사
|
||||
count_efficacy = content.count('id="efficacyFilter"')
|
||||
if count_efficacy > 1:
|
||||
print(f" FAIL: id=\"efficacyFilter\" 중복 {count_efficacy}개 발견!")
|
||||
return False
|
||||
else:
|
||||
print(f" PASS: id=\"efficacyFilter\" 중복 없음 (개수: {count_efficacy})")
|
||||
|
||||
# herbInfoEfficacyFilter 존재 확인
|
||||
if 'id="herbInfoEfficacyFilter"' in content:
|
||||
print(" PASS: herbInfoEfficacyFilter ID 정상 존재")
|
||||
else:
|
||||
print(" FAIL: herbInfoEfficacyFilter ID 없음!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_efficacy_tags():
|
||||
"""효능 태그 조회 API 검증"""
|
||||
print("\n2. 효능 태그 목록 조회...")
|
||||
response = requests.get(f"{BASE_URL}/api/efficacy-tags")
|
||||
if response.status_code != 200:
|
||||
print(f" FAIL: {response.status_code}")
|
||||
return False
|
||||
|
||||
tags = response.json()
|
||||
if not isinstance(tags, list):
|
||||
print(f" FAIL: 응답이 리스트가 아님 - {type(tags)}")
|
||||
return False
|
||||
|
||||
print(f" PASS: {len(tags)}개의 효능 태그 조회 성공")
|
||||
for tag in tags[:3]:
|
||||
print(f" - {tag.get('name', '')}: {tag.get('description', '')}")
|
||||
return True
|
||||
|
||||
|
||||
def test_herb_masters_api():
|
||||
"""약재 마스터 목록 + herb_id 포함 여부 검증"""
|
||||
print("\n3. 약재 마스터 목록 조회 (herb_id 포함 여부 확인)...")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs/masters")
|
||||
if response.status_code != 200:
|
||||
print(f" FAIL: {response.status_code}")
|
||||
return False
|
||||
|
||||
result = response.json()
|
||||
if not result.get('success'):
|
||||
print(f" FAIL: success=False")
|
||||
return False
|
||||
|
||||
herbs = result.get('data', [])
|
||||
print(f" PASS: {len(herbs)}개의 약재 조회 성공")
|
||||
|
||||
if not herbs:
|
||||
print(" FAIL: 약재 데이터 없음")
|
||||
return False
|
||||
|
||||
first = herbs[0]
|
||||
|
||||
# herb_id 확인
|
||||
if 'herb_id' in first:
|
||||
print(f" PASS: herb_id 필드 존재 (값: {first['herb_id']})")
|
||||
else:
|
||||
print(f" FAIL: herb_id 필드 누락! 키 목록: {list(first.keys())}")
|
||||
return False
|
||||
|
||||
# ingredient_code 확인
|
||||
if 'ingredient_code' in first:
|
||||
print(f" PASS: ingredient_code 필드 존재")
|
||||
else:
|
||||
print(" FAIL: ingredient_code 필드 누락!")
|
||||
return False
|
||||
|
||||
# efficacy_tags가 리스트인지 확인
|
||||
if isinstance(first.get('efficacy_tags'), list):
|
||||
print(f" PASS: efficacy_tags가 리스트 형식")
|
||||
else:
|
||||
print(f" FAIL: efficacy_tags 형식 오류: {first.get('efficacy_tags')}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_herb_extended_info():
|
||||
"""약재 확장 정보 조회 API 검증"""
|
||||
print("\n4. 약재 확장 정보 조회 (herb_id=1 기준)...")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs/1/extended")
|
||||
if response.status_code != 200:
|
||||
print(f" FAIL: {response.status_code}")
|
||||
return False
|
||||
|
||||
info = response.json()
|
||||
if not isinstance(info, dict):
|
||||
print(f" FAIL: 응답이 dict가 아님")
|
||||
return False
|
||||
|
||||
print(f" PASS: herb_id=1 확장 정보 조회 성공")
|
||||
print(f" - herb_name: {info.get('herb_name', '-')}")
|
||||
print(f" - name_korean: {info.get('name_korean', '-')}")
|
||||
print(f" - property: {info.get('property', '-')}")
|
||||
return True
|
||||
|
||||
|
||||
def test_herb_masters_has_extended_fields():
|
||||
"""약재 마스터 목록에 확장 정보(property, main_effects)가 포함되는지 검증"""
|
||||
print("\n5. 약재 마스터에 확장 정보 필드 포함 여부...")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs/masters")
|
||||
result = response.json()
|
||||
herbs = result.get('data', [])
|
||||
|
||||
required_fields = ['ingredient_code', 'herb_name', 'herb_id', 'has_stock',
|
||||
'efficacy_tags', 'property', 'main_effects']
|
||||
first = herbs[0] if herbs else {}
|
||||
missing = [f for f in required_fields if f not in first]
|
||||
|
||||
if missing:
|
||||
print(f" FAIL: 누락된 필드: {missing}")
|
||||
return False
|
||||
|
||||
print(f" PASS: 필수 필드 모두 존재: {required_fields}")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
print("=== 약재 정보 페이지 렌더링 수정 검증 테스트 ===")
|
||||
print(f"시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"서버: {BASE_URL}")
|
||||
print("-" * 50)
|
||||
|
||||
results = []
|
||||
results.append(("HTML 구조 검증", test_html_structure()))
|
||||
results.append(("효능 태그 API", test_efficacy_tags()))
|
||||
results.append(("약재 마스터 API (herb_id)", test_herb_masters_api()))
|
||||
results.append(("약재 확장 정보 API", test_herb_extended_info()))
|
||||
results.append(("약재 마스터 필드 완전성", test_herb_masters_has_extended_fields()))
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
success = sum(1 for _, r in results if r)
|
||||
total = len(results)
|
||||
print(f"테스트 결과: {success}/{total} 성공")
|
||||
for name, result in results:
|
||||
status = "PASS" if result else "FAIL"
|
||||
print(f" [{status}] {name}")
|
||||
|
||||
if success == total:
|
||||
print("\n모든 테스트 통과. 약재 정보 페이지가 정상적으로 동작해야 합니다.")
|
||||
else:
|
||||
print(f"\n{total - success}개 테스트 실패. 추가 수정이 필요합니다.")
|
||||
|
||||
return success == total
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
162
dev_scripts/test_herb_info_ui.py
Normal file
162
dev_scripts/test_herb_info_ui.py
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Playwright를 사용한 약재 정보 페이지 UI 테스트"""
|
||||
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
import time
|
||||
|
||||
async def test_herb_info_page():
|
||||
async with async_playwright() as p:
|
||||
# 브라우저 시작
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page()
|
||||
|
||||
# 콘솔 메시지 캡처
|
||||
console_messages = []
|
||||
page.on("console", lambda msg: console_messages.append(f"{msg.type}: {msg.text}"))
|
||||
|
||||
# 페이지 에러 캡처
|
||||
page_errors = []
|
||||
page.on("pageerror", lambda err: page_errors.append(str(err)))
|
||||
|
||||
try:
|
||||
print("=== Playwright 약재 정보 페이지 테스트 ===\n")
|
||||
|
||||
# 1. 메인 페이지 접속
|
||||
print("1. 메인 페이지 접속...")
|
||||
await page.goto("http://localhost:5001")
|
||||
await page.wait_for_load_state("networkidle")
|
||||
|
||||
# 2. 약재 정보 메뉴 클릭
|
||||
print("2. 약재 정보 메뉴 클릭...")
|
||||
herb_info_link = page.locator('a[data-page="herb-info"]')
|
||||
is_visible = await herb_info_link.is_visible()
|
||||
print(f" - 약재 정보 메뉴 표시 여부: {is_visible}")
|
||||
|
||||
if is_visible:
|
||||
await herb_info_link.click()
|
||||
await page.wait_for_timeout(2000) # 2초 대기
|
||||
|
||||
# 3. herb-info 페이지 표시 확인
|
||||
print("\n3. 약재 정보 페이지 요소 확인...")
|
||||
herb_info_div = page.locator('#herb-info')
|
||||
is_herb_info_visible = await herb_info_div.is_visible()
|
||||
print(f" - herb-info div 표시: {is_herb_info_visible}")
|
||||
|
||||
if is_herb_info_visible:
|
||||
# 검색 섹션 확인
|
||||
search_section = page.locator('#herb-search-section')
|
||||
is_search_visible = await search_section.is_visible()
|
||||
print(f" - 검색 섹션 표시: {is_search_visible}")
|
||||
|
||||
# 약재 카드 그리드 확인
|
||||
herb_grid = page.locator('#herbInfoGrid')
|
||||
is_grid_visible = await herb_grid.is_visible()
|
||||
print(f" - 약재 그리드 표시: {is_grid_visible}")
|
||||
|
||||
# 약재 카드 개수 확인
|
||||
await page.wait_for_selector('.herb-info-card', timeout=5000)
|
||||
herb_cards = await page.locator('.herb-info-card').count()
|
||||
print(f" - 표시된 약재 카드 수: {herb_cards}개")
|
||||
|
||||
if herb_cards > 0:
|
||||
# 첫 번째 약재 카드 정보 확인
|
||||
first_card = page.locator('.herb-info-card').first
|
||||
card_title = await first_card.locator('.card-title').text_content()
|
||||
print(f" - 첫 번째 약재: {card_title}")
|
||||
|
||||
# 카드 클릭으로 상세 보기 (카드 전체가 클릭 가능)
|
||||
print("\n4. 약재 상세 정보 확인...")
|
||||
# herb-info-card는 클릭 가능한 카드이므로 직접 클릭
|
||||
if True:
|
||||
await first_card.click()
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
# 상세 모달 확인
|
||||
modal = page.locator('#herbDetailModal')
|
||||
is_modal_visible = await modal.is_visible()
|
||||
print(f" - 상세 모달 표시: {is_modal_visible}")
|
||||
|
||||
if is_modal_visible:
|
||||
modal_title = await modal.locator('.modal-title').text_content()
|
||||
print(f" - 모달 제목: {modal_title}")
|
||||
|
||||
# 모달 닫기
|
||||
close_btn = modal.locator('button.btn-close')
|
||||
if await close_btn.is_visible():
|
||||
await close_btn.click()
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
# 5. 검색 기능 테스트
|
||||
print("\n5. 검색 기능 테스트...")
|
||||
search_input = page.locator('#herbSearchInput')
|
||||
if await search_input.is_visible():
|
||||
await search_input.fill("감초")
|
||||
await page.locator('#herbSearchBtn').click()
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
search_result_count = await page.locator('.herb-info-card').count()
|
||||
print(f" - '감초' 검색 결과: {search_result_count}개")
|
||||
|
||||
# 6. 효능별 보기 테스트
|
||||
print("\n6. 효능별 보기 전환...")
|
||||
efficacy_btn = page.locator('button[data-view="efficacy"]')
|
||||
if await efficacy_btn.is_visible():
|
||||
await efficacy_btn.click()
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
efficacy_section = page.locator('#herb-efficacy-section')
|
||||
is_efficacy_visible = await efficacy_section.is_visible()
|
||||
print(f" - 효능별 섹션 표시: {is_efficacy_visible}")
|
||||
|
||||
if is_efficacy_visible:
|
||||
tag_buttons = await page.locator('.efficacy-tag-btn').count()
|
||||
print(f" - 효능 태그 버튼 수: {tag_buttons}개")
|
||||
else:
|
||||
print(" ⚠️ herb-info div가 표시되지 않음!")
|
||||
|
||||
# 디버깅: 현재 활성 페이지 확인
|
||||
active_pages = await page.locator('.main-content.active').count()
|
||||
print(f" - 활성 페이지 수: {active_pages}")
|
||||
|
||||
# 디버깅: herb-info의 display 스타일 확인
|
||||
herb_info_style = await herb_info_div.get_attribute('style')
|
||||
print(f" - herb-info style: {herb_info_style}")
|
||||
|
||||
# 디버깅: herb-info의 클래스 확인
|
||||
herb_info_classes = await herb_info_div.get_attribute('class')
|
||||
print(f" - herb-info classes: {herb_info_classes}")
|
||||
|
||||
# 7. 콘솔 에러 확인
|
||||
print("\n7. 콘솔 메시지 확인...")
|
||||
if console_messages:
|
||||
print(" 콘솔 메시지:")
|
||||
for msg in console_messages[:10]: # 처음 10개만 출력
|
||||
print(f" - {msg}")
|
||||
else:
|
||||
print(" ✓ 콘솔 메시지 없음")
|
||||
|
||||
if page_errors:
|
||||
print(" ⚠️ 페이지 에러:")
|
||||
for err in page_errors:
|
||||
print(f" - {err}")
|
||||
else:
|
||||
print(" ✓ 페이지 에러 없음")
|
||||
|
||||
# 스크린샷 저장
|
||||
await page.screenshot(path="/root/kdrug/herb_info_page.png")
|
||||
print("\n스크린샷 저장: /root/kdrug/herb_info_page.png")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 테스트 실패: {e}")
|
||||
# 에러 시 스크린샷
|
||||
await page.screenshot(path="/root/kdrug/herb_info_error.png")
|
||||
print("에러 스크린샷 저장: /root/kdrug/herb_info_error.png")
|
||||
|
||||
finally:
|
||||
await browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Playwright 테스트 시작...\n")
|
||||
asyncio.run(test_herb_info_page())
|
||||
print("\n테스트 완료!")
|
||||
54
dev_scripts/test_improved_import.py
Normal file
54
dev_scripts/test_improved_import.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
개선된 Excel 입고 처리 테스트
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.append('/root/kdrug')
|
||||
|
||||
from excel_processor import ExcelProcessor
|
||||
import pandas as pd
|
||||
|
||||
def test_excel_processing():
|
||||
"""Excel 처리 테스트"""
|
||||
processor = ExcelProcessor()
|
||||
|
||||
# 한의정보 샘플 파일 테스트
|
||||
print("=== 한의정보 샘플 파일 처리 테스트 ===\n")
|
||||
|
||||
if processor.read_excel('sample/한의정보.xlsx'):
|
||||
print(f"✓ 파일 읽기 성공")
|
||||
print(f"✓ 형식 감지: {processor.format_type}")
|
||||
|
||||
# 처리
|
||||
df = processor.process()
|
||||
print(f"✓ 데이터 처리 완료: {len(df)}행")
|
||||
|
||||
# 보험코드 확인
|
||||
if 'insurance_code' in df.columns:
|
||||
print("\n보험코드 샘플 (처리 후):")
|
||||
for idx, code in enumerate(df['insurance_code'].head(5)):
|
||||
herb_name = df.iloc[idx]['herb_name']
|
||||
print(f" {herb_name}: {code} (길이: {len(str(code))})")
|
||||
|
||||
print("\n=== 한의사랑 샘플 파일 처리 테스트 ===\n")
|
||||
|
||||
processor2 = ExcelProcessor()
|
||||
if processor2.read_excel('sample/한의사랑.xlsx'):
|
||||
print(f"✓ 파일 읽기 성공")
|
||||
print(f"✓ 형식 감지: {processor2.format_type}")
|
||||
|
||||
# 처리
|
||||
df2 = processor2.process()
|
||||
print(f"✓ 데이터 처리 완료: {len(df2)}행")
|
||||
|
||||
# 보험코드 확인
|
||||
if 'insurance_code' in df2.columns:
|
||||
print("\n보험코드 샘플 (처리 후):")
|
||||
for idx, code in enumerate(df2['insurance_code'].head(5)):
|
||||
herb_name = df2.iloc[idx]['herb_name']
|
||||
print(f" {herb_name}: {code} (길이: {len(str(code))})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_excel_processing()
|
||||
134
dev_scripts/test_js_debug.py
Normal file
134
dev_scripts/test_js_debug.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""JavaScript 디버깅을 위한 Playwright 테스트"""
|
||||
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
async def debug_herb_info():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page()
|
||||
|
||||
# 콘솔 메시지 캡처
|
||||
console_messages = []
|
||||
page.on("console", lambda msg: console_messages.append({
|
||||
"type": msg.type,
|
||||
"text": msg.text,
|
||||
"args": msg.args
|
||||
}))
|
||||
|
||||
# 네트워크 요청 캡처
|
||||
network_requests = []
|
||||
page.on("request", lambda req: network_requests.append({
|
||||
"url": req.url,
|
||||
"method": req.method
|
||||
}))
|
||||
|
||||
# 네트워크 응답 캡처
|
||||
network_responses = []
|
||||
async def log_response(response):
|
||||
if "/api/" in response.url:
|
||||
try:
|
||||
body = await response.text()
|
||||
network_responses.append({
|
||||
"url": response.url,
|
||||
"status": response.status,
|
||||
"body": body[:200] if body else None
|
||||
})
|
||||
except:
|
||||
pass
|
||||
page.on("response", log_response)
|
||||
|
||||
try:
|
||||
# 페이지 접속
|
||||
print("페이지 접속 중...")
|
||||
await page.goto("http://localhost:5001")
|
||||
await page.wait_for_load_state("networkidle")
|
||||
|
||||
# JavaScript 실행하여 직접 함수 호출
|
||||
print("\n직접 JavaScript 함수 테스트...")
|
||||
|
||||
# loadHerbInfo 함수 존재 확인
|
||||
has_function = await page.evaluate("typeof loadHerbInfo === 'function'")
|
||||
print(f"1. loadHerbInfo 함수 존재: {has_function}")
|
||||
|
||||
# loadAllHerbsInfo 함수 존재 확인
|
||||
has_all_herbs = await page.evaluate("typeof loadAllHerbsInfo === 'function'")
|
||||
print(f"2. loadAllHerbsInfo 함수 존재: {has_all_herbs}")
|
||||
|
||||
# displayHerbCards 함수 존재 확인
|
||||
has_display = await page.evaluate("typeof displayHerbCards === 'function'")
|
||||
print(f"3. displayHerbCards 함수 존재: {has_display}")
|
||||
|
||||
# 약재 정보 페이지로 이동
|
||||
await page.click('a[data-page="herb-info"]')
|
||||
await page.wait_for_timeout(2000)
|
||||
|
||||
# herbInfoGrid 요소 확인
|
||||
grid_exists = await page.evaluate("document.getElementById('herbInfoGrid') !== null")
|
||||
print(f"4. herbInfoGrid 요소 존재: {grid_exists}")
|
||||
|
||||
# herbInfoGrid 내용 확인
|
||||
grid_html = await page.evaluate("document.getElementById('herbInfoGrid')?.innerHTML || 'EMPTY'")
|
||||
print(f"5. herbInfoGrid 내용 길이: {len(grid_html)} 문자")
|
||||
if grid_html and grid_html != 'EMPTY':
|
||||
print(f" 처음 100자: {grid_html[:100]}...")
|
||||
|
||||
# API 호출 직접 테스트
|
||||
print("\n\nAPI 응답 직접 테스트...")
|
||||
api_response = await page.evaluate("""
|
||||
fetch('/api/herbs/masters')
|
||||
.then(res => res.json())
|
||||
.then(data => ({
|
||||
success: data.success,
|
||||
dataLength: data.data ? data.data.length : 0,
|
||||
firstItem: data.data ? data.data[0] : null
|
||||
}))
|
||||
.catch(err => ({ error: err.toString() }))
|
||||
""")
|
||||
print(f"API 응답: {api_response}")
|
||||
|
||||
# displayHerbCards 직접 호출 테스트
|
||||
if api_response.get('dataLength', 0) > 0:
|
||||
print("\n\ndisplayHerbCards 직접 호출...")
|
||||
await page.evaluate("""
|
||||
fetch('/api/herbs/masters')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (typeof displayHerbCards === 'function') {
|
||||
displayHerbCards(data.data);
|
||||
} else {
|
||||
console.error('displayHerbCards 함수가 없습니다');
|
||||
}
|
||||
})
|
||||
""")
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
# 다시 확인
|
||||
grid_html_after = await page.evaluate("document.getElementById('herbInfoGrid')?.innerHTML || 'EMPTY'")
|
||||
print(f"displayHerbCards 호출 후 내용 길이: {len(grid_html_after)} 문자")
|
||||
|
||||
card_count = await page.evaluate("document.querySelectorAll('.herb-card').length")
|
||||
print(f"herb-card 요소 개수: {card_count}")
|
||||
|
||||
# 콘솔 메시지 출력
|
||||
print("\n\n=== 콘솔 메시지 ===")
|
||||
for msg in console_messages:
|
||||
if 'error' in msg['type'].lower():
|
||||
print(f"❌ {msg['type']}: {msg['text']}")
|
||||
else:
|
||||
print(f"📝 {msg['type']}: {msg['text']}")
|
||||
|
||||
# API 응답 상태 확인
|
||||
print("\n\n=== API 응답 ===")
|
||||
for resp in network_responses:
|
||||
if '/api/herbs/masters' in resp['url']:
|
||||
print(f"URL: {resp['url']}")
|
||||
print(f"상태: {resp['status']}")
|
||||
print(f"응답: {resp['body'][:100] if resp['body'] else 'No body'}")
|
||||
|
||||
finally:
|
||||
await browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(debug_herb_info())
|
||||
108
dev_scripts/test_lot_validation.py
Normal file
108
dev_scripts/test_lot_validation.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
로트 배분 검증 테스트 - 재고 부족 및 잘못된 배분 테스트
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:5001"
|
||||
|
||||
def test_insufficient_stock():
|
||||
print("=== 로트 배분 검증 테스트 ===\n")
|
||||
|
||||
# 1. 배분 합계가 맞지 않는 경우
|
||||
print("1. 배분 합계가 필요량과 맞지 않는 경우")
|
||||
|
||||
compound_data = {
|
||||
"patient_id": 1,
|
||||
"formula_id": None,
|
||||
"je_count": 1,
|
||||
"cheop_total": 1,
|
||||
"pouch_total": 1,
|
||||
"ingredients": [
|
||||
{
|
||||
"herb_item_id": 63,
|
||||
"grams_per_cheop": 100.0,
|
||||
"total_grams": 100.0,
|
||||
"origin": "manual",
|
||||
"lot_assignments": [
|
||||
{"lot_id": 208, "quantity": 50.0}, # 50g
|
||||
{"lot_id": 219, "quantity": 30.0} # 30g = 총 80g (100g 필요)
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/api/compounds", json=compound_data, headers={"Content-Type": "application/json"})
|
||||
|
||||
if response.status_code != 200:
|
||||
result = response.json()
|
||||
print(f" ✅ 예상된 오류 발생: {result.get('error')}")
|
||||
else:
|
||||
print(f" ❌ 오류가 발생해야 하는데 성공함")
|
||||
|
||||
# 2. 로트 재고가 부족한 경우
|
||||
print("\n2. 로트 재고가 부족한 경우")
|
||||
|
||||
compound_data = {
|
||||
"patient_id": 1,
|
||||
"formula_id": None,
|
||||
"je_count": 1,
|
||||
"cheop_total": 1,
|
||||
"pouch_total": 1,
|
||||
"ingredients": [
|
||||
{
|
||||
"herb_item_id": 63,
|
||||
"grams_per_cheop": 5000.0, # 5000g 요청
|
||||
"total_grams": 5000.0,
|
||||
"origin": "manual",
|
||||
"lot_assignments": [
|
||||
{"lot_id": 208, "quantity": 5000.0} # 로트 208에 5000g 요청 (실제로는 4784g만 있음)
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/api/compounds", json=compound_data, headers={"Content-Type": "application/json"})
|
||||
|
||||
if response.status_code != 200:
|
||||
result = response.json()
|
||||
print(f" ✅ 예상된 오류 발생: {result.get('error')}")
|
||||
else:
|
||||
print(f" ❌ 오류가 발생해야 하는데 성공함")
|
||||
|
||||
# 3. 존재하지 않는 로트
|
||||
print("\n3. 존재하지 않는 로트 ID 사용")
|
||||
|
||||
compound_data = {
|
||||
"patient_id": 1,
|
||||
"formula_id": None,
|
||||
"je_count": 1,
|
||||
"cheop_total": 1,
|
||||
"pouch_total": 1,
|
||||
"ingredients": [
|
||||
{
|
||||
"herb_item_id": 63,
|
||||
"grams_per_cheop": 10.0,
|
||||
"total_grams": 10.0,
|
||||
"origin": "manual",
|
||||
"lot_assignments": [
|
||||
{"lot_id": 99999, "quantity": 10.0} # 존재하지 않는 로트
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = requests.post(f"{BASE_URL}/api/compounds", json=compound_data, headers={"Content-Type": "application/json"})
|
||||
|
||||
if response.status_code != 200:
|
||||
result = response.json()
|
||||
print(f" ✅ 예상된 오류 발생: {result.get('error')}")
|
||||
else:
|
||||
print(f" ❌ 오류가 발생해야 하는데 성공함")
|
||||
|
||||
print("\n✅ 모든 검증 테스트 완료 - 잘못된 요청을 올바르게 거부함")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_insufficient_stock()
|
||||
112
dev_scripts/test_multi_lot_compound.py
Normal file
112
dev_scripts/test_multi_lot_compound.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
복합 로트 사용 E2E 테스트
|
||||
- 당귀 2개 로트를 수동 배분하여 커스텀 조제 테스트
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:5001"
|
||||
|
||||
def test_multi_lot_compound():
|
||||
print("=== 복합 로트 사용 E2E 테스트 시작 ===\n")
|
||||
|
||||
# 1. 당귀 재고 현황 확인
|
||||
print("1. 당귀(휴먼일당귀) 재고 현황 확인")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs/63/available-lots")
|
||||
if response.status_code == 200:
|
||||
data = response.json()['data']
|
||||
print(f" - 약재명: {data['herb_name']}")
|
||||
print(f" - 총 재고: {data['total_quantity']}g")
|
||||
|
||||
for origin in data['origins']:
|
||||
print(f"\n [{origin['origin_country']}] 로트 {origin['lot_count']}개, 총 {origin['total_quantity']}g")
|
||||
for lot in origin['lots']:
|
||||
print(f" - 로트 #{lot['lot_id']}: {lot['quantity_onhand']}g @ {lot['unit_price_per_g']}원/g")
|
||||
else:
|
||||
print(f" ❌ 오류: {response.status_code}")
|
||||
return
|
||||
|
||||
# 2. 커스텀 조제 생성 (당귀 100g 필요)
|
||||
print("\n2. 커스텀 조제 생성 - 당귀 100g를 2개 로트로 수동 배분")
|
||||
|
||||
compound_data = {
|
||||
"patient_id": 1, # 테스트 환자
|
||||
"formula_id": None, # 커스텀 조제
|
||||
"je_count": 1,
|
||||
"cheop_total": 1,
|
||||
"pouch_total": 1,
|
||||
"ingredients": [
|
||||
{
|
||||
"herb_item_id": 63, # 휴먼일당귀
|
||||
"grams_per_cheop": 100.0,
|
||||
"total_grams": 100.0, # total_grams 추가
|
||||
"origin": "manual", # 수동 배분
|
||||
"lot_assignments": [
|
||||
{"lot_id": 208, "quantity": 60.0}, # 중국산 60g
|
||||
{"lot_id": 219, "quantity": 40.0} # 한국산 40g
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
print(" - 로트 배분:")
|
||||
print(" * 로트 #208 (중국산): 60g")
|
||||
print(" * 로트 #219 (한국산): 40g")
|
||||
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/compounds",
|
||||
json=compound_data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get('success'):
|
||||
compound_id = result.get('compound_id')
|
||||
total_cost = result.get('total_cost')
|
||||
print(f"\n ✅ 조제 성공!")
|
||||
print(f" - 조제 ID: {compound_id}")
|
||||
print(f" - 총 원가: {total_cost}원")
|
||||
|
||||
# 3. 조제 상세 확인
|
||||
print("\n3. 조제 상세 정보 확인")
|
||||
response = requests.get(f"{BASE_URL}/api/compounds/{compound_id}")
|
||||
if response.status_code == 200:
|
||||
detail = response.json()['data']
|
||||
|
||||
print(" - 소비 내역:")
|
||||
for con in detail.get('consumptions', []):
|
||||
print(f" * 로트 #{con['lot_id']}: {con['quantity_used']}g @ {con['unit_cost_per_g']}원/g = {con['cost_amount']}원")
|
||||
|
||||
# 4. 재고 변동 확인
|
||||
print("\n4. 재고 변동 확인")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs/63/available-lots")
|
||||
if response.status_code == 200:
|
||||
after_data = response.json()['data']
|
||||
print(" - 조제 후 재고:")
|
||||
for origin in after_data['origins']:
|
||||
for lot in origin['lots']:
|
||||
if lot['lot_id'] in [208, 219]:
|
||||
print(f" * 로트 #{lot['lot_id']} ({origin['origin_country']}): {lot['quantity_onhand']}g")
|
||||
|
||||
print("\n✅ 복합 로트 사용 테스트 성공!")
|
||||
print(" - 2개의 로트를 수동으로 배분하여 조제")
|
||||
print(" - 각 로트별 재고가 정확히 차감됨")
|
||||
print(" - 소비 내역이 올바르게 기록됨")
|
||||
|
||||
else:
|
||||
print(f" ❌ 상세 조회 실패: {response.status_code}")
|
||||
else:
|
||||
print(f" ❌ 조제 실패: {result.get('error')}")
|
||||
else:
|
||||
print(f" ❌ API 호출 실패: {response.status_code}")
|
||||
print(f" 응답: {response.text}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_multi_lot_compound()
|
||||
except Exception as e:
|
||||
print(f"\n❌ 테스트 중 오류 발생: {e}")
|
||||
60
dev_scripts/test_simple.py
Normal file
60
dev_scripts/test_simple.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
간단한 프론트엔드 확인
|
||||
"""
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
import time
|
||||
|
||||
def test_purchase_display():
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
print("페이지 로드 중...")
|
||||
page.goto("http://localhost:5001", wait_until="networkidle")
|
||||
|
||||
# 입고 관리 화면으로 직접 이동
|
||||
print("\n입고 관리 화면 확인...")
|
||||
page.goto("http://localhost:5001/#purchase", wait_until="networkidle")
|
||||
time.sleep(2) # JavaScript 렌더링 대기
|
||||
|
||||
# API 데이터와 실제 렌더링 비교
|
||||
print("\n=== API 데이터 vs 화면 렌더링 확인 ===")
|
||||
|
||||
# API 응답 확인
|
||||
api_data = page.evaluate("""
|
||||
fetch('/api/purchase-receipts')
|
||||
.then(response => response.json())
|
||||
.then(data => data)
|
||||
""")
|
||||
time.sleep(1)
|
||||
|
||||
# 테이블 확인
|
||||
table_html = page.evaluate("document.querySelector('#purchaseReceiptsList').innerHTML")
|
||||
|
||||
print(f"\nAPI 응답 총금액: {api_data.get('data', [{}])[0].get('total_amount', 0)}")
|
||||
|
||||
# 화면에 표시된 총금액 찾기
|
||||
try:
|
||||
total_amount_cell = page.query_selector('.fw-bold.text-primary')
|
||||
if total_amount_cell:
|
||||
print(f"화면 표시 총금액: {total_amount_cell.text_content()}")
|
||||
else:
|
||||
print("총금액 셀을 찾을 수 없습니다.")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 테이블 전체 내용
|
||||
print("\n테이블 HTML (처음 200자):")
|
||||
print(table_html[:200] if table_html else "테이블이 비어있음")
|
||||
|
||||
# 스크린샷
|
||||
page.screenshot(path="/root/kdrug/purchase_test.png")
|
||||
print("\n스크린샷 저장: /root/kdrug/purchase_test.png")
|
||||
|
||||
browser.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_purchase_display()
|
||||
97
dev_scripts/test_upload_api.py
Normal file
97
dev_scripts/test_upload_api.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
API를 통한 Excel 입고 처리 테스트
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
# API 베이스 URL
|
||||
BASE_URL = "http://localhost:5001"
|
||||
|
||||
def test_upload_excel():
|
||||
"""Excel 업로드 테스트"""
|
||||
|
||||
# 1. 도매상 목록 확인
|
||||
print("=== 도매상 목록 확인 ===")
|
||||
response = requests.get(f"{BASE_URL}/api/suppliers")
|
||||
suppliers = response.json()
|
||||
|
||||
if suppliers['success'] and suppliers['data']:
|
||||
print(f"✓ 도매상 {len(suppliers['data'])}개 조회")
|
||||
supplier_id = suppliers['data'][0]['supplier_id']
|
||||
supplier_name = suppliers['data'][0]['name']
|
||||
print(f"✓ 선택된 도매상: {supplier_name} (ID: {supplier_id})")
|
||||
else:
|
||||
print("도매상이 없습니다. 새로 생성합니다.")
|
||||
# 도매상 생성
|
||||
supplier_data = {
|
||||
'name': '한의정보',
|
||||
'business_no': '123-45-67890',
|
||||
'contact_person': '담당자',
|
||||
'phone': '02-1234-5678'
|
||||
}
|
||||
response = requests.post(f"{BASE_URL}/api/suppliers", json=supplier_data)
|
||||
result = response.json()
|
||||
if result['success']:
|
||||
supplier_id = result['supplier_id']
|
||||
print(f"✓ 도매상 생성 완료 (ID: {supplier_id})")
|
||||
else:
|
||||
print(f"✗ 도매상 생성 실패: {result.get('error')}")
|
||||
return
|
||||
|
||||
# 2. Excel 파일 업로드
|
||||
print("\n=== Excel 파일 업로드 ===")
|
||||
|
||||
# 파일 열기
|
||||
file_path = 'sample/한의정보.xlsx'
|
||||
with open(file_path, 'rb') as f:
|
||||
files = {'file': ('한의정보.xlsx', f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
|
||||
data = {'supplier_id': supplier_id}
|
||||
|
||||
# 업로드
|
||||
response = requests.post(f"{BASE_URL}/api/upload/purchase", files=files, data=data)
|
||||
|
||||
# 결과 확인
|
||||
result = response.json()
|
||||
if result['success']:
|
||||
print(f"✓ 업로드 성공!")
|
||||
print(f" - 형식: {result['summary']['format']}")
|
||||
print(f" - 처리된 행: {result['summary']['processed_rows']}")
|
||||
if 'processed_items' in result['summary']:
|
||||
print(f" - 처리된 품목: {result['summary']['processed_items']}")
|
||||
if 'total_amount' in result['summary']:
|
||||
total = result['summary']['total_amount']
|
||||
if isinstance(total, (int, float)):
|
||||
print(f" - 총액: {total:,.0f}원")
|
||||
else:
|
||||
print(f" - 총액: {total}원")
|
||||
else:
|
||||
print(f"✗ 업로드 실패: {result.get('error')}")
|
||||
|
||||
# 3. 입고된 herb_items 확인
|
||||
print("\n=== 입고된 herb_items 확인 ===")
|
||||
response = requests.get(f"{BASE_URL}/api/herbs")
|
||||
herbs = response.json()
|
||||
|
||||
if herbs['success']:
|
||||
print(f"✓ 총 {len(herbs['data'])}개 herb_items")
|
||||
# 샘플 출력
|
||||
for herb in herbs['data'][:5]:
|
||||
print(f" - {herb['herb_name']}: 보험코드={herb.get('insurance_code', 'N/A')}, 재고={herb.get('stock_quantity', 0):,.0f}g")
|
||||
|
||||
# 4. 재고 현황 확인
|
||||
print("\n=== 재고 현황 확인 ===")
|
||||
response = requests.get(f"{BASE_URL}/api/inventory/summary")
|
||||
inventory = response.json()
|
||||
|
||||
if inventory['success']:
|
||||
summary = inventory['data']
|
||||
print(f"✓ 재고 요약:")
|
||||
print(f" - 총 품목: {summary['total_items']}개")
|
||||
print(f" - 재고 있는 품목: {summary['items_with_stock']}개")
|
||||
print(f" - 총 재고 가치: {summary['total_value']:,.0f}원")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_upload_excel()
|
||||
Reference in New Issue
Block a user