Multi-tenant factory inspection system (SpiFox, Enkid, Alpet): - FastAPI backend with JWT auth, PostgreSQL (asyncpg) - Next.js 16 frontend with App Router, SWR data fetching - Machines CRUD with equipment parts management - Part lifecycle tracking (hours/count/date) with counters - Partial unique index for soft-delete support - 24 pytest tests passing, E2E verified Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
93 lines
4.7 KiB
Python
93 lines
4.7 KiB
Python
"""add_equipment_parts_counters_replacement_logs
|
|
|
|
Revision ID: 247755b06e67
|
|
Revises: f39c75bd9514
|
|
Create Date: 2026-02-10 11:29:35.848537
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '247755b06e67'
|
|
down_revision: Union[str, Sequence[str], None] = 'f39c75bd9514'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Upgrade schema."""
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table('equipment_parts',
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('tenant_id', sa.String(length=50), nullable=False),
|
|
sa.Column('machine_id', sa.UUID(), nullable=False),
|
|
sa.Column('name', sa.String(length=100), nullable=False),
|
|
sa.Column('part_number', sa.String(length=50), nullable=True),
|
|
sa.Column('category', sa.String(length=50), nullable=True),
|
|
sa.Column('lifecycle_type', sa.String(length=20), nullable=False),
|
|
sa.Column('lifecycle_limit', sa.Float(), nullable=False),
|
|
sa.Column('alarm_threshold', sa.Float(), nullable=True),
|
|
sa.Column('counter_source', sa.String(length=20), nullable=True),
|
|
sa.Column('installed_at', postgresql.TIMESTAMP(timezone=True), nullable=False),
|
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
|
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), nullable=True),
|
|
sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), nullable=True),
|
|
sa.ForeignKeyConstraint(['machine_id'], ['machines.id'], ),
|
|
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('tenant_id', 'machine_id', 'name', name='uq_equipment_part_tenant_machine_name')
|
|
)
|
|
op.create_index('ix_equipment_parts_tenant_id', 'equipment_parts', ['tenant_id'], unique=False)
|
|
op.create_index('ix_equipment_parts_tenant_machine', 'equipment_parts', ['tenant_id', 'machine_id'], unique=False)
|
|
op.create_table('part_counters',
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('tenant_id', sa.String(length=50), nullable=False),
|
|
sa.Column('equipment_part_id', sa.UUID(), nullable=False),
|
|
sa.Column('current_value', sa.Float(), nullable=True),
|
|
sa.Column('lifecycle_pct', sa.Float(), nullable=True),
|
|
sa.Column('last_reset_at', postgresql.TIMESTAMP(timezone=True), nullable=False),
|
|
sa.Column('last_updated_at', postgresql.TIMESTAMP(timezone=True), nullable=False),
|
|
sa.Column('version', sa.Integer(), nullable=True),
|
|
sa.ForeignKeyConstraint(['equipment_part_id'], ['equipment_parts.id'], ),
|
|
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('equipment_part_id')
|
|
)
|
|
op.create_index('ix_part_counters_tenant_id', 'part_counters', ['tenant_id'], unique=False)
|
|
op.create_table('part_replacement_logs',
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('tenant_id', sa.String(length=50), nullable=False),
|
|
sa.Column('equipment_part_id', sa.UUID(), nullable=False),
|
|
sa.Column('replaced_by', sa.UUID(), nullable=True),
|
|
sa.Column('replaced_at', postgresql.TIMESTAMP(timezone=True), nullable=False),
|
|
sa.Column('counter_at_replacement', sa.Float(), nullable=False),
|
|
sa.Column('lifecycle_pct_at_replacement', sa.Float(), nullable=False),
|
|
sa.Column('reason', sa.String(length=200), nullable=True),
|
|
sa.Column('notes', sa.Text(), nullable=True),
|
|
sa.ForeignKeyConstraint(['equipment_part_id'], ['equipment_parts.id'], ),
|
|
sa.ForeignKeyConstraint(['replaced_by'], ['users.id'], ),
|
|
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_part_replacement_tenant_date', 'part_replacement_logs', ['tenant_id', 'replaced_at'], unique=False)
|
|
op.create_index('ix_part_replacement_tenant_part', 'part_replacement_logs', ['tenant_id', 'equipment_part_id'], unique=False)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Downgrade schema."""
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_index('ix_part_replacement_tenant_part', table_name='part_replacement_logs')
|
|
op.drop_index('ix_part_replacement_tenant_date', table_name='part_replacement_logs')
|
|
op.drop_table('part_replacement_logs')
|
|
op.drop_index('ix_part_counters_tenant_id', table_name='part_counters')
|
|
op.drop_table('part_counters')
|
|
op.drop_index('ix_equipment_parts_tenant_machine', table_name='equipment_parts')
|
|
op.drop_index('ix_equipment_parts_tenant_id', table_name='equipment_parts')
|
|
op.drop_table('equipment_parts')
|
|
# ### end Alembic commands ###
|