增加部分付款和部分交货状态、工作台筛选、权限校验、跟进记录及 v4 数据迁移;补充 Windows EXE 构建与验收脚本。附 16 页 PDF 使用升级手册及企业微信详细配置说明、生成脚本和测试截图。验证:76 项回归测试、EXE 浏览器验收与 PDF 逐页校验通过。
108 lines
5.6 KiB
Python
108 lines
5.6 KiB
Python
"""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 >= 4:
|
|
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)
|
|
if version < 3:
|
|
merge_approval(con, tables)
|
|
add_tracking(con)
|
|
con.execute('PRAGMA user_version=4')
|
|
con.commit()
|
|
except Exception:
|
|
con.rollback()
|
|
raise
|
|
|
|
|
|
def add_tracking(con):
|
|
# Historical invoices, payments and deliveries are unknown until recorded.
|
|
additions = {
|
|
'needs_prepayment': 'BOOLEAN NOT NULL DEFAULT 0',
|
|
'invoice_status': "VARCHAR(20) NOT NULL DEFAULT 'unknown'",
|
|
'delivery_status': "VARCHAR(20) NOT NULL DEFAULT 'unknown'",
|
|
'payment_status': "VARCHAR(20) NOT NULL DEFAULT 'unknown'",
|
|
'payment_date': "VARCHAR(10) NOT NULL DEFAULT ''",
|
|
'fulfillment_note': "TEXT NOT NULL DEFAULT ''",
|
|
'payment_note': "TEXT NOT NULL DEFAULT ''",
|
|
'fulfillment_revision': 'INTEGER NOT NULL DEFAULT -1',
|
|
'payment_revision': 'INTEGER NOT NULL DEFAULT -1',
|
|
}
|
|
existing = {row[1] for row in con.execute('PRAGMA table_info(orders)')}
|
|
for name, definition in additions.items():
|
|
if name not in existing:
|
|
con.execute('ALTER TABLE orders ADD COLUMN %s %s' % (name, definition))
|
|
|
|
|
|
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'""")
|