feat: 库存看板 BOM 标签页新增「模拟齐套」试算模式
- BOM 物料齐套标签页新增模拟齐套工具条: 输入计划生产数量(套/PCS), 点模拟即按 单套用量×数量 算出每颗料总需求用量, 与当前在库对比, 缺口为正的行整行标红并显示缺多少 - 每行新增「需求用量」「缺口」两列(默认隐藏, 模拟时显示); 每棵 BOM 标题出现模拟结论徽章 (模拟齐套 / 模拟缺料 M/N 种); 选"全部 BOM"时对所有 BOM 同时模拟 - 提供「重置」清除模拟结果, 纯前端计算、不改动任何库存数据 - 新增 .sim-bar / .row-sim-danger / .sim-badge 样式 - 教程同步补充模拟齐套章节
This commit is contained in:
@@ -53,6 +53,16 @@
|
||||
</select>
|
||||
<span class="muted" id="bomCount"></span>
|
||||
</div>
|
||||
|
||||
<div class="sim-bar">
|
||||
<label for="simQty">模拟需求量</label>
|
||||
<input id="simQty" type="number" min="1" step="1" placeholder="如 1000" />
|
||||
<span class="muted">套 / PCS(该 BOM 计划生产数量)</span>
|
||||
<button type="button" class="btn primary" onclick="simulateBom()">▶ 模拟齐套</button>
|
||||
<button type="button" class="btn" onclick="resetSim()">重置</button>
|
||||
<span class="muted" id="simHint"></span>
|
||||
</div>
|
||||
|
||||
{% for bb in bom_board %}
|
||||
<div class="card" data-bid="{{ bb.b.id }}" style="margin-bottom:16px">
|
||||
<div class="card-head">
|
||||
@@ -61,14 +71,18 @@
|
||||
{% if bb.status=='danger' %}<span class="badge danger">缺料</span>
|
||||
{% elif bb.status=='warn' %}<span class="badge warn">部分偏低</span>
|
||||
{% else %}<span class="badge ok">齐套</span>{% endif %}
|
||||
<span class="badge sim-badge" id="simBadge-{{ bb.b.id }}" style="display:none"></span>
|
||||
</div>
|
||||
<table class="tbl" style="box-shadow:none; margin:0">
|
||||
<thead>
|
||||
<tr><th>物料编号</th><th>型号 / 规格</th><th>单位</th><th>单套用量</th><th>当前在库</th><th>状态</th></tr>
|
||||
<tr>
|
||||
<th>物料编号</th><th>型号 / 规格</th><th>单位</th><th>单套用量</th><th>当前在库</th><th>状态</th>
|
||||
<th class="sim-col" style="display:none">需求用量</th><th class="sim-col" style="display:none">缺口</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for it in bb.parts %}
|
||||
<tr class="row-{{ it.status }}">
|
||||
<tr class="row-{{ it.status }}" data-qty="{{ it.qty }}" data-stock="{{ it.stock }}">
|
||||
<td><a href="{{ url_for('material_detail', mid=it.m.id) }}">{{ it.m.part_number }}</a></td>
|
||||
<td>{{ it.m.manufacturer or '-' }}</td>
|
||||
<td>{{ it.m.unit }}</td>
|
||||
@@ -79,9 +93,11 @@
|
||||
{% elif it.status=='warn' %}<span class="badge warn">偏低</span>
|
||||
{% else %}<span class="badge ok">正常</span>{% endif %}
|
||||
</td>
|
||||
<td class="sim-col num" style="display:none"></td>
|
||||
<td class="sim-col num" style="display:none"></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="muted">该 BOM 暂无组成物料</td></tr>
|
||||
<tr><td colspan="8" class="muted">该 BOM 暂无组成物料</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -112,6 +128,58 @@ function filterBom(){
|
||||
var cnt = document.getElementById('bomCount');
|
||||
if(cnt) cnt.textContent = (v==='all') ? ('共 ' + cards.length + ' 棵 BOM') : ('已选 1 / ' + cards.length + ' 棵');
|
||||
}
|
||||
function fmtNum(n){
|
||||
// 去掉多余小数, 保留至多3位
|
||||
var r = Math.round(n*1000)/1000;
|
||||
return r.toLocaleString('zh-CN', {maximumFractionDigits:3});
|
||||
}
|
||||
function simulateBom(){
|
||||
var qEl = document.getElementById('simQty');
|
||||
var q = parseFloat(qEl.value);
|
||||
if(!q || q <= 0){ alert('请输入有效的模拟需求量(正整数)'); return; }
|
||||
var sel = document.getElementById('bomSelect');
|
||||
var v = sel ? sel.value : 'all';
|
||||
var cards = (v==='all')
|
||||
? document.querySelectorAll('#tab-bom .card[data-bid]')
|
||||
: document.querySelectorAll('#tab-bom .card[data-bid="'+v+'"]');
|
||||
var anyShort = false;
|
||||
cards.forEach(function(card){
|
||||
var shortage = 0, total = 0;
|
||||
card.querySelectorAll('tbody tr[data-qty]').forEach(function(tr){
|
||||
var qpu = parseFloat(tr.getAttribute('data-qty'));
|
||||
var stock = parseFloat(tr.getAttribute('data-stock'));
|
||||
var need = qpu * q;
|
||||
var gap = need - stock;
|
||||
total++;
|
||||
if(gap > 0){ shortage++; tr.classList.add('row-sim-danger'); }
|
||||
else { tr.classList.remove('row-sim-danger'); }
|
||||
var cols = tr.querySelectorAll('.sim-col');
|
||||
cols[0].textContent = fmtNum(need);
|
||||
cols[1].textContent = gap > 0 ? ('缺 ' + fmtNum(gap)) : '充足';
|
||||
cols[0].style.display = '';
|
||||
cols[1].style.display = '';
|
||||
});
|
||||
card.querySelectorAll('th.sim-col').forEach(function(th){ th.style.display=''; });
|
||||
var badge = document.getElementById('simBadge-'+card.getAttribute('data-bid'));
|
||||
if(badge){
|
||||
badge.style.display = '';
|
||||
if(shortage>0){ badge.className='badge sim-badge danger'; badge.textContent='模拟缺料: 缺 '+shortage+'/'+total+' 种'; anyShort=true; }
|
||||
else { badge.className='badge sim-badge ok'; badge.textContent='模拟齐套: '+total+' 种充足'; }
|
||||
}
|
||||
});
|
||||
var hint = document.getElementById('simHint');
|
||||
if(hint){
|
||||
if(cards.length>1) hint.textContent = anyShort ? ('共模拟 '+cards.length+' 棵 BOM,存在缺料') : ('共模拟 '+cards.length+' 棵 BOM,全部齐套');
|
||||
else if(cards.length===1) hint.textContent = anyShort ? '该 BOM 模拟缺料' : '该 BOM 模拟齐套';
|
||||
}
|
||||
}
|
||||
function resetSim(){
|
||||
document.querySelectorAll('#tab-bom .sim-col').forEach(function(e){ e.style.display='none'; });
|
||||
document.querySelectorAll('#tab-bom tbody tr').forEach(function(tr){ tr.classList.remove('row-sim-danger'); });
|
||||
document.querySelectorAll('#tab-bom .sim-badge').forEach(function(b){ b.style.display='none'; });
|
||||
var q = document.getElementById('simQty'); if(q) q.value='';
|
||||
var h = document.getElementById('simHint'); if(h) h.textContent='';
|
||||
}
|
||||
filterBom();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user