testing new features
This commit is contained in:
+419
@@ -0,0 +1,419 @@
|
||||
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,
|
||||
)
|
||||
|
||||
app = typer.Typer(name="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="Код валюты (RUB, USD, EUR...)"),
|
||||
) -> None:
|
||||
"""Показать или установить базовую валюту модели"""
|
||||
model = _load_model()
|
||||
if currency:
|
||||
model.base_currency = currency.upper()
|
||||
_save_model(model)
|
||||
console.print(f"[green]OK[/green] Базовая валюта установлена: {model.base_currency}")
|
||||
else:
|
||||
console.print(f"Базовая валюта: [bold]{model.base_currency}[/bold]")
|
||||
|
||||
|
||||
# ── rate ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def rate_set(
|
||||
from_currency: str = typer.Argument(..., help="Из валюты"),
|
||||
to_currency: str = typer.Argument(..., help="В валюту"),
|
||||
rate: float = typer.Argument(..., help="Курс"),
|
||||
) -> 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]OK[/green] Курс {fc} → {tc} = {rate}")
|
||||
|
||||
|
||||
@app.command(name="rate-list")
|
||||
def rate_list() -> None:
|
||||
"""Список курсов валют"""
|
||||
model = _load_model()
|
||||
if not model.exchange_rates:
|
||||
console.print("[yellow]Курсы не заданы[/yellow]")
|
||||
return
|
||||
table = Table(title="Курсы валют")
|
||||
table.add_column("Из", style="cyan")
|
||||
table.add_column("В", style="cyan")
|
||||
table.add_column("Курс", 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="Из валюты"),
|
||||
to_currency: str = typer.Argument(..., help="В валюту"),
|
||||
) -> 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]OK[/green] Курс {fc} → {tc} удалён")
|
||||
else:
|
||||
console.print(f"[red]Курс {fc} → {tc} не найден[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── account ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def account_add(
|
||||
name: str = typer.Option(..., "--name", "-n", help="Название счёта"),
|
||||
balance: float = typer.Option(0.0, "--balance", "-b", help="Начальный баланс"),
|
||||
currency: str = typer.Option("", "--currency", "-c", help="Валюта счёта"),
|
||||
) -> 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)
|
||||
console.print(f"[green]OK[/green] Счёт '{name}' добавлен (баланс: {balance}, валюта: {curr})")
|
||||
|
||||
|
||||
@app.command(name="account-list")
|
||||
def account_list() -> None:
|
||||
"""Список счетов"""
|
||||
model = _load_model()
|
||||
if not model.accounts:
|
||||
console.print("[yellow]Счета не добавлены[/yellow]")
|
||||
return
|
||||
table = Table(title="Счета")
|
||||
table.add_column("ID", style="dim")
|
||||
table.add_column("Название", style="cyan")
|
||||
table.add_column("Валюта")
|
||||
table.add_column("Баланс", 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="ID или название счёта"),
|
||||
) -> 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]OK[/green] Счёт '{identifier}' удалён")
|
||||
else:
|
||||
console.print(f"[red]Счёт '{identifier}' не найден[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── transaction ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def transaction_add(
|
||||
account: str = typer.Option(..., "--account", "-a", help="ID или название счёта"),
|
||||
amount: float = typer.Option(..., "--amount", "-m", help="Сумма (доход/расход)"),
|
||||
category: str = typer.Option("", "--category", "-c", help="Категория"),
|
||||
description: str = typer.Option("", "--description", "-d", help="Описание"),
|
||||
date: str = typer.Option("", "--date", "-D", help="Дата (YYYY-MM-DD)"),
|
||||
) -> None:
|
||||
"""Добавить транзакцию"""
|
||||
model = _load_model()
|
||||
account_id = _resolve_account_id(model, account)
|
||||
if not account_id:
|
||||
console.print(f"[red]Счёт '{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 = "доход" if amount >= 0 else "расход"
|
||||
console.print(f"[green]OK[/green] Транзакция '{kind}' на {abs(amount):.2f} (счёт: {account})")
|
||||
|
||||
|
||||
@app.command(name="transaction-list")
|
||||
def transaction_list() -> None:
|
||||
"""Список транзакций"""
|
||||
model = _load_model()
|
||||
if not model.transactions:
|
||||
console.print("[yellow]Транзакции не добавлены[/yellow]")
|
||||
return
|
||||
table = Table(title="Транзакции")
|
||||
table.add_column("ID", style="dim")
|
||||
table.add_column("Дата")
|
||||
table.add_column("Счёт")
|
||||
table.add_column("Категория")
|
||||
table.add_column("Сумма", justify="right")
|
||||
table.add_column("Описание")
|
||||
for t in model.transactions:
|
||||
account_name = _account_name(model, t.account)
|
||||
table.add_row(
|
||||
str(t.id)[:8], t.date, account_name, t.category,
|
||||
f"{t.amount:,.2f}", t.description,
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command(name="transaction-remove")
|
||||
def transaction_remove(
|
||||
identifier: str = typer.Argument(..., help="ID транзакции"),
|
||||
) -> 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]OK[/green] Транзакция '{identifier}' удалена")
|
||||
else:
|
||||
console.print(f"[red]Транзакция '{identifier}' не найдена[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── recurring ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def recurring_add(
|
||||
amount: float = typer.Option(..., "--amount", "-m", help="Сумма"),
|
||||
category: str = typer.Option("", "--category", "-c", help="Категория"),
|
||||
frequency: str = typer.Option("monthly", "--frequency", "-f", help="Периодичность"),
|
||||
start_date: str = typer.Option("", "--start", "-s", help="Дата начала (YYYY-MM-DD)"),
|
||||
end_date: str = typer.Option("", "--end", "-e", help="Дата окончания (YYYY-MM-DD)"),
|
||||
) -> 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)
|
||||
console.print(f"[green]OK[/green] Регулярный платёж на {abs(amount):.2f} ({frequency})")
|
||||
|
||||
|
||||
@app.command(name="recurring-list")
|
||||
def recurring_list() -> None:
|
||||
"""Список регулярных платежей"""
|
||||
model = _load_model()
|
||||
if not model.recurring:
|
||||
console.print("[yellow]Регулярные платежи не добавлены[/yellow]")
|
||||
return
|
||||
table = Table(title="Регулярные платежи")
|
||||
table.add_column("ID", style="dim")
|
||||
table.add_column("Начало")
|
||||
table.add_column("Конец")
|
||||
table.add_column("Период")
|
||||
table.add_column("Сумма", justify="right")
|
||||
table.add_column("Категория")
|
||||
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="ID регулярного платежа"),
|
||||
) -> 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]OK[/green] Регулярный платёж '{identifier}' удалён")
|
||||
else:
|
||||
console.print(f"[red]Регулярный платёж '{identifier}' не найден[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── asset ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def asset_add(
|
||||
name: str = typer.Option(..., "--name", "-n", help="Название актива"),
|
||||
value: float = typer.Option(0.0, "--value", "-v", help="Стоимость"),
|
||||
growth_rate: float = typer.Option(0.0, "--growth", "-g", help="Годовой рост (%)"),
|
||||
) -> 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]OK[/green] Актив '{name}' добавлен (стоимость: {value})")
|
||||
|
||||
|
||||
@app.command(name="asset-list")
|
||||
def asset_list() -> None:
|
||||
"""Список активов"""
|
||||
model = _load_model()
|
||||
if not model.assets:
|
||||
console.print("[yellow]Активы не добавлены[/yellow]")
|
||||
return
|
||||
table = Table(title="Активы")
|
||||
table.add_column("ID", style="dim")
|
||||
table.add_column("Название", style="cyan")
|
||||
table.add_column("Стоимость", justify="right")
|
||||
table.add_column("Рост (%)", 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="ID или название актива"),
|
||||
) -> 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]OK[/green] Актив '{identifier}' удалён")
|
||||
else:
|
||||
console.print(f"[red]Актив '{identifier}' не найден[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
# ── liability ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.command()
|
||||
def liability_add(
|
||||
name: str = typer.Option(..., "--name", "-n", help="Название"),
|
||||
balance: float = typer.Option(0.0, "--balance", "-b", help="Остаток долга"),
|
||||
interest: float = typer.Option(0.0, "--interest", "-i", help="Процентная ставка (%)"),
|
||||
payment: float = typer.Option(0.0, "--payment", "-p", help="Ежемесячный платёж"),
|
||||
) -> 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]OK[/green] Обязательство '{name}' добавлено (долг: {balance})")
|
||||
|
||||
|
||||
@app.command(name="liability-list")
|
||||
def liability_list() -> None:
|
||||
"""Список обязательств"""
|
||||
model = _load_model()
|
||||
if not model.liabilities:
|
||||
console.print("[yellow]Обязательства не добавлены[/yellow]")
|
||||
return
|
||||
table = Table(title="Обязательства")
|
||||
table.add_column("ID", style="dim")
|
||||
table.add_column("Название", style="cyan")
|
||||
table.add_column("Долг", justify="right")
|
||||
table.add_column("Ставка", justify="right")
|
||||
table.add_column("Платёж", 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="ID или название обязательства"),
|
||||
) -> 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]OK[/green] Обязательство '{identifier}' удалено")
|
||||
else:
|
||||
console.print(f"[red]Обязательство '{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]
|
||||
Reference in New Issue
Block a user