diff --git a/.github/img/all_transactions.png b/.github/img/all_transactions.png index 7c3ac84d..317aeed7 100644 Binary files a/.github/img/all_transactions.png and b/.github/img/all_transactions.png differ diff --git a/.github/img/calendar.png b/.github/img/calendar.png index 7fc12878..1624e472 100644 Binary files a/.github/img/calendar.png and b/.github/img/calendar.png differ diff --git a/.github/img/monthly_view.png b/.github/img/monthly_view.png index 5bb7f140..f99c1bc0 100644 Binary files a/.github/img/monthly_view.png and b/.github/img/monthly_view.png differ diff --git a/.github/img/networth.png b/.github/img/networth.png index aa18755c..715a97ff 100644 Binary files a/.github/img/networth.png and b/.github/img/networth.png differ diff --git a/.github/img/yearly.png b/.github/img/yearly.png index 1c671d03..a643b8cd 100644 Binary files a/.github/img/yearly.png and b/.github/img/yearly.png differ diff --git a/app/apps/common/management/commands/load_demo_data.py b/app/apps/common/management/commands/load_demo_data.py new file mode 100644 index 00000000..d0e45644 --- /dev/null +++ b/app/apps/common/management/commands/load_demo_data.py @@ -0,0 +1,448 @@ +"""Builds the demo dataset with every date relative to today. + +Run on an empty database (the demo reset flushes first). Covers every feature: +multi-currency accounts, exchange rates, an ETF and Bitcoin tracked as +currencies with DCA strategies, transfers, a credit card using reference +dates, installment plans, recurring transactions, quick transactions, rules, +filter presets, muted/unpaid/archived/deactivated examples. +""" + +import calendar +import random +from datetime import date, datetime, time +from decimal import ROUND_HALF_UP, Decimal + +from dateutil.relativedelta import relativedelta +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand +from django.db import transaction as db_transaction +from django.db.models import Q +from django.utils import timezone + +from apps.accounts.models import Account, AccountGroup +from apps.common.middleware.thread_local import delete_current_user, write_current_user +from apps.currencies.models import Currency, ExchangeRate +from apps.dca.models import DCAEntry, DCAStrategy +from apps.rules.models import TransactionRule, TransactionRuleAction +from apps.transactions.models import ( + FilterPreset, + InstallmentPlan, + QuickTransaction, + RecurringTransaction, + Transaction, + TransactionCategory, + TransactionEntity, + TransactionTag, +) + +DEMO_EMAIL = "demo@demo.com" +DEMO_PASSWORD = "wygiwyhdemo" +HISTORY_MONTHS = 12 # months of history before the current one + +IN = Transaction.Type.INCOME +EX = Transaction.Type.EXPENSE + + +def q(value, places=2): + return Decimal(str(value)).quantize(Decimal(1).scaleb(-places), ROUND_HALF_UP) + + +class Command(BaseCommand): + help = "Loads demo data with dates relative to today. Expects an empty database." + + def handle(self, *args, **options): + User = get_user_model() + if User.objects.filter(email=DEMO_EMAIL).exists(): + if options["verbosity"]: + self.stdout.write("Demo user already exists, skipping.") + return + + self.today = timezone.localdate() + self.rng = random.Random(42) # same demo every day, only shifted in time + + with db_transaction.atomic(): + user = User.objects.create_user( + email=DEMO_EMAIL, + password=DEMO_PASSWORD, + first_name="Demo", + last_name="User", + ) + # SharedObject/OwnedObject.save() pick the owner from here + write_current_user(user) + try: + self._build(user) + finally: + delete_current_user() + + if options["verbosity"]: + self.stdout.write(self.style.SUCCESS("Demo data loaded.")) + + # ---------- helpers ---------- + + def day(self, months, day): + """Date `months` away from the current month, day clamped to the month.""" + first = self.today.replace(day=1) + relativedelta(months=months) + last = calendar.monthrange(first.year, first.month)[1] + return first.replace(day=min(day, last)) + + def tx(self, account, kind, amount, when, description, category=None, + tags=(), entities=(), reference=None, paid=None, mute=False, notes=""): + t = Transaction.objects.create( + account=account, + type=kind, + amount=amount, + date=when, + reference_date=(reference or when).replace(day=1), + description=description, + category=self.cats[category] if category else None, + is_paid=(when <= self.today) if paid is None else paid, + mute=mute, + notes=notes, + ) + if tags: + t.tags.set([self.tags[n] for n in tags]) + if entities: + t.entities.set([self.ents[n] for n in entities]) + return t + + def transfer(self, source, target, amount, when, description, + to_amount=None, mute=True, category=None): + """Same shape as TransferForm.save(): an expense and an income.""" + out = self.tx(source, EX, amount, when, description, category, mute=mute) + inc = self.tx(target, IN, to_amount or amount, when, description, category, mute=mute) + return out, inc + + def rate(self, series, months): + return series[months + HISTORY_MONTHS] + + # ---------- data ---------- + + def _build(self, user): + today = self.today + rng = self.rng + + # Currencies, including an ETF and airline miles tracked as currencies + usd = Currency.objects.create(code="USD", name="US Dollar", decimal_places=2, prefix="$ ") + eur = Currency.objects.create( + code="EUR", name="Euro", decimal_places=2, prefix="€ ", exchange_currency=usd + ) + btc = Currency.objects.create( + code="BTC", name="Bitcoin", decimal_places=8, prefix="₿ ", exchange_currency=usd + ) + vwce = Currency.objects.create( + code="VWCE", name="Vanguard FTSE All-World ETF", decimal_places=4, + suffix=" VWCE", exchange_currency=eur, + ) + miles = Currency.objects.create( + code="MILES", name="Airline Miles", decimal_places=0, suffix=" mi", + exchange_currency=usd, + ) + + # Monthly rate history so net worth charts move; index 0 = oldest month + n = HISTORY_MONTHS + 1 + eur_usd = [q(1.06 + 0.04 * i / n + rng.uniform(-0.01, 0.01), 4) for i in range(n)] + btc_usd = [q(58000 + 40000 * i / n + rng.uniform(-4000, 4000)) for i in range(n)] + vwce_eur = [q(112 + 20 * i / n + rng.uniform(-2, 2)) for i in range(n)] + for i in range(n): + months = i - HISTORY_MONTHS + first = self.day(months, 1) + when = timezone.make_aware(datetime.combine(first, time(12))) + if months == 0: + when = timezone.now() # latest rate is "today" + for src, dst, series in [ + (eur, usd, eur_usd), (btc, usd, btc_usd), (vwce, eur, vwce_eur) + ]: + ExchangeRate.objects.create( + from_currency=src, to_currency=dst, rate=series[i], date=when + ) + ExchangeRate.objects.create( + from_currency=miles, to_currency=usd, rate=Decimal("0.012"), + date=timezone.make_aware(datetime.combine(self.day(-HISTORY_MONTHS, 1), time(12))), + ) + + # Accounts + everyday = AccountGroup.objects.create(name="Everyday") + investing = AccountGroup.objects.create(name="Savings & Investing") + rewards = AccountGroup.objects.create(name="Rewards") + + checking = Account.objects.create(name="Checking", group=everyday, currency=usd) + card = Account.objects.create(name="Credit Card", group=everyday, currency=usd) + cash = Account.objects.create(name="Cash Wallet", group=everyday, currency=usd) + savings = Account.objects.create( + name="High-Yield Savings", group=investing, currency=usd, is_asset=True + ) + euro = Account.objects.create( + name="Euro Account", group=investing, currency=eur, exchange_currency=usd + ) + brokerage = Account.objects.create( + name="Brokerage (VWCE)", group=investing, currency=vwce, is_asset=True + ) + btc_wallet = Account.objects.create( + name="Bitcoin Wallet", group=investing, currency=btc, is_asset=True + ) + miles_acc = Account.objects.create(name="Airline Miles", group=rewards, currency=miles) + old_bank = Account.objects.create( + name="Old Bank (closed)", group=everyday, currency=usd, is_archived=True + ) + + self.cats = { + name: TransactionCategory.objects.create(name=name) + for name in [ + "Salary", "Freelance", "Interest", "Rent", "Groceries", "Dining", + "Transport", "Utilities", "Subscriptions", "Health", "Travel", + "Shopping", "Investments", "Fees", "Rewards", "Gifts", "Transfers", + ] + } + self.cats["Cable TV"] = TransactionCategory.objects.create( + name="Cable TV", active=False # deactivated: hidden from new transactions + ) + self.tags = { + name: TransactionTag.objects.create(name=name) + for name in ["essentials", "fun", "work", "home", "travel", "reimbursable"] + } + self.ents = { + name: TransactionEntity.objects.create(name=name) + for name in [ + "Acme Corp", "Northwind (client)", "Landlord", "Power & Light Co.", + "FiberNet", "Fresh Market", "Corner Bistro", "Sushi House", "Uber", + "City Transit", "StreamFlix", "Iron Gym", "Pharma Plus", "BuyMore", + "Alex", "Sky Airlines", + ] + } + + btc_dca = DCAStrategy.objects.create( + name="Bitcoin monthly", target_currency=btc, payment_currency=usd, + notes="Buy $100 of Bitcoin every month, no matter the price.", + ) + etf_dca = DCAStrategy.objects.create( + name="World ETF (VWCE)", target_currency=vwce, payment_currency=eur, + notes="€250 into a global index ETF every month.", + ) + + # Installment plans and recurring transactions first, so card statements + # below include them + laptop = InstallmentPlan.objects.create( + account=card, type=EX, description="New laptop", number_of_installments=12, + start_date=self.day(-4, 14), recurrence=InstallmentPlan.Recurrence.MONTHLY, + installment_amount=Decimal("108.25"), category=self.cats["Shopping"], + notes="12 interest-free installments on the credit card.", + ) + laptop.entities.set([self.ents["BuyMore"]]) + laptop.create_transactions() + dental = InstallmentPlan.objects.create( + account=checking, type=EX, description="Dental treatment", + number_of_installments=3, start_date=self.day(-1, 16), + recurrence=InstallmentPlan.Recurrence.MONTHLY, + installment_amount=Decimal("240.00"), category=self.cats["Health"], + ) + dental.create_transactions() + + # Recurring transactions + def recurring(account, amount, description, category, start, entity=None, + recurrence=RecurringTransaction.RecurrenceType.MONTH, paused=False): + r = RecurringTransaction.objects.create( + account=account, type=EX, amount=amount, description=description, + category=self.cats[category], start_date=start, recurrence_type=recurrence, + recurrence_interval=1, keep_at_most=3, is_paused=paused, + ) + if entity: + r.entities.set([self.ents[entity]]) + if not paused: + r.create_upcoming_transactions() + return r + + recurring(card, Decimal("15.99"), "StreamFlix", "Subscriptions", + self.day(-HISTORY_MONTHS, 3), "StreamFlix") + recurring(checking, Decimal("59.99"), "Internet", "Utilities", + self.day(-HISTORY_MONTHS, 15), "FiberNet") + recurring(checking, Decimal("39.90"), "Gym membership", "Health", + self.day(-HISTORY_MONTHS, 2), "Iron Gym") + recurring(card, Decimal("14.99"), "Domain renewal", "Subscriptions", + self.day(-10, 9), recurrence=RecurringTransaction.RecurrenceType.YEAR) + recurring(card, Decimal("6.99"), "Magazine subscription", "Subscriptions", + self.day(-3, 11), paused=True) + + # Work trip paid on the card, muted so it stays out of monthly totals + self.tx(card, EX, Decimal("230.00"), self.day(-2, 9), "Client visit (reimbursable)", + "Travel", ["work", "reimbursable"], mute=True, + notes="Muted: Acme pays this back, so it shouldn't count as spending.") + self.tx(checking, IN, Decimal("230.00"), self.day(-1, 5), "Expense reimbursement", + tags=["work", "reimbursable"], entities=["Acme Corp"], mute=True) + + # ---- month by month ---- + for m in range(-HISTORY_MONTHS, 2): + past_or_current = m <= 0 + + self.tx(checking, IN, Decimal("5400.00") if m >= -6 else Decimal("5100.00"), + self.day(m, 5), "Salary", "Salary", ["work"], ["Acme Corp"]) + self.tx(checking, EX, Decimal("1650.00"), self.day(m, 1), "Rent", "Rent", + ["home", "essentials"], ["Landlord"]) + self.tx(checking, EX, q(rng.uniform(70, 135)), self.day(m, 12), "Electricity bill", + "Utilities", ["home", "essentials"], ["Power & Light Co."]) + self.transfer(checking, savings, Decimal("600.00"), self.day(m, 6), + "Monthly savings", category="Transfers") + + if not past_or_current: + continue + + # Credit card: closes on the 25th, later purchases count for next month + for _ in range(rng.randint(5, 7)): + d = self.day(m, rng.randint(1, 28)) + if d > today: + continue + kind = rng.choice(["groceries", "groceries", "dining", "uber", "shopping"]) + ref = self.day(m + 1, 1) if d.day > 25 else d + if kind == "groceries": + self.tx(card, EX, q(rng.uniform(45, 160)), d, "Groceries", "Groceries", + ["essentials"], ["Fresh Market"], reference=ref) + elif kind == "dining": + place = rng.choice(["Corner Bistro", "Sushi House"]) + self.tx(card, EX, q(rng.uniform(28, 95)), d, f"Dinner at {place}", "Dining", + ["fun"], [place], reference=ref) + elif kind == "uber": + self.tx(card, EX, q(rng.uniform(9, 32)), d, "Uber ride", "Transport", + entities=["Uber"], reference=ref) + else: + self.tx(card, EX, q(rng.uniform(25, 140)), d, "Household stuff", "Shopping", + ["home"], ["BuyMore"], reference=ref) + + # Pay last month's card statement (a transfer between your own accounts) + statement = sum( + Transaction.objects.filter( + account=card, type=EX, reference_date=self.day(m - 1, 1) + ).values_list("amount", flat=True), + Decimal("0"), + ) + if statement: + self.transfer(checking, card, statement, self.day(m, 10), + "Credit card payment", category="Transfers") + + # Miles earned on card spending; small cash expenses + if self.day(m, 27) <= today: + self.tx(miles_acc, IN, Decimal(rng.randint(400, 900)), self.day(m, 27), + "Miles earned on card spending", "Rewards", entities=["Sky Airlines"]) + for _ in range(2): + d = self.day(m, rng.randint(1, 28)) + if d <= today: + self.tx(cash, EX, q(rng.choice([3.5, 4.25, 4.5, 2.75])), d, + rng.choice(["Coffee", "Bus ticket"]), + rng.choice(["Dining", "Transport"])) + + # Interest on savings + if self.day(m, 28) <= today: + self.tx(savings, IN, q(rng.uniform(18, 32)), self.day(m, 28), + "Savings interest", "Interest") + + # Bitcoin DCA: $100 per month at that month's price + d = self.day(m, 22) + if d <= today: + price = self.rate(btc_usd, m) + sats = q(Decimal(100) / price, 8) + out, inc = self.transfer(checking, btc_wallet, Decimal("100.00"), d, + "Bitcoin DCA", to_amount=sats, mute=False, + category="Investments") + DCAEntry.objects.create(strategy=btc_dca, date=d, amount_paid=Decimal("100.00"), + amount_received=sats, expense_transaction=out, + income_transaction=inc) + + # ETF DCA: €250 per month from the Euro account + d = self.day(m, 20) + if d <= today: + price = self.rate(vwce_eur, m) + shares = q(Decimal(250) / price, 4) + out, inc = self.transfer(euro, brokerage, Decimal("250.00"), d, + "Buy VWCE", to_amount=shares, mute=False, + category="Investments") + DCAEntry.objects.create(strategy=etf_dca, date=d, amount_paid=Decimal("250.00"), + amount_received=shares, expense_transaction=out, + income_transaction=inc) + self.tx(euro, EX, Decimal("1.50"), d, "Broker fee", "Fees") + + # Every 3 months: convert dollars to euros at that month's rate + if m % 3 == 0 and self.day(m, 7) <= today: + usd_amount = Decimal("1000.00") + self.transfer(checking, euro, usd_amount, self.day(m, 7), + "Currency exchange USD → EUR", + to_amount=q(usd_amount / self.rate(eur_usd, m)), + category="Transfers") + + # Freelance paid in euros, every other month + if m % 2 == 0 and self.day(m, 15) <= today: + self.tx(euro, IN, q(rng.choice([650, 800, 1200])), self.day(m, 15), + "Freelance project", "Freelance", ["work"], ["Northwind (client)"]) + + # ATM withdrawal every other month + if m % 2 == 1 and self.day(m, 3) <= today: + self.transfer(checking, cash, Decimal("100.00"), self.day(m, 3), + "ATM withdrawal", category="Transfers") + + # ---- one-offs ---- + self.tx(miles_acc, IN, Decimal("30000"), self.day(-HISTORY_MONTHS, 4), + "Credit card sign-up bonus", "Rewards", entities=["Sky Airlines"]) + trip = -5 + self.tx(miles_acc, EX, Decimal("25000"), self.day(trip, 2), "Flight to Lisbon", + "Travel", ["travel"], ["Sky Airlines"], notes="Paid entirely with miles!") + self.tx(euro, EX, Decimal("420.00"), self.day(trip, 14), "Hotel in Lisbon", "Travel", + ["travel", "fun"]) + self.tx(euro, EX, Decimal("96.40"), self.day(trip, 15), "Pastéis & dinners", "Dining", + ["travel", "fun"]) + # Someone owes you: unpaid income in the past shows up as late + self.tx(checking, IN, Decimal("85.00"), self.day(-1, 20), + "Alex owes me for concert tickets", "Gifts", entities=["Alex"], paid=False, + notes="Unpaid income is a projection until you mark it as paid.") + # Upcoming expense that isn't paid yet + self.tx(checking, EX, Decimal("612.00"), self.day(1, 18), "Car insurance (yearly)", + "Transport", ["essentials"]) + # Cancelled service on a deactivated category + self.tx(checking, EX, Decimal("49.90"), self.day(-HISTORY_MONTHS, 8), "Cable TV", + "Cable TV", ["home"]) + # Archived account with its history and the move out of it + self.tx(old_bank, IN, Decimal("1200.00"), date(2015, 3, 2), "Opening balance", + notes="Very old transactions are no problem.") + self.transfer(old_bank, checking, Decimal("1200.00"), self.day(-HISTORY_MONTHS, 3), + "Closing old bank account", category="Transfers") + # Balance reconciliation: the wallet had less cash than recorded + self.tx(cash, EX, Decimal("12.40"), self.day(-2, 28), "Balance reconciliation") + + # Generated occurrences start unpaid; everything up to today happened already + Transaction.objects.filter(date__lte=today, is_paid=False).filter( + Q(installment_plan__isnull=False) | Q(recurring_transaction__isnull=False) + ).update(is_paid=True) + + # Quick transactions: one-click templates + QuickTransaction.objects.create( + name="Coffee", account=cash, type=EX, amount=Decimal("4.50"), + description="Coffee", category=self.cats["Dining"], + ) + QuickTransaction.objects.create( + name="Bus ticket", account=cash, type=EX, amount=Decimal("2.75"), + description="Bus ticket", category=self.cats["Transport"], + ) + lunch = QuickTransaction.objects.create( + name="Lunch near the office", account=card, type=EX, amount=Decimal("13.90"), + description="Lunch", category=self.cats["Dining"], + ) + lunch.tags.set([self.tags["work"]]) + + # Rules run on transactions you create or edit + ride = TransactionRule.objects.create( + name="Ride-hailing is Transport", + description="Categorize Uber and Lyft rides automatically.", + trigger="'uber' in description.lower() or 'lyft' in description.lower()", + ) + TransactionRuleAction.objects.create(rule=ride, field="category", value="'Transport'") + TransactionRuleAction.objects.create(rule=ride, field="entities", value="['Uber']") + big = TransactionRule.objects.create( + name="Flag big expenses", + description="Add a reminder to expenses of $500 or more.", + trigger="is_expense and amount >= 500", + ) + TransactionRuleAction.objects.create( + rule=big, field="notes", value="'Big expense: keep the receipt!'" + ) + + # Saved filters for the transactions page + FilterPreset.objects.create(owner=user, name="Unpaid", parameters={"is_paid": ["0"]}) + FilterPreset.objects.create( + owner=user, name="Credit card", parameters={"account": [str(card.id)]} + ) diff --git a/app/apps/common/tasks.py b/app/apps/common/tasks.py index 39e577d4..370e2594 100644 --- a/app/apps/common/tasks.py +++ b/app/apps/common/tasks.py @@ -78,14 +78,10 @@ def reset_demo_data(timestamp=None): ) logger.info("Database flushed successfully.") - # 2. Load data from the fixture - # TO-DO: Roll dates over based on today's date - fixture_name = "fixtures/demo_data.json" - logger.info(f"Loading data from fixture: {fixture_name}...") - management.call_command( - "loaddata", fixture_name, database=DEFAULT_DB_ALIAS, verbosity=1 - ) - logger.info(f"Data loaded successfully from {fixture_name}.") + # 2. Load demo data, with dates relative to today + logger.info("Loading demo data...") + management.call_command("load_demo_data", verbosity=1) + logger.info("Demo data loaded successfully.") logger.info("Daily demo data reset completed.") diff --git a/app/apps/common/tests/test_demo_data.py b/app/apps/common/tests/test_demo_data.py new file mode 100644 index 00000000..8dc96cc1 --- /dev/null +++ b/app/apps/common/tests/test_demo_data.py @@ -0,0 +1,89 @@ +from datetime import date +from unittest import mock + +from django.contrib.auth import get_user_model +from django.core.management import call_command +from django.test import TestCase, override_settings +from django.urls import reverse + +from apps.common.middleware.thread_local import delete_current_user, write_current_user +from apps.dca.models import DCAStrategy +from apps.transactions.models import Transaction + + +@override_settings( + STORAGES={ + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage" + }, + }, + WHITENOISE_AUTOREFRESH=True, +) +class LoadDemoDataTests(TestCase): + def load(self, today): + with mock.patch("django.utils.timezone.localdate", return_value=today): + call_command("load_demo_data", verbosity=0) + user = get_user_model().objects.get(email="demo@demo.com") + write_current_user(user) + self.addCleanup(delete_current_user) + return user + + def test_dates_follow_today(self): + """Demo data must be visible on first load, whatever day it is.""" + today = date(2031, 7, 1) # the 1st is the edge case for "late" + user = self.load(today) + + this_month = Transaction.objects.filter(reference_date=today.replace(day=1)) + self.assertTrue(this_month.filter(is_paid=True).exists()) + self.assertTrue(this_month.filter(description="Salary").exists()) + # Upcoming, unpaid items exist so projections have something to show + self.assertTrue( + Transaction.objects.filter(date__gt=today, is_paid=False).exists() + ) + # Past unpaid items show up on the late transactions page + self.assertTrue( + Transaction.objects.filter(date__lt=today, is_paid=False).exists() + ) + # Nothing in the past is left as an unpaid installment/recurring occurrence + self.assertFalse( + Transaction.objects.filter( + date__lte=today, is_paid=False, installment_plan__isnull=False + ).exists() + ) + self.assertTrue(user.check_password("wygiwyhdemo")) + + def test_running_twice_is_a_no_op(self): + self.load(date(2031, 7, 14)) + count = Transaction.all_objects.count() + call_command("load_demo_data", verbosity=0) + self.assertEqual(Transaction.all_objects.count(), count) + + def test_pages_render_with_demo_data(self): + today = date.today() + user = self.load(today) + self.client.force_login(user) + htmx = {"HTTP_HX_REQUEST": "true"} + month = {"month": today.month, "year": today.year} + urls = [ + reverse("monthly_overview", kwargs=month), + reverse("monthly_summary", kwargs=month), + reverse("monthly_account_summary", kwargs=month), + reverse("monthly_currency_summary", kwargs=month), + reverse("monthly_transactions_list", kwargs=month), + reverse("net_worth_current"), + reverse("net_worth_projected"), + reverse("yearly_index_account"), + reverse("transactions_all_list"), + reverse("insights_late_transactions"), + reverse("insights_emergency_fund"), + reverse("quick_transactions_index"), + reverse("rules_index"), + ] + [ + reverse("dca_strategy_detail", kwargs={"strategy_id": s.id}) + for s in DCAStrategy.objects.all() + ] + for url in urls: + with self.subTest(url=url): + response = self.client.get(url, **htmx) + self.assertLess(response.status_code, 400) diff --git a/app/fixtures/demo_data.json b/app/fixtures/demo_data.json deleted file mode 100644 index 1304f0df..00000000 --- a/app/fixtures/demo_data.json +++ /dev/null @@ -1,1022 +0,0 @@ -[ - { - "model": "users.user", - "pk": 1, - "fields": { - "password": "pbkdf2_sha256$870000$kUmqeqdenjc2yFS8qbniwS$7qOMXzKG+yFmezdjhptkwuMJlqlZnQHXgAnonWurpBk=", - "last_login": "2025-03-31T03:22:25Z", - "is_superuser": false, - "first_name": "Demo", - "last_name": "User", - "is_staff": false, - "is_active": true, - "date_joined": "2025-03-31T03:21:04Z", - "email": "demo@demo.com", - "groups": [], - "user_permissions": [] - } - }, - { - "model": "users.usersettings", - "pk": 1, - "fields": { - "user": 1, - "hide_amounts": false, - "mute_sounds": false, - "date_format": "SHORT_DATE_FORMAT", - "datetime_format": "SHORT_DATETIME_FORMAT", - "number_format": "AA", - "language": "auto", - "timezone": "auto", - "start_page": "MONTHLY_OVERVIEW" - } - }, - { - "model": "transactions.transactioncategory", - "pk": 1, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Bitcoin", - "mute": false, - "active": true, - "shared_with": [] - } - }, - { - "model": "transactions.transactioncategory", - "pk": 2, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Salary", - "mute": false, - "active": true, - "shared_with": [] - } - }, - { - "model": "transactions.transactioncategory", - "pk": 3, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Debt", - "mute": false, - "active": true, - "shared_with": [] - } - }, - { - "model": "transactions.transactioncategory", - "pk": 4, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Streaming", - "mute": false, - "active": true, - "shared_with": [] - } - }, - { - "model": "transactions.transactiontag", - "pk": 1, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Company X", - "active": true, - "shared_with": [] - } - }, - { - "model": "transactions.transactionentity", - "pk": 1, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Sister", - "active": true, - "shared_with": [] - } - }, - { - "model": "transactions.transaction", - "pk": 1, - "fields": { - "owner": 1, - "account": 3, - "type": "EX", - "is_paid": true, - "date": "2025-03-31", - "reference_date": "2025-03-01", - "amount": "1000.000000000000000000000000000000", - "description": "Bought some bitcoin", - "notes": "This is a transfer to buy Bitcoin", - "category": 1, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:46:09.858Z", - "updated_at": "2025-03-31T15:24:39.515Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 2, - "fields": { - "owner": 1, - "account": 4, - "type": "IN", - "is_paid": true, - "date": "2025-03-31", - "reference_date": "2025-03-01", - "amount": "0.012000000000000000000000000000", - "description": "Bought some bitcoin", - "notes": "This is a transfer to buy Bitcoin", - "category": 1, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:46:09.881Z", - "updated_at": "2025-03-31T15:24:39.530Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 3, - "fields": { - "owner": 1, - "account": 3, - "type": "IN", - "is_paid": true, - "date": "2025-03-31", - "reference_date": "2025-03-01", - "amount": "20000.000000000000000000000000000000", - "description": "My totally real salary", - "notes": "", - "category": 2, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:47:39.200Z", - "updated_at": "2025-03-31T14:47:39.200Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 4, - "fields": { - "owner": 1, - "account": 2, - "type": "IN", - "is_paid": false, - "date": "2025-03-31", - "reference_date": "2025-03-01", - "amount": "1000.000000000000000000000000000000", - "description": "My sister owns me some money, but hasn't paid yet", - "notes": "If unpaid, this is a projected income", - "category": 3, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:48:43.660Z", - "updated_at": "2025-03-31T14:49:01.611Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 5, - "fields": { - "owner": 1, - "account": 2, - "type": "IN", - "is_paid": true, - "date": "2009-01-01", - "reference_date": "2009-01-01", - "amount": "1000.000000000000000000000000000000", - "description": "Very old transaction? No problem", - "notes": "", - "category": null, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:49:35.099Z", - "updated_at": "2025-03-31T14:50:23.535Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 6, - "fields": { - "owner": 1, - "account": 1, - "type": "EX", - "is_paid": true, - "date": "2025-03-01", - "reference_date": "2025-04-01", - "amount": "20.000000000000000000000000000000", - "description": "This transaction happened in one month, but should be counted for the next month", - "notes": "Good for credit cards", - "category": null, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:51:55.012Z", - "updated_at": "2025-03-31T14:51:55.012Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 7, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-03-31", - "reference_date": "2025-03-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 1, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.013Z", - "updated_at": "2025-03-31T14:53:36.013Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 8, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-04-30", - "reference_date": "2025-04-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 2, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.034Z", - "updated_at": "2025-03-31T14:53:36.034Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 9, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-05-31", - "reference_date": "2025-05-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 3, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.048Z", - "updated_at": "2025-03-31T14:53:36.048Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 10, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-06-30", - "reference_date": "2025-06-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 4, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.063Z", - "updated_at": "2025-03-31T14:53:36.063Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 11, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-07-31", - "reference_date": "2025-07-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 5, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.079Z", - "updated_at": "2025-03-31T14:53:36.079Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 12, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-08-31", - "reference_date": "2025-08-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 6, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.094Z", - "updated_at": "2025-03-31T14:53:36.094Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 13, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-09-30", - "reference_date": "2025-09-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 7, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.110Z", - "updated_at": "2025-03-31T14:53:36.110Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 14, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-10-31", - "reference_date": "2025-10-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 8, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.126Z", - "updated_at": "2025-03-31T14:53:36.126Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 15, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-11-30", - "reference_date": "2025-11-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 9, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.144Z", - "updated_at": "2025-03-31T14:53:36.144Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 16, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-12-31", - "reference_date": "2025-12-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 10, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.162Z", - "updated_at": "2025-03-31T14:53:36.162Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 17, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2026-01-31", - "reference_date": "2026-01-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 11, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.178Z", - "updated_at": "2025-03-31T14:53:36.178Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 18, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2026-02-05", - "reference_date": "2026-02-01", - "amount": "10.000000000000000000000000000000", - "description": "I've bought something in 12 installments", - "notes": "", - "category": null, - "installment_plan": 1, - "installment_id": 12, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:53:36.195Z", - "updated_at": "2025-03-31T14:53:36.195Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 19, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-03-31", - "reference_date": "2025-03-01", - "amount": "24.990000000000000000000000000000", - "description": "Netflix", - "notes": "", - "category": 4, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": 1, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:55:18.754Z", - "updated_at": "2025-03-31T14:55:18.754Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 20, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-04-30", - "reference_date": "2025-04-01", - "amount": "24.990000000000000000000000000000", - "description": "Netflix", - "notes": "", - "category": 4, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": 1, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:55:18.771Z", - "updated_at": "2025-03-31T14:55:18.771Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 21, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-05-31", - "reference_date": "2025-05-01", - "amount": "24.990000000000000000000000000000", - "description": "Netflix", - "notes": "", - "category": 4, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": 1, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:55:18.783Z", - "updated_at": "2025-03-31T14:55:18.783Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 22, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-06-30", - "reference_date": "2025-06-01", - "amount": "24.990000000000000000000000000000", - "description": "Netflix", - "notes": "", - "category": 4, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": 1, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:55:18.795Z", - "updated_at": "2025-03-31T14:55:18.795Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 23, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-07-31", - "reference_date": "2025-07-01", - "amount": "24.990000000000000000000000000000", - "description": "Netflix", - "notes": "", - "category": 4, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": 1, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:55:18.807Z", - "updated_at": "2025-03-31T14:55:18.807Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 24, - "fields": { - "owner": 1, - "account": 6, - "type": "EX", - "is_paid": false, - "date": "2025-08-31", - "reference_date": "2025-08-01", - "amount": "24.990000000000000000000000000000", - "description": "Netflix", - "notes": "", - "category": 4, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": 1, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:55:18.818Z", - "updated_at": "2025-03-31T14:55:18.818Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.transaction", - "pk": 25, - "fields": { - "owner": 1, - "account": 1, - "type": "IN", - "is_paid": true, - "date": "2025-03-31", - "reference_date": "2025-03-01", - "amount": "1020.000000000000000000000000000000", - "description": "Balance reconciliation", - "notes": "", - "category": null, - "installment_plan": null, - "installment_id": null, - "recurring_transaction": null, - "internal_note": "", - "internal_id": null, - "deleted": false, - "created_at": "2025-03-31T14:55:52.905Z", - "updated_at": "2025-03-31T14:56:11.264Z", - "deleted_at": null, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.installmentplan", - "pk": 1, - "fields": { - "account": 6, - "type": "EX", - "description": "I've bought something in 12 installments", - "number_of_installments": 12, - "installment_start": 1, - "installment_total_number": 12, - "start_date": "2025-03-31", - "reference_date": "2025-03-31", - "end_date": "2026-02-28", - "recurrence": "monthly", - "installment_amount": "10.000000000000000000000000000000", - "category": null, - "notes": "", - "add_description_to_transaction": true, - "add_notes_to_transaction": false, - "tags": [], - "entities": [] - } - }, - { - "model": "transactions.recurringtransaction", - "pk": 1, - "fields": { - "is_paused": false, - "account": 6, - "type": "EX", - "amount": "24.990000000000000000000000000000", - "description": "Netflix", - "category": 4, - "notes": "Netflix is a recurring transaction", - "reference_date": "2025-03-31", - "start_date": "2025-03-31", - "end_date": null, - "recurrence_type": "month", - "recurrence_interval": 1, - "last_generated_date": "2025-08-12", - "last_generated_reference_date": "2025-08-12", - "add_description_to_transaction": true, - "add_notes_to_transaction": false, - "tags": [], - "entities": [] - } - }, - { - "model": "currencies.currency", - "pk": 1, - "fields": { - "code": "USD", - "name": "Dollar", - "decimal_places": 2, - "prefix": "$ ", - "suffix": "", - "exchange_currency": null - } - }, - { - "model": "currencies.currency", - "pk": 2, - "fields": { - "code": "BTC", - "name": "Bitcoin", - "decimal_places": 8, - "prefix": "₿ ", - "suffix": "", - "exchange_currency": 1 - } - }, - { - "model": "currencies.currency", - "pk": 3, - "fields": { - "code": "ETH", - "name": "Ethereum", - "decimal_places": 8, - "prefix": "⟠ ", - "suffix": "", - "exchange_currency": 1 - } - }, - { - "model": "currencies.exchangerate", - "pk": 1, - "fields": { - "from_currency": 3, - "to_currency": 1, - "rate": "1880.610000000000000000000000000000", - "date": "2025-03-31T14:42:00Z" - } - }, - { - "model": "currencies.exchangerate", - "pk": 2, - "fields": { - "from_currency": 2, - "to_currency": 1, - "rate": "83825.100000000000000000000000000000", - "date": "2025-03-31T14:43:00Z" - } - }, - { - "model": "dca.dcastrategy", - "pk": 1, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Bitcoin", - "target_currency": 2, - "payment_currency": 1, - "notes": "", - "created_at": "2025-03-31T15:24:09.226Z", - "updated_at": "2025-03-31T15:24:09.226Z", - "shared_with": [] - } - }, - { - "model": "dca.dcaentry", - "pk": 1, - "fields": { - "strategy": 1, - "date": "2025-03-31", - "amount_paid": "1000.000000000000000000000000000000", - "amount_received": "0.012000000000000000000000000000", - "expense_transaction": 1, - "income_transaction": 2, - "notes": "", - "created_at": "2025-03-31T15:24:39.537Z", - "updated_at": "2025-03-31T15:24:39.537Z" - } - }, - { - "model": "accounts.accountgroup", - "pk": 1, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Coinbase", - "shared_with": [] - } - }, - { - "model": "accounts.accountgroup", - "pk": 2, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Bank of America", - "shared_with": [] - } - }, - { - "model": "accounts.account", - "pk": 1, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Savings", - "group": 2, - "currency": 1, - "exchange_currency": null, - "is_asset": true, - "is_archived": false, - "shared_with": [] - } - }, - { - "model": "accounts.account", - "pk": 2, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Cash", - "group": null, - "currency": 1, - "exchange_currency": null, - "is_asset": false, - "is_archived": false, - "shared_with": [] - } - }, - { - "model": "accounts.account", - "pk": 3, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Checking", - "group": 2, - "currency": 1, - "exchange_currency": null, - "is_asset": false, - "is_archived": false, - "shared_with": [] - } - }, - { - "model": "accounts.account", - "pk": 4, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Bitcoin", - "group": 1, - "currency": 2, - "exchange_currency": null, - "is_asset": true, - "is_archived": false, - "shared_with": [] - } - }, - { - "model": "accounts.account", - "pk": 5, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Ethereum", - "group": 1, - "currency": 3, - "exchange_currency": null, - "is_asset": true, - "is_archived": false, - "shared_with": [] - } - }, - { - "model": "accounts.account", - "pk": 6, - "fields": { - "owner": 1, - "visibility": "private", - "name": "Credit card", - "group": 2, - "currency": 1, - "exchange_currency": null, - "is_asset": false, - "is_archived": false, - "shared_with": [] - } - } -]