Files
nifodea/cli/config.py
T
2026-07-22 12:12:09 +03:00

420 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]