完善采购审批流程、企业微信通知和采购专用章
This commit is contained in:
+108
-14
@@ -1,13 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据模型:用户、供应商、物料、订单、明细、审核记录。"""
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer,
|
||||
String, Text, create_engine)
|
||||
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer,
|
||||
String, Text, LargeBinary, UniqueConstraint, create_engine, event)
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class FixedDecimal(TypeDecorator):
|
||||
"""SQLite stores integers; Python always receives Decimal, including SUM."""
|
||||
impl = Integer
|
||||
cache_ok = True
|
||||
|
||||
def __init__(self, places=2):
|
||||
self.places = places
|
||||
self.factor = 10 ** places
|
||||
super().__init__()
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
return int((Decimal(str(value)) * self.factor).quantize(Decimal('1'), rounding=ROUND_HALF_UP))
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
return Decimal(value) / self.factor if value is not None else None
|
||||
|
||||
COMPANY_NAME = u"苏州博特维智能科技有限公司"
|
||||
DEFAULT_DELIVERY_PLACE = (u"苏州市工业园区星汉街5号腾飞新苏工业坊B幢5楼05-06单元 "
|
||||
u"胡乃琛 13913172516")
|
||||
@@ -24,17 +46,16 @@ DOC_TYPES = {
|
||||
|
||||
STATUS_TEXT = {
|
||||
"draft": u"草稿",
|
||||
"pending_review": u"待主管审核",
|
||||
"pending_approval": u"待经理批准",
|
||||
"approved": u"已批准",
|
||||
"pending_approval": u"待审批",
|
||||
"approved": u"审批通过",
|
||||
"rejected": u"已驳回",
|
||||
"voided": u"已作废",
|
||||
}
|
||||
|
||||
ROLE_TEXT = {
|
||||
"admin": u"系统管理员",
|
||||
"maker": u"制单员",
|
||||
"reviewer": u"主管审核",
|
||||
"approver": u"经理批准",
|
||||
"approver": u"审批员",
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +68,8 @@ class User(Base):
|
||||
role = Column(String(20), nullable=False, default="maker")
|
||||
active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
session_version = Column(Integer, nullable=False, default=1)
|
||||
must_change_password = Column(Boolean, nullable=False, default=True)
|
||||
|
||||
|
||||
class Supplier(Base):
|
||||
@@ -69,14 +92,14 @@ class Material(Base):
|
||||
code = Column(String(80), nullable=False)
|
||||
spec = Column(String(300))
|
||||
unit = Column(String(20))
|
||||
price = Column(Float, default=0)
|
||||
price = Column('price_units', FixedDecimal(4), default=0)
|
||||
active = Column(Boolean, default=True)
|
||||
|
||||
|
||||
class Order(Base):
|
||||
__tablename__ = "orders"
|
||||
id = Column(Integer, primary_key=True)
|
||||
order_no = Column(String(50), nullable=False)
|
||||
order_no = Column(String(50), nullable=False, unique=True)
|
||||
doc_type = Column(String(20), nullable=False, default="purchase")
|
||||
|
||||
supplier_name = Column(String(200))
|
||||
@@ -96,7 +119,7 @@ class Order(Base):
|
||||
delivery_place = Column(String(300), default=DEFAULT_DELIVERY_PLACE)
|
||||
note = Column(Text)
|
||||
|
||||
total_amount = Column(Float, default=0)
|
||||
total_amount = Column('total_cents', FixedDecimal(), default=0)
|
||||
|
||||
status = Column(String(20), default="draft")
|
||||
created_by = Column(Integer, ForeignKey("users.id"))
|
||||
@@ -108,11 +131,17 @@ class Order(Base):
|
||||
approved_by = Column(String(50))
|
||||
approved_at = Column(DateTime)
|
||||
reject_reason = Column(Text)
|
||||
reviewed_by_id = Column(Integer, ForeignKey('users.id'))
|
||||
approved_by_id = Column(Integer, ForeignKey('users.id'))
|
||||
revision = Column(Integer, nullable=False, default=0)
|
||||
row_version = Column(Integer, nullable=False, default=1)
|
||||
__mapper_args__ = {'version_id_col': row_version}
|
||||
|
||||
items = relationship("OrderItem", backref="order", cascade="all, delete-orphan",
|
||||
order_by="OrderItem.seq")
|
||||
logs = relationship("ApprovalLog", backref="order", cascade="all, delete-orphan",
|
||||
order_by="ApprovalLog.id")
|
||||
versions = relationship('OrderVersion', backref='order', order_by='OrderVersion.revision')
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
@@ -131,9 +160,9 @@ class OrderItem(Base):
|
||||
material_code = Column(String(80))
|
||||
material_spec = Column(String(300))
|
||||
unit = Column(String(20))
|
||||
qty = Column(Float, default=0)
|
||||
price = Column(Float, default=0)
|
||||
amount = Column(Float, default=0)
|
||||
qty = Column('qty_units', FixedDecimal(4), default=0)
|
||||
price = Column('price_units', FixedDecimal(4), default=0)
|
||||
amount = Column('amount_cents', FixedDecimal(), default=0)
|
||||
delivery_date = Column(String(30))
|
||||
remark = Column(String(200))
|
||||
|
||||
@@ -146,9 +175,74 @@ class ApprovalLog(Base):
|
||||
user_name = Column(String(50))
|
||||
comment = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
revision = Column(Integer, nullable=False, default=0)
|
||||
from_status = Column(String(20))
|
||||
to_status = Column(String(20))
|
||||
|
||||
|
||||
class OrderVersion(Base):
|
||||
__tablename__ = 'order_versions'
|
||||
__table_args__ = (UniqueConstraint('order_id', 'revision'),)
|
||||
id = Column(Integer, primary_key=True)
|
||||
order_id = Column(Integer, ForeignKey('orders.id'), nullable=False)
|
||||
revision = Column(Integer, nullable=False)
|
||||
content_json = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
pdf_data = Column(LargeBinary)
|
||||
pdf_sha256 = Column(String(64))
|
||||
template_version = Column(String(30))
|
||||
|
||||
|
||||
class CreationRequest(Base):
|
||||
__tablename__ = 'creation_requests'
|
||||
key = Column(String(64), primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
operation = Column(String(80), nullable=False)
|
||||
order_id = Column(Integer, ForeignKey('orders.id'), nullable=False)
|
||||
|
||||
|
||||
class NotificationSettings(Base):
|
||||
__tablename__ = 'notification_settings'
|
||||
id = Column(Integer, primary_key=True)
|
||||
enabled = Column(Boolean, nullable=False, default=False)
|
||||
base_url = Column(String(300), nullable=False, default='')
|
||||
review_webhook = Column(Text, nullable=False, default='')
|
||||
approval_webhook = Column(Text, nullable=False, default='')
|
||||
result_webhook = Column(Text, nullable=False, default='')
|
||||
next_dispatch_at = Column(DateTime)
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
__tablename__ = 'notifications'
|
||||
id = Column(Integer, primary_key=True)
|
||||
log_id = Column(Integer, ForeignKey('approval_logs.id'), unique=True)
|
||||
order_id = Column(Integer, ForeignKey('orders.id'))
|
||||
revision = Column(Integer, nullable=False, default=0)
|
||||
channel = Column(String(20), nullable=False)
|
||||
event = Column(String(50), nullable=False)
|
||||
expected_status = Column(String(20))
|
||||
status = Column(String(20), nullable=False, default='pending', index=True)
|
||||
attempts = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
next_attempt_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
sent_at = Column(DateTime)
|
||||
lease_until = Column(DateTime)
|
||||
lease_token = Column(String(32))
|
||||
last_error = Column(String(200), nullable=False, default='')
|
||||
|
||||
|
||||
def init_db(db_path):
|
||||
engine = create_engine("sqlite:///" + db_path, echo=False)
|
||||
from migrations import upgrade_database
|
||||
existed = Path(db_path).exists()
|
||||
upgrade_database(db_path)
|
||||
engine = create_engine("sqlite:///" + db_path, echo=False, hide_parameters=True, connect_args={'timeout': 30})
|
||||
@event.listens_for(engine, 'connect')
|
||||
def sqlite_settings(connection, _):
|
||||
connection.execute('PRAGMA foreign_keys=ON')
|
||||
connection.execute('PRAGMA busy_timeout=30000')
|
||||
Base.metadata.create_all(engine)
|
||||
if not existed:
|
||||
with engine.connect() as con:
|
||||
con.exec_driver_sql('PRAGMA user_version=3')
|
||||
return engine, sessionmaker(bind=engine)
|
||||
|
||||
Reference in New Issue
Block a user