feat: Add currency and unit support across models, routes, and templates; enhance UI for consumption, costs, and production
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
31
routes/ui.py
31
routes/ui.py
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user