Files
HartOMat/backend/app/domains/products/models.py
T
Hartmut 251dd703ed feat(B2): add tenant model + migrations 035/036 + RLS policies
Migration 035: tenants table with 'Schaeffler' default seed.
Migration 036: tenant_id FK on all tables, RLS policies, backfill.
New domains/tenants/ with CRUD router (admin only).
All domain models extended with tenant_id FK.
core/database.py: get_db_for_tenant with RLS context setter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 16:30:41 +01:00

105 lines
5.3 KiB
Python

import uuid
import enum
from datetime import datetime
from sqlalchemy import String, DateTime, Boolean, Text, ForeignKey, BigInteger, Enum as SAEnum, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, JSONB
from app.database import Base
class ProcessingStatus(str, enum.Enum):
pending = "pending"
processing = "processing"
completed = "completed"
failed = "failed"
class CadFile(Base):
__tablename__ = "cad_files"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
original_name: Mapped[str] = mapped_column(String(500), nullable=False)
stored_path: Mapped[str] = mapped_column(String(1000), nullable=False)
file_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
file_size: Mapped[int] = mapped_column(BigInteger, nullable=True)
parsed_objects: Mapped[dict] = mapped_column(JSONB, nullable=True)
thumbnail_path: Mapped[str] = mapped_column(String(1000), nullable=True)
gltf_path: Mapped[str] = mapped_column(String(1000), nullable=True)
processing_status: Mapped[ProcessingStatus] = mapped_column(
SAEnum(ProcessingStatus), default=ProcessingStatus.pending, nullable=False
)
error_message: Mapped[str] = mapped_column(String(2000), nullable=True)
render_log: Mapped[dict] = mapped_column(JSONB, nullable=True)
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
order_items: Mapped[list["OrderItem"]] = relationship("OrderItem", back_populates="cad_file")
products: Mapped[list["Product"]] = relationship("Product", back_populates="cad_file")
class Product(Base):
__tablename__ = "products"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
pim_id: Mapped[str] = mapped_column(String(500), nullable=False)
name: Mapped[str | None] = mapped_column(String(500), nullable=True)
category_key: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
ebene1: Mapped[str | None] = mapped_column(String(500), nullable=True)
ebene2: Mapped[str | None] = mapped_column(String(500), nullable=True)
baureihe: Mapped[str | None] = mapped_column(String(500), nullable=True)
produkt_baureihe: Mapped[str | None] = mapped_column(String(500), nullable=True)
lagertyp: Mapped[str | None] = mapped_column(String(500), nullable=True)
name_cad_modell: Mapped[str | None] = mapped_column(String(500), nullable=True, index=True)
gewuenschte_bildnummer: Mapped[str | None] = mapped_column(String(500), nullable=True)
medias_rendering: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
components: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
cad_part_materials: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
cad_file_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("cad_files.id", ondelete="SET NULL"), nullable=True
)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
arbeitspaket: Mapped[str | None] = mapped_column(String(500), nullable=True)
source_excel: Mapped[str | None] = mapped_column(String(1000), nullable=True)
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=True, index=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
cad_file: Mapped["CadFile | None"] = relationship("CadFile", back_populates="products")
tenant: Mapped["Tenant | None"] = relationship("Tenant", back_populates="products", lazy="noload")
order_lines: Mapped[list["OrderLine"]] = relationship(
"OrderLine", back_populates="product", cascade="all, delete-orphan"
)
render_positions: Mapped[list["ProductRenderPosition"]] = relationship(
"ProductRenderPosition", back_populates="product",
cascade="all, delete-orphan", order_by="ProductRenderPosition.sort_order"
)
@property
def thumbnail_url(self) -> str | None:
if self.cad_file and self.cad_file.thumbnail_path:
from pathlib import Path
return f"/thumbnails/{Path(self.cad_file.thumbnail_path).name}"
return None
@property
def processing_status(self) -> str | None:
if self.cad_file:
return self.cad_file.processing_status.value if hasattr(
self.cad_file.processing_status, 'value'
) else str(self.cad_file.processing_status)
return None
@property
def cad_parsed_objects(self) -> list[str] | None:
if self.cad_file and self.cad_file.parsed_objects:
return self.cad_file.parsed_objects.get("objects") or []
return None