137 lines
5.5 KiB
Python
137 lines
5.5 KiB
Python
from decimal import Decimal
|
||
|
||
LABELS = {
|
||
"en": {
|
||
"cost_report_title": "Capex and Costs {year}",
|
||
"cost_report_subtitle": "{dept} · cost budget: {prior} actual vs. {year} with adjustment",
|
||
"total_across": "Total across {n} categories",
|
||
"by_category": "Budget by category",
|
||
"by_category_sub": "Total cost per category, {prior} vs. {year}",
|
||
"change_by_category": "Change by category",
|
||
"change_by_category_sub": "Percentage change from {prior} actual to {year} with adjustment",
|
||
"change_legend": "Orange = cost increase · Blue = cost reduction",
|
||
"prior_actual": "{prior} actual",
|
||
"adjusted": "{year} with adjustment",
|
||
"base": "{year} without adjustment",
|
||
"change_eur": "Change (€)",
|
||
"change_pct": "Change (%)",
|
||
"cost_lines": "{n} cost lines",
|
||
"cost_line": "1 cost line",
|
||
"summary": "Summary",
|
||
"summary_sub": "All categories, {prior} actual vs. {year} with adjustment",
|
||
"category": "Category",
|
||
"line": "Line",
|
||
"site": "Site",
|
||
"group": "Group",
|
||
"total": "Total",
|
||
"factor": "Factor",
|
||
"reason": "Reason",
|
||
"remarks": "Remarks",
|
||
"budget_title": "Budget {year} · {dept}",
|
||
"budget_sub": "Monthly budget by category",
|
||
"revenue_total": "Sub total revenue",
|
||
"sub_total": "Sub total {name}",
|
||
"total_costs": "Total costs",
|
||
"status": "Status",
|
||
"exported": "Exported",
|
||
"draft": "Draft",
|
||
"in_review": "In review",
|
||
"approved": "Approved",
|
||
"inputs": "Inputs",
|
||
"months": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||
"no_data": "No cost lines yet.",
|
||
"site_filter": "Site: {site}",
|
||
},
|
||
"pt": {
|
||
"cost_report_title": "Capex e Custos para {year}",
|
||
"cost_report_subtitle": "{dept} · orçamento de custos: real {prior} vs. {year} com ajuste",
|
||
"total_across": "Total de {n} categorias",
|
||
"by_category": "Orçamento por categoria",
|
||
"by_category_sub": "Custo total por categoria, {prior} vs. {year}",
|
||
"change_by_category": "Variação por categoria",
|
||
"change_by_category_sub": "Variação percentual do real {prior} para {year} com ajuste",
|
||
"change_legend": "Laranja = aumento de custo · Azul = redução de custo",
|
||
"prior_actual": "{prior} real",
|
||
"adjusted": "{year} com ajuste",
|
||
"base": "{year} sem ajuste",
|
||
"change_eur": "Variação (€)",
|
||
"change_pct": "Variação (%)",
|
||
"cost_lines": "{n} linhas de custo",
|
||
"cost_line": "1 linha de custo",
|
||
"summary": "Resumo",
|
||
"summary_sub": "Todas as categorias, real {prior} vs. {year} com ajuste",
|
||
"category": "Categoria",
|
||
"line": "Linha",
|
||
"site": "Unidade",
|
||
"group": "Grupo",
|
||
"total": "Total",
|
||
"factor": "Fator",
|
||
"reason": "Motivo",
|
||
"remarks": "Observações",
|
||
"budget_title": "Orçamento {year} · {dept}",
|
||
"budget_sub": "Orçamento mensal por categoria",
|
||
"revenue_total": "Subtotal receitas",
|
||
"sub_total": "Subtotal {name}",
|
||
"total_costs": "Total de custos",
|
||
"status": "Estado",
|
||
"exported": "Exportado",
|
||
"draft": "Rascunho",
|
||
"in_review": "Em revisão",
|
||
"approved": "Aprovado",
|
||
"inputs": "Dados",
|
||
"months": ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"],
|
||
"no_data": "Ainda não há linhas de custo.",
|
||
"site_filter": "Unidade: {site}",
|
||
},
|
||
}
|
||
|
||
|
||
class T:
|
||
def __init__(self, lang: str, year: int, dept: str = ""):
|
||
self.lang = lang if lang in LABELS else "pt"
|
||
self.labels = LABELS[self.lang]
|
||
self.ctx = {"year": year, "prior": year - 1, "dept": dept}
|
||
|
||
def __call__(self, key: str, **kw):
|
||
value = self.labels[key]
|
||
return value.format(**{**self.ctx, **kw}) if isinstance(value, str) else value
|
||
|
||
def _group(self, value: Decimal, decimals: int) -> str:
|
||
q = Decimal(1).scaleb(-decimals) if decimals else Decimal(1)
|
||
text = f"{abs(Decimal(value)).quantize(q):,.{decimals}f}"
|
||
whole, _, frac = text.partition(".")
|
||
whole = whole.replace(",", " ")
|
||
if not frac:
|
||
return whole
|
||
return whole + ("," if self.lang == "pt" else ".") + frac
|
||
|
||
def eur(self, value, decimals: int = 2, signed: bool = False) -> str:
|
||
value = Decimal(value or 0)
|
||
sign = ("+" if value > 0 else "−" if value < 0 else "") if signed else ("−" if value < 0 else "")
|
||
body = self._group(value, decimals)
|
||
return f"{sign}{body} €" if self.lang == "pt" else f"{sign}€{body}"
|
||
|
||
def num(self, value, decimals: int = 2) -> str:
|
||
value = Decimal(value or 0)
|
||
return ("\u2212" if value < 0 else "") + self._group(value, decimals)
|
||
|
||
def pct(self, value, signed: bool = True) -> str:
|
||
if value is None:
|
||
return "—"
|
||
value = Decimal(value)
|
||
sign = ("+" if value > 0 else "−" if value < 0 else "") if signed else ""
|
||
return f"{sign}{self._group(value, 2)}%"
|
||
|
||
def short_eur(self, value: float) -> str:
|
||
v = abs(value)
|
||
sign = "−" if value < 0 else ""
|
||
if v >= 1_000_000:
|
||
body = f"{v / 1_000_000:g}M"
|
||
elif v >= 1000:
|
||
body = f"{v / 1000:g}k"
|
||
else:
|
||
body = f"{v:g}"
|
||
if self.lang == "pt":
|
||
return f"{sign}{body.replace('.', ',')} €"
|
||
return f"{sign}€{body}"
|