feat: 物料编号可编辑 + 工作台行内编辑/搜索 + 编号冲突合并/删除
- 物料编辑页物料编号放开可编辑并校验唯一性 - 工厂工作台看板行内可编辑型号/单位/安全库存/物料编号 - 工作台新增物料编号/型号搜索过滤 - 编号冲突时支持合并到已存在物料(保留流水)或删除整条物料(删前备份)
This commit is contained in:
@@ -17,6 +17,7 @@ from functools import wraps
|
||||
|
||||
from flask import (Flask, render_template, request, redirect, url_for,
|
||||
session, flash, g, send_file, jsonify)
|
||||
from sqlalchemy import update
|
||||
|
||||
from models import (db, User, Material, Inbound, Outbound,
|
||||
Bom, BomItem, ProdOrder, ProdOrderAlloc, OperationLog, Factory, Transfer, Substitute)
|
||||
@@ -456,7 +457,12 @@ def factory_workbench(fid):
|
||||
f = db.session.get(Factory, fid)
|
||||
if not f:
|
||||
return redirect(url_for("factories"))
|
||||
mats = db.session.scalars(db.select(Material).order_by(Material.part_number)).all()
|
||||
q = (request.args.get("q") or "").strip()
|
||||
stmt = db.select(Material)
|
||||
if q:
|
||||
like = f"%{q}%"
|
||||
stmt = stmt.where((Material.part_number.like(like)) | (Material.manufacturer.like(like)))
|
||||
mats = db.session.scalars(stmt.order_by(Material.part_number)).all()
|
||||
sm = stock_map(fid)
|
||||
rows = []
|
||||
negative = low = 0
|
||||
@@ -481,7 +487,7 @@ def factory_workbench(fid):
|
||||
comps.append({"m": mm, "qty": it.qty_per_unit, "stock": st,
|
||||
"status": _status_of(st, mm.safety_stock)})
|
||||
bom_data.append({"b": b, "comps": comps})
|
||||
return render_template("factory_workbench.html", f=f, rows=rows,
|
||||
return render_template("factory_workbench.html", f=f, rows=rows, q=q,
|
||||
negative=negative, low=low, bom_data=bom_data)
|
||||
|
||||
|
||||
@@ -539,6 +545,125 @@ def factory_import_materials(fid):
|
||||
return redirect(url_for("factory_workbench", fid=fid))
|
||||
|
||||
|
||||
def _merge_material_into(src_id, dst_id):
|
||||
"""把物料 src_id 合并到 dst_id: 转移所有引用(src->dst), 去重替代料, 删除 src。
|
||||
|
||||
系统中所有外键均指向整数主键 material.id (不指向 part_number),
|
||||
所以"允许物料编号改成已存在的号" = 把当前物料并入那个已存在物料。
|
||||
合并后 dst 保留其编号, src 的库存/出入库/BOM组成/替代料/调拨/生产订单调入计划并入 dst。
|
||||
"""
|
||||
# 1) 转移所有引用该物料的记录到目标物料
|
||||
for tbl in (Inbound, Outbound, BomItem, ProdOrderAlloc, Transfer):
|
||||
db.session.execute(update(tbl).where(tbl.material_id == src_id).values(material_id=dst_id))
|
||||
db.session.execute(update(Substitute).where(Substitute.main_id == src_id).values(main_id=dst_id))
|
||||
db.session.execute(update(Substitute).where(Substitute.sub_id == src_id).values(sub_id=dst_id))
|
||||
db.session.flush()
|
||||
# 2) 替代料去重 + 去除自引用(main==sub)
|
||||
seen = set()
|
||||
for s in db.session.scalars(
|
||||
db.select(Substitute).where((Substitute.main_id == dst_id) | (Substitute.sub_id == dst_id))).all():
|
||||
if s.main_id == s.sub_id:
|
||||
db.session.delete(s)
|
||||
continue
|
||||
key = (s.main_id, s.sub_id)
|
||||
if key in seen:
|
||||
db.session.delete(s)
|
||||
continue
|
||||
seen.add(key)
|
||||
db.session.flush()
|
||||
# 3) 删除源物料
|
||||
src = db.session.get(Material, src_id)
|
||||
if src:
|
||||
db.session.delete(src)
|
||||
|
||||
|
||||
@app.route("/factory/<int:fid>/material/<int:mid>/quickedit", methods=["POST"])
|
||||
@login_required
|
||||
@role_required("admin", "manager")
|
||||
def factory_material_quickedit(fid, mid):
|
||||
"""工厂工作台内联快速编辑物料主数据(物料编号、型号/规格、单位、安全库存)。
|
||||
|
||||
不修改库存(实时聚合)。物料编号是唯一业务编号, 改前校验全局唯一(排除自身)。
|
||||
保存后返回 JSON, 供前端原地刷新。
|
||||
"""
|
||||
f = db.session.get(Factory, fid)
|
||||
m = db.session.get(Material, mid)
|
||||
if not f or not m:
|
||||
return jsonify({"ok": False, "msg": "工厂或物料不存在"}), 404
|
||||
|
||||
# 物料编号(可编辑, 允许改成"已存在的号" = 合并到那个物料)
|
||||
new_pn = (request.form.get("part_number") or "").strip()
|
||||
old_pn = (m.part_number or "").strip()
|
||||
# 仅当真正改变(去除首尾空白后不同)才做处理, 避免只是重新输入同样编号(或多了空格)被误判
|
||||
if new_pn and new_pn != old_pn:
|
||||
dup = db.session.scalar(
|
||||
db.select(Material).where(Material.part_number == new_pn, Material.id != mid))
|
||||
if dup:
|
||||
# 已存在的号: 默认先询问是否合并(前端二次确认后带 confirm_merge 重提)
|
||||
if not request.form.get("confirm_merge"):
|
||||
return jsonify({
|
||||
"ok": False, "needs_confirm": True, "dup_id": dup.id,
|
||||
"msg": (f"物料编号 {new_pn} 已存在 (已被物料 id={dup.id} "
|
||||
f"{dup.manufacturer or ''} 占用, 该物料可能不在当前工厂看板显示)。\n"
|
||||
f"点「确定」将把当前物料【合并】到该编号物料: 其库存/出入库/BOM/替代料/调拨"
|
||||
f"记录并入目标, 当前物料记录被删除。\n"
|
||||
f"若不想合并, 请点「取消」并另填一个不重复的编号。")
|
||||
}), 409
|
||||
# 确认合并: 把 m 并入 dup
|
||||
_merge_material_into(m.id, dup.id)
|
||||
db.session.commit()
|
||||
log_action("工厂工作台合并物料", f"{old_pn}(id={m.id}) -> {new_pn}(id={dup.id})", f"厂 {f.code}")
|
||||
return jsonify({"ok": True, "merged": True, "part_number": dup.part_number,
|
||||
"msg": f"已合并到 {new_pn}; 原记录删除"})
|
||||
m.part_number = new_pn
|
||||
|
||||
m.manufacturer = request.form.get("manufacturer", m.manufacturer) or ""
|
||||
m.unit = (request.form.get("unit", m.unit) or "个") or "个"
|
||||
try:
|
||||
m.safety_stock = float(request.form.get("safety_stock") or 0)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
m.note = request.form.get("note", m.note) or m.note
|
||||
db.session.commit()
|
||||
log_action("工厂工作台编辑物料", f"{old_pn} -> {m.part_number}", f"厂 {f.code}")
|
||||
return jsonify({"ok": True, "part_number": m.part_number, "manufacturer": m.manufacturer,
|
||||
"unit": m.unit, "safety_stock": m.safety_stock})
|
||||
|
||||
|
||||
@app.route("/factory/<int:fid>/material/<int:mid>/delete", methods=["POST"])
|
||||
@login_required
|
||||
@role_required("admin", "manager")
|
||||
def factory_material_delete(fid, mid):
|
||||
"""删除整条物料记录(及其所有库存/出入库/BOM组成/替代料/调拨/生产订单调入计划)。
|
||||
|
||||
⚠️ 破坏性操作: 删除前自动备份数据库到 _backup/del_mat_<时间戳>.db。
|
||||
"""
|
||||
f = db.session.get(Factory, fid)
|
||||
m = db.session.get(Material, mid)
|
||||
if not f or not m:
|
||||
return jsonify({"ok": False, "msg": "工厂或物料不存在"}), 404
|
||||
pn = m.part_number
|
||||
# 删除前备份
|
||||
try:
|
||||
_bak = os.path.join(EXE_DIR, "_backup", f"del_mat_{datetime.now():%Y%m%d_%H%M%S}.db")
|
||||
os.makedirs(os.path.dirname(_bak), exist_ok=True)
|
||||
shutil.copy(db_path, _bak)
|
||||
except Exception:
|
||||
_bak = None
|
||||
# 删除所有引用该物料的记录
|
||||
for tbl in (Inbound, Outbound, BomItem, ProdOrderAlloc, Transfer):
|
||||
db.session.query(tbl).filter(tbl.material_id == mid).delete(synchronize_session=False)
|
||||
db.session.query(Substitute).filter(
|
||||
(Substitute.main_id == mid) | (Substitute.sub_id == mid)).delete(synchronize_session=False)
|
||||
db.session.delete(m)
|
||||
db.session.commit()
|
||||
log_action("工厂工作台删除物料", pn,
|
||||
f"厂 {f.code}" + (f"; 备份 {os.path.basename(_bak)}" if _bak else "(备份失败)"))
|
||||
return jsonify({"ok": True, "msg": f"已删除物料 {pn} 及其全部关联记录"
|
||||
+ (f"(数据库已备份 {os.path.basename(_bak)})" if _bak else "")})
|
||||
|
||||
|
||||
|
||||
@app.route("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
@@ -638,12 +763,24 @@ def material_edit(mid):
|
||||
if not m:
|
||||
return redirect(url_for("materials"))
|
||||
if request.method == "POST":
|
||||
new_pn = (request.form.get("part_number") or "").strip()
|
||||
old_pn = m.part_number
|
||||
if not new_pn:
|
||||
flash("物料编号不能为空", "error")
|
||||
return render_template("material_form.html", m=m)
|
||||
# 唯一性校验(排除自身)
|
||||
dup = db.session.scalar(
|
||||
db.select(Material).where(Material.part_number == new_pn, Material.id != mid))
|
||||
if dup:
|
||||
flash(f"物料编号 {new_pn} 已存在, 请更换", "error")
|
||||
return render_template("material_form.html", m=m)
|
||||
m.part_number = new_pn
|
||||
m.manufacturer = request.form.get("manufacturer", "")
|
||||
m.unit = request.form.get("unit", "个") or "个"
|
||||
m.safety_stock = float(request.form.get("safety_stock") or 0)
|
||||
m.note = request.form.get("note", "")
|
||||
db.session.commit()
|
||||
log_action("编辑物料", m.part_number)
|
||||
log_action("编辑物料", f"{old_pn} -> {new_pn}")
|
||||
flash("已保存", "ok")
|
||||
return redirect(url_for("materials"))
|
||||
return render_template("material_form.html", m=m)
|
||||
|
||||
@@ -44,12 +44,17 @@ label { display: block; margin: 10px 0 4px; font-size: 13px; color: #44525f; }
|
||||
input, select, textarea {
|
||||
width: 100%; padding: 8px 10px; border: 1px solid #cdd5df; border-radius: 6px; font-size: 14px;
|
||||
}
|
||||
.cell-inp { width: 100%; box-sizing: border-box; padding: 4px 6px; border: 1px solid #c9d2dc; border-radius: 4px; font-size: 13px; }
|
||||
.cell-inp.num { text-align: right; }
|
||||
.readonly-field { width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid #dfe3e8; border-radius: 6px; background: #f1f3f6; color: #6b7280; font-size: 14px; }
|
||||
.btn.small, button.small { padding: 5px 10px; font-size: 12px; border-radius: 5px; }
|
||||
button, .btn {
|
||||
display: inline-block; cursor: pointer; border: none; border-radius: 6px;
|
||||
padding: 9px 16px; font-size: 14px; text-decoration: none; background: #eef1f5; color: #1f2733;
|
||||
}
|
||||
button.primary, .btn.primary { background: #1f3a5f; color: #fff; }
|
||||
button.secondary, .btn.secondary { background: #2f7d5b; color: #fff; }
|
||||
button.danger, .btn.danger { background: #e6453c; color: #fff; border-color: #d23b32; }
|
||||
button:hover, .btn:hover { filter: brightness(.96); }
|
||||
.link-danger { background: none; border: none; color: #c0392b; cursor: pointer; padding: 0 4px; font-size: 14px; }
|
||||
.link-danger:hover { text-decoration: underline; }
|
||||
|
||||
@@ -2,93 +2,107 @@
|
||||
{% block title %}工厂工作台 · {{ f.code }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="toolbar">
|
||||
<h3 style="margin:0">工厂工作台:{{ f.code }} · {{ f.name or '' }}
|
||||
<span class="badge {{ 'ok' if f.enabled else 'warn' }}">{{ '启用' if f.enabled else '停用' }}</span>
|
||||
<div class="toolbar" data-page-node-id="h8tr6moQVxGSdE9dKEg6bd">
|
||||
<h3 style="margin:0" data-page-node-id="mN5n1l6U4DnPTMkfHhjkpo">工厂工作台:{{ f.code }} · {{ f.name or '' }}
|
||||
<span class="badge {{ 'ok' if f.enabled else 'warn' }}" data-page-node-id="9lYmmds1mElbCd1ib8GNYD">{{ '启用' if f.enabled else '停用' }}</span>
|
||||
</h3>
|
||||
<a class="btn" href="{{ url_for('factories') }}">← 返回工厂列表</a>
|
||||
<a class="btn" href="{{ url_for('factories') }}" data-page-node-id="gLM7aoKY7uRuTSOFXVSBaL">← 返回工厂列表</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('kanban')">📊 物料看板</button>
|
||||
<button class="tab" onclick="switchTab('bom')">🧩 BOM 齐套</button>
|
||||
<button class="tab" onclick="switchTab('import')">⊞ 批量导入</button>
|
||||
<div class="tabs" data-page-node-id="ante6pBhAiQZDcPHSkk7pm">
|
||||
<button class="tab active" onclick="switchTab('kanban')" data-page-node-id="t8Y9L0Rv6nJWGYeAc4Butp">📊 物料看板</button>
|
||||
<button class="tab" onclick="switchTab('bom')" data-page-node-id="1vqssKcNPuRFxIfonFhl2W">🧩 BOM 齐套</button>
|
||||
<button class="tab" onclick="switchTab('import')" data-page-node-id="VulCglmTCTpl2I8BI40TEX">⊞ 批量导入</button>
|
||||
</div>
|
||||
|
||||
<!-- 物料看板 -->
|
||||
<div id="tab-kanban" class="tab-pane">
|
||||
<div class="stat-row" style="margin:12px 0">
|
||||
<div class="stat"><div class="num">{{ rows|length }}</div><div class="lbl">有库存物料</div></div>
|
||||
<div class="stat warn"><div class="num">{{ low }}</div><div class="lbl">低于安全库存</div></div>
|
||||
<div class="stat danger"><div class="num">{{ negative }}</div><div class="lbl">库存为负</div></div>
|
||||
<div id="tab-kanban" class="tab-pane" data-page-node-id="O5tr8yIq5PuggrETsUYwhX">
|
||||
<form method="get" action="{{ url_for('factory_workbench', fid=f.id) }}" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:12px 0" data-page-node-id="DP9ksglRvmmkKQtWX8GzZC">
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="搜索物料编号 / 型号规格"
|
||||
style="width:260px;padding:8px 10px;border:1px solid #cdd5df;border-radius:6px;font-size:14px" data-page-node-id="DtGH4sWm3Q8IaidV8Vcsvl">
|
||||
<button class="primary" type="submit" data-page-node-id="9zgPeVdBfILSpKfD4TsDB2">搜索</button>
|
||||
{% if q %}<a class="btn" href="{{ url_for('factory_workbench', fid=f.id) }}" data-page-node-id="y6UrlI6Z8PXiCHpif7cspa">清除</a>{% endif %}
|
||||
</form>
|
||||
{% if q %}<p class="small muted" style="margin:0 0 10px" data-page-node-id="bSjz9L1Hcc4kra4Z4DRTIT">筛选「{{ q }}」命中 <b data-page-node-id="gYjIGP7vAGwZROP3und6Mt">{{ rows|length }}</b> 条(仅含在厂库存不为 0 的物料)</p>{% endif %}
|
||||
<div class="stat-row" style="margin:12px 0" data-page-node-id="XdjnYnwPez6NSo8RabtdEw">
|
||||
<div class="stat" data-page-node-id="iWmneOIg5GaNg13oi8Tpml"><div class="num" data-page-node-id="sDfnYz7FDudHWa1F7hWxUj">{{ rows|length }}</div><div class="lbl" data-page-node-id="yUuytA7Ee2uZjYvyOVwdGW">有库存物料</div></div>
|
||||
<div class="stat warn" data-page-node-id="9WprNxfiSAhkHKJoAfKVUs"><div class="num" data-page-node-id="PFYgLN2kA2eh3ehng7sL4E">{{ low }}</div><div class="lbl" data-page-node-id="cFVCxICUvwmYDEFJZ5LDBc">低于安全库存</div></div>
|
||||
<div class="stat danger" data-page-node-id="OqyIPhwXMA2FKrB5ElPOnP"><div class="num" data-page-node-id="ymG0ZfkOIrB5d4yj8v3EMR">{{ negative }}</div><div class="lbl" data-page-node-id="EgDYjJQoOn0L8dkCLwkgE1">库存为负</div></div>
|
||||
</div>
|
||||
<table class="tbl">
|
||||
<thead><tr><th>物料编号</th><th>型号/规格</th><th>单位</th><th>安全库存</th><th>在厂库存</th><th>状态</th></tr></thead>
|
||||
<tbody>
|
||||
<table class="tbl" data-page-node-id="v2dpNrmLrYl4iBusCHbYId">
|
||||
<thead data-page-node-id="pfxxaWDexxHnrtTIFGOSvu"><tr data-page-node-id="7tQDDZE1giJATqe3rbI0V0"><th data-page-node-id="7PQzAHQJqUuBvUEQahPffK">物料编号</th><th data-page-node-id="u4s8ycQodHcEKS8Aou9MLv">型号/规格</th><th data-page-node-id="BJqNtTZALVaxrbIcqkZsb1">单位</th><th data-page-node-id="GIF5kOqgw826EHX7PEfF27">安全库存</th><th data-page-node-id="SK7R13nwY2nvGYlhCeRu37">在厂库存</th><th data-page-node-id="wImv6pW0xYFq35YHkUHEqk">状态</th><th data-page-node-id="MwGW0LuvnhgNhyhix4ge2L">操作</th></tr></thead>
|
||||
<tbody data-page-node-id="taNmAZ8i4abDFIQijzVWOs">
|
||||
{% for r in rows %}
|
||||
<tr class="{{ 'row-danger' if r.status=='danger' else ('row-warn' if r.status=='warn' else '') }}">
|
||||
<td>{{ r.m.part_number }}</td>
|
||||
<td>{{ r.m.manufacturer or '-' }}</td>
|
||||
<td>{{ r.m.unit or '-' }}</td>
|
||||
<td class="num">{{ r.m.safety_stock or 0 }}</td>
|
||||
<td class="num">{{ '%.2f'|format(r.stock) }}</td>
|
||||
<td><span class="badge {{ 'danger' if r.status=='danger' else ('warn' if r.status=='warn' else 'ok') }}">{{ '负库存' if r.status=='danger' else ('低于安全库存' if r.status=='warn' else '正常') }}</span></td>
|
||||
<tr class="{{ 'row-danger' if r.status=='danger' else ('row-warn' if r.status=='warn' else '') }}"
|
||||
data-mid="{{ r.m.id }}" data-fid="{{ f.id }}"
|
||||
data-pn="{{ r.m.part_number }}" data-mf="{{ r.m.manufacturer or '' }}"
|
||||
data-unit="{{ r.m.unit or '' }}" data-ss="{{ r.m.safety_stock or 0 }}" data-page-node-id="2cidlTmV6rQwsq9PrbpeeR">
|
||||
<td data-page-node-id="wJDGDx053XiyTJva6zouSf">{{ r.m.part_number }}</td>
|
||||
<td data-page-node-id="GLAB4ys6K8tlRL09h0SVSX">{{ r.m.manufacturer or '-' }}</td>
|
||||
<td data-page-node-id="VDNrmbPZHQlBIJ05AoQly5">{{ r.m.unit or '-' }}</td>
|
||||
<td class="num" data-page-node-id="EjDg4DmtWkB5OjEr7Jaw0J">{{ r.m.safety_stock or 0 }}</td>
|
||||
<td class="num" data-page-node-id="V5Fp579iiSV2MBYW3ZF2i6">{{ '%.2f'|format(r.stock) }}</td>
|
||||
<td data-page-node-id="z4HE32qyI5SFj3Kkb0rhS2"><span class="badge {{ 'danger' if r.status=='danger' else ('warn' if r.status=='warn' else 'ok') }}" data-page-node-id="n6KadEnTE72NuD06GKXEIx">{{ '负库存' if r.status=='danger' else ('低于安全库存' if r.status=='warn' else '正常') }}</span></td>
|
||||
<td data-page-node-id="7DvKrplCD4RKbwZvM67dCa">
|
||||
<span id="acts_{{ r.m.id }}" data-page-node-id="LvRHno48lkdWEGjdKxTHXX"><button class="btn small" type="button" onclick="editRow(this)" data-page-node-id="NSU10L6H6zY5TGeTPGQFEz">编辑</button></span>
|
||||
<button class="btn small danger" type="button" onclick="delRow(this)" data-page-node-id="0sV7BYBp0CUIRvBU73nREM">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="muted">该厂暂无库存数据</td></tr>
|
||||
<tr data-page-node-id="osy9WfjSd5DiIkWUlHquBR"><td colspan="7" class="muted" data-page-node-id="TR0Ye9v5IRzsMkB8O5DadZ">该厂暂无库存数据</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- BOM 齐套 -->
|
||||
<div id="tab-bom" class="tab-pane" style="display:none">
|
||||
<div id="tab-bom" class="tab-pane" style="display:none" data-page-node-id="0dcZdYpNL1yp8lUhBww5DZ">
|
||||
{% for bd in bom_data %}
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3 style="margin-top:0">{{ bd.b.code }} ({{ bd.b.name or '-' }})
|
||||
<span class="small muted">· 单套用量 × 该厂库存</span></h3>
|
||||
<div class="card" style="margin-bottom:16px" data-page-node-id="KQOJWc5nHbG4VjhSaQPuMk">
|
||||
<h3 style="margin-top:0" data-page-node-id="ycs6WD46CdMJ8UcSRCy5f6">{{ bd.b.code }} ({{ bd.b.name or '-' }})
|
||||
<span class="small muted" data-page-node-id="U6NLfEkrS2iQqgZimxy4zl">· 单套用量 × 该厂库存</span></h3>
|
||||
{% if bd.comps %}
|
||||
<table class="tbl">
|
||||
<thead><tr><th>物料编号</th><th>型号/规格</th><th>单套用量</th><th>该厂库存</th><th>状态</th></tr></thead>
|
||||
<tbody>
|
||||
<table class="tbl" data-page-node-id="UdJnn9kfq0ajEZWGKyzVOL">
|
||||
<thead data-page-node-id="ARRG4xD4kWgudXephSkWRB"><tr data-page-node-id="nlsJQ9mpAJYIhFm75kH05N"><th data-page-node-id="V2dk9RMlqoxhgKdJtcLOMh">物料编号</th><th data-page-node-id="Kjj7qJMrh899UoknQcDLNA">型号/规格</th><th data-page-node-id="pkEIgqReAFzWQmTG58bBhE">单套用量</th><th data-page-node-id="amLBx4jB5xYtw9KX74SBbU">该厂库存</th><th data-page-node-id="4cEYL4lgAaUBmObLLkX0Km">状态</th></tr></thead>
|
||||
<tbody data-page-node-id="mhRWoazwMuPyFzrrAIVuq7">
|
||||
{% for c in bd.comps %}
|
||||
<tr class="{{ 'row-danger' if c.status=='danger' else ('row-warn' if c.status=='warn' else '') }}">
|
||||
<td>{{ c.m.part_number }}</td>
|
||||
<td>{{ c.m.manufacturer or '-' }}</td>
|
||||
<td class="num">{{ '%.3f'|format(c.qty) }}</td>
|
||||
<td class="num">{{ '%.2f'|format(c.stock) }}</td>
|
||||
<td><span class="badge {{ 'danger' if c.status=='danger' else ('warn' if c.status=='warn' else 'ok') }}">{{ '负库存' if c.status=='danger' else ('低于安全库存' if c.status=='warn' else '正常') }}</span></td>
|
||||
<tr class="{{ 'row-danger' if c.status=='danger' else ('row-warn' if c.status=='warn' else '') }}" data-page-node-id="9d7M31Zqmc5feItheDMB71">
|
||||
<td data-page-node-id="jv8YvfrtYpEsLkFsFyrwiO">{{ c.m.part_number }}</td>
|
||||
<td data-page-node-id="pRtaop8GPgnCJGFS4Cugle">{{ c.m.manufacturer or '-' }}</td>
|
||||
<td class="num" data-page-node-id="3uop7BWAV6eYdlVz5eqakf">{{ '%.3f'|format(c.qty) }}</td>
|
||||
<td class="num" data-page-node-id="jfNj0QeLPUrrBSfsPjzsHT">{{ '%.2f'|format(c.stock) }}</td>
|
||||
<td data-page-node-id="MToeQ16NLjIvuPdfGFLojv"><span class="badge {{ 'danger' if c.status=='danger' else ('warn' if c.status=='warn' else 'ok') }}" data-page-node-id="3a5chV8jORWtOZKE3kVKYM">{{ '负库存' if c.status=='danger' else ('低于安全库存' if c.status=='warn' else '正常') }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="small muted">该 BOM 没有组成物料</p>
|
||||
<p class="small muted" data-page-node-id="htEBvmB0x6pS6bfdx1SFaA">该 BOM 没有组成物料</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">暂无 BOM</p>
|
||||
<p class="muted" data-page-node-id="f188trWNRvLEGIEFhxwrC1">暂无 BOM</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 批量导入 -->
|
||||
<div id="tab-import" class="tab-pane" style="display:none">
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3 style="margin-top:0">导入入库流水到 {{ f.code }}</h3>
|
||||
<p class="small muted">上传 Excel/CSV(列:物料编号 / 数量 / 批次 / 日期 / 备注),将生成归属 {{ f.code }} 的入库流水。</p>
|
||||
<form method="post" action="{{ url_for('factory_import_inbound', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||
<input type="file" name="file" accept=".xlsx,.csv" required>
|
||||
<button class="primary" type="submit">上传并导入入库</button>
|
||||
<a class="btn" href="{{ url_for('download_template', kind='transactions') }}">⬇ 下载模板</a>
|
||||
<div id="tab-import" class="tab-pane" style="display:none" data-page-node-id="NRRM2DunropDEi85SDdAeY">
|
||||
<div class="card" style="margin-bottom:16px" data-page-node-id="pmIAFmv6QG2cAhVHKguzCC">
|
||||
<h3 style="margin-top:0" data-page-node-id="gAGSs6J0QvEnz8OmMFZdaW">导入入库流水到 {{ f.code }}</h3>
|
||||
<p class="small muted" data-page-node-id="dRwIvucQurC2wYwZd7grcI">上传 Excel/CSV(列:物料编号 / 数量 / 批次 / 日期 / 备注),将生成归属 {{ f.code }} 的入库流水。</p>
|
||||
<form method="post" action="{{ url_for('factory_import_inbound', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap" data-page-node-id="Oli4teU6igJvERarUlj8i8">
|
||||
<input type="file" name="file" accept=".xlsx,.csv" required data-page-node-id="QvuR34OQvaVh8GOCZ4g69g">
|
||||
<button class="primary" type="submit" data-page-node-id="62RYuou3Nbtk1RAVVli1eE">上传并导入入库</button>
|
||||
<a class="btn" href="{{ url_for('download_template', kind='transactions') }}" data-page-node-id="XukAuHeUyL7T9wJs6aDfhT">⬇ 下载模板</a>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0">导入物料主数据(期初库存归属 {{ f.code }})</h3>
|
||||
<p class="small muted">上传物料 Excel(列:物料编号 / 型号 / 单位 / 安全库存 / 期初库存),期初库存将写入 {{ f.code }}。</p>
|
||||
<form method="post" action="{{ url_for('factory_import_materials', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
||||
<input type="file" name="file" accept=".xlsx,.csv" required>
|
||||
<button class="primary" type="submit">上传并导入物料</button>
|
||||
<a class="btn" href="{{ url_for('download_template', kind='materials') }}">⬇ 下载模板</a>
|
||||
<div class="card" data-page-node-id="G6s6IIwsdxtO2QoglkQu5O">
|
||||
<h3 style="margin-top:0" data-page-node-id="sBnAiFr1fC7vAnge2CYt9L">导入物料主数据(期初库存归属 {{ f.code }})</h3>
|
||||
<p class="small muted" data-page-node-id="NHW4ZB8CHbwjA6xqxkOAJK">上传物料 Excel(列:物料编号 / 型号 / 单位 / 安全库存 / 期初库存),期初库存将写入 {{ f.code }}。</p>
|
||||
<form method="post" action="{{ url_for('factory_import_materials', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap" data-page-node-id="9Y7Ec13v0Yo6okhmgErNLS">
|
||||
<input type="file" name="file" accept=".xlsx,.csv" required data-page-node-id="3PqBIl65X6AWBok5cKoO4S">
|
||||
<button class="primary" type="submit" data-page-node-id="iU26xXbC5XkB2oLWJtUVws">上传并导入物料</button>
|
||||
<a class="btn" href="{{ url_for('download_template', kind='materials') }}" data-page-node-id="o5mKNqqJtZJffB2hM595GA">⬇ 下载模板</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,5 +114,77 @@ function switchTab(name){
|
||||
event.target.classList.add('active');
|
||||
document.getElementById('tab-'+name).style.display='block';
|
||||
}
|
||||
|
||||
function esc(s){
|
||||
return (s==null?'':String(s)).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
// 进入某行编辑态: 物料编号、型号/规格、单位、安全库存 变为输入框
|
||||
function editRow(btn){
|
||||
var tr = btn.closest('tr');
|
||||
tr.dataset.view = tr.innerHTML; // 保存视图态, 供取消还原
|
||||
var mid = tr.dataset.mid, pn = tr.dataset.pn, mf = tr.dataset.mf, unit = tr.dataset.unit, ss = tr.dataset.ss;
|
||||
var c = tr.children;
|
||||
c[0].innerHTML = '<input class="cell-inp" id="pn_'+mid+'" value="'+esc(pn)+'">';
|
||||
c[1].innerHTML = '<input class="cell-inp" id="mf_'+mid+'" value="'+esc(mf)+'">';
|
||||
c[2].innerHTML = '<input class="cell-inp" id="unit_'+mid+'" value="'+esc(unit)+'">';
|
||||
c[3].innerHTML = '<input class="cell-inp num" id="ss_'+mid+'" type="number" step="0.01" min="0" value="'+esc(ss)+'">';
|
||||
document.getElementById('acts_'+mid).innerHTML =
|
||||
'<button class="btn small primary" type="button" onclick="saveRow(this)">保存</button> '
|
||||
+ '<button class="btn small" type="button" onclick="cancelRow(this)">取消</button>';
|
||||
}
|
||||
|
||||
function cancelRow(btn){
|
||||
var tr = btn.closest('tr');
|
||||
tr.innerHTML = tr.dataset.view; // 还原视图态
|
||||
}
|
||||
|
||||
// 提交保存(可递归: 二次确认合并后重提)
|
||||
function postSave(fid, mid, fd){
|
||||
fetch('/factory/'+fid+'/material/'+mid+'/quickedit', {method:'POST', body: fd})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
if(d.ok){
|
||||
location.reload(); // 安全库存/合并可能影响状态与列表, 整页刷新保证一致
|
||||
} else if(d.needs_confirm){
|
||||
if(confirm(d.msg)){
|
||||
fd.append('confirm_merge','1'); // 用户确认把当前物料合并到已存在的号
|
||||
postSave(fid, mid, fd);
|
||||
}
|
||||
} else {
|
||||
alert('保存失败: ' + (d.msg || '未知错误'));
|
||||
}
|
||||
})
|
||||
.catch(function(e){ alert('请求失败: ' + e); });
|
||||
}
|
||||
|
||||
function saveRow(btn){
|
||||
var tr = btn.closest('tr');
|
||||
var mid = tr.dataset.mid, fid = tr.dataset.fid;
|
||||
var pn = document.getElementById('pn_'+mid).value.trim();
|
||||
var mf = document.getElementById('mf_'+mid).value;
|
||||
var unit = document.getElementById('unit_'+mid).value;
|
||||
var ss = document.getElementById('ss_'+mid).value;
|
||||
if(!pn){ alert('物料编号不能为空'); return; }
|
||||
var fd = new FormData();
|
||||
fd.append('part_number', pn);
|
||||
fd.append('manufacturer', mf);
|
||||
fd.append('unit', unit);
|
||||
fd.append('safety_stock', ss);
|
||||
postSave(fid, mid, fd);
|
||||
}
|
||||
|
||||
// 删除整条物料记录(含其全部关联记录); 后端会自动备份数据库
|
||||
function delRow(btn){
|
||||
var tr = btn.closest('tr');
|
||||
var mid = tr.dataset.mid, fid = tr.dataset.fid;
|
||||
var pn = tr.dataset.pn;
|
||||
if(!confirm('确定删除物料「'+pn+'」?\n将同时删除其所有入库/出库/BOM组成/替代料/调拨/生产订单调入计划记录,'
|
||||
+ '且不可撤销(系统会自动备份数据库)。')) return;
|
||||
fetch('/factory/'+fid+'/material/'+mid+'/delete', {method:'POST'})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){ if(d.ok){ location.reload(); } else { alert('删除失败: ' + (d.msg||'未知错误')); } })
|
||||
.catch(function(e){ alert('请求失败: ' + e); });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<form method="post" class="card form-card">
|
||||
<h3>{{ '编辑物料' if m else '新增物料' }}</h3>
|
||||
<label>物料编号 *</label>
|
||||
<input name="part_number" value="{{ m.part_number if m else '' }}" {{ 'readonly' if m }} required>
|
||||
<input name="part_number" value="{{ m.part_number if m else '' }}" {{ 'required' if m else 'required' }}>
|
||||
<p class="small muted" style="margin-top:2px">物料编码为唯一业务编号(不可与其他物料重复)。</p>
|
||||
<label>型号 / 规格</label>
|
||||
<input name="manufacturer" value="{{ m.manufacturer if m else '' }}">
|
||||
<label>单位</label>
|
||||
|
||||
Reference in New Issue
Block a user