Spaces:
Sleeping
Sleeping
File size: 3,116 Bytes
bfd6783 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | """Monthly budgets per category."""
from __future__ import annotations
from typing import Sequence
from .. import render
from ..analytics import budget_status, month_bounds
from ..categories import CATEGORIES, coerce
from ..categories import label as category_label
from ..models import Budget
from ..money import parse_amount
from .base import BotContext, CommandSpec, Reply, SimpleFeature
class BudgetsFeature(SimpleFeature):
name = "budgets"
description = "🎯 Budgets"
priority = 20
def commands(self) -> Sequence[CommandSpec]:
return (
CommandSpec("budget", "Set a monthly budget", "<category> <amount>"),
CommandSpec("budgets", "How you're tracking against budgets"),
CommandSpec("unbudget", "Remove a budget", "<category>"),
CommandSpec("categories", "List the category names"),
)
def cmd_budget(self, ctx: BotContext) -> Reply:
args = ctx.args.strip()
if not args:
return Reply(
"Set a monthly cap, e.g. <code>/budget food_dining 8000</code>.\n"
"/categories lists the names."
)
parts = args.split()
category = coerce(parts[0])
if category == "other" and parts[0].lower() not in ("other",):
return Reply(
f"I don't recognise the category <code>{render.escape(parts[0])}</code>.\n"
"/categories lists what I understand."
)
amount = parse_amount(" ".join(parts[1:]), ctx.settings.default_currency)
if amount is None or amount.minor <= 0:
return Reply("I need an amount, e.g. <code>/budget groceries 12000</code>.")
budget = Budget(
user_id=ctx.user_id,
category=category,
amount_minor=amount.minor,
currency=amount.currency or ctx.settings.default_currency,
)
ctx.store.set_budget(budget)
return Reply(
f"🎯 Budget set: {render.escape(category_label(category))} "
f"{render.money(budget.amount_minor, budget.currency)}/month."
)
def cmd_budgets(self, ctx: BotContext) -> Reply:
budgets = ctx.store.get_budgets(ctx.user_id)
start, _ = month_bounds(ctx.today)
expenses = ctx.store.list_expenses(ctx.user_id, start, ctx.today)
return Reply(render.budget_list(budget_status(budgets, expenses)))
def cmd_unbudget(self, ctx: BotContext) -> Reply:
category = coerce(ctx.args.strip())
if ctx.store.delete_budget(ctx.user_id, category):
return Reply(f"Removed the {render.escape(category_label(category))} budget.")
return Reply("No budget set for that category.")
def cmd_categories(self, ctx: BotContext) -> Reply:
lines = ["<b>Categories</b>"]
lines.extend(
f"<code>{name}</code> — {render.escape(category_label(name))}"
for name in CATEGORIES
)
lines.append("")
lines.append("Tag an entry to override: <code>250 lunch #transport</code>")
return Reply("\n".join(lines))
|