gmal-scope-builder/backend/app/models/gmal.py
DJP e18976fdb2 Initial commit - GMAL Scope Builder
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>
2026-03-27 17:35:14 -04:00

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))