增加委外管理文件文件
This commit is contained in:
parent
a04c222f0a
commit
2f87fda0de
22
frontend/assets/xlsx.min.js
vendored
Normal file
22
frontend/assets/xlsx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
404
frontend/js/components/finished-goods-receipt.js
Normal file
404
frontend/js/components/finished-goods-receipt.js
Normal 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()">×</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
|
||||
};
|
||||
})();
|
||||
384
frontend/js/components/outsourcing-material-issue.js
Normal file
384
frontend/js/components/outsourcing-material-issue.js
Normal 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()">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
window.MaterialIssue = {
|
||||
search,
|
||||
resetSearch,
|
||||
closeModal,
|
||||
save,
|
||||
goPage,
|
||||
loadOrderMaterials
|
||||
};
|
||||
})();
|
||||
429
frontend/js/components/outsourcing-orders.js
Normal file
429
frontend/js/components/outsourcing-orders.js
Normal 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()">×</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
window.OutsourcingOrders = {
|
||||
search,
|
||||
resetSearch,
|
||||
viewDetail,
|
||||
deleteOrder,
|
||||
closeModal,
|
||||
save,
|
||||
goPage
|
||||
};
|
||||
})();
|
||||
263
frontend/js/components/outsourcing-wip-stock.js
Normal file
263
frontend/js/components/outsourcing-wip-stock.js
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
window.OutsourcingWipStock = {
|
||||
search,
|
||||
resetSearch,
|
||||
goPage,
|
||||
loadList
|
||||
};
|
||||
})();
|
||||
167
frontend/js/components/shipment-audit.js
Normal file
167
frontend/js/components/shipment-audit.js
Normal 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>`;
|
||||
});
|
||||
BIN
server/uploads/repair_images/1772006261842_2026-02-25_155742.png
Normal file
BIN
server/uploads/repair_images/1772006261842_2026-02-25_155742.png
Normal file
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
37
test-xlsx.html
Normal 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
232
test_outsourcing_system.py
Normal 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()
|
||||
97
test_purchase_demand_ordered.py
Normal file
97
test_purchase_demand_ordered.py
Normal 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("测试完成!")
|
||||
221
委外管理系统使用说明.md
Normal file
221
委外管理系统使用说明.md
Normal 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自动计算:
|
||||
- 电阻-10K:100个(BOM用量1个/台)
|
||||
- 电容-100uF:200个(BOM用量2个/台)
|
||||
- 芯片-主控:100个(BOM用量1个/台)
|
||||
|
||||
**步骤3:采购物料**
|
||||
在"采购需求清单"中计算采购需求,下单采购
|
||||
|
||||
**步骤4:物料到货**
|
||||
在"期初库存"中更新库存数量
|
||||
|
||||
**步骤5:委外发料**
|
||||
- 选择委外工单:WW202603130001
|
||||
- 发料日期:2026-03-14
|
||||
- 发料明细:
|
||||
- 电阻-10K:100个
|
||||
- 电容-100uF:200个
|
||||
- 芯片-主控: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:入库日期
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请联系系统管理员或查看系统日志。
|
||||
Loading…
Reference in New Issue
Block a user