- inventory_lots 현황 확인 - supplier_product_catalog 샘플 데이터 추가 - 가격 기반 매칭 테스트 - variant 속성 파싱 로직 검증 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
428 lines
14 KiB
Python
428 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
기존 inventory_lots에 variant 정보 적용
|
|
"""
|
|
|
|
import sqlite3
|
|
import json
|
|
from datetime import datetime
|
|
|
|
def get_connection():
|
|
"""데이터베이스 연결"""
|
|
return sqlite3.connect('database/kdrug.db')
|
|
|
|
def check_current_lots():
|
|
"""현재 inventory_lots 상태 확인"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
print("\n" + "="*80)
|
|
print("현재 입고된 Inventory Lots 현황")
|
|
print("="*80)
|
|
|
|
cursor.execute("""
|
|
SELECT
|
|
l.lot_id,
|
|
l.herb_item_id,
|
|
h.herb_name,
|
|
h.insurance_code,
|
|
l.quantity_onhand,
|
|
l.unit_price_per_g,
|
|
l.origin_country,
|
|
l.received_date,
|
|
s.name as supplier_name,
|
|
l.display_name
|
|
FROM inventory_lots l
|
|
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
|
|
JOIN purchase_receipt_lines prl ON l.receipt_line_id = prl.line_id
|
|
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
|
|
JOIN suppliers s ON pr.supplier_id = s.supplier_id
|
|
ORDER BY l.received_date DESC, h.herb_name
|
|
""")
|
|
|
|
lots = cursor.fetchall()
|
|
|
|
print(f"\n총 {len(lots)}개의 로트가 있습니다.\n")
|
|
|
|
for lot in lots[:10]: # 처음 10개만 출력
|
|
print(f"Lot #{lot[0]}: {lot[2]} (보험코드: {lot[3]})")
|
|
print(f" - 재고: {lot[4]:.1f}g, 단가: {lot[5]:.1f}원/g")
|
|
print(f" - 원산지: {lot[6]}, 공급처: {lot[8]}")
|
|
print(f" - display_name: {lot[9] if lot[9] else '없음'}")
|
|
print()
|
|
|
|
conn.close()
|
|
return lots
|
|
|
|
def insert_sample_catalog_data():
|
|
"""공급처 카탈로그 샘플 데이터 추가"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
print("\n" + "="*80)
|
|
print("공급처 카탈로그 샘플 데이터 추가")
|
|
print("="*80)
|
|
|
|
# 휴먼허브 supplier_id 조회
|
|
cursor.execute("SELECT supplier_id FROM suppliers WHERE name = '(주)휴먼허브'")
|
|
supplier_id = cursor.fetchone()
|
|
|
|
if not supplier_id:
|
|
print("휴먼허브 공급처를 찾을 수 없습니다.")
|
|
conn.close()
|
|
return
|
|
|
|
supplier_id = supplier_id[0]
|
|
|
|
# 휴먼허브 실제 가격 기반 데이터 (원산지별로 구분)
|
|
catalog_items = [
|
|
('신흥숙지황(9증)', 20.0, '1kg'),
|
|
('휴먼갈근[한국산]', 16.8, '1kg'),
|
|
('휴먼감초', 22.1, '1kg'),
|
|
('휴먼건강[페루산]', 12.4, '1kg'),
|
|
('휴먼건강.土[한국산]', 51.4, '1kg'),
|
|
('휴먼계지', 5.8, '1kg'),
|
|
('휴먼구기자(영하)', 17.9, '1kg'),
|
|
('휴먼길경.片', 10.6, '1kg'),
|
|
('휴먼대추(한국산)', 20.0, '1kg'),
|
|
('휴먼마황', 9.6, '1kg'),
|
|
('휴먼반하(생강백반제)', 33.7, '1kg'),
|
|
('휴먼백출', 11.8, '1kg'),
|
|
('휴먼복령', 11.5, '1kg'),
|
|
('휴먼석고', 4.7, '1kg'),
|
|
('휴먼세신.中', 129.0, '1kg'),
|
|
('휴먼오미자<토매지>', 17.5, '1kg'),
|
|
('휴먼용안육', 20.7, '1kg'),
|
|
('휴먼육계', 14.6, '1kg'),
|
|
('휴먼일당귀', 12.9, '1kg'),
|
|
('휴먼자소엽.土', 13.8, '1kg'),
|
|
('휴먼작약', 18.7, '1kg'),
|
|
('휴먼작약(주자.酒炙)', 24.6, '1kg'),
|
|
('휴먼전호[재배]', 14.0, '1kg'),
|
|
('휴먼지각', 10.0, '1kg'),
|
|
('휴먼지황.건', 11.5, '1kg'),
|
|
('휴먼진피', 13.7, '1kg'),
|
|
('휴먼창출[북창출]', 13.5, '1kg'),
|
|
('휴먼천궁.일', 11.9, '1kg'),
|
|
('휴먼황기(직절.小)', 9.9, '1kg'),
|
|
]
|
|
|
|
|
|
# 기존 데이터 삭제
|
|
cursor.execute("DELETE FROM supplier_product_catalog WHERE supplier_id = ?", (supplier_id,))
|
|
|
|
# 새 데이터 삽입
|
|
for item in catalog_items:
|
|
raw_name, unit_price, package_unit = item
|
|
|
|
try:
|
|
cursor.execute("""
|
|
INSERT INTO supplier_product_catalog
|
|
(supplier_id, raw_name, unit_price, package_unit, stock_status, last_updated)
|
|
VALUES (?, ?, ?, ?, '재고있음', date('now'))
|
|
""", (supplier_id, raw_name, unit_price, package_unit))
|
|
print(f" 추가: {raw_name} - {unit_price:.1f}원/g")
|
|
except sqlite3.IntegrityError:
|
|
print(f" 중복: {raw_name} (이미 존재)")
|
|
|
|
conn.commit()
|
|
|
|
# 추가된 항목 수 확인
|
|
cursor.execute("SELECT COUNT(*) FROM supplier_product_catalog WHERE supplier_id = ?", (supplier_id,))
|
|
count = cursor.fetchone()[0]
|
|
print(f"\n총 {count}개의 카탈로그 항목이 등록되었습니다.")
|
|
|
|
conn.close()
|
|
|
|
def match_lots_with_catalog():
|
|
"""가격 기반으로 lot과 카탈로그 매칭"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
print("\n" + "="*80)
|
|
print("가격 기반 Lot-Catalog 매칭")
|
|
print("="*80)
|
|
|
|
# 한의사랑에서 입고된 lot들 조회
|
|
cursor.execute("""
|
|
SELECT DISTINCT
|
|
l.lot_id,
|
|
h.herb_name,
|
|
l.unit_price_per_g,
|
|
s.supplier_id,
|
|
s.name as supplier_name
|
|
FROM inventory_lots l
|
|
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
|
|
JOIN purchase_receipt_lines prl ON l.receipt_line_id = prl.line_id
|
|
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
|
|
JOIN suppliers s ON pr.supplier_id = s.supplier_id
|
|
WHERE s.name = '한의사랑'
|
|
AND l.display_name IS NULL
|
|
""")
|
|
|
|
lots = cursor.fetchall()
|
|
matched_count = 0
|
|
|
|
for lot in lots:
|
|
lot_id, herb_name, unit_price, supplier_id, supplier_name = lot
|
|
|
|
# 가격 기반 매칭 (±0.5원 허용)
|
|
cursor.execute("""
|
|
SELECT raw_name, unit_price
|
|
FROM supplier_product_catalog
|
|
WHERE supplier_id = ?
|
|
AND ABS(unit_price - ?) < 0.5
|
|
""", (supplier_id, unit_price))
|
|
|
|
matches = cursor.fetchall()
|
|
|
|
if matches:
|
|
if len(matches) == 1:
|
|
# 정확히 1개 매칭
|
|
raw_name = matches[0][0]
|
|
|
|
# display_name 업데이트
|
|
cursor.execute("""
|
|
UPDATE inventory_lots
|
|
SET display_name = ?
|
|
WHERE lot_id = ?
|
|
""", (raw_name, lot_id))
|
|
|
|
# lot_variants 생성
|
|
cursor.execute("""
|
|
INSERT OR REPLACE INTO lot_variants
|
|
(lot_id, raw_name, parsed_at, parsed_method)
|
|
VALUES (?, ?, datetime('now'), 'price_match')
|
|
""", (lot_id, raw_name))
|
|
|
|
print(f" 매칭 성공: Lot #{lot_id} {herb_name} -> {raw_name}")
|
|
matched_count += 1
|
|
|
|
else:
|
|
# 여러 개 매칭 - 약재명으로 추가 필터링
|
|
print(f" 다중 매칭: Lot #{lot_id} {herb_name} (단가: {unit_price:.1f}원)")
|
|
for match in matches:
|
|
print(f" - {match[0]} ({match[1]:.1f}원/g)")
|
|
|
|
# 약재명이 포함된 것 선택
|
|
if herb_name in match[0]:
|
|
raw_name = match[0]
|
|
|
|
cursor.execute("""
|
|
UPDATE inventory_lots
|
|
SET display_name = ?
|
|
WHERE lot_id = ?
|
|
""", (raw_name, lot_id))
|
|
|
|
cursor.execute("""
|
|
INSERT OR REPLACE INTO lot_variants
|
|
(lot_id, raw_name, parsed_at, parsed_method)
|
|
VALUES (?, ?, datetime('now'), 'price_herb_match')
|
|
""", (lot_id, raw_name))
|
|
|
|
print(f" -> 선택: {raw_name}")
|
|
matched_count += 1
|
|
break
|
|
else:
|
|
print(f" 매칭 실패: Lot #{lot_id} {herb_name} (단가: {unit_price:.1f}원)")
|
|
|
|
conn.commit()
|
|
print(f"\n총 {matched_count}/{len(lots)}개의 로트가 매칭되었습니다.")
|
|
|
|
conn.close()
|
|
return matched_count
|
|
|
|
def parse_variant_attributes():
|
|
"""raw_name에서 variant 속성 파싱"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
print("\n" + "="*80)
|
|
print("Variant 속성 파싱")
|
|
print("="*80)
|
|
|
|
cursor.execute("""
|
|
SELECT variant_id, raw_name
|
|
FROM lot_variants
|
|
WHERE form IS NULL AND processing IS NULL
|
|
""")
|
|
|
|
variants = cursor.fetchall()
|
|
|
|
for variant_id, raw_name in variants:
|
|
# 기본 파싱 로직
|
|
form = None
|
|
processing = None
|
|
selection_state = None
|
|
grade = None
|
|
|
|
# 형태 파싱 (각, 片, 절편, 직절, 土 등)
|
|
if '.각' in raw_name:
|
|
form = '각'
|
|
elif '.片' in raw_name or '[片]' in raw_name:
|
|
form = '片'
|
|
elif '절편' in raw_name:
|
|
form = '절편'
|
|
elif '직절' in raw_name:
|
|
form = '직절'
|
|
elif '.土' in raw_name:
|
|
form = '土'
|
|
|
|
# 가공 파싱 (9증, 酒炙, 비열, 회 등)
|
|
if '9증' in raw_name:
|
|
processing = '9증'
|
|
elif '酒炙' in raw_name or '주자' in raw_name:
|
|
processing = '酒炙'
|
|
elif '비열' in raw_name or '非熱' in raw_name:
|
|
processing = '비열'
|
|
elif '생강백반제' in raw_name:
|
|
processing = '생강백반제'
|
|
elif '.건[회]' in raw_name or '[회]' in raw_name:
|
|
processing = '회'
|
|
|
|
# 선별상태 파싱 (야생, 토매지, 재배 등)
|
|
if '야생' in raw_name:
|
|
selection_state = '야생'
|
|
elif '토매지' in raw_name:
|
|
selection_state = '토매지'
|
|
elif '재배' in raw_name:
|
|
selection_state = '재배'
|
|
elif '영하' in raw_name:
|
|
selection_state = '영하'
|
|
|
|
# 등급 파싱 (특, 名品, 中, 小, 1호, YB2, 당 등)
|
|
if '[특]' in raw_name or '.특' in raw_name:
|
|
grade = '특'
|
|
elif '名品' in raw_name:
|
|
grade = '名品'
|
|
elif '.中' in raw_name:
|
|
grade = '中'
|
|
elif '.小' in raw_name:
|
|
grade = '小'
|
|
elif '1호' in raw_name:
|
|
grade = '1호'
|
|
elif 'YB2' in raw_name:
|
|
grade = 'YB2'
|
|
elif '.당' in raw_name:
|
|
grade = '당'
|
|
elif '[완]' in raw_name:
|
|
grade = '완'
|
|
|
|
# 업데이트
|
|
cursor.execute("""
|
|
UPDATE lot_variants
|
|
SET form = ?, processing = ?, selection_state = ?, grade = ?
|
|
WHERE variant_id = ?
|
|
""", (form, processing, selection_state, grade, variant_id))
|
|
|
|
attributes = []
|
|
if form: attributes.append(f"형태:{form}")
|
|
if processing: attributes.append(f"가공:{processing}")
|
|
if selection_state: attributes.append(f"선별:{selection_state}")
|
|
if grade: attributes.append(f"등급:{grade}")
|
|
|
|
if attributes:
|
|
print(f" {raw_name}")
|
|
print(f" -> {', '.join(attributes)}")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def show_results():
|
|
"""최종 결과 확인"""
|
|
conn = get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
print("\n" + "="*80)
|
|
print("Variant 적용 결과")
|
|
print("="*80)
|
|
|
|
cursor.execute("""
|
|
SELECT
|
|
l.lot_id,
|
|
h.herb_name,
|
|
l.display_name,
|
|
v.form,
|
|
v.processing,
|
|
v.selection_state,
|
|
v.grade,
|
|
l.unit_price_per_g,
|
|
l.quantity_onhand
|
|
FROM inventory_lots l
|
|
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
|
|
LEFT JOIN lot_variants v ON l.lot_id = v.lot_id
|
|
WHERE l.display_name IS NOT NULL
|
|
ORDER BY h.herb_name, l.lot_id
|
|
""")
|
|
|
|
results = cursor.fetchall()
|
|
|
|
current_herb = None
|
|
for result in results:
|
|
lot_id, herb_name, display_name, form, processing, selection_state, grade, price, qty = result
|
|
|
|
if herb_name != current_herb:
|
|
print(f"\n[{herb_name}]")
|
|
current_herb = herb_name
|
|
|
|
print(f" Lot #{lot_id}: {display_name or herb_name}")
|
|
print(f" 재고: {qty:.0f}g, 단가: {price:.1f}원/g")
|
|
|
|
attributes = []
|
|
if form: attributes.append(f"형태:{form}")
|
|
if processing: attributes.append(f"가공:{processing}")
|
|
if selection_state: attributes.append(f"선별:{selection_state}")
|
|
if grade: attributes.append(f"등급:{grade}")
|
|
|
|
if attributes:
|
|
print(f" 속성: {', '.join(attributes)}")
|
|
|
|
# 통계
|
|
cursor.execute("""
|
|
SELECT
|
|
COUNT(DISTINCT l.lot_id) as total_lots,
|
|
COUNT(DISTINCT CASE WHEN l.display_name IS NOT NULL THEN l.lot_id END) as lots_with_display,
|
|
COUNT(DISTINCT v.lot_id) as lots_with_variants
|
|
FROM inventory_lots l
|
|
LEFT JOIN lot_variants v ON l.lot_id = v.lot_id
|
|
""")
|
|
|
|
stats = cursor.fetchone()
|
|
print("\n" + "-"*40)
|
|
print(f"전체 로트: {stats[0]}개")
|
|
print(f"display_name 설정됨: {stats[1]}개")
|
|
print(f"variant 정보 있음: {stats[2]}개")
|
|
|
|
conn.close()
|
|
|
|
def main():
|
|
"""메인 실행 함수"""
|
|
print("\n" + "="*80)
|
|
print("Inventory Lots Variant System 적용")
|
|
print("="*80)
|
|
|
|
# 1. 현재 lot 상태 확인
|
|
lots = check_current_lots()
|
|
|
|
if not lots:
|
|
print("입고된 로트가 없습니다.")
|
|
return
|
|
|
|
# 2. 공급처 카탈로그 데이터 추가
|
|
insert_sample_catalog_data()
|
|
|
|
# 3. 가격 기반 매칭
|
|
matched = match_lots_with_catalog()
|
|
|
|
if matched > 0:
|
|
# 4. variant 속성 파싱
|
|
parse_variant_attributes()
|
|
|
|
# 5. 결과 확인
|
|
show_results()
|
|
|
|
print("\n완료!")
|
|
|
|
if __name__ == "__main__":
|
|
main() |