ERP/frontend/js/components/purchase-demand.js
2026-03-13 10:33:38 +08:00

1632 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 采购需求清单管理
(() => {
let demandList = [];
let productList = [];
let currentPage = 1;
const pageSize = 20;
const statusMap = {
'pending': { text: '待处理', color: 'var(--warning)' },
'ordered': { text: '已下单', color: 'var(--info)' },
'received': { text: '已收货', color: 'var(--primary)' },
'completed': { text: '已完成', color: 'var(--success)' },
'cancelled': { text: '已取消', color: 'var(--text-2)' }
};
Router.register('/plan-mgmt/purchase-demand', async () => {
const html = `
<style>
#purchase-demand-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#purchase-demand-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#purchase-demand-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#purchase-demand-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#purchase-demand-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#purchase-demand-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;
}
#purchase-demand-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#purchase-demand-page tbody tr:last-child td {
border-bottom: none;
}
#purchase-demand-page tbody tr:hover {
background: var(--hover);
}
#purchase-demand-page .text-center {
text-align: center;
color: var(--text-2);
}
#purchase-demand-page .btn-text {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
padding: 4px 8px;
margin-right: 8px;
}
#purchase-demand-page .btn-text:hover {
text-decoration: underline;
}
#purchase-demand-page .btn-danger {
color: var(--danger);
}
#purchase-demand-page .btn {
color: var(--text);
}
#purchase-demand-page .btn.btn-danger {
color: white;
}
#purchase-demand-page .btn.btn-primary {
color: white;
}
#purchase-demand-page .btn.btn-secondary {
color: var(--text);
}
#purchase-demand-page .btn-sm {
color: var(--text);
}
#purchase-demand-page .btn-sm.btn-danger {
color: white;
}
#purchase-demand-page .btn-sm.btn-secondary {
color: var(--text);
}
#purchase-demand-page .product-tabs-wrapper {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 20px;
}
#purchase-demand-page .product-tab {
min-width: 100px;
height: 51px;
border-radius: 15px;
cursor: pointer;
transition: 0.3s ease;
background: linear-gradient(
to bottom right,
#2e8eff 0%,
rgba(46, 142, 255, 0) 30%
);
background-color: rgba(46, 142, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: 2px;
}
#purchase-demand-page .product-tab:hover,
#purchase-demand-page .product-tab:focus {
background-color: rgba(46, 142, 255, 0.7);
box-shadow: 0 0 10px rgba(46, 142, 255, 0.5);
outline: none;
}
#purchase-demand-page .product-tab.active {
background-color: rgba(46, 142, 255, 0.9);
box-shadow: 0 0 15px rgba(46, 142, 255, 0.7);
}
#purchase-demand-page .product-tab-inner {
width: 100%;
height: 47px;
border-radius: 13px;
background-color: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
color: #66b3ff;
font-weight: 600;
font-size: 14px;
text-shadow: 0 0 2px rgba(102, 179, 255, 0.5);
}
[data-theme="light"] #purchase-demand-page .product-tab-inner {
color: #0066cc;
text-shadow: none;
}
#purchase-demand-page .product-tab.active .product-tab-inner {
color: #003d7a;
font-weight: 700;
}
</style>
<div id="purchase-demand-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">采购需求清单</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="calc-demand-btn" class="btn btn-primary" style="margin-right: 10px;">计算采购需求</button>
<button id="import-excel-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
<button id="export-excel-btn" class="btn btn-secondary" style="margin-right: 10px;" onclick="PurchaseDemand.exportExcel()">导出 Excel</button>
<button id="sync-stock-btn" class="btn btn-secondary" style="margin-right: 10px;" onclick="PurchaseDemand.syncToInitialStock()">同步到期初库存</button>
<button id="delete-product-btn" class="btn btn-danger" style="margin-right: 10px;" onclick="PurchaseDemand.deleteProductData()">删除产品数据</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
</div>
<div class="content-area">
<!-- 产品选择标签页 -->
<div style="padding: 0 20px;">
<div class="product-tabs-wrapper" id="product-tabs-container">
<button class="product-tab active" data-product="" onclick="PurchaseDemand.switchProduct('')">
<div class="product-tab-inner">全部</div>
</button>
</div>
</div>
<div class="filter-section" style="padding: 20px; display: flex; gap: 15px; align-items: center; justify-content: space-between;">
<div style="display: flex; gap: 15px; align-items: center; flex: 1;">
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码、名称或供应商" style="flex: 1; max-width: 400px;" />
<select id="filter-status" class="input" style="width: 150px;">
<option value="">全部状态</option>
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
<button class="btn btn-secondary" onclick="PurchaseDemand.search()">搜索</button>
<button class="btn btn-secondary" onclick="PurchaseDemand.resetSearch()">重置</button>
</div>
<button id="batch-edit-status-btn" class="btn btn-secondary" onclick="PurchaseDemand.openBatchEditStatusModal()">批量编辑状态</button>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="PurchaseDemand.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
<th>需求编号</th>
<th>物料编码</th>
<th>物料名称</th>
<th>订单数量</th>
<th>BOM用量</th>
<th>总需求</th>
<th>期初库存</th>
<th>净需求</th>
<th>最小包装</th>
<th style="background: var(--success-bg); color: var(--text);">实际采购</th>
<th>单位</th>
<th>供应商</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="demand-list">
<tr><td colspan="15" 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="calc-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2>计算采购需求</h2>
<button class="modal-close" onclick="PurchaseDemand.closeCalcModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>选择产品 <span style="color: var(--danger);">*</span></label>
<select id="product-select" class="input">
<option value="">请选择产品</option>
</select>
</div>
<div class="field">
<label>订单数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="order-qty" class="input" min="1" value="1" placeholder="输入客户订单数量" />
</div>
<div class="field">
<label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
<input type="checkbox" id="use-initial-stock" checked style="width: 18px; height: 18px; cursor: pointer; margin-right: 10px;" />
<div>
<div style="font-weight: 500; color: var(--text);">使用期初库存菜单中的库存数据</div>
<div style="margin-top: 4px; font-size: 12px; color: var(--text-3);">
勾选后将使用期初库存管理中的最新库存数据进行计算,否则使用导入时的库存数据
</div>
</div>
</label>
</div>
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 14px; color: var(--text-2);">
<p style="margin: 0 0 8px 0;">📋 计算将:</p>
<ul style="margin: 0; padding-left: 20px;">
<li>根据选中产品的BOM展开所有物料</li>
<li>计算每种物料的总需求量</li>
<li>扣减期初库存得出净需求</li>
<li>按最小包装取整得出实际采购量</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeCalcModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.doCalculate()">开始计算</button>
</div>
</div>
</div>
<!-- 更新状态弹窗 -->
<div id="status-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>更新状态</h2>
<button class="modal-close" onclick="PurchaseDemand.closeStatusModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>状态</label>
<select id="update-status" class="input">
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</div>
<div class="field">
<label>备注</label>
<textarea id="update-remark" class="input" rows="3" placeholder="可选填写备注信息"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeStatusModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.doUpdateStatus()">确定</button>
</div>
</div>
</div>
<!-- Excel导入弹窗 -->
<div id="import-excel-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>导入采购需求清单</h2>
<button class="modal-close" onclick="PurchaseDemand.closeImportModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>产品编码 <span style="color: var(--danger);">*</span></label>
<input type="text" id="import-product-code" class="input" placeholder="请输入产品编码AP05" />
</div>
<div class="field">
<label>选择Excel文件 <span style="color: var(--danger);">*</span></label>
<input type="file" id="excel-file" class="input" accept=".xlsx,.xls" />
<div style="margin-top: 8px; font-size: 12px; color: var(--text-3);">
支持 .xlsx 和 .xls 格式,文件大小不超过 10MB
</div>
<button type="button" class="btn btn-secondary" style="margin-top: 8px;" onclick="testXLSX()">测试XLSX库</button>
</div>
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 14px; color: var(--text-2); margin-bottom: 12px;">
<strong>Excel文件格式要求</strong>
</div>
<table style="width: 100%; font-size: 13px; border-collapse: collapse;">
<thead>
<tr style="background: var(--surface-2);">
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">列名</th>
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">说明</th>
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">必填</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">型号</td>
<td style="padding: 6px; border: 1px solid var(--border;">物料编码/型号</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">供应商</td>
<td style="padding: 6px; border: 1px solid var(--border;">供应商名称</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">期初库存</td>
<td style="padding: 6px; border: 1px solid var(--border;">当前库存数量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">MOQ</td>
<td style="padding: 6px; border: 1px solid var(--border;">最小订购量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">单机数量</td>
<td style="padding: 6px; border: 1px solid var(--border;">BOM单机用量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
</tbody>
</table>
</div>
<div id="import-preview" style="margin-top: 16px; display: none;">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">预览数据前5条</div>
<div class="table-container" style="max-height: 200px;">
<table class="data-table" id="preview-table">
<thead>
<tr>
<th>型号</th>
<th>供应商</th>
<th>期初库存</th>
<th>MOQ</th>
<th>单机数量</th>
</tr>
</thead>
<tbody id="preview-tbody">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeImportModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.importExcel()">导入</button>
</div>
</div>
</div>
<!-- 编辑采购需求弹窗 -->
<div id="edit-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>编辑采购需求</h2>
<button class="modal-close" onclick="PurchaseDemand.closeEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>物料编码</label>
<input type="text" id="edit-material-code" class="input" readonly />
</div>
<div class="field">
<label>物料名称</label>
<input type="text" id="edit-material-name" class="input" readonly />
</div>
<div class="field">
<label>订单数量</label>
<input type="number" id="edit-order-qty" class="input" min="0" />
</div>
<div class="field">
<label>单机用量</label>
<input type="number" id="edit-bom-unit-qty" class="input" min="0" step="0.01" />
</div>
<div class="field">
<label>期初库存</label>
<input type="number" id="edit-initial-stock" class="input" min="0" />
</div>
<div class="field">
<label>最小包装</label>
<input type="number" id="edit-min-package" class="input" min="1" />
</div>
<div class="field">
<label>实际采购数量</label>
<input type="number" id="edit-actual-purchase-qty" class="input" min="0" />
</div>
<div class="field">
<label>单位</label>
<input type="text" id="edit-unit" class="input" />
</div>
<div class="field">
<label>供应商</label>
<input type="text" id="edit-supplier" class="input" />
</div>
<div class="field">
<label>状态</label>
<select id="edit-status" class="input">
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</div>
<div class="field">
<label>备注</label>
<textarea id="edit-remark" class="input" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeEditModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.saveEdit()">保存</button>
</div>
</div>
</div>
<!-- 批量编辑状态弹窗 -->
<div id="batch-edit-status-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2>批量编辑状态</h2>
<button class="modal-close" onclick="PurchaseDemand.closeBatchEditStatusModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>选择状态</label>
<select id="batch-status" class="input">
<option value="">请选择状态</option>
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</div>
<div class="field">
<label>应用范围</label>
<div style="display: flex; gap: 20px; margin-top: 8px;">
<label for="batch-scope-selected" style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" id="batch-scope-selected" name="batch-scope" value="selected" checked style="margin-right: 8px;">
仅当前选中的记录
</label>
<label for="batch-scope-all" style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" id="batch-scope-all" name="batch-scope" value="all" style="margin-right: 8px;">
指定产品的所有记录
</label>
</div>
<div id="batch-product-select-container" style="margin-top: 12px; display: none;">
<label style="font-size: 14px; margin-bottom: 4px; display: block;">选择产品</label>
<select id="batch-product-select" class="input" style="width: 100%;">
<option value="">请选择产品</option>
</select>
</div>
</div>
<div class="field">
<label>备注(可选)</label>
<textarea id="batch-remark" class="input" rows="3" placeholder="可以为所有选中的项目添加统一的备注"></textarea>
</div>
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px;">
<div style="font-size: 14px; color: var(--text-2);">
<p style="margin: 0;">已选择 <span id="selected-count" style="font-weight: bold; color: var(--primary);">0</span> 条记录</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeBatchEditStatusModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.saveBatchEditStatus()">保存</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('calc-demand-btn')?.addEventListener('click', () => openCalcModal());
document.getElementById('import-excel-btn')?.addEventListener('click', () => openImportModal());
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') PurchaseDemand.search();
});
document.getElementById('excel-file')?.addEventListener('change', handleFileSelect);
loadList();
loadProducts();
}, 0);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/purchase-demand');
demandList = res.list || [];
console.log('加载采购需求列表:', demandList.length, '条'); // 调试日志
console.log('前3条数据:', demandList.slice(0, 3)); // 调试日志
console.log('完整数据结构:', demandList[0]); // 查看第一条数据的完整结构
console.log('数据中的所有字段:', Object.keys(demandList[0] || {})); // 查看所有字段名
console.log('第一条数据的product_code:', demandList[0]?.product_code); // 直接查看第一条的product_code
console.log('第二条数据的product_code:', demandList[1]?.product_code); // 直接查看第二条的product_code
// 更新产品标签
updateProductTabs();
renderList();
} catch (e) {
console.error('加载采购需求失败:', e);
document.getElementById('demand-list').innerHTML = '<tr><td colspan="15" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
function updateProductTabs() {
// 提取所有产品编码
const products = new Set();
demandList.forEach((item, index) => {
// 优先使用 product_code 字段,如果没有则从 demand_no 中提取
let productCode = item.product_code;
console.log(`${index + 1}条数据的product_code:`, productCode); // 调试每条数据
if (productCode) {
products.add(productCode);
}
});
console.log('提取到的产品编码:', Array.from(products)); // 调试日志
console.log('检查数据是否有product_code字段:', demandList.some(item => 'product_code' in item)); // 检查字段是否存在
// 更新标签页
const tabsContainer = document.getElementById('product-tabs-container');
if (!tabsContainer) return;
const productArray = Array.from(products).sort();
let html = `<button class="product-tab ${!currentProduct ? 'active' : ''}" data-product="" onclick="PurchaseDemand.switchProduct('')">
<div class="product-tab-inner">全部</div>
</button>`;
productArray.forEach(product => {
const isActive = product === currentProduct ? 'active' : '';
html += `<button class="product-tab ${isActive}" data-product="${product}" onclick="PurchaseDemand.switchProduct('${product}')">
<div class="product-tab-inner">${product}</div>
</button>`;
});
tabsContainer.innerHTML = html;
}
function switchProduct(productCode) {
currentProduct = productCode; // 保存当前选中的产品
window.PurchaseDemand.currentProduct = productCode; // 也在全局对象中保存
// 更新标签样式
document.querySelectorAll('.product-tab').forEach(tab => {
if (tab.dataset.product === productCode) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
renderList();
}
async function loadProducts() {
try {
// 从采购需求表中获取产品列表
const res = await API.get('/api/purchase-demand');
const demandData = res.list || [];
// 提取唯一的产品编码
const products = {};
demandData.forEach(item => {
if (item.product_code) {
products[item.product_code] = item.product_code;
}
});
productList = Object.keys(products).map(code => ({
product_code: code,
product_name: code // 使用产品编码作为名称
}));
console.log('从采购需求表加载到的产品列表:', productList); // 调试日志
const select = document.getElementById('product-select');
if (select) {
select.innerHTML = '<option value="">请选择产品</option>' +
productList.map(p => `<option value="${escapeHtml(p.product_code)}">${escapeHtml(p.product_code)}</option>`).join('');
}
} catch (e) {
console.error('加载产品列表失败:', e);
}
}
let currentProduct = ''; // 当前选中的产品
function toggleSelectAll(source) {
const checkboxes = document.querySelectorAll('.row-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = source.checked;
});
}
function renderList() {
const tbody = document.getElementById('demand-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
const filterStatus = document.getElementById('filter-status')?.value || '';
// 按产品分组
let grouped = {};
demandList.forEach(item => {
const productCode = item.product_code || '';
if (!grouped[productCode]) {
grouped[productCode] = [];
}
grouped[productCode].push(item);
});
// 如果选择了特定产品,只显示该产品的数据
if (currentProduct) {
grouped = currentProduct in grouped ? { [currentProduct]: grouped[currentProduct] } : {};
}
// 合并所有数据用于搜索和过滤
let allData = [];
Object.values(grouped).forEach(items => {
allData = allData.concat(items);
});
// 应用搜索过滤
if (keyword) {
allData = allData.filter(item =>
(item.demand_no || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_name || '').toLowerCase().includes(keyword)
);
}
if (filterStatus) {
allData = allData.filter(item => item.status === filterStatus);
}
// 分页
const totalPages = Math.ceil(allData.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = allData.slice(start, start + pageSize);
if (pageData.length === 0) {
tbody.innerHTML = '<tr><td colspan="15" class="text-center">暂无数据</td></tr>';
} else {
// 按产品分组渲染
let html = '';
let currentGroup = null;
// 计算每个产品的总采购数量(跨所有页面)
const productTotals = {};
allData.forEach(item => {
if (item.product_code) {
productTotals[item.product_code] = (productTotals[item.product_code] || 0) + (item.actual_purchase_qty || 0);
}
});
pageData.forEach(item => {
if (item.product_code !== currentGroup) {
currentGroup = item.product_code;
// 只在有产品编码时才添加分组标题
if (currentGroup) {
const productTotal = productTotals[currentGroup] || 0;
html += `<tr style="background: var(--surface-2);">
<td colspan="10" style="font-weight: bold; padding: 12px 16px; color: var(--text);">
产品: ${currentGroup}
</td>
<td colspan="5" style="font-weight: bold; padding: 12px 16px; color: var(--primary); text-align: right;">
总采购数量: ${productTotal}
</td>
</tr>`;
}
}
const status = statusMap[item.status] || { text: item.status, color: 'var(--text-2)' };
html += `
<tr>
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.demand_no || '')}</span></td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td>${item.order_qty || 0}</td>
<td>${item.bom_unit_qty || 1}</td>
<td>${item.total_demand || 0}</td>
<td style="color: var(--info);">${item.initial_stock || 0}</td>
<td style="color: ${item.net_demand > 0 ? 'var(--warning)' : 'var(--success)'};">${item.net_demand || 0}</td>
<td>${item.min_package || 1}</td>
<td style="font-weight: 700; color: var(--text); background: var(--success-bg);">${item.actual_purchase_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${escapeHtml(item.supplier || '-')}</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>
<button class="btn btn-sm btn-secondary" onclick="PurchaseDemand.edit(${item.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="PurchaseDemand.delete(${item.id})">删除</button>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
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="PurchaseDemand.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="PurchaseDemand.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openCalcModal() {
document.getElementById('product-select').value = '';
document.getElementById('order-qty').value = 1;
document.getElementById('calc-modal').style.display = 'flex';
// 重新加载产品列表
loadProducts();
}
function closeCalcModal() {
document.getElementById('calc-modal').style.display = 'none';
}
async function doCalculate() {
const productCode = document.getElementById('product-select').value;
const orderQty = parseInt(document.getElementById('order-qty').value) || 0;
const useInitialStock = document.getElementById('use-initial-stock').checked;
if (!productCode) {
alert('请选择产品');
return;
}
if (orderQty <= 0) {
alert('订单数量必须大于0');
return;
}
try {
// 基于现有采购需求重新计算
const res = await API.post('/api/purchase-demand/recalculate', {
product_code: productCode,
order_qty: orderQty,
use_initial_stock: useInitialStock
});
closeCalcModal();
alert(res.message || '计算完成');
loadList();
// 显示计算结果摘要
if (res.list && res.list.length > 0) {
const totalPurchase = res.list.reduce((sum, item) => sum + (item.actual_purchase_qty || 0), 0);
console.log(`采购需求重新计算完成,需求编号: ${res.demand_no},共 ${res.list.length} 种物料,总采购数量: ${totalPurchase}`);
}
} catch (e) {
alert(e.message || '计算失败');
}
}
async function calcFromOrders() {
if (!confirm('将根据所有客户订单自动计算采购需求,确定继续吗?')) {
return;
}
try {
const res = await API.post('/api/purchase-demand/calculate-from-orders', {});
alert(res.message || '计算完成');
loadList();
} catch (e) {
alert(e.message || '计算失败');
}
}
let updatingId = null;
function updateStatus(id) {
updatingId = id;
const item = demandList.find(x => x.id === id);
document.getElementById('update-status').value = item?.status || 'pending';
document.getElementById('update-remark').value = item?.remark || '';
document.getElementById('status-modal').style.display = 'flex';
}
function closeStatusModal() {
document.getElementById('status-modal').style.display = 'none';
updatingId = null;
}
async function doUpdateStatus() {
if (!updatingId) return;
const status = document.getElementById('update-status').value;
const remark = document.getElementById('update-remark').value.trim();
try {
await API.put(`/api/purchase-demand/${updatingId}`, { status, remark });
closeStatusModal();
alert('更新成功');
loadList();
} catch (e) {
alert(e.message || '更新失败');
}
}
async function deleteDemand(id) {
if (!confirm('确定要删除这条采购需求吗?')) return;
try {
await API.delete(`/api/purchase-demand/${id}`);
alert('删除成功');
loadList();
} catch (e) {
alert(e.message || '删除失败');
}
}
async function batchDelete() {
const checked = document.querySelectorAll('.row-checkbox:checked');
if (checked.length === 0) {
alert('请先选择要删除的项');
return;
}
if (!confirm(`确定要删除选中的 ${checked.length} 条采购需求吗?`)) return;
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
try {
await API.post('/api/purchase-demand/batch-delete', { ids });
alert('批量删除成功');
loadList();
} catch (e) {
alert(e.message || '批量删除失败');
}
}
function toggleSelectAll(checkbox) {
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
}
function search() {
currentPage = 1;
renderList();
}
function resetSearch() {
document.getElementById('search-keyword').value = '';
document.getElementById('filter-status').value = '';
currentPage = 1;
renderList();
}
function goPage(page) {
currentPage = page;
renderList();
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// Excel导入相关函数
let excelData = [];
function openImportModal() {
document.getElementById('excel-file').value = '';
document.getElementById('import-preview').style.display = 'none';
document.getElementById('import-excel-modal').style.display = 'flex';
}
function closeImportModal() {
document.getElementById('import-excel-modal').style.display = 'none';
excelData = [];
}
// 测试XLSX库
window.testXLSX = function() {
console.log('测试XLSX库...');
console.log('typeof XLSX:', typeof XLSX);
if (typeof XLSX !== 'undefined') {
console.log('XLSX版本:', XLSX.version);
API.toast('XLSX库已加载版本: ' + XLSX.version);
} else {
console.error('XLSX库未加载');
// 尝试动态加载
loadXLSXLibrary();
}
};
// 动态加载XLSX库
function loadXLSXLibrary() {
if (typeof XLSX !== 'undefined') {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = './assets/xlsx.min.js';
script.onload = () => {
console.log('XLSX库动态加载成功版本:', XLSX.version);
API.toast('XLSX库加载成功');
resolve();
};
script.onerror = () => {
console.error('XLSX库加载失败');
API.toast('XLSX库加载失败请刷新页面重试', 'error');
reject(new Error('Failed to load XLSX library'));
};
document.head.appendChild(script);
});
}
// 确保XLSX库已加载
async function ensureXLSXLoaded() {
if (typeof XLSX === 'undefined') {
try {
await loadXLSXLibrary();
} catch (err) {
throw err;
}
}
}
async function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
try {
// 确保XLSX库已加载
await ensureXLSXLoaded();
// 检查文件大小
if (file.size > 10 * 1024 * 1024) {
API.toast('文件大小不能超过10MB', 'error');
e.target.value = '';
return;
}
// 检查文件类型
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.xlsx') && !fileName.endsWith('.xls')) {
API.toast('请选择Excel文件.xlsx或.xls格式', 'error');
e.target.value = '';
return;
}
// 读取Excel文件
const reader = new FileReader();
reader.onload = async function(e) {
try {
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 });
console.log('Excel解析结果:', jsonData); // 调试日志
// 验证数据格式
if (jsonData.length < 2) {
API.toast('Excel文件没有数据', 'error');
return;
}
// 获取表头
const headers = jsonData[0];
console.log('表头:', headers); // 调试日志
const requiredHeaders = ['型号', '供应商', '期初库存', 'MOQ', '单机数量'];
const missingHeaders = requiredHeaders.filter(h => !headers.includes(h));
if (missingHeaders.length > 0) {
API.toast(`缺少必要的列:${missingHeaders.join(', ')}。当前列:${headers.join(', ')}`, 'error');
return;
}
// 解析数据
excelData = [];
let emptyRows = 0;
let validRows = 0;
for (let i = 1; i < jsonData.length; i++) {
const row = jsonData[i];
if (!row) {
emptyRows++;
continue;
}
// 获取各列的值
const model = String(row[headers.indexOf('型号')] || '').trim();
const supplier = String(row[headers.indexOf('供应商')] || '').trim();
const initial_stock = row[headers.indexOf('期初库存')] || 0;
const moq = row[headers.indexOf('MOQ')] || 1;
const per_unit_qty = row[headers.indexOf('单机数量')] || 1;
// 跳过完全空白的行
if (!model && !supplier) {
emptyRows++;
continue;
}
const item = {
model: model,
supplier: supplier,
initial_stock: parseInt(initial_stock) || 0,
moq: parseInt(moq) || 1,
per_unit_qty: parseInt(per_unit_qty) || 1
};
console.log(`${i}行数据:`, item); // 调试日志
excelData.push(item);
validRows++;
}
console.log(`总共处理 ${jsonData.length - 1} 行,有效数据 ${validRows} 条,空行 ${emptyRows}`);
if (excelData.length === 0) {
API.toast('Excel文件没有有效数据', 'error');
return;
}
// 显示预览
showPreview(excelData.slice(0, 5));
API.toast(`成功读取 ${excelData.length} 条数据`, 'success');
} catch (err) {
console.error('解析Excel失败:', err);
API.toast(`解析Excel文件失败${err.message || '请检查文件格式'}`, 'error');
}
};
reader.readAsArrayBuffer(file);
} catch (err) {
console.error('读取文件失败:', err);
API.toast('读取文件失败', 'error');
}
}
function showPreview(data) {
const tbody = document.getElementById('preview-tbody');
tbody.innerHTML = data.map(item => `
<tr>
<td>${escapeHtml(item.model)}</td>
<td>${escapeHtml(item.supplier)}</td>
<td>${item.initial_stock}</td>
<td>${item.moq}</td>
<td>${item.per_unit_qty}</td>
</tr>
`).join('');
document.getElementById('import-preview').style.display = 'block';
}
async function edit(id) {
try {
const res = await API.get(`/api/purchase-demand/${id}`);
const item = res;
if (!item) {
alert('未找到该采购需求');
return;
}
// 填充表单
document.getElementById('edit-material-code').value = item.material_code || '';
document.getElementById('edit-material-name').value = item.material_name || '';
document.getElementById('edit-order-qty').value = item.order_qty || 0;
document.getElementById('edit-bom-unit-qty').value = item.bom_unit_qty || 0;
document.getElementById('edit-initial-stock').value = item.initial_stock || 0;
document.getElementById('edit-min-package').value = item.min_package || 1;
document.getElementById('edit-actual-purchase-qty').value = item.actual_purchase_qty || 0;
document.getElementById('edit-unit').value = item.unit || 'pcs';
document.getElementById('edit-supplier').value = item.supplier || '';
document.getElementById('edit-status').value = item.status || 'pending';
document.getElementById('edit-remark').value = item.remark || '';
// 保存当前编辑的ID
window.PurchaseDemand.editingId = id;
// 显示弹窗
document.getElementById('edit-modal').style.display = 'flex';
} catch (e) {
console.error('获取数据失败:', e);
alert('获取数据失败');
}
}
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
window.PurchaseDemand.editingId = null;
}
async function saveEdit() {
const id = window.PurchaseDemand.editingId;
if (!id) {
alert('无效的编辑ID');
return;
}
// 获取表单数据
const data = {
order_qty: parseInt(document.getElementById('edit-order-qty').value) || 0,
bom_unit_qty: parseFloat(document.getElementById('edit-bom-unit-qty').value) || 0,
initial_stock: parseInt(document.getElementById('edit-initial-stock').value) || 0,
min_package: parseInt(document.getElementById('edit-min-package').value) || 1,
actual_purchase_qty: parseInt(document.getElementById('edit-actual-purchase-qty').value) || 0,
unit: document.getElementById('edit-unit').value.trim() || 'pcs',
supplier: document.getElementById('edit-supplier').value.trim(),
status: document.getElementById('edit-status').value,
remark: document.getElementById('edit-remark').value.trim()
};
try {
const res = await API.put(`/api/purchase-demand/${id}`, data);
if (res.ok) {
alert('保存成功');
closeEditModal();
loadList();
} else {
alert(res.error || '保存失败');
}
} catch (e) {
console.error('保存失败:', e);
alert('保存失败');
}
}
function openBatchEditStatusModal() {
// 获取选中的复选框
const checkboxes = document.querySelectorAll('.row-checkbox:checked');
const selectedCount = checkboxes.length;
// 更新选中数量显示
document.getElementById('selected-count').textContent = selectedCount;
// 加载产品列表
loadProductOptions();
// 重置表单
document.getElementById('batch-status').value = '';
document.getElementById('batch-remark').value = '';
document.querySelector('input[name="batch-scope"][value="selected"]').checked = true;
document.getElementById('batch-product-select-container').style.display = 'none';
// 添加事件监听
document.getElementById('batch-scope-all').onchange = function() {
document.getElementById('batch-product-select-container').style.display =
this.checked ? 'block' : 'none';
};
document.getElementById('batch-scope-selected').onchange = function() {
document.getElementById('batch-product-select-container').style.display =
this.checked ? 'none' : 'block';
};
// 显示弹窗
document.getElementById('batch-edit-status-modal').style.display = 'flex';
}
function loadProductOptions() {
// 获取所有产品
const products = new Set();
demandList.forEach(item => {
if (item.product_code) {
products.add(item.product_code);
}
});
const productSelect = document.getElementById('batch-product-select');
productSelect.innerHTML = '<option value="">请选择产品</option>';
// 添加产品选项
Array.from(products).sort().forEach(product => {
const option = document.createElement('option');
option.value = product;
option.textContent = product;
productSelect.appendChild(option);
});
// 如果当前有选中的产品,设为默认值
if (currentProduct) {
productSelect.value = currentProduct;
}
}
function closeBatchEditStatusModal() {
document.getElementById('batch-edit-status-modal').style.display = 'none';
}
async function saveBatchEditStatus() {
const status = document.getElementById('batch-status').value;
const remark = document.getElementById('batch-remark').value.trim();
const scope = document.querySelector('input[name="batch-scope"]:checked').value;
if (!status) {
alert('请选择状态');
return;
}
let requestData = {
status: status,
remark: remark
};
let confirmMessage = '';
if (scope === 'all') {
// 更新指定产品的所有记录
const selectedProduct = document.getElementById('batch-product-select').value;
if (!selectedProduct) {
alert('请选择要更新的产品');
return;
}
requestData.product_code = selectedProduct;
confirmMessage = `确定要将"${selectedProduct}"的所有记录状态更新为"${getStatusText(status)}"吗?`;
} else {
// 仅更新选中的记录
const checkboxes = document.querySelectorAll('.row-checkbox:checked');
const ids = Array.from(checkboxes).map(cb => parseInt(cb.dataset.id));
if (ids.length === 0) {
alert('请选择要更新的记录');
return;
}
requestData.ids = ids;
confirmMessage = `确定要将选中的 ${ids.length} 条记录状态更新为"${getStatusText(status)}"吗?`;
}
if (!confirm(confirmMessage)) {
return;
}
try {
const res = await API.post('/api/purchase-demand/batch-update-status', requestData);
if (res.ok) {
alert(`成功更新 ${res.count} 条记录`);
closeBatchEditStatusModal();
loadList();
// 清除选择
document.getElementById('select-all').checked = false;
} else {
alert(res.error || '更新失败');
}
} catch (e) {
console.error('批量更新失败:', e);
alert('更新失败');
}
}
function getStatusText(status) {
const statusMap = {
'pending': '待处理',
'ordered': '已下单',
'received': '已收货',
'completed': '已完成',
'cancelled': '已取消'
};
return statusMap[status] || status;
}
async function syncToInitialStock() {
try {
// 获取当前显示的数据
const res = await API.get('/api/purchase-demand');
let dataToSync = res.list || [];
// 应用当前的筛选条件
const keyword = document.getElementById('search-keyword').value.trim();
const statusFilter = document.getElementById('filter-status').value;
const currentProduct = window.PurchaseDemand.currentProduct || '';
if (currentProduct) {
dataToSync = dataToSync.filter(item => item.product_code === currentProduct);
}
if (statusFilter) {
dataToSync = dataToSync.filter(item => item.status === statusFilter);
}
if (keyword) {
const lowerKeyword = keyword.toLowerCase();
dataToSync = dataToSync.filter(item =>
(item.material_code && item.material_code.toLowerCase().includes(lowerKeyword)) ||
(item.material_name && item.material_name.toLowerCase().includes(lowerKeyword)) ||
(item.supplier && item.supplier.toLowerCase().includes(lowerKeyword))
);
}
if (dataToSync.length === 0) {
alert('没有数据可同步');
return;
}
// 提取唯一物料
const uniqueMaterials = {};
dataToSync.forEach(item => {
const key = item.material_code;
if (!uniqueMaterials[key] || item.initial_stock > uniqueMaterials[key].initial_stock) {
uniqueMaterials[key] = {
material_code: item.material_code,
material_name: item.material_name,
stock_qty: item.initial_stock,
unit: item.unit || 'pcs',
min_package: item.min_package || 1,
supplier: item.supplier || ''
};
}
});
const materialsToSync = Object.values(uniqueMaterials);
if (!confirm(`确定要将 ${materialsToSync.length} 个物料的期初库存同步到期初库存管理吗?\n注意:如果物料已存在,将更新其库存数量。`)) {
return;
}
const res2 = await API.post('/api/purchase-demand/sync-to-initial-stock', {
materials: materialsToSync
});
if (res2.ok) {
alert(`成功同步 ${res2.success_count} 个物料的期初库存`);
} else {
alert(res2.error || '同步失败');
}
} catch (e) {
console.error('同步失败:', e);
alert('同步失败');
}
}
async function deleteProductData() {
const currentProduct = window.PurchaseDemand.currentProduct || '';
if (!currentProduct) {
alert('请先选择一个产品');
return;
}
if (!confirm(`确定要删除产品 ${currentProduct} 的所有采购需求数据吗?\n此操作不可恢复!`)) {
return;
}
try {
const res = await API.post('/api/purchase-demand/delete-product', {
product_code: currentProduct
});
if (res.ok) {
alert(`成功删除产品 ${currentProduct}${res.count} 条数据`);
window.PurchaseDemand.currentProduct = '';
loadList();
} else {
alert(res.error || '删除失败');
}
} catch (e) {
alert('删除失败:' + e.message);
}
}
async function exportExcel() {
try {
// 获取当前显示的数据(包括筛选条件)
const res = await API.get('/api/purchase-demand');
let dataToExport = res.list || [];
// 应用当前的筛选条件
const keyword = document.getElementById('search-keyword').value.trim();
const statusFilter = document.getElementById('filter-status').value;
const currentProduct = window.PurchaseDemand.currentProduct || '';
if (currentProduct) {
dataToExport = dataToExport.filter(item => item.product_code === currentProduct);
}
if (statusFilter) {
dataToExport = dataToExport.filter(item => item.status === statusFilter);
}
if (keyword) {
const lowerKeyword = keyword.toLowerCase();
dataToExport = dataToExport.filter(item =>
(item.material_code && item.material_code.toLowerCase().includes(lowerKeyword)) ||
(item.material_name && item.material_name.toLowerCase().includes(lowerKeyword)) ||
(item.supplier && item.supplier.toLowerCase().includes(lowerKeyword))
);
}
if (dataToExport.length === 0) {
alert('没有数据可导出');
return;
}
// 确保XLSX库已加载
await ensureXLSXLoaded();
// 准备导出数据
const exportData = dataToExport.map(item => ({
'需求编号': item.demand_no || '',
'产品编码': item.product_code || '',
'物料编码': item.material_code || '',
'物料名称': item.material_name || '',
'订单数量': item.order_qty || 0,
'单机用量': item.bom_unit_qty || 0,
'总需求': item.total_demand || 0,
'期初库存': item.initial_stock || 0,
'净需求': item.net_demand || 0,
'最小包装': item.min_package || 1,
'实际采购数量': item.actual_purchase_qty || 0,
'单位': item.unit || 'pcs',
'供应商': item.supplier || '',
'状态': getStatusText(item.status),
'备注': item.remark || '',
'创建时间': item.created_at || '',
'创建人': item.created_by || ''
}));
// 创建工作簿
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "采购需求清单");
// 设置列宽
const colWidths = [
{ wch: 20 }, // 需求编号
{ wch: 12 }, // 产品编码
{ wch: 20 }, // 物料编码
{ wch: 30 }, // 物料名称
{ wch: 10 }, // 订单数量
{ wch: 10 }, // 单机用量
{ wch: 10 }, // 总需求
{ wch: 10 }, // 期初库存
{ wch: 10 }, // 净需求
{ wch: 10 }, // 最小包装
{ wch: 15 }, // 实际采购数量
{ wch: 8 }, // 单位
{ wch: 20 }, // 供应商
{ wch: 10 }, // 状态
{ wch: 20 }, // 备注
{ wch: 20 }, // 创建时间
{ wch: 10 } // 创建人
];
ws['!cols'] = colWidths;
// 生成文件名
const now = new Date();
const timestamp = now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') + '_' +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0');
const filename = `采购需求清单_${currentProduct || '全部'}_${timestamp}.xlsx`;
// 导出文件
XLSX.writeFile(wb, filename);
console.log(`成功导出 ${dataToExport.length} 条数据到 ${filename}`);
} catch (err) {
console.error('导出失败:', err);
alert('导出失败,请稍后重试');
}
}
function getStatusText(status) {
const statusMap = {
'pending': '待处理',
'ordered': '已下单',
'received': '已收货',
'completed': '已完成',
'cancelled': '已取消'
};
return statusMap[status] || status;
}
async function importExcel() {
if (excelData.length === 0) {
API.toast('没有可导入的数据', 'error');
return;
}
// 获取产品编码
const productCode = document.getElementById('import-product-code').value.trim();
if (!productCode) {
API.toast('请输入产品编码', 'error');
return;
}
try {
// 确保XLSX库已加载虽然这里不需要但为了保持一致性
await ensureXLSXLoaded();
console.log('准备导入的数据:', excelData); // 调试日志
console.log('产品编码:', productCode); // 调试日志
const res = await API.post('/api/purchase-demand/import', {
data: excelData,
product_code: productCode
});
console.log('导入响应:', res); // 调试日志
if (res.ok) {
API.toast(`成功导入 ${res.count || excelData.length} 条数据`, 'success');
if (res.errors && res.errors.length > 0) {
console.log('导入错误:', res.errors);
}
closeImportModal();
loadList(); // 重新加载列表
} else {
API.toast(res.error || '导入失败', 'error');
}
} catch (err) {
console.error('导入失败:', err);
API.toast('导入失败,请稍后重试', 'error');
}
}
window.PurchaseDemand = {
search,
resetSearch,
delete: deleteDemand,
updateStatus,
closeCalcModal,
doCalculate,
openImportModal,
closeImportModal,
handleFileSelect,
importExcel,
exportExcel,
deleteProductData,
edit,
closeEditModal,
saveEdit,
openBatchEditStatusModal,
closeBatchEditStatusModal,
saveBatchEditStatus,
syncToInitialStock,
toggleSelectAll,
switchProduct,
goPage
};
})();