diff --git a/apply_variants_to_lots.py b/apply_variants_to_lots.py new file mode 100644 index 0000000..2a31b56 --- /dev/null +++ b/apply_variants_to_lots.py @@ -0,0 +1,428 @@ +#!/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() \ No newline at end of file