Ratecard lines now store total_hours as per-1-asset hours (= base_hours, linked to the GMAL row), with volume tracked separately. Aggregators (team_shape, ratecard summary, Excel matrix, in-app ratecard tab) multiply by volume themselves when computing total effort. Display behavior is preserved; storage semantics are clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.6 KiB
Python
76 lines
2.6 KiB
Python
"""Build ratecards from confirmed matches."""
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.gmal import GmalHours, ModelType
|
|
from app.models.project import Project, ClientAsset, Match, RatecardLine
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def build_ratecard(db: AsyncSession, project: Project) -> list[RatecardLine]:
|
|
"""Build a ratecard for a project from its confirmed (selected) matches.
|
|
|
|
For each selected match, looks up the hours per role from gmal_hours
|
|
for the project's model type, multiplies by volume, and creates ratecard lines.
|
|
|
|
Returns the created RatecardLine objects.
|
|
"""
|
|
# Clear existing ratecard lines for this project
|
|
existing = await db.execute(
|
|
select(RatecardLine).where(RatecardLine.project_id == project.id)
|
|
)
|
|
for line in existing.scalars().all():
|
|
await db.delete(line)
|
|
await db.flush()
|
|
|
|
# Get all client assets with their selected matches
|
|
assets_result = await db.execute(
|
|
select(ClientAsset).where(ClientAsset.project_id == project.id)
|
|
)
|
|
client_assets = assets_result.scalars().all()
|
|
|
|
lines = []
|
|
|
|
for client_asset in client_assets:
|
|
# Get the selected match for this asset
|
|
match_result = await db.execute(
|
|
select(Match).where(
|
|
Match.client_asset_id == client_asset.id,
|
|
Match.is_selected == True,
|
|
)
|
|
)
|
|
selected_match = match_result.scalars().first()
|
|
if not selected_match:
|
|
logger.warning(f"No selected match for client asset {client_asset.id}: {client_asset.raw_name}")
|
|
continue
|
|
|
|
# Get hours for this GMAL asset and model type
|
|
hours_result = await db.execute(
|
|
select(GmalHours).where(
|
|
GmalHours.gmal_asset_id == selected_match.gmal_asset_id,
|
|
GmalHours.model_type == project.model_type,
|
|
)
|
|
)
|
|
gmal_hours = hours_result.scalars().all()
|
|
|
|
for gh in gmal_hours:
|
|
base = round(float(gh.hours), 2)
|
|
line = RatecardLine(
|
|
project_id=project.id,
|
|
client_asset_id=client_asset.id,
|
|
gmal_asset_id=selected_match.gmal_asset_id,
|
|
role_id=gh.role_id,
|
|
base_hours=base,
|
|
volume=client_asset.volume,
|
|
total_hours=base,
|
|
)
|
|
db.add(line)
|
|
lines.append(line)
|
|
|
|
await db.flush()
|
|
logger.info(f"Built ratecard with {len(lines)} lines for project {project.id}")
|
|
return lines
|