210 lines
9.5 KiB
Python
210 lines
9.5 KiB
Python
from __future__ import annotations
|
||
|
||
from html import escape
|
||
|
||
from app.categories import category_label
|
||
from app.domain import FamilyInfo, Totals, Transaction, TransactionKind
|
||
from app.parser import format_money
|
||
|
||
|
||
MONTHS = (
|
||
"",
|
||
"январь",
|
||
"февраль",
|
||
"март",
|
||
"апрель",
|
||
"май",
|
||
"июнь",
|
||
"июль",
|
||
"август",
|
||
"сентябрь",
|
||
"октябрь",
|
||
"ноябрь",
|
||
"декабрь",
|
||
)
|
||
|
||
|
||
WELCOME = """<b>Привет! Я помогу держать деньги под контролем 👋</b>
|
||
|
||
Просто присылай сумму и описание — я сам определю категорию:
|
||
|
||
<code>467 ярче</code>
|
||
<code>3000 бензин</code>
|
||
<code>+ 85000 зарплата</code>
|
||
|
||
Обычная запись — расход. Знак <b>+</b> или слово <b>доход</b> — поступление.
|
||
|
||
Каждый вечер я пришлю короткий итог дня и месяца. А в разделе «Семья» можно подключить общий бюджет с супругом. Начнём?"""
|
||
|
||
|
||
HELP_TEXT = """<b>Как записывать операции</b>
|
||
|
||
Расход:
|
||
<code>1250 продукты</code>
|
||
<code>3 500 ремонт машины</code>
|
||
|
||
Доход:
|
||
<code>+ 75000 зарплата</code>
|
||
<code>доход 5000 подработка</code>
|
||
|
||
Можно вводить копейки: <code>199,90 кофе</code>.
|
||
|
||
После записи категорию можно изменить, а ошибочную операцию — удалить. Кнопки внизу показывают итоги и аналитику.
|
||
|
||
В семейном режиме кнопка <b>«Общий долг»</b> включает автоматический расчёт: расходы увеличивают долг, доходы уменьшают.
|
||
|
||
Через <b>«Категории»</b> можно добавить свои категории и подкатегории. Кнопка «Изменить категорию» переносит любую запись, в том числе из «Другое»."""
|
||
|
||
|
||
def transaction_confirmation(transaction: Transaction, debt: int | None = None) -> str:
|
||
is_income = transaction.kind is TransactionKind.INCOME
|
||
heading = "Доход записан" if is_income else "Расход записан"
|
||
icon = "🟢" if is_income else "🔴"
|
||
signed_amount = transaction.amount_kopecks if is_income else -transaction.amount_kopecks
|
||
debt_line = f"\n\n{debt_short_text(debt)}" if debt is not None else ""
|
||
return (
|
||
f"{icon} <b>{heading}</b>\n\n"
|
||
f"<b>{format_money(signed_amount, signed=True)}</b> · {escape(transaction.description)}\n"
|
||
f"{transaction_category_text(transaction)}{debt_line}"
|
||
)
|
||
|
||
|
||
def summary_text(title: str, totals: Totals) -> str:
|
||
balance_icon = "🟢" if totals.balance >= 0 else "🔴"
|
||
return (
|
||
f"<b>{escape(title)}</b>\n\n"
|
||
f"↗️ Доходы <b>{format_money(totals.income)}</b>\n"
|
||
f"↘️ Расходы <b>{format_money(totals.expense)}</b>\n"
|
||
f"━━━━━━━━━━━━\n"
|
||
f"{balance_icon} Баланс <b>{format_money(totals.balance, signed=True)}</b>"
|
||
)
|
||
|
||
|
||
def analytics_text(period_name: str, category_totals: dict, total: int) -> str:
|
||
title = f"📊 <b>Расходы {escape(period_name)}</b>"
|
||
if total == 0:
|
||
return f"{title}\n\nПока расходов нет. Самое время записать первый 👇"
|
||
|
||
lines = [title, f"Всего: <b>{format_money(total)}</b>", ""]
|
||
for label, amount in category_totals.items():
|
||
percent = round(amount / total * 100)
|
||
filled = max(1, round(percent / 10))
|
||
bar = "▰" * filled + "▱" * (10 - filled)
|
||
lines.append(escape(label))
|
||
lines.append(f"<code>{bar}</code> {percent}% · <b>{format_money(amount)}</b>")
|
||
lines.append("")
|
||
return "\n".join(lines).rstrip()
|
||
|
||
|
||
def recent_text(transactions: list[Transaction]) -> str:
|
||
if not transactions:
|
||
return "🧾 <b>Последние записи</b>\n\nЗдесь пока пусто."
|
||
lines = ["🧾 <b>Последние записи</b>", ""]
|
||
for transaction in transactions:
|
||
is_income = transaction.kind is TransactionKind.INCOME
|
||
amount = transaction.amount_kopecks if is_income else -transaction.amount_kopecks
|
||
author = f" · {escape(transaction.author_name)}" if transaction.author_name else ""
|
||
lines.append(
|
||
f"{transaction.occurred_at:%d.%m} <b>{format_money(amount, signed=True)}</b> "
|
||
f"{escape(transaction.description)}{author}"
|
||
)
|
||
lines.append("")
|
||
lines.append("Удалить или исправить запись можно сразу после её добавления.")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def family_status_text(info: FamilyInfo | None) -> str:
|
||
if info is None:
|
||
return (
|
||
"👨👩👧 <b>Семейный бюджет</b>\n\n"
|
||
"Объедините два Telegram-аккаунта. После подключения вы оба будете видеть "
|
||
"общие итоги, аналитику и новые записи в реальном времени."
|
||
)
|
||
members = "\n".join(f"• {escape(name)}" for name in info.members)
|
||
waiting = (
|
||
"\n\nОтправьте код супругу. Подключение произойдёт только после вашего подтверждения."
|
||
if len(info.members) < 2
|
||
else ""
|
||
)
|
||
chat = (
|
||
f"\n\n💬 Общий чат: <b>{escape(info.notification_chat_title or 'Подключён')}</b>"
|
||
if info.notification_chat_id is not None
|
||
else (
|
||
"\n\n💬 <b>Общий чат пока не подключён.</b> Создатель бюджета должен добавить "
|
||
"бота в семейную группу, стать администратором группы и отправить там /family_chat."
|
||
)
|
||
)
|
||
return (
|
||
"👨👩👧 <b>Семейный бюджет подключён</b>\n\n"
|
||
f"<b>Участники:</b>\n{members}\n\n"
|
||
f"Код приглашения: <code>{escape(info.invite_code)}</code>"
|
||
f"{waiting}{chat}"
|
||
)
|
||
|
||
|
||
def family_transaction_notice(transaction: Transaction, debt: int | None = None) -> str:
|
||
is_income = transaction.kind is TransactionKind.INCOME
|
||
action = "добавил(а) доход" if is_income else "добавил(а) расход"
|
||
icon = "🟢" if is_income else "🔴"
|
||
amount = transaction.amount_kopecks if is_income else -transaction.amount_kopecks
|
||
author = escape(transaction.author_name or "Супруг")
|
||
debt_line = f"\n\n{debt_short_text(debt)}" if debt is not None else ""
|
||
return (
|
||
f"👨👩👧 <b>{author} {action}</b>\n\n"
|
||
f"{icon} <b>{format_money(amount, signed=True)}</b> · {escape(transaction.description)}\n"
|
||
f"{transaction_category_text(transaction)}{debt_line}"
|
||
)
|
||
|
||
|
||
def daily_report_text(day: Totals, month: Totals, debt: int | None = None) -> str:
|
||
balance_icon = "✨" if day.balance >= 0 else "📌"
|
||
debt_section = f"\n\n{debt_short_text(debt)}" if debt is not None else ""
|
||
return (
|
||
"🌙 <b>Итог дня</b>\n\n"
|
||
f"Потрачено сегодня: <b>{format_money(day.expense)}</b>\n"
|
||
f"Получено сегодня: <b>{format_money(day.income)}</b>\n"
|
||
f"{balance_icon} Баланс дня: <b>{format_money(day.balance, signed=True)}</b>\n\n"
|
||
"<b>С начала месяца</b>\n"
|
||
f"Расходы: <b>{format_money(month.expense)}</b>\n"
|
||
f"Доходы: <b>{format_money(month.income)}</b>\n"
|
||
f"Баланс: <b>{format_money(month.balance, signed=True)}</b>"
|
||
f"{debt_section}"
|
||
)
|
||
|
||
|
||
def debt_short_text(debt: int) -> str:
|
||
if debt > 0:
|
||
return f"💳 Общий долг: <b>{format_money(debt)}</b>"
|
||
if debt < 0:
|
||
return f"🌿 Семейный резерв: <b>{format_money(abs(debt))}</b>"
|
||
return "🎉 Общий долг: <b>погашен</b>"
|
||
|
||
|
||
def debt_status_text(debt: int | None, can_edit: bool) -> str:
|
||
if debt is None:
|
||
status = "Общий долг пока не указан."
|
||
else:
|
||
status = debt_short_text(debt)
|
||
edit_hint = (
|
||
"\n\nУкажите текущую сумму долга. С этого момента каждый расход будет её увеличивать, "
|
||
"а каждый доход — уменьшать."
|
||
if can_edit
|
||
else "\n\nИзменить исходную сумму может только создатель семейного бюджета."
|
||
)
|
||
return f"<b>💳 Общий долг</b>\n\n{status}{edit_hint}"
|
||
|
||
|
||
def transaction_category_text(transaction: Transaction) -> str:
|
||
if transaction.budget_category_name and transaction.parent_category_name:
|
||
return (
|
||
f"{escape(transaction.parent_category_icon or '📁')} "
|
||
f"{escape(transaction.parent_category_name)} → "
|
||
f"{escape(transaction.budget_category_icon or '📁')} "
|
||
f"{escape(transaction.budget_category_name)}"
|
||
)
|
||
if transaction.budget_category_name:
|
||
return (
|
||
f"{escape(transaction.budget_category_icon or '📁')} "
|
||
f"{escape(transaction.budget_category_name)}"
|
||
)
|
||
return category_label(transaction.category)
|