mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-21 20:09:57 -06:00
@@ -17,29 +17,58 @@ depends_on = None
|
||||
|
||||
def upgrade():
|
||||
"""Create quote_templates table"""
|
||||
op.create_table('quote_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('template_data', sa.Text(), nullable=True),
|
||||
sa.Column('default_tax_rate', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||
sa.Column('default_currency_code', sa.String(length=3), nullable=True),
|
||||
sa.Column('default_payment_terms', sa.String(length=100), nullable=True),
|
||||
sa.Column('default_terms', sa.Text(), nullable=True),
|
||||
sa.Column('default_valid_until_days', sa.Integer(), nullable=True),
|
||||
sa.Column('default_requires_approval', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('default_approval_level', sa.Integer(), nullable=True),
|
||||
sa.Column('default_items', sa.Text(), nullable=True),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('is_public', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('usage_count', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('ix_quote_templates_name', 'quote_templates', ['name'], unique=False)
|
||||
op.create_index('ix_quote_templates_created_by', 'quote_templates', ['created_by'], unique=False)
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
def _has_table(name: str) -> bool:
|
||||
try:
|
||||
return name in inspector.get_table_names()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _has_index(table_name: str, index_name: str) -> bool:
|
||||
try:
|
||||
return any((idx.get("name") or "") == index_name for idx in inspector.get_indexes(table_name))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not _has_table('quote_templates'):
|
||||
op.create_table('quote_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('template_data', sa.Text(), nullable=True),
|
||||
sa.Column('default_tax_rate', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||
sa.Column('default_currency_code', sa.String(length=3), nullable=True),
|
||||
sa.Column('default_payment_terms', sa.String(length=100), nullable=True),
|
||||
sa.Column('default_terms', sa.Text(), nullable=True),
|
||||
sa.Column('default_valid_until_days', sa.Integer(), nullable=True),
|
||||
sa.Column('default_requires_approval', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('default_approval_level', sa.Integer(), nullable=True),
|
||||
sa.Column('default_items', sa.Text(), nullable=True),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('is_public', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.Column('usage_count', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 057] ℹ Table quote_templates already exists, skipping creation")
|
||||
|
||||
# Ensure indexes exist (best-effort / idempotent)
|
||||
if _has_table('quote_templates'):
|
||||
if not _has_index('quote_templates', 'ix_quote_templates_name'):
|
||||
try:
|
||||
op.create_index('ix_quote_templates_name', 'quote_templates', ['name'], unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
if not _has_index('quote_templates', 'ix_quote_templates_created_by'):
|
||||
try:
|
||||
op.create_index('ix_quote_templates_created_by', 'quote_templates', ['created_by'], unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -23,147 +23,230 @@ depends_on = None
|
||||
|
||||
def upgrade():
|
||||
"""Add new feature tables"""
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
def _has_table(name: str) -> bool:
|
||||
try:
|
||||
return name in inspector.get_table_names()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _has_index(table_name: str, index_name: str) -> bool:
|
||||
try:
|
||||
return any((idx.get("name") or "") == index_name for idx in inspector.get_indexes(table_name))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Project Templates
|
||||
op.create_table('project_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('config', sa.JSON(), nullable=False, server_default='{}'),
|
||||
sa.Column('tasks', sa.JSON(), nullable=True, server_default='[]'),
|
||||
sa.Column('category', sa.String(100), nullable=True),
|
||||
sa.Column('tags', sa.JSON(), nullable=True, server_default='[]'),
|
||||
sa.Column('is_public', sa.Boolean(), nullable=False, server_default='0'),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('usage_count', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('last_used_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_project_templates_name'), 'project_templates', ['name'], unique=False)
|
||||
op.create_index(op.f('ix_project_templates_category'), 'project_templates', ['category'], unique=False)
|
||||
op.create_index(op.f('ix_project_templates_is_public'), 'project_templates', ['is_public'], unique=False)
|
||||
op.create_index(op.f('ix_project_templates_created_by'), 'project_templates', ['created_by'], unique=False)
|
||||
if not _has_table('project_templates'):
|
||||
op.create_table('project_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('config', sa.JSON(), nullable=False, server_default='{}'),
|
||||
sa.Column('tasks', sa.JSON(), nullable=True, server_default='[]'),
|
||||
sa.Column('category', sa.String(100), nullable=True),
|
||||
sa.Column('tags', sa.JSON(), nullable=True, server_default='[]'),
|
||||
sa.Column('is_public', sa.Boolean(), nullable=False, server_default='0'),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('usage_count', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('last_used_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 065] ℹ Table project_templates already exists, skipping creation")
|
||||
|
||||
for idx_name, cols in [
|
||||
(op.f('ix_project_templates_name'), ['name']),
|
||||
(op.f('ix_project_templates_category'), ['category']),
|
||||
(op.f('ix_project_templates_is_public'), ['is_public']),
|
||||
(op.f('ix_project_templates_created_by'), ['created_by']),
|
||||
]:
|
||||
if _has_table('project_templates') and not _has_index('project_templates', idx_name):
|
||||
try:
|
||||
op.create_index(idx_name, 'project_templates', cols, unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Invoice Approval Workflow
|
||||
op.create_table('invoice_approvals',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('invoice_id', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(20), nullable=False, server_default='pending'),
|
||||
sa.Column('stages', sa.JSON(), nullable=False, server_default='[]'),
|
||||
sa.Column('current_stage', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('total_stages', sa.Integer(), nullable=False, server_default='1'),
|
||||
sa.Column('requested_by', sa.Integer(), nullable=False),
|
||||
sa.Column('requested_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('approved_by', sa.Integer(), nullable=True),
|
||||
sa.Column('approved_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('rejected_by', sa.Integer(), nullable=True),
|
||||
sa.Column('rejected_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('rejection_reason', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ),
|
||||
sa.ForeignKeyConstraint(['requested_by'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['approved_by'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['rejected_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_invoice_approvals_invoice_id'), 'invoice_approvals', ['invoice_id'], unique=False)
|
||||
op.create_index(op.f('ix_invoice_approvals_status'), 'invoice_approvals', ['status'], unique=False)
|
||||
op.create_index(op.f('ix_invoice_approvals_requested_by'), 'invoice_approvals', ['requested_by'], unique=False)
|
||||
op.create_index(op.f('ix_invoice_approvals_approved_by'), 'invoice_approvals', ['approved_by'], unique=False)
|
||||
if not _has_table('invoice_approvals'):
|
||||
op.create_table('invoice_approvals',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('invoice_id', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(20), nullable=False, server_default='pending'),
|
||||
sa.Column('stages', sa.JSON(), nullable=False, server_default='[]'),
|
||||
sa.Column('current_stage', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('total_stages', sa.Integer(), nullable=False, server_default='1'),
|
||||
sa.Column('requested_by', sa.Integer(), nullable=False),
|
||||
sa.Column('requested_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('approved_by', sa.Integer(), nullable=True),
|
||||
sa.Column('approved_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('rejected_by', sa.Integer(), nullable=True),
|
||||
sa.Column('rejected_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('rejection_reason', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ),
|
||||
sa.ForeignKeyConstraint(['requested_by'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['approved_by'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['rejected_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 065] ℹ Table invoice_approvals already exists, skipping creation")
|
||||
|
||||
for idx_name, cols in [
|
||||
(op.f('ix_invoice_approvals_invoice_id'), ['invoice_id']),
|
||||
(op.f('ix_invoice_approvals_status'), ['status']),
|
||||
(op.f('ix_invoice_approvals_requested_by'), ['requested_by']),
|
||||
(op.f('ix_invoice_approvals_approved_by'), ['approved_by']),
|
||||
]:
|
||||
if _has_table('invoice_approvals') and not _has_index('invoice_approvals', idx_name):
|
||||
try:
|
||||
op.create_index(idx_name, 'invoice_approvals', cols, unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Payment Gateways
|
||||
op.create_table('payment_gateways',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(50), nullable=False),
|
||||
sa.Column('provider', sa.String(50), nullable=False),
|
||||
sa.Column('config', sa.Text(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('is_test_mode', sa.Boolean(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_index(op.f('ix_payment_gateways_name'), 'payment_gateways', ['name'], unique=True)
|
||||
op.create_index(op.f('ix_payment_gateways_is_active'), 'payment_gateways', ['is_active'], unique=False)
|
||||
if not _has_table('payment_gateways'):
|
||||
op.create_table('payment_gateways',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(50), nullable=False),
|
||||
sa.Column('provider', sa.String(50), nullable=False),
|
||||
sa.Column('config', sa.Text(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('is_test_mode', sa.Boolean(), nullable=False, server_default='0'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
else:
|
||||
print("[Migration 065] ℹ Table payment_gateways already exists, skipping creation")
|
||||
|
||||
idx_pg_name = op.f('ix_payment_gateways_name')
|
||||
idx_pg_active = op.f('ix_payment_gateways_is_active')
|
||||
if _has_table('payment_gateways') and not _has_index('payment_gateways', idx_pg_name):
|
||||
try:
|
||||
op.create_index(idx_pg_name, 'payment_gateways', ['name'], unique=True)
|
||||
except Exception:
|
||||
pass
|
||||
if _has_table('payment_gateways') and not _has_index('payment_gateways', idx_pg_active):
|
||||
try:
|
||||
op.create_index(idx_pg_active, 'payment_gateways', ['is_active'], unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Payment Transactions
|
||||
op.create_table('payment_transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('invoice_id', sa.Integer(), nullable=False),
|
||||
sa.Column('gateway_id', sa.Integer(), nullable=False),
|
||||
sa.Column('transaction_id', sa.String(200), nullable=False),
|
||||
sa.Column('amount', sa.Numeric(10, 2), nullable=False),
|
||||
sa.Column('currency', sa.String(3), nullable=False, server_default='EUR'),
|
||||
sa.Column('gateway_fee', sa.Numeric(10, 2), nullable=True),
|
||||
sa.Column('net_amount', sa.Numeric(10, 2), nullable=True),
|
||||
sa.Column('status', sa.String(20), nullable=False),
|
||||
sa.Column('payment_method', sa.String(50), nullable=True),
|
||||
sa.Column('gateway_response', sa.JSON(), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('error_code', sa.String(50), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('processed_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ),
|
||||
sa.ForeignKeyConstraint(['gateway_id'], ['payment_gateways.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('transaction_id')
|
||||
)
|
||||
op.create_index(op.f('ix_payment_transactions_invoice_id'), 'payment_transactions', ['invoice_id'], unique=False)
|
||||
op.create_index(op.f('ix_payment_transactions_gateway_id'), 'payment_transactions', ['gateway_id'], unique=False)
|
||||
op.create_index(op.f('ix_payment_transactions_transaction_id'), 'payment_transactions', ['transaction_id'], unique=True)
|
||||
op.create_index(op.f('ix_payment_transactions_status'), 'payment_transactions', ['status'], unique=False)
|
||||
if not _has_table('payment_transactions'):
|
||||
op.create_table('payment_transactions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('invoice_id', sa.Integer(), nullable=False),
|
||||
sa.Column('gateway_id', sa.Integer(), nullable=False),
|
||||
sa.Column('transaction_id', sa.String(200), nullable=False),
|
||||
sa.Column('amount', sa.Numeric(10, 2), nullable=False),
|
||||
sa.Column('currency', sa.String(3), nullable=False, server_default='EUR'),
|
||||
sa.Column('gateway_fee', sa.Numeric(10, 2), nullable=True),
|
||||
sa.Column('net_amount', sa.Numeric(10, 2), nullable=True),
|
||||
sa.Column('status', sa.String(20), nullable=False),
|
||||
sa.Column('payment_method', sa.String(50), nullable=True),
|
||||
sa.Column('gateway_response', sa.JSON(), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('error_code', sa.String(50), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('processed_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ),
|
||||
sa.ForeignKeyConstraint(['gateway_id'], ['payment_gateways.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('transaction_id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 065] ℹ Table payment_transactions already exists, skipping creation")
|
||||
|
||||
for idx_name, cols, unique in [
|
||||
(op.f('ix_payment_transactions_invoice_id'), ['invoice_id'], False),
|
||||
(op.f('ix_payment_transactions_gateway_id'), ['gateway_id'], False),
|
||||
(op.f('ix_payment_transactions_transaction_id'), ['transaction_id'], True),
|
||||
(op.f('ix_payment_transactions_status'), ['status'], False),
|
||||
]:
|
||||
if _has_table('payment_transactions') and not _has_index('payment_transactions', idx_name):
|
||||
try:
|
||||
op.create_index(idx_name, 'payment_transactions', cols, unique=unique)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Calendar Integrations
|
||||
op.create_table('calendar_integrations',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('provider', sa.String(50), nullable=False),
|
||||
sa.Column('access_token', sa.Text(), nullable=False),
|
||||
sa.Column('refresh_token', sa.Text(), nullable=True),
|
||||
sa.Column('token_expires_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('calendar_id', sa.String(200), nullable=True),
|
||||
sa.Column('calendar_name', sa.String(200), nullable=True),
|
||||
sa.Column('sync_settings', sa.JSON(), nullable=False, server_default='{}'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('last_sync_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('last_sync_status', sa.String(20), nullable=True),
|
||||
sa.Column('last_sync_error', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_calendar_integrations_user_id'), 'calendar_integrations', ['user_id'], unique=False)
|
||||
op.create_index(op.f('ix_calendar_integrations_provider'), 'calendar_integrations', ['provider'], unique=False)
|
||||
op.create_index(op.f('ix_calendar_integrations_is_active'), 'calendar_integrations', ['is_active'], unique=False)
|
||||
if not _has_table('calendar_integrations'):
|
||||
op.create_table('calendar_integrations',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('provider', sa.String(50), nullable=False),
|
||||
sa.Column('access_token', sa.Text(), nullable=False),
|
||||
sa.Column('refresh_token', sa.Text(), nullable=True),
|
||||
sa.Column('token_expires_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('calendar_id', sa.String(200), nullable=True),
|
||||
sa.Column('calendar_name', sa.String(200), nullable=True),
|
||||
sa.Column('sync_settings', sa.JSON(), nullable=False, server_default='{}'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
|
||||
sa.Column('last_sync_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('last_sync_status', sa.String(20), nullable=True),
|
||||
sa.Column('last_sync_error', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 065] ℹ Table calendar_integrations already exists, skipping creation")
|
||||
|
||||
for idx_name, cols in [
|
||||
(op.f('ix_calendar_integrations_user_id'), ['user_id']),
|
||||
(op.f('ix_calendar_integrations_provider'), ['provider']),
|
||||
(op.f('ix_calendar_integrations_is_active'), ['is_active']),
|
||||
]:
|
||||
if _has_table('calendar_integrations') and not _has_index('calendar_integrations', idx_name):
|
||||
try:
|
||||
op.create_index(idx_name, 'calendar_integrations', cols, unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Calendar Sync Events
|
||||
op.create_table('calendar_sync_events',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('integration_id', sa.Integer(), nullable=False),
|
||||
sa.Column('event_type', sa.String(50), nullable=False),
|
||||
sa.Column('time_entry_id', sa.Integer(), nullable=True),
|
||||
sa.Column('calendar_event_id', sa.String(200), nullable=True),
|
||||
sa.Column('direction', sa.String(20), nullable=False),
|
||||
sa.Column('status', sa.String(20), nullable=False),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('synced_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['integration_id'], ['calendar_integrations.id'], ),
|
||||
sa.ForeignKeyConstraint(['time_entry_id'], ['time_entries.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_calendar_sync_events_integration_id'), 'calendar_sync_events', ['integration_id'], unique=False)
|
||||
op.create_index(op.f('ix_calendar_sync_events_event_type'), 'calendar_sync_events', ['event_type'], unique=False)
|
||||
op.create_index(op.f('ix_calendar_sync_events_time_entry_id'), 'calendar_sync_events', ['time_entry_id'], unique=False)
|
||||
op.create_index(op.f('ix_calendar_sync_events_calendar_event_id'), 'calendar_sync_events', ['calendar_event_id'], unique=False)
|
||||
op.create_index(op.f('ix_calendar_sync_events_status'), 'calendar_sync_events', ['status'], unique=False)
|
||||
if not _has_table('calendar_sync_events'):
|
||||
op.create_table('calendar_sync_events',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('integration_id', sa.Integer(), nullable=False),
|
||||
sa.Column('event_type', sa.String(50), nullable=False),
|
||||
sa.Column('time_entry_id', sa.Integer(), nullable=True),
|
||||
sa.Column('calendar_event_id', sa.String(200), nullable=True),
|
||||
sa.Column('direction', sa.String(20), nullable=False),
|
||||
sa.Column('status', sa.String(20), nullable=False),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('synced_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['integration_id'], ['calendar_integrations.id'], ),
|
||||
sa.ForeignKeyConstraint(['time_entry_id'], ['time_entries.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 065] ℹ Table calendar_sync_events already exists, skipping creation")
|
||||
|
||||
for idx_name, cols in [
|
||||
(op.f('ix_calendar_sync_events_integration_id'), ['integration_id']),
|
||||
(op.f('ix_calendar_sync_events_event_type'), ['event_type']),
|
||||
(op.f('ix_calendar_sync_events_time_entry_id'), ['time_entry_id']),
|
||||
(op.f('ix_calendar_sync_events_calendar_event_id'), ['calendar_event_id']),
|
||||
(op.f('ix_calendar_sync_events_status'), ['status']),
|
||||
]:
|
||||
if _has_table('calendar_sync_events') and not _has_index('calendar_sync_events', idx_name):
|
||||
try:
|
||||
op.create_index(idx_name, 'calendar_sync_events', cols, unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
@@ -27,6 +27,22 @@ def _has_column(inspector, table_name: str, column_name: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _has_table(inspector, table_name: str) -> bool:
|
||||
"""Check if a table exists"""
|
||||
try:
|
||||
return table_name in inspector.get_table_names()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _has_index(inspector, table_name: str, index_name: str) -> bool:
|
||||
"""Check if an index exists on a table"""
|
||||
try:
|
||||
return any((idx.get("name") or "") == index_name for idx in inspector.get_indexes(table_name))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def upgrade():
|
||||
"""Add custom_fields to clients and create link_templates table"""
|
||||
bind = op.get_bind()
|
||||
@@ -35,30 +51,44 @@ def upgrade():
|
||||
bool_true_default = '1' if dialect_name == 'sqlite' else ('true' if dialect_name == 'postgresql' else '1')
|
||||
|
||||
# Add custom_fields column to clients table if it doesn't exist
|
||||
if 'clients' in inspector.get_table_names():
|
||||
if _has_table(inspector, 'clients'):
|
||||
if not _has_column(inspector, 'clients', 'custom_fields'):
|
||||
# Use portable JSON type for cross-db compatibility (SQLite + PostgreSQL).
|
||||
op.add_column('clients', sa.Column('custom_fields', sa.JSON(), nullable=True))
|
||||
|
||||
# Create link_templates table
|
||||
op.create_table(
|
||||
'link_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('url_template', sa.String(length=1000), nullable=False),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True),
|
||||
sa.Column('field_key', sa.String(length=100), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text(bool_true_default)),
|
||||
sa.Column('order', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_link_templates_is_active', 'link_templates', ['is_active'])
|
||||
op.create_index('idx_link_templates_field_key', 'link_templates', ['field_key'])
|
||||
# Create link_templates table (idempotent; some installs may already have it)
|
||||
if not _has_table(inspector, 'link_templates'):
|
||||
op.create_table(
|
||||
'link_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('url_template', sa.String(length=1000), nullable=False),
|
||||
sa.Column('icon', sa.String(length=50), nullable=True),
|
||||
sa.Column('field_key', sa.String(length=100), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text(bool_true_default)),
|
||||
sa.Column('order', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 075] ℹ Table link_templates already exists, skipping creation")
|
||||
|
||||
# Ensure indexes exist (best-effort / idempotent)
|
||||
if _has_table(inspector, 'link_templates'):
|
||||
if not _has_index(inspector, 'link_templates', 'idx_link_templates_is_active'):
|
||||
try:
|
||||
op.create_index('idx_link_templates_is_active', 'link_templates', ['is_active'])
|
||||
except Exception:
|
||||
pass
|
||||
if not _has_index(inspector, 'link_templates', 'idx_link_templates_field_key'):
|
||||
try:
|
||||
op.create_index('idx_link_templates_field_key', 'link_templates', ['field_key'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
@@ -67,13 +97,13 @@ def downgrade():
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
# Drop link_templates table
|
||||
if 'link_templates' in inspector.get_table_names():
|
||||
if _has_table(inspector, 'link_templates'):
|
||||
op.drop_index('idx_link_templates_field_key', table_name='link_templates')
|
||||
op.drop_index('idx_link_templates_is_active', table_name='link_templates')
|
||||
op.drop_table('link_templates')
|
||||
|
||||
# Remove custom_fields column from clients table
|
||||
if 'clients' in inspector.get_table_names():
|
||||
if _has_table(inspector, 'clients'):
|
||||
if _has_column(inspector, 'clients', 'custom_fields'):
|
||||
op.drop_column('clients', 'custom_fields')
|
||||
|
||||
|
||||
@@ -17,66 +17,131 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
def _has_table(name: str) -> bool:
|
||||
try:
|
||||
return name in inspector.get_table_names()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _has_index(table_name: str, index_name: str) -> bool:
|
||||
try:
|
||||
return any((idx.get("name") or "") == index_name for idx in inspector.get_indexes(table_name))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _has_column(table_name: str, column_name: str) -> bool:
|
||||
try:
|
||||
return column_name in {c["name"] for c in inspector.get_columns(table_name)}
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Create time_entry_templates table
|
||||
op.create_table('time_entry_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('project_id', sa.Integer(), nullable=False),
|
||||
sa.Column('task_id', sa.Integer(), nullable=True),
|
||||
sa.Column('default_duration_minutes', sa.Integer(), nullable=True),
|
||||
sa.Column('default_notes', sa.Text(), nullable=True),
|
||||
sa.Column('tags', sa.String(length=500), nullable=True),
|
||||
sa.Column('billable', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('usage_count', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('last_used_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
|
||||
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_time_entry_templates_project_id'), 'time_entry_templates', ['project_id'], unique=False)
|
||||
op.create_index(op.f('ix_time_entry_templates_task_id'), 'time_entry_templates', ['task_id'], unique=False)
|
||||
op.create_index(op.f('ix_time_entry_templates_user_id'), 'time_entry_templates', ['user_id'], unique=False)
|
||||
if not _has_table('time_entry_templates'):
|
||||
op.create_table('time_entry_templates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('project_id', sa.Integer(), nullable=False),
|
||||
sa.Column('task_id', sa.Integer(), nullable=True),
|
||||
sa.Column('default_duration_minutes', sa.Integer(), nullable=True),
|
||||
sa.Column('default_notes', sa.Text(), nullable=True),
|
||||
sa.Column('tags', sa.String(length=500), nullable=True),
|
||||
sa.Column('billable', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('usage_count', sa.Integer(), nullable=False, server_default='0'),
|
||||
sa.Column('last_used_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
|
||||
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 022] ℹ Table time_entry_templates already exists, skipping creation")
|
||||
|
||||
# Ensure indexes exist (best-effort / idempotent)
|
||||
idx_te_project = op.f('ix_time_entry_templates_project_id')
|
||||
idx_te_task = op.f('ix_time_entry_templates_task_id')
|
||||
idx_te_user = op.f('ix_time_entry_templates_user_id')
|
||||
if _has_table('time_entry_templates'):
|
||||
if not _has_index('time_entry_templates', idx_te_project):
|
||||
try:
|
||||
op.create_index(idx_te_project, 'time_entry_templates', ['project_id'], unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
if not _has_index('time_entry_templates', idx_te_task):
|
||||
try:
|
||||
op.create_index(idx_te_task, 'time_entry_templates', ['task_id'], unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
if not _has_index('time_entry_templates', idx_te_user):
|
||||
try:
|
||||
op.create_index(idx_te_user, 'time_entry_templates', ['user_id'], unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Create activities table
|
||||
op.create_table('activities',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('action', sa.String(length=50), nullable=False),
|
||||
sa.Column('entity_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('entity_id', sa.Integer(), nullable=False),
|
||||
sa.Column('entity_name', sa.String(length=500), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('extra_data', sa.JSON(), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
||||
sa.Column('user_agent', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_activities_action'), 'activities', ['action'], unique=False)
|
||||
op.create_index(op.f('ix_activities_created_at'), 'activities', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_activities_entity_id'), 'activities', ['entity_id'], unique=False)
|
||||
op.create_index(op.f('ix_activities_entity_type'), 'activities', ['entity_type'], unique=False)
|
||||
op.create_index(op.f('ix_activities_user_id'), 'activities', ['user_id'], unique=False)
|
||||
op.create_index('ix_activities_user_created', 'activities', ['user_id', 'created_at'], unique=False)
|
||||
op.create_index('ix_activities_entity', 'activities', ['entity_type', 'entity_id'], unique=False)
|
||||
if not _has_table('activities'):
|
||||
op.create_table('activities',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('action', sa.String(length=50), nullable=False),
|
||||
sa.Column('entity_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('entity_id', sa.Integer(), nullable=False),
|
||||
sa.Column('entity_name', sa.String(length=500), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('extra_data', sa.JSON(), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
||||
sa.Column('user_agent', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
else:
|
||||
print("[Migration 022] ℹ Table activities already exists, skipping creation")
|
||||
|
||||
# Ensure indexes exist (best-effort / idempotent)
|
||||
if _has_table('activities'):
|
||||
for idx_name, cols in [
|
||||
(op.f('ix_activities_action'), ['action']),
|
||||
(op.f('ix_activities_created_at'), ['created_at']),
|
||||
(op.f('ix_activities_entity_id'), ['entity_id']),
|
||||
(op.f('ix_activities_entity_type'), ['entity_type']),
|
||||
(op.f('ix_activities_user_id'), ['user_id']),
|
||||
('ix_activities_user_created', ['user_id', 'created_at']),
|
||||
('ix_activities_entity', ['entity_type', 'entity_id']),
|
||||
]:
|
||||
if not _has_index('activities', idx_name):
|
||||
try:
|
||||
op.create_index(idx_name, 'activities', cols, unique=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add user preference columns to users table
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('email_notifications', sa.Boolean(), nullable=False, server_default='true'))
|
||||
batch_op.add_column(sa.Column('notification_overdue_invoices', sa.Boolean(), nullable=False, server_default='true'))
|
||||
batch_op.add_column(sa.Column('notification_task_assigned', sa.Boolean(), nullable=False, server_default='true'))
|
||||
batch_op.add_column(sa.Column('notification_task_comments', sa.Boolean(), nullable=False, server_default='true'))
|
||||
batch_op.add_column(sa.Column('notification_weekly_summary', sa.Boolean(), nullable=False, server_default='false'))
|
||||
batch_op.add_column(sa.Column('timezone', sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column('date_format', sa.String(length=20), nullable=False, server_default='YYYY-MM-DD'))
|
||||
batch_op.add_column(sa.Column('time_format', sa.String(length=10), nullable=False, server_default='24h'))
|
||||
batch_op.add_column(sa.Column('week_start_day', sa.Integer(), nullable=False, server_default='1'))
|
||||
if _has_table('users'):
|
||||
if not _has_column('users', 'email_notifications'):
|
||||
batch_op.add_column(sa.Column('email_notifications', sa.Boolean(), nullable=False, server_default='true'))
|
||||
if not _has_column('users', 'notification_overdue_invoices'):
|
||||
batch_op.add_column(sa.Column('notification_overdue_invoices', sa.Boolean(), nullable=False, server_default='true'))
|
||||
if not _has_column('users', 'notification_task_assigned'):
|
||||
batch_op.add_column(sa.Column('notification_task_assigned', sa.Boolean(), nullable=False, server_default='true'))
|
||||
if not _has_column('users', 'notification_task_comments'):
|
||||
batch_op.add_column(sa.Column('notification_task_comments', sa.Boolean(), nullable=False, server_default='true'))
|
||||
if not _has_column('users', 'notification_weekly_summary'):
|
||||
batch_op.add_column(sa.Column('notification_weekly_summary', sa.Boolean(), nullable=False, server_default='false'))
|
||||
if not _has_column('users', 'timezone'):
|
||||
batch_op.add_column(sa.Column('timezone', sa.String(length=50), nullable=True))
|
||||
if not _has_column('users', 'date_format'):
|
||||
batch_op.add_column(sa.Column('date_format', sa.String(length=20), nullable=False, server_default='YYYY-MM-DD'))
|
||||
if not _has_column('users', 'time_format'):
|
||||
batch_op.add_column(sa.Column('time_format', sa.String(length=10), nullable=False, server_default='24h'))
|
||||
if not _has_column('users', 'week_start_day'):
|
||||
batch_op.add_column(sa.Column('week_start_day', sa.Integer(), nullable=False, server_default='1'))
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
Reference in New Issue
Block a user