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)