feat: initial commit
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""initial schema
|
||||
|
||||
Revision ID: 001
|
||||
Revises:
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = "001"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# users
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("email", sa.String(255), nullable=False, unique=True),
|
||||
sa.Column("password_hash", sa.String(255), nullable=False),
|
||||
sa.Column("full_name", sa.String(255), nullable=False),
|
||||
sa.Column("role", sa.Enum("admin", "client", name="userrole"), nullable=False, server_default="client"),
|
||||
sa.Column("is_active", sa.Boolean, nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_users_email", "users", ["email"])
|
||||
|
||||
# templates
|
||||
op.create_table(
|
||||
"templates",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("category_key", sa.String(100), nullable=False, unique=True),
|
||||
sa.Column("standard_fields", postgresql.JSONB, nullable=False, server_default="{}"),
|
||||
sa.Column("component_schema", postgresql.JSONB, nullable=False, server_default="{}"),
|
||||
sa.Column("description", sa.Text, nullable=True),
|
||||
sa.Column("is_active", sa.Boolean, nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_templates_category_key", "templates", ["category_key"])
|
||||
|
||||
# cad_files
|
||||
op.create_table(
|
||||
"cad_files",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("original_name", sa.String(500), nullable=False),
|
||||
sa.Column("stored_path", sa.String(1000), nullable=False),
|
||||
sa.Column("file_hash", sa.String(64), nullable=False, unique=True),
|
||||
sa.Column("file_size", sa.BigInteger, nullable=True),
|
||||
sa.Column("parsed_objects", postgresql.JSONB, nullable=True),
|
||||
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
|
||||
sa.Column("gltf_path", sa.String(1000), nullable=True),
|
||||
sa.Column(
|
||||
"processing_status",
|
||||
sa.Enum("pending", "processing", "completed", "failed", name="processingstatus"),
|
||||
nullable=False,
|
||||
server_default="pending",
|
||||
),
|
||||
sa.Column("error_message", sa.String(2000), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_cad_files_file_hash", "cad_files", ["file_hash"])
|
||||
|
||||
# orders
|
||||
op.create_table(
|
||||
"orders",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("order_number", sa.String(50), nullable=False, unique=True),
|
||||
sa.Column("template_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("templates.id"), nullable=True),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum("draft", "submitted", "processing", "completed", "rejected", name="orderstatus"),
|
||||
nullable=False,
|
||||
server_default="draft",
|
||||
),
|
||||
sa.Column("created_by", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False),
|
||||
sa.Column("source_excel", sa.String(1000), nullable=True),
|
||||
sa.Column("notes", sa.Text, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_orders_order_number", "orders", ["order_number"])
|
||||
|
||||
# order_items
|
||||
op.create_table(
|
||||
"order_items",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("order_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("orders.id"), nullable=False),
|
||||
sa.Column("row_index", sa.Integer, nullable=False),
|
||||
sa.Column("ebene1", sa.String(500), nullable=True),
|
||||
sa.Column("ebene2", sa.String(500), nullable=True),
|
||||
sa.Column("baureihe", sa.String(500), nullable=True),
|
||||
sa.Column("pim_id", sa.String(500), nullable=True),
|
||||
sa.Column("produkt_baureihe", sa.String(500), nullable=True),
|
||||
sa.Column("gewaehltes_produkt", sa.String(500), nullable=True),
|
||||
sa.Column("name_cad_modell", sa.String(500), nullable=True),
|
||||
sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True),
|
||||
sa.Column("lagertyp", sa.String(500), nullable=True),
|
||||
sa.Column("medias_rendering", sa.Boolean, nullable=True),
|
||||
sa.Column("components", postgresql.JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("cad_file_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("cad_files.id"), nullable=True),
|
||||
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
|
||||
sa.Column(
|
||||
"ai_validation_status",
|
||||
sa.Enum("not_started", "pending", "completed", "failed", name="aivalidationstatus"),
|
||||
nullable=False,
|
||||
server_default="not_started",
|
||||
),
|
||||
sa.Column("ai_validation_result", postgresql.JSONB, nullable=True),
|
||||
sa.Column(
|
||||
"item_status",
|
||||
sa.Enum("pending", "approved", "rejected", name="itemstatus"),
|
||||
nullable=False,
|
||||
server_default="pending",
|
||||
),
|
||||
sa.Column("notes", sa.Text, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# audit_log
|
||||
op.create_table(
|
||||
"audit_log",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True),
|
||||
sa.Column("action", sa.String(100), nullable=False),
|
||||
sa.Column("entity_type", sa.String(100), nullable=True),
|
||||
sa.Column("entity_id", sa.String(255), nullable=True),
|
||||
sa.Column("details", postgresql.JSONB, nullable=True),
|
||||
sa.Column("timestamp", sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("audit_log")
|
||||
op.drop_table("order_items")
|
||||
op.drop_table("orders")
|
||||
op.drop_table("cad_files")
|
||||
op.drop_table("templates")
|
||||
op.drop_table("users")
|
||||
op.execute("DROP TYPE IF EXISTS userrole")
|
||||
op.execute("DROP TYPE IF EXISTS orderstatus")
|
||||
op.execute("DROP TYPE IF EXISTS processingstatus")
|
||||
op.execute("DROP TYPE IF EXISTS aivalidationstatus")
|
||||
op.execute("DROP TYPE IF EXISTS itemstatus")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""system settings table
|
||||
|
||||
Revision ID: 002
|
||||
Revises: 001
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "002"
|
||||
down_revision: Union[str, None] = "001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"system_settings",
|
||||
sa.Column("key", sa.String(100), primary_key=True),
|
||||
sa.Column("value", sa.Text(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True, server_default=sa.func.now()),
|
||||
)
|
||||
# Insert defaults
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('thumbnail_renderer', 'pillow', NOW())"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("system_settings")
|
||||
@@ -0,0 +1,31 @@
|
||||
"""blender render settings
|
||||
|
||||
Revision ID: 003
|
||||
Revises: 002
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "003"
|
||||
down_revision: Union[str, None] = "002"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('blender_engine', 'cycles', NOW()),"
|
||||
"('blender_cycles_samples', '256', NOW()),"
|
||||
"('blender_eevee_samples', '64', NOW()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM system_settings WHERE key IN "
|
||||
"('blender_engine', 'blender_cycles_samples', 'blender_eevee_samples')"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""threejs render size setting
|
||||
|
||||
Revision ID: 004
|
||||
Revises: 003
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "004"
|
||||
down_revision: Union[str, None] = "003"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('threejs_render_size', '512', NOW()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM system_settings WHERE key = 'threejs_render_size'"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""set threejs_render_size default to 1024
|
||||
|
||||
Revision ID: 005
|
||||
Revises: 004
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "005"
|
||||
down_revision: Union[str, None] = "004"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"UPDATE system_settings SET value = '1024', updated_at = NOW() "
|
||||
"WHERE key = 'threejs_render_size' AND value = '512'"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"UPDATE system_settings SET value = '512', updated_at = NOW() "
|
||||
"WHERE key = 'threejs_render_size' AND value = '1024'"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""thumbnail format setting (jpg | png)
|
||||
|
||||
Revision ID: 006
|
||||
Revises: 005
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "006"
|
||||
down_revision: Union[str, None] = "005"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) VALUES "
|
||||
"('thumbnail_format', 'jpg', NOW()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"DELETE FROM system_settings WHERE key = 'thumbnail_format'"
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""materials table and cad_part_materials column
|
||||
|
||||
Revision ID: 007
|
||||
Revises: 006
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision: str = "007"
|
||||
down_revision: Union[str, None] = "006"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"materials",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("name", sa.String(200), nullable=False, unique=True),
|
||||
sa.Column("description", sa.Text, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.text("NOW()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.text("NOW()"), nullable=False),
|
||||
)
|
||||
op.execute("ALTER TABLE order_items ADD COLUMN IF NOT EXISTS cad_part_materials JSONB NOT NULL DEFAULT '[]'::jsonb")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("ALTER TABLE order_items DROP COLUMN IF EXISTS cad_part_materials")
|
||||
op.drop_table("materials")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Add created_by and source to materials
|
||||
|
||||
Revision ID: 008
|
||||
Revises: 007
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision: str = "008"
|
||||
down_revision: Union[str, None] = "007"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE materials "
|
||||
"ADD COLUMN IF NOT EXISTS created_by UUID REFERENCES users(id) ON DELETE SET NULL, "
|
||||
"ADD COLUMN IF NOT EXISTS source VARCHAR(20) NOT NULL DEFAULT 'manual'"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE materials "
|
||||
"DROP COLUMN IF EXISTS created_by, "
|
||||
"DROP COLUMN IF EXISTS source"
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Add render_log JSONB column to cad_files
|
||||
|
||||
Revision ID: 009
|
||||
Revises: 008
|
||||
Create Date: 2026-03-01
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "009"
|
||||
down_revision: Union[str, None] = "008"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE cad_files "
|
||||
"ADD COLUMN IF NOT EXISTS render_log JSONB"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"ALTER TABLE cad_files "
|
||||
"DROP COLUMN IF EXISTS render_log"
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""KPI analytics and pricing tiers
|
||||
|
||||
Revision ID: 010
|
||||
Revises: 009
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "010"
|
||||
down_revision: Union[str, None] = "009"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add project_manager to userrole enum — must be outside a transaction
|
||||
op.execute("COMMIT")
|
||||
op.execute("ALTER TYPE userrole ADD VALUE IF NOT EXISTS 'project_manager'")
|
||||
op.execute("BEGIN")
|
||||
|
||||
# Lifecycle timestamps + estimated_price on orders
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS submitted_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS processing_started_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS rejected_at TIMESTAMP WITHOUT TIME ZONE"
|
||||
)
|
||||
op.execute(
|
||||
"ALTER TABLE orders "
|
||||
"ADD COLUMN IF NOT EXISTS estimated_price NUMERIC(12, 2)"
|
||||
)
|
||||
|
||||
# pricing_tiers table
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pricing_tiers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
category_key VARCHAR(100) NOT NULL,
|
||||
quality_level VARCHAR(50) NOT NULL DEFAULT 'Normal',
|
||||
price_per_item NUMERIC(10, 2) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_pricing_tier UNIQUE (category_key, quality_level)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_pricing_tiers_category_key "
|
||||
"ON pricing_tiers (category_key)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE IF EXISTS pricing_tiers")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS estimated_price")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS rejected_at")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS completed_at")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS processing_started_at")
|
||||
op.execute("ALTER TABLE orders DROP COLUMN IF EXISTS submitted_at")
|
||||
# Note: removing enum values is not supported in PostgreSQL without full recreation
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Product library — products, output_types, order_lines tables
|
||||
|
||||
Revision ID: 011
|
||||
Revises: 010
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "011"
|
||||
down_revision: Union[str, None] = "010"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
pim_id VARCHAR(500) UNIQUE NOT NULL,
|
||||
name VARCHAR(500),
|
||||
category_key VARCHAR(100),
|
||||
ebene1 VARCHAR(500),
|
||||
ebene2 VARCHAR(500),
|
||||
baureihe VARCHAR(500),
|
||||
produkt_baureihe VARCHAR(500),
|
||||
lagertyp VARCHAR(500),
|
||||
name_cad_modell VARCHAR(500),
|
||||
components JSONB NOT NULL DEFAULT '[]',
|
||||
cad_part_materials JSONB NOT NULL DEFAULT '[]',
|
||||
cad_file_id UUID REFERENCES cad_files(id) ON DELETE SET NULL,
|
||||
notes TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
source_excel VARCHAR(1000),
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_products_category_key ON products (category_key)")
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_products_name_cad_modell ON products (name_cad_modell)")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS output_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
renderer VARCHAR(50) NOT NULL DEFAULT 'threejs',
|
||||
render_settings JSONB NOT NULL DEFAULT '{}',
|
||||
output_format VARCHAR(20) NOT NULL DEFAULT 'png',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS order_lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id),
|
||||
output_type_id UUID REFERENCES output_types(id),
|
||||
gewuenschte_bildnummer VARCHAR(500),
|
||||
item_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
render_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
result_path VARCHAR(1000),
|
||||
render_log JSONB,
|
||||
ai_validation_status VARCHAR(20) NOT NULL DEFAULT 'not_started',
|
||||
ai_validation_result JSONB,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_order_lines_order_id ON order_lines (order_id)")
|
||||
op.execute("CREATE INDEX IF NOT EXISTS ix_order_lines_product_id ON order_lines (product_id)")
|
||||
# Partial unique indexes to handle NULL output_type_id correctly
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE IF EXISTS order_lines")
|
||||
op.execute("DROP TABLE IF EXISTS output_types")
|
||||
op.execute("DROP TABLE IF EXISTS products")
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Backfill products and order_lines from order_items
|
||||
|
||||
Revision ID: 012
|
||||
Revises: 011
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "012"
|
||||
down_revision: Union[str, None] = "011"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Seed default output type
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO output_types (id, name, renderer, output_format, sort_order)
|
||||
VALUES (gen_random_uuid(), '3D Thumbnail', 'threejs', 'png', 0)
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
# 2. Create products from distinct pim_id in order_items
|
||||
# For each distinct pim_id, take fields from the most recently updated row
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO products (
|
||||
pim_id, name, category_key, ebene1, ebene2, baureihe,
|
||||
produkt_baureihe, lagertyp, name_cad_modell,
|
||||
components, cad_part_materials, cad_file_id, source_excel
|
||||
)
|
||||
SELECT DISTINCT ON (oi.pim_id)
|
||||
oi.pim_id,
|
||||
oi.gewaehltes_produkt AS name,
|
||||
t.category_key,
|
||||
oi.ebene1,
|
||||
oi.ebene2,
|
||||
oi.baureihe,
|
||||
oi.produkt_baureihe,
|
||||
oi.lagertyp,
|
||||
oi.name_cad_modell,
|
||||
oi.components,
|
||||
COALESCE(oi.cad_part_materials, '[]'::jsonb) AS cad_part_materials,
|
||||
oi.cad_file_id,
|
||||
o.source_excel
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
LEFT JOIN templates t ON t.id = o.template_id
|
||||
WHERE oi.pim_id IS NOT NULL
|
||||
AND oi.pim_id <> ''
|
||||
ORDER BY oi.pim_id, oi.updated_at DESC
|
||||
ON CONFLICT (pim_id) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
# 3. Create order_lines from order_items where pim_id IS NOT NULL
|
||||
# 3a. Rows with medias_rendering = true → link to '3D Thumbnail' output type
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO order_lines (
|
||||
order_id, product_id, output_type_id,
|
||||
gewuenschte_bildnummer, item_status, render_status,
|
||||
ai_validation_status, ai_validation_result, notes
|
||||
)
|
||||
SELECT
|
||||
oi.order_id,
|
||||
p.id AS product_id,
|
||||
ot.id AS output_type_id,
|
||||
oi.gewuenschte_bildnummer,
|
||||
oi.item_status::TEXT,
|
||||
CASE
|
||||
WHEN oi.cad_file_id IS NOT NULL THEN 'pending'
|
||||
ELSE 'pending'
|
||||
END AS render_status,
|
||||
oi.ai_validation_status::TEXT,
|
||||
oi.ai_validation_result,
|
||||
oi.notes
|
||||
FROM order_items oi
|
||||
JOIN products p ON p.pim_id = oi.pim_id
|
||||
JOIN output_types ot ON ot.name = '3D Thumbnail'
|
||||
WHERE oi.pim_id IS NOT NULL
|
||||
AND oi.pim_id <> ''
|
||||
AND oi.medias_rendering = TRUE
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
# 3b. Rows with medias_rendering = false → tracking only (no output_type_id)
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO order_lines (
|
||||
order_id, product_id, output_type_id,
|
||||
gewuenschte_bildnummer, item_status, render_status,
|
||||
ai_validation_status, ai_validation_result, notes
|
||||
)
|
||||
SELECT
|
||||
oi.order_id,
|
||||
p.id AS product_id,
|
||||
NULL AS output_type_id,
|
||||
oi.gewuenschte_bildnummer,
|
||||
oi.item_status::TEXT,
|
||||
'pending' AS render_status,
|
||||
oi.ai_validation_status::TEXT,
|
||||
oi.ai_validation_result,
|
||||
oi.notes
|
||||
FROM order_items oi
|
||||
JOIN products p ON p.pim_id = oi.pim_id
|
||||
WHERE oi.pim_id IS NOT NULL
|
||||
AND oi.pim_id <> ''
|
||||
AND (oi.medias_rendering IS NULL OR oi.medias_rendering = FALSE)
|
||||
ON CONFLICT DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DELETE FROM order_lines")
|
||||
op.execute("DELETE FROM products")
|
||||
op.execute("DELETE FROM output_types WHERE name = '3D Thumbnail'")
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Add gewuenschte_bildnummer and medias_rendering to products
|
||||
|
||||
Revision ID: 013
|
||||
Revises: 012
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "013"
|
||||
down_revision: Union[str, None] = "012"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("products", sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True))
|
||||
op.add_column("products", sa.Column("medias_rendering", sa.Boolean(), nullable=True))
|
||||
|
||||
# Backfill from order_items where available
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE products p
|
||||
SET gewuenschte_bildnummer = sub.gewuenschte_bildnummer,
|
||||
medias_rendering = sub.medias_rendering
|
||||
FROM (
|
||||
SELECT DISTINCT ON (oi.pim_id)
|
||||
oi.pim_id,
|
||||
oi.gewuenschte_bildnummer,
|
||||
oi.medias_rendering
|
||||
FROM order_items oi
|
||||
WHERE oi.pim_id IS NOT NULL AND oi.pim_id <> ''
|
||||
ORDER BY oi.pim_id, oi.updated_at DESC
|
||||
) sub
|
||||
WHERE p.pim_id = sub.pim_id
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("products", "medias_rendering")
|
||||
op.drop_column("products", "gewuenschte_bildnummer")
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Add compatible_categories to output_types
|
||||
|
||||
Revision ID: 014
|
||||
Revises: 013
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
revision: str = "014"
|
||||
down_revision: Union[str, None] = "013"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"output_types",
|
||||
sa.Column("compatible_categories", JSONB, server_default="[]", nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("output_types", "compatible_categories")
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Add Flamenco render backend support
|
||||
|
||||
Revision ID: 015
|
||||
Revises: 014
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "015"
|
||||
down_revision: Union[str, None] = "014"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# output_types: render_backend + is_animation
|
||||
op.add_column(
|
||||
"output_types",
|
||||
sa.Column("render_backend", sa.String(20), server_default="auto", nullable=False),
|
||||
)
|
||||
op.add_column(
|
||||
"output_types",
|
||||
sa.Column("is_animation", sa.Boolean(), server_default="false", nullable=False),
|
||||
)
|
||||
|
||||
# order_lines: flamenco tracking columns
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("flamenco_job_id", sa.String(100), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("render_backend_used", sa.String(20), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("render_started_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column("render_completed_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
|
||||
# Seed system settings for Flamenco
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value) VALUES "
|
||||
"('render_backend', 'celery'), "
|
||||
"('flamenco_manager_url', 'http://flamenco-manager:8080'), "
|
||||
"('flamenco_worker_count', '1') "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("order_lines", "render_completed_at")
|
||||
op.drop_column("order_lines", "render_started_at")
|
||||
op.drop_column("order_lines", "render_backend_used")
|
||||
op.drop_column("order_lines", "flamenco_job_id")
|
||||
op.drop_column("output_types", "is_animation")
|
||||
op.drop_column("output_types", "render_backend")
|
||||
|
||||
op.execute("DELETE FROM system_settings WHERE key IN ('render_backend', 'flamenco_manager_url', 'flamenco_worker_count')")
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Pricing enhancements: OutputType→PricingTier link, per-line unit_price, transparent_bg, default tier
|
||||
|
||||
Revision ID: 016
|
||||
Revises: 015
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "016"
|
||||
down_revision: Union[str, None] = "015"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# OutputType → PricingTier link
|
||||
op.add_column("output_types", sa.Column("pricing_tier_id", sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
"fk_output_types_pricing_tier_id",
|
||||
"output_types",
|
||||
"pricing_tiers",
|
||||
["pricing_tier_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.create_index("ix_output_types_pricing_tier_id", "output_types", ["pricing_tier_id"])
|
||||
|
||||
# Transparent background option for Blender PNG renders
|
||||
op.add_column("output_types", sa.Column(
|
||||
"transparent_bg", sa.Boolean(), nullable=False, server_default="false",
|
||||
))
|
||||
|
||||
# Per-line price snapshot
|
||||
op.add_column("order_lines", sa.Column("unit_price", sa.Numeric(10, 2), nullable=True))
|
||||
|
||||
# Seed global default tier (idempotent via ON CONFLICT)
|
||||
op.execute("""
|
||||
INSERT INTO pricing_tiers (category_key, quality_level, price_per_item, description, is_active, created_at, updated_at)
|
||||
VALUES ('default', 'Normal', 25.00, 'Global fallback price', true, NOW(), NOW())
|
||||
ON CONFLICT ON CONSTRAINT uq_pricing_tier DO NOTHING
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("order_lines", "unit_price")
|
||||
op.drop_column("output_types", "transparent_bg")
|
||||
op.drop_index("ix_output_types_pricing_tier_id", table_name="output_types")
|
||||
op.drop_constraint("fk_output_types_pricing_tier_id", "output_types", type_="foreignkey")
|
||||
op.drop_column("output_types", "pricing_tier_id")
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Fix stale order_lines.item_status: auto-approve lines for non-draft orders
|
||||
|
||||
Revision ID: 017
|
||||
Revises: 016
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
|
||||
revision: str = "017"
|
||||
down_revision: Union[str, None] = "016"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Order lines belonging to submitted/processing/completed orders should
|
||||
# be "approved", not stuck at "pending". The new Product Library workflow
|
||||
# has no per-item approval step — submission implies approval.
|
||||
op.execute("""
|
||||
UPDATE order_lines
|
||||
SET item_status = 'approved'
|
||||
WHERE item_status = 'pending'
|
||||
AND order_id IN (
|
||||
SELECT id FROM orders
|
||||
WHERE status IN ('submitted', 'processing', 'completed')
|
||||
)
|
||||
""")
|
||||
|
||||
# Lines belonging to rejected orders should be "rejected".
|
||||
op.execute("""
|
||||
UPDATE order_lines
|
||||
SET item_status = 'rejected'
|
||||
WHERE item_status = 'pending'
|
||||
AND order_id IN (
|
||||
SELECT id FROM orders WHERE status = 'rejected'
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Cannot reliably revert — the original values were all "pending" anyway.
|
||||
pass
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Render templates — .blend file templates per category/output type
|
||||
|
||||
Revision ID: 018
|
||||
Revises: 017
|
||||
Create Date: 2026-03-02
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "018"
|
||||
down_revision: Union[str, None] = "017"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"render_templates",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("name", sa.String(300), nullable=False),
|
||||
sa.Column("category_key", sa.String(100), nullable=True),
|
||||
sa.Column("output_type_id", UUID(as_uuid=True), sa.ForeignKey("output_types.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("blend_file_path", sa.Text, nullable=False),
|
||||
sa.Column("original_filename", sa.String(500), nullable=False),
|
||||
sa.Column("target_collection", sa.String(200), server_default="Product", nullable=False),
|
||||
sa.Column("material_replace_enabled", sa.Boolean, server_default="false", nullable=False),
|
||||
sa.Column("is_active", sa.Boolean, server_default="true", nullable=False),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.text("now()"), nullable=False),
|
||||
)
|
||||
|
||||
# Unique constraint: one active template per (category_key, output_type_id) combo
|
||||
op.create_index(
|
||||
"ix_render_templates_active_unique",
|
||||
"render_templates",
|
||||
["category_key", "output_type_id"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("is_active = true"),
|
||||
)
|
||||
|
||||
# Seed material_library_path setting
|
||||
op.execute(
|
||||
"INSERT INTO system_settings (key, value, updated_at) "
|
||||
"VALUES ('material_library_path', '', now()) "
|
||||
"ON CONFLICT (key) DO NOTHING"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_render_templates_active_unique", table_name="render_templates")
|
||||
op.drop_table("render_templates")
|
||||
op.execute("DELETE FROM system_settings WHERE key = 'material_library_path'")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Schaeffler standard materials — add schaeffler_code column and seed 35 materials
|
||||
|
||||
Revision ID: 019
|
||||
Revises: 018
|
||||
Create Date: 2026-03-02
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
revision = "019"
|
||||
down_revision = "018"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("materials", sa.Column("schaeffler_code", sa.Integer(), nullable=True))
|
||||
|
||||
from app.data.schaeffler_materials import SCHAEFFLER_MATERIALS
|
||||
|
||||
conn = op.get_bind()
|
||||
now = datetime.utcnow().isoformat()
|
||||
for mat in SCHAEFFLER_MATERIALS:
|
||||
desc = mat["description"].replace("'", "''")
|
||||
name = mat["name"].replace("'", "''")
|
||||
conn.execute(sa.text(
|
||||
f"INSERT INTO materials (id, name, description, source, schaeffler_code, created_at, updated_at) "
|
||||
f"VALUES ('{uuid.uuid4()}', '{name}', '{desc}', '{mat['source']}', "
|
||||
f"{mat['schaeffler_code']}, '{now}', '{now}') "
|
||||
f"ON CONFLICT (name) DO NOTHING"
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DELETE FROM materials WHERE source = 'schaeffler_standard'")
|
||||
op.drop_column("materials", "schaeffler_code")
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Material aliases — substitution/alias system for material name resolution
|
||||
|
||||
Revision ID: 020
|
||||
Revises: 019
|
||||
Create Date: 2026-03-02
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
revision = "020"
|
||||
down_revision = "019"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create material_aliases table
|
||||
op.create_table(
|
||||
"material_aliases",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
|
||||
sa.Column(
|
||||
"material_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("materials.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("alias", sa.String(300), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# Case-insensitive unique index on alias
|
||||
op.create_index(
|
||||
"uq_material_aliases_alias_lower",
|
||||
"material_aliases",
|
||||
[sa.text("lower(alias)")],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
# Index on material_id for FK lookups
|
||||
op.create_index(
|
||||
"ix_material_aliases_material_id",
|
||||
"material_aliases",
|
||||
["material_id"],
|
||||
)
|
||||
|
||||
# Seed aliases from naming_scheme.xlsx Materialmapping data
|
||||
_seed_aliases()
|
||||
|
||||
|
||||
def _seed_aliases() -> None:
|
||||
from app.data.material_alias_seeds import MATERIAL_ALIAS_SEEDS
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
for entry in MATERIAL_ALIAS_SEEDS:
|
||||
material_name = entry["material_name"]
|
||||
|
||||
# Look up material by name
|
||||
result = conn.execute(
|
||||
sa.text("SELECT id FROM materials WHERE name = :name"),
|
||||
{"name": material_name},
|
||||
)
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
# Material not seeded yet, skip
|
||||
continue
|
||||
|
||||
material_id = row[0]
|
||||
|
||||
for alias_str in entry["aliases"]:
|
||||
# Skip if alias already exists (case-insensitive)
|
||||
existing = conn.execute(
|
||||
sa.text("SELECT id FROM material_aliases WHERE lower(alias) = lower(:alias)"),
|
||||
{"alias": alias_str},
|
||||
)
|
||||
if existing.fetchone():
|
||||
continue
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"INSERT INTO material_aliases (id, material_id, alias, created_at) "
|
||||
"VALUES (:id, :material_id, :alias, :created_at)"
|
||||
),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"material_id": str(material_id),
|
||||
"alias": alias_str,
|
||||
"created_at": datetime.utcnow(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_material_aliases_material_id", table_name="material_aliases")
|
||||
op.drop_index("uq_material_aliases_alias_lower", table_name="material_aliases")
|
||||
op.drop_table("material_aliases")
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Notification center — add target_user_id, read_at, notification to audit_log
|
||||
|
||||
Revision ID: 021
|
||||
Revises: 020
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "021"
|
||||
down_revision = "020"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"audit_log",
|
||||
sa.Column(
|
||||
"target_user_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"audit_log",
|
||||
sa.Column("read_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"audit_log",
|
||||
sa.Column(
|
||||
"notification",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("false"),
|
||||
),
|
||||
)
|
||||
|
||||
# Composite index for user notification queries
|
||||
op.create_index(
|
||||
"ix_audit_log_target_notification",
|
||||
"audit_log",
|
||||
["target_user_id", "notification", "read_at"],
|
||||
)
|
||||
|
||||
# Partial index for listing recent notifications
|
||||
op.create_index(
|
||||
"ix_audit_log_notification_ts",
|
||||
"audit_log",
|
||||
["notification", "timestamp"],
|
||||
postgresql_where=sa.text("notification = true"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_audit_log_notification_ts", table_name="audit_log")
|
||||
op.drop_index("ix_audit_log_target_notification", table_name="audit_log")
|
||||
op.drop_column("audit_log", "notification")
|
||||
op.drop_column("audit_log", "read_at")
|
||||
op.drop_column("audit_log", "target_user_id")
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Product variants — per-product material variant support
|
||||
|
||||
Revision ID: 022
|
||||
Revises: 021
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision = "022"
|
||||
down_revision = "021"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# --- New table: product_variants ---
|
||||
op.create_table(
|
||||
"product_variants",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("product_id", UUID(as_uuid=True), sa.ForeignKey("products.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("name", sa.String(500), nullable=False),
|
||||
sa.Column("gewuenschte_bildnummer", sa.String(500), nullable=True),
|
||||
sa.Column("components", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("is_default", sa.Boolean, nullable=False, server_default="false"),
|
||||
sa.Column("source_excel", sa.String(1000), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
# Unique constraint: (product_id, lower(name))
|
||||
op.create_index(
|
||||
"uq_product_variants_product_name",
|
||||
"product_variants",
|
||||
[sa.text("product_id"), sa.text("lower(name)")],
|
||||
unique=True,
|
||||
)
|
||||
|
||||
# --- Alter products ---
|
||||
op.add_column("products", sa.Column("arbeitspaket", sa.String(500), nullable=True))
|
||||
|
||||
# Drop existing unique constraint on pim_id — PIM-ID is a class-level
|
||||
# identifier shared by many products so it must NOT be unique.
|
||||
op.drop_constraint("products_pim_id_key", "products", type_="unique")
|
||||
op.create_index(
|
||||
"uq_products_produkt_baureihe",
|
||||
"products",
|
||||
[sa.text("lower(produkt_baureihe)")],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("produkt_baureihe IS NOT NULL AND is_active = true"),
|
||||
)
|
||||
|
||||
# --- Alter order_lines ---
|
||||
op.add_column(
|
||||
"order_lines",
|
||||
sa.Column(
|
||||
"variant_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("product_variants.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
|
||||
# --- Backfill: create default variants for existing products ---
|
||||
op.execute("""
|
||||
INSERT INTO product_variants (id, product_id, name, components, is_default, source_excel, created_at, updated_at)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
p.id,
|
||||
COALESCE(p.name, p.pim_id),
|
||||
COALESCE(p.components, '[]'::jsonb),
|
||||
true,
|
||||
p.source_excel,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM products p
|
||||
WHERE p.name IS NOT NULL OR p.pim_id IS NOT NULL
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("order_lines", "variant_id")
|
||||
op.drop_index("uq_products_produkt_baureihe", "products")
|
||||
op.create_unique_constraint("products_pim_id_key", "products", ["pim_id"])
|
||||
op.drop_column("products", "arbeitspaket")
|
||||
op.drop_index("uq_product_variants_product_name", "product_variants")
|
||||
op.drop_table("product_variants")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Fix order_line unique constraints to include variant_id
|
||||
|
||||
The old constraints (order_id, product_id) caused 409 errors when
|
||||
multiple variants of the same product were added to the same order.
|
||||
New constraints use COALESCE(variant_id, nil_uuid) so different
|
||||
variants of the same product can coexist.
|
||||
|
||||
Revision ID: 023
|
||||
Revises: 022
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "023"
|
||||
down_revision = "022"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Drop old constraints
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
# Recreate with variant_id included (COALESCE handles NULLs)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
f"ON order_lines (order_id, product_id, COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id, "
|
||||
f"COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
# Restore original constraints without variant_id
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Add lighting_only column to render_templates
|
||||
|
||||
When lighting_only=True the render script uses the template's World/HDRI for
|
||||
lighting but always computes an auto-camera for product framing. This is
|
||||
useful for HDR-only templates that don't define a fixed camera angle.
|
||||
|
||||
Revision ID: 024
|
||||
Revises: 023
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '024'
|
||||
down_revision = '023'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'render_templates',
|
||||
sa.Column(
|
||||
'lighting_only',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default='false',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('render_templates', 'lighting_only')
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Add cycles_device column to output_types
|
||||
|
||||
Revision ID: 025
|
||||
Revises: 024
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '025'
|
||||
down_revision = '024'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'output_types',
|
||||
sa.Column('cycles_device', sa.String(10), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('output_types', 'cycles_device')
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Add shadow_catcher_enabled to render_templates.
|
||||
|
||||
Revision ID: 026
|
||||
Revises: 025
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '026'
|
||||
down_revision = '025'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'render_templates',
|
||||
sa.Column('shadow_catcher_enabled', sa.Boolean(), nullable=False,
|
||||
server_default='false'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('render_templates', 'shadow_catcher_enabled')
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Remove product variant system — products are unique, no variant concept.
|
||||
|
||||
Revision ID: 027
|
||||
Revises: 026
|
||||
Create Date: 2026-03-03
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "027"
|
||||
down_revision = "026"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Drop variant-aware unique indexes on order_lines
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
# Drop variant_id column from order_lines
|
||||
op.execute("ALTER TABLE order_lines DROP COLUMN IF EXISTS variant_id")
|
||||
|
||||
# Deduplicate tracking-only lines (output_type_id IS NULL) — keep the newest row per
|
||||
# (order_id, product_id) pair so the unique index can be created cleanly.
|
||||
op.execute("""
|
||||
DELETE FROM order_lines
|
||||
WHERE output_type_id IS NULL
|
||||
AND id NOT IN (
|
||||
SELECT DISTINCT ON (order_id, product_id) id
|
||||
FROM order_lines
|
||||
WHERE output_type_id IS NULL
|
||||
ORDER BY order_id, product_id, created_at DESC
|
||||
)
|
||||
""")
|
||||
|
||||
# Deduplicate render lines — keep the newest row per (order_id, product_id, output_type_id).
|
||||
op.execute("""
|
||||
DELETE FROM order_lines
|
||||
WHERE output_type_id IS NOT NULL
|
||||
AND id NOT IN (
|
||||
SELECT DISTINCT ON (order_id, product_id, output_type_id) id
|
||||
FROM order_lines
|
||||
WHERE output_type_id IS NOT NULL
|
||||
ORDER BY order_id, product_id, output_type_id, created_at DESC
|
||||
)
|
||||
""")
|
||||
|
||||
# Recreate simpler unique indexes without variant_id
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
# Drop product_variants table (CASCADE removes its indexes automatically)
|
||||
op.execute("DROP TABLE IF EXISTS product_variants CASCADE")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Recreate product_variants table
|
||||
op.execute("""
|
||||
CREATE TABLE product_variants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
name VARCHAR(500) NOT NULL,
|
||||
gewuenschte_bildnummer VARCHAR(500),
|
||||
components JSONB NOT NULL DEFAULT '[]',
|
||||
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||
source_excel VARCHAR(1000),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_product_variants_product_name "
|
||||
"ON product_variants (product_id, lower(name))"
|
||||
)
|
||||
op.execute("CREATE INDEX ON product_variants (product_id)")
|
||||
|
||||
# Add back variant_id to order_lines
|
||||
op.execute(
|
||||
"ALTER TABLE order_lines ADD COLUMN variant_id UUID "
|
||||
"REFERENCES product_variants(id) ON DELETE SET NULL"
|
||||
)
|
||||
|
||||
# Drop simple indexes and restore variant-aware indexes
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
f"ON order_lines (order_id, product_id, COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id, "
|
||||
f"COALESCE(variant_id, '{NIL_UUID}'::uuid)) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Add product_render_positions table and render_position_id on order_lines.
|
||||
|
||||
Revision ID: 028
|
||||
Revises: 027
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "028"
|
||||
down_revision = "027"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
NIL_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── New table: product_render_positions ──────────────────────────────────
|
||||
op.execute("""
|
||||
CREATE TABLE product_render_positions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
rotation_x DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
rotation_y DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
rotation_z DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_render_positions_product_name "
|
||||
"ON product_render_positions (product_id, lower(name))"
|
||||
)
|
||||
op.execute("CREATE INDEX ix_render_positions_product_id ON product_render_positions (product_id)")
|
||||
|
||||
# ── Add render_position_id to order_lines ────────────────────────────────
|
||||
op.execute(
|
||||
"ALTER TABLE order_lines ADD COLUMN render_position_id UUID "
|
||||
"REFERENCES product_render_positions(id) ON DELETE SET NULL"
|
||||
)
|
||||
|
||||
# ── Update unique indexes to include position ─────────────────────────────
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
op.execute(
|
||||
f"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
f"ON order_lines (order_id, product_id, COALESCE(render_position_id, '{NIL_UUID}'::uuid)) "
|
||||
f"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
f"ON order_lines (order_id, product_id, output_type_id, "
|
||||
f"COALESCE(render_position_id, '{NIL_UUID}'::uuid)) "
|
||||
f"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Restore original unique indexes (without position)
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_tracking")
|
||||
op.execute("DROP INDEX IF EXISTS uq_order_lines_render")
|
||||
|
||||
op.execute("ALTER TABLE order_lines DROP COLUMN IF EXISTS render_position_id")
|
||||
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_tracking "
|
||||
"ON order_lines (order_id, product_id) "
|
||||
"WHERE output_type_id IS NULL"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX uq_order_lines_render "
|
||||
"ON order_lines (order_id, product_id, output_type_id) "
|
||||
"WHERE output_type_id IS NOT NULL"
|
||||
)
|
||||
|
||||
op.execute("DROP TABLE IF EXISTS product_render_positions CASCADE")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Seed default render positions (3/4 Front + 3/4 Rear) for all existing products.
|
||||
|
||||
Revision ID: 029
|
||||
Revises: 028
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "029"
|
||||
down_revision = "028"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Insert two default positions for every product that currently has none.
|
||||
# The CTE guarantees both rows are inserted for the same set of products.
|
||||
op.execute("""
|
||||
WITH products_without_positions AS (
|
||||
SELECT p.id AS product_id
|
||||
FROM products p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM product_render_positions rp WHERE rp.product_id = p.id
|
||||
)
|
||||
)
|
||||
INSERT INTO product_render_positions
|
||||
(id, product_id, name, rotation_x, rotation_y, rotation_z,
|
||||
is_default, sort_order, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), product_id,
|
||||
'3/4 Front', -15.0, 45.0, 0.0, true, 0, NOW(), NOW()
|
||||
FROM products_without_positions
|
||||
UNION ALL
|
||||
SELECT gen_random_uuid(), product_id,
|
||||
'3/4 Rear', -15.0, -135.0, 0.0, false, 1, NOW(), NOW()
|
||||
FROM products_without_positions
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Remove positions named exactly '3/4 Front' or '3/4 Rear'
|
||||
# where they are the only two positions on that product (i.e. seeded ones).
|
||||
op.execute("""
|
||||
DELETE FROM product_render_positions
|
||||
WHERE name IN ('3/4 Front', '3/4 Rear')
|
||||
AND product_id IN (
|
||||
SELECT product_id
|
||||
FROM product_render_positions
|
||||
GROUP BY product_id
|
||||
HAVING COUNT(*) = 2
|
||||
AND bool_or(name = '3/4 Front')
|
||||
AND bool_or(name = '3/4 Rear')
|
||||
)
|
||||
""")
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Seed 'Default' (unrotated) render position for all existing products.
|
||||
|
||||
Revision ID: 030
|
||||
Revises: 029
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "030"
|
||||
down_revision = "029"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Add 'Default' (0°/0°/0°) to every product that doesn't already have it.
|
||||
op.execute("""
|
||||
INSERT INTO product_render_positions
|
||||
(id, product_id, name, rotation_x, rotation_y, rotation_z,
|
||||
is_default, sort_order, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), p.id,
|
||||
'Default', 0.0, 0.0, 0.0, false, 2, NOW(), NOW()
|
||||
FROM products p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM product_render_positions rp
|
||||
WHERE rp.product_id = p.id
|
||||
AND lower(rp.name) = 'default'
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
DELETE FROM product_render_positions
|
||||
WHERE lower(name) = 'default'
|
||||
AND rotation_x = 0.0
|
||||
AND rotation_y = 0.0
|
||||
AND rotation_z = 0.0
|
||||
""")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Add camera_orbit to render_templates
|
||||
|
||||
Revision ID: 031_camera_orbit
|
||||
Revises: 030_seed_default_position
|
||||
Create Date: 2026-03-04
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '031'
|
||||
down_revision = '030'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'render_templates',
|
||||
sa.Column('camera_orbit', sa.Boolean(), nullable=False,
|
||||
server_default='true'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('render_templates', 'camera_orbit')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user