Files
star 882508aee9 feat: 物料编号可编辑 + 工作台行内编辑/搜索 + 编号冲突合并/删除
- 物料编辑页物料编号放开可编辑并校验唯一性
- 工厂工作台看板行内可编辑型号/单位/安全库存/物料编号
- 工作台新增物料编号/型号搜索过滤
- 编号冲突时支持合并到已存在物料(保留流水)或删除整条物料(删前备份)
2026-10-10 16:08:33 +08:00

191 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}工厂工作台 · {{ f.code }}{% endblock %}
{% block content %}
<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') }}" data-page-node-id="gLM7aoKY7uRuTSOFXVSBaL">← 返回工厂列表</a>
</div>
<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" 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" 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 '') }}"
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 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" data-page-node-id="0dcZdYpNL1yp8lUhBww5DZ">
{% for bd in bom_data %}
<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" 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 '') }}" 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" data-page-node-id="htEBvmB0x6pS6bfdx1SFaA">该 BOM 没有组成物料</p>
{% endif %}
</div>
{% else %}
<p class="muted" data-page-node-id="f188trWNRvLEGIEFhxwrC1">暂无 BOM</p>
{% endfor %}
</div>
<!-- 批量导入 -->
<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" 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>
<script>
function switchTab(name){
document.querySelectorAll('.tab').forEach(b=>b.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach(p=>p.style.display='none');
event.target.classList.add('active');
document.getElementById('tab-'+name).style.display='block';
}
function esc(s){
return (s==null?'':String(s)).replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 %}