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
+51 -6
View File
@@ -25,6 +25,7 @@ from models import (COMPANY_NAME, DOC_TYPES, ROLE_TEXT, STATUS_TEXT,
DEFAULT_OUR_PHONE, DEFAULT_OUR_TAX_NO, DEFAULT_TAX_RATE,
ApprovalLog, Material, Order, OrderItem, Supplier, User,
init_db)
from material_prices import remember_approved_prices, supplier_price_map
from pdf_export import build_pdf
# 打包成 EXE 后:资源在临时目录 _MEIPASS,数据必须写在 EXE 同级目录
@@ -100,6 +101,7 @@ def orders():
doc_type = request.args.get("doc_type", "")
date_from = request.args.get("date_from", "")
date_to = request.args.get("date_to", "")
material_kw = request.args.get('material_kw', '').strip()[:100]
if kw:
q = q.filter(Order.order_no.like("%%%s%%" % kw) |
Order.supplier_name.like("%%%s%%" % kw))
@@ -111,15 +113,26 @@ def orders():
q = q.filter(Order.order_date >= date_from)
if date_to:
q = q.filter(Order.order_date <= date_to)
if material_kw:
pattern = '%%%s%%' % material_kw
q = q.filter(Order.items.any((OrderItem.material_code.ilike(pattern)) |
(OrderItem.material_spec.ilike(pattern))))
if u.role == "maker":
q = q.filter(Order.created_by == u.id)
tracking = request.args.get('tracking', '')
q = filter_tracking(q, tracking)
if not tracking and request.args.get('show_completed') != '1':
if not tracking and not kw and not material_kw and request.args.get('show_completed') != '1':
q = q.filter(or_(Order.status != 'approved', ~tracking_conditions()['closed']))
rows = q.order_by(Order.id.desc()).all()
matched_items = {}
if material_kw and rows:
for item in (db.query(OrderItem).filter(OrderItem.order_id.in_([o.id for o in rows]),
(OrderItem.material_code.ilike(pattern)) |
(OrderItem.material_spec.ilike(pattern)))
.order_by(OrderItem.order_id, OrderItem.seq)):
matched_items.setdefault(item.order_id, []).append(item)
return render_template("orders.html", user=u, rows=rows, status_text=STATUS_TEXT,
doc_types=DOC_TYPES, args=request.args)
doc_types=DOC_TYPES, args=request.args, matched_items=matched_items)
@app.route("/import_template.xlsx")
@@ -165,8 +178,14 @@ def suppliers():
db.commit()
flash(u"已保存", "ok")
return redirect(url_for("suppliers"))
rows = db.query(Supplier).order_by(Supplier.id).all()
return render_template("suppliers.html", user=u, rows=rows)
kw = request.args.get('kw', '').strip()[:100]
query = db.query(Supplier)
if kw:
pattern = '%%%s%%' % kw
query = query.filter(Supplier.name.ilike(pattern) | Supplier.short_name.ilike(pattern) |
Supplier.code.ilike(pattern))
rows = query.order_by(Supplier.id).all()
return render_template("suppliers.html", user=u, rows=rows, kw=kw)
@app.route("/suppliers/<int:sid>/toggle", methods=["POST"])
@@ -307,8 +326,26 @@ def materials():
db.commit()
flash(u"已保存", "ok")
return redirect(url_for("materials"))
rows = db.query(Material).order_by(Material.id).all()
return render_template("materials.html", user=u, rows=rows)
kw = request.args.get('kw', '').strip()[:100]
query = db.query(Material)
if kw:
pattern = '%%%s%%' % kw
query = query.filter(Material.code.ilike(pattern) | Material.spec.ilike(pattern))
rows = query.order_by(Material.id).all()
return render_template("materials.html", user=u, rows=rows, kw=kw)
@app.post('/materials/prices/from-approved-orders')
@login_required
@role_required('admin')
def sync_approved_material_prices():
# Old approved orders may predate automatic catalog price updates.
orders = (db.query(Order).filter(Order.status == 'approved')
.order_by(Order.approved_at, Order.id).all())
count = sum(remember_approved_prices(db, order) for order in orders)
db.commit()
flash('已按审批先后同步 %s 张订单,更新 %s 条物料单价。' % (len(orders), count), 'ok')
return redirect(url_for('materials'))
@app.route("/materials/<int:mid>/toggle", methods=["POST"])
@@ -431,6 +468,14 @@ def api_suppliers():
"payment_terms": s.payment_terms} for s in rows])
@app.route('/api/material-prices')
@login_required
@role_required('maker')
def api_material_prices():
return jsonify(supplier_price_map(db, request.args.get('supplier_name', ''),
request.args.get('supplier_tax_no', ''), current_user()))
# ---------------- 用户 ----------------
@app.route("/users", methods=["GET", "POST"])
@login_required
+52
View File
@@ -0,0 +1,52 @@
"""Keep the material catalog's latest approved price and supplier-specific quotes."""
from decimal import Decimal
from models import Material, Order, OrderItem
def remember_approved_prices(db, order):
"""Update only unambiguous material codes in the approval transaction."""
updated = 0
for item in order.items:
code = (item.material_code or '').strip()
if not code or item.price is None or item.price <= 0:
continue
matches = db.query(Material).filter(Material.code == code).all()
exact = [material for material in matches if (material.spec or '').strip() == (item.material_spec or '').strip()]
if len(exact) == 1:
material = exact[0]
elif len(matches) == 1:
material = matches[0]
elif not matches:
material = Material(code=code, spec=item.material_spec or '', unit=item.unit or '', active=True)
db.add(material)
else:
# Duplicate codes with different specs are ambiguous; preserve both records.
continue
material.price = item.price
updated += 1
return updated
def supplier_price_map(db, supplier_name, supplier_tax_no='', user=None):
"""Most recent approved unit price for each code from the chosen supplier."""
name = (supplier_name or '').strip()
if not name:
return {}
query = (db.query(OrderItem.material_code, OrderItem.price, Order.supplier_tax_no)
.join(Order, OrderItem.order_id == Order.id)
.filter(Order.status == 'approved', Order.supplier_name == name,
OrderItem.material_code != '')
.order_by(Order.approved_at.desc(), Order.id.desc(), OrderItem.seq.desc()))
if user and user.role == 'maker':
query = query.filter(Order.created_by == user.id)
rows = query.all()
tax = (supplier_tax_no or '').strip()
if tax and any((row.supplier_tax_no or '').strip() == tax for row in rows):
rows = [row for row in rows if (row.supplier_tax_no or '').strip() == tax]
result = {}
for row in rows:
code = (row.material_code or '').strip()
if code and code not in result and row.price is not None:
result[code] = str(Decimal(row.price))
return result
+6
View File
@@ -17,6 +17,7 @@ from order_logic import (ORDER_FIELDS, ITEM_FIELDS, add_log, can_edit, can_appro
validate_order)
from pdf_export import PDF_TEMPLATE_VERSION, build_pdf, seal_layout
from order_tracking import can_follow, can_record_payment
from material_prices import remember_approved_prices, supplier_price_map
from security import current_user, db, login_required, role_required
@@ -109,6 +110,7 @@ def register_order_routes(app):
return render_template('order_edit.html', user=user, o=order, editable=True,
suppliers=db.query(Supplier).filter_by(active=True).all(),
materials=db.query(Material).filter_by(active=True).all(), doc_types=DOC_TYPES,
supplier_prices=supplier_price_map(db, order.supplier_name, order.supplier_tax_no, user),
total_cn=amount_to_cn(order.total_amount))
@app.route('/orders/<int:oid>/view')
@@ -161,6 +163,7 @@ def register_order_routes(app):
version.pdf_sha256 = hashlib.sha256(version.pdf_data).hexdigest()
version.template_version = (PDF_TEMPLATE_VERSION + '-legacy-current'
if version.template_version == 'legacy-current' else PDF_TEMPLATE_VERSION)
remember_approved_prices(db, order)
add_log(order, '审批通过并归档正式 PDF', user, request.form.get('comment'), old)
db.commit()
flash('审批通过,正式 PDF 已归档', 'ok')
@@ -263,11 +266,14 @@ def register_order_routes(app):
order.order_date = datetime.now().strftime('%Y-%m-%d')
order.items = [OrderItem(seq=i, **{field: getattr(item, field) for field in ITEM_FIELDS})
for i, item in enumerate(source.items, 1)]
for item in order.items:
item.delivery_date = ''
recalc(order)
db.add(order)
remember_creation(key, 'copy:%s' % oid, order)
add_log(order, '复制创建草稿', user, '来源:' + source.order_no)
db.commit()
flash('已复制为新草稿。请填写新交期,并核对单价和订单条款。', 'ok')
return redirect(url_for('order_edit', oid=order.id))
@app.route('/orders/<int:oid>/pdf')
+12
View File
@@ -11,6 +11,10 @@ from security import current_user, db, login_required
TRACKING_FILTERS = {
'open': '待跟进订单',
'work_undelivered': '未交付订单(含部分交货)',
'work_invoice_due': '已交付已收票待付款',
'work_completed': '已完成订单',
'work_other': '其他待跟进 / 待核对',
'prepayment': '需预付款',
'invoice_due': '已收票待付款',
'unpaid': '未付款',
@@ -35,9 +39,17 @@ def tracking_conditions():
completed = and_(current_payment, current_fulfillment, Order.payment_status == 'paid',
Order.delivery_status == 'delivered')
closed = and_(completed, Order.invoice_status == 'received')
work_undelivered = and_(current_fulfillment, Order.delivery_status.in_(('pending', 'partial')))
work_invoice_due = and_(current_fulfillment, current_payment,
Order.delivery_status == 'delivered', Order.invoice_status == 'received',
Order.payment_status.in_(('unpaid', 'partial')))
return {
'open': ~closed,
'closed': closed,
'work_undelivered': work_undelivered,
'work_invoice_due': work_invoice_due,
'work_completed': closed,
'work_other': and_(~closed, ~work_undelivered, ~work_invoice_due),
'prepayment': and_(current_fulfillment, Order.needs_prepayment.is_(True), payment_due),
'invoice_due': and_(current_fulfillment, Order.invoice_status == 'received', payment_due),
'unpaid': and_(current_payment, Order.payment_status == 'unpaid'),
+16
View File
@@ -0,0 +1,16 @@
/* Filter existing supplier/material options without changing their submitted IDs. */
window.POFilterSelect = {
bind: function (input, select) {
if (!input || !select) return;
const options = Array.from(select.options);
const normalize = value => String(value || '').trim().toLocaleLowerCase();
input.addEventListener('input', function () {
const term = normalize(input.value);
const matches = options.filter((option, index) => index === 0 ||
normalize(option.textContent + ' ' + (option.dataset.search || '')).includes(term));
select.replaceChildren(...matches);
select.selectedIndex = 0;
select.setAttribute('aria-label', term ? `匹配 ${matches.length - 1} 项,请选择` : '请选择');
});
}
};
+9 -1
View File
@@ -35,7 +35,7 @@ table.grid td.num, table.grid th.num { text-align: right; }
.form-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 16px; }
.form-grid.two { grid-template-columns: repeat(2, 1fr); }
label { display: block; font-size: 12px; color: #666; margin-bottom: 3px; }
input:not([type]), input[type=text], input[type=password], input[type=number], input[type=date], select, textarea {
input:not([type]), input[type=text], input[type=search], input[type=password], input[type=number], input[type=date], select, textarea {
width: 100%; padding: 6px 8px; border: 1px solid #ccd3da; border-radius: 4px;
font-size: 13px; font-family: inherit; background: #fff;
}
@@ -133,6 +133,13 @@ table.grid tr.row-off .btn { opacity: .8; }
/* Payment / delivery workspace: independent states, task-first rows. */
.tracking-dashboard .card-head { align-items: flex-start; }
.tracking-dashboard .card-head > a { flex-shrink: 0; white-space: nowrap; }
.order-stage-links { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 14px 0 8px; }
.order-stage { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 12px; border: 1px solid #dce4ed; border-radius: 6px; color: #123a6b; }
.order-stage:hover, .order-stage.selected { background: #eef5ff; border-color: #1668dc; text-decoration: none; }
.order-stage span { white-space: nowrap; font-size: 12px; }
.matched-materials { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; color: #42526a; font-size: 12px; }
.matched-materials span { padding: 2px 6px; background: #eef5ff; border-radius: 4px; }
.copy-order-form { margin: 7px 0 4px; }
.tracking-dashboard .card-head p { margin: 6px 0 0; }
.tracking-focus .stat { position: relative; border-top: 3px solid #d99a30; padding-right: 28px; }
.tracking-focus .focus-paid_undelivered { border-top-color: #1668dc; }
@@ -196,6 +203,7 @@ table.grid tr.row-off .btn { opacity: .8; }
.tracking-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.tracking-forms { grid-template-columns: minmax(0, 1fr); }
.tracking-panels { grid-template-columns: minmax(0, 1fr); }
.order-stage-links { grid-template-columns: minmax(0, 1fr); }
table.grid.order-cards { display: block; min-width: 0; }
table.order-cards thead { display: none; }
table.order-cards tbody { display: block; }
+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 %}