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:
212
static/app.js
212
static/app.js
@@ -2028,6 +2028,13 @@ $(document).ready(function() {
|
||||
response.data.forEach(supplier => {
|
||||
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) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user