feat: 수동입고 기능 구현 및 입고일 날짜 포맷 버그 수정
- 수동입고 API (POST /api/purchase-receipts/manual) 추가 - 수동입고 모달 UI 구현 (도매상 선택, 품목 동적 추가, 금액 자동계산) - 도매상 등록 모달 z-index 처리 (수동입고 모달 위에 표시) - Excel 입고 시 receipt_date 튜플/대시 없는 날짜 포맷 정규화 - inventory_lots에 lot_number, expiry_date 저장 누락 수정 - CLAUDE.md 추가 (lot_id vs lot_number 구분 가이드) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3d13c0b1f3
commit
3a39951fdc
8
CLAUDE.md
Normal file
8
CLAUDE.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# CLAUDE.md - AI 개발 가이드
|
||||||
|
|
||||||
|
## DB 주의사항
|
||||||
|
|
||||||
|
### inventory_lots 테이블: lot_id vs lot_number
|
||||||
|
- `lot_id` (INTEGER PK): 시스템이 자동 생성하는 내부 식별자. 재고 추적/조제/원장 등 모든 로직에서 로트를 참조할 때 사용.
|
||||||
|
- `lot_number` (TEXT, nullable): 도매상이 부여한 납품 로트번호. 사용자가 직접 입력하는 참고용 텍스트.
|
||||||
|
- INSERT 시 `lot_number`와 `expiry_date`를 빠뜨리지 말 것. 둘 다 nullable이지만 사용자가 입력했으면 반드시 저장해야 함.
|
||||||
174
app.py
174
app.py
@ -168,6 +168,40 @@ def create_patient():
|
|||||||
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/patients/<int:patient_id>', methods=['PUT'])
|
||||||
|
def update_patient(patient_id):
|
||||||
|
"""환자 정보 수정"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT patient_id FROM patients WHERE patient_id = ?", (patient_id,))
|
||||||
|
if not cursor.fetchone():
|
||||||
|
return jsonify({'success': False, 'error': '환자를 찾을 수 없습니다'}), 404
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE patients
|
||||||
|
SET name = ?, phone = ?, jumin_no = ?, gender = ?,
|
||||||
|
birth_date = ?, address = ?, notes = ?
|
||||||
|
WHERE patient_id = ?
|
||||||
|
""", (
|
||||||
|
data.get('name'),
|
||||||
|
data.get('phone'),
|
||||||
|
data.get('jumin_no'),
|
||||||
|
data.get('gender'),
|
||||||
|
data.get('birth_date'),
|
||||||
|
data.get('address'),
|
||||||
|
data.get('notes'),
|
||||||
|
patient_id
|
||||||
|
))
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': '환자 정보가 수정되었습니다'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
# ==================== 약재 관리 API ====================
|
# ==================== 약재 관리 API ====================
|
||||||
|
|
||||||
@app.route('/api/herbs', methods=['GET'])
|
@app.route('/api/herbs', methods=['GET'])
|
||||||
@ -710,6 +744,11 @@ def upload_purchase_excel():
|
|||||||
# receipt_date를 문자열로 확실히 변환
|
# receipt_date를 문자열로 확실히 변환
|
||||||
receipt_date_str = str(receipt_date)
|
receipt_date_str = str(receipt_date)
|
||||||
|
|
||||||
|
# YYYY-MM-DD 포맷으로 정규화
|
||||||
|
clean_date = receipt_date_str.replace('-', '')
|
||||||
|
if len(clean_date) == 8 and clean_date.isdigit():
|
||||||
|
receipt_date_str = f"{clean_date[:4]}-{clean_date[4:6]}-{clean_date[6:8]}"
|
||||||
|
|
||||||
# 입고장 번호 생성 (PR-YYYYMMDD-XXXX)
|
# 입고장 번호 생성 (PR-YYYYMMDD-XXXX)
|
||||||
date_str = receipt_date_str.replace('-', '')
|
date_str = receipt_date_str.replace('-', '')
|
||||||
|
|
||||||
@ -833,7 +872,7 @@ def upload_purchase_excel():
|
|||||||
(herb_item_id, supplier_id, receipt_line_id, received_date, origin_country,
|
(herb_item_id, supplier_id, receipt_line_id, received_date, origin_country,
|
||||||
unit_price_per_g, quantity_received, quantity_onhand)
|
unit_price_per_g, quantity_received, quantity_onhand)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (herb_item_id, supplier_id, line_id, str(receipt_date),
|
""", (herb_item_id, supplier_id, line_id, receipt_date_str,
|
||||||
row.get('origin_country'), unit_price, quantity, quantity))
|
row.get('origin_country'), unit_price, quantity, quantity))
|
||||||
lot_id = cursor.lastrowid
|
lot_id = cursor.lastrowid
|
||||||
|
|
||||||
@ -870,6 +909,139 @@ def upload_purchase_excel():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
# ==================== 수동 입고 API ====================
|
||||||
|
|
||||||
|
@app.route('/api/purchase-receipts/manual', methods=['POST'])
|
||||||
|
def create_manual_receipt():
|
||||||
|
"""수동 입고 처리"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 필수값 검증
|
||||||
|
supplier_id = data.get('supplier_id')
|
||||||
|
receipt_date = data.get('receipt_date')
|
||||||
|
notes = data.get('notes', '')
|
||||||
|
lines = data.get('lines', [])
|
||||||
|
|
||||||
|
if not supplier_id:
|
||||||
|
return jsonify({'success': False, 'error': '도매상을 선택해주세요.'}), 400
|
||||||
|
if not receipt_date:
|
||||||
|
return jsonify({'success': False, 'error': '입고일을 입력해주세요.'}), 400
|
||||||
|
if not lines or len(lines) == 0:
|
||||||
|
return jsonify({'success': False, 'error': '입고 품목을 1개 이상 추가해주세요.'}), 400
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 도매상 존재 확인
|
||||||
|
cursor.execute("SELECT name FROM suppliers WHERE supplier_id = ?", (supplier_id,))
|
||||||
|
supplier_info = cursor.fetchone()
|
||||||
|
if not supplier_info:
|
||||||
|
return jsonify({'success': False, 'error': '유효하지 않은 도매상입니다.'}), 400
|
||||||
|
|
||||||
|
# 입고장 번호 생성 (PR-YYYYMMDD-XXXX)
|
||||||
|
date_str = receipt_date.replace('-', '')
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT MAX(CAST(SUBSTR(receipt_no, -4) AS INTEGER))
|
||||||
|
FROM purchase_receipts
|
||||||
|
WHERE receipt_no LIKE ?
|
||||||
|
""", (f'PR-{date_str}-%',))
|
||||||
|
max_num = cursor.fetchone()[0]
|
||||||
|
next_num = (max_num or 0) + 1
|
||||||
|
receipt_no = f"PR-{date_str}-{next_num:04d}"
|
||||||
|
|
||||||
|
# 총 금액 계산
|
||||||
|
total_amount = sum(
|
||||||
|
float(line.get('quantity_g', 0)) * float(line.get('unit_price_per_g', 0))
|
||||||
|
for line in lines
|
||||||
|
)
|
||||||
|
|
||||||
|
# 입고장 헤더 생성
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO purchase_receipts (supplier_id, receipt_date, receipt_no, total_amount, source_file, notes)
|
||||||
|
VALUES (?, ?, ?, ?, 'MANUAL', ?)
|
||||||
|
""", (supplier_id, receipt_date, receipt_no, total_amount, notes))
|
||||||
|
receipt_id = cursor.lastrowid
|
||||||
|
|
||||||
|
processed_count = 0
|
||||||
|
for line in lines:
|
||||||
|
ingredient_code = line.get('ingredient_code')
|
||||||
|
quantity_g = float(line.get('quantity_g', 0))
|
||||||
|
unit_price = float(line.get('unit_price_per_g', 0))
|
||||||
|
origin_country = line.get('origin_country', '')
|
||||||
|
lot_number = line.get('lot_number', '')
|
||||||
|
expiry_date = line.get('expiry_date', '')
|
||||||
|
line_total = quantity_g * unit_price
|
||||||
|
|
||||||
|
if not ingredient_code or quantity_g <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# herb_items에서 해당 ingredient_code 조회
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT herb_item_id FROM herb_items
|
||||||
|
WHERE ingredient_code = ?
|
||||||
|
""", (ingredient_code,))
|
||||||
|
herb = cursor.fetchone()
|
||||||
|
|
||||||
|
if not herb:
|
||||||
|
# herb_masters에서 약재명 가져와서 herb_items 생성
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT herb_name FROM herb_masters
|
||||||
|
WHERE ingredient_code = ?
|
||||||
|
""", (ingredient_code,))
|
||||||
|
master = cursor.fetchone()
|
||||||
|
herb_name = master[0] if master else ingredient_code
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO herb_items (ingredient_code, herb_name)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""", (ingredient_code, herb_name))
|
||||||
|
herb_item_id = cursor.lastrowid
|
||||||
|
else:
|
||||||
|
herb_item_id = herb[0]
|
||||||
|
|
||||||
|
# 입고장 라인 생성
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO purchase_receipt_lines
|
||||||
|
(receipt_id, herb_item_id, origin_country, quantity_g, unit_price_per_g, line_total)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""", (receipt_id, herb_item_id, origin_country, quantity_g, unit_price, line_total))
|
||||||
|
line_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 재고 로트 생성
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO inventory_lots
|
||||||
|
(herb_item_id, supplier_id, receipt_line_id, received_date, origin_country,
|
||||||
|
unit_price_per_g, quantity_received, quantity_onhand, lot_number, expiry_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (herb_item_id, supplier_id, line_id, receipt_date,
|
||||||
|
origin_country, unit_price, quantity_g, quantity_g,
|
||||||
|
lot_number or None, expiry_date or None))
|
||||||
|
lot_id = cursor.lastrowid
|
||||||
|
|
||||||
|
# 재고 원장 기록
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO stock_ledger
|
||||||
|
(event_type, herb_item_id, lot_id, quantity_delta, unit_cost_per_g,
|
||||||
|
reference_table, reference_id)
|
||||||
|
VALUES ('RECEIPT', ?, ?, ?, ?, 'purchase_receipts', ?)
|
||||||
|
""", (herb_item_id, lot_id, quantity_g, unit_price, receipt_id))
|
||||||
|
|
||||||
|
processed_count += 1
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': '수동 입고가 완료되었습니다.',
|
||||||
|
'receipt_no': receipt_no,
|
||||||
|
'summary': {
|
||||||
|
'item_count': processed_count,
|
||||||
|
'total_amount': f"{total_amount:,.0f}원"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
# ==================== 입고장 조회/관리 API ====================
|
# ==================== 입고장 조회/관리 API ====================
|
||||||
|
|
||||||
@app.route('/api/purchase-receipts', methods=['GET'])
|
@app.route('/api/purchase-receipts', methods=['GET'])
|
||||||
|
|||||||
212
static/app.js
212
static/app.js
@ -2028,6 +2028,13 @@ $(document).ready(function() {
|
|||||||
response.data.forEach(supplier => {
|
response.data.forEach(supplier => {
|
||||||
filterSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
|
filterSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 수동 입고용 셀렉트 박스도 업데이트
|
||||||
|
const manualSelect = $('#manualReceiptSupplier');
|
||||||
|
manualSelect.empty().append('<option value="">도매상을 선택하세요</option>');
|
||||||
|
response.data.forEach(supplier => {
|
||||||
|
manualSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -2066,6 +2073,211 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== 수동 입고 ====================
|
||||||
|
|
||||||
|
// 전체 약재 목록 로드 (입고용 - 재고 필터 없음)
|
||||||
|
function loadAllHerbsForSelect(selectElement) {
|
||||||
|
$.get('/api/herbs/masters', function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
selectElement.empty().append('<option value="">약재 선택</option>');
|
||||||
|
response.data.forEach(herb => {
|
||||||
|
let displayName = herb.herb_name;
|
||||||
|
if (herb.herb_name_hanja) {
|
||||||
|
displayName += ` (${herb.herb_name_hanja})`;
|
||||||
|
}
|
||||||
|
selectElement.append(`<option value="${herb.ingredient_code}" data-herb-name="${herb.herb_name}">${displayName}</option>`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let manualReceiptLineCount = 0;
|
||||||
|
|
||||||
|
function addManualReceiptLine() {
|
||||||
|
manualReceiptLineCount++;
|
||||||
|
const row = `
|
||||||
|
<tr data-row="${manualReceiptLineCount}">
|
||||||
|
<td>
|
||||||
|
<select class="form-control form-control-sm manual-herb-select">
|
||||||
|
<option value="">약재 선택</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm manual-qty-input text-end"
|
||||||
|
min="0.1" step="0.1" placeholder="0.0">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control form-control-sm manual-price-input text-end"
|
||||||
|
min="0" step="0.1" placeholder="0.0">
|
||||||
|
</td>
|
||||||
|
<td class="text-end manual-line-total">0</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm manual-origin-input" placeholder="예: 중국">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control form-control-sm manual-lot-input" placeholder="로트번호">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="date" class="form-control form-control-sm manual-expiry-input">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger remove-manual-line">
|
||||||
|
<i class="bi bi-x"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
$('#manualReceiptLines').append(row);
|
||||||
|
|
||||||
|
const newRow = $(`#manualReceiptLines tr[data-row="${manualReceiptLineCount}"]`);
|
||||||
|
loadAllHerbsForSelect(newRow.find('.manual-herb-select'));
|
||||||
|
|
||||||
|
// 금액 자동 계산
|
||||||
|
newRow.find('.manual-qty-input, .manual-price-input').on('input', function() {
|
||||||
|
updateManualReceiptLineTotals();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 삭제 버튼
|
||||||
|
newRow.find('.remove-manual-line').on('click', function() {
|
||||||
|
$(this).closest('tr').remove();
|
||||||
|
updateManualReceiptLineTotals();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateManualReceiptLineTotals() {
|
||||||
|
let totalQty = 0;
|
||||||
|
let totalAmount = 0;
|
||||||
|
$('#manualReceiptLines tr').each(function() {
|
||||||
|
const qty = parseFloat($(this).find('.manual-qty-input').val()) || 0;
|
||||||
|
const price = parseFloat($(this).find('.manual-price-input').val()) || 0;
|
||||||
|
const lineTotal = qty * price;
|
||||||
|
$(this).find('.manual-line-total').text(lineTotal.toLocaleString('ko-KR'));
|
||||||
|
totalQty += qty;
|
||||||
|
totalAmount += lineTotal;
|
||||||
|
});
|
||||||
|
$('#manualReceiptTotalQty').text(totalQty.toLocaleString('ko-KR'));
|
||||||
|
$('#manualReceiptTotalAmount').text(totalAmount.toLocaleString('ko-KR'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모달 열릴 때 초기화
|
||||||
|
$('#manualReceiptModal').on('show.bs.modal', function() {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
$('#manualReceiptDate').val(today);
|
||||||
|
$('#manualReceiptSupplier').val('');
|
||||||
|
$('#manualReceiptNotes').val('');
|
||||||
|
$('#manualReceiptLines').empty();
|
||||||
|
manualReceiptLineCount = 0;
|
||||||
|
updateManualReceiptLineTotals();
|
||||||
|
addManualReceiptLine();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 품목 추가 버튼
|
||||||
|
$('#addManualReceiptLineBtn').on('click', function() {
|
||||||
|
addManualReceiptLine();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 새 도매상 등록 버튼 (수동 입고 모달에서)
|
||||||
|
$('#manualReceiptAddSupplierBtn').on('click', function() {
|
||||||
|
$('#supplierModal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 도매상 모달이 수동입고 모달 위에 뜨도록 z-index 조정
|
||||||
|
$('#supplierModal').on('shown.bs.modal', function() {
|
||||||
|
if ($('#manualReceiptModal').hasClass('show')) {
|
||||||
|
$(this).css('z-index', 1060);
|
||||||
|
$('.modal-backdrop').last().css('z-index', 1055);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#supplierModal').on('hidden.bs.modal', function() {
|
||||||
|
$(this).css('z-index', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 입고 저장
|
||||||
|
$('#saveManualReceiptBtn').on('click', function() {
|
||||||
|
const supplierId = $('#manualReceiptSupplier').val();
|
||||||
|
const receiptDate = $('#manualReceiptDate').val();
|
||||||
|
const notes = $('#manualReceiptNotes').val();
|
||||||
|
|
||||||
|
if (!supplierId) {
|
||||||
|
alert('도매상을 선택해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!receiptDate) {
|
||||||
|
alert('입고일을 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
let valid = true;
|
||||||
|
$('#manualReceiptLines tr').each(function() {
|
||||||
|
const ingredientCode = $(this).find('.manual-herb-select').val();
|
||||||
|
const qty = parseFloat($(this).find('.manual-qty-input').val()) || 0;
|
||||||
|
const price = parseFloat($(this).find('.manual-price-input').val()) || 0;
|
||||||
|
|
||||||
|
if (!ingredientCode) {
|
||||||
|
valid = false;
|
||||||
|
alert('약재를 선택해주세요.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (qty <= 0) {
|
||||||
|
valid = false;
|
||||||
|
alert('수량을 입력해주세요.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (price <= 0) {
|
||||||
|
valid = false;
|
||||||
|
alert('단가를 입력해주세요.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push({
|
||||||
|
ingredient_code: ingredientCode,
|
||||||
|
quantity_g: qty,
|
||||||
|
unit_price_per_g: price,
|
||||||
|
origin_country: $(this).find('.manual-origin-input').val(),
|
||||||
|
lot_number: $(this).find('.manual-lot-input').val(),
|
||||||
|
expiry_date: $(this).find('.manual-expiry-input').val()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) return;
|
||||||
|
if (lines.length === 0) {
|
||||||
|
alert('입고 품목을 1개 이상 추가해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = $(this);
|
||||||
|
btn.prop('disabled', true).text('저장 중...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/purchase-receipts/manual',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
supplier_id: supplierId,
|
||||||
|
receipt_date: receiptDate,
|
||||||
|
notes: notes,
|
||||||
|
lines: lines
|
||||||
|
}),
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
alert(`수동 입고 완료!\n입고번호: ${response.receipt_no}\n품목 수: ${response.summary.item_count}\n총 금액: ${response.summary.total_amount}`);
|
||||||
|
$('#manualReceiptModal').modal('hide');
|
||||||
|
loadPurchaseReceipts();
|
||||||
|
} else {
|
||||||
|
alert('오류: ' + response.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
const msg = xhr.responseJSON ? xhr.responseJSON.error : '서버 오류가 발생했습니다.';
|
||||||
|
alert('오류: ' + msg);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 입고 저장');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 입고장 업로드
|
// 입고장 업로드
|
||||||
$('#purchaseUploadForm').on('submit', function(e) {
|
$('#purchaseUploadForm').on('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -266,6 +266,9 @@
|
|||||||
<div id="purchase" class="main-content">
|
<div id="purchase" class="main-content">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h3>입고 관리</h3>
|
<h3>입고 관리</h3>
|
||||||
|
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#manualReceiptModal">
|
||||||
|
<i class="bi bi-plus-circle"></i> 수동 입고
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 입고장 목록 -->
|
<!-- 입고장 목록 -->
|
||||||
@ -1838,6 +1841,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 수동 입고 모달 -->
|
||||||
|
<div class="modal fade" id="manualReceiptModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-success text-white">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> 수동 입고</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- 입고 헤더 정보 -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">입고일 <span class="text-danger">*</span></label>
|
||||||
|
<input type="date" class="form-control" id="manualReceiptDate">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">도매상 <span class="text-danger">*</span></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<select class="form-control" id="manualReceiptSupplier">
|
||||||
|
<option value="">도매상을 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="manualReceiptAddSupplierBtn" title="새 도매상 등록">
|
||||||
|
<i class="bi bi-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label">비고</label>
|
||||||
|
<input type="text" class="form-control" id="manualReceiptNotes" placeholder="비고 입력">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 품목 테이블 -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-sm">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:25%">약재명 <span class="text-danger">*</span></th>
|
||||||
|
<th style="width:10%">수량(g) <span class="text-danger">*</span></th>
|
||||||
|
<th style="width:12%">g당 단가 <span class="text-danger">*</span></th>
|
||||||
|
<th style="width:12%">금액</th>
|
||||||
|
<th style="width:10%">원산지</th>
|
||||||
|
<th style="width:12%">로트번호</th>
|
||||||
|
<th style="width:12%">유효기한</th>
|
||||||
|
<th style="width:5%"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="manualReceiptLines">
|
||||||
|
<!-- 동적 행 추가 -->
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="table-warning fw-bold">
|
||||||
|
<td>합계</td>
|
||||||
|
<td id="manualReceiptTotalQty" class="text-end">0</td>
|
||||||
|
<td></td>
|
||||||
|
<td id="manualReceiptTotalAmount" class="text-end">0</td>
|
||||||
|
<td colspan="4"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm" id="addManualReceiptLineBtn">
|
||||||
|
<i class="bi bi-plus"></i> 품목 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||||
|
<button type="button" class="btn btn-success" id="saveManualReceiptBtn">
|
||||||
|
<i class="bi bi-check-circle"></i> 입고 저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 로트 배분 모달 -->
|
<!-- 로트 배분 모달 -->
|
||||||
<div class="modal fade" id="lotAllocationModal" tabindex="-1">
|
<div class="modal fade" id="lotAllocationModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user