Files
HartOMat/backend/app/domains/orders/models.py
T
Hartmut 9d6def84c1 feat: per-order-line material override — override materials for individual renders
- Add `material_override` nullable column on OrderLine (DB migration)
- Line override takes priority over OutputType override
- PATCH /orders/{id}/lines/{id} endpoint to update material_override
- Inline dropdown on each order line in the OrderDetail page
- Amber background when override is active
- Same output type, different material per line — no need to create a new output type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:33:00 +01:00

172 lines
8.9 KiB
Python

import uuid
import enum
from datetime import datetime
from decimal import Decimal
from sqlalchemy import String, DateTime, Enum as SAEnum, ForeignKey, Text, Integer, Boolean, Numeric
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, JSONB
from app.database import Base
class OrderStatus(str, enum.Enum):
draft = "draft"
submitted = "submitted"
processing = "processing"
completed = "completed"
rejected = "rejected"
class Order(Base):
__tablename__ = "orders"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
order_number: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
template_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("templates.id"), nullable=True)
status: Mapped[OrderStatus] = mapped_column(SAEnum(OrderStatus), default=OrderStatus.draft, nullable=False)
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
source_excel: Mapped[str] = mapped_column(String(1000), nullable=True)
notes: Mapped[str] = mapped_column(Text, nullable=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)
submitted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
processing_started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
rejected_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
rejection_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
estimated_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
tenant_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=True, index=True
)
template: Mapped["Template"] = relationship("Template", back_populates="orders")
created_by_user: Mapped["User"] = relationship("User", back_populates="orders", foreign_keys=[created_by])
tenant: Mapped["Tenant | None"] = relationship("Tenant", back_populates="orders", lazy="noload")
items: Mapped[list["OrderItem"]] = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
lines: Mapped[list["OrderLine"]] = relationship(
"OrderLine", back_populates="order", cascade="all, delete-orphan"
)
class ItemStatus(str, enum.Enum):
pending = "pending"
approved = "approved"
rejected = "rejected"
class AIValidationStatus(str, enum.Enum):
not_started = "not_started"
pending = "pending"
completed = "completed"
failed = "failed"
class OrderItem(Base):
__tablename__ = "order_items"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
order_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("orders.id"), nullable=False)
row_index: Mapped[int] = mapped_column(Integer, nullable=False)
# 11 Standard fields (columns 0-10, skip col 5)
ebene1: Mapped[str] = mapped_column(String(500), nullable=True)
ebene2: Mapped[str] = mapped_column(String(500), nullable=True)
baureihe: Mapped[str] = mapped_column(String(500), nullable=True)
pim_id: Mapped[str] = mapped_column(String(500), nullable=True)
produkt_baureihe: Mapped[str] = mapped_column(String(500), nullable=True)
# col 5 is skipped (separator)
gewaehltes_produkt: Mapped[str] = mapped_column(String(500), nullable=True)
name_cad_modell: Mapped[str] = mapped_column(String(500), nullable=True)
gewuenschte_bildnummer: Mapped[str] = mapped_column(String(500), nullable=True)
lagertyp: Mapped[str] = mapped_column(String(500), nullable=True)
medias_rendering: Mapped[bool] = mapped_column(Boolean, nullable=True)
# Component pairs (cols 11+): [{part_name, material, component_type, column_index}]
components: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
# CAD linkage
cad_file_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("cad_files.id"), nullable=True)
thumbnail_path: Mapped[str] = mapped_column(String(1000), nullable=True)
# Material assignments per CAD part: [{part_name, material}]
cad_part_materials: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
# AI validation
ai_validation_status: Mapped[AIValidationStatus] = mapped_column(
SAEnum(AIValidationStatus), default=AIValidationStatus.not_started, nullable=False
)
ai_validation_result: Mapped[dict] = mapped_column(JSONB, nullable=True)
item_status: Mapped[ItemStatus] = mapped_column(SAEnum(ItemStatus), default=ItemStatus.pending, nullable=False)
notes: Mapped[str] = mapped_column(Text, 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: Mapped["Order"] = relationship("Order", back_populates="items")
cad_file: Mapped["CadFile"] = relationship("CadFile", back_populates="order_items")
@property
def cad_parsed_objects(self) -> list[str] | None:
"""Part names extracted from the linked STEP file, for Pydantic serialization."""
if self.cad_file and self.cad_file.parsed_objects:
return self.cad_file.parsed_objects.get("objects") or []
return None
class OrderLine(Base):
__tablename__ = "order_lines"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
order_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("orders.id", ondelete="CASCADE"), nullable=False, index=True
)
product_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("products.id"), nullable=False, index=True
)
output_type_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("output_types.id"), nullable=True
)
gewuenschte_bildnummer: Mapped[str | None] = mapped_column(String(500), nullable=True)
item_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
render_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
result_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
render_log: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
render_job_doc: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
ai_validation_status: Mapped[str] = mapped_column(String(20), nullable=False, default="not_started")
ai_validation_result: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
flamenco_job_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
render_backend_used: Mapped[str | None] = mapped_column(String(20), nullable=True)
render_started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
render_completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
unit_price: Mapped[Decimal | None] = mapped_column(Numeric(10, 2), nullable=True)
render_position_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("product_render_positions.id", ondelete="SET NULL"),
nullable=True,
)
global_render_position_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("global_render_positions.id", ondelete="SET NULL"),
nullable=True,
)
material_override: Mapped[str | None] = mapped_column(String(200), nullable=True, default=None)
notes: Mapped[str | None] = mapped_column(Text, 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: Mapped["Order"] = relationship("Order", back_populates="lines")
product: Mapped["Product"] = relationship("Product", back_populates="order_lines")
output_type: Mapped["OutputType | None"] = relationship("OutputType", back_populates="order_lines")
render_position: Mapped["ProductRenderPosition | None"] = relationship(
"ProductRenderPosition", back_populates="order_lines"
)
global_render_position: Mapped["GlobalRenderPosition | None"] = relationship(
"GlobalRenderPosition", back_populates="order_lines"
)