diff --git a/static/style.css b/static/style.css
index 1bc67d5..3b07a00 100644
--- a/static/style.css
+++ b/static/style.css
@@ -78,6 +78,14 @@ button:hover, .btn:hover { filter: brightness(.96); }
.bom-filter label { margin: 0; white-space: nowrap; }
.bom-filter select { width: auto; min-width: 260px; }
+/* BOM 模拟齐套模式 */
+.sim-bar { display: flex; align-items: center; gap: 10px; margin: 0 0 16px; flex-wrap: wrap; padding: 10px 12px; background: #f7f9fc; border: 1px solid #e3e8ef; border-radius: 8px; }
+.sim-bar label { margin: 0; white-space: nowrap; }
+.sim-bar input[type=number] { width: 130px; }
+.badge.sim-badge { margin-left: auto; }
+tr.row-sim-danger { background: #fff1f0 !important; }
+tr.row-sim-danger td { color: #cf1322; }
+
/* 批量导入窗口(模态) */
.modal-mask {
position: fixed; inset: 0; background: rgba(15,23,35,.5);
diff --git a/templates/dashboard.html b/templates/dashboard.html
index aed06c9..2959fd4 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -53,6 +53,16 @@
+
+
+
+
+ 套 / PCS(该 BOM 计划生产数量)
+
+
+
+
+
{% for bb in bom_board %}
@@ -61,14 +71,18 @@
{% if bb.status=='danger' %}缺料
{% elif bb.status=='warn' %}部分偏低
{% else %}齐套{% endif %}
+
- | 物料编号 | 型号 / 规格 | 单位 | 单套用量 | 当前在库 | 状态 |
+
+ | 物料编号 | 型号 / 规格 | 单位 | 单套用量 | 当前在库 | 状态 |
+ 需求用量 | 缺口 |
+
{% for it in bb.parts %}
-
+
| {{ it.m.part_number }} |
{{ it.m.manufacturer or '-' }} |
{{ it.m.unit }} |
@@ -79,9 +93,11 @@
{% elif it.status=='warn' %}偏低
{% else %}正常{% endif %}
+ |
+ |
{% else %}
- | 该 BOM 暂无组成物料 |
+ | 该 BOM 暂无组成物料 |
{% endfor %}
@@ -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();
{% endblock %}
diff --git a/物料入库出库系统_使用教程.md b/物料入库出库系统_使用教程.md
index 09a472c..9d2289c 100644
--- a/物料入库出库系统_使用教程.md
+++ b/物料入库出库系统_使用教程.md
@@ -79,6 +79,12 @@
> **BOM 物料齐套标签页**:看板顶部有「📦 库存预警 / 🔧 BOM 物料齐套」两个标签(仅管理员、主管可见)。切到 **BOM 物料齐套** 后,顶部有一个「选择 BOM」下拉框:默认「全部 BOM(总览)」会逐棵列出其组成物料的「单套用量 / 当前在库 / 状态」;选中某棵 BOM 则只看那棵。右侧显示「共 N 棵 / 已选 1/N 棵」计数。每棵 BOM 标题处给出整体齐套结论(齐套 / 部分偏低 / 缺料),点物料编号可跳到该料明细。这样不用进 BOM 列表就能在看板上直接看清每棵 BOM 里每颗料的库存情况。
+> **模拟齐套(试算缺料)**:在 BOM 物料齐套标签页里,「选择 BOM」下拉框下方有一条「模拟齐套」工具条。输入该 BOM 的**计划生产数量**(如 1000 套 / PCS),点「▶ 模拟齐套」,系统按每颗料的「单套用量 × 计划数量」算出**总需求用量**,与**当前在库**对比:
+> - 每行右侧新增「需求用量」「缺口」两列;缺口为正的行(需求 > 在库)整行标红并显示「缺 X」;
+> - 该 BOM 标题处出现模拟结论徽章:「模拟齐套 / N 种充足」或「模拟缺料 / 缺 M/N 种」;
+> - 选择「全部 BOM(总览)」时会对每棵 BOM 同时模拟,工具条右侧给出整体结论。
+> 点「重置」可清除模拟结果。该模式**只做试算、不改任何库存数据**,适合接单前快速判断"这批货能不能齐套、还差多少料"。
+
---
## 五、物料管理