feat: BOM型号可编辑 + 生产订单BOM去括号 + 物料明细各工厂实时库存

- BOM编辑页「型号」输入框取消 readonly,可修改;bom_edit 增加非空与唯一校验(重复提示「BOM 型号已存在」、空值提示「BOM 型号必填」),改名不影响关联生产订单与流水
- 生产订单列表/详情/表单中 BOM 显示由 CODE (NAME) 改为 CODE · NAME,去掉括号
- 物料明细页新增「各工厂实时库存」表(工厂编号/名称/实时库存),显示每个工厂的实时聚合库存(入−出),不受顶栏工厂过滤影响
This commit is contained in:
2026-10-15 14:03:16 +08:00
parent 012e018b12
commit 639d7f1545
6 changed files with 44 additions and 5 deletions
+24 -1
View File
@@ -831,11 +831,21 @@ def material_detail(mid):
stmt_out.order_by(Outbound.occur_date.desc(), Outbound.id.desc())
).all()
fmap = {f.id: f.name for f in db.session.scalars(db.select(Factory)).all()}
# 各工厂实时库存(按工厂拆分, 实时聚合 inbound-outbound)
inb_by_f = {r[0]: float(r[1]) for r in db.session.execute(
db.text("SELECT factory_id, COALESCE(SUM(qty),0) FROM inbound WHERE material_id=:mid GROUP BY factory_id"),
{"mid": mid}).fetchall()}
out_by_f = {r[0]: float(r[1]) for r in db.session.execute(
db.text("SELECT factory_id, COALESCE(SUM(qty),0) FROM outbound WHERE material_id=:mid GROUP BY factory_id"),
{"mid": mid}).fetchall()}
per_factory = [{"f": f, "stock": inb_by_f.get(f.id, 0) - out_by_f.get(f.id, 0)}
for f in db.session.scalars(db.select(Factory).order_by(Factory.code)).all()]
return render_template("material_detail.html", m=m,
stock=sm.get(m.id, 0),
inb_total=inb_total.get(m.id, 0),
out_total=out_total.get(m.id, 0),
inbounds=inbounds, outbounds=outbounds, fmap=fmap)
inbounds=inbounds, outbounds=outbounds, fmap=fmap,
per_factory=per_factory)
# ---------------- 入库 / 出库 ----------------
@@ -1366,8 +1376,21 @@ def bom_edit(bid):
return redirect(url_for("boms"))
mats = db.session.scalars(db.select(Material).order_by(Material.part_number)).all()
if request.method == "POST":
# BOM 型号(简码)可编辑: 校验非空 + 不与其它 BOM 重复
new_code = request.form.get("code", "").strip()
b.name = request.form.get("name", "")
b.note = request.form.get("note", "")
raw = db.session.scalars(db.select(BomItem).where(BomItem.bom_id == b.id)).all()
items = [{"pn": db.session.get(Material, it.material_id).part_number,
"qty": it.qty_per_unit} for it in raw]
if not new_code:
flash("BOM 型号必填", "error")
return render_template("bom_form.html", b=b, mats=mats, items=items, form_code=new_code)
dup = db.session.scalar(db.select(Bom).where(Bom.code == new_code, Bom.id != b.id))
if dup:
flash("BOM 型号已存在", "error")
return render_template("bom_form.html", b=b, mats=mats, items=items, form_code=new_code)
b.code = new_code
db.session.execute(db.delete(BomItem).where(BomItem.bom_id == b.id))
pns = request.form.getlist("pn")
qts = request.form.getlist("qty")
+1 -1
View File
@@ -4,7 +4,7 @@
<form method="post" class="card form-card" style="max-width:640px">
<h3>{{ '编辑 BOM' if b else '新增 BOM' }}</h3>
<label>BOM 型号 *</label>
<input name="code" value="{{ b.code if b else '' }}" required {% if b %}readonly{% endif %}>
<input name="code" value="{{ form_code if form_code is defined else (b.code if b else '') }}" required>
<label>名称</label>
<input name="name" value="{{ b.name if b else '' }}">
<label>备注</label>
+16
View File
@@ -27,6 +27,22 @@
<div class="stat"><div class="num">{{ '%.2f'|format(out_total) }}</div><div class="lbl">消耗总数量</div></div>
</div>
<h3>各工厂实时库存(实时聚合)</h3>
<table class="tbl">
<thead><tr><th>工厂编号</th><th>工厂名称</th><th>实时库存</th></tr></thead>
<tbody>
{% for pf in per_factory %}
<tr>
<td>{{ pf.f.code }}</td>
<td>{{ pf.f.name or '-' }}</td>
<td class="num">{{ '%.2f'|format(pf.stock) }}</td>
</tr>
{% else %}
<tr><td colspan="3" class="muted">暂无工厂</td></tr>
{% endfor %}
</tbody>
</table>
<h3>入库流水(每次入库时间 / 数量)</h3>
<table class="tbl">
<thead><tr><th>入库日期</th><th>工厂</th><th>送货日期</th><th>录入时间</th><th>数量</th><th>批次号</th><th>来源 / 供应商</th><th>经手人</th><th>备注</th></tr></thead>
+1 -1
View File
@@ -8,7 +8,7 @@
</div>
<div class="card" style="margin-bottom:16px">
<p class="small muted" style="margin-top:0"><b>下单信息(按下单时记录)</b></p>
<p>BOM: <b>{{ bom.code if bom else '-' }}</b> ({{ bom.name if bom else '' }}) | 接单量: <b>{{ '%.2f'|format(o.qty) }}</b> | 生产工厂: <b>{% if factory %}{{ factory.code }}{% if factory.name %} · {{ factory.name }}{% endif %}{% else %}未指定(按汇总库存){% endif %}</b> | 经手人: {{ o.operator or '-' }}{% if o.created_at %} | 下单时间: {{ o.created_at.strftime('%Y-%m-%d %H:%M') }}{% endif %}</p>
<p>BOM: <b>{{ bom.code if bom else '-' }}</b>{% if bom and bom.name %} · {{ bom.name }}{% endif %} | 接单量: <b>{{ '%.2f'|format(o.qty) }}</b> | 生产工厂: <b>{% if factory %}{{ factory.code }}{% if factory.name %} · {{ factory.name }}{% endif %}{% else %}未指定(按汇总库存){% endif %}</b> | 经手人: {{ o.operator or '-' }}{% if o.created_at %} | 下单时间: {{ o.created_at.strftime('%Y-%m-%d %H:%M') }}{% endif %}</p>
{% if o.remark %}<p class="small muted">备注: {{ o.remark }}</p>{% endif %}
<p class="small muted" style="margin-bottom:0">提示:点击「编辑订单」保存后,该订单会冲销已产生的调入/领料流水并清空调入计划,自动回到「刚下单」状态(待领料、未调拨),可重新规划。</p>
</div>
+1 -1
View File
@@ -11,7 +11,7 @@
<select name="bom_id" required>
<option value="">-- 选择 BOM --</option>
{% for b in boms %}
<option value="{{ b.id }}" {% if o and o.bom_id == b.id %}selected{% endif %}>{{ b.code }} ({{ b.name or '-' }})</option>
<option value="{{ b.id }}" {% if o and o.bom_id == b.id %}selected{% endif %}>{{ b.code }}{% if b.name %} · {{ b.name }}{% endif %}</option>
{% endfor %}
</select>
<label>接单量 (台/套) *</label>
+1 -1
View File
@@ -11,7 +11,7 @@
{% for d in data %}
<tr>
<td>{{ d.o.order_no }}</td>
<td>{{ d.bom.code if d.bom else '-' }} ({{ d.bom.name if d.bom else '' }})</td>
<td>{{ d.bom.code if d.bom else '-' }}{% if d.bom and d.bom.name %} · {{ d.bom.name }}{% endif %}</td>
<td class="num">{{ '%.2f'|format(d.o.qty) }}</td>
<td>{% if d.factory %}{{ d.factory.code }}{% if d.factory.name %} · {{ d.factory.name }}{% endif %}{% else %}未指定{% endif %}</td>
<td><span class="badge {{ 'ok' if d.o.status=='picked' else 'warn' }}">{{ '已领料' if d.o.status=='picked' else '待领料' }}</span></td>