完善采购审批流程、企业微信通知和采购专用章
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from flask import abort, request
|
||||
from models import ApprovalLog, CreationRequest, DOC_TYPES, Order, OrderItem, OrderVersion
|
||||
from money import MAX_TOTAL, ZERO, line_amount, money, number
|
||||
from security import current_user, db
|
||||
|
||||
ORDER_FIELDS = ('supplier_name', 'supplier_contact', 'supplier_phone', 'supplier_tax_no',
|
||||
'supplier_bank_name', 'supplier_bank_account', 'payment_terms', 'our_company',
|
||||
'our_contact', 'our_phone', 'our_tax_no', 'tax_rate', 'order_date', 'delivery_place', 'note')
|
||||
ITEM_FIELDS = ('material_code', 'material_spec', 'unit', 'qty', 'price', 'amount', 'delivery_date', 'remark')
|
||||
|
||||
|
||||
def scope(query, user=None):
|
||||
user = user or current_user()
|
||||
if user.role == 'maker':
|
||||
return query.filter(Order.created_by == user.id)
|
||||
if user.role not in ('admin', 'approver'):
|
||||
abort(403)
|
||||
return query
|
||||
|
||||
|
||||
def get_order(oid, mutation=False):
|
||||
order = scope(db.query(Order)).filter(Order.id == oid).first()
|
||||
if order is None:
|
||||
abort(404)
|
||||
if mutation and request.form.get('row_version') != str(order.row_version):
|
||||
abort(409, description='订单已经更新或页面已过期,请刷新后核对再操作')
|
||||
return order
|
||||
|
||||
|
||||
def can_edit(order, user):
|
||||
return bool(user and user.role in ('maker', 'admin') and order.status in ('draft', 'rejected')
|
||||
and (user.role == 'admin' or order.created_by == user.id))
|
||||
|
||||
|
||||
def can_approve(order, user):
|
||||
return (user.role in ('approver', 'admin') and order.status == 'pending_approval'
|
||||
and order.created_by != user.id)
|
||||
|
||||
|
||||
def can_reject(order, user):
|
||||
return can_approve(order, user)
|
||||
|
||||
|
||||
def next_order_no(doc_type):
|
||||
if doc_type not in DOC_TYPES:
|
||||
raise ValueError('单据类型无效')
|
||||
head = ('PO' if doc_type == 'purchase' else 'WO') + datetime.now().strftime('%Y%m%d')
|
||||
seq = 0
|
||||
for (value,) in db.query(Order.order_no).filter(Order.order_no.like(head + '%')):
|
||||
tail = value[len(head):].lstrip('-')
|
||||
if tail.isdigit():
|
||||
seq = max(seq, int(tail))
|
||||
return '%s%04d' % (head, seq + 1)
|
||||
|
||||
|
||||
def creation_request(operation):
|
||||
key = request.form.get('request_key', '')
|
||||
if not re.fullmatch(r'[a-f0-9]{32}', key):
|
||||
raise ValueError('新建页面已过期,请重新打开')
|
||||
existing = db.get(CreationRequest, key)
|
||||
if existing and (existing.user_id != current_user().id or existing.operation != operation):
|
||||
abort(409)
|
||||
return key, existing
|
||||
|
||||
|
||||
def remember_creation(key, operation, order):
|
||||
db.flush()
|
||||
db.add(CreationRequest(key=key, user_id=current_user().id, operation=operation, order_id=order.id))
|
||||
|
||||
|
||||
def normalize_date(value, label='日期', required=False):
|
||||
if value in (None, ''):
|
||||
if required:
|
||||
raise ValueError('请填写%s' % label)
|
||||
return ''
|
||||
if isinstance(value, (datetime, date)):
|
||||
return value.strftime('%Y-%m-%d')
|
||||
value = str(value).strip()
|
||||
try:
|
||||
return datetime.strptime(value, '%Y-%m-%d').strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
raise ValueError('%s应使用 YYYY-MM-DD 格式' % label)
|
||||
|
||||
|
||||
def recalc(order):
|
||||
total = ZERO
|
||||
for item in order.items:
|
||||
item.amount = line_amount(item.qty, item.price)
|
||||
total += item.amount
|
||||
if total > MAX_TOTAL:
|
||||
raise ValueError('订单合计金额过大')
|
||||
order.total_amount = money(total)
|
||||
|
||||
|
||||
def make_item(values, seq):
|
||||
code, spec, unit, qty, price, delivery, remark = values
|
||||
limits = [(code, 80, '物料代码'), (spec, 300, '物料规格'), (unit, 20, '单位'), (remark, 200, '备注')]
|
||||
for value, limit, label in limits:
|
||||
if len(str(value or '')) > limit:
|
||||
raise ValueError('第 %s 行%s超过 %s 字' % (seq, label, limit))
|
||||
return OrderItem(seq=seq, material_code=str(code or '').strip(), material_spec=str(spec or '').strip(),
|
||||
unit=str(unit or '').strip(), qty=number(qty, '第 %s 行数量' % seq),
|
||||
price=number(price, '第 %s 行单价' % seq), delivery_date=normalize_date(delivery, '第 %s 行交期' % seq),
|
||||
remark=str(remark or '').strip())
|
||||
|
||||
|
||||
def save_form(order, form):
|
||||
for field in ORDER_FIELDS:
|
||||
value = form.get(field, '').strip()
|
||||
limit = getattr(Order, field).property.columns[0].type.length
|
||||
if len(value) > (limit or 10000):
|
||||
raise ValueError('单据字段内容过长:%s' % field)
|
||||
setattr(order, field, normalize_date(value, '采购日期') if field == 'order_date' else value)
|
||||
columns = [form.getlist('item_' + key) for key in ('code', 'spec', 'unit', 'qty', 'price', 'date', 'remark')]
|
||||
count = max(map(len, columns), default=0)
|
||||
if count > 2000:
|
||||
raise ValueError('单张订单最多 2000 行明细')
|
||||
items = []
|
||||
for i in range(count):
|
||||
values = [col[i] if i < len(col) else '' for col in columns]
|
||||
if not any(str(x or '').strip() for x in values[:2]):
|
||||
if any(str(x or '').strip() not in ('', '0', '0.0', '0.0000') for x in values[2:]):
|
||||
raise ValueError('第 %s 行请填写物料代码或规格' % (i + 1))
|
||||
continue
|
||||
items.append(make_item(values, len(items) + 1))
|
||||
order.items = items
|
||||
recalc(order)
|
||||
# Item-only changes must invalidate stale approval/edit forms as well.
|
||||
order.row_version += 1
|
||||
|
||||
|
||||
def validate_order(order):
|
||||
for field, label in [('supplier_name', '供应商'), ('our_company', '需方名称'),
|
||||
('payment_terms', '付款方式'), ('delivery_place', '交货地点')]:
|
||||
if not (getattr(order, field) or '').strip():
|
||||
raise ValueError('提交前请填写%s' % label)
|
||||
normalize_date(order.order_date, '采购日期', required=True)
|
||||
if not order.items:
|
||||
raise ValueError('明细为空,不能提交')
|
||||
for i, item in enumerate(order.items, 1):
|
||||
if not (item.material_code or item.material_spec) or not item.unit:
|
||||
raise ValueError('第 %s 行请填写物料及单位' % i)
|
||||
number(item.qty, '第 %s 行数量' % i, allow_zero=False)
|
||||
number(item.price, '第 %s 行单价' % i, allow_zero=False)
|
||||
normalize_date(item.delivery_date, '第 %s 行交期' % i, required=True)
|
||||
recalc(order)
|
||||
|
||||
|
||||
def clear_approval(order):
|
||||
order.reviewed_by = order.reviewed_at = order.reviewed_by_id = None
|
||||
order.approved_by = order.approved_at = order.approved_by_id = None
|
||||
|
||||
|
||||
def snapshot(order):
|
||||
content = {field: getattr(order, field) for field in ORDER_FIELDS}
|
||||
content.update(order_no=order.order_no, doc_type=order.doc_type, total_amount=str(order.total_amount),
|
||||
created_by=order.created_by, created_by_name=order.created_by_name,
|
||||
items=[{field: str(getattr(item, field)) if isinstance(getattr(item, field), Decimal)
|
||||
else getattr(item, field) for field in ITEM_FIELDS} for item in order.items])
|
||||
return json.dumps(content, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
|
||||
def add_log(order, action, user, comment=None, from_status=None):
|
||||
db.flush()
|
||||
log = ApprovalLog(order_id=order.id, action=action, user_id=user.id, user_name=user.name,
|
||||
revision=order.revision, comment=(comment or '')[:2000],
|
||||
from_status=from_status, to_status=order.status)
|
||||
db.add(log)
|
||||
if from_status and from_status != order.status:
|
||||
from notifications import enqueue_transition
|
||||
db.flush()
|
||||
enqueue_transition(db, order, log)
|
||||
|
||||
|
||||
def submit(order, user, comment):
|
||||
validate_order(order)
|
||||
old = order.status
|
||||
clear_approval(order)
|
||||
order.revision += 1
|
||||
order.status = 'pending_approval'
|
||||
order.submitted_at = datetime.now()
|
||||
order.reject_reason = None
|
||||
db.add(OrderVersion(order_id=order.id, revision=order.revision, content_json=snapshot(order)))
|
||||
add_log(order, '提交审批', user, comment, old)
|
||||
Reference in New Issue
Block a user