新增订单收票付款交货跟进并完善使用升级手册
增加部分付款和部分交货状态、工作台筛选、权限校验、跟进记录及 v4 数据迁移;补充 Windows EXE 构建与验收脚本。附 16 页 PDF 使用升级手册及企业微信详细配置说明、生成脚本和测试截图。验证:76 项回归测试、EXE 浏览器验收与 PDF 逐页校验通过。
This commit is contained in:
+15
-4
@@ -14,6 +14,7 @@ from security import db, current_user, login_required, role_required, init_secur
|
||||
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 excel_io import read_rows, read_material_rows, read_supplier_rows, save_upload
|
||||
from flask import abort
|
||||
|
||||
@@ -51,6 +52,7 @@ app.config['PO_UPLOADS'] = UPLOADS
|
||||
init_security(app, Session, INSTANCE)
|
||||
app.jinja_env.filters['price'] = format_price
|
||||
register_order_routes(app)
|
||||
register_tracking_routes(app)
|
||||
from notification_routes import register_notification_routes
|
||||
register_notification_routes(app)
|
||||
|
||||
@@ -74,8 +76,11 @@ def index():
|
||||
counts = {s: scope(db.query(Order)).filter_by(status=s).count() for s in STATUS_TEXT}
|
||||
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()
|
||||
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,
|
||||
status_text=STATUS_TEXT, role_text=ROLE_TEXT)
|
||||
|
||||
|
||||
@@ -103,6 +108,7 @@ 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', ''))
|
||||
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)
|
||||
@@ -479,13 +485,17 @@ def export_ledger():
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.append([u"采购单号", u"单据类型", u"供应商", u"采购日期", u"状态", u"金额",
|
||||
u"制单人", u"审批人", u"创建时间"])
|
||||
u"制单人", u"审批人", u"创建时间", '需预付款', '收票状态', '付款状态',
|
||||
'付款日期', '交货状态', '采购跟进备注', '付款备注', '变更后待核对'])
|
||||
for o in rows:
|
||||
ws.append([o.order_no, o.doc_type_text, o.supplier_name, o.order_date,
|
||||
o.status_text, o.total_amount, o.created_by_name,
|
||||
o.approved_by,
|
||||
o.created_at.strftime("%Y-%m-%d %H:%M") if o.created_at else ""])
|
||||
for col, w in zip("ABCDEFGHI", (18, 14, 30, 12, 12, 12, 12, 12, 18)):
|
||||
o.created_at.strftime("%Y-%m-%d %H:%M") if o.created_at else "",
|
||||
'是' if o.needs_prepayment else '否', o.invoice_status_text, o.payment_status_text,
|
||||
o.payment_date, o.delivery_status_text, o.fulfillment_note, o.payment_note,
|
||||
'、'.join(label for label, stale in [('采购跟进', o.fulfillment_stale), ('付款', o.payment_stale)] if stale)])
|
||||
for col, w in zip("ABCDEFGHIJKLMNOPQ", (18, 14, 30, 12, 12, 12, 12, 12, 18, 12, 12, 12, 14, 12, 36, 36, 22)):
|
||||
ws.column_dimensions[col].width = w
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
@@ -528,7 +538,8 @@ if __name__ == "__main__":
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
|
||||
while port_busy(port) and port < 8120:
|
||||
port += 1
|
||||
if getattr(sys, "frozen", False) or os.environ.get("PO_OPEN_BROWSER") == "1":
|
||||
open_browser = os.environ.get("PO_OPEN_BROWSER")
|
||||
if open_browser == "1" or (getattr(sys, "frozen", False) and open_browser != "0"):
|
||||
threading.Timer(1.5, lambda: webbrowser.open("http://127.0.0.1:%d" % port)).start()
|
||||
print("=" * 56)
|
||||
print(" 博特维采购订单管理系统已启动")
|
||||
|
||||
+24
-3
@@ -14,7 +14,7 @@ def upgrade_database(path):
|
||||
if 'orders' not in tables:
|
||||
return
|
||||
version = con.execute('PRAGMA user_version').fetchone()[0]
|
||||
if version >= 3:
|
||||
if version >= 4:
|
||||
return
|
||||
duplicates = con.execute('SELECT order_no, COUNT(*) FROM orders GROUP BY order_no HAVING COUNT(*) > 1').fetchall()
|
||||
if duplicates:
|
||||
@@ -26,14 +26,35 @@ def upgrade_database(path):
|
||||
try:
|
||||
if version < 2:
|
||||
upgrade_fixed_amounts(con)
|
||||
merge_approval(con, tables)
|
||||
con.execute('PRAGMA user_version=3')
|
||||
if version < 3:
|
||||
merge_approval(con, tables)
|
||||
add_tracking(con)
|
||||
con.execute('PRAGMA user_version=4')
|
||||
con.commit()
|
||||
except Exception:
|
||||
con.rollback()
|
||||
raise
|
||||
|
||||
|
||||
def add_tracking(con):
|
||||
# Historical invoices, payments and deliveries are unknown until recorded.
|
||||
additions = {
|
||||
'needs_prepayment': 'BOOLEAN NOT NULL DEFAULT 0',
|
||||
'invoice_status': "VARCHAR(20) NOT NULL DEFAULT 'unknown'",
|
||||
'delivery_status': "VARCHAR(20) NOT NULL DEFAULT 'unknown'",
|
||||
'payment_status': "VARCHAR(20) NOT NULL DEFAULT 'unknown'",
|
||||
'payment_date': "VARCHAR(10) NOT NULL DEFAULT ''",
|
||||
'fulfillment_note': "TEXT NOT NULL DEFAULT ''",
|
||||
'payment_note': "TEXT NOT NULL DEFAULT ''",
|
||||
'fulfillment_revision': 'INTEGER NOT NULL DEFAULT -1',
|
||||
'payment_revision': 'INTEGER NOT NULL DEFAULT -1',
|
||||
}
|
||||
existing = {row[1] for row in con.execute('PRAGMA table_info(orders)')}
|
||||
for name, definition in additions.items():
|
||||
if name not in existing:
|
||||
con.execute('ALTER TABLE orders ADD COLUMN %s %s' % (name, definition))
|
||||
|
||||
|
||||
def upgrade_fixed_amounts(con):
|
||||
additions = {
|
||||
'users': {'session_version': 'INTEGER NOT NULL DEFAULT 1', 'must_change_password': 'BOOLEAN NOT NULL DEFAULT 1'},
|
||||
|
||||
+34
-1
@@ -58,6 +58,10 @@ ROLE_TEXT = {
|
||||
"approver": u"审批员",
|
||||
}
|
||||
|
||||
INVOICE_STATUS = {'unknown': '未登记', 'pending': '未收票', 'received': '已收票'}
|
||||
DELIVERY_STATUS = {'unknown': '未登记', 'pending': '未交货', 'partial': '部分交货', 'delivered': '已交货'}
|
||||
PAYMENT_STATUS = {'unknown': '未登记', 'unpaid': '未付款', 'partial': '部分付款', 'paid': '已付款'}
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
@@ -135,6 +139,15 @@ class Order(Base):
|
||||
approved_by_id = Column(Integer, ForeignKey('users.id'))
|
||||
revision = Column(Integer, nullable=False, default=0)
|
||||
row_version = Column(Integer, nullable=False, default=1)
|
||||
needs_prepayment = Column(Boolean, nullable=False, default=False)
|
||||
invoice_status = Column(String(20), nullable=False, default='pending')
|
||||
delivery_status = Column(String(20), nullable=False, default='pending')
|
||||
payment_status = Column(String(20), nullable=False, default='unpaid')
|
||||
payment_date = Column(String(10), nullable=False, default='')
|
||||
fulfillment_note = Column(Text, nullable=False, default='')
|
||||
payment_note = Column(Text, nullable=False, default='')
|
||||
fulfillment_revision = Column(Integer, nullable=False, default=-1)
|
||||
payment_revision = Column(Integer, nullable=False, default=-1)
|
||||
__mapper_args__ = {'version_id_col': row_version}
|
||||
|
||||
items = relationship("OrderItem", backref="order", cascade="all, delete-orphan",
|
||||
@@ -151,6 +164,26 @@ class Order(Base):
|
||||
def doc_type_text(self):
|
||||
return DOC_TYPES.get(self.doc_type, self.doc_type)
|
||||
|
||||
@property
|
||||
def invoice_status_text(self):
|
||||
return INVOICE_STATUS.get(self.invoice_status, '未登记')
|
||||
|
||||
@property
|
||||
def delivery_status_text(self):
|
||||
return DELIVERY_STATUS.get(self.delivery_status, '未登记')
|
||||
|
||||
@property
|
||||
def payment_status_text(self):
|
||||
return PAYMENT_STATUS.get(self.payment_status, '未登记')
|
||||
|
||||
@property
|
||||
def fulfillment_stale(self):
|
||||
return self.fulfillment_revision >= 0 and self.fulfillment_revision != self.revision
|
||||
|
||||
@property
|
||||
def payment_stale(self):
|
||||
return self.payment_revision >= 0 and self.payment_revision != self.revision
|
||||
|
||||
|
||||
class OrderItem(Base):
|
||||
__tablename__ = "order_items"
|
||||
@@ -244,5 +277,5 @@ def init_db(db_path):
|
||||
Base.metadata.create_all(engine)
|
||||
if not existed:
|
||||
with engine.connect() as con:
|
||||
con.exec_driver_sql('PRAGMA user_version=3')
|
||||
con.exec_driver_sql('PRAGMA user_version=4')
|
||||
return engine, sessionmaker(bind=engine)
|
||||
|
||||
@@ -14,6 +14,7 @@ from order_logic import (ORDER_FIELDS, ITEM_FIELDS, add_log, can_edit, can_appro
|
||||
next_order_no, recalc, remember_creation, save_form, snapshot, submit,
|
||||
validate_order)
|
||||
from pdf_export import build_pdf, seal_layout
|
||||
from order_tracking import can_follow, can_record_payment
|
||||
from security import current_user, db, login_required, role_required
|
||||
|
||||
|
||||
@@ -88,6 +89,7 @@ def register_order_routes(app):
|
||||
user, order = current_user(), get_order(oid)
|
||||
return render_template('order_view.html', user=user, o=order, editable=can_edit(order, user),
|
||||
may_approve=can_approve(order, user),
|
||||
may_follow=can_follow(order, user), may_pay=can_record_payment(order, user),
|
||||
seal=seal_layout(order.our_company) if can_approve(order, user) else None,
|
||||
may_reject=can_reject(order, user), total_cn=amount_to_cn(order.total_amount),
|
||||
status_text=STATUS_TEXT, role_text=ROLE_TEXT)
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""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 models import DELIVERY_STATUS, INVOICE_STATUS, PAYMENT_STATUS, Order
|
||||
from order_logic import add_log, get_order, normalize_date
|
||||
from security import current_user, db, login_required
|
||||
|
||||
|
||||
TRACKING_FILTERS = {
|
||||
'prepayment': '需预付款',
|
||||
'invoice_due': '已收票待付款',
|
||||
'unpaid': '未付款',
|
||||
'partial_payment': '部分付款',
|
||||
'paid': '已付款',
|
||||
'undelivered': '未交货',
|
||||
'partial_delivery': '部分交货',
|
||||
'delivered': '已交货',
|
||||
'paid_undelivered': '已付款未交齐',
|
||||
'completed': '已付款且已交货',
|
||||
'unrecorded': '跟进待登记',
|
||||
'recheck': '变更后待核对',
|
||||
}
|
||||
|
||||
|
||||
def tracking_conditions():
|
||||
current_fulfillment = or_(Order.fulfillment_revision < 0, Order.fulfillment_revision == Order.revision)
|
||||
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')))
|
||||
return {
|
||||
'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'),
|
||||
'partial_payment': and_(current_payment, Order.payment_status == 'partial'),
|
||||
'paid': and_(current_payment, Order.payment_status == 'paid'),
|
||||
'undelivered': and_(current_fulfillment, Order.delivery_status == 'pending'),
|
||||
'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'),
|
||||
'unrecorded': or_(Order.invoice_status == 'unknown', Order.delivery_status == 'unknown',
|
||||
Order.payment_status == 'unknown'),
|
||||
'recheck': or_(~current_fulfillment, ~current_payment),
|
||||
}
|
||||
|
||||
|
||||
def filter_tracking(query, key):
|
||||
if not key:
|
||||
return query
|
||||
conditions = tracking_conditions()
|
||||
if key not in conditions:
|
||||
abort(400, description='无效的订单跟进筛选条件')
|
||||
return query.filter(Order.status == 'approved', conditions[key])
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
def can_record_payment(order, user):
|
||||
return order.status == 'approved' and user.role in ('approver', 'admin')
|
||||
|
||||
|
||||
def enum_value(form, key, choices):
|
||||
value = form.get(key, '')
|
||||
if value not in choices:
|
||||
raise ValueError('请选择有效的跟进状态')
|
||||
return value
|
||||
|
||||
|
||||
def note_value(form, key):
|
||||
value = form.get(key, '').strip()
|
||||
if len(value) > 500:
|
||||
raise ValueError('备注最多填写 500 个字符')
|
||||
return value
|
||||
|
||||
|
||||
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}
|
||||
|
||||
@app.post('/orders/<int:oid>/follow-up')
|
||||
@login_required
|
||||
def order_follow_up(oid):
|
||||
order, user = get_order(oid, mutation=True), current_user()
|
||||
if not can_follow(order, user):
|
||||
abort(403)
|
||||
if set(request.form).intersection(('payment_status', 'payment_date', 'payment_note')):
|
||||
abort(403, description='付款状态须由审批员或管理员登记')
|
||||
invoice = enum_value(request.form, 'invoice_status', INVOICE_STATUS)
|
||||
delivery = enum_value(request.form, 'delivery_status', DELIVERY_STATUS)
|
||||
prepayment = request.form.get('needs_prepayment', '0')
|
||||
if prepayment not in ('0', '1'):
|
||||
raise ValueError('预付款标记无效')
|
||||
note = note_value(request.form, 'fulfillment_note')
|
||||
reason = note_value(request.form, 'reason')
|
||||
if ((order.invoice_status == 'received' and invoice != 'received') or
|
||||
(order.delivery_status in ('partial', 'delivered') and
|
||||
list(DELIVERY_STATUS).index(delivery) < list(DELIVERY_STATUS).index(order.delivery_status))):
|
||||
if not reason:
|
||||
raise ValueError('回退收票或交货状态时,请填写更正原因')
|
||||
changes = []
|
||||
values = {'needs_prepayment': prepayment == '1', 'invoice_status': invoice,
|
||||
'delivery_status': delivery, 'fulfillment_note': note}
|
||||
labels = {'needs_prepayment': ('预付款', {True: '需要', False: '不需要'}),
|
||||
'invoice_status': ('收票', INVOICE_STATUS), 'delivery_status': ('交货', DELIVERY_STATUS)}
|
||||
for key, value in values.items():
|
||||
old = getattr(order, key)
|
||||
if old != value:
|
||||
label, choices = labels.get(key, ('跟进备注', {}))
|
||||
changes.append('%s:%s → %s' % (label, choices.get(old, old or '空'), choices.get(value, value or '空')))
|
||||
setattr(order, key, value)
|
||||
if order.fulfillment_stale:
|
||||
changes.append('已核对本次变更后的采购跟进状态')
|
||||
if changes or order.fulfillment_revision != order.revision:
|
||||
order.fulfillment_revision = order.revision
|
||||
add_log(order, '更新采购跟进', user, ';'.join(changes + (['更正原因:' + reason] if reason else [])))
|
||||
db.commit()
|
||||
flash('采购跟进已保存', 'ok')
|
||||
return redirect(url_for('order_view', oid=oid) + '#order-tracking')
|
||||
|
||||
@app.post('/orders/<int:oid>/payment')
|
||||
@login_required
|
||||
def order_payment(oid):
|
||||
order, user = get_order(oid, mutation=True), current_user()
|
||||
if not can_record_payment(order, user):
|
||||
abort(403)
|
||||
if set(request.form).intersection(('needs_prepayment', 'invoice_status', 'delivery_status', 'fulfillment_note')):
|
||||
abort(403, description='采购跟进须在对应表单中登记')
|
||||
status = enum_value(request.form, 'payment_status', PAYMENT_STATUS)
|
||||
paid_date = normalize_date(request.form.get('payment_date'), '付款日期', required=status in ('partial', 'paid'))
|
||||
if paid_date and paid_date > date.today().isoformat():
|
||||
raise ValueError('实际付款日期不能晚于今天')
|
||||
if status in ('unpaid', 'unknown'):
|
||||
paid_date = ''
|
||||
note = note_value(request.form, 'payment_note')
|
||||
reason = note_value(request.form, 'reason')
|
||||
if (order.payment_status in ('partial', 'paid') and
|
||||
list(PAYMENT_STATUS).index(status) < list(PAYMENT_STATUS).index(order.payment_status) and not reason):
|
||||
raise ValueError('回退付款状态时,请填写更正原因')
|
||||
changes = []
|
||||
for key, value, label in [('payment_status', status, '付款'), ('payment_date', paid_date, '付款日期'),
|
||||
('payment_note', note, '付款备注')]:
|
||||
old = getattr(order, key)
|
||||
if old != value:
|
||||
choices = PAYMENT_STATUS if key == 'payment_status' else {}
|
||||
changes.append('%s:%s → %s' % (label, choices.get(old, old or '空'), choices.get(value, value or '空')))
|
||||
setattr(order, key, value)
|
||||
if order.payment_stale:
|
||||
changes.append('已核对本次变更后的付款状态')
|
||||
if changes or order.payment_revision != order.revision:
|
||||
order.payment_revision = order.revision
|
||||
add_log(order, '登记付款状态', user, ';'.join(changes + (['更正原因:' + reason] if reason else [])))
|
||||
db.commit()
|
||||
flash('付款状态已保存', 'ok')
|
||||
return redirect(url_for('order_view', oid=oid) + '#order-tracking')
|
||||
@@ -119,6 +119,16 @@ table.grid tr.row-off .btn { opacity: .8; }
|
||||
.seal-code { font-family: Arial, "Helvetica Neue", sans-serif; font-weight: 400; }
|
||||
|
||||
.notification-wide { grid-column: 1 / -1; }
|
||||
.tracking-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; padding: 8px 0 16px; }
|
||||
.tracking-forms { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 24px; }
|
||||
.tracking-forms form { min-width: 0; padding-top: 4px; border-top: 1px solid #e2e6ea; }
|
||||
.tracking-stat { color: #222; }
|
||||
.tracking-stat:hover { border-color: #1668dc; text-decoration: none; background: #f7faff; }
|
||||
.tracking-warning { color: #a35c00; font-size: 12px; margin-top: 4px; }
|
||||
.badge.track-unknown { background: #eef0f2; color: #666; }
|
||||
.badge.track-pending, .badge.track-unpaid { background: #fff4e0; color: #a35c00; }
|
||||
.badge.track-partial { background: #e6f0ff; color: #14509b; }
|
||||
.badge.track-received, .badge.track-delivered, .badge.track-paid { background: #e8f6ee; color: #12703f; }
|
||||
.mobile-only { display: none; }
|
||||
@media (max-width: 800px) {
|
||||
.topbar { height: auto; min-height: 52px; flex-wrap: wrap; gap: 12px; padding: 12px 16px; }
|
||||
@@ -131,6 +141,8 @@ table.grid tr.row-off .btn { opacity: .8; }
|
||||
.form-grid, .form-grid.two { grid-template-columns: 1fr; }
|
||||
.form-grid > [style*="grid-column"] { grid-column: auto !important; }
|
||||
.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); }
|
||||
table.grid { min-width: 680px; }
|
||||
table.approval-flow { min-width: 0; }
|
||||
.mobile-only { display: block; }
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
{% endwith %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<footer class="foot">{{ COMPANY_NAME }} · 采购订单管理系统 v3.0</footer>
|
||||
<footer class="foot">{{ COMPANY_NAME }} · 采购订单管理系统 v4.0</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,6 +8,31 @@
|
||||
<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>
|
||||
{% 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>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="7" class="center muted">暂无审批通过的订单</td></tr>{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if todo %}
|
||||
<div class="card">
|
||||
<h2>待我处理({{ todo|length }})</h2>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<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 %}
|
||||
{% 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-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 %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,6 +81,8 @@
|
||||
{% if o.note %}<p class="muted">补充说明:{{ o.note }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'order_tracking.html' %}
|
||||
|
||||
<div class="card">
|
||||
<h2>审批流程</h2>
|
||||
<table class="grid approval-flow">
|
||||
@@ -172,7 +174,7 @@
|
||||
<h3>提交版本</h3>
|
||||
{% for version in o.versions %}<p><a href="{{ url_for('order_version',oid=o.id,revision=version.revision) }}">第 {{ version.revision }} 次提交 · {{ version.created_at.strftime('%Y-%m-%d %H:%M') }}</a>{{ ' · 已归档正式文件' if version.pdf_data else '' }}</p>
|
||||
{% else %}<p class="muted">尚无提交快照;升级前的审批记录可在下方查看。</p>{% endfor %}
|
||||
<h3>审批记录</h3>
|
||||
<h3>审批与跟进记录</h3>
|
||||
<ul class="timeline">
|
||||
{% for lg in o.logs %}
|
||||
<li>{{ lg.created_at.strftime('%Y-%m-%d %H:%M') }} · {{ lg.user_name }} · {{ lg.action }}(第 {{ lg.revision }} 次提交)
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<input type="date" name="date_from" value="{{ args.get('date_from','') }}" style="width:150px;">
|
||||
<span>~</span>
|
||||
<input type="date" name="date_to" value="{{ args.get('date_to','') }}" style="width:150px;">
|
||||
<select name="tracking" aria-label="付款与交货筛选" style="width:170px;">
|
||||
<option value="">全部跟进情况</option>
|
||||
{% for key, label in TRACKING_FILTERS.items() %}<option value="{{ key }}" {{ 'selected' if args.get('tracking') == key else '' }}>{{ label }}</option>{% endfor %}
|
||||
</select>
|
||||
<button class="btn primary">查询</button>
|
||||
<span class="grow"></span>
|
||||
<a class="btn" href="{{ url_for('export_ledger') }}">导出 Excel 台账</a>
|
||||
@@ -27,7 +31,7 @@
|
||||
<table class="grid">
|
||||
<tr>
|
||||
<th>单号</th><th>类型</th><th>供应商</th><th>采购日期</th><th class="num">金额</th>
|
||||
<th>状态</th><th>制单</th><th>审批</th><th class="nowrap">操作</th>
|
||||
<th>审批状态</th><th>付款与交货</th><th>制单</th><th>审批</th><th class="nowrap">操作</th>
|
||||
</tr>
|
||||
{% for o in rows %}
|
||||
<tr>
|
||||
@@ -37,6 +41,13 @@
|
||||
<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">
|
||||
@@ -45,7 +56,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="9" class="center muted">没有符合条件的订单</td></tr>
|
||||
<tr><td colspan="10" class="center muted">没有符合条件的订单</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
# 博特维采购订单管理系统 v3
|
||||
# 博特维采购订单管理系统 v4
|
||||
|
||||
完整的运行、账号初始化、制单审批、撤回、作废、变更重审、备份和旧数据升级步骤,请阅读仓库根目录的 [运行与升级说明](../运行与升级说明.md)。
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
- 管理员在“通知配置”填写服务主机内网地址和企业微信群 Webhook,可发送测试通知并查看失败重试记录。
|
||||
- 手机连公司 Wi-Fi 后打开订单通知链接,登录后回到订单;盖章并确认才会提交审批。
|
||||
- 源码版本更新后,原有 EXE 需要重新打包。
|
||||
- 管理员可兼任审批员;已审批订单由制单员维护预付款、收票和交货情况,审批员或管理员登记付款,工作台按各类状态汇总。
|
||||
|
||||
Reference in New Issue
Block a user