- 개발/테스트 스크립트를 dev_scripts/ 폴더로 이동 - 스크린샷을 screenshots/ 폴더로 이동 - 백업 파일 보존 (.backup) - 처방 관련 추가 스크립트 포함 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
315 lines
12 KiB
Python
315 lines
12 KiB
Python
#!/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() |