增加部分付款和部分交货状态、工作台筛选、权限校验、跟进记录及 v4 数据迁移;补充 Windows EXE 构建与验收脚本。附 16 页 PDF 使用升级手册及企业微信详细配置说明、生成脚本和测试截图。验证:76 项回归测试、EXE 浏览器验收与 PDF 逐页校验通过。
164 lines
8.1 KiB
Python
164 lines
8.1 KiB
Python
"""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')
|