Implement issue 1 search, order reuse and tracking

This commit is contained in:
reddaniel110
2026-10-14 11:25:43 +08:00
parent 0e45a58e5b
commit 4aaa6c6a14
17 changed files with 363 additions and 19 deletions
+4
View File
@@ -11,6 +11,10 @@
<div class="card tracking-dashboard" id="tracking-board">
{% from 'tracking_macros.html' import progress, invoice, tasks, actions with context %}
<div class="card-head"><div><h2>付款与交货待办</h2><p class="muted">先核对变更,再跟进付款与交货。同一订单可能出现在多个提醒中。</p></div><a href="{{ url_for('orders') }}">全部订单</a></div>
<div class="order-stage-links" aria-label="按订单进展查看">
{% for key in ['work_undelivered', 'work_invoice_due', 'work_completed'] %}<a class="order-stage {{ 'selected' if tracking_key == key else '' }}" data-stage="{{ key }}" href="{{ url_for('index', tracking=key) }}#tracking-board"><b>{{ TRACKING_FILTERS[key] }}</b><span>{{ tracking_counts[key] }} 单 →</span></a>{% endfor %}
</div>
<p class="muted">交货、收票或付款未登记,以及变更后待核对的订单,可在 <a href="{{ url_for('index', tracking='work_other') }}#tracking-board">其他待跟进({{ tracking_counts['work_other'] }})</a>查看。</p>
<div class="stat-row tracking-focus">
{% for key in ['prepayment','invoice_due','paid_undelivered','recheck'] %}
<a class="stat tracking-stat focus-{{ key }} {{ 'selected' if tracking_key == key else '' }}" data-tracking="{{ key }}" href="{{ url_for('index', tracking=key) }}#tracking-board"><div class="v">{{ tracking_counts[key] }}</div><div class="k">{{ TRACKING_FILTERS[key] }}</div><span class="stat-arrow" aria-hidden="true">→</span></a>
+8 -3
View File
@@ -9,7 +9,7 @@
<div><label>物料代码 *</label><input type="text" name="code" required placeholder="M-1004"></div>
<div><label>物料规格 / 名称</label><input type="text" name="spec"></div>
<div><label>单位</label><input type="text" name="unit" placeholder="件"></div>
<div><label>默认含税单价</label><input type="number" step="0.01" name="price" value="0"></div>
<div><label>默认含税单价(审批后更新)</label><input type="number" step="0.0001" name="price" value="0"></div>
</div>
<div style="margin-top:12px;"><button class="btn primary">新增</button></div>
</form>
@@ -18,14 +18,19 @@
<div class="card">
<div class="card-head">
<h2>物料列表</h2>
{% if user.role == 'admin' %}<form method="post" action="{{ url_for('sync_approved_material_prices') }}" onsubmit="return confirm('将按审批先后用已审批订单价格更新物料库默认价。是否继续?')"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button type="submit" class="btn sm">同步历史审批价</button></form>{% endif %}
<button type="button" class="btn primary sm" onclick="document.getElementById('matImpModal').style.display='flex'">批量导入 Excel</button>
</div>
<form method="get" class="toolbar" role="search">
<input type="search" name="kw" value="{{ kw }}" placeholder="搜索物料代码或规格" aria-label="搜索物料库" style="max-width:320px;">
<button class="btn primary">搜索</button>{% if kw %}<a class="btn" href="{{ url_for('materials') }}">清除</a>{% endif %}
</form>
<table class="grid">
<tr><th>物料代码</th><th>物料规格</th><th>单位</th><th class="num">默认含税单价</th><th>操作</th></tr>
{% for m in rows %}
<tr class="{% if not m.active %}row-off{% endif %}">
<td>{{ m.code }}</td><td>{{ m.spec }}</td><td class="center">{{ m.unit }}</td>
<td class="num">{{ '%.2f'|format(m.price) }}</td>
<td class="num">{{ '%.4f'|format(m.price) }}</td>
<td class="nowrap">
<button type="button" class="btn sm" onclick="openEdit(this)"
data-id="{{ m.id }}" data-code="{{ m.code }}"
@@ -105,7 +110,7 @@
</div>
<div class="form-grid two">
<div><label>单位</label><input type="text" name="unit" id="edit_unit"></div>
<div><label>默认含税单价</label><input type="number" step="0.01" name="price" id="edit_price" value="0"></div>
<div><label>默认含税单价</label><input type="number" step="0.0001" name="price" id="edit_price" value="0"></div>
</div>
</div>
<div class="modal-foot">
+22 -2
View File
@@ -18,9 +18,10 @@
<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 }}">{{ s.name }}</option>{% endfor %}
{% 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>
@@ -61,6 +62,7 @@
<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 %}
@@ -82,6 +84,7 @@
<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 %}
@@ -140,18 +143,32 @@
</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 = opt.dataset.price || 0;
tr.querySelector('.price').value = supplierPrices[opt.dataset.code] || opt.dataset.price || 0;
calc();
});
}
@@ -187,6 +204,8 @@ function delRow(btn){
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;
@@ -200,6 +219,7 @@ document.getElementById('supplierPick').addEventListener('change', async functio
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');
+5 -2
View File
@@ -15,10 +15,11 @@
</div>
<div>
<label>供应商(留空则手工填写)</label>
<select name="supplier_id">
<input type="search" id="supplierSearch" placeholder="搜索名称、简称或编号" aria-label="搜索供应商">
<select name="supplier_id" id="supplierPickNew" style="margin-top:4px;">
<option value="">-- 手工填写 --</option>
{% for s in suppliers %}
<option value="{{ s.id }}">{{ s.name }}</option>
<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>
@@ -29,4 +30,6 @@
</div>
</form>
</div>
<script src="{{ url_for('static', filename='search_select.js') }}"></script>
<script>POFilterSelect.bind(document.getElementById('supplierSearch'), document.getElementById('supplierPickNew'));</script>
{% endblock %}
+6 -4
View File
@@ -4,7 +4,8 @@
<h1 class="page">订单列表</h1>
<div class="card">
<form class="toolbar" method="get">
<input type="text" name="kw" placeholder="单号 / 供应商" value="{{ args.get('kw','') }}" style="width:180px;">
<input type="search" name="kw" placeholder="单号 / 供应商" value="{{ args.get('kw','') }}" aria-label="搜索单号或供应商" style="width:180px;">
<input type="search" name="material_kw" placeholder="物料代码 / 规格" value="{{ args.get('material_kw','') }}" aria-label="按物料搜索订单" style="width:180px;">
<select name="status" style="width:140px;">
<option value="">全部状态</option>
{% for k, v in STATUS_TEXT.items() %}
@@ -30,18 +31,19 @@
<a class="btn" href="{{ url_for('export_ledger') }}">导出 Excel 台账</a>
{% if user.role in ['maker','admin'] %}<a class="btn primary" href="{{ url_for('order_new') }}">新建订单</a>{% endif %}
</form>
{% if not args.get('tracking') and args.get('show_completed') != '1' %}<p class="muted">已收起付款、交货、收票均完成的订单。勾选“显示全部完成订单”可查看。</p>{% endif %}
{% if not args.get('tracking') and not args.get('kw') and not args.get('material_kw') and args.get('show_completed') != '1' %}<p class="muted">已收起付款、交货、收票均完成的订单。搜索时会同时查找历史已完成订单。</p>{% endif %}
{% if args.get('material_kw') %}<p class="hint">物料搜索结果同时包含历史已完成订单。匹配的物料显示在订单下方。</p>{% endif %}
<table class="grid orders-table order-cards">
<thead><tr><th>订单 / 供应商</th><th>审批状态</th><th class="num">金额</th><th>付款进度</th><th>交货进度</th><th>收票 / 待办</th><th>操作</th></tr></thead>
<tbody>{% for o in rows %}
<tr data-order-id="{{ o.id }}">
<td data-label="订单" class="order-identity"><a href="{{ url_for('order_view', oid=o.id) }}"><b>{{ o.order_no }}</b></a><div class="supplier-name">{{ o.supplier_name }}</div><div class="muted">{{ o.doc_type_text }} · {{ o.order_date }}</div><div class="muted">制单 {{ o.created_by_name }} · 审批 {{ o.approved_by or '—' }}</div>{% if o.needs_prepayment %}<span class="badge track-partial">需提前付款{{ '(待核对)' if o.fulfillment_stale else '' }}</span>{% endif %}</td>
<td data-label="订单" class="order-identity"><a href="{{ url_for('order_view', oid=o.id) }}"><b>{{ o.order_no }}</b></a><div class="supplier-name">{{ o.supplier_name }}</div><div class="muted">{{ o.doc_type_text }} · {{ o.order_date }}</div><div class="muted">制单 {{ o.created_by_name }} · 审批 {{ o.approved_by or '—' }}</div>{% if o.needs_prepayment %}<span class="badge track-partial">需提前付款{{ '(待核对)' if o.fulfillment_stale else '' }}</span>{% endif %}{% if matched_items.get(o.id) %}<div class="matched-materials"><b>匹配物料:</b>{% for it in matched_items[o.id][:5] %}<span>{{ it.material_code or '无代码' }} · {{ it.material_spec or '未填规格' }}</span>{% endfor %}{% if matched_items[o.id]|length > 5 %}<span>另有 {{ matched_items[o.id]|length - 5 }} 项</span>{% endif %}</div>{% endif %}</td>
<td data-label="审批"><span class="badge {{ o.status }}">{{ o.status_text }}</span></td>
<td data-label="订单金额" class="num">¥{{ '%.2f'|format(o.total_amount) }}</td>
<td data-label="付款">{{ progress(o, 'payment') }}</td>
<td data-label="交货">{{ progress(o, 'delivery') }}</td>
<td data-label="收票 / 待办">{{ invoice(o) }}{{ tasks(o) }}</td>
<td data-label="操作" class="order-next">{{ actions(o, user) }}<a class="tracking-detail-link" href="{{ url_for('order_pdf', oid=o.id) }}" target="_blank">下载 / 预览 PDF</a></td>
<td data-label="操作" class="order-next">{{ actions(o, user) }}{% if user.role in ['maker', 'admin'] %}<form method="post" action="{{ url_for('order_copy', oid=o.id) }}" class="copy-order-form"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><input type="hidden" name="row_version" value="{{ o.row_version }}"><input type="hidden" name="request_key" value="{{ request_key() }}"><button class="btn sm" type="submit">复制下单</button></form>{% endif %}<a class="tracking-detail-link" href="{{ url_for('order_pdf', oid=o.id) }}" target="_blank">下载 / 预览 PDF</a></td>
</tr>
{% else %}<tr class="empty-row"><td colspan="7" class="center muted">没有符合条件的订单</td></tr>{% endfor %}</tbody>
</table>
+4
View File
@@ -25,6 +25,10 @@
<h2>供应商列表</h2>
<button type="button" class="btn primary sm" onclick="document.getElementById('supImpModal').style.display='flex'">批量导入 Excel</button>
</div>
<form method="get" class="toolbar" role="search">
<input type="search" name="kw" value="{{ kw }}" placeholder="搜索名称、简称或编号" aria-label="搜索供应商" style="max-width:320px;">
<button class="btn primary">搜索</button>{% if kw %}<a class="btn" href="{{ url_for('suppliers') }}">清除</a>{% endif %}
</form>
<table class="grid">
<tr><th>编号</th><th>名称</th><th>联系人</th><th>电话</th><th>付款方式</th><th>税号</th><th>开户银行</th><th>银行帐号</th><th>操作</th></tr>
{% for s in rows %}