feat(planner): currency formatting by country (UK=£, US=$, EU=€)

Planner now renders currency symbol and thousands-separator style based
on the selected country:
  UK → £450,000  (pound, comma thousands)
  US → $450,000  (dollar, comma thousands)
  EU + SE → €450.000  (euro, dot thousands — unchanged)

Implementation:
- COUNTRY_CURRENCY mapping + CURRENCY_DEFAULT added to calculator.py;
  5 info-string annotations updated to use derived sym variable
- _fmt_currency, _fmt_k, _fmt_n Jinja2 filters now read g.currency_sym
  and g.currency_eu_style (safe EUR fallback via getattr)
- planner index + calculate routes set g.currency_* and pass
  currency_sym to template context before render
- 16 slider label locale keys updated: (€) → ({currency}) in both
  en.json and de.json; slider macro applies tformat(currency=…)
- businessplan.py: _fmt_eur renamed to _fmt_cur(n, sym, eu_style);
  get_plan_sections derives currency from state and binds a fmt lambda;
  capex/opex items gain formatted_amount field
- plan.html: inline € replaced with {{ item.formatted_amount }}

1017 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-21 01:51:46 +01:00
parent 0b4f2f2180
commit 5662a7dce3
8 changed files with 120 additions and 81 deletions

View File

@@ -23,19 +23,24 @@ def _detect_lang() -> str:
def _fmt_currency(n) -> str: def _fmt_currency(n) -> str:
"""Format as EUR with German-style thousands separator (€50.000).""" """Format currency using request-context symbol and locale style."""
sym = getattr(g, "currency_sym", "\u20ac")
eu_style = getattr(g, "currency_eu_style", True)
n = round(float(n)) n = round(float(n))
s = f"{abs(n):,}".replace(",", ".") s = f"{abs(n):,}"
return f"-\u20ac{s}" if n < 0 else f"\u20ac{s}" if eu_style:
s = s.replace(",", ".")
return f"-{sym}{s}" if n < 0 else f"{sym}{s}"
def _fmt_k(n) -> str: def _fmt_k(n) -> str:
"""Short currency: €50K, €1.2M, or full fmt_currency.""" """Short currency: €50K, €1.2M, or full _fmt_currency."""
sym = getattr(g, "currency_sym", "\u20ac")
n = float(n) n = float(n)
if abs(n) >= 1_000_000: if abs(n) >= 1_000_000:
return f"\u20ac{n/1_000_000:.1f}M" return f"{sym}{n/1_000_000:.1f}M"
if abs(n) >= 1_000: if abs(n) >= 1_000:
return f"\u20ac{n/1_000:.0f}K" return f"{sym}{n/1_000:.0f}K"
return _fmt_currency(n) return _fmt_currency(n)
@@ -50,8 +55,10 @@ def _fmt_x(n) -> str:
def _fmt_n(n) -> str: def _fmt_n(n) -> str:
"""Format integer with German thousands separator: 1.234.""" """Format integer with locale-aware thousands separator: 1.234 or 1,234."""
return f"{round(float(n)):,}".replace(",", ".") eu_style = getattr(g, "currency_eu_style", True)
s = f"{round(float(n)):,}"
return s.replace(",", ".") if eu_style else s
def _tformat(s: str, **kwargs) -> str: def _tformat(s: str, **kwargs) -> str:

View File

@@ -9,16 +9,20 @@ from pathlib import Path
from .core import fetch_one from .core import fetch_one
from .i18n import get_translations from .i18n import get_translations
from .planner.calculator import calc, validate_state from .planner.calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, calc, validate_state
TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan" TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan"
def _fmt_eur(n) -> str: def _fmt_cur(n, sym: str = "\u20ac", eu_style: bool = True) -> str:
"""Format number as EUR with thousands separator.""" """Format number as currency with locale-aware thousands separator."""
if n is None: if n is None:
return "-" return "-"
return f"\u20ac{n:,.0f}" v = round(float(n))
s = f"{abs(v):,}"
if eu_style:
s = s.replace(",", ".")
return f"-{sym}{s}" if v < 0 else f"{sym}{s}"
def _fmt_pct(n) -> str: def _fmt_pct(n) -> str:
@@ -44,16 +48,20 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
s = state s = state
t = get_translations(language) t = get_translations(language)
cur = COUNTRY_CURRENCY.get(s.get("country", "DE"), CURRENCY_DEFAULT)
sym, eu_style = cur["sym"], cur["eu_style"]
fmt = lambda n: _fmt_cur(n, sym, eu_style) # noqa: E731
venue_type = t["bp_indoor"] if s["venue"] == "indoor" else t["bp_outdoor"] venue_type = t["bp_indoor"] if s["venue"] == "indoor" else t["bp_outdoor"]
own_type = t["bp_own"] if s["own"] == "buy" else t["bp_rent"] own_type = t["bp_own"] if s["own"] == "buy" else t["bp_rent"]
payback_str = _fmt_months(d["paybackIdx"], t) payback_str = _fmt_months(d["paybackIdx"], t)
irr_str = _fmt_pct(d["irr"]) irr_str = _fmt_pct(d["irr"])
total_capex_str = _fmt_eur(d["capex"]) total_capex_str = fmt(d["capex"])
equity_str = _fmt_eur(d["equity"]) equity_str = fmt(d["equity"])
loan_str = _fmt_eur(d["loanAmount"]) loan_str = fmt(d["loanAmount"])
per_court_str = _fmt_eur(d["capexPerCourt"]) per_court_str = fmt(d["capexPerCourt"])
per_sqm_str = _fmt_eur(d["capexPerSqm"]) per_sqm_str = fmt(d["capexPerSqm"])
sections = { sections = {
"lang": language, "lang": language,
@@ -70,8 +78,8 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
"total_capex": total_capex_str, "total_capex": total_capex_str,
"equity": equity_str, "equity": equity_str,
"loan": loan_str, "loan": loan_str,
"y1_revenue": _fmt_eur(d["annuals"][0]["revenue"]) if d["annuals"] else "-", "y1_revenue": fmt(d["annuals"][0]["revenue"]) if d["annuals"] else "-",
"y3_ebitda": _fmt_eur(d["stabEbitda"]), "y3_ebitda": fmt(d["stabEbitda"]),
"irr": irr_str, "irr": irr_str,
"payback": payback_str, "payback": payback_str,
}, },
@@ -79,7 +87,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# Investment Plan (CAPEX) # Investment Plan (CAPEX)
"investment": { "investment": {
"heading": t["bp_investment"], "heading": t["bp_investment"],
"items": d["capexItems"], "items": [{**i, "formatted_amount": fmt(i["amount"])} for i in d["capexItems"]],
"total": total_capex_str, "total": total_capex_str,
"per_court": per_court_str, "per_court": per_court_str,
"per_sqm": per_sqm_str, "per_sqm": per_sqm_str,
@@ -88,20 +96,20 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
# Operating Costs # Operating Costs
"operations": { "operations": {
"heading": t["bp_operations"], "heading": t["bp_operations"],
"items": d["opexItems"], "items": [{**i, "formatted_amount": fmt(i["amount"])} for i in d["opexItems"]],
"monthly_total": _fmt_eur(d["opex"]), "monthly_total": fmt(d["opex"]),
"annual_total": _fmt_eur(d["annualOpex"]), "annual_total": fmt(d["annualOpex"]),
}, },
# Revenue Model # Revenue Model
"revenue": { "revenue": {
"heading": t["bp_revenue"], "heading": t["bp_revenue"],
"weighted_rate": _fmt_eur(d["weightedRate"]), "weighted_rate": fmt(d["weightedRate"]),
"utilization": _fmt_pct(s["utilTarget"] / 100), "utilization": _fmt_pct(s["utilTarget"] / 100),
"gross_monthly": _fmt_eur(d["grossRevMonth"]), "gross_monthly": fmt(d["grossRevMonth"]),
"net_monthly": _fmt_eur(d["netRevMonth"]), "net_monthly": fmt(d["netRevMonth"]),
"ebitda_monthly": _fmt_eur(d["ebitdaMonth"]), "ebitda_monthly": fmt(d["ebitdaMonth"]),
"net_cf_monthly": _fmt_eur(d["netCFMonth"]), "net_cf_monthly": fmt(d["netCFMonth"]),
}, },
# 5-Year P&L # 5-Year P&L
@@ -110,10 +118,10 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
"years": [ "years": [
{ {
"year": a["year"], "year": a["year"],
"revenue": _fmt_eur(a["revenue"]), "revenue": fmt(a["revenue"]),
"ebitda": _fmt_eur(a["ebitda"]), "ebitda": fmt(a["ebitda"]),
"debt_service": _fmt_eur(a["ds"]), "debt_service": fmt(a["ds"]),
"net_cf": _fmt_eur(a["ncf"]), "net_cf": fmt(a["ncf"]),
} }
for a in d["annuals"] for a in d["annuals"]
], ],
@@ -127,8 +135,8 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
"loan": loan_str, "loan": loan_str,
"interest_rate": f"{s['interestRate']}%", "interest_rate": f"{s['interestRate']}%",
"term": t["bp_years"].format(n=s["loanTerm"]), "term": t["bp_years"].format(n=s["loanTerm"]),
"monthly_payment": _fmt_eur(d["monthlyPayment"]), "monthly_payment": fmt(d["monthlyPayment"]),
"annual_debt_service": _fmt_eur(d["annualDebtService"]), "annual_debt_service": fmt(d["annualDebtService"]),
"ltv": _fmt_pct(d["ltv"]), "ltv": _fmt_pct(d["ltv"]),
}, },
@@ -151,12 +159,12 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
"months": [ "months": [
{ {
"month": m["m"], "month": m["m"],
"revenue": _fmt_eur(m["totalRev"]), "revenue": fmt(m["totalRev"]),
"opex": _fmt_eur(abs(m["opex"])), "opex": fmt(abs(m["opex"])),
"ebitda": _fmt_eur(m["ebitda"]), "ebitda": fmt(m["ebitda"]),
"debt": _fmt_eur(abs(m["loan"])), "debt": fmt(abs(m["loan"])),
"ncf": _fmt_eur(m["ncf"]), "ncf": fmt(m["ncf"]),
"cumulative": _fmt_eur(m["cum"]), "cumulative": fmt(m["cum"]),
} }
for m in d["months"][:12] for m in d["months"][:12]
], ],
@@ -220,6 +228,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
"debt": t["bp_lbl_debt"], "debt": t["bp_lbl_debt"],
"cumulative": t["bp_lbl_cumulative"], "cumulative": t["bp_lbl_cumulative"],
"disclaimer": t["bp_lbl_disclaimer"], "disclaimer": t["bp_lbl_disclaimer"],
"currency_sym": sym,
}, },
} }

View File

@@ -652,9 +652,9 @@
"sl_sqm_sgl_hall": "Hallen-m² pro Einzelplatz", "sl_sqm_sgl_hall": "Hallen-m² pro Einzelplatz",
"sl_sqm_dbl_outdoor": "Grundstück-m² pro Doppelplatz", "sl_sqm_dbl_outdoor": "Grundstück-m² pro Doppelplatz",
"sl_sqm_sgl_outdoor": "Grundstück-m² pro Einzelplatz", "sl_sqm_sgl_outdoor": "Grundstück-m² pro Einzelplatz",
"sl_rate_peak": "Spitzenstundensatz ()", "sl_rate_peak": "Spitzenstundensatz ({currency})",
"sl_rate_offpeak": "Nebenstundensatz ()", "sl_rate_offpeak": "Nebenstundensatz ({currency})",
"sl_rate_single": "Einzelplatz-Stundensatz ()", "sl_rate_single": "Einzelplatz-Stundensatz ({currency})",
"sl_peak_pct": "Anteil Spitzenstunden", "sl_peak_pct": "Anteil Spitzenstunden",
"sl_booking_fee": "Plattformprovision", "sl_booking_fee": "Plattformprovision",
"sl_util_target": "Ziel-Auslastung", "sl_util_target": "Ziel-Auslastung",
@@ -668,9 +668,9 @@
"sl_retail_rev": "Einzelhandel / Platz", "sl_retail_rev": "Einzelhandel / Platz",
"sl_court_cost_dbl": "Platzkosten — Doppel", "sl_court_cost_dbl": "Platzkosten — Doppel",
"sl_court_cost_sgl": "Platzkosten — Einzel", "sl_court_cost_sgl": "Platzkosten — Einzel",
"sl_hall_cost_sqm": "Hallenbau (/m²)", "sl_hall_cost_sqm": "Hallenbau ({currency}/m²)",
"sl_foundation_sqm": "Fundament (/m²)", "sl_foundation_sqm": "Fundament ({currency}/m²)",
"sl_land_price_sqm": "Grundstückspreis (/m²)", "sl_land_price_sqm": "Grundstückspreis ({currency}/m²)",
"sl_hvac": "Lüftung & Klimaanlage", "sl_hvac": "Lüftung & Klimaanlage",
"sl_electrical": "Elektro + Beleuchtung", "sl_electrical": "Elektro + Beleuchtung",
"sl_sanitary": "Sanitär / Umkleide", "sl_sanitary": "Sanitär / Umkleide",
@@ -680,7 +680,7 @@
"sl_hvac_upgrade": "Lüftungsausbau", "sl_hvac_upgrade": "Lüftungsausbau",
"sl_lighting_upgrade": "Beleuchtungsausbau", "sl_lighting_upgrade": "Beleuchtungsausbau",
"sl_fitout": "Ausbau & Empfang", "sl_fitout": "Ausbau & Empfang",
"sl_outdoor_foundation": "Beton (/m²)", "sl_outdoor_foundation": "Beton ({currency}/m²)",
"sl_outdoor_site_work": "Erschließung", "sl_outdoor_site_work": "Erschließung",
"sl_outdoor_lighting": "Beleuchtung pro Platz", "sl_outdoor_lighting": "Beleuchtung pro Platz",
"sl_outdoor_fencing": "Einzäunung", "sl_outdoor_fencing": "Einzäunung",
@@ -688,17 +688,17 @@
"sl_working_capital": "Betriebskapital", "sl_working_capital": "Betriebskapital",
"sl_contingency": "Reserve", "sl_contingency": "Reserve",
"sl_budget_target": "Dein Budgetziel", "sl_budget_target": "Dein Budgetziel",
"sl_rent_sqm": "Miete (/m²/Monat)", "sl_rent_sqm": "Miete ({currency}/m²/Monat)",
"sl_outdoor_rent": "Monatliche Grundstücksmiete", "sl_outdoor_rent": "Monatliche Grundstücksmiete",
"sl_property_tax": "Grundsteuer / Monat", "sl_property_tax": "Grundsteuer / Monat",
"sl_insurance": "Versicherung (/Monat)", "sl_insurance": "Versicherung ({currency}/Monat)",
"sl_electricity": "Strom (/Monat)", "sl_electricity": "Strom ({currency}/Monat)",
"sl_heating": "Heizung (/Monat)", "sl_heating": "Heizung ({currency}/Monat)",
"sl_water": "Wasser (/Monat)", "sl_water": "Wasser ({currency}/Monat)",
"sl_maintenance": "Wartung (/Monat)", "sl_maintenance": "Wartung ({currency}/Monat)",
"sl_cleaning": "Reinigung (/Monat)", "sl_cleaning": "Reinigung ({currency}/Monat)",
"sl_marketing": "Marketing / Sonstiges (/Monat)", "sl_marketing": "Marketing / Sonstiges ({currency}/Monat)",
"sl_staff": "Personal (/Monat)", "sl_staff": "Personal ({currency}/Monat)",
"sl_loan_pct": "Fremdkapitalquote (LTC)", "sl_loan_pct": "Fremdkapitalquote (LTC)",
"sl_interest_rate": "Zinssatz", "sl_interest_rate": "Zinssatz",
"sl_loan_term": "Kreditlaufzeit", "sl_loan_term": "Kreditlaufzeit",

View File

@@ -652,9 +652,9 @@
"sl_sqm_sgl_hall": "Hall m² per Single Court", "sl_sqm_sgl_hall": "Hall m² per Single Court",
"sl_sqm_dbl_outdoor": "Land m² per Double Court", "sl_sqm_dbl_outdoor": "Land m² per Double Court",
"sl_sqm_sgl_outdoor": "Land m² per Single Court", "sl_sqm_sgl_outdoor": "Land m² per Single Court",
"sl_rate_peak": "Peak Hour Rate ()", "sl_rate_peak": "Peak Hour Rate ({currency})",
"sl_rate_offpeak": "Off-Peak Hour Rate ()", "sl_rate_offpeak": "Off-Peak Hour Rate ({currency})",
"sl_rate_single": "Single Court Rate ()", "sl_rate_single": "Single Court Rate ({currency})",
"sl_peak_pct": "Peak Hours Share", "sl_peak_pct": "Peak Hours Share",
"sl_booking_fee": "Platform Fee", "sl_booking_fee": "Platform Fee",
"sl_util_target": "Target Utilization", "sl_util_target": "Target Utilization",
@@ -668,9 +668,9 @@
"sl_retail_rev": "Retail / Court", "sl_retail_rev": "Retail / Court",
"sl_court_cost_dbl": "Court Cost — Double", "sl_court_cost_dbl": "Court Cost — Double",
"sl_court_cost_sgl": "Court Cost — Single", "sl_court_cost_sgl": "Court Cost — Single",
"sl_hall_cost_sqm": "Hall Construction (/m²)", "sl_hall_cost_sqm": "Hall Construction ({currency}/m²)",
"sl_foundation_sqm": "Foundation (/m²)", "sl_foundation_sqm": "Foundation ({currency}/m²)",
"sl_land_price_sqm": "Land Price (/m²)", "sl_land_price_sqm": "Land Price ({currency}/m²)",
"sl_hvac": "HVAC System", "sl_hvac": "HVAC System",
"sl_electrical": "Electrical + Lighting", "sl_electrical": "Electrical + Lighting",
"sl_sanitary": "Sanitary / Changing", "sl_sanitary": "Sanitary / Changing",
@@ -680,7 +680,7 @@
"sl_hvac_upgrade": "HVAC Upgrade", "sl_hvac_upgrade": "HVAC Upgrade",
"sl_lighting_upgrade": "Lighting Upgrade", "sl_lighting_upgrade": "Lighting Upgrade",
"sl_fitout": "Fit-Out & Reception", "sl_fitout": "Fit-Out & Reception",
"sl_outdoor_foundation": "Concrete (/m²)", "sl_outdoor_foundation": "Concrete ({currency}/m²)",
"sl_outdoor_site_work": "Site Work", "sl_outdoor_site_work": "Site Work",
"sl_outdoor_lighting": "Lighting per Court", "sl_outdoor_lighting": "Lighting per Court",
"sl_outdoor_fencing": "Fencing", "sl_outdoor_fencing": "Fencing",
@@ -688,17 +688,17 @@
"sl_working_capital": "Working Capital", "sl_working_capital": "Working Capital",
"sl_contingency": "Contingency", "sl_contingency": "Contingency",
"sl_budget_target": "Your Budget Target", "sl_budget_target": "Your Budget Target",
"sl_rent_sqm": "Rent (/m²/month)", "sl_rent_sqm": "Rent ({currency}/m²/month)",
"sl_outdoor_rent": "Monthly Land Rent", "sl_outdoor_rent": "Monthly Land Rent",
"sl_property_tax": "Property Tax / month", "sl_property_tax": "Property Tax / month",
"sl_insurance": "Insurance (/mo)", "sl_insurance": "Insurance ({currency}/mo)",
"sl_electricity": "Electricity (/mo)", "sl_electricity": "Electricity ({currency}/mo)",
"sl_heating": "Heating (/mo)", "sl_heating": "Heating ({currency}/mo)",
"sl_water": "Water (/mo)", "sl_water": "Water ({currency}/mo)",
"sl_maintenance": "Maintenance (/mo)", "sl_maintenance": "Maintenance ({currency}/mo)",
"sl_cleaning": "Cleaning (/mo)", "sl_cleaning": "Cleaning ({currency}/mo)",
"sl_marketing": "Marketing / Misc (/mo)", "sl_marketing": "Marketing / Misc ({currency}/mo)",
"sl_staff": "Staff (/mo)", "sl_staff": "Staff ({currency}/mo)",
"sl_loan_pct": "Loan-to-Cost (LTC)", "sl_loan_pct": "Loan-to-Cost (LTC)",
"sl_interest_rate": "Interest Rate", "sl_interest_rate": "Interest Rate",
"sl_loan_term": "Loan Term", "sl_loan_term": "Loan Term",

View File

@@ -95,6 +95,20 @@ DEFAULTS = {
} }
CURRENCY_DEFAULT = {"sym": "\u20ac", "eu_style": True}
COUNTRY_CURRENCY: dict[str, dict] = {
"DE": CURRENCY_DEFAULT,
"ES": CURRENCY_DEFAULT,
"IT": CURRENCY_DEFAULT,
"FR": CURRENCY_DEFAULT,
"NL": CURRENCY_DEFAULT,
"SE": CURRENCY_DEFAULT,
"UK": {"sym": "\u00a3", "eu_style": False},
"US": {"sym": "$", "eu_style": False},
}
def validate_state(s: dict) -> dict: def validate_state(s: dict) -> dict:
"""Apply defaults and coerce types. Returns a clean copy.""" """Apply defaults and coerce types. Returns a clean copy."""
out = {**DEFAULTS} out = {**DEFAULTS}
@@ -155,6 +169,7 @@ def calc(s: dict, lang: str = "en") -> dict:
derived-data dict (the `d` object from the JS version). derived-data dict (the `d` object from the JS version).
""" """
names = get_calc_item_names(lang) names = get_calc_item_names(lang)
sym = COUNTRY_CURRENCY.get(s.get("country", "DE"), CURRENCY_DEFAULT)["sym"]
d: dict = {} d: dict = {}
is_in = s["venue"] == "indoor" is_in = s["venue"] == "indoor"
is_buy = s["own"] == "buy" is_buy = s["own"] == "buy"
@@ -198,12 +213,12 @@ def calc(s: dict, lang: str = "en") -> dict:
if is_in: if is_in:
if is_buy: if is_buy:
ci(names["hall_construction"], d["hallSqm"] * s["hallCostSqm"], ci(names["hall_construction"], d["hallSqm"] * s["hallCostSqm"],
f"{d['hallSqm']}m\u00b2 \u00d7 \u20ac{s['hallCostSqm']}/m\u00b2") f"{d['hallSqm']}m\u00b2 \u00d7 {sym}{s['hallCostSqm']}/m\u00b2")
ci(names["foundation"], d["hallSqm"] * s["foundationSqm"], ci(names["foundation"], d["hallSqm"] * s["foundationSqm"],
f"{d['hallSqm']}m\u00b2 \u00d7 \u20ac{s['foundationSqm']}/m\u00b2") f"{d['hallSqm']}m\u00b2 \u00d7 {sym}{s['foundationSqm']}/m\u00b2")
land_sqm = _round(d["hallSqm"] * 1.25) land_sqm = _round(d["hallSqm"] * 1.25)
ci(names["land_purchase"], land_sqm * s["landPriceSqm"], ci(names["land_purchase"], land_sqm * s["landPriceSqm"],
f"{land_sqm}m\u00b2 \u00d7 \u20ac{s['landPriceSqm']}/m\u00b2") f"{land_sqm}m\u00b2 \u00d7 {sym}{s['landPriceSqm']}/m\u00b2")
ci(names["transaction_costs"], _round(land_sqm * s["landPriceSqm"] * 0.1), "~10% of land") ci(names["transaction_costs"], _round(land_sqm * s["landPriceSqm"] * 0.1), "~10% of land")
ci(names["hvac_system"], s["hvac"]) ci(names["hvac_system"], s["hvac"])
ci(names["electrical_lighting"], s["electrical"] * light_mult) ci(names["electrical_lighting"], s["electrical"] * light_mult)
@@ -225,7 +240,7 @@ def calc(s: dict, lang: str = "en") -> dict:
ci(names["permits_compliance"], s["permitsCompliance"]) ci(names["permits_compliance"], s["permitsCompliance"])
if is_buy: if is_buy:
ci(names["land_purchase"], d["outdoorLandSqm"] * s["landPriceSqm"], ci(names["land_purchase"], d["outdoorLandSqm"] * s["landPriceSqm"],
f"{d['outdoorLandSqm']}m\u00b2 \u00d7 \u20ac{s['landPriceSqm']}/m\u00b2") f"{d['outdoorLandSqm']}m\u00b2 \u00d7 {sym}{s['landPriceSqm']}/m\u00b2")
ci(names["transaction_costs"], _round(d["outdoorLandSqm"] * s["landPriceSqm"] * 0.1)) ci(names["transaction_costs"], _round(d["outdoorLandSqm"] * s["landPriceSqm"] * 0.1))
ci(names["equipment"], s["equipment"] + total_courts * 300) ci(names["equipment"], s["equipment"] + total_courts * 300)
@@ -253,7 +268,7 @@ def calc(s: dict, lang: str = "en") -> dict:
if is_in: if is_in:
rent_amount = _round(d["hallSqm"] * s["rentSqm"]) rent_amount = _round(d["hallSqm"] * s["rentSqm"])
oi(names["rent"], d["hallSqm"] * s["rentSqm"], oi(names["rent"], d["hallSqm"] * s["rentSqm"],
f"{d['hallSqm']}m\u00b2 \u00d7 \u20ac{s['rentSqm']}/m\u00b2") f"{d['hallSqm']}m\u00b2 \u00d7 {sym}{s['rentSqm']}/m\u00b2")
else: else:
rent_amount = s["outdoorRent"] rent_amount = s["outdoorRent"]
oi(names["rent"], s["outdoorRent"]) oi(names["rent"], s["outdoorRent"])

View File

@@ -19,7 +19,7 @@ from ..core import (
waitlist_gate, waitlist_gate,
) )
from ..i18n import get_translations from ..i18n import get_translations
from .calculator import DEFAULTS, calc, validate_state from .calculator import COUNTRY_CURRENCY, CURRENCY_DEFAULT, DEFAULTS, calc, validate_state
bp = Blueprint( bp = Blueprint(
"planner", "planner",
@@ -328,6 +328,9 @@ async def index():
lang = g.get("lang", "en") lang = g.get("lang", "en")
d = calc(s, lang=lang) d = calc(s, lang=lang)
augment_d(d, s, lang) augment_d(d, s, lang)
cur = COUNTRY_CURRENCY.get(s["country"], CURRENCY_DEFAULT)
g.currency_sym = cur["sym"]
g.currency_eu_style = cur["eu_style"]
return await render_template( return await render_template(
"planner.html", "planner.html",
s=s, s=s,
@@ -337,6 +340,7 @@ async def index():
active_tab="capex", active_tab="capex",
country_presets=COUNTRY_PRESETS, country_presets=COUNTRY_PRESETS,
defaults=DEFAULTS, defaults=DEFAULTS,
currency_sym=cur["sym"],
) )
@@ -351,12 +355,16 @@ async def calculate():
active_tab = form.get("activeTab", "capex") active_tab = form.get("activeTab", "capex")
if active_tab not in {"capex", "operating", "cashflow", "returns", "metrics"}: if active_tab not in {"capex", "operating", "cashflow", "returns", "metrics"}:
active_tab = "capex" active_tab = "capex"
cur = COUNTRY_CURRENCY.get(s["country"], CURRENCY_DEFAULT)
g.currency_sym = cur["sym"]
g.currency_eu_style = cur["eu_style"]
return await render_template( return await render_template(
"partials/calculate_response.html", "partials/calculate_response.html",
s=s, s=s,
d=d, d=d,
active_tab=active_tab, active_tab=active_tab,
lang=lang, lang=lang,
currency_sym=cur["sym"],
) )

View File

@@ -14,7 +14,7 @@
{% macro slider(name, label, min, max, step, value, tip='') %} {% macro slider(name, label, min, max, step, value, tip='') %}
<div class="slider-group"> <div class="slider-group">
<label> <label>
<span class="slider-group__label">{{ label }}</span>{% if tip %}<span class="ti">i<span class="tp">{{ tip }}</span></span>{% endif %} <span class="slider-group__label">{{ label | tformat(currency=currency_sym) }}</span>{% if tip %}<span class="ti">i<span class="tp">{{ tip }}</span></span>{% endif %}
</label> </label>
<div class="slider-combo"> <div class="slider-combo">
<input type="range" name="{{ name }}" min="{{ min }}" max="{{ max }}" step="{{ step }}" value="{{ value }}" <input type="range" name="{{ name }}" min="{{ min }}" max="{{ max }}" step="{{ step }}" value="{{ value }}"

View File

@@ -56,7 +56,7 @@
{% for item in s.investment.items %} {% for item in s.investment.items %}
<tr> <tr>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td style="text-align:right">&euro;{{ "{:,.0f}".format(item.amount) }}</td> <td style="text-align:right">{{ item.formatted_amount }}</td>
<td style="color:#94A3B8;font-size:8pt">{{ item.info }}</td> <td style="color:#94A3B8;font-size:8pt">{{ item.info }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -97,7 +97,7 @@
{% for item in s.operations.items %} {% for item in s.operations.items %}
<tr> <tr>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td style="text-align:right">&euro;{{ "{:,.0f}".format(item.amount) }}</td> <td style="text-align:right">{{ item.formatted_amount }}</td>
<td style="color:#94A3B8;font-size:8pt">{{ item.info }}</td> <td style="color:#94A3B8;font-size:8pt">{{ item.info }}</td>
</tr> </tr>
{% endfor %} {% endfor %}