#!/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()