Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ See [docker/README.md](docker/README.md) and [docs/CONFIGURATION.md](docs/CONFIG
- Negative expenses are supported for refunds and corrections.
- PWA support with push notifications.
- Activity feed with edits and deletions.
- Bulk import of a group's expenses from a CSV export, mapping columns and categories.
- Detailed balances per person and per group.

## UI preview
Expand All @@ -52,7 +53,7 @@ Groups are the primary way to use SplitPro. You can invite friends by email, or

### 4) Data utilities

Splitwise import supports friends and groups (partial import). Expenses themselves are not imported yet. You can export data from the balances view and account settings.
Splitwise import supports friends and groups (partial import); Splitwise expenses themselves are not imported yet. A group's expenses can be bulk imported from a CSV export, such as a monthly statement from a bank or a personal finance app. Columns are detected from the header row, the file's own categories are matched onto SplitPro's, and rows carrying the opposite sign are imported as money received. See [docs/CSV_IMPORT.md](docs/CSV_IMPORT.md). You can export data from the balances view and account settings.

### 5) Translations

Expand Down Expand Up @@ -95,6 +96,7 @@ Bank integration allows you to load transactions from providers like Plaid and c
- [docs/CURRENCY_CONVERSIONS.md](docs/CURRENCY_CONVERSIONS.md)
- [docs/RECURRING_TRANSACTIONS.md](docs/RECURRING_TRANSACTIONS.md)
- [docs/BANK_TRANSACTIONS.md](docs/BANK_TRANSACTIONS.md)
- [docs/CSV_IMPORT.md](docs/CSV_IMPORT.md)
- [docker/README.md](docker/README.md)

## Versions
Expand Down
146 changes: 146 additions & 0 deletions docs/CSV_IMPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# CSV IMPORT

SplitPro can import a group's expenses in bulk from a CSV file, for example a monthly export from
a bank or a personal finance app. No configuration or external service is required.

## How to use it

1. Open a group, then the group info drawer (the ⓘ icon).
2. Under **Actions**, choose **Import expenses from CSV**.
3. Pick the file. SplitPro reads the header row and guesses which columns hold the date,
the description and the amount.
4. Check the options and the preview, then import.

## What gets created

Every selected row becomes one expense in the group:

- Split **equally** between all group members.
- Paid by whoever is selected in **Paid by** (the importing user by default).
- Categorised from the file's own category column where it can be matched, and **General** otherwise.
- Dated from the file's date column.

Imported expenses are ordinary expenses. Nothing about them is special afterwards, and each one can
be edited or deleted individually.

## Columns

Columns are mapped into SplitPro's Date, Description, and Amount. The import operation attempts
to recognize the appropriate columns.

| Role | Recognised header names |
| ------------------------- | ----------------------------------------------------------------------------------------------- |
| Date | `Transaction Date`, `Date`, `Posted`, `Day` |
| Description (one or more) | `Payee`, `Description`, `Merchant`, `Narrative`, `Details`, `Note`, `Memo`, `Reference`, `Name` |
| Amount | `Amount`, `Value`, `Debit`, `Total`, `Sum` |

Detection is case-insensitive and matches substrings, so `Transaction Payee` is recognised as the
description. Whatever is detected can be changed, so a file with unrecognised headers still works.

### Combining columns into the description

A SplitPro expense has a single description, but exports often spread it over several fields --
a payee plus a note, or a merchant plus a memo. **Description takes any number of columns**, chosen
with tick boxes rather than a dropdown. Every matching column is ticked automatically, and you can
add or remove any of them.

Ticked columns are joined in CSV order, separated by `-`. Columns that are blank on a given row
are skipped, so a mostly-empty note column costs nothing:

| Payee | Note | Imported description |
| -------------------- | ------------- | ---------------------------------- |
| `Whole Foods Market` | `weekly shop` | `Whole Foods Market - weekly shop` |
| `Haggen` | | `Haggen` |

A row where every ticked column is blank is flagged as missing a description and cannot be
selected.

## Categories

SplitPro has a fixed category list, so the file's own categories are matched onto it. The value is
tried whole first, then split on `:`, `.` and `-` and each part tried in turn. A specific item beats
a broad section, and anything unrecognised falls back to **General** rather than failing the row.

| In the file | Imported as |
| ------------------------ | ---------------------------------------------------- |
| `Food:Groceries` | Groceries |
| `Pets:Pet Supplies` | Pets |
| `Utilities` | Utilities |
| `Utilities:Web Services` | Utilities (the leaf is unknown, the section matches) |
| `Dining-Out` | Dining Out (matched before splitting) |
| `Widgets` | General |

Matching ignores case, spaces and punctuation, so `Dining Out`, `dining-out` and `diningOut` all
resolve to the same category. The preview shows each row's resolved category as an icon, so a file
whose categories do not match is obvious before you import.

Leave the category column unmapped to file everything under General.

## Amount sign and money received

Exports disagree about which side of zero means spending. The **Amount sign** option says which
convention the file uses:

- **Expenses are negative** — `-$118.24` is money spent. This is the common bank convention.
- **Expenses are positive** — `118.24` is money spent.

Rows with the **opposite** sign are money received — a refund, a rebate, a reimbursement — and are
imported as negative expenses, which move balances the other way. They are labelled `Received` in
the preview.

Getting this option backwards is not silent: every row flips between expense and received in the
preview before anything is written.

## Dates

The date layout is detected from the file and can be overridden. `7/27/26` is unambiguously
month-first because 27 cannot be a month; a file where every row is ambiguous (`7/5/26`) defaults to
month-first, so check the preview if your export is day-first.

Month names are matched in English only. For other languages, export dates in a numeric or ISO
format.

## Duplicates

Re-importing a file you have already imported is an easy mistake to make with a monthly workflow.
Rows matching an existing expense in the group on **date, amount, description and currency** are
labelled `Already in group` and start unselected. This is advisory — you can select them anyway.

## Amount format

Currency symbols and thousands separators are stripped, so `-$1,234.56` reads correctly. Numbers are
interpreted using the **separators of your app language**: with English selected, `1.234,56` is not
read as 1234.56. The preview shows the parsed value, so a mismatch is visible before importing.

Any other character makes the amount unreadable, so a typo like `1O.00` is flagged rather than
imported as 1.00. The selected currency's code is allowed, as in `USD 12.00`.

Accounting-style parentheses for negatives (`(118.24)`) are not recognised as a sign; such a value
reads as positive. Export plain signed numbers instead.

One currency applies to the whole import; there is no per-row currency column.

## File format

Standard RFC 4180 CSV:

- Comma separated, first line is the header.
- Fields may be quoted; quoted fields may contain commas, newlines and `""`-escaped quotes.
- LF and CRLF line endings are both accepted, as is a leading byte order mark.
- Blank lines are ignored.

Rows that cannot be used are listed in the preview with a reason (unreadable date, unreadable
amount, zero amount, missing description) and cannot be selected.

## Example

```csv
Date,Chk #,Transaction Payee,Note,Account,Category,Amount
7/27/26,0,Whole Foods Market,,Apple Card,Food:Groceries,-$118.24
7/28/26,0,Haggen,,Apple Card,Food:Groceries,-$30.04
7/13/26,0,Payment to xfinity,,Checking (0967),Utilities,-$55.00
8/2/26,0,Utility rebate,,Checking (0967),Utilities,$40.00
```

With **Expenses are negative**, the first three rows import as expenses and the rebate imports as
money received.
41 changes: 41 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,47 @@
"title": "Group statistics",
"total_expenses": "Total expenses"
},
"import_csv": {
"title": "Import expenses from CSV",
"note": "Every row becomes an expense split equally between all group members. An attempt to map the incoming category is made and non-matches default to the General category.",
"choose_file": "Choose file",
"no_file_chosen": "No file chosen",
"columns": "Columns",
"column_date": "Date",
"column_description": "Description",
"column_description_hint": "Tick one or more columns. They are joined in order, and blank ones are skipped.",
"column_amount": "Amount",
"column_category": "Category",
"column_unmapped": "Not mapped",
"options": "Options",
"amount_sign": "Amount sign",
"amount_sign_options": {
"expenses_negative": "Expenses are negative",
"expenses_positive": "Expenses are positive"
},
"date_format": "Date format",
"paid_by": "Paid by",
"currency": "Currency",
"preview": "Preview",
"selected": "{{selected}} of {{total}} selected",
"received": "Received",
"already_in_group": "Already in group",
"no_name": "No description",
"select_row": "Import line {{line}}: {{name}}",
"importing": "Imported {{done}} of {{total}}",
"row_errors": {
"invalid_date": "Unreadable date",
"invalid_amount": "Unreadable amount",
"zero_amount": "Zero amount",
"missing_name": "Missing description"
},
"messages": {
"import_success": "Imported {{count}} expenses",
"import_partial": "Imported {{count}} expenses before the import failed",
"no_rows_found": "That file has no rows below the header",
"duplicates_unavailable": "Could not load this group's expenses to check for duplicates. Reload to try again."
}
},
"messages": {
"default_split_cleared": "Default split cleared",
"default_split_updated": "Default split updated",
Expand Down
Loading