kdrug-inventory-system/dev_scripts/analyze_inventory_full.py
시골약사 ad9ac396e2 chore: 개발 파일 정리 및 구조화
- 개발/테스트 스크립트를 dev_scripts/ 폴더로 이동
- 스크린샷을 screenshots/ 폴더로 이동
- 백업 파일 보존 (.backup)
- 처방 관련 추가 스크립트 포함

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 04:44:48 +00:00

315 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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