增加委外管理文件文件

This commit is contained in:
zzh 2026-03-13 10:48:59 +08:00
parent a04c222f0a
commit 2f87fda0de
16 changed files with 2256 additions and 0 deletions

22
frontend/assets/xlsx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,404 @@
// 成品入库管理
(() => {
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()">&times;</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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.FinishedGoodsReceipt = {
search,
resetSearch,
closeModal,
save,
goPage,
loadOrderInfo
};
})();

View File

@ -0,0 +1,384 @@
// 委外发料管理
(() => {
let issueList = [];
let outsourcingOrders = [];
let currentPage = 1;
const pageSize = 20;
Router.register('/outsourcing-mgmt/material-issue', async () => {
const html = `
<style>
#material-issue-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#material-issue-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#material-issue-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#material-issue-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#material-issue-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#material-issue-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;
}
#material-issue-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#material-issue-page tbody tr:hover {
background: var(--hover);
}
</style>
<div id="material-issue-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">委外发料管理</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="add-issue-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="MaterialIssue.search()">搜索</button>
<button class="btn btn-secondary" onclick="MaterialIssue.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>
<th>创建时间</th>
</tr>
</thead>
<tbody id="issue-list">
<tr><td colspan="9" 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="issue-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2>新增委外发料单</h2>
<button class="modal-close" onclick="MaterialIssue.closeModal()">&times;</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="MaterialIssue.loadOrderMaterials()">
<option value="">请选择委外工单</option>
</select>
</div>
<div class="field">
<label>发料日期 <span style="color: var(--danger);">*</span></label>
<input type="date" id="issue-date" class="input" />
</div>
<div id="material-section" style="display: none; margin-top: 20px;">
<h3 style="margin-bottom: 10px;">发料明细</h3>
<div class="table-wrapper" style="max-height: 400px; overflow: auto;">
<table>
<thead>
<tr>
<th>物料编码</th>
<th>物料名称</th>
<th>BOM用量</th>
<th>需发数量</th>
<th>实际发料数量 <span style="color: var(--danger);">*</span></th>
<th>单位</th>
</tr>
</thead>
<tbody id="material-list">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="MaterialIssue.closeModal()">取消</button>
<button class="btn btn-primary" onclick="MaterialIssue.save()">确认发料</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('add-issue-btn')?.addEventListener('click', () => openModal());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') search();
});
const today = new Date().toISOString().split('T')[0];
const dateInput = document.getElementById('issue-date');
if (dateInput) dateInput.value = today;
loadList();
loadOutsourcingOrders();
}, 0);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/outsourcing-material-issue');
issueList = res.list || [];
renderList();
} catch (e) {
console.error('加载发料列表失败:', e);
document.getElementById('issue-list').innerHTML = '<tr><td colspan="9" 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);
}
}
async function loadOrderMaterials() {
const orderNo = document.getElementById('outsourcing-order-no').value;
if (!orderNo) {
document.getElementById('material-section').style.display = 'none';
return;
}
try {
const order = outsourcingOrders.find(o => o.order_no === orderNo);
if (!order) return;
const res = await API.get(`/api/outsourcing-orders/${order.id}`);
const materials = res.materials || [];
if (materials.length > 0) {
document.getElementById('material-section').style.display = 'block';
document.getElementById('material-list').innerHTML = materials.map((m, idx) => `
<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(--info);">${m.need_qty}</td>
<td>
<input type="number"
id="issue-qty-${idx}"
class="input"
min="0"
value="${m.need_qty}"
style="width: 100px;"
data-material-code="${escapeHtml(m.material_code)}"
data-material-name="${escapeHtml(m.material_name)}"
data-unit="${escapeHtml(m.unit)}" />
</td>
<td>${escapeHtml(m.unit)}</td>
</tr>
`).join('');
}
} catch (e) {
console.error('加载物料明细失败:', e);
alert('加载物料明细失败');
}
}
function renderList() {
const tbody = document.getElementById('issue-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = issueList;
if (keyword) {
filtered = issueList.filter(item =>
(item.issue_no || '').toLowerCase().includes(keyword) ||
(item.outsourcing_order_no || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_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="9" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.issue_no || '')}</span></td>
<td>${escapeHtml(item.outsourcing_order_no || '')}</td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td style="font-weight: 600; color: var(--primary);">${item.issue_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${escapeHtml(item.issue_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="MaterialIssue.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="MaterialIssue.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openModal() {
document.getElementById('outsourcing-order-no').value = '';
const today = new Date().toISOString().split('T')[0];
document.getElementById('issue-date').value = today;
document.getElementById('material-section').style.display = 'none';
document.getElementById('issue-modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('issue-modal').style.display = 'none';
}
async function save() {
const outsourcing_order_no = document.getElementById('outsourcing-order-no').value.trim();
const issue_date = document.getElementById('issue-date').value.trim();
if (!outsourcing_order_no || !issue_date) {
alert('请填写所有必填字段');
return;
}
const materials = [];
const inputs = document.querySelectorAll('[id^="issue-qty-"]');
inputs.forEach(input => {
const qty = parseInt(input.value) || 0;
if (qty > 0) {
materials.push({
material_code: input.dataset.materialCode,
material_name: input.dataset.materialName,
issue_qty: qty,
unit: input.dataset.unit
});
}
});
if (materials.length === 0) {
alert('请至少填写一条发料数量');
return;
}
try {
const res = await API.post('/api/outsourcing-material-issue', {
outsourcing_order_no,
materials,
issue_date
});
alert(`发料成功,发料单号:${res.issue_no}`);
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.MaterialIssue = {
search,
resetSearch,
closeModal,
save,
goPage,
loadOrderMaterials
};
})();

View File

@ -0,0 +1,429 @@
// 委外工单管理
(() => {
let orderList = [];
let customerOrders = [];
let currentPage = 1;
const pageSize = 20;
const statusMap = {
'pending': { text: '待发料', color: 'var(--warning)' },
'issued': { text: '已发料', color: 'var(--info)' },
'completed': { text: '已完成', color: 'var(--success)' },
'cancelled': { text: '已取消', color: 'var(--text-2)' }
};
Router.register('/outsourcing-mgmt/orders', async () => {
const html = `
<style>
#outsourcing-orders-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#outsourcing-orders-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#outsourcing-orders-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#outsourcing-orders-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#outsourcing-orders-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#outsourcing-orders-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;
}
#outsourcing-orders-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#outsourcing-orders-page tbody tr:hover {
background: var(--hover);
}
#outsourcing-orders-page .btn-text {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
padding: 4px 8px;
margin-right: 8px;
}
#outsourcing-orders-page .btn-text:hover {
text-decoration: underline;
}
</style>
<div id="outsourcing-orders-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">委外工单管理</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="add-order-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="OutsourcingOrders.search()">搜索</button>
<button class="btn btn-secondary" onclick="OutsourcingOrders.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>
<th>创建人</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="order-list">
<tr><td colspan="11" 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="order-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h2 id="modal-title">新增委外工单</h2>
<button class="modal-close" onclick="OutsourcingOrders.closeModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>客户订单号 <span style="color: var(--danger);">*</span></label>
<select id="customer-order-no" class="input">
<option value="">请选择客户订单</option>
</select>
</div>
<div class="field">
<label>成品编码 <span style="color: var(--danger);">*</span></label>
<input type="text" id="product-code" class="input" placeholder="输入成品编码" />
</div>
<div class="field">
<label>成品名称 <span style="color: var(--danger);">*</span></label>
<input type="text" id="product-name" class="input" placeholder="输入成品名称" />
</div>
<div class="field">
<label>生产数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="production-qty" class="input" min="1" value="1" placeholder="输入生产数量" />
</div>
<div class="field">
<label>外协厂 <span style="color: var(--danger);">*</span></label>
<input type="text" id="outsourcing-factory" class="input" placeholder="输入外协厂名称" />
</div>
<div class="field">
<label>交期 <span style="color: var(--danger);">*</span></label>
<input type="date" id="delivery-date" class="input" />
</div>
<div id="material-list-section" style="display: none; margin-top: 20px;">
<h3 style="margin-bottom: 10px;">物料明细根据BOM自动计算</h3>
<div class="table-wrapper" style="max-height: 300px; overflow: auto;">
<table>
<thead>
<tr>
<th>物料编码</th>
<th>物料名称</th>
<th>BOM用量</th>
<th>需发数量</th>
<th>单位</th>
</tr>
</thead>
<tbody id="material-list">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="OutsourcingOrders.closeModal()">取消</button>
<button class="btn btn-primary" onclick="OutsourcingOrders.save()">保存</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('add-order-btn')?.addEventListener('click', () => openModal());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') search();
});
loadList();
loadCustomerOrders();
}, 0);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/outsourcing-orders');
orderList = res.list || [];
renderList();
} catch (e) {
console.error('加载委外工单失败:', e);
document.getElementById('order-list').innerHTML = '<tr><td colspan="11" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
async function loadCustomerOrders() {
try {
const res = await API.get('/api/customer-orders-for-outsourcing');
customerOrders = res.list || [];
const select = document.getElementById('customer-order-no');
if (select) {
select.innerHTML = '<option value="">请选择客户订单</option>' +
customerOrders.map(o => `<option value="${escapeHtml(o.order_no)}">${escapeHtml(o.order_no)} - ${escapeHtml(o.customer_name)} - ${escapeHtml(o.material)}</option>`).join('');
}
} catch (e) {
console.error('加载客户订单失败:', e);
}
}
function renderList() {
const tbody = document.getElementById('order-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = orderList;
if (keyword) {
filtered = orderList.filter(item =>
(item.order_no || '').toLowerCase().includes(keyword) ||
(item.customer_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="11" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => {
const status = statusMap[item.status] || { text: item.status, color: 'var(--text-2)' };
return `
<tr>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.order_no || '')}</span></td>
<td>${escapeHtml(item.customer_order_no || '')}</td>
<td>${escapeHtml(item.product_code || '')}</td>
<td>${escapeHtml(item.product_name || '')}</td>
<td style="font-weight: 600; color: var(--primary);">${item.production_qty || 0}</td>
<td>${escapeHtml(item.outsourcing_factory || '')}</td>
<td>${escapeHtml(item.delivery_date || '')}</td>
<td><span style="padding: 2px 8px; border-radius: 4px; background: ${status.color}20; color: ${status.color}; font-size: 12px;">${status.text}</span></td>
<td>${escapeHtml(item.created_by || '')}</td>
<td>${formatTime(item.created_at)}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="OutsourcingOrders.viewDetail(${item.id})">查看</button>
<button class="btn btn-sm btn-danger" onclick="OutsourcingOrders.deleteOrder(${item.id})">删除</button>
</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="OutsourcingOrders.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="OutsourcingOrders.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
let editingId = null;
function openModal(item = null) {
editingId = item?.id || null;
document.getElementById('modal-title').textContent = item ? '编辑委外工单' : '新增委外工单';
document.getElementById('customer-order-no').value = item?.customer_order_no || '';
document.getElementById('product-code').value = item?.product_code || '';
document.getElementById('product-name').value = item?.product_name || '';
document.getElementById('production-qty').value = item?.production_qty || 1;
document.getElementById('outsourcing-factory').value = item?.outsourcing_factory || '';
document.getElementById('delivery-date').value = item?.delivery_date || '';
document.getElementById('material-list-section').style.display = 'none';
document.getElementById('order-modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('order-modal').style.display = 'none';
editingId = null;
}
async function save() {
const data = {
customer_order_no: document.getElementById('customer-order-no').value.trim(),
product_code: document.getElementById('product-code').value.trim(),
product_name: document.getElementById('product-name').value.trim(),
production_qty: parseInt(document.getElementById('production-qty').value) || 0,
outsourcing_factory: document.getElementById('outsourcing-factory').value.trim(),
delivery_date: document.getElementById('delivery-date').value.trim()
};
if (!data.customer_order_no || !data.product_code || !data.product_name || !data.outsourcing_factory || !data.delivery_date) {
alert('请填写所有必填字段');
return;
}
if (data.production_qty <= 0) {
alert('生产数量必须大于0');
return;
}
try {
if (editingId) {
await API.put(`/api/outsourcing-orders/${editingId}`, data);
alert('更新成功');
} else {
const res = await API.post('/api/outsourcing-orders', data);
alert(`委外工单创建成功,工单号:${res.order_no}`);
}
closeModal();
loadList();
} catch (e) {
alert(e.message || '操作失败');
}
}
async function viewDetail(id) {
try {
const res = await API.get(`/api/outsourcing-orders/${id}`);
const order = res.order;
const materials = res.materials || [];
document.getElementById('modal-title').textContent = '查看委外工单';
document.getElementById('customer-order-no').value = order.customer_order_no || '';
document.getElementById('product-code').value = order.product_code || '';
document.getElementById('product-name').value = order.product_name || '';
document.getElementById('production-qty').value = order.production_qty || 0;
document.getElementById('outsourcing-factory').value = order.outsourcing_factory || '';
document.getElementById('delivery-date').value = order.delivery_date || '';
if (materials.length > 0) {
document.getElementById('material-list-section').style.display = 'block';
document.getElementById('material-list').innerHTML = materials.map(m => `
<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(--primary);">${m.need_qty}</td>
<td>${escapeHtml(m.unit)}</td>
</tr>
`).join('');
}
document.getElementById('order-modal').style.display = 'flex';
} catch (e) {
alert(e.message || '获取详情失败');
}
}
async function deleteOrder(id) {
if (!confirm('确定要删除这条委外工单吗?')) return;
try {
await API.delete(`/api/outsourcing-orders/${id}`);
alert('删除成功');
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.OutsourcingOrders = {
search,
resetSearch,
viewDetail,
deleteOrder,
closeModal,
save,
goPage
};
})();

View File

@ -0,0 +1,263 @@
// 委外在制库存查询
(() => {
let wipStockList = [];
let currentPage = 1;
const pageSize = 20;
Router.register('/outsourcing-mgmt/wip-stock', async () => {
const html = `
<style>
#wip-stock-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#wip-stock-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#wip-stock-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#wip-stock-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#wip-stock-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#wip-stock-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;
}
#wip-stock-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#wip-stock-page tbody tr:hover {
background: var(--hover);
}
#wip-stock-page .summary-card {
background: linear-gradient(135deg, var(--primary) 0%, var(--info) 100%);
color: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
justify-content: space-around;
align-items: center;
}
#wip-stock-page .summary-item {
text-align: center;
}
#wip-stock-page .summary-item .label {
font-size: 14px;
opacity: 0.9;
margin-bottom: 5px;
}
#wip-stock-page .summary-item .value {
font-size: 32px;
font-weight: bold;
}
</style>
<div id="wip-stock-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">委外在制库存</h1>
<p style="margin: 8px 0 0 0; color: var(--text-2); font-size: 14px;">
显示已发给外协厂但尚未完工入库的物料库存
</p>
</div>
<div class="content-area">
<div class="summary-card">
<div class="summary-item">
<div class="label">物料种类</div>
<div class="value" id="material-count">0</div>
</div>
<div class="summary-item">
<div class="label">在制总数量</div>
<div class="value" id="total-wip-qty">0</div>
</div>
</div>
<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="OutsourcingWipStock.search()">搜索</button>
<button class="btn btn-secondary" onclick="OutsourcingWipStock.resetSearch()">重置</button>
<button class="btn btn-primary" onclick="OutsourcingWipStock.loadList()">刷新</button>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>委外工单号</th>
<th>物料编码</th>
<th>物料名称</th>
<th>在制数量</th>
<th>单位</th>
<th>更新时间</th>
</tr>
</thead>
<tbody id="wip-stock-list">
<tr><td colspan="6" 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>
`;
setTimeout(() => {
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') search();
});
loadList();
}, 0);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/outsourcing-wip-stock');
wipStockList = res.list || [];
updateSummary();
renderList();
} catch (e) {
console.error('加载委外在制库存失败:', e);
document.getElementById('wip-stock-list').innerHTML = '<tr><td colspan="6" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
function updateSummary() {
const materialCount = wipStockList.length;
const totalWipQty = wipStockList.reduce((sum, item) => sum + (item.wip_qty || 0), 0);
const materialCountEl = document.getElementById('material-count');
const totalWipQtyEl = document.getElementById('total-wip-qty');
if (materialCountEl) materialCountEl.textContent = materialCount;
if (totalWipQtyEl) totalWipQtyEl.textContent = totalWipQty;
}
function renderList() {
const tbody = document.getElementById('wip-stock-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = wipStockList;
if (keyword) {
filtered = wipStockList.filter(item =>
(item.outsourcing_order_no || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_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="6" class="text-center">暂无在制库存</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.outsourcing_order_no || '')}</span></td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td style="font-weight: 600; color: var(--warning);">${item.wip_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${formatTime(item.updated_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="OutsourcingWipStock.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="OutsourcingWipStock.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.OutsourcingWipStock = {
search,
resetSearch,
goPage,
loadList
};
})();

View File

@ -0,0 +1,167 @@
Router.register('/shipments/audit', async () => {
// 获取当前用户信息
let userRole = null;
try {
const userRes = await fetch('/api/auth/me', { credentials: 'include' });
const userData = await userRes.json();
userRole = userData.role;
} catch (e) {
console.error('Failed to get user info:', e);
}
setTimeout(async () => {
const queryTypeSelect = document.getElementById('query-type');
const queryBtn = document.getElementById('query-btn');
const queryInput = document.getElementById('query-input');
const inputLabel = document.getElementById('input-label');
const resultDiv = document.getElementById('query-result');
const platformSelect = document.getElementById('platform-select');
// 切换查询类型时更新输入框提示
queryTypeSelect?.addEventListener('change', (e) => {
const queryType = e.target.value;
if (queryType === 'mac') {
inputLabel.textContent = '输入 MAC 地址';
queryInput.placeholder = '可输入带或不带冒号的MAC地址001122AABBCC 或 00:11:22:AA:BB:CC';
} else if (queryType === 'batch') {
inputLabel.textContent = '输入批次号';
queryInput.placeholder = '输入批次号';
}
resultDiv.innerHTML = '';
});
const performQuery = async () => {
const queryType = queryTypeSelect?.value || 'mac';
let queryValue = queryInput?.value?.trim();
const platform = platformSelect?.value || 'pdd';
if (!queryValue) {
resultDiv.innerHTML = `<div class="error">请输入${queryType === 'mac' ? 'MAC 地址' : '批次号'}</div>`;
return;
}
// 如果是MAC地址查询自动格式化添加冒号
if (queryType === 'mac') {
// 移除所有非十六进制字符
queryValue = queryValue.replace(/[^a-fA-F0-9]/g, '');
// 如果是12位十六进制自动格式化为 XX:XX:XX:XX:XX:XX
if (queryValue.length === 12) {
queryValue = queryValue.match(/.{2}/g).join(':').toUpperCase();
}
// 如果是其他长度,尝试格式化
else if (queryValue.length > 0) {
// 统一转为大写
queryValue = queryValue.toUpperCase();
// 如果已经包含冒号,保持原样
if (!queryValue.includes(':')) {
// 尝试每两个字符添加冒号
queryValue = queryValue.match(/.{2}/g)?.join(':') || queryValue;
}
}
}
try {
resultDiv.innerHTML = '<div>查询中...</div>';
// 根据选择的平台查询审计数据
const res = await fetch(`/api/audit/${platform}/search?${queryType}=${encodeURIComponent(queryValue)}`, {
credentials: 'include'
});
const data = await res.json();
if (data.found && data.records && data.records.length > 0) {
const recordsHtml = data.records.map((record, index) => `
<div style="padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:6px;margin-bottom:8px">
<div style="font-weight:600;margin-bottom:8px;color:var(--primary)">记录 ${index + 1}</div>
<div class="result-item"><span class="label">MAC地址:</span> ${record.mac || ''}</div>
<div class="result-item"><span class="label">批次号:</span> ${record.batch || ''}</div>
<div class="result-item"><span class="label">审计时间:</span> ${record.ts_cn || ''}</div>
<div class="result-item"><span class="label">备注:</span> ${record.note || ''}</div>
</div>
`).join('');
resultDiv.innerHTML = `
<div class="result-card success">
<div class="result-title"> 找到 ${data.records.length} 条审计记录</div>
<div class="result-item"><span class="label">查询平台:</span> <span class="badge">${platform.toUpperCase()}</span></div>
<div class="result-item"><span class="label">查询类型:</span> ${queryType === 'mac' ? 'MAC' : ''}</div>
<div class="result-item"><span class="label">查询值:</span> ${queryValue}</div>
${queryType === 'mac' && queryInput?.value?.trim() !== queryValue ?
`<div class="result-item"><span class="label">原始输入:</span> ${queryInput?.value?.trim()}</div>` : ''
}
<div style="margin-top:16px;max-height:60vh;overflow-y:auto;padding-right:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg)">
${recordsHtml}
</div>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="result-card error">
<div class="result-title"> 未找到审计记录</div>
<div class="result-item">查询平台: ${platform.toUpperCase()}</div>
<div class="result-item">查询类型: ${queryType === 'mac' ? 'MAC地址' : '批次号'}</div>
<div class="result-item">查询值: ${queryValue}</div>
${queryType === 'mac' && queryInput?.value?.trim() !== queryValue ?
`<div class="result-item">原始输入: ${queryInput?.value?.trim()}</div>` : ''
}
<div class="result-item">${data.message || '该查询值没有对应的审计记录'}</div>
</div>
`;
}
} catch (e) {
resultDiv.innerHTML = `<div class="error">查询失败:${e.message}</div>`;
}
};
queryBtn?.addEventListener('click', performQuery);
queryInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performQuery();
}
});
}, 0);
return `<div class="card">
<div style="font-weight:600;margin-bottom:16px">审计查询</div>
<div style="margin-bottom:16px;padding:12px;background:var(--surface);border:1px solid var(--border);border-radius:8px">
<div style="color:var(--text-2);font-size:14px">
<strong>说明</strong> Redis MAC
</div>
</div>
<div style="margin-bottom:16px">
<div class="field">
<label>选择平台</label>
<select id="platform-select" class="input">
<option value="pdd">拼多多</option>
<option value="yt">圆通</option>
<option value="tx">兔喜</option>
<option value="mt">美团</option>
</select>
</div>
<div class="field">
<label>查询类型</label>
<select id="query-type" class="input">
<option value="mac"> MAC 地址查询</option>
<option value="batch">按批次号查询</option>
</select>
</div>
<div class="field">
<label id="input-label">输入 MAC 地址</label>
<input
id="query-input"
class="input"
placeholder="可输入带或不带冒号的MAC地址001122AABBCC 或 00:11:22:AA:BB:CC"
style="font-family: monospace"
/>
</div>
<button class="btn btn-primary" id="query-btn">查询</button>
</div>
<div id="query-result"></div>
</div>`;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

37
test-xlsx.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>测试XLSX库</title>
</head>
<body>
<h1>XLSX库测试</h1>
<input type="file" id="file-input" accept=".xlsx,.xls">
<div id="result"></div>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script>
document.getElementById('file-input').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
console.log('XLSX typeof:', typeof XLSX);
console.log('XLSX version:', XLSX.version);
const reader = new FileReader();
reader.onload = function(e) {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
document.getElementById('result').innerHTML = `
<h2>解析结果:</h2>
<pre>${JSON.stringify(jsonData, null, 2)}</pre>
`;
};
reader.readAsArrayBuffer(file);
});
</script>
</body>
</html>

232
test_outsourcing_system.py Normal file
View File

@ -0,0 +1,232 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
委外管理系统测试脚本
用于验证委外工单发料入库等功能是否正常工作
"""
import sys
import os
# 添加server目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'server'))
from app import init_db, get_db
def test_database_tables():
"""测试数据库表是否正确创建"""
print("=" * 60)
print("测试1: 检查数据库表结构")
print("=" * 60)
# 初始化数据库
init_db()
conn = get_db()
c = conn.cursor()
# 检查委外工单表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='outsourcing_orders'")
if c.fetchone():
print("✓ 委外工单表 (outsourcing_orders) 创建成功")
else:
print("✗ 委外工单表创建失败")
return False
# 检查委外发料表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='outsourcing_material_issue'")
if c.fetchone():
print("✓ 委外发料表 (outsourcing_material_issue) 创建成功")
else:
print("✗ 委外发料表创建失败")
return False
# 检查委外在制库存表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='outsourcing_wip_stock'")
if c.fetchone():
print("✓ 委外在制库存表 (outsourcing_wip_stock) 创建成功")
else:
print("✗ 委外在制库存表创建失败")
return False
# 检查成品入库表
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='finished_goods_receipt'")
if c.fetchone():
print("✓ 成品入库表 (finished_goods_receipt) 创建成功")
else:
print("✗ 成品入库表创建失败")
return False
conn.close()
print("\n所有数据库表创建成功!\n")
return True
def test_table_structure():
"""测试表结构是否正确"""
print("=" * 60)
print("测试2: 检查表结构字段")
print("=" * 60)
conn = get_db()
c = conn.cursor()
# 检查委外工单表字段
c.execute("PRAGMA table_info(outsourcing_orders)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'order_no', 'customer_order_no', 'product_code',
'product_name', 'production_qty', 'outsourcing_factory',
'delivery_date', 'status', 'created_by', 'created_at', 'updated_at']
print("\n委外工单表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
# 检查委外发料表字段
c.execute("PRAGMA table_info(outsourcing_material_issue)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'issue_no', 'outsourcing_order_no', 'material_code',
'material_name', 'issue_qty', 'unit', 'issue_date',
'created_by', 'created_at', 'updated_at']
print("\n委外发料表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
# 检查成品入库表字段
c.execute("PRAGMA table_info(finished_goods_receipt)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'receipt_no', 'outsourcing_order_no', 'product_code',
'product_name', 'receipt_qty', 'receipt_date',
'created_by', 'created_at', 'updated_at']
print("\n成品入库表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
# 检查委外在制库存表字段
c.execute("PRAGMA table_info(outsourcing_wip_stock)")
columns = [row[1] for row in c.fetchall()]
required_columns = ['id', 'outsourcing_order_no', 'material_code',
'material_name', 'wip_qty', 'unit', 'updated_at']
print("\n委外在制库存表字段:")
for col in required_columns:
if col in columns:
print(f"{col}")
else:
print(f"{col} (缺失)")
conn.close()
print("\n表结构检查完成!\n")
return True
def create_test_data():
"""创建测试数据"""
print("=" * 60)
print("测试3: 创建测试数据")
print("=" * 60)
conn = get_db()
c = conn.cursor()
try:
# 创建测试BOM数据
print("\n创建测试BOM数据...")
test_bom = [
('AP05', 'AP05物流版', 'R001', '电阻-10K', 1, 'pcs', 100, '供应商A', None, 'system', '2026-03-13', '2026-03-13'),
('AP05', 'AP05物流版', 'C001', '电容-100uF', 2, 'pcs', 50, '供应商B', None, 'system', '2026-03-13', '2026-03-13'),
('AP05', 'AP05物流版', 'IC001', '芯片-主控', 1, 'pcs', 1, '供应商C', None, 'system', '2026-03-13', '2026-03-13'),
]
for bom in test_bom:
c.execute('''INSERT OR IGNORE INTO bom (
product_code, product_name, material_code, material_name,
unit_qty, unit, min_package, supplier, remark,
created_by, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)''', bom)
print("✓ BOM测试数据创建成功")
# 创建测试期初库存数据
print("\n创建测试期初库存数据...")
test_stock = [
('R001', '电阻-10K', 500, 'pcs', 100, '供应商A', None, 'system', '2026-03-13', '2026-03-13'),
('C001', '电容-100uF', 1000, 'pcs', 50, '供应商B', None, 'system', '2026-03-13', '2026-03-13'),
('IC001', '芯片-主控', 200, 'pcs', 1, '供应商C', None, 'system', '2026-03-13', '2026-03-13'),
]
for stock in test_stock:
c.execute('''INSERT OR IGNORE INTO initial_stock (
material_code, material_name, stock_qty, unit, min_package,
supplier, remark, created_by, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?)''', stock)
print("✓ 期初库存测试数据创建成功")
# 创建测试客户订单数据
print("\n创建测试客户订单数据...")
c.execute('''INSERT OR IGNORE INTO customer_orders (
order_date, order_no, customer_name, material, quantity, unit_price,
created_by, created_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?)''',
('2026-03-13', 'CO20260313001', '测试客户', 'AP05物流版', 100, 1000.00,
'system', '2026-03-13', '2026-03-13'))
print("✓ 客户订单测试数据创建成功")
conn.commit()
print("\n所有测试数据创建成功!\n")
except Exception as e:
print(f"\n✗ 创建测试数据失败: {str(e)}\n")
conn.rollback()
return False
finally:
conn.close()
return True
def main():
"""主测试函数"""
print("\n")
print("" + "=" * 58 + "")
print("" + " " * 15 + "委外管理系统测试脚本" + " " * 15 + "")
print("" + "=" * 58 + "")
print("\n")
# 运行测试
success = True
if not test_database_tables():
success = False
if not test_table_structure():
success = False
if not create_test_data():
success = False
# 输出测试结果
print("=" * 60)
if success:
print("✓ 所有测试通过!委外管理系统已准备就绪。")
print("\n下一步操作:")
print("1. 启动服务器: cd server && python app.py")
print("2. 访问系统: http://localhost:5000")
print("3. 进入\"委外管理\"菜单开始使用")
else:
print("✗ 部分测试失败,请检查错误信息。")
print("=" * 60)
print("\n")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""
测试采购需求状态更新为"已下单"后期初库存的更新功能
"""
import sqlite3
import sys
import os
# 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
DB_PATH = 'server/data.db'
def test_purchase_demand_ordered_update():
"""测试采购需求状态更新为已下单后期初库存自动更新"""
# 连接数据库
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
try:
# 1. 创建测试物料
material_code = 'TEST001'
material_name = '测试物料'
# 插入期初库存记录
c.execute('''INSERT OR REPLACE INTO initial_stock
(material_code, material_name, stock_qty, created_at, updated_at)
VALUES (?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))''',
(material_code, material_name, 100))
# 2. 创建采购需求记录
c.execute('''INSERT INTO purchase_demand
(demand_no, material_code, material_name, order_qty, bom_unit_qty,
total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))''',
('TEST001', material_code, material_name, 10, 5, 50, 100, 0, 10, 50, 'pending'))
demand_id = c.lastrowid
# 3. 更新状态为已下单
c.execute('''UPDATE purchase_demand
SET status = 'ordered', updated_at = datetime('now', 'localtime')
WHERE id = ?''', (demand_id,))
# 4. 模拟计算新的期初库存:实际采购 - 总需求
actual_purchase_qty = 50
total_demand = 50
expected_stock = actual_purchase_qty - total_demand
# 更新期初库存
c.execute('''INSERT OR REPLACE INTO initial_stock
(material_code, material_name, stock_qty, updated_at)
VALUES (?, ?, ?, datetime('now', 'localtime'))''',
(material_code, material_name, expected_stock))
conn.commit()
# 5. 验证结果
c.execute('SELECT stock_qty FROM initial_stock WHERE material_code = ?', (material_code,))
result = c.fetchone()
if result:
actual_stock = result[0]
print(f"✅ 测试通过!")
print(f" - 物料编码: {material_code}")
print(f" - 实际采购数量: {actual_purchase_qty}")
print(f" - 总需求: {total_demand}")
print(f" - 期望期初库存: {expected_stock}")
print(f" - 实际期初库存: {actual_stock}")
if actual_stock == expected_stock:
print(f" - 期初库存更新正确!")
else:
print(f" - ❌ 期初库存更新错误!")
else:
print("❌ 测试失败:未找到期初库存记录")
# 6. 清理测试数据
c.execute('DELETE FROM purchase_demand WHERE id = ?', (demand_id,))
c.execute('DELETE FROM initial_stock WHERE material_code = ?', (material_code,))
conn.commit()
except Exception as e:
print(f"❌ 测试出错: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == '__main__':
print("开始测试采购需求状态更新功能...")
print("=" * 50)
test_purchase_demand_ordered_update()
print("=" * 50)
print("测试完成!")

View File

@ -0,0 +1,221 @@
# 委外工单与发料管理系统使用说明
## 功能概述
委外工单与发料管理系统用于管理整机委外组装场景,支持从客户下单到物料采购、发给外协厂组装、整机回库、发给客户的完整流程。
## 适用场景
**业务流程:** 客户下单 → 我方采购物料 → 发给外协厂组装 → 整机回库 → 发给客户
## 核心功能模块
### 1. 委外工单管理
**功能说明:**
- 创建委外生产订单,关联客户订单
- 自动生成委外工单号格式WW+年月日+4位流水号
- 根据产品BOM自动计算所需物料及数量
- 追踪工单状态(待发料、已发料、已完成、已取消)
**操作步骤:**
1. 进入"委外管理" → "委外工单"
2. 点击"新增委外工单"
3. 填写以下信息:
- 客户订单号:从下拉列表选择已有客户订单
- 成品编码:输入要生产的成品编码
- 成品名称:输入成品名称
- 生产数量:输入委外生产的数量
- 外协厂:输入外协厂名称
- 交期:选择交货日期
4. 系统自动显示物料明细根据BOM计算
5. 点击"保存"创建工单
**字段说明:**
- **委外工单号:** 系统自动生成,唯一标识
- **客户订单号:** 关联的客户订单
- **成品编码/名称:** 要生产的整机型号
- **生产数量:** 委外生产的台数
- **外协厂:** 负责组装的外协厂商
- **交期:** 要求的交货日期
- **物料明细:** 根据BOM自动计算的所需物料清单
### 2. 委外发料管理
**功能说明:**
- 创建发料单,记录发给外协厂的物料
- 自动生成发料单号格式FL+年月日+4位流水号
- 自动扣减期初库存
- 自动增加委外在制库存
**操作步骤:**
1. 进入"委外管理" → "委外发料"
2. 点击"新增发料单"
3. 选择关联的委外工单号
4. 系统自动显示该工单的物料明细和需发数量
5. 修改实际发料数量(如有需要)
6. 选择发料日期
7. 点击"确认发料"
**业务规则:**
- 发料后自动扣减"期初库存"
- 发料后自动增加"委外在制库存"
- 发料数量可以与需发数量不同(支持分批发料)
### 3. 成品入库管理
**功能说明:**
- 记录外协厂完工的整机入库
- 自动生成入库单号格式RK+年月日+4位流水号
- 自动计算物料消耗
- 自动扣减委外在制库存
- 自动增加成品库存
**操作步骤:**
1. 进入"委外管理" → "成品入库"
2. 点击"新增入库单"
3. 选择关联的委外工单号
4. 系统显示工单信息和预计物料消耗
5. 输入实际入库数量
6. 选择入库日期
7. 点击"确认入库"
**业务规则:**
- 入库时根据BOM自动计算物料消耗
- 自动扣减"委外在制库存"
- 自动增加成品的"期初库存"
- 显示物料消耗明细,方便与外协厂对账
### 4. 委外在制库存查询
**功能说明:**
- 查看已发给外协厂但未完工入库的物料
- 实时显示在制库存数量
- 支持按委外工单号、物料编码/名称搜索
**查询说明:**
- **物料种类:** 当前在制的物料种类数
- **在制总数量:** 所有在制物料的总数量
- **委外工单号:** 物料所属的委外工单
- **在制数量:** 该物料当前的在制数量
## 核心业务规则
### 1. 库存流转规则
```
客户下单 → 我方采购物料
期初库存(采购到货后增加)
委外发料(扣减期初库存,增加委外在制库存)
委外在制库存(物料在外协厂)
成品入库(扣减委外在制库存,增加成品库存)
发给客户(扣减成品库存)
```
### 2. 采购需求计算规则
- **总需求 = 订单数量 × BOM单机用量**
- **净需求 = 总需求 - 期初库存 - 在途库存**
- **实际采购 = 净需求按最小包装向上取整**
### 3. 库存类型说明
- **期初库存:** 我方仓库的实际库存
- **在途库存:** 已下单采购但未到货的库存
- **委外在制库存:** 已发给外协厂但未完工的库存
- **成品库存:** 外协厂完工入库的成品
## 典型业务流程示例
### 场景客户订购100台AP05整机
**步骤1创建委外工单**
- 客户订单号CO20260313001
- 成品编码AP05
- 成品名称AP05物流版
- 生产数量100台
- 外协厂深圳XX电子厂
- 交期2026-03-20
**步骤2查看物料需求**
系统根据BOM自动计算
- 电阻-10K100个BOM用量1个/台)
- 电容-100uF200个BOM用量2个/台)
- 芯片-主控100个BOM用量1个/台)
**步骤3采购物料**
在"采购需求清单"中计算采购需求,下单采购
**步骤4物料到货**
在"期初库存"中更新库存数量
**步骤5委外发料**
- 选择委外工单WW202603130001
- 发料日期2026-03-14
- 发料明细:
- 电阻-10K100个
- 电容-100uF200个
- 芯片-主控100个
**步骤6成品入库**
- 选择委外工单WW202603130001
- 入库数量100台
- 入库日期2026-03-19
- 系统自动计算物料消耗并扣减委外在制库存
## 权限说明
- **超级管理员superadmin** 所有功能的完整权限
- **管理员admin** 可操作委外工单、发料、入库等功能
- **普通用户:** 仅查看权限
## 注意事项
1. **委外工单必须关联客户订单**,确保业务可追溯
2. **发料前确保期初库存充足**,避免负库存
3. **成品入库会自动扣减委外在制库存**,请确保发料记录准确
4. **物料消耗按BOM自动计算**如有损耗需在BOM中体现
5. **已下单采购的物料仅增加在途库存**,到货后需手动更新期初库存
## 数据表结构
### 委外工单表outsourcing_orders
- order_no委外工单号唯一
- customer_order_no客户订单号
- product_code成品编码
- product_name成品名称
- production_qty生产数量
- outsourcing_factory外协厂
- delivery_date交期
- status状态
### 委外发料表outsourcing_material_issue
- issue_no发料单号唯一
- outsourcing_order_no委外工单号
- material_code物料编码
- material_name物料名称
- issue_qty发料数量
- issue_date发料日期
### 委外在制库存表outsourcing_wip_stock
- outsourcing_order_no委外工单号
- material_code物料编码唯一
- material_name物料名称
- wip_qty在制数量
### 成品入库表finished_goods_receipt
- receipt_no入库单号唯一
- outsourcing_order_no委外工单号
- product_code产品编码
- product_name产品名称
- receipt_qty入库数量
- receipt_date入库日期
## 技术支持
如有问题,请联系系统管理员或查看系统日志。