feat: 库存看板增加 BOM 物料齐套视角 + BOM 详情显示当前在库/状态
- 库存看板新增「BOM 物料齐套」标签页(仅 admin/manager), 逐棵 BOM 列出组成物料的 单套用量/当前在库/状态, 并给出整体齐套结论 - BOM 详情页组成物料表新增「当前在库」「状态」两列 - 抽出统一 _status_of() 判定函数, 红/黄/绿逻辑与看板一致 - 新增 tools/verify_bom_board.py 端到端验证脚本(已通过) - 新增使用教程 物料入库出库系统_使用教程.md - .gitignore 忽略 _backup/(旧 EXE 备份)
This commit is contained in:
@@ -19,6 +19,9 @@ request.log
|
||||
# ---- 临时校验文件 ----
|
||||
_check_tmp/
|
||||
|
||||
# ---- 旧 EXE 备份(打包前移走, 非源码, 不入库) ----
|
||||
_backup/
|
||||
|
||||
# ---- 编辑器 ----
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -99,6 +99,15 @@ def stock_map():
|
||||
return {i: inb.get(i, 0) - out.get(i, 0) for i in ids}
|
||||
|
||||
|
||||
def _status_of(stock, safety):
|
||||
"""库存状态: danger=负库存, warn=低于安全库存, ok=正常"""
|
||||
if stock < 0:
|
||||
return "danger"
|
||||
if stock < (safety or 0):
|
||||
return "warn"
|
||||
return "ok"
|
||||
|
||||
|
||||
def inbound_total_map():
|
||||
"""返回 {material_id: 入库总数量}"""
|
||||
rows = db.session.execute(
|
||||
@@ -276,16 +285,40 @@ def dashboard():
|
||||
low = 0
|
||||
for m in mats:
|
||||
stock = sm.get(m.id, 0)
|
||||
if stock < 0:
|
||||
status = _status_of(stock, m.safety_stock)
|
||||
if status == "danger":
|
||||
negative += 1
|
||||
status = "danger"
|
||||
elif stock < (m.safety_stock or 0):
|
||||
elif status == "warn":
|
||||
low += 1
|
||||
status = "warn"
|
||||
else:
|
||||
status = "ok"
|
||||
rows.append({"m": m, "stock": stock, "status": status})
|
||||
return render_template("dashboard.html", rows=rows, negative=negative, low=low)
|
||||
|
||||
# BOM 物料齐套看板(仅管理员/主管可见)
|
||||
can_bom = bool(g.user and g.user.role in ("admin", "manager"))
|
||||
bom_board = []
|
||||
if can_bom:
|
||||
boms = db.session.scalars(db.select(Bom).order_by(Bom.code)).all()
|
||||
for b in boms:
|
||||
items = db.session.scalars(
|
||||
db.select(BomItem).where(BomItem.bom_id == b.id).order_by(BomItem.id)
|
||||
).all()
|
||||
item_rows = []
|
||||
for it in items:
|
||||
m = db.session.get(Material, it.material_id)
|
||||
if not m:
|
||||
continue
|
||||
stock = sm.get(m.id, 0)
|
||||
item_rows.append({"m": m, "qty": it.qty_per_unit,
|
||||
"stock": stock, "status": _status_of(stock, m.safety_stock)})
|
||||
if any(x["status"] == "danger" for x in item_rows):
|
||||
bstatus = "danger"
|
||||
elif any(x["status"] == "warn" for x in item_rows):
|
||||
bstatus = "warn"
|
||||
else:
|
||||
bstatus = "ok"
|
||||
bom_board.append({"b": b, "parts": item_rows, "status": bstatus})
|
||||
|
||||
return render_template("dashboard.html", rows=rows, negative=negative, low=low,
|
||||
bom_board=bom_board, can_bom=can_bom)
|
||||
|
||||
|
||||
# ---------------- 物料 ----------------
|
||||
@@ -555,10 +588,15 @@ def bom_detail(bid):
|
||||
db.select(BomItem).where(BomItem.bom_id == b.id)
|
||||
.order_by(BomItem.id)
|
||||
).all()
|
||||
sm = stock_map()
|
||||
rows = []
|
||||
for it in items:
|
||||
m = db.session.get(Material, it.material_id)
|
||||
rows.append({"m": m, "qty": it.qty_per_unit})
|
||||
if not m:
|
||||
continue
|
||||
stock = sm.get(m.id, 0)
|
||||
rows.append({"m": m, "qty": it.qty_per_unit,
|
||||
"stock": stock, "status": _status_of(stock, m.safety_stock)})
|
||||
return render_template("bom_detail.html", b=b, rows=rows)
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,16 @@ button:hover, .btn:hover { filter: brightness(.96); }
|
||||
.small { font-size: 12px; }
|
||||
.txn-form { max-width: 520px; margin-bottom: 22px; }
|
||||
|
||||
/* 看板标签切换 */
|
||||
.tabs { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
|
||||
.tabs .tab {
|
||||
background: #eef1f5; color: #44525f; border: 1px solid #d7dde5;
|
||||
padding: 8px 16px; font-size: 14px; cursor: pointer; border-radius: 6px;
|
||||
}
|
||||
.tabs .tab.active { background: #1f3a5f; color: #fff; border-color: #1f3a5f; }
|
||||
.card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 15px; }
|
||||
.card-head strong { font-size: 16px; }
|
||||
|
||||
/* 批量导入窗口(模态) */
|
||||
.modal-mask {
|
||||
position: fixed; inset: 0; background: rgba(15,23,35,.5);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,14 +7,20 @@
|
||||
<div class="stat danger"><div class="num">{{ negative }}</div><div class="lbl">库存为负(缺料)</div></div>
|
||||
</div>
|
||||
|
||||
<h3>库存预警看板</h3>
|
||||
<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 }}">
|
||||
<tr class="row-{{ r.status }}">
|
||||
<td>{{ r.m.part_number }}</td>
|
||||
<td>{{ r.m.manufacturer or '-' }}</td>
|
||||
<td>{{ r.m.unit }}</td>
|
||||
@@ -31,4 +37,56 @@
|
||||
{% 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 %}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""验证: 库存看板 BOM 齐套标签页 + BOM 详情页在库/状态列。
|
||||
|
||||
不依赖已存在的 app.db, 自建临时库 + 种子数据做断言。
|
||||
"""
|
||||
import os, sys, tempfile, importlib.util
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
# 用临时库, 不污染正式数据
|
||||
tmp = tempfile.mktemp(suffix=".db")
|
||||
os.environ["WMS_DB"] = tmp
|
||||
|
||||
import app as appmod
|
||||
from models import db, Material, Bom, BomItem, Inbound
|
||||
from datetime import date
|
||||
|
||||
appmod.seed_admin()
|
||||
with appmod.app.app_context():
|
||||
m = Material(part_number="P0001", manufacturer="测试IC-A", unit="颗", safety_stock=100)
|
||||
db.session.add(m); db.session.flush()
|
||||
b = Bom(code="T03", name="测试产品T03"); db.session.add(b); db.session.flush()
|
||||
db.session.add(BomItem(bom_id=b.id, material_id=m.id, qty_per_unit=2))
|
||||
# 入库 30, 安全库存 100 -> 在库 30 < 100 -> 偏低(warn)
|
||||
db.session.add(Inbound(material_id=m.id, qty=30, source="测试", occur_date=date.today()))
|
||||
db.session.commit()
|
||||
BID = b.id # 在上下文内取出 id, 避免外部访问 detached 实例
|
||||
|
||||
client = appmod.app.test_client()
|
||||
# 登录 admin
|
||||
r = client.post("/login", data={"username": "admin", "password": "admin123"}, follow_redirects=True)
|
||||
assert r.status_code == 200, f"login failed: {r.status_code}"
|
||||
|
||||
# 1) 看板页应包含 BOM 齐套标签(用 id="tab-bom" 区块判断, 而非注释文本)
|
||||
r = client.get("/dashboard")
|
||||
html = r.get_data(as_text=True)
|
||||
assert "库存预警" in html, "看板缺少库存预警标签"
|
||||
assert 'id="tab-bom"' in html, "看板缺少 BOM 齐套标签页"
|
||||
print("[OK] 管理员看板页含 BOM 齐套标签页")
|
||||
|
||||
# 2) BOM 详情页应包含 当前在库 / 状态 列, 且 P0001 偏低
|
||||
r = client.get(f"/bom/{BID}")
|
||||
html = r.get_data(as_text=True)
|
||||
assert "当前在库" in html, "BOM 详情缺少 当前在库 列"
|
||||
assert "状态" in html, "BOM 详情缺少 状态 列"
|
||||
assert "P0001" in html, "BOM 详情未展示组成物料编号"
|
||||
assert "偏低" in html, "P0001 在库30<安全100 应显示偏低, 实际未标黄"
|
||||
assert "30.00" in html, "BOM 详情未显示正确在库数量"
|
||||
print("[OK] BOM 详情页展示 当前在库/状态, P0001 正确标为偏低")
|
||||
|
||||
# 3) operator 角色不应看到 BOM 标签 (权限隔离)
|
||||
from models import User
|
||||
import hashlib
|
||||
def sha256(s): return hashlib.sha256(s.encode()).hexdigest()
|
||||
with appmod.app.app_context():
|
||||
if not db.session.scalar(db.select(User).where(User.username == "op1")):
|
||||
db.session.add(User(username="op1", password=sha256("op123"), role="operator", display_name="操作工"))
|
||||
db.session.commit()
|
||||
r = client.post("/logout")
|
||||
r = client.post("/login", data={"username": "op1", "password": "op123"}, follow_redirects=True)
|
||||
r = client.get("/dashboard")
|
||||
html = r.get_data(as_text=True)
|
||||
assert 'id="tab-bom"' not in html, "operator 不应看到 BOM 齐套标签"
|
||||
print("[OK] operator 角色看不到 BOM 齐套标签(权限隔离正确)")
|
||||
|
||||
os.remove(tmp)
|
||||
print("\nALL PASS ✅")
|
||||
@@ -0,0 +1,253 @@
|
||||
# 物料入库 / 出库管理系统 —— 使用教程
|
||||
|
||||
> 适用对象:仓库管理员、车间计划员、系统管理员
|
||||
> 系统形态:Windows 单文件 EXE(双击即用,免安装数据库,中文界面)
|
||||
> 版本:M1 基础版 + M2 核算版 + M3 治理版
|
||||
|
||||
---
|
||||
|
||||
## 一、系统能做什么
|
||||
|
||||
把工厂车间的「物料台账 + BOM 用量核算」从 Excel 宽表搬进系统,解决三个核心痛点:
|
||||
|
||||
| 你现在的麻烦 | 用了系统之后 |
|
||||
|---|---|
|
||||
| 库存 = 入库 − 出库 靠手算公式,错一处全列错 | 在库数量实时计算,负数/低库存自动标红预警 |
|
||||
| BOM 用量要手动乘接单量 | 录入订单接单量,自动展开每种料的「需求量」并提示缺料 |
|
||||
| 多人改表无记录、无法分工 | 带登录、三种角色、操作日志,谁改了什么可追溯 |
|
||||
|
||||
**四个核心概念要先分清:**
|
||||
|
||||
- **物料**:一颗料的主数据(编号、规格型号、单位、安全库存)。
|
||||
- **入库 / 出库**:每一笔收发货的流水记录,按物料汇总成「在库数量」。
|
||||
- **BOM**:一个产品的「用料清单」,记录每种料「单套用量」是多少。
|
||||
- **生产订单**:某个产品接了多少单(接单量),系统按 BOM 自动算出要领哪些料、领多少。
|
||||
|
||||
---
|
||||
|
||||
## 二、安装与启动
|
||||
|
||||
1. 把 `MaterialWMS.exe` 放到任意文件夹(建议单独一个目录,例如 `D:\MaterialWMS\`)。
|
||||
2. **双击** `MaterialWMS.exe` —— 程序会在后台启动本地服务,并**自动打开浏览器**到 `http://127.0.0.1:8080`。
|
||||
3. 数据库文件 `app.db` 会自动生成在 **EXE 同目录**,所有数据都存在这里。
|
||||
|
||||
**常用操作:**
|
||||
|
||||
| 需求 | 做法 |
|
||||
|---|---|
|
||||
| 换端口(8080 被占用) | 命令行运行 `MaterialWMS.exe 8081`,访问 `http://127.0.0.1:8081` |
|
||||
| 备份数据 | 关闭程序后,复制同目录的 `app.db` 到别处即可 |
|
||||
| 指定数据库位置 | 启动前设置环境变量 `WMS_DB=绝对路径\xxx.db` |
|
||||
| 多台电脑共用 | 把 `app.db` 放网络盘或定期拷贝同步(单机程序,非多用户服务器) |
|
||||
|
||||
> ⚠️ 数据全在 `app.db` 一个文件里,**删掉它就全没了**。至少每周备份一次。
|
||||
|
||||
---
|
||||
|
||||
## 三、登录与账号
|
||||
|
||||
- 打开页面首先看到登录框。
|
||||
- **默认管理员账号**:用户名 `admin`,密码 `admin123`(首次使用请尽快改密码或新增专属账号,见第十一节)。
|
||||
|
||||
### 三种角色权限
|
||||
|
||||
| 菜单 | admin(管理员) | manager(主管) | operator(操作员) |
|
||||
|---|:---:|:---:|:---:|
|
||||
| 库存看板 | ✅ | ✅ | ✅ |
|
||||
| 物料(列表 / 明细) | ✅ | ✅ | ✅ |
|
||||
| 入库 / 出库 | ✅ | ✅ | ✅ |
|
||||
| 物料新增 / 编辑 / 删除 | ✅ | ✅ | ❌ |
|
||||
| BOM 管理 | ✅ | ✅ | ❌ |
|
||||
| 生产订单 | ✅ | ✅ | ❌ |
|
||||
| 报表 / 导出 Excel | ✅ | ✅ | ❌ |
|
||||
| 操作日志 | ✅ | ✅ | ❌ |
|
||||
| 用户管理 | ✅ | ❌ | ❌ |
|
||||
|
||||
> 顶部导航栏会**按当前账号角色自动显示/隐藏**菜单,看不到的菜单就是当前角色没有权限。
|
||||
|
||||
---
|
||||
|
||||
## 四、库存看板(首页)
|
||||
|
||||
登录后默认进入「库存看板」,一眼看清所有物料的库存状态:
|
||||
|
||||
- 🟢 **正常**:在库 ≥ 安全库存
|
||||
- 🟡 **预警(黄)**:在库 < 安全库存,该补货了
|
||||
- 🔴 **异常(红)**:在库为负,说明出库多于入库,数据有矛盾需核查
|
||||
|
||||
顶部会汇总「负库存数量」「低于安全库存数量」两个计数,方便快速排查。
|
||||
|
||||
> **BOM 物料齐套标签页**:看板顶部有「📦 库存预警 / 🔧 BOM 物料齐套」两个标签(仅管理员、主管可见)。切到 **BOM 物料齐套** 后,会逐棵 BOM 列出其组成物料的「单套用量 / 当前在库 / 状态」,并在 BOM 标题处给出整体齐套结论(齐套 / 部分偏低 / 缺料)。点物料编号可跳到该料明细。这样不用进 BOM 列表就能在看板上直接看清每棵 BOM 里每颗料的库存情况。
|
||||
|
||||
---
|
||||
|
||||
## 五、物料管理
|
||||
|
||||
### 5.1 物料列表
|
||||
路径:导航栏 **物料**。
|
||||
表格字段:物料编号、型号/规格、单位、安全库存、**在库数量**、**入库总数量**、**消耗总数量**、操作。
|
||||
|
||||
- 顶部搜索框支持按「物料编号」或「型号规格」模糊查找。
|
||||
- 点 **明细** 进入单颗物料的流水页(见 5.4)。
|
||||
|
||||
### 5.2 新增物料(admin / manager)
|
||||
点 **+ 新增物料**,填写:
|
||||
- **物料编号**(必填,唯一,例如 `P0001`)
|
||||
- **型号/规格**(选填,即规格型号)
|
||||
- **单位**(默认「个」)
|
||||
- **安全库存**(低于此值看板标黄)
|
||||
- **备注**
|
||||
|
||||
### 5.3 编辑 / 删除(admin / manager)
|
||||
列表行内「编辑」改信息;「删除」会移除该物料(含其流水,操作需谨慎)。
|
||||
|
||||
### 5.4 物料明细与流水查询
|
||||
点列表里的 **明细** 链接,进入单颗物料页面,顶部三张卡片显示在库 / 入库总 / 消耗总;下方两张表分别是:
|
||||
- **入库流水**:入库日期、录入时间、数量、批次、来源、经手人
|
||||
- **出库流水**:出库日期、录入时间、数量、批次、用途、关联单号、经手人
|
||||
|
||||
> 这就是「查看每颗物料每次的入库时间」的入口。
|
||||
|
||||
### 5.5 用 Excel 批量导入物料(admin / manager)
|
||||
1. 在 **物料** 页点 **⬇ 下载导入模板**,得到 `物料导入模板.xlsx`。
|
||||
2. 按表头填:**物料编号 / 型号规格 / 单位 / 安全库存 / 期初库存**(后四列可选)。
|
||||
3. 回到 **物料** 页(或导入入口)上传文件,系统先解析再写入。
|
||||
4. **期初库存 > 0** 的物料,会自动写入一条来源为「期初」的入库流水,作为开局库存。
|
||||
|
||||
---
|
||||
|
||||
## 六、入库操作
|
||||
|
||||
路径:导航栏 **入库**。两种录入方式:
|
||||
|
||||
### 6.1 单笔录入
|
||||
选物料 → 填批次号、数量、经手人、来源/供应商、业务日期、备注 → 提交。库存实时 +N,并写操作日志。
|
||||
|
||||
### 6.2 批量导入窗口
|
||||
点右上角 **⊞ 批量导入窗口**,支持两种方式:
|
||||
- **粘贴文本**:每行一条 `物料编号 数量 批次 日期 备注`(列间空格/制表符分隔,带表头行会自动跳过)。
|
||||
- **上传 Excel/CSV**:表头需含 `物料编号 / 数量 / 批次 / 日期 / 备注`(中英文列名都识别)。
|
||||
|
||||
点 **解析预览** 后会逐行校验(物料不存在、数量≤0 会标红),确认无误再点 **确认导入**。
|
||||
|
||||
---
|
||||
|
||||
## 七、出库操作
|
||||
|
||||
路径:导航栏 **出库**。界面与入库一致,区别:
|
||||
- 字段为「用途」「关联单号」而非来源。
|
||||
- **库存不足保护**:出库数量 > 当前在库时,系统拦截并提示「库存不足」,不会让库存变负。
|
||||
|
||||
批量导入窗口同样可用(见 6.2),勾选「出库」时同样有库存校验。
|
||||
|
||||
---
|
||||
|
||||
## 八、BOM 管理(admin / manager)
|
||||
|
||||
路径:导航栏 **BOM**。
|
||||
|
||||
### 8.1 新增 BOM(手工)
|
||||
点 **+ 新增 BOM**,填 BOM 型号、名称,然后逐行添加「物料编号 + 单套用量」(只能选已存在的物料)。
|
||||
|
||||
### 8.2 BOM 详情(显示规格型号)
|
||||
点列表里的型号进入详情页,列出每项组成物料的:**物料编号 / 型号规格 / 单位 / 单套用量**。点物料编号可跳到该物料明细。
|
||||
|
||||
### 8.3 批量导入 BOM(推荐)
|
||||
在 **BOM** 页点 **⊞ 批量导入窗口**,支持两种来源:
|
||||
|
||||
**① 上传嘉立创 / Altium BOM(最省事)**
|
||||
- 每个 worksheet 就是一个 BOM,**sheet 名 = BOM 型号**;
|
||||
- 列头识别 `Item / Part Number / Manufacture Part Number / Quantity / Reference / Part / Package`;
|
||||
- `Part Number` 关联物料,`Quantity` 列即单套用量。
|
||||
- 点 **⬇ 下载模板** 拿到的就是这种格式,直接照填即可。
|
||||
|
||||
**② 粘贴文本**
|
||||
```
|
||||
#BOM M4021 示例产品A
|
||||
P0001 2
|
||||
P0002 0.5
|
||||
|
||||
#BOM M4022 示例产品B
|
||||
P0003 4
|
||||
```
|
||||
以 `#BOM <型号> <名称>` 开头,其下每行 `物料编号 单套用量`,空行分隔不同 BOM。
|
||||
|
||||
> 💡 **物料不存在会自动新建**:导入时若某 `Part Number` 在系统里没有,会先建好物料主数据再挂到 BOM,并在预览里标「新建」。
|
||||
|
||||
解析预览确认后点 **确认导入** 即可。
|
||||
|
||||
---
|
||||
|
||||
## 九、生产订单与领料(admin / manager)
|
||||
|
||||
路径:导航栏 **生产订单**。
|
||||
|
||||
1. **新建订单**:选 BOM + 填接单量 → 生成订单号(如 `WO20260920xxx`)。
|
||||
2. **订单详情(用量展开)**:系统按 `需求量 = 接单量 × 单套用量` 算出每种料需要领多少,并和当前在库对比:
|
||||
- 够 → 正常;不够 → **缺料标红**,列出缺口数量。
|
||||
3. **一键领料**:点「领料」,系统按 BOM 用量自动生成一批出库流水(用途记为「生产领用」,关联订单号)。若仍有缺料会提示,但不会阻止操作——需人工确认。
|
||||
|
||||
这一步等价于原来 Excel 里「接单量 × 单套用量」的手算,且自动扣减库存、留痕。
|
||||
|
||||
---
|
||||
|
||||
## 十、报表与操作日志(admin / manager)
|
||||
|
||||
- **报表**:导航栏 **报表**,全量库存表,预警状态标红,可点 **⬇ 导出库存 Excel** 下载 `库存导出_日期.xlsx`。
|
||||
- **日志**:导航栏 **日志**,记录所有关键操作(新增/编辑/删除物料、入库、出库、导入、领料等),含操作人、动作、目标、明细,最多保留 200 条,用于审计追溯。
|
||||
|
||||
---
|
||||
|
||||
## 十一、用户管理(仅 admin)
|
||||
|
||||
路径:导航栏 **用户**(仅管理员可见)。
|
||||
- **新增用户**:填用户名、密码、角色(admin / manager / operator)、显示名。
|
||||
- **启用 / 停用**:停用后该账号无法登录(不能停用自己)。
|
||||
|
||||
> 建议:日常使用别共用 `admin`,给每位同事建独立账号并赋对应角色,方便日志追溯。
|
||||
|
||||
---
|
||||
|
||||
## 十二、数据备份与维护(重要)
|
||||
|
||||
| 项目 | 说明 |
|
||||
|---|---|
|
||||
| 数据库文件 | `app.db`,位于 EXE 同目录,**唯一数据源** |
|
||||
| 备份方法 | 关闭程序 → 复制 `app.db` 到备份盘 / 云盘 |
|
||||
| 还原方法 | 用备份的 `app.db` 覆盖同目录文件,重启 EXE |
|
||||
| 换电脑 | 把 `MaterialWMS.exe` + `app.db` 一起拷贝过去即可 |
|
||||
| 指定库路径 | 设置环境变量 `WMS_DB` 指向自定义位置 |
|
||||
|
||||
---
|
||||
|
||||
## 十三、常见问题 FAQ
|
||||
|
||||
**Q1:打开后浏览器没反应 / 白屏?**
|
||||
先确认 `MaterialWMS.exe` 是否在运行(任务管理器看进程)。若 8080 端口被占,用 `MaterialWMS.exe 8081` 换端口,并访问对应地址。
|
||||
|
||||
**Q2:提示「库存不足」无法出库?**
|
||||
当前在库小于你要出的数量。先核对入库是否录全,或去物料明细查流水。系统故意拦截,避免库存变负。
|
||||
|
||||
**Q3:BOM 导入后某项被「跳过」?**
|
||||
该项物料编号在系统里找不到,且不是可自动新建的情况(如用量≤0)。先用「物料导入」建好主数据,或允许自动新建后重新导入。
|
||||
|
||||
**Q4:Excel 导入提示「格式不符」?**
|
||||
检查表头是否含必需列:物料导入需 `物料编号`;BOM 简单表需 `BOM型号/物料编号/单套用量`;嘉立创表需 `Part Number/Quantity`。下载对应模板对照最稳妥。
|
||||
|
||||
**Q5:多人同时用一台电脑?**
|
||||
本系统是**单机程序**,同一时刻一个 `app.db`。如需多人协作,应部署为共享服务或错峰使用,并勤于备份。
|
||||
|
||||
**Q6:忘记 admin 密码?**
|
||||
关闭程序,用备份恢复,或联系部署人重置 `app.db`(暂无自助找回,需直接改库)。
|
||||
|
||||
---
|
||||
|
||||
## 附:快速上手清单
|
||||
|
||||
1. 双击 `MaterialWMS.exe`,浏览器打开,用 `admin / admin123` 登录。
|
||||
2. 下载「物料导入模板」,把现有物料清单填好导入。
|
||||
3. 用「BOM 批量导入窗口」上传嘉立创 BOM(或粘贴文本),建好产品用料。
|
||||
4. 日常收发货:入库 / 出库 单笔或批量录入。
|
||||
5. 接单后:生产订单新建 → 看缺料 → 一键领料。
|
||||
6. 每天看一眼「库存看板」的红/黄预警,每周备份一次 `app.db`。
|
||||
7. 给同事建独立账号并分配角色(admin 专属)。
|
||||
Reference in New Issue
Block a user