Files

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)