feat: Implement initial capex calculation feature
- Added CapexComponentInput, CapexParameters, CapexCalculationRequest, CapexCalculationResult, and related schemas for capex calculations. - Introduced calculate_initial_capex function to aggregate capex components and compute totals and timelines. - Created ProjectCapexRepository and ScenarioCapexRepository for managing capex snapshots in the database. - Developed capex.html template for capturing and displaying initial capex data. - Registered common Jinja2 filters for formatting currency and percentages. - Implemented unit and integration tests for capex calculation functionality. - Updated unit of work to include new repositories for capex management.
This commit is contained in:
@@ -97,6 +97,106 @@ class ProfitabilityCalculationResult(BaseModel):
|
||||
currency: str | None
|
||||
|
||||
|
||||
class CapexComponentInput(BaseModel):
|
||||
"""Capex component entry supplied by the UI."""
|
||||
|
||||
id: int | None = Field(default=None, ge=1)
|
||||
name: str = Field(..., min_length=1)
|
||||
category: str = Field(..., min_length=1)
|
||||
amount: float = Field(..., ge=0)
|
||||
currency: str | None = Field(None, min_length=3, max_length=3)
|
||||
spend_year: int | None = Field(None, ge=0, le=120)
|
||||
notes: str | None = Field(None, max_length=500)
|
||||
|
||||
@field_validator("currency")
|
||||
@classmethod
|
||||
def _uppercase_currency(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.strip().upper()
|
||||
|
||||
@field_validator("category")
|
||||
@classmethod
|
||||
def _normalise_category(cls, value: str) -> str:
|
||||
return value.strip().lower()
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def _trim_name(cls, value: str) -> str:
|
||||
return value.strip()
|
||||
|
||||
|
||||
class CapexParameters(BaseModel):
|
||||
"""Global parameters applied to capex calculations."""
|
||||
|
||||
currency_code: str | None = Field(None, min_length=3, max_length=3)
|
||||
contingency_pct: float | None = Field(0, ge=0, le=100)
|
||||
discount_rate_pct: float | None = Field(None, ge=0, le=100)
|
||||
evaluation_horizon_years: int | None = Field(10, ge=1, le=100)
|
||||
|
||||
@field_validator("currency_code")
|
||||
@classmethod
|
||||
def _uppercase_currency(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.strip().upper()
|
||||
|
||||
|
||||
class CapexCalculationOptions(BaseModel):
|
||||
"""Optional behaviour flags for capex calculations."""
|
||||
|
||||
persist: bool = False
|
||||
|
||||
|
||||
class CapexCalculationRequest(BaseModel):
|
||||
"""Request payload for capex aggregation."""
|
||||
|
||||
components: List[CapexComponentInput] = Field(default_factory=list)
|
||||
parameters: CapexParameters = Field(
|
||||
default_factory=CapexParameters, # type: ignore[arg-type]
|
||||
)
|
||||
options: CapexCalculationOptions = Field(
|
||||
default_factory=CapexCalculationOptions, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
class CapexCategoryBreakdown(BaseModel):
|
||||
"""Breakdown entry describing category totals."""
|
||||
|
||||
category: str
|
||||
amount: float = Field(..., ge=0)
|
||||
share: float | None = Field(None, ge=0, le=100)
|
||||
|
||||
|
||||
class CapexTotals(BaseModel):
|
||||
"""Aggregated totals for capex workflows."""
|
||||
|
||||
overall: float = Field(..., ge=0)
|
||||
contingency_pct: float = Field(0, ge=0, le=100)
|
||||
contingency_amount: float = Field(..., ge=0)
|
||||
with_contingency: float = Field(..., ge=0)
|
||||
by_category: List[CapexCategoryBreakdown] = Field(default_factory=list)
|
||||
|
||||
|
||||
class CapexTimelineEntry(BaseModel):
|
||||
"""Spend profile entry grouped by year."""
|
||||
|
||||
year: int
|
||||
spend: float = Field(..., ge=0)
|
||||
cumulative: float = Field(..., ge=0)
|
||||
|
||||
|
||||
class CapexCalculationResult(BaseModel):
|
||||
"""Response body for capex calculations."""
|
||||
|
||||
totals: CapexTotals
|
||||
timeline: List[CapexTimelineEntry] = Field(default_factory=list)
|
||||
components: List[CapexComponentInput] = Field(default_factory=list)
|
||||
parameters: CapexParameters
|
||||
options: CapexCalculationOptions
|
||||
currency: str | None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ImpurityInput",
|
||||
"ProfitabilityCalculationRequest",
|
||||
@@ -104,5 +204,13 @@ __all__ = [
|
||||
"ProfitabilityMetrics",
|
||||
"CashFlowEntry",
|
||||
"ProfitabilityCalculationResult",
|
||||
"CapexComponentInput",
|
||||
"CapexParameters",
|
||||
"CapexCalculationOptions",
|
||||
"CapexCalculationRequest",
|
||||
"CapexCategoryBreakdown",
|
||||
"CapexTotals",
|
||||
"CapexTimelineEntry",
|
||||
"CapexCalculationResult",
|
||||
"ValidationError",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user