Files
order_system/po_app/order_tracking.py
T
reddaniel110 768b4705dc 新增订单收票付款交货跟进并完善使用升级手册
增加部分付款和部分交货状态、工作台筛选、权限校验、跟进记录及 v4 数据迁移;补充 Windows EXE 构建与验收脚本。附 16 页 PDF 使用升级手册及企业微信详细配置说明、生成脚本和测试截图。验证:76 项回归测试、EXE 浏览器验收与 PDF 逐页校验通过。
2026-09-16 13:40:43 +08:00

164 lines
8.1 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, 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')