diff --git a/app.py b/app.py index ae8f88f..c5fdc1b 100644 --- a/app.py +++ b/app.py @@ -58,8 +58,76 @@ def init_db(): with get_db() as conn: conn.executescript(schema) + + cursor = conn.cursor() + + # formulas 테이블 마이그레이션: official_formula_id FK 컬럼 추가 + cursor.execute("PRAGMA table_info(formulas)") + existing_cols = {row[1] for row in cursor.fetchall()} + if 'official_formula_id' not in existing_cols: + cursor.execute("ALTER TABLE formulas ADD COLUMN official_formula_id INTEGER REFERENCES official_formulas(official_formula_id)") + + # official_formulas 테이블 마이그레이션: reference_notes 컬럼 추가 + cursor.execute("PRAGMA table_info(official_formulas)") + of_cols = {row[1] for row in cursor.fetchall()} + if 'reference_notes' not in of_cols: + cursor.execute("ALTER TABLE official_formulas ADD COLUMN reference_notes TEXT") + + # 100처방 원방 마스터 시드 데이터 로드 + cursor.execute("SELECT COUNT(*) FROM official_formulas") + if cursor.fetchone()[0] == 0: + seed_official_formulas(conn) + + conn.commit() print("Database initialized successfully") + +def seed_official_formulas(conn): + """100처방 원방 마스터 데이터 시드""" + official_100 = [ + (1, '가미온담탕', '의종금감'), (2, '가미패독산', '경악전서'), (3, '갈근탕', '상한론'), + (4, '강활유풍탕', '의학발명'), (5, '계지가용골모려탕', '금궤요략'), (6, '계지작약지모탕', '금궤요략'), + (7, '곽향정기산', '화제국방'), (8, '구미강활탕', '차사난지'), (9, '궁귀교애탕', '금궤요략'), + (10, '귀비탕', '제생방'), (11, '귀출파징탕', '동의보감'), (12, '금수육군전', '경악전서'), + (13, '녹용대보탕', '갑병원류서촉'), (14, '당귀사역가오수유생강탕', '상한론'), (15, '당귀수산', '의학입문'), + (16, '당귀육황탕', '난실비장'), (17, '당귀작약산', '금궤요략'), (18, '대강활탕', '위생보감'), + (19, '대건중탕', '금궤요략'), (20, '대금음자', '화제국방'), (21, '대방풍탕', '화제국방'), + (22, '대청룡탕', '상한론'), (23, '대황목단피탕', '금궤요략'), (24, '독활기생탕', '천금방'), + (25, '마행의감탕', '금궤요략'), (26, '마황부자세신탕', '상한론'), (27, '반하백출천마탕', '의학심오'), + (28, '반하사심탕', '상한론'), (29, '반하후박탕', '금궤요략'), (30, '방기황기탕', '금궤요략'), + (31, '방풍통성산', '선명논방'), (32, '배농산급탕', '춘림헌방함'), (33, '백출산', '외대비요'), + (34, '보생탕', '부인양방'), (35, '보중익기탕', '비위론'), (36, '복령음', '외대비요'), + (37, '분심기음', '직지방'), (38, '사군자탕', '화제국방'), (39, '사물탕', '화제국방'), + (40, '삼령백출산', '화제국방'), (41, '삼소음', '화제국방'), (42, '삼출건비탕', '동의보감'), + (43, '삼환사심탕', '금궤요략'), (44, '생혈윤부탕', '의학정전'), (45, '세간명목탕', '중보만병회춘'), + (46, '소건중탕', '상한론'), (47, '소시호탕', '상한론'), (48, '소요산', '화제국방'), + (49, '소자강기탕', '화제국방'), (50, '소적정원산', '의학입문'), (51, '소청룡탕', '상한론'), + (52, '소풍산', '외과정종'), (53, '소풍활혈탕', '심씨존생서'), (54, '속명탕', '금궤요략'), + (55, '승마갈근탕', '염씨소아방론'), (56, '시함탕', '중정통속상한론'), (57, '시호계강탕', '상한론'), + (58, '시호억간탕', '의학입문'), (59, '시호청간탕', '구치유요'), (60, '십전대보탕', '화제국방'), + (61, '쌍화탕', '화제국방'), (62, '안중산', '화제국방'), (63, '양격산', '화제국방'), + (64, '연령고본단', '만병회춘'), (65, '영감강미신하인탕', '금궤요략'), (66, '영계출감탕', '상한론'), + (67, '오약순기산', '화제국방'), (68, '오적산', '화제국방'), (69, '온경탕', '금궤요략'), + (70, '온백원', '화제금궤'), (71, '용담사간탕', '의종금감'), (72, '월비탕', '금궤요략'), + (73, '위령탕', '만병회춘'), (74, '육군자탕', '부인양방'), (75, '육미지황환', '소아약증직결'), + (76, '육울탕', '단계심법'), (77, '이기거풍산', '고금의감'), (78, '이중환', '상한론'), + (79, '이진탕', '화제국방'), (80, '인삼양영탕', '화제국방'), (81, '인삼양위탕', '화제국방'), + (82, '인삼패독산', '소아약증질결'), (83, '인진오령산', '금궤요략'), (84, '자감초탕', '상한론'), + (85, '자음강화탕', '만병회춘'), (86, '자음건비탕', '만병회푼'), (87, '저령탕', '상한론'), + (88, '조경종옥탕', '고금의감'), (89, '지황음자', '선명논방'), (90, '진무탕', '상한론'), + (91, '청간해올탕', '증치준승'), (92, '청금강화탕', '고금의감'), (93, '청상방풍탕', '만병회춘'), + (94, '청서익기탕', '비위론'), (95, '청심연자음', '화제국방'), (96, '평위산', '화제국방'), + (97, '형계연교탕', '일관당'), (98, '형방패독산', '섭생중묘방'), (99, '황련아교탕', '상한론'), + (100, '황련해독탕', '외대비요'), + ] + + cursor = conn.cursor() + for num, name, source in official_100: + cursor.execute(""" + INSERT INTO official_formulas (formula_number, formula_name, source_text) + VALUES (?, ?, ?) + """, (num, name, source)) + # 라우트: 메인 페이지 @app.route('/') def index(): @@ -380,20 +448,148 @@ def get_herbs_by_ingredient(ingredient_code): # ==================== 처방 관리 API ==================== -@app.route('/api/formulas', methods=['GET']) -def get_formulas(): - """처방 목록 조회""" +@app.route('/api/official-formulas', methods=['GET']) +def get_official_formulas(): + """100처방 원방 마스터 목록 조회""" + try: + search = request.args.get('search', '').strip() + with get_db() as conn: + cursor = conn.cursor() + if search: + cursor.execute(""" + SELECT official_formula_id, formula_number, formula_name, + formula_name_hanja, source_text, description, reference_notes + FROM official_formulas + WHERE formula_name LIKE ? OR formula_name_hanja LIKE ? + OR source_text LIKE ? OR reference_notes LIKE ? + ORDER BY formula_number + """, (f'%{search}%', f'%{search}%', f'%{search}%', f'%{search}%')) + else: + cursor.execute(""" + SELECT official_formula_id, formula_number, formula_name, + formula_name_hanja, source_text, description, reference_notes + FROM official_formulas + ORDER BY formula_number + """) + formulas = [dict(row) for row in cursor.fetchall()] + return jsonify({'success': True, 'data': formulas}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/official-formulas/', methods=['PUT']) +def update_official_formula(official_formula_id): + """100처방 원방 마스터 정보 수정""" + try: + data = request.json + with get_db() as conn: + cursor = conn.cursor() + update_fields = [] + update_values = [] + for field in ['formula_name_hanja', 'description', 'reference_notes']: + if field in data: + update_fields.append(f'{field} = ?') + update_values.append(data[field]) + if not update_fields: + return jsonify({'error': '수정할 항목이 없습니다'}), 400 + update_fields.append('updated_at = CURRENT_TIMESTAMP') + update_values.append(official_formula_id) + cursor.execute(f""" + UPDATE official_formulas SET {', '.join(update_fields)} + WHERE official_formula_id = ? + """, update_values) + conn.commit() + return jsonify({'success': True, 'message': '수정되었습니다'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/official-formulas//ingredients', methods=['GET']) +def get_official_formula_ingredients(official_formula_id): + """100처방 원방 구성 약재 조회""" try: with get_db() as conn: cursor = conn.cursor() cursor.execute(""" - SELECT formula_id, formula_code, formula_name, formula_type, - base_cheop, base_pouches, description, efficacy - FROM formulas - WHERE is_active = 1 - ORDER BY formula_name + SELECT ofi.ingredient_code, ofi.grams_per_cheop, ofi.notes, ofi.sort_order, + hm.herb_name, hm.herb_name_hanja + FROM official_formula_ingredients ofi + JOIN herb_masters hm ON ofi.ingredient_code = hm.ingredient_code + WHERE ofi.official_formula_id = ? + ORDER BY ofi.sort_order + """, (official_formula_id,)) + ingredients = [dict(row) for row in cursor.fetchall()] + return jsonify({'success': True, 'data': ingredients}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/formulas', methods=['GET']) +def get_formulas(): + """처방 목록 조회 (100처방 대비 가감 정보 포함)""" + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT f.formula_id, f.formula_code, f.formula_name, f.formula_type, + f.base_cheop, f.base_pouches, f.description, f.efficacy, + f.official_formula_id, + of2.formula_name as official_name + FROM formulas f + LEFT JOIN official_formulas of2 ON f.official_formula_id = of2.official_formula_id + WHERE f.is_active = 1 + ORDER BY f.formula_name """) formulas = [dict(row) for row in cursor.fetchall()] + + # 100처방 기반 처방에 대해 가감 정보 계산 + for formula in formulas: + if formula.get('official_formula_id'): + official_id = formula['official_formula_id'] + formula_id = formula['formula_id'] + + # 원방 구성 조회 + cursor.execute(""" + SELECT ofi.ingredient_code, hm.herb_name, ofi.grams_per_cheop + FROM official_formula_ingredients ofi + JOIN herb_masters hm ON ofi.ingredient_code = hm.ingredient_code + WHERE ofi.official_formula_id = ? + """, (official_id,)) + original = {row['ingredient_code']: {'name': row['herb_name'], 'grams': row['grams_per_cheop']} for row in cursor.fetchall()} + + # 내 처방 구성 조회 + cursor.execute(""" + SELECT fi.ingredient_code, hm.herb_name, fi.grams_per_cheop + FROM formula_ingredients fi + JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code + WHERE fi.formula_id = ? + """, (formula_id,)) + current = {row['ingredient_code']: {'name': row['herb_name'], 'grams': row['grams_per_cheop']} for row in cursor.fetchall()} + + added = [] + removed = [] + modified = [] + + # 추가된 약재 + for code, info in current.items(): + if code not in original: + added.append(f"{info['name']} {info['grams']}g") + + # 제거된 약재 + for code, info in original.items(): + if code not in current: + removed.append(info['name']) + + # 용량 변경 + for code in current: + if code in original: + orig_g = original[code]['grams'] + curr_g = current[code]['grams'] + if abs(orig_g - curr_g) > 0.01: + modified.append(f"{original[code]['name']} {orig_g}g→{curr_g}g") + + formula['custom_added'] = added + formula['custom_removed'] = removed + formula['custom_modified'] = modified + formula['is_custom'] = bool(added or removed or modified) + return jsonify({'success': True, 'data': formulas}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @@ -410,8 +606,8 @@ def create_formula(): # 처방 마스터 생성 cursor.execute(""" INSERT INTO formulas (formula_code, formula_name, formula_type, - base_cheop, base_pouches, description, efficacy, created_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + base_cheop, base_pouches, description, efficacy, created_by, official_formula_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( data.get('formula_code'), data['formula_name'], @@ -420,20 +616,21 @@ def create_formula(): data.get('base_pouches', 30), data.get('description'), data.get('efficacy'), - data.get('created_by', 'system') + data.get('created_by', 'system'), + data.get('official_formula_id') )) formula_id = cursor.lastrowid - # 구성 약재 추가 + # 구성 약재 추가 (ingredient_code 기반) if 'ingredients' in data: for idx, ingredient in enumerate(data['ingredients']): cursor.execute(""" - INSERT INTO formula_ingredients (formula_id, herb_item_id, + INSERT INTO formula_ingredients (formula_id, ingredient_code, grams_per_cheop, notes, sort_order) VALUES (?, ?, ?, ?, ?) """, ( formula_id, - ingredient['herb_item_id'], + ingredient.get('ingredient_code', ingredient.get('herb_item_id')), ingredient['grams_per_cheop'], ingredient.get('notes'), idx @@ -1141,11 +1338,22 @@ def get_purchase_receipt_detail(receipt_id): receipt_data = dict(receipt) # 입고장 상세 라인 조회 (display_name 포함) + # prl.*를 쓰면 prl.lot_number/expiry_date와 il.lot_number/expiry_date가 충돌하므로 명시적으로 나열 cursor.execute(""" SELECT - prl.*, + prl.line_id, + prl.receipt_id, + prl.herb_item_id, + prl.origin_country, + prl.quantity_g, + prl.unit_price_per_g, + prl.line_total, + prl.created_at, + COALESCE(il.lot_number, prl.lot_number) as lot_number, + COALESCE(il.expiry_date, prl.expiry_date) as expiry_date, h.herb_name, h.insurance_code, + h.ingredient_code, il.lot_id, il.quantity_onhand as current_stock, il.display_name, @@ -1268,6 +1476,165 @@ def update_purchase_receipt_line(receipt_id, line_id): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/purchase-receipts//bulk', methods=['PUT']) +def bulk_update_purchase_receipt(receipt_id): + """입고장 헤더 + 전체 라인 일괄 수정""" + try: + data = request.get_json() + notes = data.get('notes') + lines = data.get('lines', []) + + with get_db() as conn: + cursor = conn.cursor() + + # 입고장 존재 확인 + cursor.execute("SELECT receipt_id FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,)) + if not cursor.fetchone(): + return jsonify({'success': False, 'error': '입고장을 찾을 수 없습니다'}), 404 + + # 각 라인 업데이트 + for line_data in lines: + line_id = line_data.get('line_id') + if not line_id: + continue + + # 기존 라인 + 로트 정보 조회 + cursor.execute(""" + SELECT prl.*, il.lot_id, il.quantity_onhand, il.quantity_received + FROM purchase_receipt_lines prl + LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id + WHERE prl.line_id = ? AND prl.receipt_id = ? + """, (line_id, receipt_id)) + + old_line = cursor.fetchone() + if not old_line: + continue + + # 수량 변경 시 재고 사용 여부 확인 + new_qty = line_data.get('quantity_g') + if new_qty is not None and float(new_qty) != float(old_line['quantity_g']): + if old_line['quantity_onhand'] != old_line['quantity_received']: + used_qty = old_line['quantity_received'] - old_line['quantity_onhand'] + return jsonify({ + 'success': False, + 'error': f'이미 {used_qty}g이 사용되어 수량을 수정할 수 없습니다' + }), 400 + + # purchase_receipt_lines 업데이트 + prl_fields = [] + prl_params = [] + + if 'quantity_g' in line_data: + prl_fields.append('quantity_g = ?') + prl_params.append(line_data['quantity_g']) + + if 'unit_price_per_g' in line_data: + prl_fields.append('unit_price_per_g = ?') + prl_params.append(line_data['unit_price_per_g']) + + if 'origin_country' in line_data: + prl_fields.append('origin_country = ?') + prl_params.append(line_data['origin_country']) + + if 'lot_number' in line_data: + prl_fields.append('lot_number = ?') + prl_params.append(line_data['lot_number'] or None) + + if 'expiry_date' in line_data: + prl_fields.append('expiry_date = ?') + prl_params.append(line_data['expiry_date'] or None) + + # line_total 자동 계산 + qty = float(line_data.get('quantity_g', old_line['quantity_g'])) + price = float(line_data.get('unit_price_per_g', old_line['unit_price_per_g'])) + line_total = qty * price + prl_fields.append('line_total = ?') + prl_params.append(line_total) + + if prl_fields: + prl_params.append(line_id) + cursor.execute(f""" + UPDATE purchase_receipt_lines + SET {', '.join(prl_fields)} + WHERE line_id = ? + """, prl_params) + + # inventory_lots 업데이트 + if old_line['lot_id']: + lot_fields = [] + lot_params = [] + + if 'lot_number' in line_data: + lot_fields.append('lot_number = ?') + lot_params.append(line_data['lot_number'] or None) + + if 'expiry_date' in line_data: + lot_fields.append('expiry_date = ?') + lot_params.append(line_data['expiry_date'] or None) + + if 'origin_country' in line_data: + lot_fields.append('origin_country = ?') + lot_params.append(line_data['origin_country']) + + if 'unit_price_per_g' in line_data: + lot_fields.append('unit_price_per_g = ?') + lot_params.append(line_data['unit_price_per_g']) + + # 수량 변경 시 재고 로트도 업데이트 + if new_qty is not None and float(new_qty) != float(old_line['quantity_g']): + lot_fields.append('quantity_received = ?') + lot_params.append(new_qty) + lot_fields.append('quantity_onhand = ?') + lot_params.append(new_qty) + + # 재고 원장에 조정 기록 + cursor.execute(""" + INSERT INTO stock_ledger + (event_type, herb_item_id, lot_id, quantity_delta, notes, reference_table, reference_id) + VALUES ('ADJUST', + (SELECT herb_item_id FROM purchase_receipt_lines WHERE line_id = ?), + ?, ?, '입고장 수정', 'purchase_receipt_lines', ?) + """, (line_id, old_line['lot_id'], + float(new_qty) - float(old_line['quantity_g']), line_id)) + + if lot_fields: + lot_params.append(old_line['lot_id']) + cursor.execute(f""" + UPDATE inventory_lots + SET {', '.join(lot_fields)} + WHERE lot_id = ? + """, lot_params) + + # 입고장 헤더 업데이트 + header_fields = [] + header_params = [] + + if notes is not None: + header_fields.append('notes = ?') + header_params.append(notes) + + # 총액 재계산 + header_fields.append("""total_amount = ( + SELECT COALESCE(SUM(line_total), 0) + FROM purchase_receipt_lines + WHERE receipt_id = ? + )""") + header_params.append(receipt_id) + + header_fields.append('updated_at = CURRENT_TIMESTAMP') + header_params.append(receipt_id) + + cursor.execute(f""" + UPDATE purchase_receipts + SET {', '.join(header_fields)} + WHERE receipt_id = ? + """, header_params) + + return jsonify({'success': True, 'message': '입고장이 수정되었습니다'}) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + @app.route('/api/purchase-receipts/', methods=['DELETE']) def delete_purchase_receipt(receipt_id): """입고장 삭제 (재고 사용 확인 후)""" @@ -1305,7 +1672,21 @@ def delete_purchase_receipt(receipt_id): ) """, (receipt_id,)) - # 2. 재고 로트 삭제 (receipt_line_id를 참조) + # 2. 취소된 조제의 소비 내역 삭제 (lot_id를 참조) + cursor.execute(""" + DELETE FROM compound_consumptions + WHERE lot_id IN ( + SELECT lot_id FROM inventory_lots + WHERE receipt_line_id IN ( + SELECT line_id FROM purchase_receipt_lines WHERE receipt_id = ? + ) + ) + AND compound_id IN ( + SELECT compound_id FROM compounds WHERE status = 'CANCELLED' + ) + """, (receipt_id,)) + + # 3. 재고 로트 삭제 (receipt_line_id를 참조) cursor.execute(""" DELETE FROM inventory_lots WHERE receipt_line_id IN ( @@ -1313,10 +1694,10 @@ def delete_purchase_receipt(receipt_id): ) """, (receipt_id,)) - # 3. 입고장 라인 삭제 (receipt_id를 참조) + # 4. 입고장 라인 삭제 (receipt_id를 참조) cursor.execute("DELETE FROM purchase_receipt_lines WHERE receipt_id = ?", (receipt_id,)) - # 4. 입고장 헤더 삭제 + # 5. 입고장 헤더 삭제 cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,)) return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'}) @@ -1802,7 +2183,7 @@ def get_stock_ledger(): s.name as supplier_name, CASE WHEN sl.event_type = 'PURCHASE' OR sl.event_type = 'RECEIPT' THEN pr.receipt_no - WHEN sl.event_type = 'CONSUME' THEN + WHEN sl.event_type IN ('CONSUME', 'RETURN') THEN CASE WHEN c.prescription_no IS NOT NULL THEN c.prescription_no || ' (' || COALESCE(f.formula_name, '직접조제') || ')' ELSE '조제#' || c.compound_id || ' (' || COALESCE(f.formula_name, '직접조제') || ')' @@ -1811,7 +2192,7 @@ def get_stock_ledger(): ELSE NULL END as reference_no, CASE - WHEN sl.event_type = 'CONSUME' THEN p.name + WHEN sl.event_type IN ('CONSUME', 'RETURN') THEN p.name ELSE NULL END as patient_name FROM stock_ledger sl @@ -1843,7 +2224,7 @@ def get_stock_ledger(): s.name as supplier_name, CASE WHEN sl.event_type = 'PURCHASE' OR sl.event_type = 'RECEIPT' THEN pr.receipt_no - WHEN sl.event_type = 'CONSUME' THEN + WHEN sl.event_type IN ('CONSUME', 'RETURN') THEN CASE WHEN c.prescription_no IS NOT NULL THEN c.prescription_no || ' (' || COALESCE(f.formula_name, '직접조제') || ')' ELSE '조제#' || c.compound_id || ' (' || COALESCE(f.formula_name, '직접조제') || ')' @@ -1852,7 +2233,7 @@ def get_stock_ledger(): ELSE NULL END as reference_no, CASE - WHEN sl.event_type = 'CONSUME' THEN p.name + WHEN sl.event_type IN ('CONSUME', 'RETURN') THEN p.name ELSE NULL END as patient_name FROM stock_ledger sl @@ -2174,6 +2555,7 @@ def get_inventory_detail(herb_item_id): il.quantity_onhand, il.unit_price_per_g, il.received_date, + il.expiry_date, il.supplier_id, s.name as supplier_name, il.quantity_onhand * il.unit_price_per_g as lot_value, @@ -2956,6 +3338,13 @@ def update_compound_status(compound_id): old_status = current['status'] + # 취소 요청 시 사전 검증 + if new_status == 'CANCELLED': + if old_status == 'CANCELLED': + return jsonify({'error': '이미 취소된 조제입니다'}), 400 + if old_status != 'PREPARED': + return jsonify({'error': '조제완료(PREPARED) 상태에서만 취소할 수 있습니다'}), 400 + # 상태 업데이트 update_fields = ['status = ?'] update_values = [new_status] @@ -2999,6 +3388,34 @@ def update_compound_status(compound_id): """, (compound_id, old_status, new_status, data.get('changed_by', 'system'), data.get('reason', ''))) + # 조제 취소 시 재고 복원 + if new_status == 'CANCELLED': + # compound_consumptions에서 사용 내역 조회 + cursor.execute(""" + SELECT consumption_id, herb_item_id, lot_id, quantity_used, unit_cost_per_g + FROM compound_consumptions + WHERE compound_id = ? + """, (compound_id,)) + consumptions = cursor.fetchall() + + for con in consumptions: + # 재고 복원 + cursor.execute(""" + UPDATE inventory_lots + SET quantity_onhand = quantity_onhand + ?, + is_depleted = CASE WHEN quantity_onhand + ? > 0 THEN 0 ELSE is_depleted END + WHERE lot_id = ? + """, (con['quantity_used'], con['quantity_used'], con['lot_id'])) + + # stock_ledger에 RETURN 이벤트 기록 (조제 취소 복원) + cursor.execute(""" + INSERT INTO stock_ledger (event_type, herb_item_id, lot_id, + quantity_delta, unit_cost_per_g, + reference_table, reference_id) + VALUES ('RETURN', ?, ?, ?, ?, 'compounds', ?) + """, (con['herb_item_id'], con['lot_id'], con['quantity_used'], + con['unit_cost_per_g'], compound_id)) + # 판매 거래 기록 (결제 완료시) if new_status == 'PAID' and 'actual_payment_amount' in data: cursor.execute("""