testing new features
This commit is contained in:
+428
@@ -0,0 +1,428 @@
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from cashflow_model import (
|
||||
Account,
|
||||
Asset,
|
||||
ExchangeRate,
|
||||
FinancialModel,
|
||||
Liability,
|
||||
RecurringCashflow,
|
||||
Transaction,
|
||||
)
|
||||
from cli.i18n import t
|
||||
|
||||
app = typer.Typer(name="config", help=t("config.help"))
|
||||
console = Console()
|
||||
|
||||
DATA_DIR = Path("data")
|
||||
MODEL_PATH = DATA_DIR / "model.json"
|
||||
|
||||
|
||||
def _load_model() -> FinancialModel:
|
||||
if MODEL_PATH.exists():
|
||||
return FinancialModel.load(MODEL_PATH)
|
||||
return FinancialModel()
|
||||
|
||||
|
||||
def _save_model(model: FinancialModel) -> None:
|
||||
model.save(MODEL_PATH)
|
||||
|
||||
|
||||
# ── base-currency ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def base_currency(
|
||||
currency: str = typer.Argument(None, help=t("cmd.config.base_currency.arg")),
|
||||
) -> None:
|
||||
"""Показать или установить базовую валюту модели"""
|
||||
model = _load_model()
|
||||
if currency:
|
||||
model.base_currency = currency.upper()
|
||||
_save_model(model)
|
||||
ok = t("global.ok")
|
||||
msg = t("cmd.config.base_currency.ok", currency=model.base_currency)
|
||||
console.print(f"[green]{ok}[/green] {msg}")
|
||||
else:
|
||||
console.print(t("cmd.config.base_currency.show", currency=f"[bold]{model.base_currency}[/bold]"))
|
||||
|
||||
|
||||
# ── rate ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def rate_set(
|
||||
from_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.from")),
|
||||
to_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.to")),
|
||||
rate: float = typer.Argument(..., help=t("cmd.config.rate_set.arg.rate")),
|
||||
) -> None:
|
||||
"""Добавить или обновить курс валюты"""
|
||||
model = _load_model()
|
||||
fc, tc = from_currency.upper(), to_currency.upper()
|
||||
existing = [r for r in model.exchange_rates if r.from_currency == fc and r.to_currency == tc]
|
||||
if existing:
|
||||
existing[0].rate = rate
|
||||
else:
|
||||
model.exchange_rates.append(ExchangeRate(
|
||||
from_currency=from_currency.upper(),
|
||||
to_currency=to_currency.upper(),
|
||||
rate=rate,
|
||||
))
|
||||
_save_model(model)
|
||||
fc, tc = from_currency.upper(), to_currency.upper()
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.rate_set.ok', fc=fc, tc=tc, rate=rate)}")
|
||||
|
||||
|
||||
@app.command(name="rate-list")
|
||||
def rate_list() -> None:
|
||||
"""Список курсов валют"""
|
||||
model = _load_model()
|
||||
if not model.exchange_rates:
|
||||
console.print(f"[yellow]{t('cmd.config.rate_list.empty')}[/yellow]")
|
||||
return
|
||||
table = Table(title=t("table.rates.title"))
|
||||
table.add_column(t("table.rates.col.from"), style="cyan")
|
||||
table.add_column(t("table.rates.col.to"), style="cyan")
|
||||
table.add_column(t("table.rates.col.rate"), justify="right")
|
||||
for r in model.exchange_rates:
|
||||
table.add_row(r.from_currency, r.to_currency, str(r.rate))
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command(name="rate-remove")
|
||||
def rate_remove(
|
||||
from_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.from")),
|
||||
to_currency: str = typer.Argument(..., help=t("cmd.config.rate_set.arg.to")),
|
||||
) -> None:
|
||||
"""Удалить курс валюты"""
|
||||
model = _load_model()
|
||||
fc, tc = from_currency.upper(), to_currency.upper()
|
||||
initial = len(model.exchange_rates)
|
||||
model.exchange_rates = [
|
||||
r for r in model.exchange_rates
|
||||
if not (r.from_currency == fc and r.to_currency == tc)
|
||||
]
|
||||
if len(model.exchange_rates) < initial:
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.rate_remove.ok', fc=fc, tc=tc)}")
|
||||
else:
|
||||
console.print(f"[red]{t('cmd.config.rate_remove.err', fc=fc, tc=tc)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── account ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def account_add(
|
||||
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.account_add.opt.name")),
|
||||
balance: float = typer.Option(0.0, "--balance", "-b", help=t("cmd.config.account_add.opt.balance")),
|
||||
currency: str = typer.Option("", "--currency", "-c", help=t("cmd.config.account_add.opt.currency")),
|
||||
) -> None:
|
||||
"""Добавить счёт"""
|
||||
model = _load_model()
|
||||
curr = currency.upper() if currency else model.base_currency
|
||||
account = Account(name=name, balance=balance, currency=curr)
|
||||
model.accounts.append(account)
|
||||
_save_model(model)
|
||||
ok = t("global.ok")
|
||||
msg = t("cmd.config.account_add.ok", name=name, balance=balance, currency=curr)
|
||||
console.print(f"[green]{ok}[/green] {msg}")
|
||||
|
||||
|
||||
@app.command(name="account-list")
|
||||
def account_list() -> None:
|
||||
"""Список счетов"""
|
||||
model = _load_model()
|
||||
if not model.accounts:
|
||||
console.print(f"[yellow]{t('cmd.config.account_list.empty')}[/yellow]")
|
||||
return
|
||||
table = Table(title=t("table.accounts_config.title"))
|
||||
table.add_column(t("table.accounts_config.col.id"), style="dim")
|
||||
table.add_column(t("table.accounts_config.col.name"), style="cyan")
|
||||
table.add_column(t("table.accounts_config.col.currency"))
|
||||
table.add_column(t("table.accounts_config.col.balance"), justify="right")
|
||||
for a in model.accounts:
|
||||
table.add_row(str(a.id)[:8], a.name, a.currency, f"{a.balance:,.2f}")
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command(name="account-remove")
|
||||
def account_remove(
|
||||
identifier: str = typer.Argument(..., help=t("cmd.config.account_remove.arg")),
|
||||
) -> None:
|
||||
"""Удалить счёт по ID или названию"""
|
||||
model = _load_model()
|
||||
initial = len(model.accounts)
|
||||
model.accounts = [
|
||||
a for a in model.accounts
|
||||
if str(a.id) != identifier and a.name != identifier
|
||||
]
|
||||
if len(model.accounts) < initial:
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.account_remove.ok', id=identifier)}")
|
||||
else:
|
||||
console.print(f"[red]{t('cmd.config.account_remove.err', id=identifier)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── transaction ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def transaction_add(
|
||||
account: str = typer.Option(..., "--account", "-a", help=t("cmd.config.transaction_add.opt.account")),
|
||||
amount: float = typer.Option(..., "--amount", "-m", help=t("cmd.config.transaction_add.opt.amount")),
|
||||
category: str = typer.Option("", "--category", "-c", help=t("cmd.config.transaction_add.opt.category")),
|
||||
description: str = typer.Option("", "--description", "-d", help=t("cmd.config.transaction_add.opt.description")),
|
||||
date: str = typer.Option("", "--date", "-D", help=t("cmd.config.transaction_add.opt.date")),
|
||||
) -> None:
|
||||
"""Добавить транзакцию"""
|
||||
model = _load_model()
|
||||
account_id = _resolve_account_id(model, account)
|
||||
if not account_id:
|
||||
console.print(f"[red]{t('cmd.config.transaction_add.err', account=account)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
txn = Transaction(
|
||||
date=date or "",
|
||||
account=account_id,
|
||||
category=category,
|
||||
amount=amount,
|
||||
description=description,
|
||||
)
|
||||
model.transactions.append(txn)
|
||||
_save_model(model)
|
||||
kind = t("cmd.config.transaction_add.kind.income") if amount >= 0 else t("cmd.config.transaction_add.kind.expense")
|
||||
ok = t("global.ok")
|
||||
msg = t("cmd.config.transaction_add.ok", kind=kind, amount=abs(amount), account=account)
|
||||
console.print(f"[green]{ok}[/green] {msg}")
|
||||
|
||||
|
||||
@app.command(name="transaction-list")
|
||||
def transaction_list() -> None:
|
||||
"""Список транзакций"""
|
||||
model = _load_model()
|
||||
if not model.transactions:
|
||||
console.print(f"[yellow]{t('cmd.config.transaction_list.empty')}[/yellow]")
|
||||
return
|
||||
table = Table(title=t("table.transactions.title"))
|
||||
table.add_column(t("table.accounts_config.col.id"), style="dim")
|
||||
table.add_column(t("table.transactions.col.date"))
|
||||
table.add_column(t("table.accounts_config.col.name"))
|
||||
table.add_column(t("table.transactions.col.category"))
|
||||
table.add_column(t("table.transactions.col.amount"), justify="right")
|
||||
table.add_column(t("table.transactions.col.description"))
|
||||
for tx in model.transactions:
|
||||
account_name = _account_name(model, tx.account)
|
||||
table.add_row(
|
||||
str(tx.id)[:8], tx.date, account_name, tx.category,
|
||||
f"{tx.amount:,.2f}", tx.description,
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command(name="transaction-remove")
|
||||
def transaction_remove(
|
||||
identifier: str = typer.Argument(..., help=t("cmd.config.transaction_remove.arg")),
|
||||
) -> None:
|
||||
"""Удалить транзакцию по ID"""
|
||||
model = _load_model()
|
||||
initial = len(model.transactions)
|
||||
model.transactions = [t for t in model.transactions if str(t.id) != identifier]
|
||||
if len(model.transactions) < initial:
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.transaction_remove.ok', id=identifier)}")
|
||||
else:
|
||||
console.print(f"[red]{t('cmd.config.transaction_remove.err', id=identifier)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── recurring ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def recurring_add(
|
||||
amount: float = typer.Option(..., "--amount", "-m", help=t("cmd.config.recurring_add.opt.amount")),
|
||||
category: str = typer.Option("", "--category", "-c", help=t("cmd.config.recurring_add.opt.category")),
|
||||
frequency: str = typer.Option("monthly", "--frequency", "-f", help=t("cmd.config.recurring_add.opt.frequency")),
|
||||
start_date: str = typer.Option("", "--start", "-s", help=t("cmd.config.recurring_add.opt.start")),
|
||||
end_date: str = typer.Option("", "--end", "-e", help=t("cmd.config.recurring_add.opt.end")),
|
||||
) -> None:
|
||||
"""Добавить регулярный платёж"""
|
||||
model = _load_model()
|
||||
rc = RecurringCashflow(
|
||||
start_date=start_date or "", end_date=end_date or "",
|
||||
frequency=frequency, amount=amount, category=category,
|
||||
)
|
||||
model.recurring.append(rc)
|
||||
_save_model(model)
|
||||
ok = t("global.ok")
|
||||
msg = t("cmd.config.recurring_add.ok", amount=abs(amount), frequency=frequency)
|
||||
console.print(f"[green]{ok}[/green] {msg}")
|
||||
|
||||
|
||||
@app.command(name="recurring-list")
|
||||
def recurring_list() -> None:
|
||||
"""Список регулярных платежей"""
|
||||
model = _load_model()
|
||||
if not model.recurring:
|
||||
console.print(f"[yellow]{t('cmd.config.recurring_list.empty')}[/yellow]")
|
||||
return
|
||||
table = Table(title=t("table.recurring.title"))
|
||||
table.add_column(t("table.recurring.col.id"), style="dim")
|
||||
table.add_column(t("table.recurring.col.start"))
|
||||
table.add_column(t("table.recurring.col.end"))
|
||||
table.add_column(t("table.recurring.col.period"))
|
||||
table.add_column(t("table.recurring.col.amount"), justify="right")
|
||||
table.add_column(t("table.recurring.col.category"))
|
||||
for r in model.recurring:
|
||||
row = (str(r.id)[:8], r.start_date, r.end_date, r.frequency, f"{r.amount:,.2f}", r.category)
|
||||
table.add_row(*row)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command(name="recurring-remove")
|
||||
def recurring_remove(
|
||||
identifier: str = typer.Argument(..., help=t("cmd.config.recurring_remove.arg")),
|
||||
) -> None:
|
||||
"""Удалить регулярный платёж по ID"""
|
||||
model = _load_model()
|
||||
initial = len(model.recurring)
|
||||
model.recurring = [r for r in model.recurring if str(r.id) != identifier]
|
||||
if len(model.recurring) < initial:
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.recurring_remove.ok', id=identifier)}")
|
||||
else:
|
||||
console.print(f"[red]{t('cmd.config.recurring_remove.err', id=identifier)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── asset ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def asset_add(
|
||||
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.asset_add.opt.name")),
|
||||
value: float = typer.Option(0.0, "--value", "-v", help=t("cmd.config.asset_add.opt.value")),
|
||||
growth_rate: float = typer.Option(0.0, "--growth", "-g", help=t("cmd.config.asset_add.opt.growth")),
|
||||
) -> None:
|
||||
"""Добавить актив"""
|
||||
model = _load_model()
|
||||
asset = Asset(name=name, value=value, growth_rate=growth_rate)
|
||||
model.assets.append(asset)
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.asset_add.ok', name=name, value=value)}")
|
||||
|
||||
|
||||
@app.command(name="asset-list")
|
||||
def asset_list() -> None:
|
||||
"""Список активов"""
|
||||
model = _load_model()
|
||||
if not model.assets:
|
||||
console.print(f"[yellow]{t('cmd.config.asset_list.empty')}[/yellow]")
|
||||
return
|
||||
table = Table(title=t("table.assets.title"))
|
||||
table.add_column(t("table.assets.col.id"), style="dim")
|
||||
table.add_column(t("table.assets.col.name"), style="cyan")
|
||||
table.add_column(t("table.assets.col.value"), justify="right")
|
||||
table.add_column(t("table.assets.col.growth"), justify="right")
|
||||
for a in model.assets:
|
||||
table.add_row(str(a.id)[:8], a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command(name="asset-remove")
|
||||
def asset_remove(
|
||||
identifier: str = typer.Argument(..., help=t("cmd.config.asset_remove.arg")),
|
||||
) -> None:
|
||||
"""Удалить актив по ID или названию"""
|
||||
model = _load_model()
|
||||
initial = len(model.assets)
|
||||
model.assets = [
|
||||
a for a in model.assets
|
||||
if str(a.id) != identifier and a.name != identifier
|
||||
]
|
||||
if len(model.assets) < initial:
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.asset_remove.ok', id=identifier)}")
|
||||
else:
|
||||
console.print(f"[red]{t('cmd.config.asset_remove.err', id=identifier)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── liability ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def liability_add(
|
||||
name: str = typer.Option(..., "--name", "-n", help=t("cmd.config.liability_add.opt.name")),
|
||||
balance: float = typer.Option(0.0, "--balance", "-b", help=t("cmd.config.liability_add.opt.balance")),
|
||||
interest: float = typer.Option(0.0, "--interest", "-i", help=t("cmd.config.liability_add.opt.interest")),
|
||||
payment: float = typer.Option(0.0, "--payment", "-p", help=t("cmd.config.liability_add.opt.payment")),
|
||||
) -> None:
|
||||
"""Добавить обязательство"""
|
||||
model = _load_model()
|
||||
liability = Liability(name=name, balance=balance, interest=interest, payment=payment)
|
||||
model.liabilities.append(liability)
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.liability_add.ok', name=name, balance=balance)}")
|
||||
|
||||
|
||||
@app.command(name="liability-list")
|
||||
def liability_list() -> None:
|
||||
"""Список обязательств"""
|
||||
model = _load_model()
|
||||
if not model.liabilities:
|
||||
console.print(f"[yellow]{t('cmd.config.liability_list.empty')}[/yellow]")
|
||||
return
|
||||
table = Table(title=t("table.liabilities.title"))
|
||||
table.add_column(t("table.liabilities.col.id"), style="dim")
|
||||
table.add_column(t("table.liabilities.col.name"), style="cyan")
|
||||
table.add_column(t("table.liabilities.col.balance"), justify="right")
|
||||
table.add_column(t("table.liabilities.col.rate"), justify="right")
|
||||
table.add_column(t("table.liabilities.col.payment"), justify="right")
|
||||
for li in model.liabilities:
|
||||
b, i, p = f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}"
|
||||
row = (str(li.id)[:8], li.name, b, i, p)
|
||||
table.add_row(*row)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command(name="liability-remove")
|
||||
def liability_remove(
|
||||
identifier: str = typer.Argument(..., help=t("cmd.config.liability_remove.arg")),
|
||||
) -> None:
|
||||
"""Удалить обязательство по ID или названию"""
|
||||
model = _load_model()
|
||||
initial = len(model.liabilities)
|
||||
model.liabilities = [
|
||||
li for li in model.liabilities
|
||||
if str(li.id) != identifier and li.name != identifier
|
||||
]
|
||||
if len(model.liabilities) < initial:
|
||||
_save_model(model)
|
||||
console.print(f"[green]{t('global.ok')}[/green] {t('cmd.config.liability_remove.ok', id=identifier)}")
|
||||
else:
|
||||
console.print(f"[red]{t('cmd.config.liability_remove.err', id=identifier)}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── helpers ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _resolve_account_id(model: FinancialModel, identifier: str) -> str | None:
|
||||
for a in model.accounts:
|
||||
if str(a.id) == identifier or a.name == identifier:
|
||||
return str(a.id)
|
||||
return None
|
||||
|
||||
|
||||
def _account_name(model: FinancialModel, account_id: str) -> str:
|
||||
for a in model.accounts:
|
||||
if str(a.id) == account_id:
|
||||
return a.name
|
||||
return account_id[:8]
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
import os
|
||||
|
||||
_TRANSLATIONS: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
def _r(key: str, text: str) -> str:
|
||||
_TRANSLATIONS.setdefault("ru", {})[key] = text
|
||||
return text
|
||||
|
||||
|
||||
def _e(key: str, text: str) -> str:
|
||||
_TRANSLATIONS.setdefault("en", {})[key] = text
|
||||
return text
|
||||
|
||||
|
||||
# ── app ────────────────────────────────────────────────────────
|
||||
|
||||
_r("app.help", "CashFlow Forecast — личная финансовая модель")
|
||||
|
||||
# ── cmd.init ───────────────────────────────────────────────────
|
||||
|
||||
_r("cmd.init.help", "Создать пустую финансовую модель")
|
||||
_r("cmd.init.ok", "Пустая модель создана в {path}")
|
||||
|
||||
# ── cmd.forecast ───────────────────────────────────────────────
|
||||
|
||||
_r("cmd.forecast.help", "Запустить прогноз денежных потоков")
|
||||
_r("cmd.forecast.opt.months", "Количество месяцев прогноза")
|
||||
_r("cmd.forecast.opt.currency", "Валюта отображения (по умолчанию — базовая валюта модели)")
|
||||
_r("cmd.forecast.title", "Прогноз на {months} мес. ({currency})")
|
||||
_r("cmd.forecast.col.account", "Счёт")
|
||||
_r("cmd.forecast.col.month", "Месяц")
|
||||
_r("cmd.forecast.col.balance", "Баланс")
|
||||
_r("cmd.forecast.col.income", "Доход")
|
||||
_r("cmd.forecast.col.expenses", "Расход")
|
||||
_r("cmd.forecast.total", "Итог: Баланс: {balance} | Доход: {income} | Расход: {expenses}")
|
||||
|
||||
# ── cmd.scenario ──────────────────────────────────────────────
|
||||
|
||||
_r("cmd.scenario.help", "Применить сценарий и показать прогноз")
|
||||
_r("cmd.scenario.arg.name", "Имя сценария: baseline, optimistic, pessimistic")
|
||||
_r("cmd.scenario.opt.months", "Количество месяцев")
|
||||
_r("cmd.scenario.err.unknown", "Неизвестный сценарий: {name}")
|
||||
_r("cmd.scenario.err.available", "Доступны: {scenarios}")
|
||||
_r("cmd.scenario.label", "Сценарий")
|
||||
_r("cmd.scenario.label.balance", "Баланс")
|
||||
_r("cmd.scenario.label.income", "Доход")
|
||||
_r("cmd.scenario.label.expenses", "Расход")
|
||||
|
||||
# ── cmd.whatif ────────────────────────────────────────────────
|
||||
|
||||
_r("cmd.whatif.help", "What-if анализ с произвольными множителями")
|
||||
_r("cmd.whatif.opt.income", "Множитель дохода")
|
||||
_r("cmd.whatif.opt.expense", "Множитель расхода")
|
||||
_r("cmd.whatif.opt.growth", "Множитель роста активов")
|
||||
_r("cmd.whatif.opt.months", "Количество месяцев")
|
||||
_r("cmd.whatif.title", "What-if анализ")
|
||||
_r("cmd.whatif.params", "Доход x{income} | Расход x{expense} | Рост x{growth}")
|
||||
|
||||
# ── cmd.compare ───────────────────────────────────────────────
|
||||
|
||||
_r("cmd.compare.help", "Сравнить все сценарии")
|
||||
_r("cmd.compare.opt.months", "Количество месяцев")
|
||||
_r("cmd.compare.title", "Сравнение сценариев ({currency})")
|
||||
_r("cmd.compare.col.scenario", "Сценарий")
|
||||
|
||||
# ── cmd.import ────────────────────────────────────────────────
|
||||
|
||||
_r("cmd.import.help", "Импорт данных из Excel")
|
||||
_r("cmd.import.arg.path", "Путь к .xlsx файлу")
|
||||
_r("cmd.import.ok",
|
||||
"Импортировано: {accounts} счетов, {transactions} транзакций, "
|
||||
"{recurring} регулярных платежей, {assets} активов, {liabilities} обязательств")
|
||||
_r("cmd.import.base_currency", "Базовая валюта: {currency}")
|
||||
_r("cmd.import.err", "Ошибка импорта: {error}")
|
||||
|
||||
# ── cmd.export ────────────────────────────────────────────────
|
||||
|
||||
_r("cmd.export.help", "Экспорт модели в Excel")
|
||||
_r("cmd.export.arg.path", "Путь для .xlsx файла")
|
||||
_r("cmd.export.ok", "Модель экспортирована в {path}")
|
||||
_r("cmd.export.err", "Ошибка экспорта: {error}")
|
||||
|
||||
# ── cmd.analyze ───────────────────────────────────────────────
|
||||
|
||||
_r("cmd.analyze.help", "AI-анализ финансовой модели")
|
||||
_r("cmd.analyze.opt.months", "Количество месяцев для анализа")
|
||||
_r("cmd.analyze.title.prompt", "Промпт для AI")
|
||||
_r("cmd.analyze.title.summary", "Сводка")
|
||||
_r("cmd.analyze.base_currency", "Базовая валюта: {currency}")
|
||||
_r("cmd.analyze.stub", "AI-ответ: заглушка. Подключите реальный API в ai/assistant.py")
|
||||
|
||||
# ── cmd.info ──────────────────────────────────────────────────
|
||||
|
||||
_r("cmd.info.help", "Сводка всей финансовой модели")
|
||||
_r("cmd.info.metadata", "Метаданные")
|
||||
_r("cmd.info.base_currency", "Базовая валюта: {currency} ({symbol})")
|
||||
_r("cmd.info.accounts", "Счетов: {count}")
|
||||
_r("cmd.info.transactions", "Транзакций: {count}")
|
||||
_r("cmd.info.recurring", "Регулярных платежей: {count}")
|
||||
_r("cmd.info.assets", "Активов: {count}")
|
||||
_r("cmd.info.liabilities", "Обязательств: {count}")
|
||||
_r("cmd.info.rates", "Курсов валют: {count}")
|
||||
_r("cmd.info.more", "... и ещё {count} транзакций")
|
||||
|
||||
# ── table headers ─────────────────────────────────────────────
|
||||
|
||||
_r("table.accounts.title", "Счета")
|
||||
_r("table.accounts_config.title", "Счета")
|
||||
_r("table.accounts.col.name", "Название")
|
||||
_r("table.accounts.col.currency", "Валюта")
|
||||
_r("table.accounts.col.balance", "Баланс")
|
||||
|
||||
_r("table.transactions.title", "Транзакции")
|
||||
_r("table.transactions.col.date", "Дата")
|
||||
_r("table.transactions.col.category", "Категория")
|
||||
_r("table.transactions.col.amount", "Сумма")
|
||||
_r("table.transactions.col.description", "Описание")
|
||||
|
||||
_r("table.recurring.title", "Регулярные платежи")
|
||||
_r("table.recurring.col.period", "Период")
|
||||
_r("table.recurring.col.amount", "Сумма")
|
||||
_r("table.recurring.col.category", "Категория")
|
||||
_r("table.recurring.col.id", "ID")
|
||||
_r("table.recurring.col.start", "Начало")
|
||||
_r("table.recurring.col.end", "Конец")
|
||||
|
||||
_r("table.assets.title", "Активы")
|
||||
_r("table.assets.col.id", "ID")
|
||||
_r("table.assets.col.name", "Название")
|
||||
_r("table.assets.col.value", "Стоимость")
|
||||
_r("table.assets.col.growth", "Рост (%)")
|
||||
|
||||
_r("table.liabilities.title", "Обязательства")
|
||||
_r("table.liabilities.col.id", "ID")
|
||||
_r("table.liabilities.col.name", "Название")
|
||||
_r("table.liabilities.col.balance", "Долг")
|
||||
_r("table.liabilities.col.rate", "Ставка")
|
||||
_r("table.liabilities.col.payment", "Платёж")
|
||||
|
||||
_r("table.rates.title", "Курсы валют")
|
||||
_r("table.rates.col.from", "Из")
|
||||
_r("table.rates.col.to", "В")
|
||||
_r("table.rates.col.rate", "Курс")
|
||||
|
||||
_r("table.accounts_config.col.id", "ID")
|
||||
_r("table.accounts_config.col.name", "Название")
|
||||
_r("table.accounts_config.col.currency", "Валюта")
|
||||
_r("table.accounts_config.col.balance", "Баланс")
|
||||
|
||||
# ── config ────────────────────────────────────────────────────
|
||||
|
||||
_r("config.help", "Управление моделью (счета, транзакции, валюты...)")
|
||||
|
||||
_r("cmd.config.base_currency.help", "Показать или установить базовую валюту модели")
|
||||
_r("cmd.config.base_currency.arg", "Код валюты (RUB, USD, EUR...)")
|
||||
_r("cmd.config.base_currency.ok", "Базовая валюта установлена: {currency}")
|
||||
_r("cmd.config.base_currency.show", "Базовая валюта: {currency}")
|
||||
|
||||
_r("cmd.config.rate_set.help", "Добавить или обновить курс валюты")
|
||||
_r("cmd.config.rate_set.arg.from", "Из валюты")
|
||||
_r("cmd.config.rate_set.arg.to", "В валюту")
|
||||
_r("cmd.config.rate_set.arg.rate", "Курс")
|
||||
_r("cmd.config.rate_set.ok", "Курс {fc} → {tc} = {rate}")
|
||||
|
||||
_r("cmd.config.rate_list.help", "Список курсов валют")
|
||||
_r("cmd.config.rate_list.empty", "Курсы не заданы")
|
||||
|
||||
_r("cmd.config.rate_remove.help", "Удалить курс валюты")
|
||||
_r("cmd.config.rate_remove.ok", "Курс {fc} → {tc} удалён")
|
||||
_r("cmd.config.rate_remove.err", "Курс {fc} → {tc} не найден")
|
||||
|
||||
_r("cmd.config.account_add.help", "Добавить счёт")
|
||||
_r("cmd.config.account_add.opt.name", "Название счёта")
|
||||
_r("cmd.config.account_add.opt.balance", "Начальный баланс")
|
||||
_r("cmd.config.account_add.opt.currency", "Валюта счёта")
|
||||
_r("cmd.config.account_add.ok", "Счёт '{name}' добавлен (баланс: {balance}, валюта: {currency})")
|
||||
|
||||
_r("cmd.config.account_list.help", "Список счетов")
|
||||
_r("cmd.config.account_list.empty", "Счета не добавлены")
|
||||
|
||||
_r("cmd.config.account_remove.help", "Удалить счёт по ID или названию")
|
||||
_r("cmd.config.account_remove.arg", "ID или название счёта")
|
||||
_r("cmd.config.account_remove.ok", "Счёт '{id}' удалён")
|
||||
_r("cmd.config.account_remove.err", "Счёт '{id}' не найден")
|
||||
|
||||
_r("cmd.config.transaction_add.help", "Добавить транзакцию")
|
||||
_r("cmd.config.transaction_add.opt.account", "ID или название счёта")
|
||||
_r("cmd.config.transaction_add.opt.amount", "Сумма (доход/расход)")
|
||||
_r("cmd.config.transaction_add.opt.category", "Категория")
|
||||
_r("cmd.config.transaction_add.opt.description", "Описание")
|
||||
_r("cmd.config.transaction_add.opt.date", "Дата (YYYY-MM-DD)")
|
||||
_r("cmd.config.transaction_add.ok", "Транзакция '{kind}' на {amount:.2f} (счёт: {account})")
|
||||
_r("cmd.config.transaction_add.kind.income", "доход")
|
||||
_r("cmd.config.transaction_add.kind.expense", "расход")
|
||||
_r("cmd.config.transaction_add.err", "Счёт '{account}' не найден")
|
||||
|
||||
_r("cmd.config.transaction_list.help", "Список транзакций")
|
||||
_r("cmd.config.transaction_list.empty", "Транзакции не добавлены")
|
||||
|
||||
_r("cmd.config.transaction_remove.help", "Удалить транзакцию по ID")
|
||||
_r("cmd.config.transaction_remove.arg", "ID транзакции")
|
||||
_r("cmd.config.transaction_remove.ok", "Транзакция '{id}' удалена")
|
||||
_r("cmd.config.transaction_remove.err", "Транзакция '{id}' не найдена")
|
||||
|
||||
_r("cmd.config.recurring_add.help", "Добавить регулярный платёж")
|
||||
_r("cmd.config.recurring_add.opt.amount", "Сумма")
|
||||
_r("cmd.config.recurring_add.opt.category", "Категория")
|
||||
_r("cmd.config.recurring_add.opt.frequency", "Периодичность")
|
||||
_r("cmd.config.recurring_add.opt.start", "Дата начала (YYYY-MM-DD)")
|
||||
_r("cmd.config.recurring_add.opt.end", "Дата окончания (YYYY-MM-DD)")
|
||||
_r("cmd.config.recurring_add.ok", "Регулярный платёж на {amount:.2f} ({frequency})")
|
||||
|
||||
_r("cmd.config.recurring_list.help", "Список регулярных платежей")
|
||||
_r("cmd.config.recurring_list.empty", "Регулярные платежи не добавлены")
|
||||
|
||||
_r("cmd.config.recurring_remove.help", "Удалить регулярный платёж по ID")
|
||||
_r("cmd.config.recurring_remove.arg", "ID регулярного платежа")
|
||||
_r("cmd.config.recurring_remove.ok", "Регулярный платёж '{id}' удалён")
|
||||
_r("cmd.config.recurring_remove.err", "Регулярный платёж '{id}' не найден")
|
||||
|
||||
_r("cmd.config.asset_add.help", "Добавить актив")
|
||||
_r("cmd.config.asset_add.opt.name", "Название актива")
|
||||
_r("cmd.config.asset_add.opt.value", "Стоимость")
|
||||
_r("cmd.config.asset_add.opt.growth", "Годовой рост (%)")
|
||||
_r("cmd.config.asset_add.ok", "Актив '{name}' добавлен (стоимость: {value})")
|
||||
|
||||
_r("cmd.config.asset_list.help", "Список активов")
|
||||
_r("cmd.config.asset_list.empty", "Активы не добавлены")
|
||||
|
||||
_r("cmd.config.asset_remove.help", "Удалить актив по ID или названию")
|
||||
_r("cmd.config.asset_remove.arg", "ID или название актива")
|
||||
_r("cmd.config.asset_remove.ok", "Актив '{id}' удалён")
|
||||
_r("cmd.config.asset_remove.err", "Актив '{id}' не найден")
|
||||
|
||||
_r("cmd.config.liability_add.help", "Добавить обязательство")
|
||||
_r("cmd.config.liability_add.opt.name", "Название")
|
||||
_r("cmd.config.liability_add.opt.balance", "Остаток долга")
|
||||
_r("cmd.config.liability_add.opt.interest", "Процентная ставка (%)")
|
||||
_r("cmd.config.liability_add.opt.payment", "Ежемесячный платёж")
|
||||
_r("cmd.config.liability_add.ok", "Обязательство '{name}' добавлено (долг: {balance})")
|
||||
|
||||
_r("cmd.config.liability_list.help", "Список обязательств")
|
||||
_r("cmd.config.liability_list.empty", "Обязательства не добавлены")
|
||||
|
||||
_r("cmd.config.liability_remove.help", "Удалить обязательство по ID или названию")
|
||||
_r("cmd.config.liability_remove.arg", "ID или название обязательства")
|
||||
_r("cmd.config.liability_remove.ok", "Обязательство '{id}' удалено")
|
||||
_r("cmd.config.liability_remove.err", "Обязательство '{id}' не найдено")
|
||||
|
||||
# ── global ─────────────────────────────────────────────────────
|
||||
|
||||
_r("global.ok", "OK")
|
||||
|
||||
# ── prompts ───────────────────────────────────────────────────
|
||||
|
||||
_r("prompt.analyze", (
|
||||
"Ты — финансовый AI-ассистент. Проанализируй финансовую модель пользователя.\n"
|
||||
"\n"
|
||||
"### Модель (JSON):\n"
|
||||
"{model_json}\n"
|
||||
"\n"
|
||||
"### Прогноз на {months} месяцев:\n"
|
||||
"{forecast_json}\n"
|
||||
"\n"
|
||||
"### Валюта:\n"
|
||||
"Базовая валюта модели: {base_currency}\n"
|
||||
"Отображаемая валюта: {display_currency}\n"
|
||||
"\n"
|
||||
"Дай анализ по пунктам:\n"
|
||||
"1. Общее финансовое состояние\n"
|
||||
"2. Тренд денежного потока (рост/падение)\n"
|
||||
"3. Достаточность ликвидности\n"
|
||||
"4. Рекомендации по улучшению"
|
||||
))
|
||||
|
||||
_r("prompt.advice", (
|
||||
"Ты — финансовый AI-ассистент. Дай персональные рекомендации.\n"
|
||||
"\n"
|
||||
"### Модель:\n"
|
||||
"{model_json}\n"
|
||||
"\n"
|
||||
"### Прогноз:\n"
|
||||
"{forecast_json}\n"
|
||||
"\n"
|
||||
"### Валюта:\n"
|
||||
"Базовая валюта модели: {base_currency}\n"
|
||||
"Отображаемая валюта: {display_currency}\n"
|
||||
"\n"
|
||||
"Вопрос пользователя: {question}\n"
|
||||
"\n"
|
||||
"Ответь как опытный финансовый консультант."
|
||||
))
|
||||
|
||||
_r("prompt.scenario_comparison", (
|
||||
"Ты — финансовый AI-ассистент. Сравни сценарии прогноза.\n"
|
||||
"\n"
|
||||
"### Результаты сценариев:\n"
|
||||
"{scenarios_json}\n"
|
||||
"\n"
|
||||
"### Валюта:\n"
|
||||
"Базовая валюта модели: {base_currency}\n"
|
||||
"Отображаемая валюта: {display_currency}\n"
|
||||
"\n"
|
||||
"Дай рекомендацию: какой сценарий наиболее вероятен и почему."
|
||||
))
|
||||
|
||||
|
||||
_current_lang = "ru"
|
||||
|
||||
|
||||
class Translator:
|
||||
def __init__(self, lang: str | None = None):
|
||||
self.lang = lang or _current_lang
|
||||
if self.lang not in _TRANSLATIONS:
|
||||
_TRANSLATIONS[self.lang] = {}
|
||||
|
||||
def t(self, key: str, **kwargs) -> str:
|
||||
src = _TRANSLATIONS.get(self.lang) or _TRANSLATIONS.get("ru") or {}
|
||||
if key in src:
|
||||
template = src[key]
|
||||
elif "ru" in _TRANSLATIONS and key in _TRANSLATIONS["ru"]:
|
||||
template = _TRANSLATIONS["ru"][key]
|
||||
else:
|
||||
return key
|
||||
return template.format(**kwargs) if kwargs else template
|
||||
|
||||
|
||||
_GLOBAL = Translator()
|
||||
|
||||
|
||||
def setup_i18n() -> None:
|
||||
lang = os.environ.get("CF_LANG", "ru")
|
||||
if lang not in _TRANSLATIONS:
|
||||
_TRANSLATIONS[lang] = {}
|
||||
_GLOBAL.lang = lang
|
||||
|
||||
|
||||
def set_lang(lang: str) -> None:
|
||||
if lang not in _TRANSLATIONS:
|
||||
_TRANSLATIONS[lang] = {}
|
||||
_GLOBAL.lang = lang
|
||||
|
||||
|
||||
def get_lang() -> str:
|
||||
return _GLOBAL.lang
|
||||
|
||||
|
||||
def t(key: str, **kwargs) -> str:
|
||||
return _GLOBAL.t(key, **kwargs)
|
||||
+195
-66
@@ -6,7 +6,9 @@ from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from ai.assistant import AssistantService
|
||||
from cashflow_model import FinancialModel
|
||||
from cashflow_model import CurrencyConverter, FinancialModel
|
||||
from cli.config import app as config_app
|
||||
from cli.i18n import setup_i18n, t
|
||||
from engine.forecast import ForecastService
|
||||
from engine.scenarios import DEFAULT_SCENARIOS, ScenarioService
|
||||
from sync.excel_sync import ExcelSync
|
||||
@@ -16,12 +18,16 @@ try:
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
app = typer.Typer(name="cf", help="CashFlow Forecast - personal finance model")
|
||||
setup_i18n()
|
||||
|
||||
app = typer.Typer(name="cf", help=t("app.help"))
|
||||
console = Console()
|
||||
|
||||
DATA_DIR = Path("data")
|
||||
MODEL_PATH = DATA_DIR / "model.json"
|
||||
|
||||
CURRENCY_OPTION = typer.Option(None, "--currency", "-c", help=t("cmd.forecast.opt.currency"))
|
||||
|
||||
|
||||
def _load_model() -> FinancialModel:
|
||||
if MODEL_PATH.exists():
|
||||
@@ -33,169 +39,292 @@ def _save_model(model: FinancialModel) -> None:
|
||||
model.save(MODEL_PATH)
|
||||
|
||||
|
||||
def _get_converter(model: FinancialModel) -> CurrencyConverter:
|
||||
return CurrencyConverter(model.exchange_rates)
|
||||
|
||||
|
||||
def _fmt(amount: float, currency: str, symbol: str) -> str:
|
||||
return f"{symbol}{amount:,.2f}"
|
||||
|
||||
|
||||
def _resolve_currency(model: FinancialModel, currency: str | None) -> tuple[str, str]:
|
||||
target = currency or model.base_currency
|
||||
converter = _get_converter(model)
|
||||
symbol = converter.get_symbol(target)
|
||||
return target, symbol
|
||||
|
||||
|
||||
def _convert_value(
|
||||
converter: CurrencyConverter, amount: float, from_curr: str, to_curr: str
|
||||
) -> float:
|
||||
if to_curr == from_curr:
|
||||
return amount
|
||||
return converter.convert(amount, from_curr, to_curr)
|
||||
|
||||
|
||||
@app.command()
|
||||
def init() -> None:
|
||||
"""Создать пустую финансовую модель"""
|
||||
model = FinancialModel()
|
||||
_save_model(model)
|
||||
console.print("[green]OK[/green] Пустая модель создана в data/model.json")
|
||||
console.print(t("cmd.init.ok", path=str(MODEL_PATH)))
|
||||
|
||||
|
||||
@app.command()
|
||||
def forecast(
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев прогноза"),
|
||||
months: int = typer.Option(12, "--months", "-m", help=t("cmd.forecast.opt.months")),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""Запустить прогноз денежных потоков"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ForecastService(model)
|
||||
results = service.forecast_cashflow(months)
|
||||
summary = service.summary(months)
|
||||
|
||||
def _cv(val):
|
||||
return _convert_value(converter, val, base_curr, target_curr)
|
||||
|
||||
def _f(val):
|
||||
return _fmt(val, target_curr, symbol)
|
||||
|
||||
if results:
|
||||
table = Table(title=f"Прогноз на {months} мес.")
|
||||
table.add_column("Счёт", style="cyan")
|
||||
table.add_column("Месяц", style="white")
|
||||
table.add_column("Баланс", justify="right", style="green")
|
||||
table.add_column("Доход", justify="right")
|
||||
table.add_column("Расход", justify="right")
|
||||
table = Table(title=t("cmd.forecast.title", months=months, currency=target_curr))
|
||||
table.add_column(t("cmd.forecast.col.account"), style="cyan")
|
||||
table.add_column(t("cmd.forecast.col.month"), style="white")
|
||||
table.add_column(t("cmd.forecast.col.balance"), justify="right", style="green")
|
||||
table.add_column(t("cmd.forecast.col.income"), justify="right")
|
||||
table.add_column(t("cmd.forecast.col.expenses"), justify="right")
|
||||
|
||||
for r in results:
|
||||
table.add_row(
|
||||
r["account"], str(r["month"]),
|
||||
f"${r['balance']:.2f}",
|
||||
f"${r['income']:.2f}",
|
||||
f"${r['expenses']:.2f}",
|
||||
_f(_cv(r["balance"])),
|
||||
_f(_cv(r["income"])),
|
||||
_f(_cv(r["expenses"])),
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
console.print(f"\n[bold]Итог:[/bold] Баланс: ${summary['total_balance']:.2f} | "
|
||||
f"Доход: ${summary['total_income']:.2f} | "
|
||||
f"Расход: ${summary['total_expenses']:.2f}")
|
||||
cb = _cv(summary["total_balance"])
|
||||
ci = _cv(summary["total_income"])
|
||||
ce = _cv(summary["total_expenses"])
|
||||
console.print(t("cmd.forecast.total", balance=_f(cb), income=_f(ci), expenses=_f(ce)))
|
||||
|
||||
|
||||
@app.command()
|
||||
def scenario(
|
||||
name: str = typer.Argument("baseline", help="Имя сценария: baseline, optimistic, pessimistic"),
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
|
||||
name: str = typer.Argument("baseline", help=t("cmd.scenario.arg.name")),
|
||||
months: int = typer.Option(12, "--months", "-m", help=t("cmd.scenario.opt.months")),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""Применить сценарий и показать прогноз"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ScenarioService(model)
|
||||
|
||||
if name in DEFAULT_SCENARIOS:
|
||||
scenario_obj = DEFAULT_SCENARIOS[name]
|
||||
else:
|
||||
console.print(f"[red]Неизвестный сценарий: {name}[/red]")
|
||||
console.print(f"Доступны: {', '.join(DEFAULT_SCENARIOS.keys())}")
|
||||
console.print(f"[red]{t('cmd.scenario.err.unknown', name=name)}[/red]")
|
||||
console.print(t("cmd.scenario.err.available", scenarios=", ".join(DEFAULT_SCENARIOS.keys())))
|
||||
raise typer.Exit(1)
|
||||
|
||||
result = service.apply(scenario_obj, months)
|
||||
console.print(f"[bold]Сценарий:[/bold] {result['scenario']}")
|
||||
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
||||
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
||||
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
|
||||
console.print(f"[bold]{t('cmd.scenario.label')}:[/bold] {result['scenario']}")
|
||||
console.print(f"[dim]{result['scenario_description']}[/dim]")
|
||||
console.print(f"Баланс: ${result['total_balance']:.2f}")
|
||||
console.print(f"Доход: ${result['total_income']:.2f}")
|
||||
console.print(f"Расход: ${result['total_expenses']:.2f}")
|
||||
console.print(f"{t('cmd.scenario.label.balance')}: {_fmt(balance, target_curr, symbol)}")
|
||||
console.print(f"{t('cmd.scenario.label.income')}: {_fmt(income, target_curr, symbol)}")
|
||||
console.print(f"{t('cmd.scenario.label.expenses')}: {_fmt(expenses, target_curr, symbol)}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def whatif(
|
||||
income_mult: float = typer.Option(1.0, "--income", "-i", help="Множитель дохода"),
|
||||
expense_mult: float = typer.Option(1.0, "--expense", "-e", help="Множитель расхода"),
|
||||
growth_mult: float = typer.Option(1.0, "--growth", "-g", help="Множитель роста активов"),
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
|
||||
income_mult: float = typer.Option(1.0, "--income", "-i", help=t("cmd.whatif.opt.income")),
|
||||
expense_mult: float = typer.Option(1.0, "--expense", "-e", help=t("cmd.whatif.opt.expense")),
|
||||
growth_mult: float = typer.Option(1.0, "--growth", "-g", help=t("cmd.whatif.opt.growth")),
|
||||
months: int = typer.Option(12, "--months", "-m", help=t("cmd.whatif.opt.months")),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""What-if анализ с произвольными множителями"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ScenarioService(model)
|
||||
result = service.what_if(income_mult, expense_mult, growth_mult, months)
|
||||
|
||||
console.print("[bold]What-if анализ[/bold]")
|
||||
console.print(f"Доход x{income_mult} | Расход x{expense_mult} | Рост x{growth_mult}")
|
||||
console.print(f"Баланс: ${result['total_balance']:.2f}")
|
||||
console.print(f"Доход: ${result['total_income']:.2f}")
|
||||
console.print(f"Расход: ${result['total_expenses']:.2f}")
|
||||
balance = _convert_value(converter, result["total_balance"], base_curr, target_curr)
|
||||
income = _convert_value(converter, result["total_income"], base_curr, target_curr)
|
||||
expenses = _convert_value(converter, result["total_expenses"], base_curr, target_curr)
|
||||
console.print(f"[bold]{t('cmd.whatif.title')}[/bold]")
|
||||
console.print(t("cmd.whatif.params", income=income_mult, expense=expense_mult, growth=growth_mult))
|
||||
console.print(f"{t('cmd.scenario.label.balance')}: {_fmt(balance, target_curr, symbol)}")
|
||||
console.print(f"{t('cmd.scenario.label.income')}: {_fmt(income, target_curr, symbol)}")
|
||||
console.print(f"{t('cmd.scenario.label.expenses')}: {_fmt(expenses, target_curr, symbol)}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def compare(
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев"),
|
||||
months: int = typer.Option(12, "--months", "-m", help=t("cmd.compare.opt.months")),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""Сравнить все сценарии"""
|
||||
model = _load_model()
|
||||
target_curr, symbol = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
base_curr = model.base_currency
|
||||
service = ScenarioService(model)
|
||||
results = service.compare(months)
|
||||
|
||||
table = Table(title="Сравнение сценариев")
|
||||
table.add_column("Сценарий", style="cyan")
|
||||
table.add_column("Баланс", justify="right")
|
||||
table.add_column("Доход", justify="right")
|
||||
table.add_column("Расход", justify="right")
|
||||
table = Table(title=t("cmd.compare.title", currency=target_curr))
|
||||
table.add_column(t("cmd.compare.col.scenario"), style="cyan")
|
||||
table.add_column(t("cmd.forecast.col.balance"), justify="right")
|
||||
table.add_column(t("cmd.forecast.col.income"), justify="right")
|
||||
table.add_column(t("cmd.forecast.col.expenses"), justify="right")
|
||||
|
||||
for name, r in results.items():
|
||||
table.add_row(
|
||||
name,
|
||||
f"${r['total_balance']:.2f}",
|
||||
f"${r['total_income']:.2f}",
|
||||
f"${r['total_expenses']:.2f}",
|
||||
)
|
||||
b = _convert_value(converter, r["total_balance"], base_curr, target_curr)
|
||||
i = _convert_value(converter, r["total_income"], base_curr, target_curr)
|
||||
e = _convert_value(converter, r["total_expenses"], base_curr, target_curr)
|
||||
fb = _fmt(b, target_curr, symbol)
|
||||
fi = _fmt(i, target_curr, symbol)
|
||||
fe = _fmt(e, target_curr, symbol)
|
||||
table.add_row(name, fb, fi, fe)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command()
|
||||
def import_xlsx(
|
||||
path: str = typer.Argument(..., help="Путь к .xlsx файлу"),
|
||||
path: str = typer.Argument(..., help=t("cmd.import.arg.path")),
|
||||
) -> None:
|
||||
"""Импорт данных из Excel"""
|
||||
sync = ExcelSync()
|
||||
try:
|
||||
model = sync.import_model(path)
|
||||
_save_model(model)
|
||||
console.print(f"[green]OK[/green] Импортировано: {len(model.accounts)} счетов, "
|
||||
f"{len(model.transactions)} транзакций, "
|
||||
f"{len(model.recurring)} регулярных платежей, "
|
||||
f"{len(model.assets)} активов, "
|
||||
f"{len(model.liabilities)} обязательств")
|
||||
console.print(t("cmd.import.ok",
|
||||
accounts=len(model.accounts),
|
||||
transactions=len(model.transactions),
|
||||
recurring=len(model.recurring),
|
||||
assets=len(model.assets),
|
||||
liabilities=len(model.liabilities)))
|
||||
console.print(t("cmd.import.base_currency", currency=model.base_currency))
|
||||
except Exception as e:
|
||||
console.print(f"[red]Ошибка импорта: {e}[/red]")
|
||||
console.print(f"[red]{t('cmd.import.err', error=str(e))}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def export_xlsx(
|
||||
path: str = typer.Argument("exports/forecast.xlsx", help="Путь для .xlsx файла"),
|
||||
path: str = typer.Argument("exports/forecast.xlsx", help=t("cmd.export.arg.path")),
|
||||
) -> None:
|
||||
"""Экспорт модели в Excel"""
|
||||
model = _load_model()
|
||||
sync = ExcelSync()
|
||||
try:
|
||||
sync.export_model(model, path)
|
||||
console.print(f"[green]OK[/green] Модель экспортирована в {path}")
|
||||
console.print(t("cmd.export.ok", path=path))
|
||||
except Exception as e:
|
||||
console.print(f"[red]Ошибка экспорта: {e}[/red]")
|
||||
console.print(f"[red]{t('cmd.export.err', error=str(e))}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def analyze(
|
||||
months: int = typer.Option(12, "--months", "-m", help="Количество месяцев для анализа"),
|
||||
months: int = typer.Option(12, "--months", "-m", help=t("cmd.analyze.opt.months")),
|
||||
currency: str | None = CURRENCY_OPTION,
|
||||
) -> None:
|
||||
"""AI-анализ финансовой модели"""
|
||||
model = _load_model()
|
||||
assistant = AssistantService(model)
|
||||
target_curr, _ = _resolve_currency(model, currency)
|
||||
converter = _get_converter(model)
|
||||
assistant = AssistantService(model, converter=converter, display_currency=target_curr)
|
||||
result = assistant.analyze(months)
|
||||
|
||||
console.print("[bold]Промпт для AI:[/bold]")
|
||||
console.print(f"[bold]{t('cmd.analyze.title.prompt')}:[/bold]")
|
||||
console.print(result["prompt"][:500] + "...\n")
|
||||
|
||||
console.print("[bold]Сводка:[/bold]")
|
||||
s = result["summary"]
|
||||
console.print(f"Баланс: ${s['total_balance']:.2f}")
|
||||
console.print(f"Доход: ${s['total_income']:.2f}")
|
||||
console.print(f"Расход: ${s['total_expenses']:.2f}")
|
||||
console.print(
|
||||
"\n[yellow]AI-ответ: заглушка. Подключите реальный API в ai/assistant.py[/yellow]"
|
||||
)
|
||||
console.print(f"[bold]{t('cmd.analyze.title.summary')}:[/bold]")
|
||||
console.print(t("cmd.analyze.base_currency", currency=model.base_currency))
|
||||
console.print(f"\n[yellow]{t('cmd.analyze.stub')}[/yellow]")
|
||||
|
||||
|
||||
@app.command()
|
||||
def info() -> None:
|
||||
"""Сводка всей финансовой модели"""
|
||||
model = _load_model()
|
||||
converter = _get_converter(model)
|
||||
symbol = converter.get_symbol(model.base_currency)
|
||||
|
||||
console.print(f"[bold]{t('cmd.info.metadata')}[/bold]")
|
||||
console.print(f" {t('cmd.info.base_currency', currency=model.base_currency, symbol=symbol)}")
|
||||
console.print(f" {t('cmd.info.accounts', count=len(model.accounts))}")
|
||||
console.print(f" {t('cmd.info.transactions', count=len(model.transactions))}")
|
||||
console.print(f" {t('cmd.info.recurring', count=len(model.recurring))}")
|
||||
console.print(f" {t('cmd.info.assets', count=len(model.assets))}")
|
||||
console.print(f" {t('cmd.info.liabilities', count=len(model.liabilities))}")
|
||||
console.print(f" {t('cmd.info.rates', count=len(model.exchange_rates))}")
|
||||
|
||||
if model.accounts:
|
||||
tbl = Table(title=t("table.accounts.title"))
|
||||
tbl.add_column(t("table.accounts.col.name"), style="cyan")
|
||||
tbl.add_column(t("table.accounts.col.currency"))
|
||||
tbl.add_column(t("table.accounts.col.balance"), justify="right")
|
||||
for a in model.accounts:
|
||||
tbl.add_row(a.name, a.currency, f"{a.balance:,.2f}")
|
||||
console.print(tbl)
|
||||
|
||||
if model.transactions:
|
||||
tbl = Table(title=t("table.transactions.title"))
|
||||
tbl.add_column(t("table.transactions.col.date"))
|
||||
tbl.add_column(t("table.transactions.col.category"))
|
||||
tbl.add_column(t("table.transactions.col.amount"), justify="right")
|
||||
tbl.add_column(t("table.transactions.col.description"))
|
||||
for tx in model.transactions[:10]:
|
||||
tbl.add_row(tx.date, tx.category, f"{tx.amount:,.2f}", tx.description)
|
||||
if len(model.transactions) > 10:
|
||||
console.print(tbl)
|
||||
console.print(t("cmd.info.more", count=len(model.transactions) - 10))
|
||||
else:
|
||||
console.print(tbl)
|
||||
|
||||
if model.recurring:
|
||||
tbl = Table(title=t("table.recurring.title"))
|
||||
tbl.add_column(t("table.recurring.col.period"))
|
||||
tbl.add_column(t("table.recurring.col.amount"), justify="right")
|
||||
tbl.add_column(t("table.recurring.col.category"))
|
||||
for r in model.recurring:
|
||||
tbl.add_row(r.frequency, f"{r.amount:,.2f}", r.category)
|
||||
console.print(tbl)
|
||||
|
||||
if model.assets:
|
||||
tbl = Table(title=t("table.assets.title"))
|
||||
tbl.add_column(t("table.assets.col.name"), style="cyan")
|
||||
tbl.add_column(t("table.assets.col.value"), justify="right")
|
||||
tbl.add_column(t("table.assets.col.growth"), justify="right")
|
||||
for a in model.assets:
|
||||
tbl.add_row(a.name, f"{a.value:,.2f}", f"{a.growth_rate:.1f}%")
|
||||
console.print(tbl)
|
||||
|
||||
if model.liabilities:
|
||||
tbl = Table(title=t("table.liabilities.title"))
|
||||
tbl.add_column(t("table.liabilities.col.name"), style="cyan")
|
||||
tbl.add_column(t("table.liabilities.col.balance"), justify="right")
|
||||
tbl.add_column(t("table.liabilities.col.rate"), justify="right")
|
||||
tbl.add_column(t("table.liabilities.col.payment"), justify="right")
|
||||
for li in model.liabilities:
|
||||
tbl.add_row(li.name, f"{li.balance:,.2f}", f"{li.interest:.1f}%", f"{li.payment:,.2f}")
|
||||
console.print(tbl)
|
||||
|
||||
|
||||
app.add_typer(config_app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
Reference in New Issue
Block a user