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:
|
||||
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 ====================
|
||||
|
||||
@app.route('/api/herbs', methods=['GET'])
|
||||
@ -710,6 +744,11 @@ def upload_purchase_excel():
|
||||
# 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)
|
||||
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,
|
||||
unit_price_per_g, quantity_received, quantity_onhand)
|
||||
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))
|
||||
lot_id = cursor.lastrowid
|
||||
|
||||
@ -870,6 +909,139 @@ def upload_purchase_excel():
|
||||
except Exception as e:
|
||||
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 ====================
|
||||
|
||||
@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 => {
|
||||
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();
|
||||
|
||||
@ -266,6 +266,9 @@
|
||||
<div id="purchase" class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3>입고 관리</h3>
|
||||
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#manualReceiptModal">
|
||||
<i class="bi bi-plus-circle"></i> 수동 입고
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 입고장 목록 -->
|
||||
@ -1838,6 +1841,81 @@
|
||||
</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-dialog modal-lg">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user