Files
order_system/po_app/models.py
T

283 lines
11 KiB
Python

# -*- coding: utf-8 -*-
"""数据模型:用户、供应商、物料、订单、明细、审核记录。"""
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
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")
# 需方(我方)默认信息:新建单据时自动预置到联系人 / 电话 / 税号 / 税点
DEFAULT_OUR_CONTACT = u"胡乃琛"
DEFAULT_OUR_PHONE = u"13913172516"
DEFAULT_OUR_TAX_NO = u"91320507MA1WW8D15D"
DEFAULT_TAX_RATE = u"13%"
DOC_TYPES = {
"purchase": u"采购单",
"outsourcing": u"委外生产订单",
}
STATUS_TEXT = {
"draft": u"草稿",
"pending_approval": u"待审批",
"approved": u"审批通过",
"rejected": u"已驳回",
"voided": u"已作废",
}
ROLE_TEXT = {
"admin": u"系统管理员",
"maker": u"制单员",
"approver": u"审批员",
}
INVOICE_STATUS = {'unknown': '未登记', 'pending': '未收票', 'received': '已收票'}
DELIVERY_STATUS = {'unknown': '未登记', 'pending': '未交货', 'partial': '部分交货', 'delivered': '已交货'}
PAYMENT_STATUS = {'unknown': '未登记', 'unpaid': '未付款', 'partial': '部分付款', 'paid': '已付款'}
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(200), nullable=False)
name = Column(String(50), nullable=False)
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):
__tablename__ = "suppliers"
id = Column(Integer, primary_key=True)
code = Column(String(50))
name = Column(String(200), nullable=False)
short_name = Column(String(60), nullable=False, default='')
contact = Column(String(50))
phone = Column(String(50))
tax_no = Column(String(80))
bank_name = Column(String(120))
bank_account = Column(String(80))
payment_terms = Column(String(120))
active = Column(Boolean, default=True)
class Material(Base):
__tablename__ = "materials"
id = Column(Integer, primary_key=True)
code = Column(String(80), nullable=False)
spec = Column(String(300))
unit = Column(String(20))
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, unique=True)
doc_type = Column(String(20), nullable=False, default="purchase")
supplier_name = Column(String(200))
supplier_contact = Column(String(50))
supplier_phone = Column(String(50))
supplier_tax_no = Column(String(80))
supplier_bank_name = Column(String(120))
supplier_bank_account = Column(String(80))
payment_terms = Column(String(120))
our_company = Column(String(200), default=COMPANY_NAME)
our_contact = Column(String(50), default=DEFAULT_OUR_CONTACT)
our_phone = Column(String(50), default=DEFAULT_OUR_PHONE)
our_tax_no = Column(String(80), default=DEFAULT_OUR_TAX_NO)
tax_rate = Column(String(30), default=DEFAULT_TAX_RATE)
order_date = Column(String(20))
delivery_place = Column(String(300), default=DEFAULT_DELIVERY_PLACE)
note = Column(Text)
total_amount = Column('total_cents', FixedDecimal(), default=0)
status = Column(String(20), default="draft")
created_by = Column(Integer, ForeignKey("users.id"))
created_by_name = Column(String(50))
created_at = Column(DateTime, default=datetime.now)
submitted_at = Column(DateTime)
reviewed_by = Column(String(50))
reviewed_at = Column(DateTime)
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)
needs_prepayment = Column(Boolean, nullable=False, default=False)
invoice_status = Column(String(20), nullable=False, default='pending')
delivery_status = Column(String(20), nullable=False, default='pending')
payment_status = Column(String(20), nullable=False, default='unpaid')
payment_date = Column(String(10), nullable=False, default='')
fulfillment_note = Column(Text, nullable=False, default='')
payment_note = Column(Text, nullable=False, default='')
fulfillment_revision = Column(Integer, nullable=False, default=-1)
payment_revision = 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):
return STATUS_TEXT.get(self.status, self.status)
@property
def doc_type_text(self):
return DOC_TYPES.get(self.doc_type, self.doc_type)
@property
def invoice_status_text(self):
return INVOICE_STATUS.get(self.invoice_status, '未登记')
@property
def delivery_status_text(self):
return DELIVERY_STATUS.get(self.delivery_status, '未登记')
@property
def payment_status_text(self):
return PAYMENT_STATUS.get(self.payment_status, '未登记')
@property
def fulfillment_stale(self):
return self.fulfillment_revision >= 0 and self.fulfillment_revision != self.revision
@property
def payment_stale(self):
return self.payment_revision >= 0 and self.payment_revision != self.revision
class OrderItem(Base):
__tablename__ = "order_items"
id = Column(Integer, primary_key=True)
order_id = Column(Integer, ForeignKey("orders.id"))
seq = Column(Integer, default=1)
material_code = Column(String(80))
material_spec = Column(String(300))
unit = Column(String(20))
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))
class ApprovalLog(Base):
__tablename__ = "approval_logs"
id = Column(Integer, primary_key=True)
order_id = Column(Integer, ForeignKey("orders.id"))
action = Column(String(50))
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):
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=5')
return engine, sessionmaker(bind=engine)