优化付款交货待办展示并支持列表快捷登记

This commit is contained in:
reddaniel110
2026-09-17 15:20:39 +08:00
parent 000f4198a8
commit 0e45a58e5b
16 changed files with 591 additions and 103 deletions
+12 -4
View File
@@ -8,13 +8,14 @@ from functools import wraps
from flask import (Flask, flash, jsonify, redirect, render_template, request,
send_file, send_from_directory, session, url_for)
from sqlalchemy import func
from sqlalchemy import func, or_
from werkzeug.security import check_password_hash, generate_password_hash
from security import db, current_user, login_required, role_required, init_security, validate_password, sign_in
from money import number, format_price
from order_logic import scope
from order_routes import register_order_routes
from order_tracking import TRACKING_FILTERS, filter_tracking, register_tracking_routes
from order_tracking import (TRACKING_FILTERS, filter_tracking, register_tracking_routes,
tracking_conditions, tracking_priority)
from excel_io import read_rows, read_material_rows, read_supplier_rows, save_upload
from flask import abort
@@ -77,10 +78,14 @@ def index():
month_total = (scope(db.query(func.sum(Order.total_amount)))
.filter(Order.status == "approved").scalar() or 0)
tracking_counts = {key: filter_tracking(scope(db.query(Order)), key).count() for key in TRACKING_FILTERS}
tracking_rows = scope(db.query(Order)).filter_by(status='approved').order_by(Order.id.desc()).limit(12).all()
tracking_key = request.args.get('tracking', 'open')
tracking_query = filter_tracking(scope(db.query(Order)), tracking_key or 'open')
tracking_total = tracking_query.count()
tracking_rows = tracking_query.order_by(tracking_priority(), Order.id.asc()).limit(12).all()
return render_template("index.html", user=u, todo=todo, my_orders=my_orders,
counts=counts, month_total=month_total,
tracking_counts=tracking_counts, tracking_rows=tracking_rows,
tracking_key=tracking_key or 'open', tracking_total=tracking_total,
status_text=STATUS_TEXT, role_text=ROLE_TEXT)
@@ -108,7 +113,10 @@ def orders():
q = q.filter(Order.order_date <= date_to)
if u.role == "maker":
q = q.filter(Order.created_by == u.id)
q = filter_tracking(q, request.args.get('tracking', ''))
tracking = request.args.get('tracking', '')
q = filter_tracking(q, tracking)
if not tracking 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()
return render_template("orders.html", user=u, rows=rows, status_text=STATUS_TEXT,
doc_types=DOC_TYPES, args=request.args)
+69 -7
View File
@@ -1,8 +1,8 @@
"""Internal order follow-up; approved documents stay frozen."""
from datetime import date
from flask import abort, flash, redirect, request, url_for
from sqlalchemy import and_, or_
from flask import abort, flash, jsonify, redirect, render_template, request, url_for
from sqlalchemy import and_, case, or_
from models import DELIVERY_STATUS, INVOICE_STATUS, PAYMENT_STATUS, Order
from order_logic import add_log, get_order, normalize_date
@@ -10,6 +10,7 @@ from security import current_user, db, login_required
TRACKING_FILTERS = {
'open': '待跟进订单',
'prepayment': '需预付款',
'invoice_due': '已收票待付款',
'unpaid': '未付款',
@@ -22,6 +23,7 @@ TRACKING_FILTERS = {
'completed': '已付款且已交货',
'unrecorded': '跟进待登记',
'recheck': '变更后待核对',
'closed': '付款、交货、收票均完成',
}
@@ -30,7 +32,12 @@ def tracking_conditions():
current_payment = or_(Order.payment_revision < 0, Order.payment_revision == Order.revision)
payment_due = and_(current_payment, Order.payment_status.in_(('unpaid', 'partial')))
delivery_open = and_(current_fulfillment, Order.delivery_status.in_(('pending', 'partial')))
completed = and_(current_payment, current_fulfillment, Order.payment_status == 'paid',
Order.delivery_status == 'delivered')
closed = and_(completed, Order.invoice_status == 'received')
return {
'open': ~closed,
'closed': closed,
'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'),
@@ -40,8 +47,7 @@ def tracking_conditions():
'partial_delivery': and_(current_fulfillment, Order.delivery_status == 'partial'),
'delivered': and_(current_fulfillment, Order.delivery_status == 'delivered'),
'paid_undelivered': and_(current_payment, Order.payment_status == 'paid', delivery_open),
'completed': and_(current_payment, current_fulfillment, Order.payment_status == 'paid',
Order.delivery_status == 'delivered'),
'completed': completed,
'unrecorded': or_(Order.invoice_status == 'unknown', Order.delivery_status == 'unknown',
Order.payment_status == 'unknown'),
'recheck': or_(~current_fulfillment, ~current_payment),
@@ -57,6 +63,48 @@ def filter_tracking(query, key):
return query.filter(Order.status == 'approved', conditions[key])
def tracking_priority():
conditions = tracking_conditions()
return case(*[(conditions[key], rank) for rank, key in enumerate(
('recheck', 'prepayment', 'invoice_due', 'paid_undelivered', 'unrecorded'))], else_=5)
def tracking_tasks(order):
if order.status != 'approved':
return []
if order.fulfillment_stale or order.payment_stale:
return [('recheck', '变更后待核对')]
tasks = []
if order.payment_status in ('unpaid', 'partial'):
if order.needs_prepayment:
tasks.append(('prepayment', '安排预付款' if order.payment_status == 'unpaid' else '核对预付款及剩余付款安排'))
elif order.invoice_status == 'received':
tasks.append(('invoice_due', '已收票,跟进付款' if order.payment_status == 'unpaid' else '跟进剩余付款'))
else:
tasks.append(('payment', '跟进付款安排'))
if order.delivery_status in ('pending', 'partial'):
tasks.append(('delivery', '跟进剩余交货' if order.delivery_status == 'partial' else '跟进交货'))
if order.invoice_status == 'pending':
tasks.append(('invoice', '跟进收票'))
if 'unknown' in (order.payment_status, order.delivery_status, order.invoice_status):
tasks.append(('unrecorded', '补充跟进登记'))
return tasks or [('closed', '付款、交货、收票均完成')]
def latest_tracking(order):
latest = {}
for log in reversed(order.logs):
if log.action in ('更新采购跟进', '登记付款状态') and log.action not in latest:
latest[log.action] = log
return latest
def tracking_response(oid):
if request.headers.get('X-Tracking-Modal') == '1':
return jsonify(ok=True)
return redirect(url_for('order_view', oid=oid) + '#order-tracking')
def can_follow(order, user):
return (order.status == 'approved' and user.role in ('maker', 'admin')
and (user.role == 'admin' or order.created_by == user.id))
@@ -84,7 +132,21 @@ def register_tracking_routes(app):
@app.context_processor
def tracking_context():
return {'INVOICE_STATUS': INVOICE_STATUS, 'DELIVERY_STATUS': DELIVERY_STATUS,
'PAYMENT_STATUS': PAYMENT_STATUS, 'TRACKING_FILTERS': TRACKING_FILTERS}
'PAYMENT_STATUS': PAYMENT_STATUS, 'TRACKING_FILTERS': TRACKING_FILTERS,
'can_follow': can_follow, 'can_record_payment': can_record_payment,
'tracking_tasks': tracking_tasks, 'latest_tracking': latest_tracking}
@app.get('/orders/<int:oid>/tracking/<kind>')
@login_required
def tracking_editor(oid, kind):
if kind not in ('follow-up', 'payment'):
abort(404)
order, user = get_order(oid), current_user()
allowed = can_follow(order, user) if kind == 'follow-up' else can_record_payment(order, user)
if not allowed:
abort(403)
template = 'tracking_editor_fragment.html' if request.args.get('fragment') == '1' else 'tracking_editor.html'
return render_template(template, o=order, user=user, kind=kind)
@app.post('/orders/<int:oid>/follow-up')
@login_required
@@ -124,7 +186,7 @@ def register_tracking_routes(app):
add_log(order, '更新采购跟进', user, ';'.join(changes + (['更正原因:' + reason] if reason else [])))
db.commit()
flash('采购跟进已保存', 'ok')
return redirect(url_for('order_view', oid=oid) + '#order-tracking')
return tracking_response(oid)
@app.post('/orders/<int:oid>/payment')
@login_required
@@ -160,4 +222,4 @@ def register_tracking_routes(app):
add_log(order, '登记付款状态', user, ';'.join(changes + (['更正原因:' + reason] if reason else [])))
db.commit()
flash('付款状态已保存', 'ok')
return redirect(url_for('order_view', oid=oid) + '#order-tracking')
return tracking_response(oid)
+70
View File
@@ -130,6 +130,58 @@ table.grid tr.row-off .btn { opacity: .8; }
.badge.track-partial { background: #e6f0ff; color: #14509b; }
.badge.track-received, .badge.track-delivered, .badge.track-paid { background: #e8f6ee; color: #12703f; }
.mobile-only { display: none; }
/* 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; }
.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; }
.tracking-focus .focus-recheck { border-top-color: #a65d26; }
.tracking-focus .stat.selected { background: #eef5ff; border-color: #1668dc; }
.tracking-focus .stat .k { color: #42526a; }
.stat-arrow { position: absolute; right: 12px; top: 24px; color: #697a91; }
.tracking-board-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
.tracking-board-head h3 { margin: 0; }
.tracking-filter { margin: 0 0 10px; flex-wrap: nowrap; }
.tracking-filter select { width: auto; max-width: 240px; }
.tracking-filter label { white-space: nowrap; }
.status-steps { display: flex; list-style: none; margin: 0; padding: 0; gap: 3px; }
.status-steps li { flex: 1; white-space: nowrap; text-align: center; padding: 6px 3px; border: 1px solid #e2e6ea; border-radius: 4px; font-size: 11px; color: #7b8490; background: #f7f8fa; }
.status-steps .current { font-weight: 600; border-color: currentColor; }
.status-steps .track-unpaid, .status-steps .track-pending { background: #fff4e0; color: #935100; }
.status-steps .track-partial { background: #e6f0ff; color: #14509b; }
.status-steps .track-paid, .status-steps .track-delivered { background: #e8f6ee; color: #12703f; }
.order-progress { min-width: 154px; }
.order-progress .badge, .order-progress .tracking-warning { display: inline-block; margin-bottom: 5px; }
.order-progress.is-stale .current { color: #777; background: #eef0f2; }
.tracking-tasks { display: flex; flex-wrap: wrap; gap: 4px; margin: 5px 0 9px; }
.task-tag { color: #586577; font-size: 12px; line-height: 1.6; }
.task-tag + .task-tag::before { content: '·'; margin-right: 4px; }
.task-prepayment, .task-invoice_due, .task-recheck { color: #935100; font-weight: 600; }
.task-closed { color: #12703f; }
.tracking-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.tracking-actions .btn { white-space: nowrap; }
.tracking-detail-link { font-size: 12px; display: inline-block; padding: 4px 0; }
.supplier-name { margin: 5px 0; overflow-wrap: anywhere; }
.order-identity { max-width: 240px; }
.tracking-table td, .orders-table td { padding-top: 12px !important; padding-bottom: 12px !important; }
.tracking-completed-link { border-top: 1px solid #e2e6ea; margin-top: 12px; padding-top: 12px; font-size: 12px; }
.completed-toggle { display: flex; align-items: center; gap: 4px; margin: 0; color: #555; }
.tracking-panels { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 12px 0 20px; }
.tracking-panel { padding: 16px; border: 1px solid #dfe5ec; border-radius: 6px; min-width: 0; }
.tracking-panel h3 { margin: 0 0 12px; }
.tracking-panel .status-steps li { font-size: 13px; padding: 9px 3px; }
.follow-note { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.7; }
.update-meta { color: #637187; font-size: 12px; margin-bottom: 0; }
#tracking-dialog { width: 540px; max-width: calc(100% - 24px); max-height: calc(100% - 32px); padding: 0; border: 0; border-radius: 10px; box-shadow: 0 16px 60px #0004; }
#tracking-dialog::backdrop { background: #15253b80; }
#tracking-dialog .modal-head { position: sticky; top: 0; background: white; z-index: 1; }
#tracking-dialog h3 { margin: 14px 0; font-size: 15px; }
#tracking-reload { margin-bottom: 12px; }
.editor-order { line-height: 1.8; margin: 0; overflow-wrap: anywhere; }
.tracking-editor-page { max-width: 600px; }
.btn:disabled { opacity: .55; cursor: wait; }
[hidden] { display: none !important; }
@media (max-width: 800px) {
.topbar { height: auto; min-height: 52px; flex-wrap: wrap; gap: 12px; padding: 12px 16px; }
.topbar .brand { width: 100%; }
@@ -143,6 +195,24 @@ table.grid tr.row-off .btn { opacity: .8; }
.stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.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); }
table.grid.order-cards { display: block; min-width: 0; }
table.order-cards thead { display: none; }
table.order-cards tbody { display: block; }
table.order-cards tr { display: block; border: 1px solid #dfe5ec; border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; background: #fff; }
table.order-cards td { display: flex; gap: 10px; align-items: center; justify-content: space-between; border: 0; padding: 7px 0 !important; text-align: left; min-width: 0; max-width: none; }
table.order-cards td::before { content: attr(data-label); font-size: 12px; color: #637187; flex: 0 0 55px; }
table.order-cards .order-identity { display: block; padding-bottom: 10px !important; border-bottom: 1px solid #edf0f4; margin-bottom: 5px; }
table.order-cards .order-identity::before, table.order-cards .order-next::before { display: none; }
table.order-cards .order-next { display: block; border-top: 1px solid #edf0f4; margin-top: 5px; padding-top: 10px !important; }
table.order-cards .order-progress { flex: 1; min-width: 0; max-width: 320px; }
table.order-cards .tracking-tasks { flex: 1; }
table.order-cards td[data-label="收票 / 待办"] { flex-wrap: wrap; }
table.order-cards .empty-row td { display: block; }
.tracking-actions .btn { display: inline-flex; align-items: center; }
.tracking-filter { width: 100%; }
.tracking-filter select { flex: 1; max-width: none; min-width: 0; }
.tracking-panel { padding: 12px; }
table.grid { min-width: 680px; }
table.approval-flow { min-width: 0; }
.mobile-only { display: block; }
+85
View File
@@ -0,0 +1,85 @@
/* Reuse server forms and existing permissions; keep edits on the current list. */
(function () {
'use strict';
var dialog = document.getElementById('tracking-dialog');
if (!dialog || typeof dialog.showModal !== 'function' || !window.fetch) return;
var content = document.getElementById('tracking-dialog-content');
var errorBox = document.getElementById('tracking-dialog-error');
var reload = document.getElementById('tracking-reload');
var close = dialog.querySelector('[data-tracking-close]');
var editorUrl = '', serial = 0, saving = false, opener;
function showError(message) {
errorBox.textContent = message;
errorBox.hidden = false;
reload.hidden = false;
}
function errorText(html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var message = doc.querySelector('.wrap .card p, body > p');
return message ? message.textContent : '暂时无法完成操作,请重新读取当前状态后再试。';
}
async function loadEditor() {
var current = ++serial;
errorBox.hidden = reload.hidden = true;
content.textContent = '正在读取最新状态…';
try {
var response = await fetch(editorUrl + '?fragment=1', {cache: 'no-store', credentials: 'same-origin'});
var html = await response.text();
if (current !== serial || !dialog.open) return;
if (response.redirected) { window.location.assign(response.url); return; }
if (!response.ok) { content.textContent = ''; showError(errorText(html)); return; }
content.innerHTML = html;
var field = content.querySelector('select, input:not([type=hidden])');
if (field) field.focus();
} catch (error) {
if (current === serial && dialog.open) { content.textContent = ''; showError('读取失败,请检查连接后重试。'); }
}
}
document.addEventListener('click', function (event) {
var link = event.target.closest('[data-quick-tracking]');
if (!link || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
opener = link;
editorUrl = link.href;
dialog.showModal();
loadEditor();
});
close.addEventListener('click', function () { if (!saving) dialog.close(); });
dialog.addEventListener('cancel', function (event) { if (saving) event.preventDefault(); });
dialog.addEventListener('close', function () {
++serial;
content.textContent = '';
if (opener) opener.focus();
});
reload.addEventListener('click', loadEditor);
dialog.addEventListener('submit', async function (event) {
var form = event.target;
event.preventDefault();
if (saving) return;
var data = new FormData(form);
var submit = form.querySelector('[type=submit]');
saving = true;
close.disabled = reload.disabled = submit.disabled = true;
errorBox.hidden = reload.hidden = true;
var retry = true;
try {
var response = await fetch(form.action, {method: 'POST', body: data,
credentials: 'same-origin', headers: {'X-Tracking-Modal': '1'}});
if (response.redirected) { window.location.assign(response.url); return; }
if (response.ok && (response.headers.get('Content-Type') || '').indexOf('application/json') >= 0) {
var result = await response.json();
if (result.ok) { window.location.reload(); return; }
}
retry = response.status === 400;
showError(errorText(await response.text()));
} catch (error) {
retry = false;
showError('连接中断,无法确认是否保存成功。请重新读取当前状态,核对后再操作。');
} finally {
saving = false;
close.disabled = reload.disabled = false;
submit.disabled = !retry;
}
});
})();
+11
View File
@@ -36,5 +36,16 @@
{% block body %}{% endblock %}
</div>
<footer class="foot">{{ COMPANY_NAME }} · 采购订单管理系统 v4.0</footer>
{% if session.get('user_id') %}
<dialog id="tracking-dialog" aria-labelledby="tracking-dialog-title">
<div class="modal-head"><span id="tracking-dialog-title">快捷登记</span><button type="button" class="btn sm" data-tracking-close aria-label="关闭快捷登记">关闭</button></div>
<div class="modal-body">
<div id="tracking-dialog-error" class="flash error" role="alert" hidden></div>
<button type="button" class="btn sm" id="tracking-reload" hidden>重新读取当前状态</button>
<div id="tracking-dialog-content" aria-live="polite"></div>
</div>
</dialog>
<script src="{{ url_for('static', filename='tracking.js') }}" defer></script>
{% endif %}
</body>
</html>
+28 -19
View File
@@ -8,29 +8,38 @@
<div class="stat"><div class="v">{{ '%.2f'|format(month_total) }}</div><div class="k">审批通过订单总额(元)</div></div>
</div>
<div class="card tracking-dashboard">
<div class="card-head"><h2>付款与交货概况</h2><a href="{{ url_for('orders', status='approved') }}">查看全部</a></div>
<p class="muted">统计当前可见、审批通过的订单。各类数量可能重叠;部分付款和部分交货按订单状态统计。</p>
<div class="stat-row">
{% for key in ['prepayment','invoice_due','unpaid','paid','undelivered','partial_delivery','paid_undelivered','completed'] %}
<a class="stat tracking-stat" data-tracking="{{ key }}" href="{{ url_for('orders', tracking=key) }}"><div class="v">{{ tracking_counts[key] }}</div><div class="k">{{ TRACKING_FILTERS[key] }}</div></a>
<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="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>
{% endfor %}
</div>
<div class="toolbar tracking-shortcuts">{% for key in ['partial_payment','delivered','unrecorded','recheck'] %}<a class="btn sm" data-tracking="{{ key }}" href="{{ url_for('orders', tracking=key) }}">{{ TRACKING_FILTERS[key] }}({{ tracking_counts[key] }})</a>{% endfor %}</div>
<h3>最近审批通过的订单</h3>
<table class="grid tracking-table">
<tr><th>订单 / 供应商</th><th class="num">订单金额</th><th>预付款</th><th>收票</th><th>付款</th><th>交货</th><th>操作</th></tr>
{% for o in tracking_rows %}
<tr>
<td><a href="{{ url_for('order_view', oid=o.id) }}#order-tracking">{{ o.order_no }}</a><div class="muted">{{ o.supplier_name }}</div>{% if o.fulfillment_stale or o.payment_stale %}<div class="tracking-warning">变更后待核对</div>{% endif %}</td>
<td class="num">{{ '%.2f'|format(o.total_amount) }}</td><td>{{ '需要' if o.needs_prepayment else '—' }}</td>
<td><span class="badge track-{{ o.invoice_status }}">{{ o.invoice_status_text }}</span></td>
<td><span class="badge track-{{ o.payment_status }}">{{ o.payment_status_text }}</span></td>
<td><span class="badge track-{{ o.delivery_status }}">{{ o.delivery_status_text }}</span></td>
<td><a class="btn sm" href="{{ url_for('order_view', oid=o.id) }}#order-tracking">查看跟进</a></td>
<div class="tracking-board-head">
<div><h3>{{ TRACKING_FILTERS[tracking_key] }} <span class="muted">共 {{ tracking_total }} 单</span></h3><p class="muted">同类优先显示较早订单;付款、交货、收票均完成后默认收起。</p></div>
<form method="get" action="{{ url_for('index') }}#tracking-board" class="toolbar tracking-filter">
<label for="board-filter">查看</label>
<select id="board-filter" name="tracking">{% for key, label in TRACKING_FILTERS.items() %}<option value="{{ key }}" {{ 'selected' if tracking_key == key else '' }}>{{ label }}({{ tracking_counts[key] }})</option>{% endfor %}</select>
<button class="btn sm">筛选</button>
</form>
</div>
<table class="grid tracking-table order-cards">
<thead><tr><th>订单 / 供应商</th><th class="num">金额</th><th>付款进度</th><th>交货进度</th><th>收票</th><th>待办 / 操作</th></tr></thead>
<tbody>{% for o in tracking_rows %}
<tr data-order-id="{{ o.id }}">
<td data-label="订单" class="order-identity"><a href="{{ url_for('order_view', oid=o.id) }}#order-tracking"><b>{{ o.order_no }}</b></a><div class="supplier-name">{{ o.supplier_name }}</div>{% if o.needs_prepayment %}<span class="badge track-partial">需提前付款{{ '(待核对)' if o.fulfillment_stale else '' }}</span>{% endif %}</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) }}</td>
<td data-label="待办 / 操作" class="order-next">{{ tasks(o) }}{{ actions(o, user) }}</td>
</tr>
{% else %}<tr><td colspan="7" class="center muted">暂无审批通过的订单</td></tr>{% endfor %}
{% else %}<tr class="empty-row"><td colspan="6" class="center muted">当前分类暂无订单。可切换分类查看其他订单。</td></tr>{% endfor %}</tbody>
</table>
{% if tracking_total > tracking_rows|length %}<p class="right"><a href="{{ url_for('orders', tracking=tracking_key) }}">查看全部 {{ tracking_total }} 单 →</a></p>{% endif %}
{% if tracking_key != 'open' %}<p><a href="{{ url_for('index') }}#tracking-board">返回全部待跟进({{ tracking_counts['open'] }})</a></p>{% endif %}
<div class="tracking-completed-link"><a data-tracking="closed" href="{{ url_for('index', tracking='closed') }}#tracking-board">查看已全部完成({{ tracking_counts['closed'] }})</a></div>
</div>
{% if todo %}
+25 -39
View File
@@ -1,44 +1,30 @@
{% from 'tracking_macros.html' import progress, invoice, tasks, form with context %}
{% set recent = latest_tracking(o) %}
<div class="card" id="order-tracking">
<h2>付款与交货跟进</h2>
<div class="tracking-summary">
<div><label>预付款安排</label><b>{{ '需提前付款' if o.needs_prepayment else '正常付款' }}</b></div>
<div><label>收票情况</label><span class="badge track-{{ o.invoice_status }}">{{ o.invoice_status_text }}</span></div>
<div><label>付款情况</label><span class="badge track-{{ o.payment_status }}">{{ o.payment_status_text }}</span>{% if o.payment_date %}<div class="muted">{{ o.payment_date }}</div>{% endif %}</div>
<div><label>交货情况</label><span class="badge track-{{ o.delivery_status }}">{{ o.delivery_status_text }}</span></div>
</div>
{% if o.fulfillment_stale or o.payment_stale %}
<p class="hint">订单内容已变更,以下记录来自变更前:{{ '采购跟进' if o.fulfillment_stale else '' }}{{ '、' if o.fulfillment_stale and o.payment_stale else '' }}{{ '付款情况' if o.payment_stale else '' }}。请对应经办人核对后保存确认。</p>
{% endif %}
<div class="card-head"><h2>付款与交货跟进</h2><span class="badge track-{{ 'partial' if o.needs_prepayment else 'unknown' }}">{{ '需提前付款' if o.needs_prepayment else '正常付款' }}{{ '(待核对)' if o.fulfillment_stale else '' }}</span></div>
{{ tasks(o) }}
{% if o.fulfillment_stale or o.payment_stale %}<p class="hint">订单内容已变更,下面保留变更前记录,请对应经办人核对后保存确认。</p>{% endif %}
{% if o.status != 'approved' %}<p class="muted">审批通过后可登记付款、收票和交货情况。已有跟进记录保留供查询。</p>{% endif %}
{% if o.fulfillment_note %}<p>采购跟进备注:{{ o.fulfillment_note }}</p>{% endif %}
{% if o.payment_note %}<p>付款备注:{{ o.payment_note }}</p>{% endif %}
<div class="tracking-panels">
<section class="tracking-panel">
<h3>付款情况</h3>
{{ progress(o, 'payment') }}
{% if o.payment_date %}<p class="muted">实际付款日期:{{ o.payment_date }}</p>{% endif %}
{% if o.payment_note %}<p class="follow-note">{{ o.payment_note }}</p>{% endif %}
{% set log = recent.get('登记付款状态') %}
<p class="update-meta">{% if log %}最近登记:{{ log.created_at.strftime('%Y-%m-%d %H:%M') }} · {{ log.user_name }}{% else %}尚无付款登记记录{% endif %}</p>
</section>
<section class="tracking-panel">
<h3>交货与收票</h3>
{{ progress(o, 'delivery') }}
<p>收票:{{ invoice(o) }}</p>
{% if o.fulfillment_note %}<p class="follow-note">{{ o.fulfillment_note }}</p>{% endif %}
{% set log = recent.get('更新采购跟进') %}
<p class="update-meta">{% if log %}最近跟进:{{ log.created_at.strftime('%Y-%m-%d %H:%M') }} · {{ log.user_name }}{% else %}尚无采购跟进记录{% endif %}</p>
</section>
</div>
<div class="tracking-forms">
{% if may_follow %}
<form id="follow-up-form" method="post" action="{{ url_for('order_follow_up', oid=o.id) }}">
<h3>制单员跟进</h3>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="row_version" value="{{ o.row_version }}">
<div class="field"><label for="needs-prepayment">付款安排</label><select id="needs-prepayment" name="needs_prepayment"><option value="0" {{ 'selected' if not o.needs_prepayment else '' }}>正常付款</option><option value="1" {{ 'selected' if o.needs_prepayment else '' }}>需提前付款</option></select></div>
<div class="form-grid two">
<div class="field"><label for="invoice-status">收票情况</label><select id="invoice-status" name="invoice_status">{% for key, label in INVOICE_STATUS.items() %}<option value="{{ key }}" {{ 'selected' if o.invoice_status == key else '' }}>{{ label }}</option>{% endfor %}</select></div>
<div class="field"><label for="delivery-status">交货情况</label><select id="delivery-status" name="delivery_status">{% for key, label in DELIVERY_STATUS.items() %}<option value="{{ key }}" {{ 'selected' if o.delivery_status == key else '' }}>{{ label }}</option>{% endfor %}</select></div>
</div>
<div class="field"><label for="fulfillment-note">跟进备注(选填)</label><textarea id="fulfillment-note" name="fulfillment_note" maxlength="500" placeholder="例如:先付30%预付款,已收到第一批货">{{ o.fulfillment_note }}</textarea></div>
<div class="field"><label for="follow-up-reason">更正原因(回退状态时必填)</label><input id="follow-up-reason" name="reason" maxlength="500"></div>
<button class="btn primary">保存采购跟进</button>
</form>
{% endif %}
{% if may_pay %}
<form id="payment-form" method="post" action="{{ url_for('order_payment', oid=o.id) }}">
<h3>审批员登记付款</h3>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="row_version" value="{{ o.row_version }}">
<div class="field"><label for="payment-status">付款状态</label><select id="payment-status" name="payment_status">{% for key, label in PAYMENT_STATUS.items() %}<option value="{{ key }}" {{ 'selected' if o.payment_status == key else '' }}>{{ label }}</option>{% endfor %}</select></div>
<div class="field"><label for="payment-date">实际付款日期(部分付款或已付款时必填)</label><input id="payment-date" type="date" name="payment_date" value="{{ o.payment_date }}"></div>
<div class="field"><label for="payment-note">付款备注(选填)</label><textarea id="payment-note" name="payment_note" maxlength="500" placeholder="例如:已付首款,剩余尾款待交货后支付">{{ o.payment_note }}</textarea></div>
<div class="field"><label for="payment-reason">更正原因(回退状态时必填)</label><input id="payment-reason" name="reason" maxlength="500"></div>
<button class="btn primary">保存付款状态</button>
</form>
{% endif %}
{% if may_follow %}{{ form(o, 'follow-up') }}{% endif %}
{% if may_pay %}{{ form(o, 'payment') }}{% endif %}
</div>
</div>
+3 -1
View File
@@ -19,6 +19,8 @@
<a class="btn" href="{{ url_for('orders') }}">返回列表</a>
</div>
{% if o.status == 'approved' %}{% include 'order_tracking.html' %}{% endif %}
<div class="card">
<h2>单据信息</h2>
<p class="mobile-only muted">单据信息和物料明细可左右滑动查看。</p>
@@ -81,7 +83,7 @@
{% if o.note %}<p class="muted">补充说明:{{ o.note }}</p>{% endif %}
</div>
{% include 'order_tracking.html' %}
{% if o.status != 'approved' %}{% include 'order_tracking.html' %}{% endif %}
<div class="card">
<h2>审批流程</h2>
+15 -29
View File
@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block body %}
{% from 'tracking_macros.html' import progress, invoice, tasks, actions with context %}
<h1 class="page">订单列表</h1>
<div class="card">
<form class="toolbar" method="get">
@@ -23,41 +24,26 @@
<option value="">全部跟进情况</option>
{% for key, label in TRACKING_FILTERS.items() %}<option value="{{ key }}" {{ 'selected' if args.get('tracking') == key else '' }}>{{ label }}</option>{% endfor %}
</select>
<label class="completed-toggle"><input type="checkbox" name="show_completed" value="1" {{ 'checked' if args.get('show_completed') == '1' else '' }}> 显示全部完成订单</label>
<button class="btn primary">查询</button>
<span class="grow"></span>
<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>
<table class="grid">
<tr>
<th>单号</th><th>类型</th><th>供应商</th><th>采购日期</th><th class="num">金额</th>
<th>审批状态</th><th>付款与交货</th><th>制单</th><th>审批</th><th class="nowrap">操作</th>
{% if not args.get('tracking') and args.get('show_completed') != '1' %}<p class="muted">已收起付款、交货、收票均完成的订单。勾选“显示全部完成订单”可查看。</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="审批"><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>
</tr>
{% for o in rows %}
<tr>
<td class="nowrap">{{ o.order_no }}</td>
<td>{{ o.doc_type_text }}</td>
<td>{{ o.supplier_name }}</td>
<td class="nowrap">{{ o.order_date }}</td>
<td class="num">{{ '%.2f'|format(o.total_amount) }}</td>
<td><span class="badge {{ o.status }}">{{ o.status_text }}</span></td>
<td>
{% if o.needs_prepayment %}<span class="badge track-partial">需提前付款</span>{% endif %}
<span class="badge track-{{ o.invoice_status }}">收票:{{ o.invoice_status_text }}</span>
<span class="badge track-{{ o.payment_status }}">付款:{{ o.payment_status_text }}</span>
<span class="badge track-{{ o.delivery_status }}">交货:{{ o.delivery_status_text }}</span>
{% if o.fulfillment_stale or o.payment_stale %}<div class="tracking-warning">变更后待核对</div>{% endif %}
</td>
<td>{{ o.created_by_name }}</td>
<td>{{ o.approved_by or '-' }}</td>
<td class="nowrap">
<a class="btn sm" href="{{ url_for('order_view', oid=o.id) }}">查看</a>
<a class="btn sm" href="{{ url_for('order_pdf', oid=o.id) }}" target="_blank">PDF</a>
</td>
</tr>
{% else %}
<tr><td colspan="10" class="center muted">没有符合条件的订单</td></tr>
{% endfor %}
{% else %}<tr class="empty-row"><td colspan="7" class="center muted">没有符合条件的订单</td></tr>{% endfor %}</tbody>
</table>
</div>
{% endblock %}
+6
View File
@@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% block body %}
<h1 class="page">{{ '登记付款' if kind == 'payment' else '登记收票 / 交货' }}</h1>
<div class="card tracking-editor-page">{% include 'tracking_editor_fragment.html' %}</div>
<a class="btn" href="{{ url_for('order_view', oid=o.id) }}#order-tracking">返回订单</a>
{% endblock %}
@@ -0,0 +1,5 @@
{% from 'tracking_macros.html' import form with context %}
<div data-tracking-editor>
<p class="editor-order"><b>{{ o.order_no }}</b><br>{{ o.supplier_name }} · ¥{{ '%.2f'|format(o.total_amount) }}</p>
{{ form(o, kind) }}
</div>
+53
View File
@@ -0,0 +1,53 @@
{% macro progress(o, kind) -%}
{% set payment = kind == 'payment' %}
{% set state = o.payment_status if payment else o.delivery_status %}
{% set stale = o.payment_stale if payment else o.fulfillment_stale %}
{% set steps = [('unpaid', '未付款'), ('partial', '部分付款'), ('paid', '全部付款')] if payment else [('pending', '未交货'), ('partial', '部分交货'), ('delivered', '全部交货')] %}
<div class="order-progress {{ 'is-stale' if stale else '' }}" data-progress="{{ kind }}">
{% if stale %}<span class="tracking-warning">变更前记录 · 待核对</span>{% elif state == 'unknown' %}<span class="badge track-unknown">未登记</span>{% endif %}
<ol class="status-steps" aria-label="{{ '付款' if payment else '交货' }}状态{{ '(变更前,待核对)' if stale else '' }}">
{% for value, label in steps %}<li class="{{ 'current track-' ~ state if state == value else '' }}" {% if state == value %}aria-current="step"{% endif %}>{{ label }}</li>{% endfor %}
</ol>
</div>
{%- endmacro %}
{% macro invoice(o) -%}
<span class="badge track-{{ 'unknown' if o.fulfillment_stale else o.invoice_status }}">{{ o.invoice_status_text }}{{ '(待核对)' if o.fulfillment_stale else '' }}</span>
{%- endmacro %}
{% macro tasks(o) -%}
<div class="tracking-tasks">{% for key, text in tracking_tasks(o) %}<span class="task-tag task-{{ key }}">{{ text }}</span>{% endfor %}</div>
{%- endmacro %}
{% macro actions(o, user) -%}
<div class="tracking-actions">
{% if can_follow(o, user) %}<a class="btn sm" data-quick-tracking href="{{ url_for('tracking_editor', oid=o.id, kind='follow-up') }}">登记收票 / 交货</a>{% endif %}
{% if can_record_payment(o, user) %}<a class="btn sm primary" data-quick-tracking href="{{ url_for('tracking_editor', oid=o.id, kind='payment') }}">登记付款</a>{% endif %}
<a class="tracking-detail-link" href="{{ url_for('order_view', oid=o.id) }}#order-tracking">详情与记录</a>
</div>
{%- endmacro %}
{% macro form(o, kind) -%}
{% set payment = kind == 'payment' %}
<form id="{{ 'payment-form' if payment else 'follow-up-form' }}" method="post" action="{{ url_for('order_payment' if payment else 'order_follow_up', oid=o.id) }}">
<h3>{{ '审批员登记付款' if payment else '制单员跟进' }}</h3>
{% if (payment and o.payment_stale) or (not payment and o.fulfillment_stale) %}<p class="hint">订单内容已变更,请核对以下变更前记录。保存即确认本次订单的跟进情况。</p>{% endif %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="row_version" value="{{ o.row_version }}">
{% if payment %}
<div class="field"><label for="payment-status">付款状态</label><select id="payment-status" name="payment_status">{% for key, label in PAYMENT_STATUS.items() %}<option value="{{ key }}" {{ 'selected' if o.payment_status == key else '' }}>{{ label }}</option>{% endfor %}</select></div>
<div class="field"><label for="payment-date">实际付款日期(部分付款或已付款时必填)</label><input id="payment-date" type="date" name="payment_date" value="{{ o.payment_date }}"></div>
<div class="field"><label for="payment-note">付款备注(选填)</label><textarea id="payment-note" name="payment_note" maxlength="500" placeholder="例如:已付首款,剩余尾款待交货后支付">{{ o.payment_note }}</textarea></div>
<div class="field"><label for="payment-reason">更正原因(回退状态时必填)</label><input id="payment-reason" name="reason" maxlength="500"></div>
{% else %}
<div class="field"><label for="needs-prepayment">付款安排</label><select id="needs-prepayment" name="needs_prepayment"><option value="0" {{ 'selected' if not o.needs_prepayment else '' }}>正常付款</option><option value="1" {{ 'selected' if o.needs_prepayment else '' }}>需提前付款</option></select></div>
<div class="form-grid two">
<div class="field"><label for="invoice-status">收票情况</label><select id="invoice-status" name="invoice_status">{% for key, label in INVOICE_STATUS.items() %}<option value="{{ key }}" {{ 'selected' if o.invoice_status == key else '' }}>{{ label }}</option>{% endfor %}</select></div>
<div class="field"><label for="delivery-status">交货情况</label><select id="delivery-status" name="delivery_status">{% for key, label in DELIVERY_STATUS.items() %}<option value="{{ key }}" {{ 'selected' if o.delivery_status == key else '' }}>{{ label }}</option>{% endfor %}</select></div>
</div>
<div class="field"><label for="fulfillment-note">跟进备注(选填)</label><textarea id="fulfillment-note" name="fulfillment_note" maxlength="500" placeholder="例如:首批货已到,剩余预计周五交齐">{{ o.fulfillment_note }}</textarea></div>
<div class="field"><label for="follow-up-reason">更正原因(回退状态时必填)</label><input id="follow-up-reason" name="reason" maxlength="500"></div>
{% endif %}
<button class="btn primary" type="submit">{{ '保存付款状态' if payment else '保存采购跟进' }}</button>
</form>
{%- endmacro %}