- Flask + SQLAlchemy 物料/入库/出库/BOM/生产订单全模块管理 - PyInstaller 打包为单文件 EXE (双击运行, 自动建库) - 物料维度: 入库总数量 / 消耗总数量 / 逐次入库流水(含时间) - BOM 详情展示组成物料的规格型号与单套用量 - 支持 Altium / 嘉立创 BOM 批量导入(文本与 Excel 双通道) - 内置端到端验证脚本(tools/_exe_util.py 等, 清理进程树防残留)
116 lines
5.0 KiB
HTML
116 lines
5.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}物料{% endblock %}
|
|
{% block content %}
|
|
<div class="toolbar">
|
|
<form method="get" class="search">
|
|
<input name="q" value="{{ q }}" placeholder="搜索物料编号 / 型号">
|
|
<button>搜索</button>
|
|
</form>
|
|
<div>
|
|
<a class="btn" href="{{ url_for('download_template', kind='materials') }}">⬇ 下载模板</a>
|
|
<button class="primary" type="button" onclick="openBatch()">⊞ 批量导入窗口</button>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="tbl">
|
|
<thead>
|
|
<tr><th>物料编号</th><th>型号 / 规格</th><th>单位</th><th>安全库存</th><th>在库</th><th>入库总数量</th><th>消耗总数量</th><th>操作</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for m in mats %}
|
|
<tr>
|
|
<td>{{ m.part_number }}</td>
|
|
<td>{{ m.manufacturer or '-' }}</td>
|
|
<td>{{ m.unit }}</td>
|
|
<td class="num">{{ '%.2f'|format(m.safety_stock or 0) }}</td>
|
|
<td class="num">{{ '%.2f'|format(sm.get(m.id, 0)) }}</td>
|
|
<td class="num">{{ '%.2f'|format(inb_total.get(m.id, 0)) }}</td>
|
|
<td class="num">{{ '%.2f'|format(out_total.get(m.id, 0)) }}</td>
|
|
<td>
|
|
<a href="{{ url_for('material_detail', mid=m.id) }}">明细</a>
|
|
<a href="{{ url_for('material_edit', mid=m.id) }}">编辑</a>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="8" class="muted">没有匹配的物料</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- 批量导入窗口(模态) -->
|
|
<div class="modal-mask" id="batchMask" style="display:none">
|
|
<div class="modal">
|
|
<div class="modal-head">
|
|
<span>批量导入物料 · 多行清单</span>
|
|
<button class="x" onclick="closeBatch()">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="small muted">每行一条: <b>物料编号 型号/规格 单位 安全库存(可选) 期初库存(可选)</b>,列之间用空格或制表符分隔。也可直接上传 Excel/CSV。</p>
|
|
<textarea id="batchText" rows="9" placeholder="P0001 示例型号A 个 100 500
|
|
P0002 示例型号B 卷 50 200"></textarea>
|
|
<div class="row-actions">
|
|
<button type="button" onclick="parsePaste()">解析预览</button>
|
|
<label class="btn" style="cursor:pointer">📁 上传文件
|
|
<input type="file" id="batchFile" accept=".xlsx,.csv" style="display:none" onchange="parseFile()">
|
|
</label>
|
|
<button type="button" class="primary" id="commitBtn" onclick="commitBatch()" disabled>确认导入</button>
|
|
</div>
|
|
<div id="batchResult" style="margin-top:12px"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let lastRows = [];
|
|
|
|
function openBatch(){ document.getElementById('batchMask').style.display='flex'; }
|
|
function closeBatch(){ document.getElementById('batchMask').style.display='none'; }
|
|
document.getElementById('batchMask').addEventListener('click', e=>{ if(e.target.id==='batchMask') closeBatch(); });
|
|
|
|
function parsePaste(){
|
|
const text = document.getElementById('batchText').value.trim();
|
|
if(!text){ alert('请先粘贴清单'); return; }
|
|
postPreview({text: text});
|
|
}
|
|
function parseFile(){
|
|
const f = document.getElementById('batchFile').files[0];
|
|
if(!f) return;
|
|
const fd = new FormData();
|
|
fd.append('file', f);
|
|
fetch('/api/import/materials/preview', {method:'POST', body:fd})
|
|
.then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
|
|
}
|
|
function postPreview(payload){
|
|
fetch('/api/import/materials/preview', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify(payload)
|
|
}).then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
|
|
}
|
|
function renderPreview(data){
|
|
lastRows = data.rows || [];
|
|
const valid = data.valid, invalid = data.invalid;
|
|
let html = `<div class="flash ${invalid? 'error':'ok'}">共 ${lastRows.length} 行 · 可导入 <b>${valid}</b> 行 · 异常 <b>${invalid}</b> 行</div>`;
|
|
html += '<table class="tbl"><thead><tr><th>物料编号</th><th>型号</th><th>单位</th><th>安全库存</th><th>期初库存</th><th>校验</th></tr></thead><tbody>';
|
|
lastRows.forEach(r=>{
|
|
const cls = r.ok? '' : 'row-danger';
|
|
const tag = r.ok? (r.exists? '<span class="badge warn">更新</span>' : '<span class="badge ok">新增</span>')
|
|
: `<span class="badge danger">${r.error||'错误'}</span>`;
|
|
html += `<tr class="${cls}"><td>${r.part_number||''}</td><td>${r.manufacturer||'-'}</td><td>${r.unit||'个'}</td><td class="num">${r.safety_stock}</td><td class="num">${r.opening_stock}</td><td>${tag}</td></tr>`;
|
|
});
|
|
html += '</tbody></table>';
|
|
document.getElementById('batchResult').innerHTML = html;
|
|
document.getElementById('commitBtn').disabled = valid===0;
|
|
}
|
|
function commitBatch(){
|
|
if(!lastRows.length) return;
|
|
fetch('/api/import/materials/commit', {
|
|
method:'POST', headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({rows: lastRows})
|
|
}).then(r=>r.json()).then(d=>{
|
|
alert(`导入完成: 新增 ${d.added} 条, 更新 ${d.updated} 条`);
|
|
if(d.added+d.updated>0) location.reload();
|
|
}).catch(e=>alert('导入失败: '+e));
|
|
}
|
|
</script>
|
|
{% endblock %}
|