Implement issue 1 search, order reuse and tracking
This commit is contained in:
+51
-6
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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')
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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} 项,请选择` : '请选择');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user