Files
Inventory-Management-System/templates/txn.html
T
star 33efc7c846 feat: 入库新增「送货日期」字段(与业务日期区分)
- models.Inbound 增加 delivery_date 列(可空, 默认 None)
- 入库单笔录入表单新增「送货日期」输入(默认当天, 可改供应商实际到货日); 出库不受影响
- 物料明细页入库流水新增「送货日期」列展示
- seed_admin 增加幂等补列: 旧库启动时自动 ALTER TABLE inbound ADD COLUMN delivery_date, 开发/EXE 均兼容
- 教程补充送货日期字段说明

(注: instance/app.db 已本地补列, 但未纳入本次提交; dist/app.db 为运行库已 gitignore)
2026-09-23 14:21:57 +08:00

218 lines
8.8 KiB
HTML
Raw 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 %}{{ '入库登记' if kind=='inbound' else '出库登记' }}{% endblock %}
{% block content %}
<div class="toolbar">
<h3 style="margin:0">{{ '入库登记' if kind=='inbound' else '出库登记' }}</h3>
<div>
<a class="btn" href="{{ url_for('download_template', kind='transactions') }}">⬇ 下载批量模板</a>
<button class="primary" type="button" onclick="openBatch()">⊞ 批量导入窗口</button>
</div>
</div>
<form method="post" class="card form-card txn-form">
<label>物料编码(输入或扫码自动定位)</label>
<div class="pn-lookup">
<input id="pnLookup" placeholder="输入物料编码,如 P0001,回车自动定位" autocomplete="off">
<span id="pnHint" class="pn-hint"></span>
</div>
<label>物料 *</label>
<select name="material_id" id="materialSelect" required>
<option value="">-- 选择物料 --</option>
{% for m in mats %}
<option value="{{ m.id }}" data-pn="{{ m.part_number }}">{{ m.part_number }} ({{ m.manufacturer or '-' }})</option>
{% endfor %}
</select>
<label>批次号</label>
<input name="batch_no">
<label>数量 *</label>
<input name="qty" type="number" step="0.01" min="0.01" required>
<label>经手人</label>
<input name="operator">
{% if kind=='inbound' %}
<label>来源 / 供应商</label>
<input name="source">
<label>送货日期</label>
<input name="delivery_date" type="date" value="{{ today }}">
{% else %}
<label>用途</label>
<input name="purpose">
<label>关联单号</label>
<input name="order_ref">
{% endif %}
<label>业务日期</label>
<input name="occur_date" type="date" value="{{ today }}">
<label>备注</label>
<input name="remark">
<div class="row-actions">
<button class="primary">{{ '入库' if kind=='inbound' else '出库' }}</button>
</div>
</form>
<h3>最近 {{ recs|length }} 条{{ '入库' if kind=='inbound' else '出库' }}记录</h3>
<table class="tbl">
<thead>
<tr>
<th>物料</th><th>批次</th><th>数量</th><th>经手人</th>
{% if kind=='inbound' %}<th>来源</th>{% else %}<th>用途</th><th>单号</th>{% endif %}
<th>日期</th>
</tr>
</thead>
<tbody>
{% for r in recs %}
{% set m = mats|selectattr('id','equalto',r.material_id)|list|first %}
<tr>
<td>{{ m.part_number if m else r.material_id }}</td>
<td>{{ r.batch_no or '-' }}</td>
<td class="num">{{ '%.2f'|format(r.qty) }}</td>
<td>{{ r.operator or '-' }}</td>
{% if kind=='inbound' %}<td>{{ r.source or '-' }}</td>
{% else %}<td>{{ r.purpose or '-' }}</td><td>{{ r.order_ref or '-' }}</td>{% endif %}
<td>{{ r.occur_date }}</td>
</tr>
{% else %}
<tr><td colspan="7" class="muted">暂无记录</td></tr>
{% endfor %}
</tbody>
</table>
<!-- 批量导入窗口(模态) -->
<div class="modal-mask" id="batchMask" style="display:none">
<div class="modal">
<div class="modal-head">
<span>批量导入{{ '入库' if kind=='inbound' else '出库' }} · 多行清单</span>
<button class="x" onclick="closeBatch()">✕</button>
</div>
<div class="modal-body">
<p class="small muted">每行一条: <b>物料编号 数量 批次(可选) 日期(可选) 备注(可选)</b>,列之间用空格或制表符分隔。也可直接上传 Excel/CSV。</p>
<textarea id="batchText" rows="8" placeholder="P0001 100 B20260911 2026-09-11 到货A
P0002 50 2026-09-11 到货B"></textarea>
<div class="row-actions">
<button type="button" onclick="parsePaste()">解析预览</button>
<label class="btn" style="cursor:pointer">📁 上传文件
<input type="file" id="batchFile" accept=".xlsx,.csv" style="display:none" onchange="parseFile()">
</label>
<button type="button" class="primary" id="commitBtn" onclick="commitBatch()" disabled>确认导入</button>
</div>
<div id="batchResult" style="margin-top:12px"></div>
</div>
</div>
</div>
<script>
const TXN_KIND = "{{ kind }}";
let lastRows = [];
/* ---- 物料编码输入框: 输入/扫码自动定位下拉物料 ---- */
(function(){
const sel = document.getElementById('materialSelect');
const pn = document.getElementById('pnLookup');
const hint = document.getElementById('pnHint');
if(!sel || !pn || !hint) return;
function lockTo(o){
sel.value = o.value;
hint.textContent = '✓ ' + o.text;
hint.className = 'pn-hint ok';
const qty = document.querySelector('.txn-form input[name=qty]');
if(qty) qty.focus();
}
function locate(){
const q = pn.value.trim().toUpperCase();
if(!q){ hint.textContent=''; hint.className='pn-hint'; return; }
const opts = Array.from(sel.options).filter(o => o.value !== '');
const exact = opts.find(o => (o.getAttribute('data-pn')||'').toUpperCase() === q);
if(exact){ lockTo(exact); return; }
const prefix = opts.filter(o => (o.getAttribute('data-pn')||'').toUpperCase().startsWith(q));
if(prefix.length === 1){ lockTo(prefix[0]); return; }
if(prefix.length > 1){
hint.textContent = '… 多个候选(' + prefix.length + '),继续输入';
hint.className = 'pn-hint warn';
return;
}
hint.textContent = '✗ 未找到物料编码:' + q;
hint.className = 'pn-hint danger';
}
pn.addEventListener('input', locate);
pn.addEventListener('keydown', e => { if(e.key === 'Enter'){ e.preventDefault(); locate(); } });
sel.addEventListener('change', () => {
const o = sel.options[sel.selectedIndex];
if(o && o.value !== ''){
pn.value = o.getAttribute('data-pn') || '';
hint.textContent = '✓ ' + o.text;
hint.className = 'pn-hint ok';
} else {
pn.value = ''; hint.textContent = ''; hint.className = 'pn-hint';
}
});
})();
function openBatch(){ document.getElementById('batchMask').style.display='flex'; }
function closeBatch(){ document.getElementById('batchMask').style.display='none'; }
document.getElementById('batchMask').addEventListener('click', e=>{ if(e.target.id==='batchMask') closeBatch(); });
function splitLine(line){
const s = line.replace(/[\t,,;;]+/g,' ').trim();
if(!s) return [];
return s.split(/\s+/);
}
function parsePaste(){
const text = document.getElementById('batchText').value.trim();
if(!text){ alert('请先粘贴清单'); return; }
const rows = [];
text.split(/\r?\n/).forEach(raw=>{
const parts = splitLine(raw);
if(!parts.length) return;
const first = parts[0];
if(/物料|编号|part\s?number|数量|qty/i.test(first)) return; // 跳过表头
const pn = parts[0];
const qty = parseFloat(parts[1]);
const batch = parts[2] || '';
const date = parts[3] || '';
const remark = parts.slice(4).join(' ');
rows.push({part_number:pn, qty:isNaN(qty)?0:qty, batch_no:batch, date:date, remark:remark});
});
if(!rows.length){ alert('未解析到有效行'); return; }
postPreview({kind:TXN_KIND, rows:rows});
}
function parseFile(){
const f = document.getElementById('batchFile').files[0];
if(!f) return;
const fd = new FormData();
fd.append('file', f);
fd.append('kind', TXN_KIND);
fetch('/api/import/transactions/preview', {method:'POST', body:fd})
.then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
}
function postPreview(payload){
fetch('/api/import/transactions/preview', {
method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload)
}).then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
}
function renderPreview(data){
lastRows = data.rows || [];
const valid = data.valid, invalid = data.invalid;
let html = `<div class="flash ${invalid? 'error':'ok'}">共 ${lastRows.length} 行 · 可导入 <b>${valid}</b> 行 · 异常 <b>${invalid}</b> 行</div>`;
html += '<table class="tbl"><thead><tr><th>物料编号</th><th>数量</th><th>批次</th><th>日期</th><th>备注</th><th>校验</th></tr></thead><tbody>';
lastRows.forEach(r=>{
const cls = r.ok? '' : 'row-danger';
const tag = r.ok? '<span class="badge ok">OK</span>' : `<span class="badge danger">${r.error||'错误'}</span>`;
html += `<tr class="${cls}"><td>${r.part_number}</td><td class="num">${r.qty}</td><td>${r.batch_no||'-'}</td><td>${r.date||'-'}</td><td>${r.remark||'-'}</td><td>${tag}</td></tr>`;
});
html += '</tbody></table>';
document.getElementById('batchResult').innerHTML = html;
document.getElementById('commitBtn').disabled = valid===0;
}
function commitBatch(){
if(!lastRows.length) return;
fetch('/api/import/transactions/commit', {
method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({kind:TXN_KIND, rows:lastRows})
}).then(r=>r.json()).then(d=>{
alert(`导入完成: 成功 ${d.added} 条${d.skipped? ',跳过 '+d.skipped+' 条':''}`);
if(d.added>0) location.reload();
}).catch(e=>alert('导入失败: '+e));
}
</script>
{% endblock %}