- Amounts default to the linked transaction's amount when left empty
- Linked transactions must be in the strategy's payment/target currency; pickers only search those
- Allow setting entities on transactions created from an entry
- Fix crash when only the expense transaction was linked
Holdings under a year annualized their rate before turning it back into
the period return, so a few days with a modest gain (+1.3% in one day is
~10,600% a year) fell outside xirr's search range and showed no percentage.
Solve the rate over the period held instead.
- One hero number with a single return line; secondary totals behind a Details toggle
- Holdings in one list card with quiet group subheaders, value and %/yr per row
- Group holdings by account group or asset class, remembered in the session
- Detail page: value and chart up top, Your position and home-currency sidebar
- Replace tooltips with visible text; keep amounts from wrapping on phones
- Unit-priced accounts now honour their income categories, tags and entities:
a matching transaction is a gain or loss, and any other unlinked one is money
in or out at that day's market price. Before, every unlinked outflow was a
loss and yields were booked as new money in.
- A transfer out of an account tracked by value counts its own amount. It used
the other side converted at the day's rate, so a coin bought from a savings
account moved the coin's price change onto the savings account.
AccountSerializer assigned the many-to-many fields with setattr, which Django rejects, so any write that included them returned a 500. The default ModelSerializer create/update already handles them.
Investments landed without demo data. The ETF, Bitcoin and savings accounts become investments, and new accounts cover the rest of the tracker: Apple shares (dividends paid into checking, a partial sale, gifted shares with an estimated cost), a retirement fund tracked by value (quarterly gains and a loss, a yearly fee) and gold. Asset classes have targets that add up to 100%, plus one without a target to show how rebalancing leaves it out.
The investment examples use their own random seed, so the rest of the demo is unchanged. The demo test now renders the investment pages and checks each kind of holding has figures.
The transfer form had no entities field and always created paid transfers. Each side now has its own entities, shown as from -> to in the list like category and tags, and a Projected/Paid toggle applies to both sides.
Editing a transfer also offered only active categories, tags and entities and non-archived accounts, so saving silently dropped a deactivated tag or failed on an archived account. The transfer's own items stay selectable, as in the transaction form.
Brazilian banks through Open Finance. Users give their Pluggy client
credentials and the ids of their connections (items), from Meu Pluggy.
- Uses the cursor-paginated /v2/transactions; /transactions is removed
after 2026-12-31. Our own filters are sent again with each cursor.
- The direction comes from `type`, not the amount's sign: credit cards
keep the card's sign, purchases are positive. Checked against a real
Meu Pluggy card and checking account; the sandbox data doesn't match
production here.
- Pending transactions keep their id once they post (a card's open bill
stays pending until it closes), so they're imported unpaid and settled
later. Future-dated ones (installments, scheduled payments) wait for
their date. Installments already come dated in their own month.
- Foreign purchases use the account's amount, with the original amount
in the notes.
- Connections that need the user to log in again show as an error.
Providers can now return pending transactions, flagged as such, when
their ids survive posting. They're imported unpaid right away and, once
the bank posts them, marked paid with their final amount and date.
- Settling happens once, so later edits stick, and one marked paid by
hand is left alone. It goes through save() so a transfer's other side
follows; no rules run again. reference_date moves only if it still
followed the date.
- Each sync looks back to the oldest unpaid synced transaction, since a
card purchase can post weeks later when the bill closes. Pending ones
older than 60 days are taken as cancelled and no longer followed.
Users paste a one-time setup token. On save it's claimed and the access
URL it returns is stored; credentials go in a header, never the URL, so
errors don't leak them.
- Providers can validate or transform their settings (clean_settings),
run by the service form once every other field is valid, so a token
isn't spent on a form that fails anyway.
- Providers can declare a minimum sync interval. SimpleFIN Bridge
disables tokens that go much past ~24 requests a day, so it's 6 hours.
- Tokens and access URLs are user input pointing the server at a host:
only SimpleFIN Bridge is allowed unless SIMPLEFIN_ALLOWED_HOSTS says
otherwise. Checked again on every request, for restored settings.
- Transactions are fetched per account in 90-day windows, Bridge's
limit per request. Pending ones are skipped.
- /api/investment-classes/ to list, create, edit and delete asset classes
- Accounts return their asset class and take investment_class_id, filterable
by investment_class
- Unique names and targets up to 100% are checked in the model, shared by the
form and the API
Provider registry modeled after automatic exchange rates: each provider
declares its own settings fields, rendered dynamically in the service form.
Lunch Flow is the first provider.
- Map provider accounts to WYGIWYH accounts with a start date and an
optional currency filter
- Hourly task syncs due services; manual sync from the list
- Deduplicates through Transaction.internal_id, optionally matching
transactions already entered by hand
- Per account errors and provider reported connection problems
Refs #498
Each word of the query must match something, and words can match
different fields: description, notes, category, tags, entities, account
and account group, a date in the user's format (the year is optional),
or an amount in the user's number format, where a whole number like 234
matches 234.00 to 234.99.
Matching through an id subquery also stops transactions with several
matching tags from showing up more than once.
Results come back as today, tomorrow, yesterday, upcoming (soonest first)
and then past (newest first); past dates used to be oldest first. The
endpoint is now capped at 100 results, as its docstring already claimed.
tom-select also re-scored and re-sorted the server's results on the
client, which scrambled the order while searching. Load-based selects now
keep the server's order, drop the previous query's results, and preload
recent transactions on focus.
TransactionSelect options now look like a compact version of the
transaction item: paid state, type colour, date, description, category,
tags, entities, amount and account. The search endpoint returns that data
as JSON, and pre-selected options carry it in data-data so edit forms
render the same way.
Nesting Row(Column(Row(Column))) shrank every field to a quarter of the
width, since Column defaults to half, and rounded-3 is a Bootstrap class
that doesn't exist here. Use the same card layout as the transfer form:
account at full width with category and tags side by side.
The temp root is resolve()d in settings but the file path was only
abspath()ed, so a symlink inside the temp folder could point outside it
and still pass the check. Resolve both sides the same way.
Also drop the unused /temp folder from the prod image.
A non-numeric, NaN or infinite value raised while parsing and threw away
the whole table. Each bad row is now skipped on its own.
Log a warning when a table has rows but none of a usable rate type, so a
bank with unknown types does not fail silently.
Add an Options field to exchange rate services. Providers get the raw
text, can validate it on save through clean_options, and describe it
with options_help.
The service form now shows API Key only for providers that require one,
and Options only for providers that read it, with that provider's help.
AllRatesToday reads its bank code from Options instead of API Key.
Restore asset classes before accounts and take a second pass over accounts
once categories, tags and entities exist, so their links survive a restore
into a fresh install.
- Asset classes (name, optional target %) managed under Management > Investments,
chosen per account
- Charts tab: allocation by asset class and currency, value against invested,
return by investment; off-target classes flagged by the 5/25 rule
- Rebalance tab: what to buy or sell per class to reach the targets
- Investment page: price against average cost with trades, monthly earnings
and income
A value the default manager hides, such as another user's object already set
on a shared record, was taken as a new name and created a duplicate named
after its id.
Import uploads were always written to /usr/src/app/temp, presets were
read from /usr/src/app/import_presets and the caches lived in /var/tmp,
so WYGIWYH only worked from the Docker image's layout. Running it as a
systemd service with a strict sandbox, or on an immutable distro like
NixOS, needs these folders to follow the app or be configurable.
- Presets are read from IMPORT_PRESETS_ROOT, the import_presets folder
next to the app.
- Uploads wait in IMPORT_TEMP_ROOT, the temp folder next to the app by
default, and movable with the IMPORT_TEMP_ROOT env var. The web views,
the API and the import service all use it.
- The file-based caches live under CACHE_ROOT, /var/tmp by default, and
movable with the CACHE_ROOT env var.
In the Docker image the app sits in /usr/src/app, so every default
resolves to the same folder as before.
The import service checked that a file was inside the temp folder with a
plain string prefix, which also accepted sibling folders such as
/usr/src/app/temp-other. It now compares paths.
CI no longer needs to create /usr/src/app before running the tests.
Fixes#472
Unlinked transactions on value-tracked accounts counted as gains and losses,
so a salary was yield and a bill was a loss. They now count as money in and
out, unless they match the account's new income categories or tags, or its
income entities. Those three define the account's result; only entities also
match income paid into other accounts.
Replace the single exchange rate option with a limit of rates kept per
currency pair (oldest deleted first) and an option to keep one rate per
day. Services that kept a single rate migrate to a limit of 1.
Investments are accounts flagged as investments; performance is derived from their transactions. Linked transfers are contributions and withdrawals (average cost, realized gains), unlinked transactions are gains or losses, and income comes from the accounts' income entities.
- Unit-priced (exchange currency) or value-tracked accounts
- Money-weighted return (XIRR), annualized after a year
- Totals converted along each currency's exchange chain, with currency effect
- Investments list and detail pages; buy/sell/income use the transfer and transaction forms
- GET /api/accounts/<id>/performance/
Transactions created from a DCA entry, or picked from existing ones, are now linked as a transfer, so investment tracking sees the purchase. A data migration links the pairs of existing entries when valid.
The update check shared the file-based default cache with cachalot, whose culling randomly deleted the key as pages loaded. Store only the latest release in a dedicated cache, compare it to APP_VERSION at render time, and drop the startup delete. Hide the banner when CHECK_FOR_UPDATES is off or APP_VERSION isn't a valid version.
- Add flag-off login page and admin password tests so the refusal tests can't pass for unrelated reasons
- Expect exactly 401 from refused basic auth
- Gate OIDC_DISABLE_LOCAL_LOGIN on an explicit OIDC_CONFIGURED flag
- Realign the README OIDC table, use "email" consistently, and warn about lockout with OIDC_ALLOW_SIGNUP=false
When the flag is on and an OIDC provider is configured, every e-mail and
password check is refused: the login page, the admin and the API's basic
auth. OIDC logins, API tokens, OAuth apps and permission checks keep working.
The login page hides the password form.