238 lines
13 KiB
HTML
238 lines
13 KiB
HTML
{% extends "base.html" %}
|
||
{% block body %}
|
||
<h1 class="page">编辑订单 · {{ o.order_no }}({{ o.doc_type_text }})</h1>
|
||
|
||
<div class="card">
|
||
<h2>单据信息</h2>
|
||
<form method="post" id="orderForm">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<input type="hidden" name="row_version" value="{{ o.row_version }}">
|
||
<div class="form-grid">
|
||
<div><label>采购单号</label><input type="text" value="{{ o.order_no }}" disabled></div>
|
||
<div><label>采购日期</label><input type="date" name="order_date" value="{{ o.order_date or '' }}"></div>
|
||
<div><label>税点</label><input type="text" name="tax_rate" value="{{ o.tax_rate or '' }}" placeholder="如 13%"></div>
|
||
</div>
|
||
|
||
<h3>供方(供应商)</h3>
|
||
<div class="form-grid">
|
||
<div>
|
||
<label>供应商</label>
|
||
<input type="text" name="supplier_name" id="supplier_name" value="{{ o.supplier_name or '' }}">
|
||
<input type="search" id="supplierSearch" placeholder="搜索供应商名称、简称或编号" aria-label="搜索供应商" style="margin-top:4px;">
|
||
<select id="supplierPick" style="margin-top:4px;">
|
||
<option value="">-- 从供应商库带入 --</option>
|
||
{% for s in suppliers %}<option value="{{ s.id }}" data-search="{{ s.short_name or '' }} {{ s.code or '' }}">{{ s.name }}{{ '(' ~ s.short_name ~ ')' if s.short_name else '' }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div><label>联系人</label><input type="text" name="supplier_contact" id="supplier_contact" value="{{ o.supplier_contact or '' }}"></div>
|
||
<div><label>电话</label><input type="text" name="supplier_phone" id="supplier_phone" value="{{ o.supplier_phone or '' }}"></div>
|
||
<div><label>付款方式</label><input type="text" name="payment_terms" id="payment_terms" value="{{ o.payment_terms or '' }}"></div>
|
||
<div><label>税号</label><input type="text" name="supplier_tax_no" id="supplier_tax_no" value="{{ o.supplier_tax_no or '' }}"></div>
|
||
<div><label>银行帐号</label><input type="text" name="supplier_bank_account" id="supplier_bank_account" value="{{ o.supplier_bank_account or '' }}"></div>
|
||
<div><label>开户银行</label><input type="text" name="supplier_bank_name" id="supplier_bank_name" value="{{ o.supplier_bank_name or '' }}"></div>
|
||
</div>
|
||
|
||
<h3>需方(本公司)</h3>
|
||
<div class="form-grid">
|
||
<div><label>需方名称</label><input type="text" name="our_company" value="{{ o.our_company or '' }}"></div>
|
||
<div><label>联系人</label><input type="text" name="our_contact" value="{{ o.our_contact or '' }}"></div>
|
||
<div><label>电话</label><input type="text" name="our_phone" value="{{ o.our_phone or '' }}"></div>
|
||
<div><label>税号</label><input type="text" name="our_tax_no" value="{{ o.our_tax_no or '' }}"></div>
|
||
<div style="grid-column: span 2;">
|
||
<label>交货地点</label>
|
||
<input type="text" name="delivery_place" value="{{ o.delivery_place or '' }}">
|
||
</div>
|
||
</div>
|
||
|
||
<h3>物料明细</h3>
|
||
<table class="grid" id="itemTable">
|
||
<tr>
|
||
<th style="width:38px;">序</th>
|
||
<th style="width:150px;">物料代码</th>
|
||
<th>物料规格</th>
|
||
<th style="width:62px;">单位</th>
|
||
<th style="width:78px;">数量</th>
|
||
<th style="width:92px;">含税单价</th>
|
||
<th style="width:96px;">金额</th>
|
||
<th style="width:118px;">交期</th>
|
||
<th style="width:130px;">备注</th>
|
||
<th style="width:44px;">删</th>
|
||
</tr>
|
||
{% for it in o.items %}
|
||
<tr>
|
||
<td class="center seq">{{ loop.index }}</td>
|
||
<td><input type="text" name="item_code" value="{{ it.material_code or '' }}">
|
||
<input type="search" class="matsearch" placeholder="搜索代码或规格" aria-label="搜索物料" style="margin-top:3px;">
|
||
<select class="matpick" style="margin-top:3px;">
|
||
<option value="">-- 物料库 --</option>
|
||
{% for m in materials %}<option value="{{ m.id }}" data-code="{{ m.code }}" data-spec="{{ m.spec }}" data-unit="{{ m.unit }}" data-price="{{ m.price }}">{{ m.code }} {{ m.spec }}</option>{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td><input type="text" name="item_spec" value="{{ it.material_spec or '' }}"></td>
|
||
<td><input type="text" name="item_unit" value="{{ it.unit or '' }}"></td>
|
||
<td><input type="number" min="0" max="999999999" step="0.0001" name="item_qty" class="qty" value="{{ it.qty or 0 }}"></td>
|
||
<td><input type="number" min="0" max="999999999" step="0.0001" name="item_price" class="price" value="{{ it.price or 0 }}"></td>
|
||
<td><input type="text" class="amt" value="{{ '%.2f'|format(it.amount or 0) }}" disabled></td>
|
||
<td><input type="date" name="item_date" value="{{ it.delivery_date or '' }}"></td>
|
||
<td><input type="text" name="item_remark" value="{{ it.remark or '' }}"></td>
|
||
<td class="center"><button type="button" class="btn sm danger" onclick="delRow(this)">×</button></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</table>
|
||
|
||
<template id="rowTpl">
|
||
<tr>
|
||
<td class="center seq">1</td>
|
||
<td><input type="text" name="item_code" value="">
|
||
<input type="search" class="matsearch" placeholder="搜索代码或规格" aria-label="搜索物料" style="margin-top:3px;">
|
||
<select class="matpick" style="margin-top:3px;">
|
||
<option value="">-- 物料库 --</option>
|
||
{% for m in materials %}<option value="{{ m.id }}" data-code="{{ m.code }}" data-spec="{{ m.spec }}" data-unit="{{ m.unit }}" data-price="{{ m.price }}">{{ m.code }} {{ m.spec }}</option>{% endfor %}
|
||
</select>
|
||
</td>
|
||
<td><input type="text" name="item_spec" value=""></td>
|
||
<td><input type="text" name="item_unit" value=""></td>
|
||
<td><input type="number" min="0" max="999999999" step="0.0001" name="item_qty" class="qty" value="0"></td>
|
||
<td><input type="number" min="0" max="999999999" step="0.0001" name="item_price" class="price" value="0"></td>
|
||
<td><input type="text" class="amt" value="0.00" disabled></td>
|
||
<td><input type="date" name="item_date" value=""></td>
|
||
<td><input type="text" name="item_remark" value=""></td>
|
||
<td class="center"><button type="button" class="btn sm danger" onclick="delRow(this)">×</button></td>
|
||
</tr>
|
||
</template>
|
||
|
||
<div class="toolbar" style="margin-top:10px;">
|
||
<button type="button" class="btn" onclick="addRow()">+ 添加一行</button>
|
||
<span class="grow"></span>
|
||
<span class="total-bar">合计:<span class="amt" id="totalAmt">{{ '%.2f'|format(o.total_amount) }}</span> 元
|
||
<span class="muted">逐行四舍五入到分后合计</span></span>
|
||
</div>
|
||
|
||
<h3>补充说明</h3>
|
||
<textarea name="note" placeholder="选填,会打印在 PDF 备注区">{{ o.note or '' }}</textarea>
|
||
|
||
<div style="margin-top:16px;">
|
||
<button class="btn primary" type="submit" name="intent" value="save">保存草稿</button>
|
||
<input name="comment" placeholder="提交说明(选填)" style="width:220px;">
|
||
<button class="btn ok" type="submit" name="intent" value="submit">保存并提交审批</button>
|
||
<a class="btn" href="{{ url_for('order_view', oid=o.id) }}">查看订单</a>
|
||
<a class="btn" href="{{ url_for('order_pdf', oid=o.id) }}" target="_blank">预览 PDF</a>
|
||
</div>
|
||
</form>
|
||
|
||
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>从 Excel 导入明细</h2>
|
||
<div class="hint">
|
||
支持 .xlsx / .xls。系统自动识别表头(物料代码、物料规格、单位、数量、含税单价、交期、备注),
|
||
没有表头则按前 7 列顺序读取。行数超出模板也没关系,PDF 会自动分页。
|
||
<a href="{{ url_for('import_template') }}">下载导入模板</a>
|
||
</div>
|
||
<form method="post" action="{{ url_for('import_excel', oid=o.id) }}" enctype="multipart/form-data" class="toolbar">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||
<input type="hidden" name="row_version" value="{{ o.row_version }}">
|
||
<input type="file" name="file" accept=".xls,.xlsx" required style="width:280px;">
|
||
<select name="mode" style="width:140px;">
|
||
<option value="append">追加到现有明细</option>
|
||
<option value="replace">替换全部明细</option>
|
||
</select>
|
||
<button class="btn primary">导入</button>
|
||
</form>
|
||
</div>
|
||
|
||
<script src="{{ url_for('static', filename='money.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='search_select.js') }}"></script>
|
||
<script>
|
||
const MATERIALS = {{ materials|map(attribute='id')|list|tojson }};
|
||
let supplierPrices = {{ supplier_prices|tojson }};
|
||
POFilterSelect.bind(document.getElementById('supplierSearch'), document.getElementById('supplierPick'));
|
||
async function updateSupplierPrices(){
|
||
const name = document.getElementById('supplier_name').value.trim();
|
||
const tax = document.getElementById('supplier_tax_no').value.trim();
|
||
if (!name) { supplierPrices = {}; return; }
|
||
try {
|
||
const query = new URLSearchParams({supplier_name: name, supplier_tax_no: tax});
|
||
const response = await fetch('{{ url_for('api_material_prices') }}?' + query.toString());
|
||
if (response.ok) supplierPrices = await response.json();
|
||
} catch (error) { /* The catalog default remains usable offline. */ }
|
||
}
|
||
function bindRow(tr){
|
||
tr.querySelectorAll('.qty, .price').forEach(i => i.addEventListener('input', calc));
|
||
const pick = tr.querySelector('.matpick');
|
||
POFilterSelect.bind(tr.querySelector('.matsearch'), pick);
|
||
if (pick) pick.addEventListener('change', function(){
|
||
const opt = this.selectedOptions[0];
|
||
if (!opt || !opt.value) return;
|
||
tr.querySelector('[name=item_code]').value = opt.dataset.code || '';
|
||
tr.querySelector('[name=item_spec]').value = opt.dataset.spec || '';
|
||
tr.querySelector('[name=item_unit]').value = opt.dataset.unit || '';
|
||
tr.querySelector('.price').value = supplierPrices[opt.dataset.code] || opt.dataset.price || 0;
|
||
calc();
|
||
});
|
||
}
|
||
function calc(){
|
||
let total = 0n, valid = true;
|
||
document.querySelectorAll('#itemTable tr').forEach((tr, idx) => {
|
||
const qty = tr.querySelector('.qty'), price = tr.querySelector('.price');
|
||
if (!qty || !price) return;
|
||
try {
|
||
const cents = POMoney.lineCents(qty.value, price.value);
|
||
tr.querySelector('.amt').value = POMoney.format(cents);
|
||
total += cents;
|
||
} catch (e) { tr.querySelector('.amt').value = '请检查'; valid = false; }
|
||
tr.querySelector('.seq').textContent = idx;
|
||
});
|
||
document.getElementById('totalAmt').textContent = valid ? POMoney.format(total) : '请检查数量和单价';
|
||
}
|
||
function addRow(){
|
||
dirty = true;
|
||
// 从 <template> 克隆,避免"明细为空时没有可复制的行"导致点击无反应
|
||
const t = document.getElementById('itemTable');
|
||
const tr = document.getElementById('rowTpl').content.firstElementChild.cloneNode(true);
|
||
tr.querySelectorAll('input').forEach(i => { if (i.type !== 'date') i.value = ''; });
|
||
const q = tr.querySelector('.qty'); if (q) q.value = 0;
|
||
const p = tr.querySelector('.price'); if (p) p.value = 0;
|
||
const a = tr.querySelector('.amt'); if (a) a.value = '0.00';
|
||
const sel = tr.querySelector('.matpick'); if (sel) sel.selectedIndex = 0;
|
||
t.appendChild(tr);
|
||
bindRow(tr); calc();
|
||
}
|
||
function delRow(btn){
|
||
dirty = true;
|
||
btn.closest('tr').remove(); calc();
|
||
}
|
||
document.querySelectorAll('#itemTable tr').forEach(bindRow);
|
||
document.getElementById('supplier_name').addEventListener('change', updateSupplierPrices);
|
||
document.getElementById('supplier_tax_no').addEventListener('change', updateSupplierPrices);
|
||
calc();
|
||
document.getElementById('supplierPick').addEventListener('change', async function(){
|
||
if (!this.value) return;
|
||
const r = await fetch('/api/suppliers').then(r => r.json());
|
||
const s = r.find(x => String(x.id) === this.value);
|
||
if (!s) return;
|
||
document.getElementById('supplier_name').value = s.name || '';
|
||
document.getElementById('supplier_contact').value = s.contact || '';
|
||
document.getElementById('supplier_phone').value = s.phone || '';
|
||
document.getElementById('supplier_tax_no').value = s.tax_no || '';
|
||
document.getElementById('supplier_bank_name').value = s.bank_name || '';
|
||
document.getElementById('supplier_bank_account').value = s.bank_account || '';
|
||
document.getElementById('payment_terms').value = s.payment_terms || '';
|
||
await updateSupplierPrices();
|
||
});
|
||
let dirty = false;
|
||
const orderForm = document.getElementById('orderForm');
|
||
orderForm.addEventListener('input', () => { dirty = true; });
|
||
orderForm.addEventListener('change', () => { dirty = true; });
|
||
orderForm.addEventListener('submit', () => { dirty = false; });
|
||
document.querySelector('[action$="/import_excel"]').addEventListener('submit', e => {
|
||
if (dirty) { e.preventDefault(); alert('请先保存当前修改,再导入明细。'); }
|
||
});
|
||
window.addEventListener('beforeunload', e => { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
|
||
document.querySelectorAll('a[target="_blank"]').forEach(a => a.addEventListener('click', e => {
|
||
if (dirty) { e.preventDefault(); alert('请先保存当前修改,再预览 PDF。'); }
|
||
}));
|
||
</script>
|
||
{% endblock %}
|