feat: Implement currency management with models, routes, and UI updates; add backfill script for existing records

This commit is contained in:
2025-10-21 10:33:08 +02:00
parent fcea39deb0
commit 672cafa5b9
14 changed files with 478 additions and 10 deletions

View File

@@ -16,7 +16,8 @@ class _CostBase(BaseModel):
scenario_id: int
amount: float
description: Optional[str] = None
currency_code: str = "USD"
currency_code: Optional[str] = "USD"
currency_id: Optional[int] = None
@field_validator("currency_code")
@classmethod
@@ -31,8 +32,12 @@ class CapexCreate(_CostBase):
class CapexRead(_CostBase):
id: int
# use from_attributes so Pydantic reads attributes off SQLAlchemy model
model_config = ConfigDict(from_attributes=True)
# optionally include nested currency info
currency: Optional["CurrencyRead"] = None
class OpexCreate(_CostBase):
pass
@@ -41,12 +46,41 @@ class OpexCreate(_CostBase):
class OpexRead(_CostBase):
id: int
model_config = ConfigDict(from_attributes=True)
currency: Optional["CurrencyRead"] = None
class CurrencyRead(BaseModel):
id: int
code: str
name: Optional[str] = None
symbol: Optional[str] = None
is_active: Optional[bool] = True
model_config = ConfigDict(from_attributes=True)
# forward refs
CapexRead.model_rebuild()
OpexRead.model_rebuild()
# Capex endpoints
@router.post("/capex", response_model=CapexRead)
def create_capex(item: CapexCreate, db: Session = Depends(get_db)):
db_item = Capex(**item.model_dump())
payload = item.model_dump()
# Prefer explicit currency_id if supplied
cid = payload.get("currency_id")
if not cid:
code = (payload.pop("currency_code", "USD") or "USD").strip().upper()
currency_cls = __import__(
"models.currency", fromlist=["Currency"]).Currency
currency = db.query(currency_cls).filter_by(code=code).one_or_none()
if currency is None:
currency = currency_cls(code=code, name=code, symbol=None)
db.add(currency)
db.flush()
payload["currency_id"] = currency.id
db_item = Capex(**payload)
db.add(db_item)
db.commit()
db.refresh(db_item)
@@ -61,7 +95,19 @@ def list_capex(db: Session = Depends(get_db)):
# Opex endpoints
@router.post("/opex", response_model=OpexRead)
def create_opex(item: OpexCreate, db: Session = Depends(get_db)):
db_item = Opex(**item.model_dump())
payload = item.model_dump()
cid = payload.get("currency_id")
if not cid:
code = (payload.pop("currency_code", "USD") or "USD").strip().upper()
currency_cls = __import__(
"models.currency", fromlist=["Currency"]).Currency
currency = db.query(currency_cls).filter_by(code=code).one_or_none()
if currency is None:
currency = currency_cls(code=code, name=code, symbol=None)
db.add(currency)
db.flush()
payload["currency_id"] = currency.id
db_item = Opex(**payload)
db.add(db_item)
db.commit()
db.refresh(db_item)

17
routes/currencies.py Normal file
View File

@@ -0,0 +1,17 @@
from typing import List, Dict, Any
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from models.currency import Currency
from routes.dependencies import get_db
router = APIRouter(prefix="/api/currencies", tags=["Currencies"])
@router.get("/", response_model=List[Dict[str, Any]])
def list_currencies(db: Session = Depends(get_db)):
results = []
for c in db.query(Currency).filter_by(is_active=True).order_by(Currency.code).all():
results.append({"id": c.code, "name": f"{c.name} ({c.code})", "symbol": c.symbol})
return results

View File

@@ -18,11 +18,14 @@ from models.scenario import Scenario
from models.simulation_result import SimulationResult
from routes.dependencies import get_db
from services.reporting import generate_report
from models.currency import Currency
CURRENCY_CHOICES: list[Dict[str, Any]] = [
{"id": "USD", "name": "US Dollar (USD)"},
{"id": "EUR", "name": "Euro (EUR)"},
{"id": "CLP", "name": "Chilean Peso (CLP)"},
{"id": "RMB", "name": "Chinese Yuan (RMB)"},
{"id": "GBP", "name": "British Pound (GBP)"},
{"id": "CAD", "name": "Canadian Dollar (CAD)"},
{"id": "AUD", "name": "Australian Dollar (AUD)"},
@@ -140,6 +143,14 @@ def _load_costs(db: Session) -> Dict[str, Any]:
}
def _load_currencies(db: Session) -> Dict[str, Any]:
items: list[Dict[str, Any]] = []
for c in db.query(Currency).filter_by(is_active=True).order_by(Currency.code).all():
items.append(
{"id": c.code, "name": f"{c.name} ({c.code})", "symbol": c.symbol})
return {"currency_options": items}
def _load_consumption(db: Session) -> Dict[str, Any]:
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
for record in (
@@ -571,7 +582,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
context.update(_load_currencies(db))
return _render(request, "costs.html", context)