├── .github └── workflows │ └── main.yml ├── .gitignore ├── .pyup.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── conftest.py ├── csv2ofx ├── __init__.py ├── __main__.py ├── main.py ├── mappings │ ├── __init__.py │ ├── abnamro.py │ ├── amazon.py │ ├── boursorama.py │ ├── capitalone.py │ ├── creditunion.py │ ├── custom.py │ ├── default.py │ ├── eqbank.py │ ├── exim.py │ ├── gls.py │ ├── ingdirect.py │ ├── ingesp.py │ ├── mdb.py │ ├── mint.py │ ├── mint_extra.py │ ├── mint_headerless.py │ ├── mintapi.py │ ├── msmoneyreport.py │ ├── n26.py │ ├── outbank.py │ ├── payoneer.py │ ├── pcmastercard.py │ ├── rabobank.py │ ├── schwabchecking.py │ ├── split_account.py │ ├── starling.py │ ├── stripe.py │ ├── ubs-ch-fr.py │ ├── ubs.py │ ├── xero.py │ └── yodlee.py ├── ofx.py ├── qif.py ├── utils.py └── utilz │ └── csvtrim ├── data ├── converted │ ├── amazon.ofx │ ├── creditunion.qif │ ├── default.ofx │ ├── default.qif │ ├── default_w_splits.ofx │ ├── default_w_splits.qif │ ├── gls.ofx │ ├── ingesp.ofx │ ├── mint.ofx │ ├── mint.qif │ ├── mint_alt.qif │ ├── mint_extra.qif │ ├── n26.ofx │ ├── outbank.ofx │ ├── payoneer.ofx │ ├── pcmastercard.ofx │ ├── schwab-checking-baltest-case1.ofx │ ├── schwab-checking-baltest-case2.ofx │ ├── schwab-checking-baltest-case3.ofx │ ├── schwab-checking-baltest-case4.ofx │ ├── schwab-checking-baltest-case5.ofx │ ├── schwab-checking-baltest-case6.ofx │ ├── schwab-checking-baltest-case7.ofx │ ├── schwab-checking-msmoney.ofx │ ├── schwab-checking.ofx │ ├── stripe-all.ofx │ ├── stripe-all.qif │ ├── stripe-default.ofx │ ├── stripe-default.qif │ ├── ubs-ch-fr.qif │ └── xero.qif ├── example │ ├── investment_example.qif │ ├── transaction_example.ofx │ └── transfer_example.ofx └── test │ ├── amazon.csv │ ├── capitalone.csv │ ├── creditunion.csv │ ├── default.csv │ ├── gls.csv │ ├── ingesp.csv │ ├── mint.csv │ ├── mint_extra.csv │ ├── mint_headerless.csv │ ├── n26-fr.csv │ ├── outbank.csv │ ├── payoneer.csv │ ├── pcmastercard.csv │ ├── schwab-checking-baltest-case1.csv │ ├── schwab-checking-baltest-case2.csv │ ├── schwab-checking-baltest-case3.csv │ ├── schwab-checking-baltest-case4.csv │ ├── schwab-checking-baltest-case5.csv │ ├── schwab-checking-baltest-case6.csv │ ├── schwab-checking-baltest-case7.csv │ ├── schwab-checking.csv │ ├── stripe-all.csv │ ├── stripe-default.csv │ ├── ubs-ch-fr_trimmed.csv │ └── xero.csv ├── docs ├── OFX 2.1.1.pdf ├── OFXAudit.xls ├── OFX_Message_Support.doc ├── QIF Specification.html ├── QIF Specification_files │ ├── a.html │ ├── a_002.html │ ├── a_003.html │ ├── a_004.html │ ├── a_005.html │ ├── bootstrap.js │ ├── jquery.js │ └── sitecss.css └── qif-file-format.txt ├── helpers ├── check-stage ├── clean ├── srcdist └── wheel ├── pyproject.toml ├── pytest.ini ├── ruff.toml ├── tests └── test_cli.py └── tox.ini /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | merge_group: 5 | push: 6 | branches-ignore: 7 | # temporary GH branches relating to merge queues (jaraco/skeleton#93) 8 | - gh-readonly-queue/** 9 | tags: 10 | # required if branches-ignore is supplied (jaraco/skeleton#103) 11 | - '**' 12 | pull_request: 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: read 17 | 18 | env: 19 | # Environment variable to support color support (jaraco/skeleton#66) 20 | FORCE_COLOR: 1 21 | 22 | # Suppress noisy pip warnings 23 | PIP_DISABLE_PIP_VERSION_CHECK: 'true' 24 | PIP_NO_WARN_SCRIPT_LOCATION: 'true' 25 | 26 | # Ensure tests can sense settings about the environment 27 | TOX_OVERRIDE: >- 28 | testenv.pass_env+=GITHUB_*,FORCE_COLOR 29 | 30 | 31 | jobs: 32 | test: 33 | strategy: 34 | # https://blog.jaraco.com/efficient-use-of-ci-resources/ 35 | matrix: 36 | python: 37 | - "3.9" 38 | - "3.13" 39 | platform: 40 | - ubuntu-latest 41 | - macos-latest 42 | - windows-latest 43 | include: 44 | - python: "3.10" 45 | platform: ubuntu-latest 46 | - python: "3.11" 47 | platform: ubuntu-latest 48 | - python: "3.12" 49 | platform: ubuntu-latest 50 | - python: "3.14" 51 | platform: ubuntu-latest 52 | - python: pypy3.10 53 | platform: ubuntu-latest 54 | runs-on: ${{ matrix.platform }} 55 | continue-on-error: ${{ matrix.python == '3.14' }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | - name: Install build dependencies 59 | # Install dependencies for building packages on pre-release Pythons 60 | # jaraco/skeleton#161 61 | if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' 62 | run: | 63 | sudo apt update 64 | sudo apt install -y libxml2-dev libxslt-dev 65 | - name: Install runtime dependencies 66 | if: matrix.platform == 'ubuntu-latest' 67 | run: | 68 | sudo apt update 69 | sudo apt install -y language-pack-fr 70 | sudo apt install -y locales 71 | sudo dpkg-reconfigure locales 72 | - name: Setup Python 73 | uses: actions/setup-python@v4 74 | with: 75 | python-version: ${{ matrix.python }} 76 | allow-prereleases: true 77 | - name: Install tox 78 | run: python -m pip install tox 79 | - name: Run 80 | run: tox 81 | 82 | check: # This job does nothing and is only used for the branch protection 83 | if: always() 84 | 85 | needs: 86 | - test 87 | 88 | runs-on: ubuntu-latest 89 | 90 | steps: 91 | - name: Decide whether the needed jobs succeeded or failed 92 | uses: re-actors/alls-green@release/v1 93 | with: 94 | jobs: ${{ toJSON(needs) }} 95 | 96 | release: 97 | environment: 98 | release 99 | permissions: 100 | contents: write 101 | id-token: write 102 | needs: 103 | - check 104 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 105 | runs-on: ubuntu-latest 106 | 107 | steps: 108 | - uses: actions/checkout@v4 109 | - name: Setup Python 110 | uses: actions/setup-python@v4 111 | with: 112 | python-version: 3.x 113 | - name: Build 114 | run: pipx run build 115 | - name: Publish 116 | uses: pypa/gh-action-pypi-publish@release/v1 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.DS_Store 3 | *.ipynb 4 | .cookiecutter 5 | .ipynb_checkpoints/* 6 | .idea/ 7 | example*.log 8 | examples/.ipynb_checkpoints/* 9 | .*.sw* 10 | 11 | # C extensions 12 | *.so 13 | 14 | # venv 15 | bin/ 16 | pyvenv.cfg 17 | 18 | # man 19 | man/ 20 | 21 | # Packages 22 | *.egg 23 | *.egg-info 24 | .eggs 25 | dist 26 | build 27 | eggs 28 | parts 29 | var 30 | sdist 31 | develop-eggs 32 | .installed.cfg 33 | lib 34 | lib64 35 | 36 | # Installer logs 37 | pip-log.txt 38 | 39 | # Unit test / coverage reports 40 | .coverage 41 | .tox 42 | htmlcov/* 43 | 44 | # Translations 45 | *.mo 46 | 47 | # Mr Developer 48 | .mr.developer.cfg 49 | .project 50 | .pydevproject 51 | 52 | # Complexity 53 | output/*.html 54 | output/*/index.html 55 | 56 | # Sphinx 57 | docs/_build 58 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | update: insecure 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Reuben Cummings 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include data * 2 | recursive-include tests * 3 | recursive-include helpers * 4 | recursive-include docs * 5 | recursive-include examples * 6 | include LICENSE 7 | include *.rst 8 | include *.md 9 | include *requirements.txt 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csv2ofx 2 | 3 | [![GHA](https://github.com/reubano/csv2ofx/actions/workflows/main.yml/badge.svg)](https://github.com/reubano/csv2ofx/actions?query=workflow%3A%22tests%22) 4 | [![versions](https://img.shields.io/pypi/pyversions/csv2ofx.svg)](https://pypi.python.org/pypi/csv2ofx) 5 | [![pypi](https://img.shields.io/pypi/v/csv2ofx.svg)](https://pypi.python.org/pypi/csv2ofx) 6 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 7 | 8 | ## INTRODUCTION 9 | 10 | [csv2ofx](http://github.com/reubano/csv2ofx) is a [Python library](#library-examples) and [command line interface program](#cli-examples) that converts CSV files to OFX and QIF files for importing into GnuCash or Moneydance or similar financial accounting programs. csv2ofx has built in support for importing csv files from mint, yoodlee, and xero. 11 | 12 | ## Requirements 13 | 14 | csv2ofx is pure Python and is [tested](https://github.com/reubano/csv2ofx/actions?query=workflow%3A%22tests%22) on a number of Pythons and platforms. 15 | 16 | ## INSTALLATION 17 | 18 | pip install csv2ofx 19 | 20 | (recommended in a [virtualenv](https://virtualenv.pypa.io/en/latest/)) 21 | 22 | ## Usage 23 | 24 | csv2ofx is intended to be used either directly from Python or from the command line. 25 | 26 | ### Library Examples 27 | 28 | *normal OFX usage* 29 | 30 | ```python 31 | import itertools as it 32 | 33 | from meza.io import read_csv, IterStringIO 34 | from csv2ofx import utils 35 | from csv2ofx.ofx import OFX 36 | from csv2ofx.mappings.default import mapping 37 | 38 | ofx = OFX(mapping) 39 | records = read_csv('path/to/file.csv', has_header=True) 40 | groups = ofx.gen_groups(records) 41 | trxns = ofx.gen_trxns(groups) 42 | cleaned_trxns = ofx.clean_trxns(trxns) 43 | data = utils.gen_data(cleaned_trxns) 44 | content = it.chain([ofx.header(), ofx.gen_body(data), ofx.footer()]) 45 | 46 | for line in IterStringIO(content): 47 | print(line) 48 | ``` 49 | 50 | *normal QIF usage* 51 | 52 | ```python 53 | import itertools as it 54 | 55 | from tabutils.io import read_csv, IterStringIO 56 | from csv2ofx import utils 57 | from csv2ofx.qif import QIF 58 | from csv2ofx.mappings.default import mapping 59 | 60 | qif = QIF(mapping) 61 | records = read_csv('path/to/file.csv', has_header=True) 62 | groups = qif.gen_groups(records) 63 | trxns = qif.gen_trxns(groups) 64 | cleaned_trxns = qif.clean_trxns(trxns) 65 | data = utils.gen_data(cleaned_trxns) 66 | content = it.chain([qif.gen_body(data), qif.footer()]) 67 | 68 | for line in IterStringIO(content): 69 | print(line) 70 | ``` 71 | 72 | ### CLI Examples 73 | 74 | *show help* 75 | 76 | csv2ofx -h 77 | 78 | ```bash 79 | usage: csv2ofx [options] 80 | 81 | description: csv2ofx converts a csv file to ofx and qif 82 | 83 | positional arguments: 84 | source the source csv file (default: stdin) 85 | dest the output file (default: stdout) 86 | 87 | options: 88 | -h, --help show this help message and exit 89 | -a, --account TYPE default account type 'CHECKING' for OFX and 'Bank' for QIF. 90 | -e, --end DATE end date (default: today) 91 | -B, --ending-balance BALANCE 92 | ending balance (default: None) 93 | -l, --language LANGUAGE 94 | the language (default: ENG) 95 | -s, --start DATE the start date 96 | -y, --dayfirst interpret the first value in ambiguous dates (e.g. 01/05/09) as the day 97 | -m, --mapping MAPPING_NAME 98 | the account mapping (default: default) 99 | -x, --custom FILE_PATH 100 | path to a custom mapping file 101 | -c, --collapse FIELD_NAME 102 | field used to combine transactions within a split for double entry statements 103 | -C, --chunksize ROWS number of rows to process at a time (default: 2 ** 14) 104 | -r, --first-row ROWS the first row to process (zero based) 105 | -R, --last-row ROWS the last row to process (zero based, negative values count from the end) 106 | -O, --first-col COLS the first column to process (zero based) 107 | -L, --list-mappings list the available mappings 108 | -V, --version show version and exit 109 | -q, --qif enables 'QIF' output instead of 'OFX' 110 | -M, --ms-money enables MS Money compatible 'OFX' output 111 | -o, --overwrite overwrite destination file if it exists 112 | -D, --server-date DATE 113 | OFX server date (default: source file mtime) 114 | -E, --encoding ENCODING 115 | File encoding (default: utf-8) 116 | -d, --debug display the options and arguments passed to the parser 117 | -v, --verbose verbose output 118 | ``` 119 | 120 | *normal usage* 121 | 122 | csv2ofx file.csv file.ofx 123 | 124 | *print output to stdout* 125 | 126 | csv2ofx ~/Downloads/transactions.csv 127 | 128 | *read input from stdin* 129 | 130 | cat file.csv | csv2ofx 131 | 132 | *qif output* 133 | 134 | csv2ofx -q file.csv 135 | 136 | *specify date range from one year ago to yesterday with qif output* 137 | 138 | csv2ofx -s '-1 year' -e yesterday -q file.csv 139 | 140 | *use yoodlee settings* 141 | 142 | csv2ofx -m yoodlee file.csv 143 | 144 | 145 | #### Special cases 146 | 147 | Some banks, like *UBS Switzerland*, may provide CSV exports that are not 148 | readily tractable by csv2ofx because of extra header or trailing lines, 149 | redundant or unwanted columns. These input files can be preprocessed with the 150 | shipped `utilz/csvtrim` shell script. F.i., with mapping `ubs-ch-fr`: 151 | 152 | csvtrim untrimmed.csv | csv2ofx -m ubs-ch-fr 153 | 154 | 155 | ## CUSTOMIZATION 156 | 157 | ### Code modification 158 | 159 | To import csv files with field names different from the default, either modify the mapping file or create your own. New mappings must be placed in the `csv2ofx/mappings` folder. The mapping object consists of a dictionary whose keys are OFX/QIF attributes and whose values are functions that should return the corresponding value from a record (csv row). The mapping function will take in a record, e.g., 160 | 161 | ```python 162 | {'Account': 'savings 2', 'Date': '1/3/15', 'Amount': 5000} 163 | ``` 164 | 165 | The most basic mapping function just returns a specific field or value, e.g., 166 | 167 | ```python 168 | from operator import itemgetter 169 | 170 | mapping = { 171 | 'bank': 'BetterBank', 172 | 'account': itemgetter('Account'), 173 | 'date': itemgetter('Date'), 174 | 'amount': itemgetter('Amount')} 175 | ``` 176 | 177 | But more complex parsing is also possible, e.g., 178 | 179 | ```python 180 | mapping = { 181 | 'account': lambda r: r['Details'].split(':')[0], 182 | 'date': lambda r: '%s/%s/%s' % (r['Month'], r['Day'], r['Year']), 183 | 'amount': lambda r: r['Amount'] * 2, 184 | 'first_row': 1, 185 | 'last_row': 10, 186 | 'filter': lambda r: float(r['Amount']) > 10, 187 | } 188 | ``` 189 | 190 | ### Required field attributes 191 | 192 | attribute | description | default field | example 193 | ----------|-------------|---------------------|-------- 194 | `account`|transaction account|Account|BetterBank Checking 195 | `date`|transaction date|Date|itemgetter('Transaction Date') 196 | `amount`|transaction amount|Amount|itemgetter('Transaction Amount') 197 | 198 | ### Optional field attributes 199 | 200 | attribute | description | default field | default value | example 201 | ----------|-------------|---------------|---------------|-------- 202 | `desc`|transaction description|Reference|n/a|shell station 203 | `payee`|transaction payee|Description|n/a|Shell 204 | `notes`|transaction notes|Notes|n/a|for gas 205 | `check_num`|the check or transaction number|Row|n/a|2 206 | `id`|transaction id|`check_num`|Num|n/a|531 207 | `bank`|the bank name|n/a|`account`|Bank 208 | `account`|transaction account type|n/a|checking|savings 209 | `account_id`|transaction account id|n/a|hash of `account`|bb_checking 210 | `type`|transaction type (either debit or credit)|n/a|CREDIT if amount > 0 else DEBIT|debit 211 | `balance`|account balance|n/a|n/a|$23.00 212 | `class`|transaction class|n/a|n/a|travel 213 | 214 | ### Optional value attributes 215 | 216 | attribute | description | default value | example 217 | ----------|-------------|---------------|-------- 218 | `has_header`|does the csv file have a header row|True 219 | `custom_header`|header row to use (e.g. if not provided in csv)|None|["Account","Date","Amount"] 220 | `is_split`|does the csv file contain split (double entry) transactions|False 221 | `currency`|the currency ISO code|USD|GBP 222 | `delimiter`|the csv field delimiter|,|; 223 | `date_fmt`|custom QIF date output format|%m/%d/%y|%m/%d/%Y 224 | `dayfirst`|interpret the first value in ambiguous dates (e.g. 01/05/09) as the day (ignored if `parse_fmt` is present)|False|True 225 | `parse_fmt`|transaction date parsing format||%m/%d/%Y 226 | `first_row`|the first row to process (zero based)|0|2 227 | `last_row`|the last row to process (zero based, negative values count from the end)|inf|-2 228 | `first_col`|the first column to process (zero based)|0|2 229 | `filter`|keep transactions for which function returns true||lambda tr: float(tr['amount']) > 10 230 | 231 | ## Scripts 232 | 233 | ### Running tests 234 | 235 | tox 236 | 237 | ## Contributing 238 | 239 | Please mimic the coding style/conventions used in this repo. When adding new classes or functions, please add the appropriate doc blocks with examples. 240 | 241 | Ready to contribute? Here's how: 242 | 243 | 1. Fork and clone. 244 | 245 | ```bash 246 | git clone https://github.com/reubano/csv2ofx 247 | cd csv2ofx 248 | ``` 249 | 250 | 2. Run tox. 251 | 252 | Either install [tox](https://tox.wiki) or install [pipx](https://pipx.pypa.io) and use it to `pipx run tox`: 253 | 254 | ```bash 255 | tox 256 | ``` 257 | 258 | Tox will run the tests and other checks (linter) in different Python environments. It will create a Python environment in `.tox/py` and install csv2ofx there. 259 | 260 | Feel free to activate that environment or create a separate one. 261 | 262 | 3. Create a branch for local development 263 | 264 | ```bash 265 | git checkout -b name-of-your-bugfix-or-feature 266 | ``` 267 | 268 | 4. Make your changes, run tests (see above), and submit a pull request through the GitHub website. 269 | 270 | ### Adding Mappings 271 | 272 | How to contribute a mapping: 273 | 274 | 1. Add the mapping in `csv2ofx/mappings/` 275 | 2. Add a simple example CSV file in `data/test/`. 276 | 3. Add the OFX or QIF file that results from the mapping and example CSV file in `data/converted/`. 277 | 4. Add a `csv2ofx` call for your mapping to the tests in `tests/test_cli.py`, in `samples`. When adding an OFX (not QIF) converted file, pay attention to the `-e` (end date) and `-D` (server date) arguments in the test. Otherwise, tests may pass locally but fail on the build server. 278 | 5. Ensure the test succeeds (see above). 279 | 280 | ## License 281 | 282 | csv2ofx is distributed under the [MIT License](http://opensource.org/licenses/MIT), the same as [meza](https://github.com/reubano/meza). 283 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | is_GitHub_Linux = bool( 5 | os.environ.get("GITHUB_ACTIONS") and platform.system() == "Linux" 6 | ) 7 | 8 | collect_ignore = [ 9 | "csv2ofx/mappings/ubs-ch-fr.py", 10 | ] * is_GitHub_Linux 11 | -------------------------------------------------------------------------------- /csv2ofx/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4:ts=4:expandtab 3 | 4 | """ 5 | csv2ofx 6 | ~~~~~~~ 7 | 8 | Converts a csv file to ofx and qif 9 | 10 | Examples: 11 | literal blocks:: 12 | 13 | python example_google.py 14 | 15 | Attributes: 16 | ENCODING (str): Default file encoding. 17 | """ 18 | 19 | import hashlib 20 | from datetime import datetime as dt 21 | from decimal import Decimal 22 | from functools import partial 23 | 24 | from dateutil.parser import parse 25 | from meza.process import group, merge 26 | 27 | from . import utils 28 | 29 | 30 | # pylint: disable=invalid-name 31 | def md5(content): 32 | return hashlib.md5(content.encode("utf-8")).hexdigest() 33 | 34 | 35 | class BalanceError(Exception): 36 | """Raised if no ending balance when MS Money compatible output requested""" 37 | 38 | pass 39 | 40 | 41 | class Content: # pylint: disable=too-many-instance-attributes 42 | """A transaction holding object""" 43 | 44 | def __init__(self, mapping=None, **kwargs): 45 | """Base content constructor 46 | Args: 47 | mapping (dict): bank mapper (see csv2ofx.mappings) 48 | kwargs (dict): Keyword arguments 49 | 50 | Kwargs: 51 | start (date): Date from which to begin including transactions. 52 | end (date): Date from which to exclude transactions. 53 | date_fmt (str): Transaction date format (defaults to '%m/%d/%y'). 54 | dayfirst (bool): Interpret the first value in ambiguous dates (e.g. 01/05/09) 55 | as the day (ignored if `parse_fmt` is present). 56 | filter (func): Keep transactions for which function returns true 57 | 58 | Examples: 59 | >>> from csv2ofx.mappings.mint import mapping 60 | >>> Content(mapping) #doctest: +ELLIPSIS 61 | 62 | """ 63 | mapping = mapping or {} 64 | 65 | # pylint doesn't like dynamically set attributes... 66 | self.amount = 0 67 | self.account = "N/A" 68 | self.parse_fmt = kwargs.get("parse_fmt") 69 | self.dayfirst = kwargs.get("dayfirst") 70 | self.filter = kwargs.get("filter") or bool 71 | self.ms_money = kwargs.get("ms_money") 72 | self.split_account = None 73 | self.inv_split_account = None 74 | self.id = None 75 | 76 | [self.__setattr__(k, v) for k, v in mapping.items()] 77 | 78 | if not hasattr(self, "is_split"): 79 | self.is_split = False 80 | 81 | if not hasattr(self, "has_header"): 82 | self.has_header = True 83 | 84 | if not callable(self.account): 85 | account = self.account 86 | self.account = lambda _: account 87 | 88 | self.start = kwargs.get("start") or dt(1970, 1, 1) 89 | self.end = kwargs.get("end") or dt.now() 90 | 91 | def parse_date(self, trxn): 92 | if self.parse_fmt: 93 | parsed = dt.strptime(self.get("date", trxn), self.parse_fmt) 94 | else: 95 | parsed = parse(self.get("date", trxn), dayfirst=self.dayfirst) 96 | 97 | return parsed 98 | 99 | def get(self, name, trxn=None, default=None): 100 | """Gets an attribute which could be either a normal attribute, 101 | a mapping function, or a mapping attribute 102 | 103 | Args: 104 | name (str): The attribute. 105 | trxn (dict): The transaction. Require if `name` is a mapping 106 | function (default: None). 107 | 108 | default (str): Value to use if `name` isn't found (default: None). 109 | 110 | Returns: 111 | (mixed): Either the value of the attribute function applied to the 112 | transaction, or the value of the attribute. 113 | 114 | Examples: 115 | >>> import datetime 116 | >>> from datetime import datetime as dt 117 | >>> from csv2ofx.mappings.mint import mapping 118 | >>> 119 | >>> trxn = {'Transaction Type': 'DEBIT', 'Amount': 1000.00} 120 | >>> start = dt(2015, 1, 1) 121 | >>> Content(mapping, start=start).get('start') # normal attribute 122 | datetime.datetime(2015, 1, 1, 0, 0) 123 | >>> Content(mapping).get('amount', trxn) # mapping function 124 | 1000.0 125 | >>> Content(mapping).get('has_header') # mapping attribute 126 | True 127 | """ 128 | try: 129 | attr = getattr(self, name) 130 | except AttributeError: 131 | attr = None 132 | value = None 133 | else: 134 | value = None 135 | 136 | try: 137 | value = value or attr(trxn) if attr else default 138 | except TypeError: 139 | value = attr 140 | except KeyError: 141 | value = default 142 | 143 | return value 144 | 145 | def include(self, trxn): 146 | """Determines whether a transaction should be included. 147 | 148 | Included if within the specified date range and any custom filter. 149 | 150 | Args: 151 | trxn (dict): The transaction. 152 | 153 | Returns: 154 | (bool): Whether or not to skip the transaction. 155 | 156 | Examples: 157 | >>> from csv2ofx.mappings.mint import mapping 158 | >>> from datetime import datetime as dt 159 | >>> 160 | >>> trxn = {'Date': '06/12/10', 'Amount': 1000.00} 161 | >>> Content(mapping, start=dt(2010, 1, 1)).include(trxn) 162 | True 163 | >>> Content(mapping, start=dt(2013, 1, 1)).include(trxn) 164 | False 165 | """ 166 | return all( 167 | filter_(trxn) 168 | for filter_ in ( 169 | self.filter, 170 | self.in_range, 171 | ) 172 | ) 173 | 174 | def in_range(self, trxn): 175 | return self.start <= self.parse_date(trxn) <= self.end 176 | 177 | def convert_amount(self, trxn): 178 | """Converts a string amount into a number 179 | 180 | Args: 181 | trxn (dict): The transaction. 182 | 183 | Returns: 184 | (decimal): The converted amount. 185 | 186 | Examples: 187 | >>> from decimal import Decimal 188 | >>> from datetime import datetime as dt 189 | >>> from csv2ofx.mappings.mint import mapping 190 | >>> 191 | >>> trxn = {'Date': '06/12/10', 'Amount': '$1,000'} 192 | >>> Content(mapping, start=dt(2010, 1, 1)).convert_amount(trxn) 193 | Decimal('1000.00') 194 | >>> trxn = {'Date': '06/12/10', 'Amount': '1.000,00€'} 195 | >>> Content(mapping, start=dt(2010, 1, 1)).convert_amount(trxn) 196 | Decimal('1000.00') 197 | """ 198 | return utils.convert_amount(self.get("amount", trxn)) 199 | 200 | def transaction_data(self, trxn): # pylint: disable=too-many-locals 201 | """gets transaction data 202 | 203 | Args: 204 | trxn (dict): the transaction 205 | 206 | Returns: 207 | (dict): the QIF content 208 | 209 | Examples: 210 | >>> import datetime 211 | >>> from decimal import Decimal 212 | >>> from csv2ofx.mappings.mint import mapping 213 | >>> trxn = { 214 | ... 'Transaction Type': 'DEBIT', 'Amount': 1000.00, 215 | ... 'Date': '06/12/10', 'Description': 'payee', 216 | ... 'Original Description': 'description', 'Notes': 'notes', 217 | ... 'Category': 'Checking', 'Account Name': 'account'} 218 | >>> Content(mapping).transaction_data(trxn) == { 219 | ... 'account_id': 'e268443e43d93dab7ebef303bbe9642f', 220 | ... 'bank_id': 'e268443e43d93dab7ebef303bbe9642f', 221 | ... 'account': 'account', 222 | ... 'split_account_id': '195917574edc9b6bbeb5be9785b6a479', 223 | ... 'shares': Decimal('0'), 'payee': 'payee', 'currency': 'USD', 224 | ... 'bank': 'account', 'class': None, 'is_investment': False, 225 | ... 'date': datetime.datetime(2010, 6, 12, 0, 0), 226 | ... 'price': Decimal('0'), 'symbol': '', 'action': '', 227 | ... 'check_num': None, 'id': 'ee86450a47899254e2faa82dca3c2cf2', 228 | ... 'split_account': 'Checking', 'type': 'DEBIT', 229 | ... 'category': '', 'amount': Decimal('-1000.00'), 230 | ... 'memo': 'description notes', 'inv_split_account': None, 231 | ... 'x_action': '', 'balance': None} 232 | True 233 | """ 234 | account = self.get("account", trxn) 235 | split_account = self.get("split_account", trxn) 236 | bank = self.get("bank", trxn, account) 237 | raw_amount = str(self.get("amount", trxn)) 238 | amount = self.convert_amount(trxn) 239 | _type = self.get("type", trxn, "").upper() 240 | 241 | if _type not in {"DEBIT", "CREDIT"}: 242 | _type = "CREDIT" if amount > 0 else "DEBIT" 243 | 244 | date = self.get("date", trxn) 245 | payee = self.get("payee", trxn) 246 | desc = self.get("desc", trxn) 247 | notes = self.get("notes", trxn) 248 | memo = f"{desc} {notes}" if desc and notes else desc or notes 249 | check_num = self.get("check_num", trxn) 250 | details = "".join(filter(None, [date, raw_amount, payee, memo])) 251 | category = self.get("category", trxn, "") 252 | shares = Decimal(self.get("shares", trxn, 0)) 253 | symbol = self.get("symbol", trxn, "") 254 | price = Decimal(self.get("price", trxn, 0)) 255 | invest = shares or (symbol and symbol != "N/A") or "invest" in category 256 | balance = self.get("balance", trxn) 257 | if balance is not None: 258 | balance = utils.convert_amount(balance) 259 | 260 | if invest: 261 | amount = abs(amount) 262 | shares = shares or (amount / price) if price else shares 263 | amount = amount or shares * price 264 | price = price or (amount / shares) if shares else price 265 | action = utils.get_action(category) 266 | x_action = utils.get_action(category, True) 267 | else: 268 | amount = -1 * abs(amount) if _type == "DEBIT" else abs(amount) 269 | action = "" 270 | x_action = "" 271 | 272 | return { 273 | "date": self.parse_date(trxn), 274 | "currency": self.get("currency", trxn, "USD"), 275 | "shares": shares, 276 | "symbol": symbol, 277 | "price": price, 278 | "action": action, 279 | "x_action": x_action, 280 | "category": category, 281 | "is_investment": invest, 282 | "bank": bank, 283 | "bank_id": self.get("bank_id", trxn, md5(bank)), 284 | "account": account, 285 | "account_id": self.get("account_id", trxn, md5(account)), 286 | "split_account": split_account, 287 | "inv_split_account": self.get("inv_split_account", trxn), 288 | "split_account_id": md5(split_account) if split_account else None, 289 | "amount": amount, 290 | "payee": payee, 291 | "memo": memo, 292 | "class": self.get("class", trxn), 293 | "id": self.get("id", trxn, check_num) or md5(details), 294 | "check_num": check_num, 295 | "type": _type, 296 | "balance": balance, 297 | } 298 | 299 | def gen_trxns(self, groups, collapse=False): 300 | """Generate transactions""" 301 | for grp, transactions in groups: 302 | if self.is_split and collapse: 303 | # group transactions by `collapse` field and sum the amounts 304 | byaccount = group(transactions, collapse) 305 | 306 | def oprtn(values): 307 | return sum(map(utils.convert_amount, values)) 308 | 309 | merger = partial(merge, pred=self.amount, op=oprtn) 310 | trxns = [merger(dicts) for _, dicts in byaccount] 311 | else: 312 | trxns = transactions 313 | 314 | yield (grp, trxns) 315 | 316 | def clean_trxns(self, groups): 317 | """Clean transactions""" 318 | for grp, trxns in groups: 319 | _args = [trxns, self.convert_amount] 320 | 321 | # if it's split, transaction inclusion is all or none 322 | if self.is_split and not self.include(trxns[0]): 323 | continue 324 | elif self.is_split and not utils.verify_splits(*_args): 325 | raise Exception("Splits do not sum to zero.") 326 | elif not self.is_split: 327 | filtered_trxns = filter(self.include, trxns) 328 | else: 329 | filtered_trxns = trxns 330 | 331 | if self.is_split: 332 | main_pos = utils.get_max_split(*_args)[0] 333 | else: 334 | main_pos = 0 335 | 336 | # pylint: disable=cell-var-from-loop 337 | def keyfunc(enum): 338 | return enum[0] != main_pos 339 | 340 | sorted_trxns = sorted(enumerate(filtered_trxns), key=keyfunc) 341 | yield (grp, main_pos, sorted_trxns) 342 | -------------------------------------------------------------------------------- /csv2ofx/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | __name__ == "__main__" and main.run() 4 | -------------------------------------------------------------------------------- /csv2ofx/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4:ts=4:expandtab 3 | 4 | """ 5 | csv2ofx.main 6 | ~~~~~~~~~~~~ 7 | 8 | Provides the primary ofx and qif conversion functions 9 | 10 | Examples: 11 | literal blocks:: 12 | 13 | python example_google.py 14 | 15 | Attributes: 16 | ENCODING (str): Default file encoding. 17 | """ 18 | 19 | import itertools as it 20 | import os.path 21 | import pathlib 22 | import sys 23 | import time 24 | import traceback 25 | from argparse import ArgumentParser, RawTextHelpFormatter 26 | from datetime import datetime as dt 27 | from importlib import import_module, util 28 | from math import inf 29 | from operator import itemgetter 30 | from pkgutil import iter_modules 31 | from pprint import pprint 32 | 33 | try: 34 | FileNotFoundError 35 | except NameError: 36 | FileNotFoundError = IOError 37 | 38 | import builtins 39 | 40 | from dateutil.parser import parse 41 | from meza.io import IterStringIO, read_csv, write 42 | 43 | from . import BalanceError, utils 44 | from .ofx import OFX 45 | from .qif import QIF 46 | 47 | parser = ArgumentParser( # pylint: disable=invalid-name 48 | description="description: csv2ofx converts a csv file to ofx and qif", 49 | prog="csv2ofx", 50 | usage="%(prog)s [options] ", 51 | formatter_class=RawTextHelpFormatter, 52 | ) 53 | 54 | TYPES = ["CHECKING", "SAVINGS", "MONEYMRKT", "CREDITLINE", "Bank", "Cash"] 55 | MAPPINGS = import_module("csv2ofx.mappings") 56 | MODULES = tuple(itemgetter(1)(m) for m in iter_modules(MAPPINGS.__path__)) 57 | 58 | 59 | def load_package_module(name): 60 | return import_module(f"csv2ofx.mappings.{name}") 61 | 62 | 63 | def load_custom_module(filepath: str): 64 | """ 65 | >>> mod = load_custom_module("csv2ofx/mappings/amazon.py") 66 | >>> mod.__name__ 67 | 'amazon' 68 | """ 69 | path = pathlib.Path(filepath) 70 | spec = util.spec_from_file_location(path.stem, path) 71 | module = util.module_from_spec(spec) 72 | spec.loader.exec_module(module) 73 | return module 74 | 75 | 76 | parser.add_argument( 77 | dest="source", nargs="?", help="the source csv file (default: stdin)" 78 | ) 79 | parser.add_argument(dest="dest", nargs="?", help="the output file (default: stdout)") 80 | parser.add_argument( 81 | "-a", 82 | "--account", 83 | metavar="TYPE", 84 | dest="account_type", 85 | choices=TYPES, 86 | help="default account type 'CHECKING' for OFX and 'Bank' for QIF.", 87 | ) 88 | parser.add_argument( 89 | "-e", 90 | "--end", 91 | metavar="DATE", 92 | help="end date (default: today)", 93 | default=str(dt.now()), 94 | ) 95 | parser.add_argument( 96 | "-B", 97 | "--ending-balance", 98 | metavar="BALANCE", 99 | type=float, 100 | help="ending balance (default: None)", 101 | ) 102 | parser.add_argument( 103 | "-l", "--language", help="the language (default: ENG)", default="ENG" 104 | ) 105 | parser.add_argument("-s", "--start", metavar="DATE", help="the start date") 106 | parser.add_argument( 107 | "-y", 108 | "--dayfirst", 109 | help="interpret the first value in ambiguous dates (e.g. 01/05/09) as the day", 110 | action="store_true", 111 | default=False, 112 | ) 113 | parser.add_argument( 114 | "-m", 115 | "--mapping", 116 | metavar="MAPPING_NAME", 117 | help="the account mapping (default: default)", 118 | default="default", 119 | choices=MODULES, 120 | ) 121 | parser.add_argument( 122 | "-x", 123 | "--custom", 124 | metavar="FILE_PATH", 125 | help="path to a custom mapping file", 126 | type=load_custom_module, 127 | ) 128 | parser.add_argument( 129 | "-c", 130 | "--collapse", 131 | metavar="FIELD_NAME", 132 | help=( 133 | "field used to combine transactions within a split for double entry statements" 134 | ), 135 | ) 136 | parser.add_argument( 137 | "-C", 138 | "--chunksize", 139 | metavar="ROWS", 140 | type=int, 141 | default=2**14, 142 | help="number of rows to process at a time (default: 2 ** 14)", 143 | ) 144 | parser.add_argument( 145 | "-r", 146 | "--first-row", 147 | metavar="ROWS", 148 | type=int, 149 | default=0, 150 | help="the first row to process (zero based)", 151 | ) 152 | parser.add_argument( 153 | "-R", 154 | "--last-row", 155 | metavar="ROWS", 156 | type=int, 157 | default=inf, 158 | help="the last row to process (zero based, negative values count from the end)", 159 | ) 160 | parser.add_argument( 161 | "-O", 162 | "--first-col", 163 | metavar="COLS", 164 | type=int, 165 | default=0, 166 | help="the first column to process (zero based)", 167 | ) 168 | parser.add_argument( 169 | "-L", 170 | "--list-mappings", 171 | help="list the available mappings", 172 | action="store_true", 173 | default=False, 174 | ) 175 | parser.add_argument( 176 | "-V", "--version", help="show version and exit", action="store_true", default=False 177 | ) 178 | parser.add_argument( 179 | "-q", 180 | "--qif", 181 | help="enables 'QIF' output instead of 'OFX'", 182 | action="store_true", 183 | default=False, 184 | ) 185 | parser.add_argument( 186 | "-M", 187 | "--ms-money", 188 | help="enables MS Money compatible 'OFX' output", 189 | action="store_true", 190 | default=False, 191 | ) 192 | parser.add_argument( 193 | "-o", 194 | "--overwrite", 195 | action="store_true", 196 | default=False, 197 | help="overwrite destination file if it exists", 198 | ) 199 | parser.add_argument( 200 | "-D", 201 | "--server-date", 202 | metavar="DATE", 203 | help="OFX server date (default: source file mtime)", 204 | ) 205 | parser.add_argument( 206 | "-E", "--encoding", default="utf-8", help="File encoding (default: utf-8)" 207 | ) 208 | parser.add_argument( 209 | "-d", 210 | "--debug", 211 | action="store_true", 212 | default=False, 213 | help="display the options and arguments passed to the parser", 214 | ) 215 | parser.add_argument( 216 | "-v", "--verbose", help="verbose output", action="store_true", default=False 217 | ) 218 | 219 | 220 | def _time_from_file(path): 221 | return os.path.getmtime(path) 222 | 223 | 224 | def run(args=None): # noqa: C901 225 | """Parses the CLI options and runs the main program""" 226 | args = parser.parse_args(args) 227 | if args.debug: 228 | pprint(dict(args._get_kwargs())) # pylint: disable=W0212 229 | sys.exit(0) 230 | 231 | if args.version: 232 | from . import __version__ as version 233 | 234 | print(f"v{version}") 235 | sys.exit(0) 236 | 237 | if args.list_mappings: 238 | print(", ".join(MODULES)) 239 | sys.exit(0) 240 | 241 | mapping = (args.custom or load_package_module(args.mapping)).mapping 242 | 243 | okwargs = { 244 | "def_type": args.account_type or "Bank" if args.qif else "CHECKING", 245 | "start": parse(args.start, dayfirst=args.dayfirst) if args.start else None, 246 | "end": parse(args.end, dayfirst=args.dayfirst) if args.end else None, 247 | "ms_money": args.ms_money, 248 | } 249 | 250 | cont = QIF(mapping, **okwargs) if args.qif else OFX(mapping, **okwargs) 251 | source = ( 252 | builtins.open(args.source, encoding=args.encoding) if args.source else sys.stdin 253 | ) 254 | 255 | ckwargs = { 256 | "has_header": cont.has_header, 257 | "custom_header": getattr(cont, "custom_header", None), 258 | "delimiter": mapping.get("delimiter", ","), 259 | "first_row": mapping.get("first_row", args.first_row), 260 | "last_row": mapping.get("last_row", args.last_row), 261 | "first_col": mapping.get("first_col", args.first_col), 262 | } 263 | 264 | try: 265 | records = read_csv(source, **ckwargs) 266 | groups = cont.gen_groups(records, args.chunksize) 267 | trxns = cont.gen_trxns(groups, args.collapse) 268 | cleaned_trxns = cont.clean_trxns(trxns) 269 | data = utils.gen_data(cleaned_trxns) 270 | body = cont.gen_body(data) 271 | 272 | if args.server_date: 273 | server_date = parse(args.server_date, dayfirst=args.dayfirst) 274 | else: 275 | try: 276 | mtime = _time_from_file(source.name) 277 | except (AttributeError, FileNotFoundError): 278 | mtime = time.time() 279 | 280 | server_date = dt.fromtimestamp(mtime) 281 | 282 | header = cont.header(date=server_date, language=args.language) 283 | footer = cont.footer(date=server_date, balance=args.ending_balance) 284 | filtered = filter(None, [header, body, footer]) 285 | content = it.chain.from_iterable(filtered) 286 | kwargs = { 287 | "overwrite": args.overwrite, 288 | "chunksize": args.chunksize, 289 | "encoding": args.encoding, 290 | } 291 | except Exception as err: # pylint: disable=broad-except 292 | source.close() if args.source else None 293 | sys.exit(err) 294 | 295 | dest = ( 296 | builtins.open(args.dest, "w", encoding=args.encoding) 297 | if args.dest 298 | else sys.stdout 299 | ) 300 | 301 | try: 302 | res = write(dest, IterStringIO(content), **kwargs) 303 | except KeyError as err: 304 | msg = f"Field {err} is missing from file. Check `mapping` option." 305 | except TypeError as err: 306 | msg = f"No data to write. {str(err)}. " 307 | 308 | if args.collapse: 309 | msg += "Check `start` and `end` options." 310 | else: 311 | msg += "Try again with `-c` option." 312 | except ValueError as err: 313 | # csv2ofx called with no arguments or broken mapping 314 | msg = f"Possible mapping problem: {str(err)}." 315 | parser.print_help() 316 | except BalanceError as err: 317 | msg = f"{err}. Try again with `--ending-balance` option." 318 | except Exception: # pylint: disable=broad-except 319 | msg = 1 320 | traceback.print_exc() 321 | else: 322 | msg = 0 if res else "No data to write. Check `start` and `end` options." 323 | finally: 324 | source.close() if args.source else None 325 | dest.close() if args.dest else None 326 | sys.exit(msg) 327 | 328 | 329 | if __name__ == "__main__": 330 | run() 331 | -------------------------------------------------------------------------------- /csv2ofx/mappings/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | csv2ofx.mappings 3 | ~~~~~~~~~~~~~~~~ 4 | 5 | Bank Mappers 6 | """ 7 | -------------------------------------------------------------------------------- /csv2ofx/mappings/abnamro.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": False, 5 | "delimiter": "\t", 6 | "bank": "ABN Amro", 7 | "currency": itemgetter("field_1"), 8 | "account": itemgetter("field_0"), 9 | "date": itemgetter("field_2"), 10 | "amount": itemgetter("field_6"), 11 | "payee": itemgetter("field_7"), 12 | } 13 | -------------------------------------------------------------------------------- /csv2ofx/mappings/amazon.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import transactions from Amazon Order History 3 | as exported by Amazon Order History Reporter 4 | (https://chrome.google.com/webstore/detail/amazon-order-history-repo/mgkilgclilajckgnedgjgnfdokkgnibi). 5 | 6 | Honors some environment variables: 7 | 8 | - ``AMAZON_INCLUDE_CARDS``: comma-separated last-four digits 9 | of cards or payment methods to include in the output. If not 10 | supplied, defaults to all cards. 11 | - ``AMAZON_EXCLUDE_CARDS``: comma-separated last-four digits 12 | of cards or payment methods to exclude in the output (supersedes 13 | include). 14 | - ``AMAZON_PURCHASES_ACCOUNT``: The OFX "account id" to use. 15 | Financial tools will use this account ID to associated with an 16 | account. If unspecified, defaults to "100000001". 17 | """ 18 | 19 | import functools 20 | import os 21 | from operator import itemgetter 22 | 23 | All = [''] 24 | """ 25 | A special value matching all cards. 26 | """ 27 | 28 | 29 | @functools.lru_cache 30 | def exclude_cards(): 31 | setting = os.environ.get('AMAZON_EXCLUDE_CARDS', None) 32 | return setting.split(',') if setting else [] 33 | 34 | 35 | @functools.lru_cache 36 | def include_cards(): 37 | setting = os.environ.get('AMAZON_INCLUDE_CARDS', None) 38 | return setting.split(',') if setting else All 39 | 40 | 41 | def filter_payment(row): 42 | include = any(card in row['payments'] for card in include_cards()) 43 | exclude = any(card in row['payments'] for card in exclude_cards()) 44 | return include and not exclude 45 | 46 | 47 | def filter_pending(row): 48 | return row['date'] != 'pending' 49 | 50 | 51 | def filter(row): 52 | return filter_pending(row) and filter_payment(row) 53 | 54 | 55 | mapping = { 56 | 'has_header': True, 57 | 'delimiter': ',', 58 | 'bank': 'Amazon Purchases', 59 | 'account_id': os.environ.get('AMAZON_PURCHASES_ACCOUNT', '100000001'), 60 | 'date': itemgetter('date'), 61 | 'amount': itemgetter('total'), 62 | 'payee': 'Amazon', 63 | 'desc': itemgetter('items'), 64 | 'id': itemgetter('order id'), 65 | 'type': 'DEBIT', 66 | 'last_row': -1, 67 | 'filter': filter, 68 | } 69 | -------------------------------------------------------------------------------- /csv2ofx/mappings/boursorama.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "is_split": False, 6 | "delimiter": ";", 7 | "bank": "Boursorama", 8 | "account": itemgetter("accountNum"), 9 | "account_id": itemgetter("accountNum"), 10 | "date": itemgetter("dateOp"), 11 | "type": "checking", 12 | "amount": lambda r: r["amount"].replace(" ", "").replace(",", "."), 13 | "currency": "EUR", 14 | "desc": itemgetter("label"), 15 | "date_fmt": "%Y%Y-%m%m-%d%d", 16 | "balance": itemgetter("accountbalance"), 17 | } 18 | -------------------------------------------------------------------------------- /csv2ofx/mappings/capitalone.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "is_split": False, 6 | "bank": "CapitalOne", 7 | "currency": "USD", 8 | "delimiter": ",", 9 | "account": itemgetter("Card No."), 10 | "date": itemgetter("Posted Date"), 11 | "type": lambda tr: "DEBIT" if tr.get("Debit") else "CREDIT", 12 | "amount": lambda tr: tr.get("Debit") or tr.get("Credit"), 13 | "desc": itemgetter("Description"), 14 | "payee": itemgetter("Description"), 15 | } 16 | -------------------------------------------------------------------------------- /csv2ofx/mappings/creditunion.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "is_split": False, 6 | "bank": "Credit Union", 7 | "currency": "USD", 8 | "account": "Credit Union", 9 | "date": itemgetter("Date"), 10 | "amount": itemgetter("Amount"), 11 | "payee": itemgetter("Description"), 12 | "notes": itemgetter("Comments"), 13 | "check_num": itemgetter("Check Number"), 14 | } 15 | -------------------------------------------------------------------------------- /csv2ofx/mappings/custom.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "is_split": False, 6 | "bank": "Bank Name", 7 | "currency": "USD", 8 | "delimiter": ",", 9 | "account": itemgetter("Field"), 10 | "account_id": itemgetter("Field"), 11 | "date": itemgetter("Field"), 12 | "type": itemgetter("Field"), 13 | "amount": itemgetter("Field"), 14 | "balance": itemgetter("Field"), 15 | "desc": itemgetter("Field"), 16 | "payee": itemgetter("Field"), 17 | "notes": itemgetter("Field"), 18 | "class": itemgetter("Field"), 19 | "id": itemgetter("Field"), 20 | "check_num": itemgetter("Field"), 21 | } 22 | -------------------------------------------------------------------------------- /csv2ofx/mappings/default.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "is_split": False, 6 | "bank": "Bank", 7 | "currency": "USD", 8 | "delimiter": ",", 9 | "account": itemgetter("Account"), 10 | "date": itemgetter("Date"), 11 | "amount": itemgetter("Amount"), 12 | "desc": itemgetter("Reference"), 13 | "payee": itemgetter("Description"), 14 | "notes": itemgetter("Notes"), 15 | "check_num": itemgetter("Num"), 16 | "id": itemgetter("Row"), 17 | } 18 | -------------------------------------------------------------------------------- /csv2ofx/mappings/eqbank.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "bank": "EQ Bank", 6 | "currency": "CAD", 7 | "delimiter": ",", 8 | "account": lambda tr: "EQ", 9 | "date": itemgetter("Date"), 10 | "desc": itemgetter("Description"), 11 | "type": lambda tr: "DEBIT" if tr.get("Out") else "CREDIT", 12 | "amount": lambda tr: tr.get("In") or tr.get("Out"), 13 | "balance": itemgetter("Balance"), 14 | } 15 | -------------------------------------------------------------------------------- /csv2ofx/mappings/exim.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "bank": "Exim", 5 | "has_header": True, 6 | "currency": "USD", 7 | "account": itemgetter("Account"), 8 | "date": itemgetter("Date"), 9 | "amount": itemgetter("Amount"), 10 | "payee": itemgetter("Narration"), 11 | "notes": itemgetter("Notes"), 12 | "id": itemgetter("Reference Number"), 13 | } 14 | -------------------------------------------------------------------------------- /csv2ofx/mappings/gls.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | 4 | def date_func(trxn): 5 | tag = trxn["Buchungstag"] 6 | return f"{tag[3:5]}/{tag[:2]}/{tag[-4:]}" 7 | 8 | 9 | mapping = { 10 | "has_header": True, 11 | "currency": "EUR", 12 | "delimiter": ";", 13 | "bank": "GLS Bank", 14 | "account": itemgetter("Kontonummer"), 15 | # Chop up the dotted German date format and put it in ridiculous M/D/Y order 16 | "date": date_func, 17 | # locale.atof does not actually know how to deal with German separators. 18 | # So we do it the crude way 19 | "amount": lambda r: r["Betrag"].replace(".", "").replace(",", "."), 20 | "desc": itemgetter("Buchungstext"), 21 | "notes": lambda r: " ".join(r[f"VWZ-{n}"] for n in range(1, 15)), 22 | "payee": itemgetter("Auftraggeber/Empfänger"), 23 | } 24 | -------------------------------------------------------------------------------- /csv2ofx/mappings/ingdirect.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.ingdirect 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via ING Direct 8 | (Australian bank) 9 | """ 10 | 11 | from operator import itemgetter 12 | 13 | mapping = { 14 | "is_split": False, 15 | "has_header": True, 16 | "account": itemgetter("Account"), 17 | "date": itemgetter("Date"), 18 | "amount": lambda tr: tr["Credit"] + tr["Debit"], 19 | "desc": itemgetter("Description"), 20 | } 21 | -------------------------------------------------------------------------------- /csv2ofx/mappings/ingesp.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.ingdirect 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via ING Direct 8 | (Spanish bank) 9 | """ 10 | 11 | import hashlib 12 | import json 13 | from operator import itemgetter 14 | 15 | 16 | def find_type(transaction): 17 | amount = float(transaction.get("amount")) 18 | return "credit" if amount > 0 else "debit" 19 | 20 | 21 | def gen_transaction_id(transaction): 22 | hasher = hashlib.sha256() 23 | stringified = json.dumps(transaction).encode("utf-8") 24 | hasher.update(stringified) 25 | return hasher.hexdigest() 26 | 27 | 28 | def get_payee(transaction): 29 | cadena = transaction.get('desc') 30 | subcadenas = [ 31 | 'Abono por campaña', 32 | 'Devolución Tarjeta', 33 | 'Nomina recibida', 34 | 'Traspaso recibido', 35 | 'Pago en', 36 | 'Recibo', 37 | 'Reintegro efectivo', 38 | 'Transferencia Bizum emitida', 39 | 'Transferencia emitida a', 40 | 'Transferencia recibida de', 41 | ] 42 | for subcadena in subcadenas: 43 | if subcadena in cadena: 44 | payee = cadena.replace(subcadena, '') 45 | break 46 | else: 47 | payee = cadena 48 | return payee 49 | 50 | 51 | def get_transaction_type(transaction): 52 | cadena = transaction.get('desc') 53 | subcadenas = [ 54 | 'Abono por campaña', 55 | 'Devolución Tarjeta', 56 | 'Nomina recibida', 57 | 'Traspaso recibido', 58 | 'Pago en', 59 | 'Recibo', 60 | 'Reintegro efectivo', 61 | 'Transferencia Bizum emitida', 62 | 'Transferencia emitida a', 63 | 'Transferencia recibida de', 64 | ] 65 | for subcadena in subcadenas: 66 | if subcadena in cadena: 67 | notes = subcadena 68 | return notes 69 | 70 | 71 | mapping = { 72 | "has_header": True, 73 | "is_split": False, 74 | "bank": "ING", 75 | "currency": "EUR", 76 | "delimiter": ",", 77 | "account": "ING checking", 78 | "type": find_type, 79 | "date": itemgetter("date"), 80 | "amount": itemgetter("amount"), 81 | "payee": get_payee, 82 | # 'desc': get_transaction_type, 83 | "class": itemgetter("class"), 84 | "id": gen_transaction_id, 85 | } 86 | -------------------------------------------------------------------------------- /csv2ofx/mappings/mdb.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.mdb 5 | ~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via moneydashboard.com 8 | """ 9 | 10 | from operator import itemgetter 11 | 12 | mapping = { 13 | "is_split": False, 14 | "has_header": True, 15 | "currency": "GBP", 16 | "account": itemgetter("Account"), 17 | "date": itemgetter("Date"), 18 | "amount": itemgetter("Amount"), 19 | "desc": itemgetter("OriginalDescription"), 20 | "payee": itemgetter("CurrentDescription"), 21 | "class": itemgetter("Tag"), 22 | } 23 | -------------------------------------------------------------------------------- /csv2ofx/mappings/mint.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.mintapi 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via mint.com 8 | """ 9 | 10 | from operator import itemgetter 11 | 12 | mapping = { 13 | "is_split": False, 14 | "has_header": True, 15 | "split_account": itemgetter("Category"), 16 | "account": itemgetter("Account Name"), 17 | "date": itemgetter("Date"), 18 | "type": itemgetter("Transaction Type"), 19 | "amount": itemgetter("Amount"), 20 | "desc": itemgetter("Original Description"), 21 | "payee": itemgetter("Description"), 22 | "notes": itemgetter("Notes"), 23 | } 24 | -------------------------------------------------------------------------------- /csv2ofx/mappings/mint_extra.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.mintapi 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via mint.com 8 | """ 9 | 10 | from operator import itemgetter 11 | 12 | from csv2ofx.utils import convert_amount 13 | 14 | mapping = { 15 | "is_split": False, 16 | "has_header": True, 17 | "split_account": itemgetter("Category"), 18 | "account": itemgetter("Account Name"), 19 | "date": itemgetter("Date"), 20 | "type": itemgetter("Transaction Type"), 21 | "amount": itemgetter("Amount"), 22 | "desc": itemgetter("Original Description"), 23 | "payee": itemgetter("Description"), 24 | "notes": itemgetter("Notes"), 25 | "first_row": 3, 26 | "last_row": -1, 27 | "filter": lambda trxn: convert_amount(trxn["Amount"]) < 2500, 28 | } 29 | -------------------------------------------------------------------------------- /csv2ofx/mappings/mint_headerless.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.mint_headerless 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via mint.com 8 | """ 9 | 10 | from operator import itemgetter 11 | 12 | mapping = { 13 | "is_split": False, 14 | "has_header": False, 15 | "split_account": itemgetter("column_6"), 16 | "account": itemgetter("column_7"), 17 | "date": itemgetter("column_1"), 18 | "type": itemgetter("column_5"), 19 | "amount": itemgetter("column_4"), 20 | "desc": itemgetter("column_3"), 21 | "payee": itemgetter("column_2"), 22 | } 23 | -------------------------------------------------------------------------------- /csv2ofx/mappings/mintapi.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.mintapi 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via the mintapi python script 8 | """ 9 | 10 | from operator import itemgetter 11 | 12 | mapping = { 13 | "is_split": False, 14 | "has_header": True, 15 | "account": itemgetter("account"), 16 | "category": itemgetter("category"), 17 | "split_account": itemgetter("category"), 18 | "type": lambda tr: "debit" if tr["isDebit"] == "TRUE" else "credit", 19 | "date": itemgetter("odate"), 20 | "amount": itemgetter("amount"), 21 | "desc": itemgetter("omerchant"), 22 | "payee": itemgetter("merchant"), 23 | "notes": itemgetter("note"), 24 | "class": lambda tr: tr["labels"] if tr.get("labels", [])[1:-1] else "", 25 | "bank": itemgetter("fi"), 26 | "currency": "USD", 27 | "id": itemgetter("id"), 28 | "shares": lambda tr: tr["shares"] if tr["shares"] != "" else 0.0, 29 | "symbol": itemgetter("symbol"), 30 | } 31 | -------------------------------------------------------------------------------- /csv2ofx/mappings/msmoneyreport.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "bank": lambda tr: tr["Account"].split(" - ")[0], 6 | "account": lambda tr: tr["Account"].split(" - ")[1:], 7 | "currency": itemgetter("Currency"), 8 | "class": itemgetter("Projects"), 9 | "check_num": itemgetter("Num"), 10 | "type": lambda tr: "debit" if tr.get("Debit") else "credit", 11 | "amount": itemgetter("Amount"), 12 | "notes": itemgetter("Memo"), 13 | "date": itemgetter("Date"), 14 | "desc": itemgetter("Category"), 15 | "payee": itemgetter("Payee"), 16 | } 17 | -------------------------------------------------------------------------------- /csv2ofx/mappings/n26.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | from operator import itemgetter 4 | 5 | 6 | def find_type(transaction): 7 | amount = float(transaction.get("Amount (EUR)")) 8 | return "credit" if amount > 0 else "debit" 9 | 10 | 11 | def gen_transaction_id(transaction): 12 | hasher = hashlib.sha256() 13 | stringified = json.dumps(transaction).encode("utf-8") 14 | hasher.update(stringified) 15 | return hasher.hexdigest() 16 | 17 | 18 | mapping = { 19 | "has_header": True, 20 | "is_split": False, 21 | "bank": "N26", 22 | "currency": "EUR", 23 | "delimiter": ",", 24 | "account": "N26 checking", 25 | "type": find_type, 26 | "date": itemgetter("Booking Date"), 27 | "amount": itemgetter("Amount (EUR)"), 28 | "payee": itemgetter("Payee"), 29 | "notes": itemgetter("Payment reference"), 30 | # 'desc': itemgetter('Payment reference'), 31 | "class": itemgetter("Category"), 32 | "id": gen_transaction_id, 33 | # 'check_num': itemgetter('Field'), 34 | } 35 | -------------------------------------------------------------------------------- /csv2ofx/mappings/outbank.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.ingdirect 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | Provides a mapping for transactions obtained from Outbank, a 7 | banking application that is able to export to CSV. 8 | Mapping build for version 2.19. 9 | """ 10 | 11 | from operator import itemgetter 12 | 13 | mapping = { 14 | "is_split": False, 15 | "has_header": True, 16 | "delimiter": ";", 17 | "account": itemgetter("Account"), 18 | "currency": itemgetter("Currency"), 19 | "payee": itemgetter("Name"), 20 | "date": itemgetter("Date"), 21 | "amount": itemgetter("Amount"), 22 | "desc": itemgetter("Reason"), 23 | } 24 | -------------------------------------------------------------------------------- /csv2ofx/mappings/payoneer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from operator import itemgetter 4 | 5 | 6 | def is_credit(row): 7 | try: 8 | return (row.get("Debit Amount") or None) is None 9 | except ValueError: 10 | return True 11 | 12 | 13 | def get_amount(row): 14 | if is_credit(row): 15 | return row.get("Credit Amount") 16 | 17 | return f'-{row.get("Debit Amount")}' 18 | 19 | 20 | def payoneer_filter(transaction): 21 | try: 22 | float(transaction.get("Credit Amount") or transaction.get("Debit Amount")) 23 | return True 24 | except ValueError: 25 | return False 26 | 27 | 28 | def get_date(row): 29 | date_str = f'{row.get("Transaction Date")} {row.get("Transaction Time")}' 30 | 31 | return datetime.strptime(date_str, "%m/%d/%Y %H:%M:%S").strftime("%Y%m%d%H%M%S") 32 | 33 | 34 | mapping = { 35 | "has_header": True, 36 | "filter": payoneer_filter, 37 | "is_split": False, 38 | "bank": "Payoneer Global Inc", 39 | "bank_id": "123", 40 | "currency": itemgetter("Currency"), 41 | "delimiter": ",", 42 | "account": os.environ.get("PAYONEER_ACCOUNT", "1000001"), 43 | "date": get_date, 44 | "parse_fmt": "%Y%m%d%H%M%S", 45 | "date_fmt": "%Y%m%d%H%M%S", 46 | "amount": get_amount, 47 | "desc": itemgetter("Description"), 48 | "payee": itemgetter("Target"), 49 | "balance": itemgetter("Running Balance"), 50 | "id": itemgetter("Transaction ID"), 51 | "type": lambda tr: "credit" if is_credit(tr) else "debit", 52 | } 53 | -------------------------------------------------------------------------------- /csv2ofx/mappings/pcmastercard.py: -------------------------------------------------------------------------------- 1 | """ 2 | For PC Financial Mastercards 3 | https://www.pcfinancial.ca/en/credit-cards 4 | """ 5 | 6 | from operator import itemgetter 7 | 8 | mapping = { 9 | "has_header": True, 10 | "bank": "PC Financial", 11 | "currency": "CAD", 12 | "account": "Mastercard", 13 | "date": itemgetter("Date"), 14 | "amount": lambda r: float(r["Amount"]) * -1.0, 15 | "payee": itemgetter('"Merchant Name"'), 16 | } 17 | -------------------------------------------------------------------------------- /csv2ofx/mappings/rabobank.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | # example to convert: 4 | # csv2ofx -m rabobank -E ISO-8859-1 CSV_O_20200630_014400.csv CSV_O_20200630_014400.ofx 5 | 6 | 7 | def date_func(trxn): 8 | # Chop up the ISO date and put it in ridiculous M/D/Y order 9 | tag = trxn["Datum"].split("-") 10 | return f"{tag[1]}/{tag[2]}/{tag[0]}" 11 | 12 | 13 | def desc_func(trxn): 14 | end = " ".join(trxn[f"Omschrijving-{n}"] for n in range(1, 4)) 15 | return "{} - {}".format(trxn["Naam tegenpartij"], end) 16 | 17 | 18 | mapping = { 19 | "has_header": True, 20 | "currency": itemgetter("Munt"), 21 | # 'delimiter': ';', 22 | "bank": "Rabobank", 23 | "account": itemgetter("IBAN/BBAN"), 24 | "id": itemgetter("Volgnr"), 25 | "date": date_func, 26 | "amount": lambda r: r["Bedrag"].replace(",", "."), 27 | "desc": desc_func, 28 | "payee": itemgetter("Naam tegenpartij"), 29 | } 30 | -------------------------------------------------------------------------------- /csv2ofx/mappings/schwabchecking.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | 'has_header': True, 5 | 'filter': lambda tr: True if tr.get('Status') == 'Posted' else False, 6 | 'is_split': False, 7 | 'bank': 'Charles Schwab Bank, N.A.', 8 | 'bank_id': '121202211', 9 | 'account_id': '12345', # Change to your actual account number if desired 10 | 'currency': 'USD', 11 | 'account': 'Charles Schwab Checking', 12 | 'date': itemgetter('Date'), 13 | 'check_num': itemgetter('CheckNumber'), 14 | 'payee': itemgetter('Description'), 15 | 'desc': itemgetter('Description'), 16 | 'type': lambda tr: 'debit' if tr.get('Withdrawal') != '' else 'credit', 17 | 'amount': lambda tr: tr.get('Deposit') or tr.get('Withdrawal'), 18 | 'balance': itemgetter('RunningBalance'), 19 | } 20 | -------------------------------------------------------------------------------- /csv2ofx/mappings/split_account.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "has_header": True, 5 | "is_split": False, 6 | "bank": "Bank", 7 | "currency": "USD", 8 | "delimiter": ",", 9 | "split_account": itemgetter("Category"), 10 | "account": itemgetter("Account"), 11 | "date": itemgetter("Date"), 12 | "amount": itemgetter("Amount"), 13 | "desc": itemgetter("Reference"), 14 | "payee": itemgetter("Description"), 15 | "notes": itemgetter("Notes"), 16 | "check_num": itemgetter("Num"), 17 | "id": itemgetter("Row"), 18 | } 19 | -------------------------------------------------------------------------------- /csv2ofx/mappings/starling.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | 4 | def fixdate(ds): 5 | dmy = ds.split("/") 6 | # BUG (!?): don't understand but stolen from ubs-ch-fr.py 7 | return ".".join((dmy[1], dmy[0], dmy[2])) 8 | 9 | 10 | mapping = { 11 | "has_header": True, 12 | "date": lambda tr: fixdate(tr["Date"]), 13 | "amount": itemgetter("Amount (GBP)"), 14 | "desc": itemgetter("Reference"), 15 | "payee": itemgetter("Counter Party"), 16 | } 17 | -------------------------------------------------------------------------------- /csv2ofx/mappings/stripe.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.stripe 5 | ~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Provides a mapping for transactions obtained via Stripe card processing 8 | 9 | Note that Stripe provides a Default set of columns or you can download 10 | All columns. (as well as custom). The Default set does not include card 11 | information, so provides no appropriate value for the PAYEE field for 12 | an anonymous transaction (missing a customer). 13 | It's suggested the All Columns format be used if not all transactions 14 | identify a customer. This mapping sets PAYEE to Customer Name if it 15 | exists, otherwise Card Name (if provided) 16 | """ 17 | 18 | from operator import itemgetter 19 | 20 | mapping = { 21 | "has_header": True, 22 | "account": "Stripe", 23 | "id": itemgetter("id"), 24 | "date": itemgetter("created"), 25 | "amount": itemgetter("amount"), 26 | "currency": itemgetter("currency"), 27 | "payee": lambda tr: tr.get("customer_description") 28 | if len(tr.get("customer_description")) > 0 29 | else tr.get("card_name", ""), 30 | "desc": itemgetter("description"), 31 | } 32 | -------------------------------------------------------------------------------- /csv2ofx/mappings/ubs-ch-fr.py: -------------------------------------------------------------------------------- 1 | # vim: sw=4:ts=4:expandtab 2 | # pylint: disable=invalid-name 3 | """ 4 | csv2ofx.mappings.ubs-ch-fr 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Mapping for UBS Switzerland (French) 8 | 9 | Exports from UBS CH e-banking Web page ("Liste des mouvements") are quite 10 | tricky: 11 | 12 | * Most transactions aren't split, but those for account fees ("Solde prix 13 | prestations") do! Thus, to make it easy, better off to remove the 14 | corresponing split lines -- the script `utilz/csvtrim` can be used to 15 | pre-process the exported CSV file 16 | 17 | * Transaction sub-classes and tags ("Etiquettes") aren't exported, thus one 18 | needs a custom filter looking in columns "Description 2/3". 19 | 20 | * The payee is not explicitly provided: it can be in columns "Description 2/3" 21 | 22 | """ 23 | 24 | # Financial numbers are expressed as "2'045.56" in de/fr/it_CH (utf8 has some 25 | # glitches, so we go for the default one) 26 | import locale 27 | from operator import itemgetter 28 | 29 | locale.setlocale(locale.LC_NUMERIC, 'fr_CH') 30 | 31 | __author__ = 'Marco "sphakka" Poleggi' 32 | 33 | 34 | def fixdate(ds): 35 | dmy = ds.split(".") 36 | # BUG (!?): can't format here ISO-style as it won't accept first 37 | # token as a valid month... 38 | return ".".join((dmy[1], dmy[0], dmy[2])) 39 | 40 | 41 | def map_descr(tr): 42 | descr = tr["Description 2"] or tr["Description 1"] 43 | return descr + (": " + tr["Description 3"] if tr["Description 3"] else "") 44 | 45 | 46 | def map_class(tr): 47 | clss = tr["Description 1"] 48 | return clss + (": " + tr["Description 2"] if tr["Description 2"] else "") 49 | 50 | 51 | def map_payee(tr): 52 | return tr["Description 3"] if tr.get("Débit") else tr["Description 2"] 53 | 54 | 55 | mapping = { 56 | "delimiter": ";", 57 | "bank": "UBS Switzerland", 58 | "has_header": True, 59 | "date_fmt": "%Y-%m-%d", # ISO 60 | "currency": itemgetter("Monn."), 61 | "account": itemgetter("Produit"), 62 | # 'Débit' and 'Crédit' columns are always provided, but only one may have 63 | # a value in a given row 64 | "type": lambda tr: "debit" if tr.get("Débit") != "" else "credit", 65 | "amount": lambda tr: locale.atof(tr["Débit"] or tr["Crédit"]), 66 | # debits show _your_ notes in "Desc 2", whereas credits report the 67 | # _payee_. Thus a better "class" value comes from "Desc 1" + "Desc 2" 68 | "class": map_class, 69 | "notes": itemgetter("Description 2"), 70 | # switch day/month (maybe file a bug: always inverted when ambiguous like 71 | # '01.02.2018') 72 | "date": lambda tr: fixdate(tr["Date de valeur"]), 73 | "desc": map_descr, 74 | "payee": map_payee, 75 | "check_num": itemgetter("N° de transaction"), 76 | "balance": lambda tr: locale.atof(tr["Solde"]), 77 | } 78 | -------------------------------------------------------------------------------- /csv2ofx/mappings/ubs.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "bank": "UBS", 5 | "has_header": True, 6 | "currency": "Ccy.", 7 | "delimiter": ";", 8 | "type": lambda tr: "debit" if tr.get("Debit") else "credit", 9 | "amount": lambda tr: tr.get("Debit", tr["Credit"]), 10 | "notes": lambda tr: " / ".join( 11 | filter(None, [tr["Description 1"], tr["Description 2"], tr["Description 3"]]) 12 | ), 13 | "date": itemgetter("Value date"), 14 | "desc": itemgetter("Description"), 15 | "payee": lambda tr: tr.get("Recipient", tr["Entered by"]), 16 | } 17 | -------------------------------------------------------------------------------- /csv2ofx/mappings/xero.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | mapping = { 4 | "is_split": True, 5 | "has_header": True, 6 | "account": itemgetter("AccountName"), 7 | "date": itemgetter("JournalDate"), 8 | "amount": itemgetter("NetAmount"), 9 | "payee": itemgetter("Description"), 10 | "notes": itemgetter("Product"), 11 | "class": itemgetter("Resource"), 12 | "id": itemgetter("JournalNumber"), 13 | "check_num": itemgetter("Reference"), 14 | } 15 | -------------------------------------------------------------------------------- /csv2ofx/mappings/yodlee.py: -------------------------------------------------------------------------------- 1 | from operator import itemgetter 2 | 3 | 4 | def note_func(tr): 5 | notes = [tr.get("desc1"), tr.get("desc1"), tr.get("desc1")] 6 | return " / ".join(filter(None, notes)) 7 | 8 | 9 | mapping = { 10 | "has_header": True, 11 | "is_split": True, 12 | "bank": lambda tr: tr["Account Name"].split(" - ")[0], 13 | "notes": note_func, 14 | "account": lambda tr: tr["Account Name"].split(" - ")[1:], 15 | "date": itemgetter("Date"), 16 | "type": itemgetter("Transaction Type"), 17 | "amount": itemgetter("Amount"), 18 | "currency": itemgetter("Currency"), 19 | "desc": itemgetter("Original Description"), 20 | "payee": itemgetter("User Description"), 21 | "class": itemgetter("Classification"), 22 | "id": itemgetter("Transaction Id"), 23 | } 24 | -------------------------------------------------------------------------------- /csv2ofx/qif.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4:ts=4:expandtab 3 | # pylint: disable=no-self-use 4 | 5 | """ 6 | csv2ofx.qif 7 | ~~~~~~~~~~~ 8 | 9 | Provides methods for generating qif content 10 | 11 | Examples: 12 | literal blocks:: 13 | 14 | python example_google.py 15 | 16 | Attributes: 17 | ENCODING (str): Default file encoding. 18 | """ 19 | 20 | from meza.fntools import chunk 21 | from meza.process import group 22 | 23 | from . import Content, utils 24 | 25 | DEF_DATE_FMT = "%m/%d/%Y" 26 | 27 | 28 | class QIF(Content): 29 | """A QIF object""" 30 | 31 | def __init__(self, mapping=None, date_fmt=DEF_DATE_FMT, **kwargs): 32 | """QIF constructor 33 | Args: 34 | mapping (dict): bank mapper (see csv2ofx.mappings) 35 | kwargs (dict): Keyword arguments 36 | 37 | Kwargs: 38 | def_type (str): Default account type. 39 | start (date): Date from which to begin including transactions. 40 | end (date): Date from which to exclude transactions. 41 | date_fmt (str): Transaction date output format (defaults to '%m/%d/%y'). 42 | 43 | Examples: 44 | >>> from csv2ofx.mappings.mint import mapping 45 | >>> QIF(mapping) #doctest: +ELLIPSIS 46 | 47 | """ 48 | self.date_fmt = date_fmt 49 | 50 | super().__init__(mapping, date_fmt=date_fmt, **kwargs) 51 | self.def_type = kwargs.get("def_type") 52 | self.prev_account = None 53 | self.prev_group = None 54 | self.account_types = { 55 | "Invst": ("roth", "ira", "401k", "vanguard"), 56 | "Bank": ("checking", "savings", "market", "income"), 57 | "Oth A": ("receivable",), 58 | "Oth L": ("payable",), 59 | "CCard": ("visa", "master", "express", "discover", "platinum"), 60 | "Cash": ("cash", "expenses"), 61 | } 62 | 63 | def header(self, **kwargs): # pylint: disable=unused-argument 64 | """Get the QIF header""" 65 | return None 66 | 67 | def transaction_data(self, tr): 68 | """gets QIF transaction data 69 | 70 | Args: 71 | tr (dict): the transaction 72 | 73 | Returns: 74 | (dict): the QIF transaction data 75 | 76 | Examples: 77 | >>> from datetime import datetime as dt 78 | >>> from decimal import Decimal 79 | >>> from csv2ofx.mappings.mint import mapping 80 | >>> tr = { 81 | ... 'Transaction Type': 'DEBIT', 'Amount': 1000.00, 82 | ... 'Date': '06/12/10', 'Description': 'payee', 83 | ... 'Original Description': 'description', 'Notes': 'notes', 84 | ... 'Category': 'Checking', 'Account Name': 'account'} 85 | >>> QIF(mapping, def_type='Bank').transaction_data(tr) == { 86 | ... 'account_id': 'e268443e43d93dab7ebef303bbe9642f', 87 | ... 'account': 'account', 'currency': 'USD', 88 | ... 'account_type': 'Bank', 'shares': Decimal('0'), 89 | ... 'is_investment': False, 'bank': 'account', 90 | ... 'split_memo': 'description notes', 'split_account_id': None, 91 | ... 'class': None, 'amount': Decimal('-1000.00'), 92 | ... 'memo': 'description notes', 93 | ... 'id': 'ee86450a47899254e2faa82dca3c2cf2', 94 | ... 'split_account': 'Checking', 95 | ... 'split_account_id': '195917574edc9b6bbeb5be9785b6a479', 96 | ... 'action': '', 'payee': 'payee', 97 | ... 'date': dt(2010, 6, 12, 0, 0), 'category': '', 98 | ... 'bank_id': 'e268443e43d93dab7ebef303bbe9642f', 99 | ... 'price': Decimal('0'), 'symbol': '', 'check_num': None, 100 | ... 'inv_split_account': None, 'x_action': '', 'type': 'DEBIT', 101 | ... 'balance': None} 102 | True 103 | """ 104 | data = super().transaction_data(tr) 105 | args = [self.account_types, self.def_type] 106 | memo = data.get("memo") 107 | _class = data.get("class") 108 | 109 | if memo and _class: 110 | split_memo = f"{memo} {_class}" 111 | else: 112 | split_memo = memo or _class 113 | 114 | new_data = { 115 | "account_type": utils.get_account_type(data["account"], *args), 116 | "split_memo": split_memo, 117 | } 118 | 119 | data.update(new_data) 120 | return data 121 | 122 | def account_start(self, **kwargs): 123 | """Gets QIF format account content 124 | 125 | Args: 126 | kwargs (dict): Output from `transaction_data`. 127 | 128 | Kwargs: 129 | account (str): The account name. 130 | account_type (str): The account type. One of ['Bank', 'Oth A', 131 | 'Oth L', 'CCard', 'Cash', 'Invst'] (required). 132 | 133 | Returns: 134 | (str): the QIF content 135 | 136 | Examples: 137 | >>> kwargs = {'account': 'account', 'account_type': 'Bank'} 138 | >>> start = '!AccountNaccountTBank^' 139 | >>> result = QIF().account_start(**kwargs) 140 | >>> start == result.replace('\\n', '').replace('\\t', '') 141 | True 142 | """ 143 | return "!Account\nN{account}\nT{account_type}\n^\n".format(**kwargs) 144 | 145 | def transaction_start(self, account_type=None, **kwargs): 146 | """Gets QIF format transaction start content 147 | 148 | Args: 149 | account_type (str): the transaction account type 150 | kwargs (dict): Output from `transaction_data`. 151 | 152 | Returns: 153 | (str): content the QIF content 154 | 155 | Examples: 156 | >>> result = QIF().transaction_start(account_type='Bank') 157 | >>> '!Type:Bank' == result.replace('\\n', '').replace('\\t', '') 158 | True 159 | """ 160 | return f"!Type:{account_type}\n" 161 | 162 | def transaction(self, **kwargs): 163 | """Gets QIF format transaction content 164 | 165 | Args: 166 | kwargs (dict): Output from `transaction_data`. 167 | 168 | Kwargs: 169 | date (date): the transaction date (required) 170 | amount (number): the transaction amount (required) 171 | payee (number): the transaction amount (required) 172 | date_fmt (str): the transaction date format (defaults to '%m/%d/%Y') 173 | memo (str): the transaction memo 174 | class (str): the transaction classification 175 | check_num (str): a unique transaction identifier 176 | 177 | Returns: 178 | (str): content the QIF content 179 | 180 | Examples: 181 | >>> from datetime import datetime as dt 182 | >>> kwargs = { 183 | ... 'payee': 'payee', 'amount': 100, 'check_num': 1, 184 | ... 'date': dt(2012, 1, 1)} 185 | >>> trxn = 'N1D01/01/2012PpayeeT100.00' 186 | >>> result = QIF().transaction(**kwargs) 187 | >>> trxn == result.replace('\\n', '').replace('\\t', '') 188 | True 189 | 190 | >>> result = QIF().transaction(date_fmt="%Y-%m-%d", **kwargs) 191 | >>> '2012-01-01' in result 192 | True 193 | """ 194 | date_fmt = kwargs.get("date_fmt", self.date_fmt) 195 | kwargs.update({"time_stamp": kwargs["date"].strftime(date_fmt)}) 196 | is_investment = kwargs.get("is_investment") 197 | is_transaction = not is_investment 198 | 199 | if self.is_split: 200 | kwargs.update({"amount": kwargs["amount"] * -1}) 201 | 202 | if is_transaction and kwargs.get("check_num"): 203 | content = "N{check_num}\n".format(**kwargs) 204 | else: 205 | content = "" 206 | 207 | content += "D{time_stamp}\n".format(**kwargs) 208 | 209 | if is_investment: 210 | if kwargs.get("inv_split_account"): 211 | content += "N{x_action}\n".format(**kwargs) 212 | else: 213 | content += "N{action}\n".format(**kwargs) 214 | 215 | content += "Y{symbol}\n".format(**kwargs) 216 | content += "I{price}\n".format(**kwargs) 217 | content += "Q{shares}\n".format(**kwargs) 218 | content += "Cc\n" 219 | else: 220 | content += "P{payee}\n".format(**kwargs) if kwargs.get("payee") else "" 221 | content += "L{class}\n".format(**kwargs) if kwargs.get("class") else "" 222 | 223 | content += "M{memo}\n".format(**kwargs) if kwargs.get("memo") else "" 224 | 225 | if is_investment and kwargs.get("commission"): 226 | content += "O{commission}\n".format(**kwargs) 227 | 228 | content += "T{amount:0.2f}\n".format(**kwargs) 229 | return content 230 | 231 | def split_content(self, **kwargs): 232 | """Gets QIF format split content 233 | 234 | Args: 235 | kwargs (dict): Output from `transaction_data`. 236 | 237 | Kwargs: 238 | split_account (str): Account to use as the transfer recipient. 239 | (useful in cases when the transaction data isn't already split) 240 | 241 | inv_split_account (str): Account to use as the investment transfer 242 | recipient. (useful in cases when the transaction data isn't 243 | already split) 244 | 245 | account (str): A unique account identifier (required if neither 246 | `split_account` nor `inv_split_account` is given). 247 | 248 | split_memo (str): the transaction split memo 249 | 250 | Returns: 251 | (str): the QIF content 252 | 253 | Examples: 254 | >>> kwargs = { 255 | ... 'account': 'account', 'split_memo': 'memo', 'amount': 100} 256 | >>> split = 'SaccountEmemo$100.00' 257 | >>> result = QIF().split_content(**kwargs) 258 | >>> split == result.replace('\\n', '').replace('\\t', '') 259 | True 260 | """ 261 | is_investment = kwargs.get("is_investment") 262 | is_transaction = not is_investment 263 | 264 | if is_investment and kwargs.get("inv_split_account"): 265 | content = "L{inv_split_account}\n".format(**kwargs) 266 | elif is_investment and self.is_split: 267 | content = "L{account}\n".format(**kwargs) 268 | elif is_transaction and kwargs.get("split_account"): 269 | content = "S{split_account}\n".format(**kwargs) 270 | elif is_transaction: 271 | content = "S{account}\n".format(**kwargs) 272 | else: 273 | content = "" 274 | 275 | if content and kwargs.get("split_memo"): 276 | content += "E{split_memo}\n".format(**kwargs) 277 | 278 | content += "${amount:0.2f}\n".format(**kwargs) if content else "" 279 | return content 280 | 281 | def transaction_end(self): 282 | """Gets QIF transaction end 283 | 284 | Returns: 285 | (str): the QIF transaction end 286 | 287 | Examples: 288 | >>> result = QIF().transaction_end() 289 | >>> result.replace('\\n', '').replace('\\t', '') == '^' 290 | True 291 | """ 292 | return "^\n" 293 | 294 | def footer(self, **kwargs): # pylint: disable=unused-argument 295 | """Gets QIF transaction footer. 296 | 297 | Returns: 298 | (str): the QIF footer 299 | 300 | Examples: 301 | >>> QIF().footer() == '' 302 | True 303 | """ 304 | return self.transaction_end() if self.is_split else "" 305 | 306 | def gen_body(self, data): 307 | """Generate the QIF body""" 308 | split_account = self.split_account or self.inv_split_account 309 | 310 | for datum in data: 311 | trxn_data = self.transaction_data(datum["trxn"]) 312 | account = self.account(datum["trxn"]) 313 | grp = datum["group"] 314 | 315 | if self.prev_group and self.prev_group != grp and self.is_split: 316 | yield self.transaction_end() 317 | 318 | if datum["is_main"] and self.prev_account != account: 319 | yield self.account_start(**trxn_data) 320 | yield self.transaction_start(**trxn_data) 321 | 322 | if (self.is_split and datum["is_main"]) or not self.is_split: 323 | yield self.transaction(**trxn_data) 324 | self.prev_account = account 325 | 326 | if (self.is_split and not datum["is_main"]) or split_account: 327 | yield self.split_content(**trxn_data) 328 | 329 | if not self.is_split: 330 | yield self.transaction_end() 331 | 332 | self.prev_group = grp 333 | 334 | def gen_groups(self, records, chunksize=None): 335 | """Generate the QIF groups""" 336 | for chnk in chunk(records, chunksize): 337 | keyfunc = self.id if self.is_split else self.account 338 | 339 | yield from group(chnk, keyfunc) 340 | -------------------------------------------------------------------------------- /csv2ofx/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4:ts=4:expandtab 3 | 4 | """ 5 | csv2ofx.utils 6 | ~~~~~~~~~~~~~ 7 | 8 | Provides miscellaneous utility methods 9 | 10 | Examples: 11 | literal blocks:: 12 | 13 | python example_google.py 14 | 15 | Attributes: 16 | ENCODING (str): Default file encoding. 17 | """ 18 | 19 | from collections import OrderedDict 20 | 21 | from meza.convert import to_decimal 22 | from meza.fntools import get_separators 23 | 24 | # NOTE: Because we are testing for substrings, the order we iterate 25 | # over this dictionary matters (so place strings like "reinvest" 26 | # above substrings like "invest") 27 | ACTION_TYPES = OrderedDict([ 28 | ("ShrsIn", ("deposit",)), 29 | ("ShrsOut", ("withdraw",)), 30 | ("ReinvDiv", ("reinvest",)), 31 | ( 32 | "Buy", 33 | ( 34 | "buy", 35 | "invest", 36 | ), 37 | ), 38 | ("Div", ("dividend",)), 39 | ("Int", ("interest",)), 40 | ("Sell", ("sell",)), 41 | ("StkSplit", ("split",)), 42 | ]) 43 | 44 | TRANSFERABLE = {"Buy", "Div", "Int", "Sell"} 45 | 46 | 47 | def get_account_type(account, account_types, def_type="n/a"): 48 | """Detect the account type of a given account 49 | 50 | Args: 51 | account (str): The account name 52 | account_types (dict): The account types with matching account names. 53 | def_type (str): The default account type. 54 | 55 | Returns: 56 | (str): The resulting account type. 57 | 58 | Examples: 59 | >>> get_account_type('somecash', {'Cash': ('cash',)}) == 'Cash' 60 | True 61 | >>> get_account_type('account', {'Cash': ('cash',)}) == 'n/a' 62 | True 63 | """ 64 | _type = def_type 65 | 66 | for key, values in account_types.items(): 67 | if any(v in account.lower() for v in values): 68 | _type = key 69 | break 70 | 71 | return _type 72 | 73 | 74 | def get_action(category, transfer=False, def_action="ShrsIn"): 75 | """Detect the investment action of a given category 76 | 77 | Args: 78 | category (str): The transaction category. 79 | transfer (bool): Is the transaction an account transfer? (default: 80 | False) 81 | def_type (str): The default action. 82 | 83 | Returns: 84 | (str): The resulting action. 85 | 86 | Examples: 87 | >>> get_action('dividend & cap gains') == 'Div' 88 | True 89 | >>> get_action('buy', True) == 'BuyX' 90 | True 91 | >>> get_action('invest') == 'Buy' 92 | True 93 | >>> get_action('reinvest') == 'ReinvDiv' 94 | True 95 | """ 96 | _type = def_action 97 | 98 | for key, values in ACTION_TYPES.items(): 99 | if any(v in category.lower() for v in values): 100 | _type = key 101 | break 102 | 103 | if transfer and _type in TRANSFERABLE: 104 | return f"{_type}X" 105 | else: 106 | return _type 107 | 108 | 109 | def convert_amount(content): 110 | """Convert number to a decimal amount 111 | 112 | Examples: 113 | >>> convert_amount('$1,000') 114 | Decimal('1000.00') 115 | >>> convert_amount('$1,000.00') 116 | Decimal('1000.00') 117 | >>> convert_amount('$1000,00') 118 | Decimal('1000.00') 119 | >>> convert_amount('$1.000,00') 120 | Decimal('1000.00') 121 | """ 122 | return to_decimal(content, **get_separators(content)) 123 | 124 | 125 | def get_max_split(splits, keyfunc): 126 | """Returns the split in a transaction with the largest absolute value 127 | 128 | Args: 129 | splits (List[dict]): return value of group_transactions() 130 | keyfunc (func): key function 131 | 132 | Returns: 133 | (Tuple[str]): splits collapsed content 134 | 135 | Examples: 136 | >>> from operator import itemgetter 137 | >>> splits = [{'amount': 350}, {'amount': -450}, {'amount': 100}] 138 | >>> get_max_split(splits, itemgetter('amount')) == (1, {'amount': -450}) 139 | True 140 | >>> splits = [{'amount': 350}, {'amount': -350}] 141 | >>> get_max_split(splits, itemgetter('amount')) == (0, {'amount': 350}) 142 | True 143 | """ 144 | 145 | def maxfunc(enum): 146 | return abs(keyfunc(enum[1])) 147 | 148 | return max(enumerate(splits), key=maxfunc) 149 | 150 | 151 | def verify_splits(splits, keyfunc): 152 | """Verifies that the splits of each transaction sum to 0 153 | 154 | Args: 155 | splits (dict): return value of group_transactions() 156 | keyfunc (func): function that returns the transaction amount 157 | 158 | Returns: 159 | (bool): true on success 160 | 161 | Examples: 162 | >>> from operator import itemgetter 163 | >>> splits = [{'amount': 100}, {'amount': -150}, {'amount': 50}] 164 | >>> verify_splits(splits, itemgetter('amount')) 165 | True 166 | >>> splits = [{'amount': 200}, {'amount': -150}, {'amount': 50}] 167 | >>> verify_splits(splits, itemgetter('amount')) 168 | False 169 | """ 170 | return not sum(map(keyfunc, splits)) 171 | 172 | 173 | def gen_data(groups): 174 | """Generate the transaction data""" 175 | for group, main_pos, sorted_trxns in groups: 176 | for pos, trxn in sorted_trxns: 177 | base_data = { 178 | "trxn": trxn, 179 | "is_main": pos == main_pos, 180 | "len": len(sorted_trxns), 181 | "group": group, 182 | } 183 | 184 | yield base_data 185 | -------------------------------------------------------------------------------- /csv2ofx/utilz/csvtrim: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################ 3 | # Remove useless trailing lines and trim columns in a CSV file to allow safe 4 | # processing by `csv2ofx`. By default, it trims files exported from UBS CH 5 | # (FR): details in function `_trim()` 6 | # 7 | # TO-DO: integrate in csv2ofx? 8 | ################################################################################ 9 | [ "$DEBUG" ] && set -x 10 | set -o pipefail 11 | 12 | __author__='Marco "sphakka" Poleggi' 13 | 14 | myself=$(basename $0) 15 | dfields='4,6,9,12-16,19-21' 16 | dseparator=';' 17 | 18 | ################################################################################ 19 | 20 | usage() { 21 | echo >&2 " 22 | Usage: 23 | 24 | $myself CSV_FILE [FIELDS [SEPARATOR]] 25 | 26 | where 27 | 28 | CSV_FILE: path to an existing file or '-' for stdin 29 | FIELDS: cut-style list of fields to keep. Default: '$dfields' 30 | SEPARATOR: a single (escaped) character. Default: '$dseparator' 31 | 32 | (default values are for exports from UBS CH (DE/FR/IT)) 33 | 34 | e.g. 35 | 36 | $myself hairy_export.csv 1,3,5-8 \; 37 | cat hairy_export.csv | $myself - 1,3,5-8 \;" 38 | 39 | exit 1 40 | } 41 | 42 | trap '[ $? -ne 0 ] && usage' EXIT 43 | 44 | input_csv=${1:?'arg #1 missing: input CSV file'} 45 | fields=${2:-$dfields} 46 | separator=${3:-$dseparator} 47 | 48 | 49 | function _trim () { 50 | local dlmtrc=${1:?'arg #1 missing: delimiter character'} 51 | local fields=${2:?'arg #2 missing: cut-style fields to keep'} 52 | local incsvf=${3:?'arg #3 missing: input CSV file'} 53 | local trmtln=${4:-'3'} # number of trailing lines to trim 54 | 55 | local head_opts= 56 | 57 | if [ "$trmtln" ]; then 58 | [[ "$trmtln" =~ ^[[:digit:]]+$ ]] || { 59 | echo >&2 "[error] ${trmtln}: number of traling lines to trim is not an integer" 60 | return 1 61 | } 62 | head_opts="-n-${trmtln}" 63 | fi 64 | 65 | # trnxs detailed as "Solde prix prestations" are split with a 66 | # "Sous-montant" value, but empty "Débit; Crédit; Solde" columns (the 67 | # trailing three). To avoid breaking csv2ofx, these must be filtered 68 | # out... The kludge is to skip rows ending with 3 consecutive delimiter chars 69 | head $head_opts $incsvf | cut -d$dlmtrc -f$fields | \ 70 | sed -nr "/${dlmtrc}${dlmtrc}${dlmtrc}\s*$/ !p" || { 71 | echo >&2 "[error] ${incsvf}: can't filter input file" 72 | return 1 73 | } 74 | } 75 | 76 | _trim $separator $fields $input_csv || { 77 | echo >&2 "[error] ${input_csv}: trimming failed..." 78 | exit 1 79 | } 80 | -------------------------------------------------------------------------------- /data/converted/amazon.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 83f2779c120eb1531fe646f32245de40 25 | 100000001 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20230604 31 | 32 | DEBIT 33 | 20221220000000 34 | -4.22 35 | 112-1635210-7125801 36 | Amazon 37 | Goof Off Household Heavy Duty Remover, 4 fl. oz. Spray, For Spots, Stains, Marks, and Messes; 38 | 39 | 40 | DEBIT 41 | 20221220000000 42 | -7.42 43 | 111-3273904-8117030 44 | Amazon 45 | Darksteve - Violet Decorative Light Bulb - Edison Light Bulb, Antique Vintage Style Light, G80 Size, E26 Base, Non-Dimmable (3w/110v); 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /data/converted/creditunion.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NCredit Union 3 | TBank 4 | ^ 5 | !Type:Bank 6 | NINV-1 7 | D02/08/2015 8 | PẰdøłƥh Noƴa 9 | T50000.00 10 | ^ 11 | NINV-2 12 | D02/08/2015 13 | PÓmary Akida 14 | T50000.00 15 | ^ 16 | NINV-3 17 | D02/24/2015 18 | PSadrick Mtel 19 | T70000.00 20 | ^ 21 | NINV-4 22 | D02/28/2015 23 | PHabibœ Said 24 | T65000.00 25 | ^ 26 | NINV-5 27 | D03/04/2015 28 | PNguluko Ómary Yahya 29 | T60000.00 30 | ^ 31 | NINV-6 32 | D03/09/2015 33 | PȢasha Ramadhani 34 | T75000.00 35 | ^ 36 | NINV-7 37 | D03/24/2015 38 | PTchênzema Tchênzema 39 | T45000.00 40 | ^ 41 | NINV-8 42 | D03/25/2015 43 | PƢunȡuƙi Chairman 44 | T50000.00 45 | ^ 46 | -------------------------------------------------------------------------------- /data/converted/default.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | eb45bd2725cb8ac50d0795432a918f86 25 | 069b30db06d047a398f9eb0940d3279c 26 | MONEYMRKT 27 | 28 | 29 | 19700101 30 | 20150908 31 | 32 | CREDIT 33 | 20150324000000 34 | 45000.00 35 | 6313fd711bc8f4288a27a757a2aa1495 36 | INV-7 37 | Tchênzema Tchênzema 38 | 39 | 40 | CREDIT 41 | 20150325000000 42 | 50000.00 43 | c18f9b25f4eb7202c77c1806547cc9d2 44 | INV-8 45 | Ƣunȡuƙi Chairman 46 | 47 | 48 | 49 | 50 | USD 51 | 52 | eb45bd2725cb8ac50d0795432a918f86 53 | 195917574edc9b6bbeb5be9785b6a479 54 | CHECKING 55 | 56 | 57 | 19700101 58 | 20150908 59 | 60 | CREDIT 61 | 20150208000000 62 | 50000.00 63 | ff388c16e4ed0b29da271188dd9a975f 64 | INV-1 65 | Ằdøłƥh Noƴa 66 | 67 | 68 | CREDIT 69 | 20150208000000 70 | 50000.00 71 | 3d7edd42734ac30cfda810e6e319cbec 72 | INV-2 73 | Ómary Akida 74 | 75 | 76 | CREDIT 77 | 20150224000000 78 | 70000.00 79 | 68276f75a2dc190454ea9a1a0f20941c 80 | INV-3 81 | Sadrick Mtel 82 | 83 | 84 | CREDIT 85 | 20150228000000 86 | 65000.00 87 | dad7ac4f00802814ed54e3ff6da18fc0 88 | INV-4 89 | Habibœ Said 90 | 91 | 92 | CREDIT 93 | 20150304000000 94 | 60000.00 95 | a09e46729f72618bf0ce373c15d1f9bf 96 | INV-5 97 | Nguluko Ómary Yahya 98 | 99 | 100 | CREDIT 101 | 20150309000000 102 | 75000.00 103 | 00a385fab455b4d6f969b8411899484e 104 | INV-6 105 | Ȣasha Ramadhani 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /data/converted/default.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NCash 3 | TCash 4 | ^ 5 | !Type:Cash 6 | NINV-7 7 | D03/24/2015 8 | PTchênzema Tchênzema 9 | T45000.00 10 | ^ 11 | NINV-8 12 | D03/25/2015 13 | PƢunȡuƙi Chairman 14 | T50000.00 15 | ^ 16 | !Account 17 | NChecking 18 | TBank 19 | ^ 20 | !Type:Bank 21 | NINV-1 22 | D02/08/2015 23 | PẰdøłƥh Noƴa 24 | T50000.00 25 | ^ 26 | NINV-2 27 | D02/08/2015 28 | PÓmary Akida 29 | T50000.00 30 | ^ 31 | NINV-3 32 | D02/24/2015 33 | PSadrick Mtel 34 | T70000.00 35 | ^ 36 | NINV-4 37 | D02/28/2015 38 | PHabibœ Said 39 | T65000.00 40 | ^ 41 | NINV-5 42 | D03/04/2015 43 | PNguluko Ómary Yahya 44 | T60000.00 45 | ^ 46 | NINV-6 47 | D03/09/2015 48 | PȢasha Ramadhani 49 | T75000.00 50 | ^ 51 | -------------------------------------------------------------------------------- /data/converted/default_w_splits.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 6313fd711bc8f4288a27a757a2aa1495 24 | 25 | 45000.00 26 | 27 | eb45bd2725cb8ac50d0795432a918f86 28 | 069b30db06d047a398f9eb0940d3279c 29 | MONEYMRKT 30 | 31 | 32 | eb45bd2725cb8ac50d0795432a918f86 33 | 134958285988bdb99b7c17836278fc55 34 | MONEYMRKT 35 | 36 | 37 | 20150324000000 38 | 39 | 40 | USD 41 | c18f9b25f4eb7202c77c1806547cc9d2 42 | 43 | 50000.00 44 | 45 | eb45bd2725cb8ac50d0795432a918f86 46 | 069b30db06d047a398f9eb0940d3279c 47 | MONEYMRKT 48 | 49 | 50 | eb45bd2725cb8ac50d0795432a918f86 51 | 134958285988bdb99b7c17836278fc55 52 | MONEYMRKT 53 | 54 | 55 | 20150325000000 56 | 57 | 58 | USD 59 | ff388c16e4ed0b29da271188dd9a975f 60 | 61 | 50000.00 62 | 63 | eb45bd2725cb8ac50d0795432a918f86 64 | 195917574edc9b6bbeb5be9785b6a479 65 | CHECKING 66 | 67 | 68 | eb45bd2725cb8ac50d0795432a918f86 69 | 134958285988bdb99b7c17836278fc55 70 | MONEYMRKT 71 | 72 | 73 | 20150208000000 74 | 75 | 76 | USD 77 | 3d7edd42734ac30cfda810e6e319cbec 78 | 79 | 50000.00 80 | 81 | eb45bd2725cb8ac50d0795432a918f86 82 | 195917574edc9b6bbeb5be9785b6a479 83 | CHECKING 84 | 85 | 86 | eb45bd2725cb8ac50d0795432a918f86 87 | 134958285988bdb99b7c17836278fc55 88 | MONEYMRKT 89 | 90 | 91 | 20150208000000 92 | 93 | 94 | USD 95 | 68276f75a2dc190454ea9a1a0f20941c 96 | 97 | 70000.00 98 | 99 | eb45bd2725cb8ac50d0795432a918f86 100 | 195917574edc9b6bbeb5be9785b6a479 101 | CHECKING 102 | 103 | 104 | eb45bd2725cb8ac50d0795432a918f86 105 | 134958285988bdb99b7c17836278fc55 106 | MONEYMRKT 107 | 108 | 109 | 20150224000000 110 | 111 | 112 | USD 113 | dad7ac4f00802814ed54e3ff6da18fc0 114 | 115 | 65000.00 116 | 117 | eb45bd2725cb8ac50d0795432a918f86 118 | 195917574edc9b6bbeb5be9785b6a479 119 | CHECKING 120 | 121 | 122 | eb45bd2725cb8ac50d0795432a918f86 123 | 134958285988bdb99b7c17836278fc55 124 | MONEYMRKT 125 | 126 | 127 | 20150228000000 128 | 129 | 130 | USD 131 | a09e46729f72618bf0ce373c15d1f9bf 132 | 133 | 60000.00 134 | 135 | eb45bd2725cb8ac50d0795432a918f86 136 | 195917574edc9b6bbeb5be9785b6a479 137 | CHECKING 138 | 139 | 140 | eb45bd2725cb8ac50d0795432a918f86 141 | 134958285988bdb99b7c17836278fc55 142 | MONEYMRKT 143 | 144 | 145 | 20150304000000 146 | 147 | 148 | USD 149 | 00a385fab455b4d6f969b8411899484e 150 | 151 | 75000.00 152 | 153 | eb45bd2725cb8ac50d0795432a918f86 154 | 195917574edc9b6bbeb5be9785b6a479 155 | CHECKING 156 | 157 | 158 | eb45bd2725cb8ac50d0795432a918f86 159 | 134958285988bdb99b7c17836278fc55 160 | MONEYMRKT 161 | 162 | 163 | 20150309000000 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /data/converted/default_w_splits.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NCash 3 | TCash 4 | ^ 5 | !Type:Cash 6 | NINV-7 7 | D03/24/2015 8 | PTchênzema Tchênzema 9 | T45000.00 10 | SExpenses 11 | $45000.00 12 | ^ 13 | NINV-8 14 | D03/25/2015 15 | PƢunȡuƙi Chairman 16 | T50000.00 17 | SExpenses 18 | $50000.00 19 | ^ 20 | !Account 21 | NChecking 22 | TBank 23 | ^ 24 | !Type:Bank 25 | NINV-1 26 | D02/08/2015 27 | PẰdøłƥh Noƴa 28 | T50000.00 29 | SExpenses 30 | $50000.00 31 | ^ 32 | NINV-2 33 | D02/08/2015 34 | PÓmary Akida 35 | T50000.00 36 | SExpenses 37 | $50000.00 38 | ^ 39 | NINV-3 40 | D02/24/2015 41 | PSadrick Mtel 42 | T70000.00 43 | SExpenses 44 | $70000.00 45 | ^ 46 | NINV-4 47 | D02/28/2015 48 | PHabibœ Said 49 | T65000.00 50 | SExpenses 51 | $65000.00 52 | ^ 53 | NINV-5 54 | D03/04/2015 55 | PNguluko Ómary Yahya 56 | T60000.00 57 | SExpenses 58 | $60000.00 59 | ^ 60 | NINV-6 61 | D03/09/2015 62 | PȢasha Ramadhani 63 | T75000.00 64 | SExpenses 65 | $75000.00 66 | ^ 67 | -------------------------------------------------------------------------------- /data/converted/gls.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | EUR 23 | 24 | d14cd6ef74114049327b8f2246eebceb 25 | e807f1fcf82d132f9bb018ca6738a19f 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20171111 31 | 32 | DEBIT 33 | 20171010000000 34 | -98.76 35 | 5d06f9f8b0417d02d496d0e4355686b5 36 | Drillisch Online AG 37 | SEPA-Basislastschrift 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /data/converted/ingesp.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | EUR 23 | 24 | 34d063e540ac5b4c50faa12a2c6b1844 25 | 0a81b5af663f48afea56886539f243fc 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20221231 31 | 32 | CREDIT 33 | 20220324000000 34 | 2.83 35 | 738f9c9dbbbcf6070bbabd53e0233806c3f0f05923610a1e0fe3d279950ae329 36 | Abono Shopping NARANJA:GALP 37 | Ventajas ING 38 | 39 | 40 | CREDIT 41 | 20220804000000 42 | 2.69 43 | d072ebce6b2b1a26eb2a58bfe31d1f6f0399699ff5e87d0704a9154a0696cc89 44 | Abono Shopping NARANJA:GALP 45 | Ventajas ING 46 | 47 | 48 | CREDIT 49 | 20221231000000 50 | 1.37 51 | bba42e0941066d29819dd7d230f305df17bb43cfaec526faa8d3fa3c0a56dccc 52 | AMZN Mktp ES 53 | Compras 54 | 55 | 56 | CREDIT 57 | 20221223000000 58 | 1394.11 59 | 71f2a047d445ddd17b33c3b08ba529bf6e6e80e482b7749256502b536421effc 60 | G PLCE SL. 61 | Nómina y otras prestaciones 62 | 63 | 64 | DEBIT 65 | 20220514000000 66 | -17.60 67 | e7c297585bfdc78f22bf0ada15ee25eef7e80eb6ffc576e191ea3729fbaaa697 68 | SPORTS BAR DANI JARQUE S BOI LLOBREGES 69 | Ocio y viajes 70 | 71 | 72 | DEBIT 73 | 20220413000000 74 | -276.89 75 | b8c9f171ad29997aa95a765a13fab63d3a3cdf999b0c150b50e546ef4afaa86d 76 | MUTUA MADRILENA AUTOMOVILISTA S. DE SEGU 77 | Vehículo y transporte 78 | 79 | 80 | DEBIT 81 | 20220729000000 82 | -1000.00 83 | 7619017c689132fb2ba0822ee44e0cbc60c7f4faa7a7463dbd586af576699189 84 | tarjeta B.B.V.A. MAT 85 | Otros gastos 86 | 87 | 88 | DEBIT 89 | 20221126000000 90 | -37.00 91 | 9255ff7417f3f6d85e14c6be9ef8d1c91e8dac3c6434ddd64ee37ab98443ae4d 92 | 93 | Otros gastos 94 | 95 | 96 | DEBIT 97 | 20220523000000 98 | -219.30 99 | 18ac6888a648d9fac238d786c7520638e52ba3405bf00165a42016c6b1304e52 100 | Salesians Mataro casal 101 | Otros gastos 102 | 103 | 104 | CREDIT 105 | 20221113000000 106 | 500.00 107 | a85de91ff0b8c177b83000f8f36e0c783fb6ab561fb93da896cfc27c277bdbed 108 | Cuenta Nómina 109 | Movimiento sin categoría 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /data/converted/mint.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | b0c693d9333d05dd9407e74e5ca2a3b9 24 | 25 | -1500.00 26 | 27 | 195917574edc9b6bbeb5be9785b6a479 28 | 195917574edc9b6bbeb5be9785b6a479 29 | CHECKING 30 | 31 | 32 | 195917574edc9b6bbeb5be9785b6a479 33 | ab0da0e987457927aebb5111d5d32c12 34 | SAVINGS 35 | 36 | 37 | 20150612000000 38 | 39 | 40 | USD 41 | 555c594e0e3b885775fe67596f62cad8 42 | 43 | 2000.00 44 | 45 | 195917574edc9b6bbeb5be9785b6a479 46 | 195917574edc9b6bbeb5be9785b6a479 47 | CHECKING 48 | 49 | 50 | 195917574edc9b6bbeb5be9785b6a479 51 | ab0da0e987457927aebb5111d5d32c12 52 | SAVINGS 53 | 54 | 55 | 20150613000000 56 | 57 | 58 | USD 59 | 3ee9a641052d5d62ec7d7247589cc8aa 60 | 61 | -2500.00 62 | 63 | 195917574edc9b6bbeb5be9785b6a479 64 | 195917574edc9b6bbeb5be9785b6a479 65 | CHECKING 66 | 67 | 68 | 195917574edc9b6bbeb5be9785b6a479 69 | ab0da0e987457927aebb5111d5d32c12 70 | SAVINGS 71 | 72 | 73 | 20150614000000 74 | 75 | 76 | USD 77 | fa33d6afdc7a621343b4049d88459e1a 78 | 79 | -1000.00 80 | 81 | ab0da0e987457927aebb5111d5d32c12 82 | ab0da0e987457927aebb5111d5d32c12 83 | SAVINGS 84 | 85 | 86 | ab0da0e987457927aebb5111d5d32c12 87 | 195917574edc9b6bbeb5be9785b6a479 88 | CHECKING 89 | 90 | 91 | 20150612000000 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /data/converted/mint.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NChecking 3 | TBank 4 | ^ 5 | !Type:Bank 6 | D06/12/2015 7 | PTransfer from Savings 8 | MAccount Transfer 9 | T-1500.00 10 | SSavings 11 | EAccount Transfer 12 | $-1500.00 13 | ^ 14 | D06/13/2015 15 | PTransfer to Savings 16 | MAccount Transfer 17 | T2000.00 18 | SSavings 19 | EAccount Transfer 20 | $2000.00 21 | ^ 22 | D06/14/2015 23 | PTransfer from Savings 24 | MAccount Transfer 25 | T-2500.00 26 | SSavings 27 | EAccount Transfer 28 | $-2500.00 29 | ^ 30 | !Account 31 | NSavings 32 | TBank 33 | ^ 34 | !Type:Bank 35 | D06/12/2015 36 | PTransfer from Checking 37 | MAccount Transfer 38 | T-1000.00 39 | SChecking 40 | EAccount Transfer 41 | $-1000.00 42 | ^ 43 | -------------------------------------------------------------------------------- /data/converted/mint_alt.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NChecking 3 | TBank 4 | ^ 5 | !Type:Bank 6 | D06/13/2015 7 | PTransfer to Savings 8 | MAccount Transfer 9 | T2000.00 10 | SSavings 11 | EAccount Transfer 12 | $2000.00 13 | ^ 14 | D06/14/2015 15 | PTransfer from Savings 16 | MAccount Transfer 17 | T-2500.00 18 | SSavings 19 | EAccount Transfer 20 | $-2500.00 21 | ^ 22 | -------------------------------------------------------------------------------- /data/converted/mint_extra.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NChecking 3 | TBank 4 | ^ 5 | !Type:Bank 6 | D06/12/2015 7 | PTransfer from Savings 8 | MAccount Transfer 9 | T-1500.00 10 | SSavings 11 | EAccount Transfer 12 | $-1500.00 13 | ^ 14 | D06/13/2015 15 | PTransfer to Savings 16 | MAccount Transfer 17 | T2000.00 18 | SSavings 19 | EAccount Transfer 20 | $2000.00 21 | ^ 22 | !Account 23 | NSavings 24 | TBank 25 | ^ 26 | !Type:Bank 27 | D06/12/2015 28 | PTransfer from Checking 29 | MAccount Transfer 30 | T-1000.00 31 | SChecking 32 | EAccount Transfer 33 | $-1000.00 34 | ^ 35 | -------------------------------------------------------------------------------- /data/converted/n26.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | EUR 23 | 24 | 89754e66afae91f7bcba8b9cbbd3aee7 25 | 57165337dd5eec7ef6c64f2b5ceb5a4d 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20250406 31 | 32 | CREDIT 33 | 20200307000000 34 | 328.00 35 | 0f784b3d076143afe1fbcf8eb9338d31a6216da00d97de878f68409a5cf0534e 36 | 37 | 38 | DEBIT 39 | 20200307000000 40 | -328.00 41 | 9b7646a2be0e6aaa04a1154d7be039da00a8f0a34dd8623be4fc79ee8064cfbf 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /data/converted/outbank.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | EUR 23 | 24 | 40250e0b5f1eb169f8fbf5d4bc28933b 25 | 40250e0b5f1eb169f8fbf5d4bc28933b 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20190301 31 | 32 | CREDIT 33 | 20190220000000 34 | 100.00 35 | 1ae208a732bfe251b108e379721db6de 36 | Jane Doe 37 | Jane Doe 38 | 39 | 40 | DEBIT 41 | 20190208000000 42 | -63.89 43 | d2bc2e4e6b309e6ac8e26f7d2f469bb6 44 | Shell Gas 45 | Shell Gas purchase MASTER 0123456789 46 | 47 | 48 | DEBIT 49 | 20190121000000 50 | -47.00 51 | af63d9f050b5f9fa392e2ea1822cd4cf 52 | Vattenfall Europe Energy 53 | A012345-67890 Energy bill Jan 2019 54 | 55 | 56 | DEBIT 57 | 20190105000000 58 | -25.00 59 | 49ca63706c5b7d4adbb7e7d9788fa323 60 | PayPal Europe S.a.r.l. et Cie S.C.A 61 | PP.1234.PP . STEAM GAMES, Your purchase at STEAM GAMES 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /data/converted/payoneer.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 123 25 | 59e711d152de7bec7304a8c2ecaf9f0f 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | DEBIT 33 | 20210503123146 34 | -100.00 35 | 123 36 | payee 37 | Transaction description 38 | 39 | 40 | CREDIT 41 | 20210503120831 42 | 120.00 43 | 1234 44 | payee 45 | Transaction description 46 | 47 | 48 | 49 | 200.00 50 | 20210503123146 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /data/converted/pcmastercard.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | CAD 23 | 24 | d221b309c3cd0d198adecb16eb8dc41d 25 | b48167465ffc278e8096a57fb3e5cf24 26 | CREDITLINE 27 | 28 | 29 | 19700101 30 | 20190120 31 | 32 | DEBIT 33 | 20190110000000 34 | -36.33 35 | 5d6f7d4a6af13d254c6582c0f54d43be 36 | 37 | 38 | DEBIT 39 | 20181215000000 40 | -13.98 41 | dd631c489b311c6bf8411c46c2e9ba31 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-baltest-case1.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | CREDIT 33 | 20220817000000 34 | 20.00 35 | d68ff9d88e633aaf2421a50077f848fe 36 | 37 | Deposit Mobile Banking 38 | Deposit Mobile Banking 39 | 40 | 41 | DEBIT 42 | 20220814000000 43 | -103.00 44 | c38117af9e65d7f8b751956b1f1e4173 45 | 46 | BMO HARRIS BANK 47 | BMO HARRIS BANK 48 | 49 | 50 | DEBIT 51 | 20220809000000 52 | -75.00 53 | 558 54 | 558 55 | Check Paid #558 56 | Check Paid #558 57 | 58 | 59 | DEBIT 60 | 20220804000000 61 | -57.27 62 | a4ce828223f505323e618e1976ecc466 63 | 64 | PAYPAL INST XFER 220803~ Tran: ACHDW 65 | PAYPAL INST XFER 220803~ Tran: ACHDW 66 | 67 | 68 | 69 | 878.47 70 | 20220817000000 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-baltest-case2.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | CREDIT 33 | 20220817000000 34 | 20.00 35 | d68ff9d88e633aaf2421a50077f848fe 36 | 37 | Deposit Mobile Banking 38 | Deposit Mobile Banking 39 | 40 | 41 | DEBIT 42 | 20220817000000 43 | -103.00 44 | ef9cd802919a077c705da67d059285fa 45 | 46 | BMO HARRIS BANK 47 | BMO HARRIS BANK 48 | 49 | 50 | DEBIT 51 | 20220804000000 52 | -57.27 53 | a4ce828223f505323e618e1976ecc466 54 | 55 | PAYPAL INST XFER 220803~ Tran: ACHDW 56 | PAYPAL INST XFER 220803~ Tran: ACHDW 57 | 58 | 59 | DEBIT 60 | 20220809000000 61 | -75.00 62 | 558 63 | 558 64 | Check Paid #558 65 | Check Paid #558 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-baltest-case3.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | DEBIT 33 | 20220804000000 34 | -57.27 35 | a4ce828223f505323e618e1976ecc466 36 | 37 | PAYPAL INST XFER 220803~ Tran: ACHDW 38 | PAYPAL INST XFER 220803~ Tran: ACHDW 39 | 40 | 41 | DEBIT 42 | 20220809000000 43 | -75.00 44 | 558 45 | 558 46 | Check Paid #558 47 | Check Paid #558 48 | 49 | 50 | DEBIT 51 | 20220817000000 52 | -103.00 53 | ef9cd802919a077c705da67d059285fa 54 | 55 | BMO HARRIS BANK 56 | BMO HARRIS BANK 57 | 58 | 59 | CREDIT 60 | 20220817000000 61 | 20.00 62 | d68ff9d88e633aaf2421a50077f848fe 63 | 64 | Deposit Mobile Banking 65 | Deposit Mobile Banking 66 | 67 | 68 | 69 | 878.47 70 | 20220817000000 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-baltest-case4.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | CREDIT 33 | 20220817000000 34 | 20.00 35 | d68ff9d88e633aaf2421a50077f848fe 36 | 37 | Deposit Mobile Banking 38 | Deposit Mobile Banking 39 | 40 | 41 | DEBIT 42 | 20220817000000 43 | -103.00 44 | ef9cd802919a077c705da67d059285fa 45 | 46 | BMO HARRIS BANK 47 | BMO HARRIS BANK 48 | 49 | 50 | DEBIT 51 | 20220809000000 52 | -75.00 53 | 558 54 | 558 55 | Check Paid #558 56 | Check Paid #558 57 | 58 | 59 | DEBIT 60 | 20220804000000 61 | -57.27 62 | a4ce828223f505323e618e1976ecc466 63 | 64 | PAYPAL INST XFER 220803~ Tran: ACHDW 65 | PAYPAL INST XFER 220803~ Tran: ACHDW 66 | 67 | 68 | 69 | 878.47 70 | 20220817000000 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-baltest-case5.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | DEBIT 33 | 20220817000000 34 | -57.27 35 | ea1f298fe817eb848e17b13e4b94b751 36 | 37 | PAYPAL INST XFER 220803~ Tran: ACHDW 38 | PAYPAL INST XFER 220803~ Tran: ACHDW 39 | 40 | 41 | DEBIT 42 | 20220817000000 43 | -75.00 44 | 558 45 | 558 46 | Check Paid #558 47 | Check Paid #558 48 | 49 | 50 | DEBIT 51 | 20220817000000 52 | -103.00 53 | ef9cd802919a077c705da67d059285fa 54 | 55 | BMO HARRIS BANK 56 | BMO HARRIS BANK 57 | 58 | 59 | CREDIT 60 | 20220817000000 61 | 20.00 62 | d68ff9d88e633aaf2421a50077f848fe 63 | 64 | Deposit Mobile Banking 65 | Deposit Mobile Banking 66 | 67 | 68 | 69 | 878.47 70 | 20220817000000 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-baltest-case6.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | CREDIT 33 | 20220817000000 34 | 20.00 35 | d68ff9d88e633aaf2421a50077f848fe 36 | 37 | Deposit Mobile Banking 38 | Deposit Mobile Banking 39 | 40 | 41 | DEBIT 42 | 20220817000000 43 | -103.00 44 | ef9cd802919a077c705da67d059285fa 45 | 46 | BMO HARRIS BANK 47 | BMO HARRIS BANK 48 | 49 | 50 | DEBIT 51 | 20220817000000 52 | -75.00 53 | 558 54 | 558 55 | Check Paid #558 56 | Check Paid #558 57 | 58 | 59 | DEBIT 60 | 20220817000000 61 | -57.27 62 | ea1f298fe817eb848e17b13e4b94b751 63 | 64 | PAYPAL INST XFER 220803~ Tran: ACHDW 65 | PAYPAL INST XFER 220803~ Tran: ACHDW 66 | 67 | 68 | 69 | 878.47 70 | 20220817000000 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-baltest-case7.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | DEBIT 33 | 20220817000000 34 | -20.00 35 | 558 36 | 558 37 | Check Paid #558 38 | Check Paid #558 39 | 40 | 41 | CREDIT 42 | 20220817000000 43 | 20.00 44 | d68ff9d88e633aaf2421a50077f848fe 45 | 46 | Deposit Mobile Banking 47 | Deposit Mobile Banking 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /data/converted/schwab-checking-msmoney.ofx: -------------------------------------------------------------------------------- 1 | OFXHEADER:100 2 | DATA:OFXSGML 3 | VERSION:102 4 | SECURITY:NONE 5 | ENCODING:USASCII 6 | CHARSET:1252 7 | COMPRESSION:NONE 8 | OLDFILEUID:NONE 9 | NEWFILEUID:NONE 10 | 11 | 12 | 13 | 14 | 0 15 | INFO 16 | 17 | 20161031112908 18 | ENG 19 | 20 | 21 | 22 | 23 | 1 24 | 25 | 0 26 | INFO 27 | 28 | 29 | USD 30 | 31 | 121202211 32 | 12345 33 | CHECKING 34 | 35 | 36 | 19700101120000 37 | 20220905120000 38 | 39 | CREDIT 40 | 20220817120000 41 | 20.00 42 | d68ff9d88e633aaf2421a50077f848fe 43 | Deposit Mobile Banking 44 | Deposit Mobile Banking 45 | 46 | 47 | DEBIT 48 | 20220814120000 49 | -103.00 50 | c38117af9e65d7f8b751956b1f1e4173 51 | BMO HARRIS BANK 52 | BMO HARRIS BANK 53 | 54 | 55 | DEBIT 56 | 20220809120000 57 | -75.00 58 | 558 59 | 558 60 | Check Paid #558 61 | Check Paid #558 62 | 63 | 64 | DEBIT 65 | 20220804120000 66 | -57.27 67 | a4ce828223f505323e618e1976ecc466 68 | PAYPAL INST XFER 220803~ Tran: A 69 | PAYPAL INST XFER 220803~ Tran: ACHDW 70 | 71 | 72 | 73 | 878.47 74 | 20220817120000 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /data/converted/schwab-checking.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 121202211 25 | 12345 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20220905 31 | 32 | CREDIT 33 | 20220817000000 34 | 20.00 35 | d68ff9d88e633aaf2421a50077f848fe 36 | 37 | Deposit Mobile Banking 38 | Deposit Mobile Banking 39 | 40 | 41 | DEBIT 42 | 20220814000000 43 | -103.00 44 | c38117af9e65d7f8b751956b1f1e4173 45 | 46 | BMO HARRIS BANK 47 | BMO HARRIS BANK 48 | 49 | 50 | DEBIT 51 | 20220809000000 52 | -75.00 53 | 558 54 | 558 55 | Check Paid #558 56 | Check Paid #558 57 | 58 | 59 | DEBIT 60 | 20220804000000 61 | -57.27 62 | a4ce828223f505323e618e1976ecc466 63 | 64 | PAYPAL INST XFER 220803~ Tran: ACHDW 65 | PAYPAL INST XFER 220803~ Tran: ACHDW 66 | 67 | 68 | 69 | 878.47 70 | 20220817000000 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/stripe-all.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | cad 23 | 24 | ce7566d1d08cc094b74cf283cf9c56a5 25 | ce7566d1d08cc094b74cf283cf9c56a5 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20210505 31 | 32 | CREDIT 33 | 20210203131200 34 | 25.00 35 | ch_1IGl9SBI9JK85yVZtjcU9OLJ 36 | Charles Darwin 37 | Entry ID: 316, Product: Big Event 2021 38 | 39 | 40 | CREDIT 41 | 20210203065300 42 | 27.50 43 | ch_1IGfFFBI9JK85yVZn6NXQs03 44 | James Newton 45 | Entry ID: 315, Product: Big Event 2021 46 | 47 | 48 | CREDIT 49 | 20210202235400 50 | 25.00 51 | ch_1IGYhLBI9JK85yVZPHrxqN27 52 | charles Darwin 53 | Entry ID: 314, Product: Big Event 2021 54 | 55 | 56 | CREDIT 57 | 20210202232100 58 | 25.00 59 | ch_1IGYBPBI9JK85yVZ0OID8QTG 60 | Steve Strong 61 | Entry ID: 313, Product: Big Event 2021 62 | 63 | 64 | CREDIT 65 | 20210316115100 66 | 10.00 67 | ch_1IVbR2BI9JK85yVZUofVdX4R 68 | Big Customer 69 | Invoice 396BEA86-0001 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/stripe-all.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NStripe 3 | TBank 4 | ^ 5 | !Type:Bank 6 | D02/03/2021 7 | PCharles Darwin 8 | MEntry ID: 316, Product: Big Event 2021 9 | T25.00 10 | ^ 11 | D02/03/2021 12 | PJames Newton 13 | MEntry ID: 315, Product: Big Event 2021 14 | T27.50 15 | ^ 16 | D02/02/2021 17 | Pcharles Darwin 18 | MEntry ID: 314, Product: Big Event 2021 19 | T25.00 20 | ^ 21 | D02/02/2021 22 | PSteve Strong 23 | MEntry ID: 313, Product: Big Event 2021 24 | T25.00 25 | ^ 26 | D03/16/2021 27 | PBig Customer 28 | MInvoice 396BEA86-0001 29 | T10.00 30 | ^ 31 | -------------------------------------------------------------------------------- /data/converted/stripe-default.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20161031112908 11 | ENG 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | INFO 20 | 21 | 22 | cad 23 | 24 | ce7566d1d08cc094b74cf283cf9c56a5 25 | ce7566d1d08cc094b74cf283cf9c56a5 26 | CHECKING 27 | 28 | 29 | 19700101 30 | 20210505 31 | 32 | CREDIT 33 | 20210203131200 34 | 25.00 35 | ch_1IGl9SBI9JK85yVZtjcU9OLJ 36 | 37 | Entry ID: 316, Product: Big Event 2021 38 | 39 | 40 | CREDIT 41 | 20210203065300 42 | 27.50 43 | ch_1IGfFFBI9JK85yVZn6NXQs03 44 | 45 | Entry ID: 315, Product: Big Event 2021 46 | 47 | 48 | CREDIT 49 | 20210202235400 50 | 25.00 51 | ch_1IGYhLBI9JK85yVZPHrxqN27 52 | 53 | Entry ID: 314, Product: Big Event 2021 54 | 55 | 56 | CREDIT 57 | 20210202232100 58 | 25.00 59 | ch_1IGYBPBI9JK85yVZ0OID8QTG 60 | 61 | Entry ID: 313, Product: Big Event 2021 62 | 63 | 64 | CREDIT 65 | 20210316115100 66 | 10.00 67 | ch_1IVbR2BI9JK85yVZUofVdX4R 68 | Big Customer 69 | Invoice 396BEA86-0001 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /data/converted/stripe-default.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NStripe 3 | TBank 4 | ^ 5 | !Type:Bank 6 | D02/03/2021 7 | MEntry ID: 316, Product: Big Event 2021 8 | T25.00 9 | ^ 10 | D02/03/2021 11 | MEntry ID: 315, Product: Big Event 2021 12 | T27.50 13 | ^ 14 | D02/02/2021 15 | MEntry ID: 314, Product: Big Event 2021 16 | T25.00 17 | ^ 18 | D02/02/2021 19 | MEntry ID: 313, Product: Big Event 2021 20 | T25.00 21 | ^ 22 | D03/16/2021 23 | PBig Customer 24 | MInvoice 396BEA86-0001 25 | T10.00 26 | ^ 27 | -------------------------------------------------------------------------------- /data/converted/ubs-ch-fr.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | N0123 45678901.23A 3 | TBank 4 | ^ 5 | !Type:Bank 6 | NA01234BC01234567 7 | D2019-03-31 8 | LSolde prix prestations 9 | MSolde prix prestations 10 | T-10.00 11 | ^ 12 | N3456789ZT1234567 13 | D2019-02-28 14 | PASSOCIATION FOO-BAR 15 | LVirement postal: ASSOCIATION FOO-BAR 16 | MASSOCIATION FOO-BAR: BVD DE QUELQUE-PART 1, 1201 GENEVE, CH ASSOCIATION FOO-BAR 17 | T240.00 18 | ^ 19 | N9979360TI2115087 20 | D2019-04-27 21 | PQuuz-baz SàrL, CH - 1203 GENEVE, E-Banking CHF intérieur 22 | LOrdre e-banking: REMB-CASH 23 | MREMB-CASH: Quuz-baz SàrL, CH - 1203 GENEVE, E-Banking CHF intérieur REMB-CASH 24 | T-200.00 25 | ^ 26 | -------------------------------------------------------------------------------- /data/converted/xero.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NIncome 3 | TBank 4 | ^ 5 | !Type:Bank 6 | NINV-0001 7 | D01/23/2015 8 | PSales 9 | T149000.00 10 | SCash 11 | EBicycle Office 12 | $113000.00 13 | SCash 14 | ECharger Office 15 | $36000.00 16 | ^ 17 | !Account 18 | NCash 19 | TCash 20 | ^ 21 | !Type:Cash 22 | NINV-0198 23 | D01/25/2015 24 | PBaisikeli 25 | LJohn 26 | MBicycle 27 | T-113000.00 28 | SIncome 29 | $-113000.00 30 | ^ 31 | !Account 32 | NChecking 33 | TBank 34 | ^ 35 | !Type:Bank 36 | D02/14/2015 37 | PTransfer from Cash 38 | T-209000.00 39 | SCash 40 | $-209000.00 41 | ^ 42 | !Account 43 | NCash 44 | TCash 45 | ^ 46 | !Type:Cash 47 | NINV-0003 48 | D01/24/2015 49 | PCharger-baisikeli (distributor price) 50 | LOffice 51 | MCharger 52 | T-30000.00 53 | SIncome 54 | $-30000.00 55 | ^ 56 | NINV-0006 57 | D01/25/2015 58 | PCharger-baisikeli (distributor price) 59 | LJohn 60 | MCharger 61 | T-30000.00 62 | SIncome 63 | $-30000.00 64 | ^ 65 | -------------------------------------------------------------------------------- /data/example/investment_example.qif: -------------------------------------------------------------------------------- 1 | !Account 2 | NJoint Brokerage Account 3 | TInvst 4 | ^ 5 | !Type:Invst 6 | D8/25/93 7 | NShrsIn 8 | Yibm4 9 | I11.260 10 | Q88.81 11 | CX 12 | T1,000.00 13 | MOpening 14 | ^ 15 | D8/25/93 16 | NBuyX 17 | Yibm4 18 | I11.030 19 | Q9.066 20 | T100.00 21 | MEst. price as of 8/25/93 22 | L[CHECKING] 23 | $100.00 24 | ^ 25 | -------------------------------------------------------------------------------- /data/example/transaction_example.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20050831165153.000[-8:PST] 11 | ENG 12 | 13 | 14 | 15 | 16 | 0 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 24 | 000000123 25 | 123456 26 | CHECKING 27 | 28 | 29 | 20050801 30 | 20050831165153.000[-8:PST] 31 | 32 | POS 33 | 20050824080000 34 | -80 35 | 219378 36 | FrogKick Scuba Gear 37 | 38 | 39 | 40 | 2156.56 41 | 20050831165153 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /data/example/transfer_example.ofx: -------------------------------------------------------------------------------- 1 | DATA:OFXSGML 2 | ENCODING:UTF-8 3 | 4 | 5 | 6 | 7 | 0 8 | INFO 9 | 10 | 20050831165153.000[-8:PST] 11 | ENG 12 | 13 | 14 | 15 | 16 | 1001 17 | 18 | 0 19 | INFO 20 | 21 | 22 | USD 23 | 1001 24 | 25 | 26 | 121099999 27 | 999988 28 | CHECKING 29 | 30 | 31 | 121099999 32 | 999977 33 | SAVINGS 34 | 35 | 200.00 36 | 37 | 20050829100000 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /data/test/amazon.csv: -------------------------------------------------------------------------------- 1 | order id,order url,items,to,date,total,shipping,shipping_refund,gift,tax,refund,payments 2 | 112-1635210-7125801,https://www.amazon.com/...,"Goof Off Household Heavy Duty Remover, 4 fl. oz. Spray, For Spots, Stains, Marks, and Messes; ",John Doe,2022-12-20,4.22,0,0,0,0.24,0,"Visa ending in 1234: December 24, 2022: $4.22; " 3 | 111-3273904-8117030,https://www.amazon.com/...,"Darksteve - Violet Decorative Light Bulb - Edison Light Bulb, Antique Vintage Style Light, G80 Size, E26 Base, Non-Dimmable (3w/110v); ",John Doe,2022-12-20,7.42,0,0,0,0.42,0,"Visa ending in 5566: December 28, 2022: $7.42; " 4 | 114-5269613-6941034,https://www.amazon.com/...,"TOPGREENER Smart Wi-Fi In-Wall Tamper Resistant Dual USB Charger Outlet, Energy Monitoring, Compatible with Amazon Alexa and Google Assistant, Outlet; ",John Doe,2022-10-11,34.12,0,0,0,1.93,0,"Visa ending in 9876: October 12, 2022: $34.12; " 5 | D01-1234567-8901234,https://www.amazon.com/...,,,pending,,,,,,, 6 | D01-5678901-2345678,https://www.amazon.com/...,,,pending,,,,,,, 7 | order id,order url,items,to,date,total,shipping,shipping_refund,gift,tax,refund,payments 8 | -------------------------------------------------------------------------------- /data/test/capitalone.csv: -------------------------------------------------------------------------------- 1 | Transaction Date,Posted Date,Card No.,Description,Category,Debit,Credit 2 | 2015-12-31,2016-01-02,1234,Airplanes R Us,Other Travel,1000.00, 3 | 2015-12-31,2016-01-02,1234,CAPITAL ONE AUTOPAY PYMT,Payment/Credit,,1000.00 4 | -------------------------------------------------------------------------------- /data/test/creditunion.csv: -------------------------------------------------------------------------------- 1 | Check Number,Date,Description,Amount,Category,Comments INV-1,2/8/15,Ằdøłƥh Noƴa,50000,Expenses, INV-2,2/8/15,Ómary Akida,50000,Expenses, INV-3,2/24/15,Sadrick Mtel,70000,Expenses, INV-4,2/28/15,Habibœ Said,65000,Expenses, INV-5,3/4/15,Nguluko Ómary Yahya,60000,Expenses, INV-6,3/9/15,Ȣasha Ramadhani,75000,Expenses, INV-7,3/24/15,Tchênzema Tchênzema,45000,Expenses, INV-8,3/25/15,Ƣunȡuƙi Chairman,50000,Expenses, -------------------------------------------------------------------------------- /data/test/default.csv: -------------------------------------------------------------------------------- 1 | Row,Num,Date,Reference,Description,Amount,Account,Category,Notes 2 | ff388c16e4ed0b29da271188dd9a975f,INV-1,2015-02-08,,"Ằdøłƥh Noƴa",50000,"Checking","Expenses", 3 | 3d7edd42734ac30cfda810e6e319cbec,INV-2,2015-02-08,,"Ómary Akida",50000,"Checking","Expenses", 4 | 68276f75a2dc190454ea9a1a0f20941c,INV-3,2015-02-24,,"Sadrick Mtel",70000,"Checking","Expenses", 5 | dad7ac4f00802814ed54e3ff6da18fc0,INV-4,2015-02-28,,"Habibœ Said",65000,"Checking","Expenses", 6 | a09e46729f72618bf0ce373c15d1f9bf,INV-5,2015-03-04,,"Nguluko Ómary Yahya",60000,"Checking","Expenses", 7 | 00a385fab455b4d6f969b8411899484e,INV-6,2015-03-09,,"Ȣasha Ramadhani",75000,"Checking","Expenses", 8 | 6313fd711bc8f4288a27a757a2aa1495,INV-7,2015-03-24,,"Tchênzema Tchênzema",45000,"Cash","Expenses", 9 | c18f9b25f4eb7202c77c1806547cc9d2,INV-8,2015-03-25,,"Ƣunȡuƙi Chairman",50000,"Cash","Expenses", 10 | -------------------------------------------------------------------------------- /data/test/gls.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reubano/csv2ofx/a36e1f2df3b841c4838bae2cc644b15194d7cbd1/data/test/gls.csv -------------------------------------------------------------------------------- /data/test/ingesp.csv: -------------------------------------------------------------------------------- 1 | date,class,subcategory,desc,notes,image,amount,balance 2 | 24/03/2022,Ventajas ING,Otras Ventajas ING,Abono por campaña Abono Shopping NARANJA:GALP,,No,2.83,1719.90 3 | 08/04/2022,Ventajas ING,Otras Ventajas ING,Abono por campaña Abono Shopping NARANJA:GALP,,No,2.69,2447.31 4 | 31/12/2022,Compras,Compras (otros),Devolución Tarjeta AMZN Mktp ES,,No,1.37,318.88 5 | 23/12/2022,Nómina y otras prestaciones,Nómina o Pensión,Nomina recibida G PLCE SL.,,No,1394.11,682.33 6 | 14/05/2022,Ocio y viajes,Cafeterías y restaurantes,Pago en SPORTS BAR DANI JARQUE S BOI LLOBREGES,,No,-17.60,1852.43 7 | 13/04/2022,Vehículo y transporte,Seguro de coche y moto,Recibo MUTUA MADRILENA AUTOMOVILISTA S. DE SEGU,,No,-276.89,837.52 8 | 29/07/2022,Otros gastos,Cajeros,Reintegro efectivo tarjeta B.B.V.A. MAT,,No,-1000.00,5049.35 9 | 26/11/2022,Otros gastos,Gasto Bizum,Transferencia Bizum emitida,,No,-37.00,456.88 10 | 23/05/2022,Otros gastos,Transferencias,Transferencia emitida a Salesians Mataro casal,,No,-219.30,1140.18 11 | 13/11/2022,Movimiento sin categoría,Transacción entre cuentas de ahorro,Traspaso recibido Cuenta Nómina,,No,500.00,-126.19 -------------------------------------------------------------------------------- /data/test/mint.csv: -------------------------------------------------------------------------------- 1 | Date,Description,Original Description,Amount,Transaction Type,Category,Account Name,Labels,Notes 6/12/15,Transfer from Checking,Account Transfer,"1,000.00",debit,Checking,Savings,, 6/12/15,Transfer from Savings,Account Transfer,"1,500.00",debit,Savings,Checking,, 6/13/15,Transfer to Savings,Account Transfer,"2,000.00",credit,Savings,Checking,, 6/14/15,Transfer from Savings,Account Transfer,"2,500.00",debit,Savings,Checking,, -------------------------------------------------------------------------------- /data/test/mint_extra.csv: -------------------------------------------------------------------------------- 1 | Date,Description,Original Description,Amount,Transaction Type,Category,Account Name,Labels,Notes 6/12/15,Transfer from Checking,Account Transfer,"1,000.00",debit,Checking,Savings,, 6/12/15,Transfer from Savings,Account Transfer,"1,500.00",debit,Savings,Checking,, 6/13/15,Transfer to Savings,Account Transfer,"2,000.00",credit,Savings,Checking,, 6/14/15,Transfer from Savings,Account Transfer,"2,500.00",debit,Savings,Checking,, Date,Description,Original Description,Amount,Transaction Type,Category,Account Name,Labels,Notes -------------------------------------------------------------------------------- /data/test/mint_headerless.csv: -------------------------------------------------------------------------------- 1 | 6/12/15,Transfer from Checking,Account Transfer,"1,000.00",debit,Checking,Savings 6/12/15,Transfer from Savings,Account Transfer,"1,500.00",debit,Savings,Checking 6/13/15,Transfer to Savings,Account Transfer,"2,000.00",credit,Savings,Checking 6/14/15,Transfer from Savings,Account Transfer,"2,500.00",debit,Savings,Checking -------------------------------------------------------------------------------- /data/test/n26-fr.csv: -------------------------------------------------------------------------------- 1 | "Booking Date","Value Date","Partner Name","Partner Iban",Type,"Payment Reference","Account Name","Amount (EUR)","Original Amount","Original Currency","Exchange Rate" 2 | 2020-03-07,,"Compte courant",DE27100110000000000000,"Credit Transfer",,"Coffre fort",328.00,,, 3 | 2020-03-07,,"Compte courant",DE27100110000000000000,"Debit Transfer",,"Coffre fort",-328.00,,, 4 | -------------------------------------------------------------------------------- /data/test/outbank.csv: -------------------------------------------------------------------------------- 1 | #;Account;Date;Value Date;Amount;Currency;Name;Number;Bank;Reason;Category;Subcategory;Tags;Note;Ultimate Receiver Name;Original Amount;Compensation Amount;Exchange Rate;Posting Key;Posting Text;Purpose Code;SEPA Reference;Client Reference;Mandate Identification;Originator Identifier 2 | 1;DE81254326973657190105;2/20/19;2/20/19;100,00;EUR;"Jane Doe";"DE21290466325050683191";"INGDDEFFXXX";"Jane Doe";"Financials & Precautions";"Refund";;;;;;;"052";"Gutschrift aus Dauerauftrag";;;;; 3 | 2;DE81254326973657190105;2/8/19;2/8/19;-63,89;EUR;"Shell Gas";"DE33674643548447246470";"CHASDEFX";"Shell Gas purchase MASTER 0123456789";"Travel";"Gas";;;;;;;"005";"Lastschrifteinzug";;"0123456789";;"0001-200000000000";"DE1234567890" 4 | 3;DE81254326973657190105;1/21/19;1/21/19;-47,00;EUR;"Vattenfall Europe Energy";"DE09550104000533925078";"AARBDE5WDOM";"A012345-67890 Energy bill Jan 2019";"Household";"Energy";;;;;;;"005";"Lastschrifteinzug";;"D0123456-R0123456";;"A012345-67890";"DE1234567890" 5 | 4;DE81254326973657190105;1/5/19;1/5/19;-25,00;EUR;"PayPal Europe S.a.r.l. et Cie S.C.A";"DE59564139545003970346";"BNPAFRPPXXX";"PP.1234.PP . STEAM GAMES, Your purchase at STEAM GAMES";"Living Expenses";"Online Shop";;;;;;;"005";"Lastschrifteinzug";;"0123456789 PP.1234.";;"B012345-67890";"LU96ZZZ000000000000000" -------------------------------------------------------------------------------- /data/test/payoneer.csv: -------------------------------------------------------------------------------- 1 | Transaction Date,Transaction Time,Time Zone,Transaction ID,Description,Credit Amount,Debit Amount,Currency,Transfer Amount,Transfer Amount Currency,Status,Running Balance,Additional Description,Store Name,Source,Target,Reference ID 2 | 05/03/2021,12:31:46,UTC,123,Transaction description,,100,USD,,,Completed,200,Payoneer additional description,,payer,payee, 3 | 05/03/2021,12:08:31,UTC,1234,Transaction description,120,,USD,,,Completed,300,Payoneer additional description,,payer,payee, 4 | -------------------------------------------------------------------------------- /data/test/pcmastercard.csv: -------------------------------------------------------------------------------- 1 | "Merchant Name","Card Used For Transaction","Date","Time","Amount" 2 | "Mobil","**** 1234","01/10/2019","06:50 PM","36.33" 3 | "APL*ITUNES.COM/BILL","**** 4567","12/15/2018","05:08 PM","13.98" 4 | -------------------------------------------------------------------------------- /data/test/schwab-checking-baltest-case1.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 3 | "08/14/2022","Posted","ATM","","BMO HARRIS BANK","$103.00","","$858.47" 4 | "08/09/2022","Posted","CHECK","558","Check Paid #558","$75.00","","$961.47" 5 | "08/04/2022","Posted","ACH","","PAYPAL INST XFER 220803~ Tran: ACHDW","$57.27","","$1,036.47" 6 | -------------------------------------------------------------------------------- /data/test/schwab-checking-baltest-case2.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 3 | "08/17/2022","Posted","ATM","","BMO HARRIS BANK","$103.00","","$858.47" 4 | "08/04/2022","Posted","ACH","","PAYPAL INST XFER 220803~ Tran: ACHDW","$57.27","","$1,036.47" 5 | "08/09/2022","Posted","CHECK","558","Check Paid #558","$75.00","","$961.47" 6 | -------------------------------------------------------------------------------- /data/test/schwab-checking-baltest-case3.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/04/2022","Posted","ACH","","PAYPAL INST XFER 220803~ Tran: ACHDW","$57.27","","$1,036.47" 3 | "08/09/2022","Posted","CHECK","558","Check Paid #558","$75.00","","$961.47" 4 | "08/17/2022","Posted","ATM","","BMO HARRIS BANK","$103.00","","$858.47" 5 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 6 | -------------------------------------------------------------------------------- /data/test/schwab-checking-baltest-case4.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 3 | "08/17/2022","Posted","ATM","","BMO HARRIS BANK","$103.00","","$858.47" 4 | "08/09/2022","Posted","CHECK","558","Check Paid #558","$75.00","","$961.47" 5 | "08/04/2022","Posted","ACH","","PAYPAL INST XFER 220803~ Tran: ACHDW","$57.27","","$1,036.47" 6 | -------------------------------------------------------------------------------- /data/test/schwab-checking-baltest-case5.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/17/2022","Posted","ACH","","PAYPAL INST XFER 220803~ Tran: ACHDW","$57.27","","$1,036.47" 3 | "08/17/2022","Posted","CHECK","558","Check Paid #558","$75.00","","$961.47" 4 | "08/17/2022","Posted","ATM","","BMO HARRIS BANK","$103.00","","$858.47" 5 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 6 | -------------------------------------------------------------------------------- /data/test/schwab-checking-baltest-case6.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 3 | "08/17/2022","Posted","ATM","","BMO HARRIS BANK","$103.00","","$858.47" 4 | "08/17/2022","Posted","CHECK","558","Check Paid #558","$75.00","","$961.47" 5 | "08/17/2022","Posted","ACH","","PAYPAL INST XFER 220803~ Tran: ACHDW","$57.27","","$1,036.47" 6 | -------------------------------------------------------------------------------- /data/test/schwab-checking-baltest-case7.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/17/2022","Posted","CHECK","558","Check Paid #558","$20.00","","$858.47" 3 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 4 | -------------------------------------------------------------------------------- /data/test/schwab-checking.csv: -------------------------------------------------------------------------------- 1 | "Date","Status","Type","CheckNumber","Description","Withdrawal","Deposit","RunningBalance" 2 | "08/17/2022","Posted","DEPOSIT","","Deposit Mobile Banking","","$20.00","$878.47" 3 | "08/14/2022","Posted","ATM","","BMO HARRIS BANK","$103.00","","$858.47" 4 | "08/09/2022","Posted","CHECK","558","Check Paid #558","$75.00","","$961.47" 5 | "08/04/2022","Posted","ACH","","PAYPAL INST XFER 220803~ Tran: ACHDW","$57.27","","$1,036.47" 6 | -------------------------------------------------------------------------------- /data/test/stripe-all.csv: -------------------------------------------------------------------------------- 1 | "id","description","seller_message","created","amount","amount_refunded","currency","converted_amount","converted_amount_refunded","fee","tax","converted_currency","mode","status","statement_descriptor","customer_id","customer_description","customer_email","captured","card_id","card_last4","card_brand","card_funding","card_exp_month","card_exp_year","card_name","card_address_line1","card_address_line2","card_address_city","card_address_state","card_address_country","card_address_zip","card_issue_country","card_fingerprint","card_cvc_status","card_avs_zip_status","card_avs_line1_status","card_tokenization_method","disputed_amount","dispute_status","dispute_reason","dispute_date","dispute_evidence_due","invoice_id","invoice_number","payment_source_type","destination","transfer","transfer_group","payment_intent_id" 2 | ch_1IGl9SBI9JK85yVZtjcU9OLJ,"Entry ID: 316, Product: Big Event 2021",Payment complete.,2021-02-03 13:12,25.00,0.00,cad,25.00,0.00,1.03,0.00,cad,Test,Paid,,,,,true,pm_1IGl9LBI9JK85yVZp5u0SDZa,0000,Visa,credit,12,2022,Charles Darwin,,,,,,Z9H 7B6,CA,ZBSdSbi8kMtMmdzP,pass,pass,,,,,,,,,,card,,,,pi_1IGl9PBI9JK85yVZ1xLwlWQW 3 | ch_1IGfFFBI9JK85yVZn6NXQs03,"Entry ID: 315, Product: Big Event 2021",Payment complete.,2021-02-03 06:53,27.50,0.00,cad,27.50,0.00,1.10,0.00,cad,Test,Paid,,,,,true,pm_1IGfF8BI9JK85yVZl7mebDo0,0000,Visa,credit,1,2022,James Newton,,,,,,J2E 7R1,CA,ZBSdSbi8kMtMmdzP,pass,pass,,,,,,,,,,card,,,,pi_1IGfFCBI9JK85yVZUghLwVay 4 | ch_1IGYhLBI9JK85yVZPHrxqN27,"Entry ID: 314, Product: Big Event 2021",Payment complete.,2021-02-02 23:54,25.00,0.00,cad,25.00,0.00,1.03,0.00,cad,Test,Paid,,,,,true,pm_1IGYhFBI9JK85yVZ4JQYPp72,0000,Visa,credit,11,2023,charles Darwin,,,,,,KIH 7B6,CA,ZBSdSbi8kMtMmdzP,pass,pass,,,,,,,,,,card,,,,pi_1IGYhIBI9JK85yVZlB9UjkOK 5 | ch_1IGYBPBI9JK85yVZ0OID8QTG,"Entry ID: 313, Product: Big Event 2021",Payment complete.,2021-02-02 23:21,25.00,0.00,cad,25.00,0.00,1.03,0.00,cad,Test,Paid,,,,,true,pm_1IGYBJBI9JK85yVZC713zkPP,0000,Visa,credit,12,2025,Steve Strong,,,,,,L2k 4J3,CA,ZBSdSbi8kMtMmdzP,pass,pass,,,,,,,,,,card,,,,pi_1IGYBMBI9JK85yVZcVnuvQ6m 6 | ch_1IVbR2BI9JK85yVZUofVdX4R,Invoice 396BEA86-0001,Payment complete.,2021-03-16 11:51,10.00,0.00,cad,10.00,0.00,0.59,0.00,cad,Live,Paid,,cus_J7gbFtFn5aCnnH,Big Customer,info@bigcustomer.example.com,true,src_1IVbR1BI9JK85yVZSaRjRQBW,0018,Visa,credit,5,2021,,,,,,,,CA,d5geTD3V0I6loY76,pass,,,,,,,,,in_1IVREgBI9JK85yVZlJIZTlnT,396BEA86-0001,card,,,,pi_1IVRFPBI9JK85yVZ3rSPuEiW 7 | -------------------------------------------------------------------------------- /data/test/stripe-default.csv: -------------------------------------------------------------------------------- 1 | "id","description","seller_message","created","amount","amount_refunded","currency","converted_amount","converted_amount_refunded","fee","tax","converted_currency","status","statement_descriptor","customer_id","customer_description","customer_email","captured","card_id","invoice_id","transfer" 2 | ch_1IGl9SBI9JK85yVZtjcU9OLJ,"Entry ID: 316, Product: Big Event 2021",Payment complete.,2021-02-03 13:12,25.00,0.00,cad,25.00,0.00,1.03,0.00,cad,Paid,,,,,true,pm_1IGl9LBI9JK85yVZp5u0SDZa,, 3 | ch_1IGfFFBI9JK85yVZn6NXQs03,"Entry ID: 315, Product: Big Event 2021",Payment complete.,2021-02-03 06:53,27.50,0.00,cad,27.50,0.00,1.10,0.00,cad,Paid,,,,,true,pm_1IGfF8BI9JK85yVZl7mebDo0,, 4 | ch_1IGYhLBI9JK85yVZPHrxqN27,"Entry ID: 314, Product: Big Event 2021",Payment complete.,2021-02-02 23:54,25.00,0.00,cad,25.00,0.00,1.03,0.00,cad,Paid,,,,,true,pm_1IGYhFBI9JK85yVZ4JQYPp72,, 5 | ch_1IGYBPBI9JK85yVZ0OID8QTG,"Entry ID: 313, Product: Big Event 2021",Payment complete.,2021-02-02 23:21,25.00,0.00,cad,25.00,0.00,1.03,0.00,cad,Paid,,,,,true,pm_1IGYBJBI9JK85yVZC713zkPP,, 6 | ch_1IVbR2BI9JK85yVZUofVdX4R,Invoice 396BEA86-0001,Payment complete.,2021-03-16 11:51,10.00,0.00,cad,10.00,0.00,0.59,0.00,cad,Paid,,cus_J7gbFtFn5aCnnH,Big Customer,info@bigcustomer.example.com,true,src_1IVbR1BI9JK85yVZSaRjRQBW,in_1IVREgBI9JK85yVZlJIZTlnT, 7 | -------------------------------------------------------------------------------- /data/test/ubs-ch-fr_trimmed.csv: -------------------------------------------------------------------------------- 1 | Produit;Monn.;Description;Date de valeur;Description 1;Description 2;Description 3;N° de transaction;Débit;Crédit;Solde 2 | 0123 45678901.23A;CHF;Compte personnel UBS;31.03.2019;Solde prix prestations;;;A01234BC01234567;10.00;;11'373.94 3 | 0123 45678901.23A;CHF;Compte personnel UBS;28.02.2019;Virement postal;ASSOCIATION FOO-BAR;BVD DE QUELQUE-PART 1, 1201 GENEVE, CH;3456789ZT1234567;;240.00;11'613.94 4 | 0123 45678901.23A;CHF;Compte personnel UBS;27.04.2019;Ordre e-banking;REMB-CASH;Quuz-baz SàrL, CH - 1203 GENEVE, E-Banking CHF intérieur;9979360TI2115087;200.00;;11'413.94 5 | -------------------------------------------------------------------------------- /data/test/xero.csv: -------------------------------------------------------------------------------- 1 | JournalNumber,JournalDate,AccountName,NetAmount,TaxCode,Resource,Product,Reference,Description 1,23-Jan-15,Income,-36000,Tax on Sales,,,INV-0001,Sales 1,23-Jan-15,Income,-113000,Tax on Sales,,,INV-0001,Sales 1,23-Jan-15,Cash,36000,,Office,Charger,INV-0001,Charger-baisikeli 1,23-Jan-15,Cash,113000,,Office,Bicycle,INV-0001,Baisikeli 3,24-Jan-15,Income,-30000,Tax on Sales,,,INV-0003,Sales 3,24-Jan-15,Cash,15000,,Office,Charger,INV-0003,Charger-baisikeli (distributor price) 3,24-Jan-15,Cash,15000,,Office,Charger,INV-0003,Charger-baisikeli (distributor price) 6,25-Jan-15,Income,-30000,Tax on Sales,,,INV-0006,Sales 6,25-Jan-15,Cash,30000,,John,Charger,INV-0006,Charger-baisikeli (distributor price) 10,25-Jan-15,Income,-113000,Tax on Sales,,,INV-0198,Sales 10,25-Jan-15,Cash,113000,,John,Bicycle,INV-0198,Baisikeli 11,14-Feb-15,Checking,209000,,,,,Transfer from Cash 11,14-Feb-15,Cash,-209000,,,,,Transfer to Checking -------------------------------------------------------------------------------- /docs/OFX 2.1.1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reubano/csv2ofx/a36e1f2df3b841c4838bae2cc644b15194d7cbd1/docs/OFX 2.1.1.pdf -------------------------------------------------------------------------------- /docs/OFXAudit.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reubano/csv2ofx/a36e1f2df3b841c4838bae2cc644b15194d7cbd1/docs/OFXAudit.xls -------------------------------------------------------------------------------- /docs/OFX_Message_Support.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reubano/csv2ofx/a36e1f2df3b841c4838bae2cc644b15194d7cbd1/docs/OFX_Message_Support.doc -------------------------------------------------------------------------------- /docs/QIF Specification_files/a.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/QIF Specification_files/a_002.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/QIF Specification_files/a_003.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/QIF Specification_files/a_004.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/QIF Specification_files/a_005.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/qif-file-format.txt: -------------------------------------------------------------------------------- 1 | QIF file format 2 | --------------- 3 | The QIF is an old and rather broken file format defined by Intuit 4 | for exporting Quicken data. It is 'broken' because the format 5 | is ambiguous in many places, non-standard between different releases 6 | and applications, and even varies subtly from country to country (in 7 | particular, the way dates and amounts are represented), and fails 8 | to define important data (such as the currency denomination, or the 9 | exchange rates when transferring between accounts marked in different 10 | currencies). Importing a QIF file can require significant manual 11 | intervention by the user in order to get the data straight. 12 | 13 | 14 | Extensions 15 | ---------- 16 | TEF -- Time and Expense Format (see below) 17 | 18 | QFX -- also known as 'Web Connect' -- very similar, and is the 19 | 'new' standard for on-line bank statement downloads. 20 | (??? or is it just 'ofx in a file' ???) 21 | 22 | 23 | Type of account identifiers 24 | ---------------------------- 25 | !Type:Bank Bank account 26 | !Type:Bill ??? (bill presentment ???) 27 | !Type:Cash Cash account 28 | !Type:CCard Credit Card account 29 | !Type:Invoice ??? (invoice presentment ???) 30 | !Type:Invst Investment account 31 | !Type:Oth A Asset account 32 | !Type:Oth S Asset account (German) 33 | !Type:Oth L Liability account 34 | !Type:Tax ??? 35 | 36 | !Account Account list or which account applies to following 37 | transactions 38 | 39 | !Type:Cat Category list 40 | !Type:Class Class list 41 | !Type:Memorized Memorized transaction list 42 | 43 | Note that !Account is used both to be a header for account information, 44 | and to be a header for a list of transactions. 45 | 46 | Also note that international versions of Quicken and MS Money often 47 | translate the Type: tags into the local language. But not always. 48 | 49 | 50 | Account Information Format 51 | -------------------------- 52 | The below typically follow an !Account identifier, and provide account 53 | data. 54 | 55 | Letter Definition 56 | N Name 57 | T Type of account 58 | D Description 59 | L Credit limit (only for credit card accounts) 60 | / Statement balance date 61 | $ Statement balance amount 62 | ^ End of entry 63 | 64 | 65 | Category Information Format 66 | --------------------------- 67 | N Category name:subcategory name 68 | D Description 69 | T Tax related if included, not tax related if omitted 70 | I Income category 71 | E Expense category (if category type is unspecified, 72 | assumes expense type) 73 | B Budget amount (optional, only appears in a Budget QIF file) 74 | R Tax schedule information 75 | ^ End of entry 76 | 77 | 78 | Class Information Format 79 | ------------------------ 80 | N Class name 81 | D Description 82 | ^ End of entry 83 | 84 | 85 | Memorized Transaction Format 86 | ---------------------------- 87 | KC Check transaction 88 | KD Deposit transaction 89 | KP Payment transaction 90 | KI Investment transaction 91 | KE Electronic payee transaction 92 | T Amount 93 | C Cleared status 94 | P Payee 95 | M Memo 96 | A Address 97 | L Category or Transfer/Class 98 | S Category/class in split 99 | E Memo in split 100 | $ Dollar amount of split 101 | 1 Amortization: First payment date 102 | 2 Amortization: Total years for loan 103 | 3 Amortization: Number of payments already made 104 | 4 Amortization: Number of periods per year 105 | 5 Amortization: Interest rate 106 | 6 Amortization: Current loan balance 107 | 7 Amortization: Original loan amount 108 | ^ End of entry 109 | 110 | Note that the K* entries must be the *last* entries in the transaction. 111 | All fields are optional. If this is an amortization record, then all 112 | seven amortization fields much be present. 113 | 114 | 115 | Investment transaction format 116 | ----------------------------- 117 | Letter Definition 118 | D Date (optional) 119 | N Action 120 | Y Security 121 | I Price 122 | Q Quantity (# of shares or split ratio) 123 | C Cleared status 124 | P first line text for transfers/reminders 125 | M Memo 126 | O Commission 127 | L Account for transfer 128 | (category/class or transfer/class) 129 | (For MiscIncX or MiscExpX actions, this will be 130 | category/class|transfer/class or |transfer/class) 131 | T Amount of transaction 132 | U Amount of transaction (higher possible value than T) 133 | $ Amount transferred 134 | ^ End of entry 135 | 136 | Note that numbers for investment transactions are positive in most 137 | cases. The importation process automatically takes care of negating 138 | the values for Actions that move funds out of an account (for example: 139 | sales, expenses and transfers). 140 | 141 | Be aware that GnuCash's file format stores the share quantity and the 142 | total value of the transaction. Prices are not stored. 143 | 144 | 145 | Non-investment transaction format 146 | --------------------------------- 147 | Letter Definition 148 | D Date 149 | T Amount 150 | U Transaction amount (higher possible value than T) 151 | C Cleared status 152 | N Number (check or reference number) 153 | P Payee/description 154 | M Memo 155 | A Address (up to 5 lines; 6th line is an optional message) 156 | L Category (category/class or transfer/class) 157 | 158 | S Category in split (category/class or transfer/class) 159 | E Memo in split 160 | $ Dollar amount of split 161 | % Percentage of split if percentages are used 162 | F Reimbursable business expense flag 163 | X Small Business extensions 164 | ^ End of entry 165 | 166 | Note that S,E and $ lines are repeated as needed for splits. 167 | 168 | 169 | Time and Expense Format 170 | ----------------------- 171 | The following QIF extension added by Iambic Software 172 | to handle time and expense tracking. This is used in particular 173 | by handhelds (Palm and WinCE). TEF is claimed to be a superset 174 | of the QIF format. 175 | 176 | TEF Files begin with the header: 177 | #TEF VERSION X.YYY 178 | Documented below is version 1.01 179 | 180 | # Any line beginning with # is a comment and not parsed 181 | B City 182 | F Reported 183 | H Report # 184 | J Attendees 185 | K Reimbursable 186 | R Receipt 187 | U Begin Odometer 188 | V End Odometer 189 | W Private 190 | X Exchange Rate 191 | Z User 192 | 193 | 1 Client 194 | 2 Project 195 | 3 Activity 196 | 4 Expense Type 197 | 5 Account 198 | 6 Vehicle 199 | 7 Currency 200 | 8 Task 201 | 9 (not used) 202 | 0 (not used) 203 | 204 | @ Billing Code 205 | ! Tax Amount 206 | % Uses Splits 207 | ( SalesTaxRate1 208 | ) SalesTaxRate2 209 | = Flat Fee Amount 210 | \ Status1 211 | / Status2 212 | & Status3 213 | < Status4 214 | > Status5 215 | ? Keyword: TIME, EXPENSE, CLIENT, PROJECT, ACTIVITY, TYPE, 216 | TASK, VEHICLE, PAYEE, CURRENCY. If absent, entry is 217 | assumed EXPENSE type as compatible with QIF 218 | 219 | * Duration hh:mm:ss 220 | 221 | + Timer On 222 | [ Start time 223 | ] End Time 224 | { TimerLastStoppedAt 225 | } (not used) 226 | | Notes 227 | 228 | 229 | When importing type CLIENT, PROJECT, ACTIVITY, TYPE, TASK, VEHICLE, 230 | PAYEE, CURRENCY the following are used: 231 | 232 | N Name 233 | C Code 234 | R Rate 235 | L Link 236 | W Private 237 | 238 | 239 | ===================================================================== 240 | General Notes 241 | ===================================================================== 242 | 243 | Dates 244 | ----- 245 | Dates in US QIF files are usually in the format MM/DD/YY, although 246 | four-digit years are not uncommon. Dates sometimes occur without the 247 | slash separator, or using other separators in place of the slash, 248 | commonly '-' and '.'. US Quicken seems to be using the ' to indicate 249 | post-2000 two-digit years (such as 01/01'00 for Jan 1 2000). Some 250 | banks appear to be using a completely undifferentiated numeric string 251 | formateed YYYYMMDD in downloaded QIF files. 252 | 253 | European QIF files may have dates in the DD/MM/YY format. 254 | 255 | 256 | Monetary Amounts 257 | ---------------- 258 | These typically occur in either US or European format: 259 | 260 | 10,000.00 Ten Thousand Dollars (US format) 261 | 10.000,00 Ten Thousand Francs (European format) 262 | 263 | An apostrophe is also used in some cases: 264 | 265 | 10'000.00 Ten Thousand Dollars (Quicken 4) 266 | 10'000,00 Ten Thousand Francs (unconfirmed) 267 | 268 | Within a given QIF file, the usage of a particular numeric format 269 | appears to be consistent within a particular field but may be 270 | different from one field to another. For example, the Share Amount 271 | field can be in European format but the Split Amount in US. No 272 | radix-point is required and no limit on decimal places is evident, so 273 | it's possible to see the number "1,000" meaning "1 franc per share" 274 | "1,000" meaning "one thousand shares" in the same transaction (!). 275 | 276 | 277 | Investment Actions 278 | ------------------ 279 | The N line of investment transactions specifies the "action" of the 280 | transaction. Although not a complete list, possible values include 281 | the following: 282 | 283 | QIF N Line Notes 284 | ============ ===== 285 | Aktab Same as ShrsOut. 286 | AktSplit Same as StkSplit. 287 | Aktzu Same as ShrsIn. 288 | Buy Buy shares. 289 | BuyX Buy shares. Used with an L line. 290 | Cash Miscellaneous cash transaction. Used with an L line. 291 | CGMid Mid-term capital gains. 292 | CGMidX Mid-term capital gains. For use with an L line. 293 | CGLong Long-term capital gains. 294 | CGLongX Long-term capital gains. For use with an L line. 295 | CGShort Short-term capital gains. 296 | CGShortX Short-term capital gains. For use with an L line. 297 | ContribX Same as XIn. Used for tax-advantaged accounts. 298 | CvrShrt Buy shares to cover a short sale. 299 | CvrShrtX Buy shares to cover a short sale. Used with an L line. 300 | Div Dividend received. 301 | DivX Dividend received. For use with an L line. 302 | Errinerg Same as Reminder. 303 | Exercise Exercise an option. 304 | ExercisX Exercise an option. For use with an L line. 305 | Expire Mark an option as expired. (Uses D, N, Y & M lines) 306 | Grant Receive a grant of stock options. 307 | Int Same as IntInc. 308 | IntX Same as IntIncX. 309 | IntInc Interest received. 310 | IntIncX Interest received. For use with an L line. 311 | K.gewsp Same as CGShort. (German) 312 | K.gewspX Same as CGShortX. (German) 313 | Kapgew Same as CGLong. Kapitalgewinnsteuer.(German) 314 | KapgewX Same as CGLongX. Kapitalgewinnsteuer. (German) 315 | Kauf Same as Buy. (German) 316 | KaufX Same as BuyX. (German) 317 | MargInt Margin interest paid. 318 | MargIntX Margin interest paid. For use with an L line. 319 | MiscExp Miscellaneous expense. 320 | MiscExpX Miscellaneous expense. For use with an L line. 321 | MiscInc Miscellaneous income. 322 | MiscIncX Miscellaneous income. For use with an L line. 323 | ReinvDiv Reinvested dividend. 324 | ReinvInt Reinvested interest. 325 | ReinvLG Reinvested long-term capital gains. 326 | Reinvkur Same as ReinvLG. 327 | Reinvksp Same as ReinvSh. 328 | ReinvMd Reinvested mid-term capital gains. 329 | ReinvSG Same as ReinvSh. 330 | ReinvSh Reinvested short-term capital gains. 331 | Reinvzin Same as ReinvDiv. 332 | Reminder Reminder. (Uses D, N, C & M lines) 333 | RtrnCap Return of capital. 334 | RtrnCapX Return of capital. For use with an L line. 335 | Sell Sell shares. 336 | SellX Sell shares. For use with an L line. 337 | ShtSell Short sale. 338 | ShrsIn Deposit shares. 339 | ShrsOut Withdraw shares. 340 | StkSplit Stock split. 341 | Verkauf Same as Sell. (German) 342 | VerkaufX Same as SellX. (German) 343 | Vest Mark options as vested. (Uses N, Y, Q, C & M lines) 344 | WithDrwX Same as XOut. Used for tax-advantaged accounts. 345 | XIn Transfer cash from another account. 346 | XOut Transfer cash to another account. 347 | 348 | 349 | Category/Transfer/Class line 350 | ---------------------------- 351 | The "L" line of most transactions specifies the category, transfer 352 | account, and class (if any) of the transaction. Square brackets 353 | surrounding the contents mean the transaction is a transfer to the 354 | named account. A forward slash separates the category/account from 355 | the class. So overall, the format is one of the following: 356 | 357 | LCategory of transaction 358 | L[Transfer account] 359 | LCategory of transaction/Class of transaction 360 | L[Transfer account]/Class of transaction 361 | 362 | In stock transactions, if the 'N' field (action) is MiscIncX or 363 | MiscExpX, there can be *two* account/class pairs on the L line, with 364 | the second guaranteed to be a transfer. I believe they are 365 | separated by a '|', like so: 366 | 367 | D01/01/2000 368 | NMiscExpX 369 | T1000.00 370 | Lexpense category/expense class|[Transfer account]/transfer class 371 | 372 | 373 | Cleared Status line 374 | ------------------- 375 | The "C" line of specifies the cleared status. The second character 376 | in the line, if present, may be any of: 377 | 378 | * Cleared 379 | c Cleared 380 | X Reconciled 381 | R Reconciled 382 | ? Budgeted 383 | ! Budgeted 384 | 385 | 386 | ===================================================================== 387 | Sample Files 388 | ===================================================================== 389 | 390 | Investment Transactions 391 | ----------------------- 392 | !Account 393 | NAssets:Investments:Mutual Fund 394 | TInvst 395 | D10/30/2006 396 | Q0.9 397 | T500 398 | PPurchase 399 | NBuyX 400 | L[Assets:Investments:Mutual Fund:Cash] 401 | YFOO 402 | ^ 403 | D11/28/2006 404 | Q0.897 405 | T100 406 | PSale 407 | NSellX 408 | L[Assets:Investments:Mutual Fund:Cash] 409 | YFOO 410 | ^ 411 | -------------------------------------------------------------------------------- /helpers/check-stage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # -*- coding: utf-8 -*- 3 | # 4 | # A script to disallow syntax errors to be committed 5 | # by running a checker (lint, pep8, pylint...) on them 6 | # 7 | # to install type ln -s check-stage .git/hooks/pre-commit 8 | 9 | # Redirect output to stderr. 10 | exec 2>&1 11 | 12 | # set path (necessary for gitx and git-gui) 13 | export PATH=$PATH:/opt/local/bin:/opt/local/sbin:/usr/local/sbin:/usr/local/bin 14 | 15 | # necessary check for initial commit 16 | if [ git rev-parse --verify HEAD >/dev/null 2>&1 ]; then 17 | against=HEAD 18 | else 19 | # Initial commit: diff against an empty tree object 20 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 21 | fi 22 | 23 | # set Internal Field Separator to newline (dash does not support $'\n') 24 | IFS=' 25 | ' 26 | 27 | # get a list of staged files 28 | for LINE in $(git diff-index --cached --full-index $against); do 29 | SHA=$(echo $LINE | cut -d' ' -f4) 30 | STATUS=$(echo $LINE | cut -d' ' -f5 | cut -d' ' -f1) 31 | FILENAME=$(echo $LINE | cut -d' ' -f5 | cut -d' ' -f2) 32 | FILEEXT=$(echo $FILENAME | sed 's/^.*\.//') 33 | 34 | # do not check deleted files 35 | if [ $STATUS == "D" ]; then 36 | continue 37 | fi 38 | 39 | # only check files with proper extension 40 | if [ $FILEEXT == 'php' ]; then 41 | PROGRAMS='php' 42 | COMMANDS='php -l' 43 | elif [ $FILEEXT == 'py' ]; then 44 | PROGRAMS=$'pep8\npylint' 45 | COMMANDS=$'pep8 --ignore=W191,E128' 46 | else 47 | continue 48 | fi 49 | 50 | for PROGRAM in $PROGRAMS; do 51 | test $(which $PROGRAM) 52 | 53 | if [ $? != 0 ]; then 54 | echo "$PROGRAM binary does not exist or is not in path" 55 | exit 1 56 | fi 57 | done 58 | 59 | # check the staged content for syntax errors 60 | for COMMAND in $COMMANDS; do 61 | git cat-file -p $SHA > tmp.txt 62 | RESULT=$(eval "$COMMAND tmp.txt") 63 | 64 | if [ $? != 0 ]; then 65 | echo "$COMMAND syntax check failed on $FILENAME" 66 | for LINE in $RESULT; do echo $LINE; done 67 | rm tmp.txt 68 | exit 1 69 | fi 70 | done 71 | done 72 | 73 | unset IFS 74 | rm tmp.txt 75 | 76 | # If there are whitespace errors, print the offending file names and fail. 77 | # exec git diff-index --check --cached $against -- 78 | -------------------------------------------------------------------------------- /helpers/clean: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # -*- coding: utf-8 -*- 3 | 4 | # remove build artifacts 5 | rm -fr build/ 6 | rm -fr dist/ 7 | rm -fr *.egg-info 8 | 9 | # remove Python file artifacts 10 | find . -name '*.pyc' -exec rm -f {} + 11 | find . -name '*.pyo' -exec rm -f {} + 12 | find . -name '*~' -exec rm -f {} + 13 | -------------------------------------------------------------------------------- /helpers/srcdist: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # -*- coding: utf-8 -*- 3 | 4 | # create a source distribution package 5 | 6 | python setup.py sdist 7 | gpg --detach-sign -a dist/*.tar.gz 8 | -------------------------------------------------------------------------------- /helpers/wheel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # -*- coding: utf-8 -*- 3 | 4 | # create a wheel package 5 | 6 | python setup.py bdist_wheel 7 | gpg --detach-sign -a dist/*.whl 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "csv2ofx" 7 | version = "0.34.0" 8 | description = "converts a csv file of transactions to an ofx or qif file" 9 | readme = "README.md" 10 | authors = [ 11 | { name = "Reuben Cummings", email = "reubano@gmail.com" } 12 | ] 13 | maintainers = [ 14 | { name = "Jason R. Coombs", email = "jaraco@jaraco.com" } 15 | ] 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Natural Language :: English", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: Implementation :: CPython", 22 | "Programming Language :: Python :: Implementation :: PyPy", 23 | "Environment :: Console", 24 | "Topic :: Software Development :: Libraries :: Python Modules", 25 | "Intended Audience :: Developers", 26 | ] 27 | dependencies = [ 28 | "python-dateutil>=2.7.2,<3.0.0", 29 | "requests>=2.18.4,<3.0.0", 30 | "meza>=0.47.0,<0.48.0", 31 | ] 32 | requires-python = ">=3.9" 33 | 34 | [project.urls] 35 | Source = "https://github.com/reubano/csv2ofx" 36 | 37 | [project.scripts] 38 | csv2ofx = "csv2ofx.main:run" 39 | 40 | [project.optional-dependencies] 41 | test = [ 42 | "pytest", 43 | "pytest-enabler", 44 | "pytest-ruff", 45 | "freezegun", 46 | ] 47 | 48 | [tool.setuptools] 49 | packages = ["csv2ofx", "csv2ofx.mappings"] 50 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs=dist build .tox .eggs 3 | addopts= 4 | --doctest-modules 5 | --import-mode importlib 6 | consider_namespace_packages=true 7 | filterwarnings= 8 | ## upstream 9 | 10 | # Ensure ResourceWarnings are emitted 11 | default::ResourceWarning 12 | 13 | # realpython/pytest-mypy#152 14 | ignore:'encoding' argument not specified::pytest_mypy 15 | 16 | # python/cpython#100750 17 | ignore:'encoding' argument not specified::platform 18 | 19 | # pypa/build#615 20 | ignore:'encoding' argument not specified::build.env 21 | 22 | # dateutil/dateutil#1284 23 | ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz 24 | 25 | ## end upstream 26 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) 2 | extend = "pyproject.toml" 3 | 4 | [lint] 5 | extend-select = [ 6 | # upstream 7 | 8 | "C901", # complex-structure 9 | "I", # isort 10 | "PERF401", # manual-list-comprehension 11 | "W", # pycodestyle Warning 12 | 13 | # Ensure modern type annotation syntax and best practices 14 | # Not including those covered by type-checkers or exclusive to Python 3.11+ 15 | "FA", # flake8-future-annotations 16 | "F404", # late-future-import 17 | "PYI", # flake8-pyi 18 | "UP006", # non-pep585-annotation 19 | "UP007", # non-pep604-annotation 20 | "UP010", # unnecessary-future-import 21 | "UP035", # deprecated-import 22 | "UP037", # quoted-annotation 23 | "UP043", # unnecessary-default-type-args 24 | 25 | # local 26 | "U", 27 | ] 28 | ignore = [ 29 | # upstream 30 | 31 | # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, 32 | # irrelevant to this project. 33 | "PYI011", # typed-argument-default-in-stub 34 | # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules 35 | "W191", 36 | "E111", 37 | "E114", 38 | "E117", 39 | "D206", 40 | "D300", 41 | "Q000", 42 | "Q001", 43 | "Q002", 44 | "Q003", 45 | "COM812", 46 | "COM819", 47 | 48 | # local 49 | ] 50 | 51 | [format] 52 | # Enable preview to get hugged parenthesis unwrapping and other nice surprises 53 | # See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 54 | preview = true 55 | # https://docs.astral.sh/ruff/settings/#format_quote-style 56 | quote-style = "preserve" 57 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import pathlib 3 | import shlex 4 | import subprocess 5 | import time 6 | 7 | import freezegun 8 | import pytest 9 | 10 | import csv2ofx.main 11 | 12 | samples = [ 13 | (["-oq"], "default.csv", "default.qif"), 14 | (["-oq", "-m split_account"], "default.csv", "default_w_splits.qif"), 15 | (["-oqc Description", "-m xero"], "xero.csv", "xero.qif"), 16 | (["-oq", "-m mint"], "mint.csv", "mint.qif"), 17 | (["-oq", "-m mint_extra"], "mint_extra.csv", "mint_extra.qif"), 18 | (["-oq", "-m mint_headerless"], "mint_headerless.csv", "mint.qif"), 19 | (["-oqs20150613", "-e20150614", "-m mint"], "mint.csv", "mint_alt.qif"), 20 | (["-oe 20150908"], "default.csv", "default.ofx"), 21 | (["-o", "-m split_account"], "default.csv", "default_w_splits.ofx"), 22 | (["-o", "-m mint"], "mint.csv", "mint.ofx"), 23 | (["-oq", "-m creditunion"], "creditunion.csv", "creditunion.qif"), 24 | ( 25 | ["-o", "-m stripe", "-e", "20210505"], 26 | "stripe-default.csv", 27 | "stripe-default.ofx", 28 | ), 29 | ( 30 | ["-o", "-m stripe", "-e", "20210505"], 31 | "stripe-all.csv", 32 | "stripe-all.ofx", 33 | ), 34 | (["-oq", "-m stripe"], "stripe-default.csv", "stripe-default.qif"), 35 | (["-oq", "-m stripe"], "stripe-all.csv", "stripe-all.qif"), 36 | ( 37 | ["-E windows-1252", "-m gls", "-e 20171111", "-o"], 38 | "gls.csv", 39 | "gls.ofx", 40 | ), 41 | ( 42 | ["-o", "-m pcmastercard", "-e 20190120"], 43 | "pcmastercard.csv", 44 | "pcmastercard.ofx", 45 | ), 46 | # ( 47 | # # N.B. input file obtained by pre-processing with 48 | # # bin/csvtrim ubs-ch-fr.csv > ubs-ch-fr_trimmed.csv 49 | # ["-oq", "-m ubs-ch-fr"], "ubs-ch-fr_trimmed.csv", "ubs-ch-fr.qif" 50 | # ), 51 | ( 52 | ["-o", "-m ingesp", "-e 20221231"], 53 | "ingesp.csv", 54 | "ingesp.ofx", 55 | ), 56 | ( 57 | ["-o", "-m schwabchecking", "-e 20220905"], 58 | "schwab-checking.csv", 59 | "schwab-checking.ofx", 60 | ), 61 | ( 62 | ["-o", "-M", "-m schwabchecking", "-e 20220905"], 63 | "schwab-checking.csv", 64 | "schwab-checking-msmoney.ofx", 65 | ), 66 | ( 67 | ["-o", "-m schwabchecking", "-e 20220905"], 68 | "schwab-checking-baltest-case1.csv", 69 | "schwab-checking-baltest-case1.ofx", 70 | ), 71 | ( 72 | ["-o", "-m schwabchecking", "-e 20220905"], 73 | "schwab-checking-baltest-case2.csv", 74 | "schwab-checking-baltest-case2.ofx", 75 | ), 76 | ( 77 | ["-o", "-m schwabchecking", "-e 20220905"], 78 | "schwab-checking-baltest-case3.csv", 79 | "schwab-checking-baltest-case3.ofx", 80 | ), 81 | ( 82 | ["-o", "-m schwabchecking", "-e 20220905"], 83 | "schwab-checking-baltest-case4.csv", 84 | "schwab-checking-baltest-case4.ofx", 85 | ), 86 | ( 87 | ["-o", "-m schwabchecking", "-e 20220905"], 88 | "schwab-checking-baltest-case5.csv", 89 | "schwab-checking-baltest-case5.ofx", 90 | ), 91 | ( 92 | ["-o", "-m schwabchecking", "-e 20220905"], 93 | "schwab-checking-baltest-case6.csv", 94 | "schwab-checking-baltest-case6.ofx", 95 | ), 96 | ( 97 | ["-o", "-m schwabchecking", "-e 20220905"], 98 | "schwab-checking-baltest-case7.csv", 99 | "schwab-checking-baltest-case7.ofx", 100 | ), 101 | ( 102 | ["-o", "-m amazon", "-e 20230604"], 103 | "amazon.csv", 104 | "amazon.ofx", 105 | ), 106 | ( 107 | ["-o", "-m payoneer", "-e 20220905"], 108 | "payoneer.csv", 109 | "payoneer.ofx", 110 | ), 111 | ( 112 | ['-m', 'n26'], 113 | 'n26-fr.csv', 114 | 'n26.ofx', 115 | ), 116 | ] 117 | 118 | 119 | @pytest.fixture(autouse=True) 120 | def amazon_env(monkeypatch): 121 | # for Amazon import; excludes transaction 3/3 122 | monkeypatch.setenv("AMAZON_EXCLUDE_CARDS", "9876") 123 | # clear the purchases account if set 124 | monkeypatch.delenv("AMAZON_PURCHASES_ACCOUNT", raising=False) 125 | 126 | 127 | data = pathlib.Path('data') 128 | 129 | 130 | def flatten_opts(opts): 131 | return list(param for group in opts for param in shlex.split(group)) 132 | 133 | 134 | @pytest.mark.parametrize(['opts', 'in_filename', 'out_filename'], samples) 135 | @freezegun.freeze_time("2016-10-31 11:29:08") 136 | def test_sample(opts, in_filename, out_filename, capsys, monkeypatch): 137 | monkeypatch.setattr(csv2ofx.main, '_time_from_file', lambda path: time.time()) 138 | arguments = [str(data / 'test' / in_filename)] 139 | command = list(itertools.chain(['csv2ofx'], flatten_opts(opts), arguments)) 140 | with pytest.raises(SystemExit) as exc: 141 | csv2ofx.main.run(command[1:]) 142 | # Success - exit code 0 143 | assert exc.value.code == 0 144 | 145 | expected = data.joinpath("converted", out_filename).read_text(encoding='utf-8') 146 | assert capsys.readouterr().out == expected, ( 147 | f"Unexpected output from {subprocess.list2cmdline(command)}" 148 | ) 149 | 150 | 151 | def test_help(): 152 | """ 153 | Assert help command completes and is present in the README. 154 | """ 155 | out = subprocess.check_output(['csv2ofx', '--help'], text=True) 156 | assert out 157 | 158 | 159 | @pytest.mark.xfail("sys.version_info < (3, 13)") 160 | def test_help_in_readme(): 161 | out = subprocess.check_output(['csv2ofx', '--help'], text=True) 162 | readme = pathlib.Path('README.md').read_text() 163 | assert out in readme, "README help is stale, please update." 164 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv] 2 | setenv = 3 | PYTHONHASHSEED=94967295 4 | PYTHONWARNINGS=all 5 | 6 | commands = 7 | pytest {posargs} 8 | 9 | extras = 10 | test 11 | --------------------------------------------------------------------------------