feat: 库存看板增加 BOM 物料齐套视角 + BOM 详情显示当前在库/状态

- 库存看板新增「BOM 物料齐套」标签页(仅 admin/manager), 逐棵 BOM 列出组成物料的
  单套用量/当前在库/状态, 并给出整体齐套结论
- BOM 详情页组成物料表新增「当前在库」「状态」两列
- 抽出统一 _status_of() 判定函数, 红/黄/绿逻辑与看板一致
- 新增 tools/verify_bom_board.py 端到端验证脚本(已通过)
- 新增使用教程 物料入库出库系统_使用教程.md
- .gitignore 忽略 _backup/(旧 EXE 备份)
This commit is contained in:
2026-09-22 11:47:07 +08:00
parent 7d34a481b3
commit 68b4831d8b
7 changed files with 476 additions and 37 deletions
+14 -5
View File
@@ -2,7 +2,12 @@
{% block title %}BOM 明细 {{ b.code }}{% endblock %}
{% block content %}
<div class="toolbar" data-page-node-id="kijLoAphCatqmeteC0fLS8">
<h3 style="margin:0" data-page-node-id="ECJxkuTNaoeZCptb3hYWET">BOM 明细 · {{ b.code }}</h3>
<h3 style="margin:0" data-page-node-id="ECJxkuTNaoeZCptb3hYWET">BOM 明细 · {{ b.code }}
{% set bstatus = 'danger' if rows|selectattr('status','equalto','danger')|list else ('warn' if rows|selectattr('status','equalto','warn')|list else 'ok') %}
{% if bstatus=='danger' %}<span class="badge danger">缺料</span>
{% elif bstatus=='warn' %}<span class="badge warn">部分偏低</span>
{% else %}<span class="badge ok">齐套</span>{% endif %}
</h3>
<div data-page-node-id="W3YF2xKkzcqbLyQSQHmDv9">
<a class="btn" href="{{ url_for('boms') }}" data-page-node-id="mkt8bYpaiDpnsyNak2CwEx">← 返回 BOM 列表</a>
<a class="btn" href="{{ url_for('bom_edit', bid=b.id) }}" data-page-node-id="b8G1I4yNJvCie28XRFo93k">编辑</a>
@@ -19,20 +24,24 @@
</table>
</div>
<h3 data-page-node-id="t3eVJx5yZD6C7dR2AhvvI1">组成物料(物料编号 / 规格型号 / 单套用量)</h3>
<h3 data-page-node-id="t3eVJx5yZD6C7dR2AhvvI1">组成物料(物料编号 / 规格型号 / 单套用量 / 当前在库 / 状态)</h3>
<table class="tbl" data-page-node-id="df7zG7vdcHnmIIpkdv6sIX">
<thead data-page-node-id="U5mUzLwlJBjGq8lEuFm0Yp"><tr data-page-node-id="B10e5ndG2Fo8DbA4uD8s5e"><th data-page-node-id="clptv2dGFiVnRCTmGNcxGx">#</th><th data-page-node-id="FrYt8QMtBKNICWDU3FNTwu">物料编号</th><th data-page-node-id="WHXRD4OE5syF9gB7S9xUtN">型号 / 规格</th><th data-page-node-id="SF4qN0XEJENLIO6zMz4Rqj">单位</th><th data-page-node-id="5IfMfV4HQupPKakWftwag4">单套用量</th></tr></thead>
<thead data-page-node-id="U5mUzLwlJBjGq8lEuFm0Yp"><tr data-page-node-id="B10e5ndG2Fo8DbA4uD8s5e"><th data-page-node-id="clptv2dGFiVnRCTmGNcxGx">#</th><th data-page-node-id="FrYt8QMtBKNICWDU3FNTwu">物料编号</th><th data-page-node-id="WHXRD4OE5syF9gB7S9xUtN">型号 / 规格</th><th data-page-node-id="SF4qN0XEJENLIO6zMz4Rqj">单位</th><th data-page-node-id="5IfMfV4HQupPKakWftwag4">单套用量</th><th>当前在库</th><th>状态</th></tr></thead>
<tbody data-page-node-id="JBr6qsb25DZ6OzSa4g7USe">
{% for r in rows %}
<tr data-page-node-id="IM9jYB1JuNEMAWNi8ehaaZ">
<tr class="row-{{ r.status }}" data-page-node-id="IM9jYB1JuNEMAWNi8ehaaZ">
<td class="num" data-page-node-id="8ad0FCRnUybzeEWz8FbALx">{{ loop.index }}</td>
<td data-page-node-id="gbLKKUuIavKSsnyYDv5tXm"><a href="{{ url_for('material_detail', mid=r.m.id) }}" data-page-node-id="m53G2ARWr2Udxdxlffdd1l">{{ r.m.part_number }}</a></td>
<td data-page-node-id="vIrrECfeuKSVoBo8MG4qw8">{{ r.m.manufacturer or '-' }}</td>
<td data-page-node-id="LdQUt8z687YhTj9wNbXNLp">{{ r.m.unit }}</td>
<td class="num" data-page-node-id="m2vUEqKxHfCxdgKNKdrMZS">{{ '%.4g'|format(r.qty) }}</td>
<td class="num">{{ '%.2f'|format(r.stock) }}</td>
<td>{% if r.status=='danger' %}<span class="badge danger">缺料</span>
{% elif r.status=='warn' %}<span class="badge warn">偏低</span>
{% else %}<span class="badge ok">正常</span>{% endif %}</td>
</tr>
{% else %}
<tr data-page-node-id="HRMX6kXmTYyKrAC01CI6EK"><td colspan="5" class="muted" data-page-node-id="G4RCcAkiwklmllhidB6cEW">该 BOM 还没有组成物料</td></tr>
<tr data-page-node-id="HRMX6kXmTYyKrAC01CI6EK"><td colspan="7" class="muted" data-page-node-id="G4RCcAkiwklmllhidB6cEW">该 BOM 还没有组成物料</td></tr>
{% endfor %}
</tbody>
</table>
+82 -24
View File
@@ -7,28 +7,86 @@
<div class="stat danger"><div class="num">{{ negative }}</div><div class="lbl">库存为负(缺料)</div></div>
</div>
<h3>库存预警看板</h3>
<table class="tbl">
<thead>
<tr><th>物料编号</th><th>型号 / 规格</th><th>单位</th><th>在库</th><th>安全库存</th><th>状态</th></tr>
</thead>
<tbody>
{% for r in rows %}
<tr class="{{ 'row-'+r.status }}">
<td>{{ r.m.part_number }}</td>
<td>{{ r.m.manufacturer or '-' }}</td>
<td>{{ r.m.unit }}</td>
<td class="num">{{ '%.2f'|format(r.stock) }}</td>
<td class="num">{{ '%.2f'|format(r.m.safety_stock or 0) }}</td>
<td>
{% if r.status=='danger' %}<span class="badge danger">缺料</span>
{% elif r.status=='warn' %}<span class="badge warn">偏低</span>
{% else %}<span class="badge ok">正常</span>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">暂无物料, 请先到"物料"页面添加</td></tr>
{% endfor %}
</tbody>
</table>
<div class="tabs">
<button class="tab active" type="button" onclick="showTab('mat', this)">📦 库存预警</button>
{% if can_bom %}<button class="tab" type="button" onclick="showTab('bom', this)">🔧 BOM 物料齐套</button>{% endif %}
</div>
<!-- 标签一: 全部物料库存 -->
<div id="tab-mat">
<table class="tbl">
<thead>
<tr><th>物料编号</th><th>型号 / 规格</th><th>单位</th><th>在库</th><th>安全库存</th><th>状态</th></tr>
</thead>
<tbody>
{% for r in rows %}
<tr class="row-{{ r.status }}">
<td>{{ r.m.part_number }}</td>
<td>{{ r.m.manufacturer or '-' }}</td>
<td>{{ r.m.unit }}</td>
<td class="num">{{ '%.2f'|format(r.stock) }}</td>
<td class="num">{{ '%.2f'|format(r.m.safety_stock or 0) }}</td>
<td>
{% if r.status=='danger' %}<span class="badge danger">缺料</span>
{% elif r.status=='warn' %}<span class="badge warn">偏低</span>
{% else %}<span class="badge ok">正常</span>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">暂无物料, 请先到"物料"页面添加</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if can_bom %}
<!-- 标签二: BOM 物料齐套看板 -->
<div id="tab-bom" style="display:none">
{% for bb in bom_board %}
<div class="card" style="margin-bottom:16px">
<div class="card-head">
<strong>{{ bb.b.code }}</strong>
<span class="muted">{{ bb.b.name or '' }}</span>
{% 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 %}
</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>
</thead>
<tbody>
{% for it in bb.parts %}
<tr class="row-{{ it.status }}">
<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>
<td class="num">{{ '%.4g'|format(it.qty) }}</td>
<td class="num">{{ '%.2f'|format(it.stock) }}</td>
<td>
{% if it.status=='danger' %}<span class="badge danger">缺料</span>
{% elif it.status=='warn' %}<span class="badge warn">偏低</span>
{% else %}<span class="badge ok">正常</span>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">该 BOM 暂无组成物料</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="muted">暂无 BOM,请先到 BOM 页面导入或新建。</p>
{% endfor %}
</div>
{% endif %}
<script>
function showTab(id, btn){
document.getElementById('tab-mat').style.display = id==='mat' ? 'block' : 'none';
document.getElementById('tab-bom').style.display = id==='bom' ? 'block' : 'none';
document.querySelectorAll('.tabs .tab').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
}
</script>
{% endblock %}