Dockerized web app (FastAPI + React + PostgreSQL) for scoping client ratecards against the GMAL master asset database. Features: - GMAL data ingestion from Excel (390 assets, 120 roles, 5 model types) - AI-powered document parsing and asset extraction (Claude Opus 4.6) - AI matching engine with parallel batching, confidence scoring, caveats - Ratecard builder with hours x volume calculation - Excel and PDF export - GMAL browser and inline editor - AI cost tracking per project (persisted to DB) - Debug panel for AI call inspection - Dark theme UI with gold (#FFC407) accent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
4.6 KiB
Python
110 lines
4.6 KiB
Python
import enum
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import String, Text, Integer, Numeric, Boolean, DateTime, Enum, ForeignKey, UniqueConstraint, Index
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class ModelType(str, enum.Enum):
|
|
CURRENT_OPLUS = "current_oplus"
|
|
AI_OPLUS = "ai_oplus"
|
|
CURRENT_LOCAL = "current_local"
|
|
AI_LOCAL = "ai_local"
|
|
ASSET_FACTORY = "asset_factory"
|
|
|
|
|
|
# Map spreadsheet header text -> enum
|
|
MODEL_TYPE_MAP = {
|
|
"Current Model - O+ Market": ModelType.CURRENT_OPLUS,
|
|
"AI Model - O+ Market": ModelType.AI_OPLUS,
|
|
"Current Model - Local Market": ModelType.CURRENT_LOCAL,
|
|
"AI Model - Local Market": ModelType.AI_LOCAL,
|
|
"Asset Factory Model": ModelType.ASSET_FACTORY,
|
|
}
|
|
|
|
|
|
class GmalAsset(Base):
|
|
__tablename__ = "gmal_assets"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
gmal_id: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, index=True)
|
|
swop_asset_id: Mapped[str | None] = mapped_column(String(50))
|
|
region: Mapped[str | None] = mapped_column(String(20))
|
|
category: Mapped[str | None] = mapped_column(String(100))
|
|
sub_category: Mapped[str | None] = mapped_column(String(100), index=True)
|
|
sub_category_description: Mapped[str | None] = mapped_column(Text)
|
|
asset_name: Mapped[str | None] = mapped_column(String(255))
|
|
complexity_level: Mapped[int | None] = mapped_column(Integer)
|
|
complexity_name: Mapped[str | None] = mapped_column(String(20))
|
|
unique_name: Mapped[str | None] = mapped_column(String(255))
|
|
asset_description: Mapped[str | None] = mapped_column(Text)
|
|
complexity_description: Mapped[str | None] = mapped_column(Text)
|
|
caveats: Mapped[str | None] = mapped_column(Text)
|
|
ai_enhanced_description: Mapped[str | None] = mapped_column(Text)
|
|
master_adapt: Mapped[str | None] = mapped_column(String(20))
|
|
ai_efficiency_pct: Mapped[float | None] = mapped_column(Numeric(5, 2))
|
|
has_hour_routes: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
hours: Mapped[list["GmalHours"]] = relationship(back_populates="asset", cascade="all, delete-orphan")
|
|
|
|
|
|
class Role(Base):
|
|
__tablename__ = "roles"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
discipline: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
|
role_title: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
entity: Mapped[str | None] = mapped_column(String(100))
|
|
resource_location: Mapped[str | None] = mapped_column(String(50))
|
|
unique_name: Mapped[str | None] = mapped_column(String(255))
|
|
sort_order: Mapped[int | None] = mapped_column(Integer)
|
|
is_programme_role: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("role_title", "entity", name="uq_role_title_entity"),
|
|
)
|
|
|
|
hours: Mapped[list["GmalHours"]] = relationship(back_populates="role")
|
|
|
|
|
|
class GmalHours(Base):
|
|
__tablename__ = "gmal_hours"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
gmal_asset_id: Mapped[int] = mapped_column(ForeignKey("gmal_assets.id"), nullable=False)
|
|
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), nullable=False)
|
|
model_type: Mapped[ModelType] = mapped_column(Enum(ModelType), nullable=False)
|
|
hours: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
|
|
|
|
asset: Mapped["GmalAsset"] = relationship(back_populates="hours")
|
|
role: Mapped["Role"] = relationship(back_populates="hours")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("gmal_asset_id", "role_id", "model_type", name="uq_gmal_role_model"),
|
|
Index("idx_gmal_hours_asset", "gmal_asset_id"),
|
|
Index("idx_gmal_hours_model", "model_type"),
|
|
)
|
|
|
|
|
|
class GmalServiceLine(Base):
|
|
__tablename__ = "gmal_service_lines"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
number: Mapped[str | None] = mapped_column(String(20))
|
|
name: Mapped[str | None] = mapped_column(String(255))
|
|
type: Mapped[str | None] = mapped_column(String(50))
|
|
gmal_id: Mapped[str | None] = mapped_column(String(50), index=True)
|
|
|
|
|
|
class RoleLevelMapping(Base):
|
|
__tablename__ = "role_level_mappings"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
role_name: Mapped[str | None] = mapped_column(String(200))
|
|
number: Mapped[str | None] = mapped_column(String(20))
|
|
level_name: Mapped[str | None] = mapped_column(String(200))
|
|
type: Mapped[str | None] = mapped_column(String(50))
|