405 lines
14 KiB
JavaScript
405 lines
14 KiB
JavaScript
// 成品入库管理
|
||
(() => {
|
||
let receiptList = [];
|
||
let outsourcingOrders = [];
|
||
let currentPage = 1;
|
||
const pageSize = 20;
|
||
|
||
Router.register('/outsourcing-mgmt/finished-goods-receipt', async () => {
|
||
const html = `
|
||
<style>
|
||
#receipt-page .page-header {
|
||
padding: 20px;
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
#receipt-page .content-area {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#receipt-page .table-wrapper {
|
||
flex: 1;
|
||
overflow: auto;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
#receipt-page table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 0;
|
||
}
|
||
|
||
#receipt-page thead {
|
||
background: var(--border);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
#receipt-page th {
|
||
padding: 8px 12px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
border-bottom: 2px solid var(--border);
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#receipt-page td {
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
}
|
||
|
||
#receipt-page tbody tr:hover {
|
||
background: var(--hover);
|
||
}
|
||
</style>
|
||
|
||
<div id="receipt-page">
|
||
<div class="page-header">
|
||
<h1 style="margin: 0; font-size: 24px;">成品入库管理</h1>
|
||
<div class="page-actions" style="margin-top: 16px;">
|
||
<button id="add-receipt-btn" class="btn btn-primary">新增入库单</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-area">
|
||
<div class="filter-section" style="padding: 20px; display: flex; gap: 15px; align-items: center;">
|
||
<input type="text" id="search-keyword" class="input" placeholder="搜索入库单号、委外工单号或产品" style="flex: 1; max-width: 400px;" />
|
||
<button class="btn btn-secondary" onclick="FinishedGoodsReceipt.search()">搜索</button>
|
||
<button class="btn btn-secondary" onclick="FinishedGoodsReceipt.resetSearch()">重置</button>
|
||
</div>
|
||
|
||
<div class="table-wrapper">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>入库单号</th>
|
||
<th>委外工单号</th>
|
||
<th>产品编码</th>
|
||
<th>产品名称</th>
|
||
<th>入库数量</th>
|
||
<th>入库日期</th>
|
||
<th>创建人</th>
|
||
<th>创建时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="receipt-list">
|
||
<tr><td colspan="8" class="text-center">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="pagination" id="pagination" style="padding: 20px; display: flex; justify-content: center; gap: 8px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增入库单弹窗 -->
|
||
<div id="receipt-modal" class="modal" style="display:none;">
|
||
<div class="modal-content" style="max-width: 700px;">
|
||
<div class="modal-header">
|
||
<h2>新增成品入库单</h2>
|
||
<button class="modal-close" onclick="FinishedGoodsReceipt.closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="field">
|
||
<label>关联委外工单号 <span style="color: var(--danger);">*</span></label>
|
||
<select id="outsourcing-order-no" class="input" onchange="FinishedGoodsReceipt.loadOrderInfo()">
|
||
<option value="">请选择委外工单</option>
|
||
</select>
|
||
</div>
|
||
<div id="order-info" style="display: none; padding: 15px; background: var(--surface); border-radius: 8px; margin-bottom: 15px;">
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||
<div><strong>产品编码:</strong><span id="info-product-code"></span></div>
|
||
<div><strong>产品名称:</strong><span id="info-product-name"></span></div>
|
||
<div><strong>生产数量:</strong><span id="info-production-qty"></span></div>
|
||
<div><strong>外协厂:</strong><span id="info-factory"></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>入库数量 <span style="color: var(--danger);">*</span></label>
|
||
<input type="number" id="receipt-qty" class="input" min="1" value="1" placeholder="输入实际收到的整机数量" />
|
||
</div>
|
||
<div class="field">
|
||
<label>入库日期 <span style="color: var(--danger);">*</span></label>
|
||
<input type="date" id="receipt-date" class="input" />
|
||
</div>
|
||
<div id="consumption-preview" style="display: none; margin-top: 20px;">
|
||
<h3 style="margin-bottom: 10px; color: var(--warning);">预计物料消耗(将从委外在制库存扣减)</h3>
|
||
<div class="table-wrapper" style="max-height: 300px; overflow: auto;">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>物料编码</th>
|
||
<th>物料名称</th>
|
||
<th>单机用量</th>
|
||
<th>预计消耗数量</th>
|
||
<th>单位</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="consumption-list">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="FinishedGoodsReceipt.closeModal()">取消</button>
|
||
<button class="btn btn-primary" onclick="FinishedGoodsReceipt.save()">确认入库</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
setTimeout(() => {
|
||
document.getElementById('add-receipt-btn')?.addEventListener('click', () => openModal());
|
||
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') search();
|
||
});
|
||
document.getElementById('receipt-qty')?.addEventListener('input', () => updateConsumptionPreview());
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const dateInput = document.getElementById('receipt-date');
|
||
if (dateInput) dateInput.value = today;
|
||
loadList();
|
||
loadOutsourcingOrders();
|
||
}, 0);
|
||
|
||
return html;
|
||
});
|
||
|
||
async function loadList() {
|
||
try {
|
||
const res = await API.get('/api/finished-goods-receipt');
|
||
receiptList = res.list || [];
|
||
renderList();
|
||
} catch (e) {
|
||
console.error('加载入库列表失败:', e);
|
||
document.getElementById('receipt-list').innerHTML = '<tr><td colspan="8" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
|
||
}
|
||
}
|
||
|
||
async function loadOutsourcingOrders() {
|
||
try {
|
||
const res = await API.get('/api/outsourcing-orders');
|
||
outsourcingOrders = res.list || [];
|
||
const select = document.getElementById('outsourcing-order-no');
|
||
if (select) {
|
||
select.innerHTML = '<option value="">请选择委外工单</option>' +
|
||
outsourcingOrders.map(o => `<option value="${escapeHtml(o.order_no)}">${escapeHtml(o.order_no)} - ${escapeHtml(o.product_name)} (${o.production_qty}台)</option>`).join('');
|
||
}
|
||
} catch (e) {
|
||
console.error('加载委外工单失败:', e);
|
||
}
|
||
}
|
||
|
||
let currentOrderMaterials = [];
|
||
|
||
async function loadOrderInfo() {
|
||
const orderNo = document.getElementById('outsourcing-order-no').value;
|
||
if (!orderNo) {
|
||
document.getElementById('order-info').style.display = 'none';
|
||
document.getElementById('consumption-preview').style.display = 'none';
|
||
currentOrderMaterials = [];
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const order = outsourcingOrders.find(o => o.order_no === orderNo);
|
||
if (!order) return;
|
||
|
||
document.getElementById('info-product-code').textContent = order.product_code || '';
|
||
document.getElementById('info-product-name').textContent = order.product_name || '';
|
||
document.getElementById('info-production-qty').textContent = order.production_qty || 0;
|
||
document.getElementById('info-factory').textContent = order.outsourcing_factory || '';
|
||
document.getElementById('order-info').style.display = 'block';
|
||
|
||
const res = await API.get(`/api/outsourcing-orders/${order.id}`);
|
||
currentOrderMaterials = res.materials || [];
|
||
|
||
updateConsumptionPreview();
|
||
} catch (e) {
|
||
console.error('加载工单信息失败:', e);
|
||
alert('加载工单信息失败');
|
||
}
|
||
}
|
||
|
||
function updateConsumptionPreview() {
|
||
const receiptQty = parseInt(document.getElementById('receipt-qty')?.value) || 0;
|
||
|
||
if (receiptQty <= 0 || currentOrderMaterials.length === 0) {
|
||
document.getElementById('consumption-preview').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
document.getElementById('consumption-preview').style.display = 'block';
|
||
document.getElementById('consumption-list').innerHTML = currentOrderMaterials.map(m => {
|
||
const consumedQty = Math.ceil(m.bom_unit_qty * receiptQty);
|
||
return `
|
||
<tr>
|
||
<td>${escapeHtml(m.material_code)}</td>
|
||
<td>${escapeHtml(m.material_name)}</td>
|
||
<td>${m.bom_unit_qty}</td>
|
||
<td style="font-weight: 600; color: var(--warning);">${consumedQty}</td>
|
||
<td>${escapeHtml(m.unit)}</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderList() {
|
||
const tbody = document.getElementById('receipt-list');
|
||
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
|
||
|
||
let filtered = receiptList;
|
||
if (keyword) {
|
||
filtered = receiptList.filter(item =>
|
||
(item.receipt_no || '').toLowerCase().includes(keyword) ||
|
||
(item.outsourcing_order_no || '').toLowerCase().includes(keyword) ||
|
||
(item.product_code || '').toLowerCase().includes(keyword) ||
|
||
(item.product_name || '').toLowerCase().includes(keyword)
|
||
);
|
||
}
|
||
|
||
const totalPages = Math.ceil(filtered.length / pageSize);
|
||
const start = (currentPage - 1) * pageSize;
|
||
const pageData = filtered.slice(start, start + pageSize);
|
||
|
||
if (pageData.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="text-center">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = pageData.map(item => `
|
||
<tr>
|
||
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.receipt_no || '')}</span></td>
|
||
<td>${escapeHtml(item.outsourcing_order_no || '')}</td>
|
||
<td>${escapeHtml(item.product_code || '')}</td>
|
||
<td>${escapeHtml(item.product_name || '')}</td>
|
||
<td style="font-weight: 600; color: var(--success);">${item.receipt_qty || 0}</td>
|
||
<td>${escapeHtml(item.receipt_date || '')}</td>
|
||
<td>${escapeHtml(item.created_by || '')}</td>
|
||
<td>${formatTime(item.created_at)}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
renderPagination(totalPages);
|
||
}
|
||
|
||
function renderPagination(totalPages) {
|
||
const container = document.getElementById('pagination');
|
||
if (totalPages <= 1) {
|
||
container.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="FinishedGoodsReceipt.goPage(${currentPage - 1})">上一页</button>`;
|
||
html += `<span style="padding: 0 12px; line-height: 32px;">第 ${currentPage} / ${totalPages} 页</span>`;
|
||
html += `<button class="btn btn-sm btn-secondary" ${currentPage >= totalPages ? 'disabled' : ''} onclick="FinishedGoodsReceipt.goPage(${currentPage + 1})">下一页</button>`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function openModal() {
|
||
document.getElementById('outsourcing-order-no').value = '';
|
||
document.getElementById('receipt-qty').value = 1;
|
||
const today = new Date().toISOString().split('T')[0];
|
||
document.getElementById('receipt-date').value = today;
|
||
document.getElementById('order-info').style.display = 'none';
|
||
document.getElementById('consumption-preview').style.display = 'none';
|
||
currentOrderMaterials = [];
|
||
document.getElementById('receipt-modal').style.display = 'flex';
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('receipt-modal').style.display = 'none';
|
||
}
|
||
|
||
async function save() {
|
||
const outsourcing_order_no = document.getElementById('outsourcing-order-no').value.trim();
|
||
const receipt_qty = parseInt(document.getElementById('receipt-qty').value) || 0;
|
||
const receipt_date = document.getElementById('receipt-date').value.trim();
|
||
|
||
if (!outsourcing_order_no || !receipt_date) {
|
||
alert('请填写所有必填字段');
|
||
return;
|
||
}
|
||
|
||
if (receipt_qty <= 0) {
|
||
alert('入库数量必须大于0');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await API.post('/api/finished-goods-receipt', {
|
||
outsourcing_order_no,
|
||
receipt_qty,
|
||
receipt_date
|
||
});
|
||
|
||
let message = `入库成功,入库单号:${res.receipt_no}`;
|
||
if (res.material_consumption && res.material_consumption.length > 0) {
|
||
message += '\n\n物料消耗明细:';
|
||
res.material_consumption.forEach(m => {
|
||
message += `\n${m.material_name}: ${m.consumed_qty}`;
|
||
});
|
||
}
|
||
|
||
alert(message);
|
||
closeModal();
|
||
loadList();
|
||
} catch (e) {
|
||
alert(e.message || '入库失败');
|
||
}
|
||
}
|
||
|
||
function search() {
|
||
currentPage = 1;
|
||
renderList();
|
||
}
|
||
|
||
function resetSearch() {
|
||
document.getElementById('search-keyword').value = '';
|
||
currentPage = 1;
|
||
renderList();
|
||
}
|
||
|
||
function goPage(page) {
|
||
currentPage = page;
|
||
renderList();
|
||
}
|
||
|
||
function formatTime(ts) {
|
||
if (!ts) return '-';
|
||
try {
|
||
const d = new Date(ts);
|
||
return d.toLocaleString('zh-CN', { hour12: false });
|
||
} catch {
|
||
return ts;
|
||
}
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
window.FinishedGoodsReceipt = {
|
||
search,
|
||
resetSearch,
|
||
closeModal,
|
||
save,
|
||
goPage,
|
||
loadOrderInfo
|
||
};
|
||
})();
|