From 40be340a63f7fff87e9823b212084c0324abd60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Sun, 15 Feb 2026 08:26:51 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=85=EA=B3=A0=EC=9E=A5=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 새로운 기능 - 입고장 목록 조회 (날짜/공급업체 필터링) - 입고장 상세 보기 (모달 팝업) - 입고장 삭제 (재고 미사용시만 가능) - 입고장 라인별 수정 API 📊 화면 구성 1. 입고장 목록 테이블 - 입고일, 공급업체, 품목수, 총수량, 총금액 - 상세보기, 삭제 버튼 2. 입고장 필터링 - 시작일/종료일 선택 - 공급업체별 조회 🔧 백엔드 API - GET /api/purchase-receipts - 입고장 목록 - GET /api/purchase-receipts/ - 입고장 상세 - PUT /api/purchase-receipts//lines/ - 라인 수정 - DELETE /api/purchase-receipts/ - 입고장 삭제 🛡️ 안전장치 - 이미 조제에 사용된 재고는 수정/삭제 불가 - 재고 원장에 모든 변동사항 기록 🤖 Generated with Claude Code Co-Authored-By: Claude --- app.py | 270 +++++++++++++++++++++++++++++++++++ sample/image.png | Bin 0 -> 9429 bytes static/app.js | 164 +++++++++++++++++++++ templates/index.html | 57 +++++++- test_db.py | 46 ++++++ uploads/20260215_081324_xlsx | Bin 0 -> 8054 bytes uploads/20260215_081332_xlsx | Bin 0 -> 7915 bytes 7 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 sample/image.png create mode 100644 test_db.py create mode 100644 uploads/20260215_081324_xlsx create mode 100644 uploads/20260215_081332_xlsx 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 0000000000000000000000000000000000000000..4b3160dcac2b3974df0840be19eb69e73741fc3a GIT binary patch literal 9429 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 SRNMq>fs>b30ai입고장이 없습니다.'); + 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 파일 업로드
- 양식: 제품코드, 업체명, 약재명, 구입일자, 구입량, 구입액, 원산지 + 지원 형식: 한의사랑, 한의정보 (자동 감지)