feat: Add Processing Opex functionality
- Introduced OpexValidationError for handling validation errors in processing opex calculations. - Implemented ProjectProcessingOpexRepository and ScenarioProcessingOpexRepository for managing project and scenario-level processing opex snapshots. - Enhanced UnitOfWork to include repositories for processing opex. - Updated sidebar navigation and scenario detail templates to include links to the new Processing Opex Planner. - Created a new template for the Processing Opex Planner with form handling for input components and parameters. - Developed integration tests for processing opex calculations, covering HTML and JSON flows, including validation for currency mismatches and unsupported frequencies. - Added unit tests for the calculation logic, ensuring correct handling of various scenarios and edge cases.
This commit is contained in:
@@ -17,10 +17,12 @@ from models import (
|
||||
PricingSettings,
|
||||
ProjectCapexSnapshot,
|
||||
ProjectProfitability,
|
||||
ProjectProcessingOpexSnapshot,
|
||||
Role,
|
||||
Scenario,
|
||||
ScenarioCapexSnapshot,
|
||||
ScenarioProfitability,
|
||||
ScenarioProcessingOpexSnapshot,
|
||||
ScenarioStatus,
|
||||
SimulationParameter,
|
||||
User,
|
||||
@@ -571,6 +573,110 @@ class ScenarioCapexRepository:
|
||||
self.session.delete(entity)
|
||||
|
||||
|
||||
class ProjectProcessingOpexRepository:
|
||||
"""Persistence operations for project-level processing opex snapshots."""
|
||||
|
||||
def __init__(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
def create(
|
||||
self, snapshot: ProjectProcessingOpexSnapshot
|
||||
) -> ProjectProcessingOpexSnapshot:
|
||||
self.session.add(snapshot)
|
||||
self.session.flush()
|
||||
return snapshot
|
||||
|
||||
def list_for_project(
|
||||
self,
|
||||
project_id: int,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
) -> Sequence[ProjectProcessingOpexSnapshot]:
|
||||
stmt = (
|
||||
select(ProjectProcessingOpexSnapshot)
|
||||
.where(ProjectProcessingOpexSnapshot.project_id == project_id)
|
||||
.order_by(ProjectProcessingOpexSnapshot.calculated_at.desc())
|
||||
)
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
return self.session.execute(stmt).scalars().all()
|
||||
|
||||
def latest_for_project(
|
||||
self,
|
||||
project_id: int,
|
||||
) -> ProjectProcessingOpexSnapshot | None:
|
||||
stmt = (
|
||||
select(ProjectProcessingOpexSnapshot)
|
||||
.where(ProjectProcessingOpexSnapshot.project_id == project_id)
|
||||
.order_by(ProjectProcessingOpexSnapshot.calculated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return self.session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
def delete(self, snapshot_id: int) -> None:
|
||||
stmt = select(ProjectProcessingOpexSnapshot).where(
|
||||
ProjectProcessingOpexSnapshot.id == snapshot_id
|
||||
)
|
||||
entity = self.session.execute(stmt).scalar_one_or_none()
|
||||
if entity is None:
|
||||
raise EntityNotFoundError(
|
||||
f"Project processing opex snapshot {snapshot_id} not found"
|
||||
)
|
||||
self.session.delete(entity)
|
||||
|
||||
|
||||
class ScenarioProcessingOpexRepository:
|
||||
"""Persistence operations for scenario-level processing opex snapshots."""
|
||||
|
||||
def __init__(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
def create(
|
||||
self, snapshot: ScenarioProcessingOpexSnapshot
|
||||
) -> ScenarioProcessingOpexSnapshot:
|
||||
self.session.add(snapshot)
|
||||
self.session.flush()
|
||||
return snapshot
|
||||
|
||||
def list_for_scenario(
|
||||
self,
|
||||
scenario_id: int,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
) -> Sequence[ScenarioProcessingOpexSnapshot]:
|
||||
stmt = (
|
||||
select(ScenarioProcessingOpexSnapshot)
|
||||
.where(ScenarioProcessingOpexSnapshot.scenario_id == scenario_id)
|
||||
.order_by(ScenarioProcessingOpexSnapshot.calculated_at.desc())
|
||||
)
|
||||
if limit is not None:
|
||||
stmt = stmt.limit(limit)
|
||||
return self.session.execute(stmt).scalars().all()
|
||||
|
||||
def latest_for_scenario(
|
||||
self,
|
||||
scenario_id: int,
|
||||
) -> ScenarioProcessingOpexSnapshot | None:
|
||||
stmt = (
|
||||
select(ScenarioProcessingOpexSnapshot)
|
||||
.where(ScenarioProcessingOpexSnapshot.scenario_id == scenario_id)
|
||||
.order_by(ScenarioProcessingOpexSnapshot.calculated_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return self.session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
def delete(self, snapshot_id: int) -> None:
|
||||
stmt = select(ScenarioProcessingOpexSnapshot).where(
|
||||
ScenarioProcessingOpexSnapshot.id == snapshot_id
|
||||
)
|
||||
entity = self.session.execute(stmt).scalar_one_or_none()
|
||||
if entity is None:
|
||||
raise EntityNotFoundError(
|
||||
f"Scenario processing opex snapshot {snapshot_id} not found"
|
||||
)
|
||||
self.session.delete(entity)
|
||||
|
||||
|
||||
class FinancialInputRepository:
|
||||
"""Persistence operations for FinancialInput entities."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user