feat: 多工厂/厂间调拨/替代料/流水台账折叠 + 重写 README
- 多工厂(factory 表) + 顶栏工厂下拉过滤, 库存按厂聚合 - 厂间调拨(transfer): 源厂减+目标厂加, 总库存不变, 双向流水对账 - 替代料关系(substitute): 出库/BOM领料/生产领料缺料时提示可替代料 - 流水台账(ledger): 入出按日期合并折叠, 按日期导出同一 Excel - 生产订单增强: 编辑/删除(级联清关联流水)/调入数量/确认调入=厂间调拨 - 入库新增送货日期字段; 出库物料编码扫码定位 - 重写 README 补全全部模块; .gitignore 排除 dist_pkg/build_pkg/*.db - 停止跟踪 instance/app.db(运行时自动生成)
This commit is contained in:
@@ -23,6 +23,13 @@
|
||||
<option value="{{ m.id }}" data-pn="{{ m.part_number }}">{{ m.part_number }} ({{ m.manufacturer or '-' }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div id="subHint" class="sub-hint" style="margin-top:8px"></div>
|
||||
<label>所属工厂 *</label>
|
||||
<select name="factory_id" required>
|
||||
{% for f in factories %}
|
||||
<option value="{{ f.id }}" {% if (current_factory_id == f.id) or (not current_factory_id and loop.first) %}selected{% endif %}>{{ f.code }} · {{ f.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>批次号</label>
|
||||
<input name="batch_no">
|
||||
<label>数量 *</label>
|
||||
@@ -49,6 +56,43 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if kind == 'outbound' %}
|
||||
<div class="card bom-issue-card" style="margin-top:18px">
|
||||
<h3 style="margin-top:0">🧩 按 BOM 批量操作(领料出库 / 调拨入库)</h3>
|
||||
<p class="small muted">选择 BOM 与数量,系统按单套用量自动生成每种组件的出库/入库流水,任意数量均可。</p>
|
||||
<form method="post" action="{{ url_for('bom_issue') }}" class="form-card">
|
||||
<label>操作类型 *</label>
|
||||
<select name="mode" id="bomMode" onchange="onBomModeChange();refreshBomPreview()">
|
||||
<option value="pick">按BOM齐套领料出库(扣本厂库存)</option>
|
||||
<option value="transfer">按BOM调拨入库(调入目标厂)</option>
|
||||
</select>
|
||||
<label>BOM 型号 *</label>
|
||||
<select name="bom_id" id="bomSel" onchange="refreshBomPreview()" required>
|
||||
<option value="">-- 选择 BOM --</option>
|
||||
{% for b in boms %}
|
||||
<option value="{{ b.id }}">{{ b.code }} ({{ b.name or '-' }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>数量(台/套)*</label>
|
||||
<input name="qty" id="bomQty" type="number" step="0.01" min="0.01" oninput="refreshBomPreview()" required>
|
||||
<label id="facLabel">本厂 *</label>
|
||||
<select name="factory_id" id="bomFac" onchange="refreshBomPreview()">
|
||||
{% for f in factories %}
|
||||
<option value="{{ f.id }}" {% if (current_factory_id == f.id) or (not current_factory_id and loop.first) %}selected{% endif %}>{{ f.code }} · {{ f.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label>经手人</label>
|
||||
<input name="operator">
|
||||
<label>业务日期</label>
|
||||
<input name="occur_date" type="date" value="{{ today }}">
|
||||
<label>备注</label>
|
||||
<input name="remark">
|
||||
<div class="row-actions"><button class="primary" type="submit">生成流水</button></div>
|
||||
</form>
|
||||
<div id="bomPreview" style="margin-top:12px"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3>最近 {{ recs|length }} 条{{ '入库' if kind=='inbound' else '出库' }}记录</h3>
|
||||
<table class="tbl">
|
||||
<thead>
|
||||
@@ -213,5 +257,71 @@ function commitBatch(){
|
||||
if(d.added>0) location.reload();
|
||||
}).catch(e=>alert('导入失败: '+e));
|
||||
}
|
||||
|
||||
/* ---- 出库页 BOM 卡片: 实时需求预览 ---- */
|
||||
function refreshBomPreview(){
|
||||
const bid = document.getElementById('bomSel');
|
||||
const qty = document.getElementById('bomQty');
|
||||
const fac = document.getElementById('bomFac');
|
||||
const mode = document.getElementById('bomMode');
|
||||
if(!bid || !qty || !fac || !mode) return;
|
||||
const box = document.getElementById('bomPreview');
|
||||
if(!bid.value || !qty.value || parseFloat(qty.value) <= 0){ box.innerHTML=''; return; }
|
||||
const u = `/api/bom/preview?bom_id=${bid.value}&qty=${encodeURIComponent(qty.value)}&factory_id=${fac.value}&mode=${mode.value}`;
|
||||
fetch(u).then(r=>r.json()).then(d=>{
|
||||
if(!d.items.length){ box.innerHTML='<p class="small muted">该 BOM 无组成物料</p>'; return; }
|
||||
let html = '<table class="tbl"><thead><tr><th>物料编号</th><th>型号</th><th>需求数量</th><th>工厂库存</th><th>状态</th></tr></thead><tbody>';
|
||||
d.items.forEach(it=>{
|
||||
const cls = it.short ? 'row-danger' : '';
|
||||
const tag = it.short ? '<span class="badge danger">缺料</span>' : '<span class="badge ok">充足</span>';
|
||||
html += `<tr class="${cls}"><td>${it.pn}</td><td>${it.mf}</td><td class="num">${it.need}</td><td class="num">${it.stock}</td><td>${tag}</td></tr>`;
|
||||
if(it.subs && it.subs.length){
|
||||
const avail = it.subs.filter(s=>s.available);
|
||||
const subTxt = it.subs.map(s=>`${s.pn}(库存${s.stock}${s.available?' 有货':' 无货'})`).join(',');
|
||||
const subCls = avail.length ? 'row-in' : '';
|
||||
html += `<tr class="${subCls}"><td colspan="5" class="small muted">↳ 可替代料: ${subTxt}</td></tr>`;
|
||||
}
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
if(d.any_short && mode.value === 'pick'){
|
||||
html = '<div class="flash error">存在缺料, 提交将被拦截</div>' + html;
|
||||
}
|
||||
box.innerHTML = html;
|
||||
}).catch(e=>{ box.innerHTML=''; });
|
||||
}
|
||||
function onBomModeChange(){
|
||||
const mode = document.getElementById('bomMode');
|
||||
const lbl = document.getElementById('facLabel');
|
||||
if(mode && lbl){ lbl.textContent = (mode.value === 'pick') ? '本厂 *' : '目标厂 *'; }
|
||||
}
|
||||
if(document.getElementById('bomMode')){
|
||||
onBomModeChange();
|
||||
}
|
||||
|
||||
/* ---- 出库单物料: 实时提示可替代料 ---- */
|
||||
function refreshSubHint(){
|
||||
const sel = document.getElementById('materialSelect');
|
||||
const box = document.getElementById('subHint');
|
||||
if(!sel || !box) return;
|
||||
const mid = sel.value;
|
||||
if(!mid){ box.innerHTML=''; return; }
|
||||
fetch(`/api/substitutes/${mid}`).then(r=>r.json()).then(d=>{
|
||||
if(!d.subs || !d.subs.length){
|
||||
box.innerHTML = '<p class="small muted">该物料暂无登记替代料</p>';
|
||||
return;
|
||||
}
|
||||
let html = '<div class="card" style="padding:10px 12px"><b>可替代料(库存提示):</b>'
|
||||
+ '<table class="tbl"><thead><tr><th>替代料编号</th><th>型号</th><th>当前库存</th><th>状态</th></tr></thead><tbody>';
|
||||
d.subs.forEach(s=>{
|
||||
const cls = s.available ? 'row-in' : 'row-out';
|
||||
const tag = s.available ? '<span class="badge ok">有货</span>' : '<span class="badge danger">无货</span>';
|
||||
html += `<tr class="${cls}"><td>${s.pn}</td><td>${s.mf}</td><td class="num">${s.stock}</td><td>${tag}</td></tr>`;
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
box.innerHTML = html;
|
||||
}).catch(()=>{ box.innerHTML=''; });
|
||||
}
|
||||
const _subSel = document.getElementById('materialSelect');
|
||||
if(_subSel){ _subSel.addEventListener('change', refreshSubHint); }
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user