完善采购审批流程、企业微信通知和采购专用章
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from flask import abort, flash, redirect, render_template, request, send_file, url_for
|
||||
from models import (COMPANY_NAME, DEFAULT_DELIVERY_PLACE, DEFAULT_OUR_CONTACT, DEFAULT_OUR_PHONE,
|
||||
DEFAULT_OUR_TAX_NO, DEFAULT_TAX_RATE, DOC_TYPES, ROLE_TEXT, STATUS_TEXT,
|
||||
Material, Order, OrderItem, OrderVersion, Supplier)
|
||||
from amount_cn import amount_to_cn
|
||||
from excel_io import read_rows, save_upload
|
||||
from order_logic import (ORDER_FIELDS, ITEM_FIELDS, add_log, can_edit, can_approve,
|
||||
can_reject, clear_approval, creation_request, get_order, make_item,
|
||||
next_order_no, recalc, remember_creation, save_form, snapshot, submit,
|
||||
validate_order)
|
||||
from pdf_export import build_pdf, seal_layout
|
||||
from security import current_user, db, login_required, role_required
|
||||
|
||||
|
||||
def pdf_bytes(order):
|
||||
buf = io.BytesIO()
|
||||
build_pdf(order, order.items, buf)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def register_order_routes(app):
|
||||
@app.route('/orders/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@role_required('maker')
|
||||
def order_new():
|
||||
user = current_user()
|
||||
if request.method == 'POST':
|
||||
key, existing = creation_request('new')
|
||||
if existing:
|
||||
return redirect(url_for('order_edit', oid=existing.order_id))
|
||||
doc_type = request.form.get('doc_type', 'purchase')
|
||||
order = Order(order_no=next_order_no(doc_type), doc_type=doc_type, status='draft',
|
||||
created_by=user.id, created_by_name=user.name, order_date=datetime.now().strftime('%Y-%m-%d'),
|
||||
our_company=COMPANY_NAME, delivery_place=DEFAULT_DELIVERY_PLACE,
|
||||
our_contact=DEFAULT_OUR_CONTACT, our_phone=DEFAULT_OUR_PHONE,
|
||||
our_tax_no=DEFAULT_OUR_TAX_NO, tax_rate=DEFAULT_TAX_RATE)
|
||||
sid = request.form.get('supplier_id')
|
||||
if sid:
|
||||
supplier = db.get(Supplier, int(sid))
|
||||
if not supplier or not supplier.active:
|
||||
raise ValueError('请选择有效供应商')
|
||||
for target, source in [('supplier_name', 'name'), ('supplier_contact', 'contact'),
|
||||
('supplier_phone', 'phone'), ('supplier_tax_no', 'tax_no'),
|
||||
('supplier_bank_name', 'bank_name'), ('supplier_bank_account', 'bank_account'),
|
||||
('payment_terms', 'payment_terms')]:
|
||||
setattr(order, target, getattr(supplier, source))
|
||||
db.add(order)
|
||||
remember_creation(key, 'new', order)
|
||||
add_log(order, '创建草稿', user)
|
||||
db.commit()
|
||||
return redirect(url_for('order_edit', oid=order.id))
|
||||
return render_template('order_new.html', user=user, suppliers=db.query(Supplier).filter_by(active=True).all(), doc_types=DOC_TYPES)
|
||||
|
||||
@app.route('/orders/<int:oid>', methods=['GET', 'POST'])
|
||||
@app.route('/orders/<int:oid>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def order_edit(oid):
|
||||
user = current_user()
|
||||
order = get_order(oid, mutation=request.method == 'POST')
|
||||
if not can_edit(order, user):
|
||||
if request.method == 'POST':
|
||||
abort(403)
|
||||
return redirect(url_for('order_view', oid=oid))
|
||||
if request.method == 'POST':
|
||||
save_form(order, request.form)
|
||||
if request.form.get('intent') == 'submit':
|
||||
submit(order, user, request.form.get('comment'))
|
||||
message = '已保存并提交审批'
|
||||
else:
|
||||
add_log(order, '保存草稿', user)
|
||||
message = '已保存'
|
||||
db.commit()
|
||||
flash(message, 'ok')
|
||||
return redirect(url_for('order_view' if order.status == 'pending_approval' else 'order_edit', oid=oid))
|
||||
return render_template('order_edit.html', user=user, o=order, editable=True,
|
||||
suppliers=db.query(Supplier).filter_by(active=True).all(),
|
||||
materials=db.query(Material).filter_by(active=True).all(), doc_types=DOC_TYPES,
|
||||
total_cn=amount_to_cn(order.total_amount))
|
||||
|
||||
@app.route('/orders/<int:oid>/view')
|
||||
@login_required
|
||||
def order_view(oid):
|
||||
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),
|
||||
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)
|
||||
|
||||
@app.route('/orders/<int:oid>/submit', methods=['POST'])
|
||||
@login_required
|
||||
def order_submit(oid):
|
||||
user, order = current_user(), get_order(oid, mutation=True)
|
||||
if not can_edit(order, user):
|
||||
abort(403)
|
||||
submit(order, user, request.form.get('comment'))
|
||||
db.commit()
|
||||
flash('已提交审批', 'ok')
|
||||
return redirect(url_for('order_view', oid=oid))
|
||||
|
||||
@app.route('/orders/<int:oid>/review', methods=['POST'])
|
||||
@login_required
|
||||
def order_review(oid):
|
||||
# Never turn a stale first-stage confirmation into a final approval.
|
||||
get_order(oid)
|
||||
abort(409, description='审核和批准已合并,请重新打开订单,核对后进行审批')
|
||||
|
||||
@app.route('/orders/<int:oid>/approve', methods=['POST'])
|
||||
@login_required
|
||||
def order_approve(oid):
|
||||
user, order = current_user(), get_order(oid, mutation=True)
|
||||
if not can_approve(order, user):
|
||||
abort(403)
|
||||
if request.form.get('stamped') != '1':
|
||||
raise ValueError('请先加盖采购专用章,再确认审批')
|
||||
validate_order(order)
|
||||
old = order.status
|
||||
if order.revision == 0:
|
||||
order.revision = 1
|
||||
db.add(OrderVersion(order_id=oid, revision=1, content_json=snapshot(order), template_version='legacy-current'))
|
||||
order.status = 'approved'
|
||||
order.approved_by, order.approved_by_id, order.approved_at = user.name, user.id, datetime.now()
|
||||
db.flush()
|
||||
version = db.query(OrderVersion).filter_by(order_id=oid, revision=order.revision).one()
|
||||
version.pdf_data = pdf_bytes(order)
|
||||
version.pdf_sha256 = hashlib.sha256(version.pdf_data).hexdigest()
|
||||
version.template_version = 'po-v3-legacy-current' if version.template_version == 'legacy-current' else 'po-v3'
|
||||
add_log(order, '审批通过并归档正式 PDF', user, request.form.get('comment'), old)
|
||||
db.commit()
|
||||
flash('审批通过,正式 PDF 已归档', 'ok')
|
||||
return redirect(url_for('order_view', oid=oid))
|
||||
|
||||
@app.route('/orders/<int:oid>/reject', methods=['POST'])
|
||||
@login_required
|
||||
def order_reject(oid):
|
||||
user, order = current_user(), get_order(oid, mutation=True)
|
||||
if not can_reject(order, user):
|
||||
abort(403)
|
||||
reason = request.form.get('comment', '').strip()
|
||||
if not reason:
|
||||
raise ValueError('请填写驳回原因')
|
||||
old = order.status
|
||||
order.status, order.reject_reason = 'rejected', reason
|
||||
clear_approval(order)
|
||||
add_log(order, '审批驳回', user, reason, old)
|
||||
db.commit()
|
||||
return redirect(url_for('order_view', oid=oid))
|
||||
|
||||
@app.route('/orders/<int:oid>/withdraw', methods=['POST'])
|
||||
@login_required
|
||||
def order_withdraw(oid):
|
||||
user, order = current_user(), get_order(oid, mutation=True)
|
||||
if order.status != 'pending_approval' or order.created_by != user.id:
|
||||
abort(403)
|
||||
reason = request.form.get('comment', '').strip()
|
||||
if not reason:
|
||||
raise ValueError('请填写撤回原因')
|
||||
old = order.status
|
||||
order.status = 'draft'
|
||||
clear_approval(order)
|
||||
add_log(order, '制单人撤回', user, reason, old)
|
||||
db.commit()
|
||||
return redirect(url_for('order_edit', oid=oid))
|
||||
|
||||
@app.route('/orders/<int:oid>/void', methods=['POST'])
|
||||
@login_required
|
||||
def order_void(oid):
|
||||
user, order = current_user(), get_order(oid, mutation=True)
|
||||
allowed = can_edit(order, user) or (order.status == 'approved' and user.role in ('admin', 'approver'))
|
||||
if not allowed:
|
||||
abort(403)
|
||||
reason = request.form.get('comment', '').strip()
|
||||
if not reason:
|
||||
raise ValueError('请填写作废原因')
|
||||
old = order.status
|
||||
order.status = 'voided'
|
||||
add_log(order, '作废订单', user, reason, old)
|
||||
db.commit()
|
||||
return redirect(url_for('order_view', oid=oid))
|
||||
|
||||
@app.route('/orders/<int:oid>/revise', methods=['POST'])
|
||||
@login_required
|
||||
def order_revise(oid):
|
||||
user, order = current_user(), get_order(oid, mutation=True)
|
||||
if order.status != 'approved' or user.role not in ('maker', 'admin') or (user.role != 'admin' and order.created_by != user.id):
|
||||
abort(403)
|
||||
reason = request.form.get('comment', '').strip()
|
||||
if not reason:
|
||||
raise ValueError('请填写变更原因')
|
||||
if order.revision == 0:
|
||||
# Preserve available legacy content without pretending it is the original submitted version.
|
||||
order.revision = 1
|
||||
data = pdf_bytes(order)
|
||||
db.add(OrderVersion(order_id=oid, revision=1, content_json=snapshot(order), pdf_data=data,
|
||||
pdf_sha256=hashlib.sha256(data).hexdigest(), template_version='legacy-current'))
|
||||
order.status = 'draft'
|
||||
clear_approval(order)
|
||||
add_log(order, '发起变更,原批准停止使用', user, reason, 'approved')
|
||||
db.commit()
|
||||
flash('已进入变更草稿,须重新审批;原审批文件请停止使用', 'ok')
|
||||
return redirect(url_for('order_edit', oid=oid))
|
||||
|
||||
@app.route('/orders/<int:oid>/versions/<int:revision>')
|
||||
@login_required
|
||||
def order_version(oid, revision):
|
||||
order = get_order(oid)
|
||||
version = db.query(OrderVersion).filter_by(order_id=oid, revision=revision).first()
|
||||
if not version:
|
||||
abort(404)
|
||||
return render_template('order_version.html', user=current_user(), o=order, version=version,
|
||||
content=json.loads(version.content_json))
|
||||
|
||||
@app.route('/orders/<int:oid>/copy', methods=['POST'])
|
||||
@login_required
|
||||
@role_required('maker')
|
||||
def order_copy(oid):
|
||||
user, source = current_user(), get_order(oid)
|
||||
key, existing = creation_request('copy:%s' % oid)
|
||||
if existing:
|
||||
return redirect(url_for('order_edit', oid=existing.order_id))
|
||||
if request.form.get('row_version') != str(source.row_version):
|
||||
abort(409, description='源订单已更新,请刷新后重试')
|
||||
order = Order(order_no=next_order_no(source.doc_type), doc_type=source.doc_type,
|
||||
status='draft', created_by=user.id, created_by_name=user.name)
|
||||
for field in ORDER_FIELDS:
|
||||
setattr(order, field, getattr(source, field))
|
||||
order.order_date = datetime.now().strftime('%Y-%m-%d')
|
||||
order.items = [OrderItem(seq=i, **{field: getattr(item, field) for field in ITEM_FIELDS})
|
||||
for i, item in enumerate(source.items, 1)]
|
||||
recalc(order)
|
||||
db.add(order)
|
||||
remember_creation(key, 'copy:%s' % oid, order)
|
||||
add_log(order, '复制创建草稿', user, '来源:' + source.order_no)
|
||||
db.commit()
|
||||
return redirect(url_for('order_edit', oid=order.id))
|
||||
|
||||
@app.route('/orders/<int:oid>/pdf')
|
||||
@login_required
|
||||
def order_pdf(oid):
|
||||
order = get_order(oid)
|
||||
version = db.query(OrderVersion).filter_by(order_id=oid, revision=order.revision).first()
|
||||
if order.status == 'approved' and version and version.pdf_data:
|
||||
data = version.pdf_data
|
||||
if hashlib.sha256(data).hexdigest() != version.pdf_sha256:
|
||||
abort(500, description='归档文件校验失败,请联系管理员')
|
||||
else:
|
||||
data = pdf_bytes(order)
|
||||
response = send_file(io.BytesIO(data), mimetype='application/pdf', as_attachment=request.args.get('dl') == '1',
|
||||
download_name='%s %s.pdf' % (order.doc_type_text, order.order_no))
|
||||
response.headers['Cache-Control'] = 'private, no-store'
|
||||
return response
|
||||
|
||||
@app.route('/orders/<int:oid>/import_excel', methods=['POST'])
|
||||
@login_required
|
||||
def import_excel(oid):
|
||||
user, order = current_user(), get_order(oid, mutation=True)
|
||||
if not can_edit(order, user):
|
||||
abort(403)
|
||||
path = save_upload(request.files.get('file'))
|
||||
try:
|
||||
rows = read_rows(path)
|
||||
except Exception as exc:
|
||||
raise ValueError('无法读取 Excel,请检查文件格式及表头') from exc
|
||||
items = []
|
||||
for row in rows:
|
||||
if not any(x not in ('', None) for x in row):
|
||||
continue
|
||||
values = (list(row) + [''] * 7)[:7]
|
||||
if not (values[0] or values[1]):
|
||||
raise ValueError('导入明细缺少物料代码或规格')
|
||||
items.append(make_item(values, len(items) + 1))
|
||||
if not items:
|
||||
raise ValueError('文件中没有有效明细,原明细已保留')
|
||||
mode = request.form.get('mode', 'append')
|
||||
if mode not in ('append', 'replace'):
|
||||
raise ValueError('导入方式无效')
|
||||
combined = items if mode == 'replace' else list(order.items) + items
|
||||
if len(combined) > 2000:
|
||||
raise ValueError('单张订单最多 2000 行明细')
|
||||
for seq, item in enumerate(combined, 1):
|
||||
item.seq = seq
|
||||
order.items = combined
|
||||
recalc(order)
|
||||
order.row_version += 1
|
||||
add_log(order, '导入明细', user, '%s %s 行' % (mode, len(items)))
|
||||
db.commit()
|
||||
flash('已导入 %s 行明细' % len(items), 'ok')
|
||||
return redirect(url_for('order_edit', oid=oid))
|
||||
Reference in New Issue
Block a user