190 lines
7.8 KiB
Python
190 lines
7.8 KiB
Python
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)
|