- 独立采购备料清单:选 BOM + 输入套数一键生成计划(计划数量 = 单套用量 × 套数),可手工增删改、导出 Excel - 备料与生产订单完全解耦,仅记录计划数量、不记账(不产生任何出入库流水) - 新增「清空清单」功能(/prepare/clear,JS 二次确认 + 操作日志),按当前工厂范围清空 - 配套:PrepPlan 模型、缺口(计划-库存)实时计算、库存随工厂切换过滤
128 lines
6.2 KiB
HTML
128 lines
6.2 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}备料清单{% endblock %}
|
||
{% block content %}
|
||
<div class="toolbar">
|
||
<h3 style="margin:0">备料清单(采购下单用)</h3>
|
||
<span class="badge warn">不记账 · 按需用×套数预填</span>
|
||
<form method="post" action="{{ url_for('prepare_clear') }}" style="display:inline"
|
||
onsubmit="return confirm('确定清空当前范围内的全部备料计划?此操作不可撤销。');">
|
||
<button type="submit" class="btn danger">清空清单</button>
|
||
</form>
|
||
<a class="btn" href="{{ url_for('prepare_export') }}">导出 Excel</a>
|
||
</div>
|
||
|
||
<div class="card" style="margin-bottom:16px">
|
||
<p class="small muted" style="margin-top:0">
|
||
本页为<b>独立的采购备料清单</b>:选一个 <b>BOM</b> 并输入<b>套数</b>,即按
|
||
<b>「计划数量 = BOM单套用量 × 套数」</b> 一键生成(可再手工改);
|
||
也可在下方手工添加任意物料并填数量。当前范围:<b>{% if current_factory %}{{ current_factory.code }} · {{ current_factory.name }}{% else %}全部工厂(汇总){% endif %}</b>;
|
||
「实际库存」「缺口」随顶栏工厂切换实时计算。
|
||
</p>
|
||
<p class="small muted" style="margin-bottom:0">
|
||
说明:本页与 Excel 导出均<b>只读</b>,不产生任何入库 / 出库流水,仅用于向供应商下单采购。
|
||
<b>缺口 = 计划数量 − 实际库存</b>(>0 表示还需采购,<0 表示库存已盈余)。
|
||
</p>
|
||
</div>
|
||
|
||
{# 添加物料 #}
|
||
<form method="post" action="{{ url_for('prepare_add') }}" class="card" style="margin-bottom:16px;display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap">
|
||
<div>
|
||
<label class="small muted">物料编号</label><br>
|
||
<input list="matlist" name="part_number" placeholder="输入或选择物料编号" style="width:240px" autocomplete="off" required>
|
||
<datalist id="matlist">
|
||
{% for m in materials %}<option value="{{ m.part_number }}">{{ m.manufacturer or '' }}</option>{% endfor %}
|
||
</datalist>
|
||
</div>
|
||
<div>
|
||
<label class="small muted">计划数量</label><br>
|
||
<input type="text" inputmode="decimal" name="qty" placeholder="可留空" style="width:120px;text-align:right">
|
||
</div>
|
||
<button type="submit" class="btn primary">添加物料</button>
|
||
<span class="muted small">手工录入单个物料(计划数量可留空,稍后填)</span>
|
||
</form>
|
||
|
||
{# 按 BOM 批量生成计划(计划数量 = 单套用量 × 套数) #}
|
||
<form method="post" action="{{ url_for('prepare_add_bom') }}" class="card" style="margin-bottom:16px;display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap">
|
||
<div>
|
||
<label class="small muted">① 选择 BOM</label><br>
|
||
<select id="bomSel" name="bom_id" style="width:300px">
|
||
<option value="">-- 选择 BOM --</option>
|
||
{% for b in bom_opts %}<option value="{{ b.id }}">{{ b.label }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="small muted">② 套数</label><br>
|
||
<input type="text" inputmode="decimal" name="sets" value="1" style="width:120px;text-align:right">
|
||
</div>
|
||
<button type="submit" class="btn primary">生成计划(需用×套数)</button>
|
||
<span class="muted small">计划数量 = BOM单套用量 × 套数,生成后可在表中手工调整</span>
|
||
</form>
|
||
|
||
<form method="post" action="{{ url_for('prepare_save') }}">
|
||
<div class="toolbar" style="justify-content:flex-end">
|
||
<span class="muted small">修改计划数量后点此保存</span>
|
||
<button type="submit" class="btn primary">保存计划数量</button>
|
||
</div>
|
||
<table class="tbl">
|
||
<thead><tr>
|
||
<th>物料编号</th><th>型号/规格</th><th>单位</th>
|
||
<th>计划数量</th><th>实际库存</th><th>缺口</th><th>状态</th><th>操作</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for r in rows %}
|
||
<tr class="{{ 'row-danger' if r.diff > 0 else '' }}">
|
||
<td><a href="{{ url_for('material_detail', mid=r.m.id) }}" title="进入工厂物料">{{ r.m.part_number }}</a></td>
|
||
<td>{{ r.m.manufacturer or '-' }}</td>
|
||
<td>{{ r.m.unit or '个' }}</td>
|
||
<td>
|
||
<input type="text" inputmode="decimal" name="plan_{{ r.id }}"
|
||
class="plan-input" style="width:90px;text-align:right"
|
||
value="{{ '%.2f'|format(r.plan) if r.plan else '' }}"
|
||
data-stock="{{ '%.2f'|format(r.stock) }}">
|
||
</td>
|
||
<td class="num">{{ '%.2f'|format(r.stock) }}</td>
|
||
<td class="num"><span class="diff" id="diff_{{ r.id }}">{{ '%.2f'|format(r.diff) }}</span></td>
|
||
<td><span class="badge {{ 'danger' if r.diff > 0 else 'ok' }}">{{ '需采购' if r.diff > 0 else ('盈余' if r.diff < 0 else '刚好') }}</span></td>
|
||
<td>
|
||
<form method="post" action="{{ url_for('prepare_delete', pid=r.id) }}" style="display:inline"
|
||
onsubmit="return confirm('确定删除该备料物料?');">
|
||
<button type="submit" class="btn danger small">删除</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="8" class="muted">还没有添加任何备料物料,请选择 BOM 加入或手工录入。</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</form>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
// 计划数量实时计算缺口 = 计划 - 实际库存
|
||
function recalcDiff(input) {
|
||
var row = input.closest('tr');
|
||
var diffEl = row.querySelector('.diff');
|
||
if (!diffEl) return;
|
||
var stock = parseFloat(input.getAttribute('data-stock')) || 0;
|
||
var val = input.value.trim();
|
||
if (val === '') { diffEl.textContent = ''; diffEl.className = 'diff'; return; }
|
||
var plan = parseFloat(val);
|
||
if (isNaN(plan)) { diffEl.textContent = '—'; diffEl.className = 'diff'; return; }
|
||
var d = Math.round((plan - stock) * 100) / 100;
|
||
diffEl.textContent = d.toFixed(2);
|
||
diffEl.className = 'diff ' + (d < 0 ? 'diff-neg' : (d > 0 ? 'diff-pos' : ''));
|
||
var badge = row.querySelector('.badge');
|
||
if (badge) {
|
||
badge.className = 'badge ' + (d > 0 ? 'danger' : 'ok');
|
||
badge.textContent = d > 0 ? '需采购' : (d < 0 ? '盈余' : '刚好');
|
||
}
|
||
}
|
||
document.querySelectorAll('.plan-input').forEach(function (inp) {
|
||
inp.addEventListener('input', function () { recalcDiff(this); });
|
||
recalcDiff(inp); // 初始(回填值)
|
||
});
|
||
</script>
|
||
{% endblock %}
|