feat: Add currency and unit support across models, routes, and templates; enhance UI for consumption, costs, and production

This commit is contained in:
2025-10-21 09:53:04 +02:00
parent 139ae04538
commit fcea39deb0
18 changed files with 354 additions and 31 deletions

View File

@@ -1,7 +1,7 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, status
from pydantic import BaseModel, ConfigDict, PositiveFloat
from pydantic import BaseModel, ConfigDict, PositiveFloat, field_validator
from sqlalchemy.orm import Session
from models.consumption import Consumption
@@ -9,10 +9,22 @@ from routes.dependencies import get_db
router = APIRouter(prefix="/api/consumption", tags=["Consumption"])
class ConsumptionBase(BaseModel):
scenario_id: int
amount: PositiveFloat
description: Optional[str] = None
unit_name: Optional[str] = None
unit_symbol: Optional[str] = None
@field_validator("unit_name", "unit_symbol")
@classmethod
def _normalize_text(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
stripped = value.strip()
return stripped or None
class ConsumptionCreate(ConsumptionBase):

View File

@@ -1,7 +1,7 @@
from typing import List, Optional
from fastapi import APIRouter, Depends
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, field_validator
from sqlalchemy.orm import Session
from models.capex import Capex
@@ -9,28 +9,36 @@ from models.opex import Opex
from routes.dependencies import get_db
router = APIRouter(prefix="/api/costs", tags=["Costs"])
# Pydantic schemas for Capex
# Pydantic schemas for CAPEX and OPEX
class CapexCreate(BaseModel):
class _CostBase(BaseModel):
scenario_id: int
amount: float
description: Optional[str] = None
currency_code: str = "USD"
@field_validator("currency_code")
@classmethod
def _normalize_currency(cls, value: Optional[str]) -> str:
code = (value or "USD").strip().upper()
return code[:3] if len(code) > 3 else code
class CapexRead(CapexCreate):
class CapexCreate(_CostBase):
pass
class CapexRead(_CostBase):
id: int
model_config = ConfigDict(from_attributes=True)
# Pydantic schemas for Opex
class OpexCreate(BaseModel):
scenario_id: int
amount: float
description: Optional[str] = None
class OpexCreate(_CostBase):
pass
class OpexRead(OpexCreate):
class OpexRead(_CostBase):
id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,7 +1,7 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, status
from pydantic import BaseModel, ConfigDict, PositiveFloat
from pydantic import BaseModel, ConfigDict, PositiveFloat, field_validator
from sqlalchemy.orm import Session
from models.production_output import ProductionOutput
@@ -9,10 +9,22 @@ from routes.dependencies import get_db
router = APIRouter(prefix="/api/production", tags=["Production"])
class ProductionOutputBase(BaseModel):
scenario_id: int
amount: PositiveFloat
description: Optional[str] = None
unit_name: Optional[str] = None
unit_symbol: Optional[str] = None
@field_validator("unit_name", "unit_symbol")
@classmethod
def _normalize_text(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
stripped = value.strip()
return stripped or None
class ProductionOutputCreate(ProductionOutputBase):

View File

@@ -19,6 +19,24 @@ from models.simulation_result import SimulationResult
from routes.dependencies import get_db
from services.reporting import generate_report
CURRENCY_CHOICES: list[Dict[str, Any]] = [
{"id": "USD", "name": "US Dollar (USD)"},
{"id": "EUR", "name": "Euro (EUR)"},
{"id": "GBP", "name": "British Pound (GBP)"},
{"id": "CAD", "name": "Canadian Dollar (CAD)"},
{"id": "AUD", "name": "Australian Dollar (AUD)"},
]
MEASUREMENT_UNITS: list[Dict[str, Any]] = [
{"id": "tonnes", "name": "Tonnes", "symbol": "t"},
{"id": "kilograms", "name": "Kilograms", "symbol": "kg"},
{"id": "pounds", "name": "Pounds", "symbol": "lb"},
{"id": "liters", "name": "Liters", "symbol": "L"},
{"id": "cubic_meters", "name": "Cubic Meters", "symbol": "m3"},
{"id": "kilowatt_hours", "name": "Kilowatt Hours", "symbol": "kWh"},
]
router = APIRouter()
# Set up Jinja2 templates directory
@@ -96,6 +114,7 @@ def _load_costs(db: Session) -> Dict[str, Any]:
"scenario_id": int(getattr(capex, "scenario_id")),
"amount": float(getattr(capex, "amount", 0.0)),
"description": getattr(capex, "description", "") or "",
"currency_code": getattr(capex, "currency_code", "USD") or "USD",
}
)
@@ -111,6 +130,7 @@ def _load_costs(db: Session) -> Dict[str, Any]:
"scenario_id": int(getattr(opex, "scenario_id")),
"amount": float(getattr(opex, "amount", 0.0)),
"description": getattr(opex, "description", "") or "",
"currency_code": getattr(opex, "currency_code", "USD") or "USD",
}
)
@@ -131,12 +151,16 @@ def _load_consumption(db: Session) -> Dict[str, Any]:
scenario_id = int(getattr(record, "scenario_id"))
amount_value = float(getattr(record, "amount", 0.0))
description = getattr(record, "description", "") or ""
unit_name = getattr(record, "unit_name", None)
unit_symbol = getattr(record, "unit_symbol", None)
grouped[scenario_id].append(
{
"id": record_id,
"scenario_id": scenario_id,
"amount": amount_value,
"description": description,
"unit_name": unit_name,
"unit_symbol": unit_symbol,
}
)
return {"consumption_by_scenario": dict(grouped)}
@@ -153,12 +177,16 @@ def _load_production(db: Session) -> Dict[str, Any]:
scenario_id = int(getattr(record, "scenario_id"))
amount_value = float(getattr(record, "amount", 0.0))
description = getattr(record, "description", "") or ""
unit_name = getattr(record, "unit_name", None)
unit_symbol = getattr(record, "unit_symbol", None)
grouped[scenario_id].append(
{
"id": record_id,
"scenario_id": scenario_id,
"amount": amount_value,
"description": description,
"unit_name": unit_name,
"unit_symbol": unit_symbol,
}
)
return {"production_by_scenario": dict(grouped)}
@@ -543,6 +571,7 @@ async def costs_view(request: Request, db: Session = Depends(get_db)):
context: Dict[str, Any] = {}
context.update(_load_scenarios(db))
context.update(_load_costs(db))
context["currency_options"] = CURRENCY_CHOICES
return _render(request, "costs.html", context)
@@ -552,6 +581,7 @@ async def consumption_view(request: Request, db: Session = Depends(get_db)):
context: Dict[str, Any] = {}
context.update(_load_scenarios(db))
context.update(_load_consumption(db))
context["unit_options"] = MEASUREMENT_UNITS
return _render(request, "consumption.html", context)
@@ -561,6 +591,7 @@ async def production_view(request: Request, db: Session = Depends(get_db)):
context: Dict[str, Any] = {}
context.update(_load_scenarios(db))
context.update(_load_production(db))
context["unit_options"] = MEASUREMENT_UNITS
return _render(request, "production.html", context)