diff --git a/app.py b/app.py index 50ea310..5663edc 100644 --- a/app.py +++ b/app.py @@ -380,6 +380,276 @@ def upload_purchase_excel(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 +# ==================== 입고장 조회/관리 API ==================== + +@app.route('/api/purchase-receipts', methods=['GET']) +def get_purchase_receipts(): + """입고장 목록 조회""" + try: + with get_db() as conn: + cursor = conn.cursor() + + # 날짜 범위 파라미터 + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + supplier_id = request.args.get('supplier_id') + + query = """ + SELECT + pr.receipt_id, + pr.receipt_date, + pr.receipt_no, + pr.total_amount, + pr.source_file, + pr.created_at, + s.name as supplier_name, + s.supplier_id, + COUNT(prl.line_id) as line_count, + SUM(prl.quantity_g) as total_quantity + FROM purchase_receipts pr + JOIN suppliers s ON pr.supplier_id = s.supplier_id + LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id + WHERE 1=1 + """ + params = [] + + if start_date: + query += " AND pr.receipt_date >= ?" + params.append(start_date) + if end_date: + query += " AND pr.receipt_date <= ?" + params.append(end_date) + if supplier_id: + query += " AND pr.supplier_id = ?" + params.append(supplier_id) + + query += " GROUP BY pr.receipt_id ORDER BY pr.receipt_date DESC, pr.created_at DESC" + + cursor.execute(query, params) + receipts = [] + for row in cursor.fetchall(): + receipt = dict(row) + # 타입 변환 (bytes 문제 해결) + for key, value in receipt.items(): + if isinstance(value, bytes): + # bytes를 float로 변환 시도 + try: + import struct + receipt[key] = struct.unpack('d', value)[0] + except: + receipt[key] = float(0) + elif key in ['receipt_date', 'created_at'] and value is not None: + receipt[key] = str(value) + + # total_amount와 total_quantity 반올림 + if 'total_amount' in receipt and receipt['total_amount'] is not None: + receipt['total_amount'] = round(float(receipt['total_amount']), 2) + if 'total_quantity' in receipt and receipt['total_quantity'] is not None: + receipt['total_quantity'] = round(float(receipt['total_quantity']), 2) + + receipts.append(receipt) + + return jsonify({'success': True, 'data': receipts}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/purchase-receipts/', methods=['GET']) +def get_purchase_receipt_detail(receipt_id): + """입고장 상세 조회""" + try: + with get_db() as conn: + cursor = conn.cursor() + + # 입고장 헤더 조회 + cursor.execute(""" + SELECT + pr.*, + s.name as supplier_name, + s.business_no as supplier_business_no, + s.phone as supplier_phone + FROM purchase_receipts pr + JOIN suppliers s ON pr.supplier_id = s.supplier_id + WHERE pr.receipt_id = ? + """, (receipt_id,)) + + receipt = cursor.fetchone() + if not receipt: + return jsonify({'success': False, 'error': '입고장을 찾을 수 없습니다'}), 404 + + receipt_data = dict(receipt) + + # 입고장 상세 라인 조회 + cursor.execute(""" + SELECT + prl.*, + h.herb_name, + h.insurance_code, + il.lot_id, + il.quantity_onhand as current_stock + FROM purchase_receipt_lines prl + JOIN herb_items h ON prl.herb_item_id = h.herb_item_id + LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id + WHERE prl.receipt_id = ? + ORDER BY prl.line_id + """, (receipt_id,)) + + lines = [dict(row) for row in cursor.fetchall()] + receipt_data['lines'] = lines + + return jsonify({'success': True, 'data': receipt_data}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/purchase-receipts//lines/', methods=['PUT']) +def update_purchase_receipt_line(receipt_id, line_id): + """입고장 라인 수정""" + try: + data = request.json + + with get_db() as conn: + cursor = conn.cursor() + + # 기존 라인 정보 조회 + 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: + return jsonify({'success': False, 'error': '입고 라인을 찾을 수 없습니다'}), 404 + + # 재고 사용 여부 확인 + 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 + + # 수정 가능한 필드만 업데이트 + update_fields = [] + params = [] + + if 'quantity_g' in data: + update_fields.append('quantity_g = ?') + params.append(data['quantity_g']) + + if 'unit_price_per_g' in data: + update_fields.append('unit_price_per_g = ?') + params.append(data['unit_price_per_g']) + + if 'line_total' in data: + update_fields.append('line_total = ?') + params.append(data['line_total']) + elif 'quantity_g' in data and 'unit_price_per_g' in data: + # 자동 계산 + line_total = float(data['quantity_g']) * float(data['unit_price_per_g']) + update_fields.append('line_total = ?') + params.append(line_total) + + if 'origin_country' in data: + update_fields.append('origin_country = ?') + params.append(data['origin_country']) + + if not update_fields: + return jsonify({'success': False, 'error': '수정할 내용이 없습니다'}), 400 + + # 입고장 라인 업데이트 + params.append(line_id) + cursor.execute(f""" + UPDATE purchase_receipt_lines + SET {', '.join(update_fields)} + WHERE line_id = ? + """, params) + + # 재고 로트 업데이트 (수량 변경시) + if 'quantity_g' in data and old_line['lot_id']: + cursor.execute(""" + UPDATE inventory_lots + SET quantity_received = ?, quantity_onhand = ? + WHERE lot_id = ? + """, (data['quantity_g'], data['quantity_g'], old_line['lot_id'])) + + # 재고 원장에 조정 기록 + 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(data['quantity_g']) - float(old_line['quantity_g']), line_id)) + + # 입고장 헤더의 총액 업데이트 + cursor.execute(""" + UPDATE purchase_receipts + SET total_amount = ( + SELECT SUM(line_total) + FROM purchase_receipt_lines + WHERE receipt_id = ? + ), + updated_at = CURRENT_TIMESTAMP + WHERE receipt_id = ? + """, (receipt_id, receipt_id)) + + 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): + """입고장 삭제 (재고 사용 확인 후)""" + try: + with get_db() as conn: + cursor = conn.cursor() + + # 재고 사용 여부 확인 + cursor.execute(""" + SELECT + COUNT(*) as used_count, + SUM(il.quantity_received - il.quantity_onhand) as used_quantity + FROM purchase_receipt_lines prl + JOIN inventory_lots il ON prl.line_id = il.receipt_line_id + WHERE prl.receipt_id = ? + AND il.quantity_onhand < il.quantity_received + """, (receipt_id,)) + + usage = cursor.fetchone() + if usage['used_count'] > 0: + return jsonify({ + 'success': False, + 'error': f'{usage["used_count"]}개 품목에서 {usage["used_quantity"]}g이 이미 사용되어 삭제할 수 없습니다' + }), 400 + + # 재고 로트 삭제 + cursor.execute(""" + DELETE FROM inventory_lots + WHERE receipt_line_id IN ( + SELECT line_id FROM purchase_receipt_lines WHERE receipt_id = ? + ) + """, (receipt_id,)) + + # 재고 원장 기록 + cursor.execute(""" + DELETE FROM stock_ledger + WHERE reference_table = 'purchase_receipts' AND reference_id = ? + """, (receipt_id,)) + + # 입고장 라인 삭제 + cursor.execute("DELETE FROM purchase_receipt_lines WHERE receipt_id = ?", (receipt_id,)) + + # 입고장 헤더 삭제 + cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,)) + + return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'}) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + # ==================== 조제 관리 API ==================== @app.route('/api/compounds', methods=['POST']) diff --git a/sample/image.png b/sample/image.png new file mode 100644 index 0000000..4b3160d Binary files /dev/null and b/sample/image.png differ diff --git a/static/app.js b/static/app.js index 46becc3..a163ee3 100644 --- a/static/app.js +++ b/static/app.js @@ -30,6 +30,9 @@ $(document).ready(function() { case 'patients': loadPatients(); break; + case 'purchase': + loadPurchaseReceipts(); + break; case 'formulas': loadFormulas(); break; @@ -490,6 +493,151 @@ $(document).ready(function() { }); } + // 입고장 목록 로드 + function loadPurchaseReceipts() { + const startDate = $('#purchaseStartDate').val(); + const endDate = $('#purchaseEndDate').val(); + const supplierId = $('#purchaseSupplier').val(); + + let url = '/api/purchase-receipts?'; + if (startDate) url += `start_date=${startDate}&`; + if (endDate) url += `end_date=${endDate}&`; + if (supplierId) url += `supplier_id=${supplierId}`; + + $.get(url, function(response) { + if (response.success) { + const tbody = $('#purchaseReceiptsList'); + tbody.empty(); + + if (response.data.length === 0) { + tbody.append('입고장이 없습니다.'); + return; + } + + response.data.forEach(receipt => { + tbody.append(` + + ${receipt.receipt_date} + ${receipt.supplier_name} + ${receipt.line_count}개 + ${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'} + ${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'} + ${receipt.source_file || '-'} + + + + + + `); + }); + + // 이벤트 바인딩 + $('.view-receipt').on('click', function() { + const receiptId = $(this).data('id'); + viewReceiptDetail(receiptId); + }); + + $('.delete-receipt').on('click', function() { + const receiptId = $(this).data('id'); + if (confirm('정말 이 입고장을 삭제하시겠습니까? 사용되지 않은 재고만 삭제 가능합니다.')) { + deleteReceipt(receiptId); + } + }); + } + }); + } + + // 입고장 상세 보기 + function viewReceiptDetail(receiptId) { + $.get(`/api/purchase-receipts/${receiptId}`, function(response) { + if (response.success) { + const data = response.data; + let linesHtml = ''; + + data.lines.forEach(line => { + linesHtml += ` + + ${line.herb_name} + ${line.insurance_code || '-'} + ${line.origin_country || '-'} + ${line.quantity_g}g + ${formatCurrency(line.unit_price_per_g)} + ${formatCurrency(line.line_total)} + ${line.current_stock}g + + `; + }); + + const modalHtml = ` + + `; + + // 기존 모달 제거 + $('#receiptDetailModal').remove(); + $('body').append(modalHtml); + $('#receiptDetailModal').modal('show'); + } + }); + } + + // 입고장 삭제 + function deleteReceipt(receiptId) { + $.ajax({ + url: `/api/purchase-receipts/${receiptId}`, + method: 'DELETE', + success: function(response) { + if (response.success) { + alert(response.message); + loadPurchaseReceipts(); + } + }, + error: function(xhr) { + alert('오류: ' + xhr.responseJSON.error); + } + }); + } + + // 입고장 조회 버튼 + $('#searchPurchaseBtn').on('click', function() { + loadPurchaseReceipts(); + }); + // 입고장 업로드 $('#purchaseUploadForm').on('submit', function(e) { e.preventDefault(); @@ -514,12 +662,28 @@ $(document).ready(function() { contentType: false, success: function(response) { if (response.success) { + let summaryHtml = ''; + if (response.summary) { + summaryHtml = `
+ + 형식: ${response.summary.format}
+ 처리: ${response.summary.processed_rows}개 라인
+ 품목: ${response.summary.total_items}종
+ 수량: ${response.summary.total_quantity}
+ 금액: ${response.summary.total_amount} +
`; + } + $('#uploadResult').html( `
${response.message} + ${summaryHtml}
` ); $('#purchaseUploadForm')[0].reset(); + + // 입고장 목록 새로고침 + loadPurchaseReceipts(); } }, error: function(xhr) { diff --git a/templates/index.html b/templates/index.html index 94a20c4..dde728e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -222,15 +222,66 @@

입고 관리

-
+ + +
+
+
입고장 목록
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + +
입고일공급업체품목 수총 수량총 금액파일명작업
+
+
+ + +
+
+
새 입고 등록 (Excel 업로드)
+
-
Excel 파일 업로드
- 양식: 제품코드, 업체명, 약재명, 구입일자, 구입량, 구입액, 원산지 + 지원 형식: 한의사랑, 한의정보 (자동 감지)