Files
order_system/po_app/order_tracking.py
T

238 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Internal order follow-up; approved documents stay frozen."""
from datetime import date
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
from security import current_user, db, login_required
TRACKING_FILTERS = {
'open': '待跟进订单',
'work_undelivered': '未交付订单(含部分交货)',
'work_invoice_due': '已交付已收票待付款',
'work_completed': '已完成订单',
'work_other': '其他待跟进 / 待核对',
'prepayment': '需预付款',
'invoice_due': '已收票待付款',
'unpaid': '未付款',
'partial_payment': '部分付款',
'paid': '已付款',
'undelivered': '未交货',
'partial_delivery': '部分交货',
'delivered': '已交货',
'paid_undelivered': '已付款未交齐',
'completed': '已付款且已交货',
'unrecorded': '跟进待登记',
'recheck': '变更后待核对',
'closed': '付款、交货、收票均完成',
}
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')))
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'),
'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': completed,
'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 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))
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,
'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
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 tracking_response(oid)
@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 tracking_response(oid)