diff --git a/.gitignore b/.gitignore index ad4a1f1..e628428 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,9 @@ poetry.toml pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python + +# MetaAgent temp files +.temp/ + +# opencode local state +.omo/ diff --git a/AGENTS.md b/AGENTS.md index b79af86..4cc65ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,35 +1,56 @@ # MetaAgent -Этот проект использует [MetaAgent](.agent/src/META_AGENT_GUIDE.md) v2.1.0 — +Этот проект использует [MetaAgent](.agent/src/GUIDE.md) v3.0.0 — набор инструкций для AI-агента. ## Контекст MetaAgent | Ресурс | Путь | |--------|------| -| Главная инструкция | .agent/src/META_AGENT_GUIDE.md | -| Протоколы фаз | .agent/src/PROTOCOLS/ | -| Шаблоны артефактов | .agent/src/TEMPLATES/ | -| Границы (что разрешено/запрещено) | .agent/src/BOUNDARIES.md | -| Правила проекта | .agent/rules/project-rules.md | -| Примеры работы | .agent/src/WORKFLOW.md | -| Версия | .agent/src/VERSION | +| Главная инструкция | `.agent/src/GUIDE.md` | +| Протоколы фаз | `.agent/src/PROTOCOLS/` | +| Команды (on-demand) | `.agent/src/COMMANDS/` | +| Шаблоны артефактов | `.agent/src/TEMPLATES/` | +| Границы (что разрешено/запрещено) | `.agent/src/BOUNDARIES.md` | +| История версий | `.agent/src/CHANGELOG.md` | +| Правила проекта | `.agent/rules/project-rules.md` | +| Пример работы | `.agent/src/WORKFLOW.md` | +| Версия | `.agent/src/VERSION` | ## Состояние сессии (если инициализировано) | Артефакт | Путь | |----------|------| -| Чекпоинты сессии | .agent/checkpoints.json | -| Манифест задач | .agent/task-manifest.json | -| Сводка для exec-агента | .agent/handoff-summary.md | -| Анализ репозитория | .agent/analysis-report.md | +| Чекпоинты сессии | `.agent/checkpoints.json` | +| Слепок проекта | `.agent/context/project-state.md` | +| Анализ репозитория | `.agent/context/analysis-report.md` | +| Дорожная карта | `.agent/roadmap/sources.md` | +| Манифест задач | `.agent/tasks/manifest.json` | +| Сводка для следующего агента | `.agent/handoff-summary.md` | +| Сводка сессии | `.agent/session-summary.md` | -## Для исполнительного агента +## Для агента -1. **Прочитай** .agent/src/META_AGENT_GUIDE.md — пойми жизненный цикл MetaAgent. -2. **Прочитай** .agent/src/BOUNDARIES.md — соблюдай границы. -3. **Прочитай** .agent/rules/project-rules.md — выполни пользовательские правила. -4. **Проверь** .agent/checkpoints.json — если существует, используй как состояние сессии. -5. **Проверь** .agent/task-manifest.json — если существует, выполняй задачи по порядку. -6. Если .agent/ не инициализирован или устарел — запусти install.sh --update для - обновления исходников MetaAgent до актуальной версии. \ No newline at end of file +Жизненный цикл MetaAgent v3.0.0: + +``` +INIT → ANALYSE → ROADMAP → [DESIGN] → DECOMPOSITION → EXECUTION → METASTATE → HANDOFF +``` + +1. **Прочитай** `.agent/src/GUIDE.md` — пойми цикл и доступные команды. +2. **Прочитай** `.agent/src/BOUNDARIES.md` — соблюдай границы. +3. **Прочитай** `.agent/rules/project-rules.md` — выполни правила пользователя. +4. **Проверь** `.agent/checkpoints.json` — если существует, используй как состояние сессии. +5. **Проверь** `.agent/context/project-state.md` — получи актуальную картину. +6. **Проверь** `.agent/tasks/manifest.json` — если существует, выполняй задачи по порядку. +7. Если `.agent/` не инициализирован или устарел — запусти `install.sh --update` для + обновления исходников MetaAgent до актуальной версии. + +## Команды (on-demand) + +В любой момент пользователь может вызвать: +- `/adr` — записать архитектурное решение +- `/red-team` — попытаться сломать дизайн +- `/risk-register` — зафиксировать допущения +- `/alt-arch` — описать альтернативу +- `/invariant-tests` — тесты-инварианты для ADR diff --git a/ai/__init__.py b/ai/__init__.py deleted file mode 100644 index 9fd77b2..0000000 --- a/ai/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from ai import prompts -from ai.assistant import AssistantError, AssistantService - -__all__ = ["AssistantService", "AssistantError", "prompts"] diff --git a/ai/assistant.py b/ai/assistant.py deleted file mode 100644 index 8321927..0000000 --- a/ai/assistant.py +++ /dev/null @@ -1,69 +0,0 @@ -import json - -from ai import prompts -from cashflow_model import CurrencyConverter, FinancialModel -from engine.forecast import ForecastService - - -class AssistantError(Exception): - pass - - -class AssistantService: - def __init__( - self, - model: FinancialModel, - converter: CurrencyConverter | None = None, - display_currency: str | None = None, - ): - self.model = model - self.converter = converter or CurrencyConverter(model.exchange_rates) - self.display_currency = display_currency or model.base_currency - - def analyze(self, months: int = 12) -> dict: - forecast_service = ForecastService(self.model) - forecast_result = forecast_service.forecast_cashflow(months) - summary = forecast_service.summary(months) - - prompt = prompts.format_context( - model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False), - forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False), - months=months, - base_currency=self.model.base_currency, - display_currency=self.display_currency, - ) - - return { - "prompt": prompt, - "summary": summary, - "forecast": forecast_result, - "ai_response": None, - } - - def advice(self, question: str, months: int = 12) -> dict: - forecast_service = ForecastService(self.model) - forecast_result = forecast_service.forecast_cashflow(months) - - prompt = prompts.ADVICE_PROMPT.format( - model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False), - forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False), - question=question, - base_currency=self.model.base_currency, - display_currency=self.display_currency, - ) - - return { - "prompt": prompt, - "ai_response": None, - } - - def compare_scenarios(self, scenarios_json: str) -> dict: - prompt = prompts.SCENARIO_COMPARISON_PROMPT.format( - scenarios_json=scenarios_json, - base_currency=self.model.base_currency, - display_currency=self.display_currency, - ) - return { - "prompt": prompt, - "ai_response": None, - } diff --git a/ai/prompts.py b/ai/prompts.py deleted file mode 100644 index 02e4b43..0000000 --- a/ai/prompts.py +++ /dev/null @@ -1,21 +0,0 @@ -from cli.i18n import t - -ANALYZE_PROMPT = t("prompt.analyze") -ADVICE_PROMPT = t("prompt.advice") -SCENARIO_COMPARISON_PROMPT = t("prompt.scenario_comparison") - - -def format_context( - model_json: str, - forecast_json: str, - months: int = 12, - base_currency: str = "RUB", - display_currency: str = "RUB", -) -> str: - return ANALYZE_PROMPT.format( - model_json=model_json, - forecast_json=forecast_json, - months=months, - base_currency=base_currency, - display_currency=display_currency, - ) diff --git a/cashflow_model/__init__.py b/cashflow_model/__init__.py deleted file mode 100644 index 033ab9b..0000000 --- a/cashflow_model/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from cashflow_model.account import Account -from cashflow_model.asset import Asset -from cashflow_model.currency import CURRENCY_SYMBOLS, CurrencyConverter, CurrencyError, ExchangeRate -from cashflow_model.liability import Liability -from cashflow_model.model import FinancialModel -from cashflow_model.recurring import RecurringCashflow -from cashflow_model.scenario import ForecastScenario -from cashflow_model.transaction import Transaction - -__all__ = [ - "Account", - "Transaction", - "RecurringCashflow", - "Asset", - "Liability", - "ForecastScenario", - "FinancialModel", - "ExchangeRate", - "CurrencyConverter", - "CurrencyError", - "CURRENCY_SYMBOLS", -] diff --git a/cashflow_model/account.py b/cashflow_model/account.py deleted file mode 100644 index f70ca25..0000000 --- a/cashflow_model/account.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass, field -from uuid import UUID, uuid4 - - -@dataclass -class Account: - id: UUID = field(default_factory=uuid4) - name: str = "" - currency: str = "USD" - balance: float = 0.0 - - def to_dict(self) -> dict: - return { - "id": str(self.id), - "name": self.name, - "currency": self.currency, - "balance": self.balance, - } - - @classmethod - def from_dict(cls, data: dict) -> "Account": - return cls( - id=UUID(data["id"]), - name=data["name"], - currency=data.get("currency", "USD"), - balance=data.get("balance", 0.0), - ) diff --git a/cashflow_model/asset.py b/cashflow_model/asset.py deleted file mode 100644 index e68f670..0000000 --- a/cashflow_model/asset.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass, field -from uuid import UUID, uuid4 - - -@dataclass -class Asset: - id: UUID = field(default_factory=uuid4) - name: str = "" - value: float = 0.0 - growth_rate: float = 0.0 - - def to_dict(self) -> dict: - return { - "id": str(self.id), - "name": self.name, - "value": self.value, - "growth_rate": self.growth_rate, - } - - @classmethod - def from_dict(cls, data: dict) -> "Asset": - return cls( - id=UUID(data["id"]), - name=data["name"], - value=data.get("value", 0.0), - growth_rate=data.get("growth_rate", 0.0), - ) diff --git a/cashflow_model/currency.py b/cashflow_model/currency.py deleted file mode 100644 index 1a0ea53..0000000 --- a/cashflow_model/currency.py +++ /dev/null @@ -1,79 +0,0 @@ -from dataclasses import dataclass - -CURRENCY_SYMBOLS = { - "RUB": "₽", - "USD": "$", - "EUR": "€", - "GBP": "£", - "CNY": "¥", - "JPY": "¥", - "KZT": "₸", - "UAH": "₴", -} - - -@dataclass -class ExchangeRate: - from_currency: str = "USD" - to_currency: str = "RUB" - rate: float = 80.0 - - def to_dict(self) -> dict: - return { - "from_currency": self.from_currency, - "to_currency": self.to_currency, - "rate": self.rate, - } - - @classmethod - def from_dict(cls, data: dict) -> "ExchangeRate": - return cls( - from_currency=data.get("from_currency", "USD"), - to_currency=data.get("to_currency", "RUB"), - rate=data.get("rate", 80.0), - ) - - -DEFAULT_RATES: list[ExchangeRate] = [ - ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0), -] - - -class CurrencyError(Exception): - pass - - -class CurrencyConverter: - def __init__(self, rates: list[ExchangeRate] | None = None): - self._rates: dict[tuple[str, str], float] = {} - if rates: - for r in rates: - self.set_rate(r.from_currency, r.to_currency, r.rate) - - def set_rate(self, from_currency: str, to_currency: str, rate: float) -> None: - if rate <= 0: - raise CurrencyError(f"Rate must be positive: {rate}") - self._rates[(from_currency, to_currency)] = rate - inverse = 1.0 / rate - self._rates[(to_currency, from_currency)] = inverse - - def get_rate(self, from_currency: str, to_currency: str) -> float: - if from_currency == to_currency: - return 1.0 - try: - return self._rates[(from_currency, to_currency)] - except KeyError: - raise CurrencyError(f"No exchange rate: {from_currency} → {to_currency}") - - def convert(self, amount: float, from_currency: str, to_currency: str) -> float: - if from_currency == to_currency: - return amount - rate = self.get_rate(from_currency, to_currency) - return round(amount * rate, 2) - - def get_symbol(self, currency: str) -> str: - return CURRENCY_SYMBOLS.get(currency, currency) - - @classmethod - def with_defaults(cls) -> "CurrencyConverter": - return cls(DEFAULT_RATES) diff --git a/cashflow_model/liability.py b/cashflow_model/liability.py deleted file mode 100644 index bdbaa01..0000000 --- a/cashflow_model/liability.py +++ /dev/null @@ -1,30 +0,0 @@ -from dataclasses import dataclass, field -from uuid import UUID, uuid4 - - -@dataclass -class Liability: - id: UUID = field(default_factory=uuid4) - name: str = "" - balance: float = 0.0 - interest: float = 0.0 - payment: float = 0.0 - - def to_dict(self) -> dict: - return { - "id": str(self.id), - "name": self.name, - "balance": self.balance, - "interest": self.interest, - "payment": self.payment, - } - - @classmethod - def from_dict(cls, data: dict) -> "Liability": - return cls( - id=UUID(data["id"]), - name=data["name"], - balance=data.get("balance", 0.0), - interest=data.get("interest", 0.0), - payment=data.get("payment", 0.0), - ) diff --git a/cashflow_model/model.py b/cashflow_model/model.py deleted file mode 100644 index 77d97d2..0000000 --- a/cashflow_model/model.py +++ /dev/null @@ -1,61 +0,0 @@ -import json -from dataclasses import dataclass, field -from pathlib import Path - -from cashflow_model.account import Account -from cashflow_model.asset import Asset -from cashflow_model.currency import DEFAULT_RATES, ExchangeRate -from cashflow_model.liability import Liability -from cashflow_model.recurring import RecurringCashflow -from cashflow_model.scenario import ForecastScenario -from cashflow_model.transaction import Transaction - - -@dataclass -class FinancialModel: - base_currency: str = "RUB" - accounts: list[Account] = field(default_factory=list) - transactions: list[Transaction] = field(default_factory=list) - recurring: list[RecurringCashflow] = field(default_factory=list) - assets: list[Asset] = field(default_factory=list) - liabilities: list[Liability] = field(default_factory=list) - scenarios: list[ForecastScenario] = field(default_factory=list) - exchange_rates: list[ExchangeRate] = field(default_factory=lambda: DEFAULT_RATES.copy()) - - def to_dict(self) -> dict: - return { - "base_currency": self.base_currency, - "accounts": [a.to_dict() for a in self.accounts], - "transactions": [t.to_dict() for t in self.transactions], - "recurring": [r.to_dict() for r in self.recurring], - "assets": [a.to_dict() for a in self.assets], - "liabilities": [li.to_dict() for li in self.liabilities], - "scenarios": [s.to_dict() for s in self.scenarios], - "exchange_rates": [r.to_dict() for r in self.exchange_rates], - } - - @classmethod - def from_dict(cls, data: dict) -> "FinancialModel": - return cls( - base_currency=data.get("base_currency", "RUB"), - accounts=[Account.from_dict(a) for a in data.get("accounts", [])], - transactions=[Transaction.from_dict(t) for t in data.get("transactions", [])], - recurring=[RecurringCashflow.from_dict(r) for r in data.get("recurring", [])], - assets=[Asset.from_dict(a) for a in data.get("assets", [])], - liabilities=[Liability.from_dict(li) for li in data.get("liabilities", [])], - scenarios=[ForecastScenario.from_dict(s) for s in data.get("scenarios", [])], - exchange_rates=[ExchangeRate.from_dict(r) for r in data.get("exchange_rates", [])], - ) - - def save(self, path: str | Path) -> None: - path = Path(path) - path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "w", encoding="utf-8") as f: - json.dump(self.to_dict(), f, indent=2, ensure_ascii=False) - - @classmethod - def load(cls, path: str | Path) -> "FinancialModel": - path = Path(path) - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - return cls.from_dict(data) diff --git a/cashflow_model/recurring.py b/cashflow_model/recurring.py deleted file mode 100644 index 6ca69f4..0000000 --- a/cashflow_model/recurring.py +++ /dev/null @@ -1,33 +0,0 @@ -from dataclasses import dataclass, field -from uuid import UUID, uuid4 - - -@dataclass -class RecurringCashflow: - id: UUID = field(default_factory=uuid4) - start_date: str = "" - end_date: str = "" - frequency: str = "monthly" - amount: float = 0.0 - category: str = "" - - def to_dict(self) -> dict: - return { - "id": str(self.id), - "start_date": self.start_date, - "end_date": self.end_date, - "frequency": self.frequency, - "amount": self.amount, - "category": self.category, - } - - @classmethod - def from_dict(cls, data: dict) -> "RecurringCashflow": - return cls( - id=UUID(data["id"]), - start_date=data.get("start_date", ""), - end_date=data.get("end_date", ""), - frequency=data.get("frequency", "monthly"), - amount=data.get("amount", 0.0), - category=data.get("category", ""), - ) diff --git a/cashflow_model/scenario.py b/cashflow_model/scenario.py deleted file mode 100644 index 349c5ba..0000000 --- a/cashflow_model/scenario.py +++ /dev/null @@ -1,33 +0,0 @@ -from dataclasses import dataclass, field -from uuid import UUID, uuid4 - - -@dataclass -class ForecastScenario: - id: UUID = field(default_factory=uuid4) - name: str = "baseline" - income_multiplier: float = 1.0 - expense_multiplier: float = 1.0 - growth_multiplier: float = 1.0 - description: str = "" - - def to_dict(self) -> dict: - return { - "id": str(self.id), - "name": self.name, - "income_multiplier": self.income_multiplier, - "expense_multiplier": self.expense_multiplier, - "growth_multiplier": self.growth_multiplier, - "description": self.description, - } - - @classmethod - def from_dict(cls, data: dict) -> "ForecastScenario": - return cls( - id=UUID(data["id"]), - name=data["name"], - income_multiplier=data.get("income_multiplier", 1.0), - expense_multiplier=data.get("expense_multiplier", 1.0), - growth_multiplier=data.get("growth_multiplier", 1.0), - description=data.get("description", ""), - ) diff --git a/cashflow_model/transaction.py b/cashflow_model/transaction.py deleted file mode 100644 index 0039556..0000000 --- a/cashflow_model/transaction.py +++ /dev/null @@ -1,33 +0,0 @@ -from dataclasses import dataclass, field -from uuid import UUID, uuid4 - - -@dataclass -class Transaction: - id: UUID = field(default_factory=uuid4) - date: str = "" - account: str = "" - category: str = "" - amount: float = 0.0 - description: str = "" - - def to_dict(self) -> dict: - return { - "id": str(self.id), - "date": self.date, - "account": self.account, - "category": self.category, - "amount": self.amount, - "description": self.description, - } - - @classmethod - def from_dict(cls, data: dict) -> "Transaction": - return cls( - id=UUID(data["id"]), - date=data["date"], - account=data.get("account", ""), - category=data.get("category", ""), - amount=data.get("amount", 0.0), - description=data.get("description", ""), - ) diff --git a/cli/__init__.py b/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cli/config.py b/cli/config.py deleted file mode 100644 index ca2c27e..0000000 --- a/cli/config.py +++ /dev/null @@ -1,428 +0,0 @@ -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] diff --git a/cli/i18n.py b/cli/i18n.py deleted file mode 100644 index 9d7c753..0000000 --- a/cli/i18n.py +++ /dev/null @@ -1,350 +0,0 @@ -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) diff --git a/cli/main.py b/cli/main.py deleted file mode 100644 index e317d31..0000000 --- a/cli/main.py +++ /dev/null @@ -1,330 +0,0 @@ -import sys -from pathlib import Path - -import typer -from rich.console import Console -from rich.table import Table - -from ai.assistant import AssistantService -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 - -try: - sys.stdout.reconfigure(encoding="utf-8") -except (AttributeError, OSError): - pass - -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(): - return FinancialModel.load(MODEL_PATH) - return FinancialModel() - - -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(t("cmd.init.ok", path=str(MODEL_PATH))) - - -@app.command() -def forecast( - 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=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(_cv(r["balance"])), - _f(_cv(r["income"])), - _f(_cv(r["expenses"])), - ) - console.print(table) - - 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=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]{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) - 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"{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=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) - - 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=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=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(): - 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=t("cmd.import.arg.path")), -) -> None: - """Импорт данных из Excel""" - sync = ExcelSync() - try: - model = sync.import_model(path) - _save_model(model) - 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]{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=t("cmd.export.arg.path")), -) -> None: - """Экспорт модели в Excel""" - model = _load_model() - sync = ExcelSync() - try: - sync.export_model(model, path) - console.print(t("cmd.export.ok", path=path)) - except Exception as e: - 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=t("cmd.analyze.opt.months")), - currency: str | None = CURRENCY_OPTION, -) -> None: - """AI-анализ финансовой модели""" - model = _load_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(f"[bold]{t('cmd.analyze.title.prompt')}:[/bold]") - console.print(result["prompt"][:500] + "...\n") - - 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() diff --git a/data/model.json b/data/model.json index 4b6532b..a0168ec 100644 --- a/data/model.json +++ b/data/model.json @@ -3,20 +3,20 @@ "base_currency": "RUB", "accounts": [ { - "id": "0283653e-2ebd-48fe-8b64-90ead1804e60", + "id": "633c38bd-acdf-4e40-b7ef-64188e3a741d", "name": "Основной", "currency": "RUB", - "balance": 50000.0 + "balance": "50000.0" } ], "transactions": [], "recurring": [], "assets": [ { - "id": "a4557779-4bb7-4b3b-b88e-1e436c3e6e43", + "id": "d15f81dd-8235-4f93-8452-ba0db359d843", "name": "Акции", - "value": 100000.0, - "growth_rate": 8.0 + "value": "100000.0", + "growth_rate": "8.0" } ], "liabilities": [], @@ -25,7 +25,7 @@ { "from_currency": "USD", "to_currency": "RUB", - "rate": 80.0 + "rate": "80" } ] } \ No newline at end of file diff --git a/engine/__init__.py b/engine/__init__.py deleted file mode 100644 index 627c049..0000000 --- a/engine/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from engine.forecast import ForecastError, ForecastService -from engine.scenarios import DEFAULT_SCENARIOS, ScenarioError, ScenarioService - -__all__ = [ - "ForecastService", - "ForecastError", - "ScenarioService", - "ScenarioError", - "DEFAULT_SCENARIOS", -] diff --git a/engine/forecast.py b/engine/forecast.py deleted file mode 100644 index eea04fe..0000000 --- a/engine/forecast.py +++ /dev/null @@ -1,103 +0,0 @@ -from copy import deepcopy - -from cashflow_model import Account, FinancialModel - - -class ForecastError(Exception): - pass - - -class ForecastService: - def __init__(self, model: FinancialModel): - self.model = deepcopy(model) - - def forecast_cashflow(self, months: int = 12) -> list[dict]: - if months < 1: - raise ForecastError("months must be >= 1") - - results = [] - for account in self.model.accounts: - monthly = self._project_account(account, months) - for m in range(months): - results.append({ - "account": account.name, - "month": m + 1, - "balance": monthly[m]["balance"], - "income": monthly[m]["income"], - "expenses": monthly[m]["expenses"], - }) - - # Asset growth — once per month, distributed across accounts proportionally - for m in range(months): - total_growth = sum( - a.value * a.growth_rate / 100 / 12 - for a in self.model.assets - ) - month_rows = [r for r in results if r["month"] == m + 1] - total_bal = sum(r["balance"] for r in month_rows) or 1 - for r in month_rows: - share = r["balance"] / total_bal - r["income"] = round(r["income"] + total_growth * share, 2) - r["balance"] = round(r["balance"] + total_growth * share, 2) - - # Compound asset values for next month - for a in self.model.assets: - a.value += a.value * a.growth_rate / 100 / 12 - - return results - - def _project_account(self, account: Account, months: int) -> list[dict]: - balance = account.balance - monthly = [] - for m in range(months): - income = 0.0 - expenses = 0.0 - - for t in self.model.transactions: - if t.account == str(account.id): - if t.amount > 0: - income += t.amount - else: - expenses += abs(t.amount) - - for r in self.model.recurring: - if r.amount > 0: - income += r.amount - else: - expenses += abs(r.amount) - - expenses += self._liability_cost(account) - - balance += income - expenses - - monthly.append({ - "balance": round(balance, 2), - "income": round(income, 2), - "expenses": round(expenses, 2), - }) - return monthly - - def _liability_cost(self, account: Account) -> float: - total = 0.0 - for liability in self.model.liabilities: - interest_cost = liability.balance * liability.interest / 100 / 12 - total += interest_cost - liability.balance -= liability.payment - interest_cost - if liability.balance < 0: - liability.balance = 0 - return total - - def summary(self, months: int = 12) -> dict: - results = self.forecast_cashflow(months) - if not results: - return {"total_balance": 0, "total_income": 0, "total_expenses": 0, "months": months} - - final = results[-1] - all_income = sum(r["income"] for r in results) - all_expenses = sum(r["expenses"] for r in results) - return { - "total_balance": final["balance"], - "total_income": round(all_income, 2), - "total_expenses": round(all_expenses, 2), - "months": months, - } diff --git a/engine/scenarios.py b/engine/scenarios.py deleted file mode 100644 index dcca293..0000000 --- a/engine/scenarios.py +++ /dev/null @@ -1,87 +0,0 @@ -from copy import deepcopy - -from cashflow_model import FinancialModel, ForecastScenario -from engine.forecast import ForecastService - - -class ScenarioError(Exception): - pass - - -DEFAULT_SCENARIOS = { - "baseline": ForecastScenario( - name="baseline", - income_multiplier=1.0, - expense_multiplier=1.0, - growth_multiplier=1.0, - description="Базовый сценарий без изменений", - ), - "optimistic": ForecastScenario( - name="optimistic", - income_multiplier=1.15, - expense_multiplier=0.95, - growth_multiplier=1.2, - description="Оптимистичный: доход +15%, расход -5%, рост активов +20%", - ), - "pessimistic": ForecastScenario( - name="pessimistic", - income_multiplier=0.85, - expense_multiplier=1.1, - growth_multiplier=0.8, - description="Пессимистичный: доход -15%, расход +10%, рост активов -20%", - ), -} - - -class ScenarioService: - def __init__(self, model: FinancialModel): - self.model = deepcopy(model) - - def apply(self, scenario: ForecastScenario, months: int = 12) -> dict: - model = deepcopy(self.model) - - for t in model.transactions: - if t.amount > 0: - t.amount *= scenario.income_multiplier - else: - t.amount *= scenario.expense_multiplier - - for r in model.recurring: - if r.category == "income": - r.amount *= scenario.income_multiplier - else: - r.amount *= scenario.expense_multiplier - - for a in model.assets: - a.growth_rate *= scenario.growth_multiplier - - service = ForecastService(model) - result = service.summary(months) - result["scenario"] = scenario.name - result["scenario_description"] = scenario.description - return result - - def compare(self, months: int = 12) -> dict: - results = {} - for name, scenario in DEFAULT_SCENARIOS.items(): - results[name] = self.apply(scenario, months) - return results - - def what_if( - self, - income_mult: float = 1.0, - expense_mult: float = 1.0, - growth_mult: float = 1.0, - months: int = 12, - ) -> dict: - scenario = ForecastScenario( - name="what-if", - income_multiplier=income_mult, - expense_multiplier=expense_mult, - growth_multiplier=growth_mult, - description=( - f"What-if: income x{income_mult}, " - f"expense x{expense_mult}, growth x{growth_mult}" - ), - ) - return self.apply(scenario, months) diff --git a/sync/__init__.py b/sync/__init__.py deleted file mode 100644 index 0a965fb..0000000 --- a/sync/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from sync.excel_sync import ExcelSync, SyncError - -__all__ = ["ExcelSync", "SyncError"] diff --git a/sync/excel_sync.py b/sync/excel_sync.py deleted file mode 100644 index f8ebe05..0000000 --- a/sync/excel_sync.py +++ /dev/null @@ -1,135 +0,0 @@ -from pathlib import Path -from uuid import UUID - -from openpyxl import Workbook, load_workbook - -from cashflow_model import ( - Account, - Asset, - ExchangeRate, - FinancialModel, - Liability, - RecurringCashflow, - Transaction, -) - - -class SyncError(Exception): - pass - - -_SHEET_CONFIG = { - "Accounts": { - "fields": ["id", "name", "currency", "balance"], - "cls": Account, - }, - "Transactions": { - "fields": ["id", "date", "account", "category", "amount", "description"], - "cls": Transaction, - }, - "Recurring": { - "fields": ["id", "start_date", "end_date", "frequency", "amount", "category"], - "cls": RecurringCashflow, - }, - "Assets": { - "fields": ["id", "name", "value", "growth_rate"], - "cls": Asset, - }, - "Liabilities": { - "fields": ["id", "name", "balance", "interest", "payment"], - "cls": Liability, - }, - "ExchangeRates": { - "fields": ["from_currency", "to_currency", "rate"], - "cls": ExchangeRate, - }, -} - - -class ExcelSync: - def import_model(self, path: str | Path) -> FinancialModel: - path = Path(path) - if not path.exists(): - raise SyncError(f"File not found: {path}") - - wb = load_workbook(path, read_only=True, data_only=True) - model = FinancialModel() - model.exchange_rates = [] - - for sheet_name, config in _SHEET_CONFIG.items(): - if sheet_name not in wb.sheetnames: - continue - ws = wb[sheet_name] - rows = list(ws.iter_rows(values_only=True)) - if len(rows) < 2: - continue - - headers = [str(h).strip().lower() if h else "" for h in rows[0]] - for row in rows[1:]: - if not any(v is not None for v in row): - continue - data = {} - for i, header in enumerate(headers): - val = row[i] if i < len(row) else None - if val is not None: - data[header] = str(val) if not isinstance(val, (int, float)) else val - self._add_to_model(model, sheet_name, data) - - if "ModelInfo" in wb.sheetnames: - ws = wb["ModelInfo"] - rows = list(ws.iter_rows(values_only=True)) - for row in rows: - if row[0] and str(row[0]).strip().lower() == "base_currency" and len(row) > 1: - model.base_currency = str(row[1]).strip() - - wb.close() - return model - - def export_model(self, model: FinancialModel, path: str | Path) -> None: - path = Path(path) - wb = Workbook() - wb.remove(wb.active) - - collections = { - "Accounts": model.accounts, - "Transactions": model.transactions, - "Recurring": model.recurring, - "Assets": model.assets, - "Liabilities": model.liabilities, - "ExchangeRates": model.exchange_rates, - } - - for sheet_name, items in collections.items(): - config = _SHEET_CONFIG[sheet_name] - ws = wb.create_sheet(title=sheet_name) - ws.append(config["fields"]) - for item in items: - row = [ - str(getattr(item, f)) if isinstance(getattr(item, f), UUID) - else getattr(item, f) - for f in config["fields"] - ] - ws.append(row) - - ws_info = wb.create_sheet(title="ModelInfo") - ws_info.append(["Property", "Value"]) - ws_info.append(["base_currency", model.base_currency]) - - wb.save(path) - - def _add_to_model(self, model: FinancialModel, sheet_name: str, data: dict) -> None: - try: - if sheet_name == "Accounts": - model.accounts.append(Account.from_dict(data)) - elif sheet_name == "Transactions": - model.transactions.append(Transaction.from_dict(data)) - elif sheet_name == "Recurring": - model.recurring.append(RecurringCashflow.from_dict(data)) - elif sheet_name == "Assets": - model.assets.append(Asset.from_dict(data)) - elif sheet_name == "Liabilities": - model.liabilities.append(Liability.from_dict(data)) - elif sheet_name == "ExchangeRates": - model.exchange_rates.append(ExchangeRate.from_dict(data)) - except Exception as e: - raise SyncError(f"Failed to parse row in {sheet_name}: {e}") from e