完善采购审批流程、企业微信通知和采购专用章

This commit is contained in:
reddaniel110
2026-09-16 11:45:21 +08:00
parent 4876d8805d
commit bf952196fa
58 changed files with 3451 additions and 1909 deletions
+108 -14
View File
@@ -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)