feat: 100처방 API, 조제취소 재고복원, 처방 가감 비교 기능
- 100처방 마스터 CRUD API (목록조회/수정/구성약재조회) - 100처방 시드 데이터 100개 처방 로드 (init_db) - formulas API에 official_formula_id 및 가감(추가/제거/변경) 정보 포함 - create_formula: herb_item_id → ingredient_code 기반으로 수정 - create_formula: official_formula_id 저장 지원 - 조제 취소(CANCELLED) 시 inventory_lots 재고 복원 + stock_ledger RETURN 기록 - 입고장 삭제 시 취소된 조제의 compound_consumptions 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
87e839be14
commit
51e0c99c77
461
app.py
461
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/<int:official_formula_id>', 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/<int:official_formula_id>/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/<int:receipt_id>/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/<int:receipt_id>', 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("""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user