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:
|
with get_db() as conn:
|
||||||
conn.executescript(schema)
|
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")
|
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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
@ -380,20 +448,148 @@ def get_herbs_by_ingredient(ingredient_code):
|
|||||||
|
|
||||||
# ==================== 처방 관리 API ====================
|
# ==================== 처방 관리 API ====================
|
||||||
|
|
||||||
@app.route('/api/formulas', methods=['GET'])
|
@app.route('/api/official-formulas', methods=['GET'])
|
||||||
def get_formulas():
|
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:
|
try:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT formula_id, formula_code, formula_name, formula_type,
|
SELECT ofi.ingredient_code, ofi.grams_per_cheop, ofi.notes, ofi.sort_order,
|
||||||
base_cheop, base_pouches, description, efficacy
|
hm.herb_name, hm.herb_name_hanja
|
||||||
FROM formulas
|
FROM official_formula_ingredients ofi
|
||||||
WHERE is_active = 1
|
JOIN herb_masters hm ON ofi.ingredient_code = hm.ingredient_code
|
||||||
ORDER BY formula_name
|
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()]
|
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})
|
return jsonify({'success': True, 'data': formulas})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
@ -410,8 +606,8 @@ def create_formula():
|
|||||||
# 처방 마스터 생성
|
# 처방 마스터 생성
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO formulas (formula_code, formula_name, formula_type,
|
INSERT INTO formulas (formula_code, formula_name, formula_type,
|
||||||
base_cheop, base_pouches, description, efficacy, created_by)
|
base_cheop, base_pouches, description, efficacy, created_by, official_formula_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (
|
""", (
|
||||||
data.get('formula_code'),
|
data.get('formula_code'),
|
||||||
data['formula_name'],
|
data['formula_name'],
|
||||||
@ -420,20 +616,21 @@ def create_formula():
|
|||||||
data.get('base_pouches', 30),
|
data.get('base_pouches', 30),
|
||||||
data.get('description'),
|
data.get('description'),
|
||||||
data.get('efficacy'),
|
data.get('efficacy'),
|
||||||
data.get('created_by', 'system')
|
data.get('created_by', 'system'),
|
||||||
|
data.get('official_formula_id')
|
||||||
))
|
))
|
||||||
formula_id = cursor.lastrowid
|
formula_id = cursor.lastrowid
|
||||||
|
|
||||||
# 구성 약재 추가
|
# 구성 약재 추가 (ingredient_code 기반)
|
||||||
if 'ingredients' in data:
|
if 'ingredients' in data:
|
||||||
for idx, ingredient in enumerate(data['ingredients']):
|
for idx, ingredient in enumerate(data['ingredients']):
|
||||||
cursor.execute("""
|
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)
|
grams_per_cheop, notes, sort_order)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""", (
|
""", (
|
||||||
formula_id,
|
formula_id,
|
||||||
ingredient['herb_item_id'],
|
ingredient.get('ingredient_code', ingredient.get('herb_item_id')),
|
||||||
ingredient['grams_per_cheop'],
|
ingredient['grams_per_cheop'],
|
||||||
ingredient.get('notes'),
|
ingredient.get('notes'),
|
||||||
idx
|
idx
|
||||||
@ -1141,11 +1338,22 @@ def get_purchase_receipt_detail(receipt_id):
|
|||||||
receipt_data = dict(receipt)
|
receipt_data = dict(receipt)
|
||||||
|
|
||||||
# 입고장 상세 라인 조회 (display_name 포함)
|
# 입고장 상세 라인 조회 (display_name 포함)
|
||||||
|
# prl.*를 쓰면 prl.lot_number/expiry_date와 il.lot_number/expiry_date가 충돌하므로 명시적으로 나열
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
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.herb_name,
|
||||||
h.insurance_code,
|
h.insurance_code,
|
||||||
|
h.ingredient_code,
|
||||||
il.lot_id,
|
il.lot_id,
|
||||||
il.quantity_onhand as current_stock,
|
il.quantity_onhand as current_stock,
|
||||||
il.display_name,
|
il.display_name,
|
||||||
@ -1268,6 +1476,165 @@ def update_purchase_receipt_line(receipt_id, line_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
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'])
|
@app.route('/api/purchase-receipts/<int:receipt_id>', methods=['DELETE'])
|
||||||
def delete_purchase_receipt(receipt_id):
|
def delete_purchase_receipt(receipt_id):
|
||||||
"""입고장 삭제 (재고 사용 확인 후)"""
|
"""입고장 삭제 (재고 사용 확인 후)"""
|
||||||
@ -1305,7 +1672,21 @@ def delete_purchase_receipt(receipt_id):
|
|||||||
)
|
)
|
||||||
""", (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("""
|
cursor.execute("""
|
||||||
DELETE FROM inventory_lots
|
DELETE FROM inventory_lots
|
||||||
WHERE receipt_line_id IN (
|
WHERE receipt_line_id IN (
|
||||||
@ -1313,10 +1694,10 @@ def delete_purchase_receipt(receipt_id):
|
|||||||
)
|
)
|
||||||
""", (receipt_id,))
|
""", (receipt_id,))
|
||||||
|
|
||||||
# 3. 입고장 라인 삭제 (receipt_id를 참조)
|
# 4. 입고장 라인 삭제 (receipt_id를 참조)
|
||||||
cursor.execute("DELETE FROM purchase_receipt_lines WHERE receipt_id = ?", (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,))
|
cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,))
|
||||||
|
|
||||||
return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'})
|
return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'})
|
||||||
@ -1802,7 +2183,7 @@ def get_stock_ledger():
|
|||||||
s.name as supplier_name,
|
s.name as supplier_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN sl.event_type = 'PURCHASE' OR sl.event_type = 'RECEIPT' THEN pr.receipt_no
|
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
|
CASE
|
||||||
WHEN c.prescription_no IS NOT NULL THEN c.prescription_no || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
WHEN c.prescription_no IS NOT NULL THEN c.prescription_no || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
||||||
ELSE '조제#' || c.compound_id || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
ELSE '조제#' || c.compound_id || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
||||||
@ -1811,7 +2192,7 @@ def get_stock_ledger():
|
|||||||
ELSE NULL
|
ELSE NULL
|
||||||
END as reference_no,
|
END as reference_no,
|
||||||
CASE
|
CASE
|
||||||
WHEN sl.event_type = 'CONSUME' THEN p.name
|
WHEN sl.event_type IN ('CONSUME', 'RETURN') THEN p.name
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END as patient_name
|
END as patient_name
|
||||||
FROM stock_ledger sl
|
FROM stock_ledger sl
|
||||||
@ -1843,7 +2224,7 @@ def get_stock_ledger():
|
|||||||
s.name as supplier_name,
|
s.name as supplier_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN sl.event_type = 'PURCHASE' OR sl.event_type = 'RECEIPT' THEN pr.receipt_no
|
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
|
CASE
|
||||||
WHEN c.prescription_no IS NOT NULL THEN c.prescription_no || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
WHEN c.prescription_no IS NOT NULL THEN c.prescription_no || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
||||||
ELSE '조제#' || c.compound_id || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
ELSE '조제#' || c.compound_id || ' (' || COALESCE(f.formula_name, '직접조제') || ')'
|
||||||
@ -1852,7 +2233,7 @@ def get_stock_ledger():
|
|||||||
ELSE NULL
|
ELSE NULL
|
||||||
END as reference_no,
|
END as reference_no,
|
||||||
CASE
|
CASE
|
||||||
WHEN sl.event_type = 'CONSUME' THEN p.name
|
WHEN sl.event_type IN ('CONSUME', 'RETURN') THEN p.name
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END as patient_name
|
END as patient_name
|
||||||
FROM stock_ledger sl
|
FROM stock_ledger sl
|
||||||
@ -2174,6 +2555,7 @@ def get_inventory_detail(herb_item_id):
|
|||||||
il.quantity_onhand,
|
il.quantity_onhand,
|
||||||
il.unit_price_per_g,
|
il.unit_price_per_g,
|
||||||
il.received_date,
|
il.received_date,
|
||||||
|
il.expiry_date,
|
||||||
il.supplier_id,
|
il.supplier_id,
|
||||||
s.name as supplier_name,
|
s.name as supplier_name,
|
||||||
il.quantity_onhand * il.unit_price_per_g as lot_value,
|
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']
|
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_fields = ['status = ?']
|
||||||
update_values = [new_status]
|
update_values = [new_status]
|
||||||
@ -2999,6 +3388,34 @@ def update_compound_status(compound_id):
|
|||||||
""", (compound_id, old_status, new_status,
|
""", (compound_id, old_status, new_status,
|
||||||
data.get('changed_by', 'system'), data.get('reason', '')))
|
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:
|
if new_status == 'PAID' and 'actual_payment_amount' in data:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user