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

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
+86
View File
@@ -0,0 +1,86 @@
"""Additive legacy SQLite migration. Never rewrites existing order numbers."""
import sqlite3
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
def upgrade_database(path):
path = Path(path)
if not path.exists():
return
with sqlite3.connect(path) as con:
tables = {r[0] for r in con.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if 'orders' not in tables:
return
version = con.execute('PRAGMA user_version').fetchone()[0]
if version >= 3:
return
duplicates = con.execute('SELECT order_no, COUNT(*) FROM orders GROUP BY order_no HAVING COUNT(*) > 1').fetchall()
if duplicates:
raise RuntimeError('升级前请处理重复单号(原数据未修改):%s' % duplicates)
backup = path.with_name(path.name + '.pre-upgrade-' + datetime.now().strftime('%Y%m%d-%H%M%S-%f') + '.bak')
with sqlite3.connect(backup) as dest:
con.backup(dest)
con.execute('BEGIN IMMEDIATE')
try:
if version < 2:
upgrade_fixed_amounts(con)
merge_approval(con, tables)
con.execute('PRAGMA user_version=3')
con.commit()
except Exception:
con.rollback()
raise
def upgrade_fixed_amounts(con):
additions = {
'users': {'session_version': 'INTEGER NOT NULL DEFAULT 1', 'must_change_password': 'BOOLEAN NOT NULL DEFAULT 1'},
'orders': {'reviewed_by_id': 'INTEGER', 'approved_by_id': 'INTEGER', 'revision': 'INTEGER NOT NULL DEFAULT 0', 'row_version': 'INTEGER NOT NULL DEFAULT 1'},
'approval_logs': {'user_id': 'INTEGER', 'revision': 'INTEGER NOT NULL DEFAULT 0', 'from_status': 'VARCHAR(20)', 'to_status': 'VARCHAR(20)'},
}
for table, columns in additions.items():
existing = {r[1] for r in con.execute('PRAGMA table_info(%s)' % table)}
for name, definition in columns.items():
if name not in existing:
con.execute('ALTER TABLE %s ADD COLUMN %s %s' % (table, name, definition))
for table, old, new, places in [
('materials', 'price', 'price_units', 4), ('orders', 'total_amount', 'total_cents', 2),
('order_items', 'qty', 'qty_units', 4), ('order_items', 'price', 'price_units', 4),
('order_items', 'amount', 'amount_cents', 2),
]:
columns = {r[1] for r in con.execute('PRAGMA table_info(%s)' % table)}
if new in columns:
continue
con.execute('ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT 0' % (table, new))
for row_id, value in con.execute('SELECT id, %s FROM %s' % (old, table)).fetchall():
val = Decimal(str(value or 0))
if not val.is_finite() or abs(val) * 10 ** places > 2 ** 63 - 1:
raise RuntimeError('无法迁移 %s.id=%s 的金额/数量,请检查原始数据' % (table, row_id))
scaled = int((val * 10 ** places).quantize(Decimal('1'), rounding=ROUND_HALF_UP))
con.execute('UPDATE %s SET %s=? WHERE id=?' % (table, new), (scaled, row_id))
con.execute('CREATE UNIQUE INDEX IF NOT EXISTS uq_orders_order_no ON orders(order_no)')
def merge_approval(con, tables):
# Check queued tasks against the OLD status before consolidating stages.
# Completed deliveries/logs and archived PDF bytes must remain untouched.
if 'notifications' in tables:
con.execute("""UPDATE notifications SET status='cancelled',
last_error='审批流程已合并,旧通知已失效', lease_token=NULL, lease_until=NULL
WHERE channel IN ('review','approval') AND status IN ('pending','retry','sending','failed')
AND NOT EXISTS (SELECT 1 FROM orders o WHERE o.id=notifications.order_id
AND o.status=notifications.expected_status AND o.revision=notifications.revision
AND o.status IN ('pending_review','pending_approval'))""")
con.execute("""UPDATE notifications SET channel='approval', expected_status='pending_approval',
event='待审批(流程已合并)' WHERE channel IN ('review','approval')
AND status IN ('pending','retry','sending','failed')""")
if 'notification_settings' in tables:
con.execute("""UPDATE notification_settings SET approval_webhook=
CASE WHEN COALESCE(approval_webhook,'')='' THEN COALESCE(review_webhook,'')
ELSE approval_webhook END, review_webhook=''""")
con.execute("""UPDATE orders SET status='pending_approval', row_version=row_version+1
WHERE status IN ('pending_review','pending_approval')""")
con.execute("""UPDATE users SET role='approver', session_version=session_version+1
WHERE role='reviewer'""")