├── .flake8
├── .git-blame-ignore-revs
├── .github
└── workflows
│ └── python-app.yml
├── .gitignore
├── .hgignore
├── CHANGELOG.md
├── CONTRIBUTING
├── LICENSE
├── README.rst
├── examples
└── plugins
│ ├── aib.py
│ ├── firstdirect.py
│ ├── n26.py
│ ├── revolut.py
│ └── wise.py
├── fix_ofxid.py
├── fixtures
├── accented_characters_latin1.ofx
├── amazon.csv
├── amazon2.csv
├── apostrophe.ofx
├── checking-dynamic-account.lgr
├── checking-partial.lgr
├── checking-payee-match.ofx
├── checking.lgr
├── checking.ofx
├── checking_order.ofx
├── comments.ofx
├── cusip.ofx
├── empty.lgr
├── fidelity-one-dtsettle.ofx
├── fidelity.ofx
├── fidelity_fee.ofx
├── income.ofx
├── investment_401k.ofx
├── mint.csv
├── multiple.lgr
├── no-institution.ofx
├── ofxclient.ini
├── paypal.csv
├── paypal.lgr
├── paypal_alternate.csv
└── venmo.csv
├── ledgerautosync
├── __init__.py
├── cli.py
├── converter.py
├── ledgerwrap.py
├── plugins.py
└── sync.py
├── poetry.lock
├── pyproject.toml
├── setup.cfg
└── tests
├── __init__.py
├── conftest.py
├── test_cli.py
├── test_converter.py
├── test_ledger.py
├── test_ofx_formatter.py
├── test_sync.py
└── test_weird_ofx.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length=100
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | 039ee5e17ead3fc2f887351a22d98a1b43b79448
--------------------------------------------------------------------------------
/.github/workflows/python-app.yml:
--------------------------------------------------------------------------------
1 | name: Python package
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 | pull_request:
7 | branches: ["master"]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
16 | steps:
17 | - uses: actions/checkout@v4
18 | - name: Set up Python ${{ matrix.python-version }}
19 | uses: actions/setup-python@v3
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - name: Install dependencies
23 | run: |
24 | sudo apt install -y ledger hledger
25 | python -m pip install --upgrade pip
26 | python -m pip install --upgrade poetry
27 | python -m pip install flake8 pytest
28 | poetry install --with=dev
29 | - name: Lint with black
30 | run: |
31 | poetry run black --check .
32 | - name: Test with pytest
33 | run: |
34 | poetry run python -m pytest
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv/
2 | *.pyc
3 | *.egg
4 | .eggs
5 | build/
6 | dist/
7 | ledger_autosync.egg-info
8 |
--------------------------------------------------------------------------------
/.hgignore:
--------------------------------------------------------------------------------
1 | venv/
2 | .*pyc
3 | build/
4 | dist/
5 | ledger_autosync.egg-info
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Unreleased
2 | - Migrated to python3
3 | - Add --reverse option to print txns in reverse
4 | - Better error handling on OFX server error
5 | - Add --shorten-account, --hardcode-account options
6 | improve user privacy
7 | - Add --payee-format argument
8 | - Move ofxid metadata to correct posting
9 | - Misc bugfixes
10 |
11 | ## Version 0.3.5
12 | - Disable default usage of python bindings
13 | - Change to using 65 spaces to align txns (per ledger-mode)
14 | - Improve .ledgerrc parsing
15 | - Better error messages
16 | - Add basic plugin system
17 | - Misc bugfixes
18 |
19 | ## Version 0.3.4
20 | - Packaging fixes
21 |
22 | ## Version 0.3.3
23 | - Fix problem building on ubuntu trusty
24 |
25 | ## Version 0.3.2
26 | - Fix problem with certain characters in transaction id
27 |
28 | ## Version 0.3.0
29 | - Support CSV files (Mint, Paypal and Amazon flavors)
30 | - Uses ticker symbol by for currencies, not CUSIP (You will need to change
31 | previous transactions which used the CUSIP so they work with new transactions)
32 | - Dividends will now be formatted correctly
33 | - Fuzzy payee matching
34 |
35 | ## Version 0.2.5
36 | - Support advanced investment transactions
37 | - Upgrade to ofxparse 0.15
38 |
39 | ## Version 0.2.4
40 | - Add `--unknown-account` argument
41 |
42 | ## Version 0.2.2
43 | - Better support for strange OFX, payee characters
44 |
45 | ## Version 0.2.1
46 | - Support ledger python API
47 |
48 | ## Version 0.2.0
49 | - Improved hledger support
50 |
51 | ## Version 0.1.4
52 | - Balance assertions
53 | - Initial balance
54 |
55 | ## Version 0.1.3
56 | - Reverse transactions
57 |
58 | ## Version 0.1.0
59 | - Initial release
60 |
--------------------------------------------------------------------------------
/CONTRIBUTING:
--------------------------------------------------------------------------------
1 | Pull request for bugs and feature enhancements are welcome at:
2 |
3 | https://github.com/egh/ledger-autosync
4 |
5 | If you are considering a large change, please open an issue first to
6 | discuss any possible implications or best practices.
7 |
8 | If writing a bug fix, please include a regression test that would fail
9 | without the fix. If writing a new feature, testing that it works as
10 | expected is very much appreciated.
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ledger-autosync
2 | ===============
3 |
4 | .. image:: https://travis-ci.org/egh/ledger-autosync.svg?branch=master
5 | :target: https://travis-ci.org/egh/ledger-autosync
6 |
7 | ledger-autosync is a program to pull down transactions from your bank
8 | and create `ledger `__ transactions for them.
9 | It is designed to only create transactions that are not already
10 | present in your ledger files (that is, it will deduplicate
11 | transactions). This should make it comparable to some of the automated
12 | synchronization features available in products like GnuCash, Mint,
13 | etc. In fact, ledger-autosync performs OFX import and synchronization
14 | better than all the alternatives I have seen.
15 |
16 | News
17 | ----
18 |
19 | v1.0.0
20 | ~~~~~~
21 |
22 | Versions of ledger-autosync before 1.0.0 printed the ofxid in a
23 | slightly incorrect position. This should not effect usage of the
24 | program, but if you would like to correct the error, see below for
25 | more details.
26 |
27 | Features
28 | --------
29 |
30 | - supports `ledger `__ 3 and
31 | `hledger `__
32 | - like ledger, ledger-autosync will never modify your files directly
33 | - interactive banking setup via
34 | `ofxclient `__
35 | - multiple banks and accounts
36 | - support for non-US currencies
37 | - support for 401k and investment accounts
38 |
39 | - tracks investments by share, not dollar value
40 | - support for complex transaction types, including transfers, buys,
41 | sells, etc.
42 |
43 | - import of downloaded OFX files, for banks not supporting automatic
44 | download
45 | - import of downloaded CSV files from Paypal, Simple, Amazon and Mint
46 | - any CSV file can be supported via plugins
47 |
48 | Platforms
49 | ---------
50 |
51 | ledger-autosync is developed on Linux with ledger 3 and python 3; it
52 | has been tested on Windows (although it will run slower) and should
53 | run on OS X. It requires ledger 3 or hledger, but it should run faster
54 | with ledger, because it will not need to start a command to check
55 | every transaction.
56 |
57 |
58 | Quickstart
59 | ----------
60 |
61 | Installation
62 | ~~~~~~~~~~~~
63 |
64 | If you are on Debian or Ubuntu, an (older) version of ledger-autosync
65 | should be available for installation. Try:
66 |
67 | ::
68 |
69 | $ sudo apt-get install ledger-autosync
70 |
71 | If you use pip, you can install the latest released version:
72 |
73 | ::
74 |
75 | $ pip install ledger-autosync
76 |
77 | You can also install from source, if you have downloaded the source:
78 |
79 | ::
80 |
81 | $ python setup.py install
82 |
83 | You may need to install the following libraries (on debian/ubuntu):
84 |
85 | ::
86 |
87 | $ sudo apt-get install libffi-dev libpython-dev libssl-dev libxml2-dev python-pip libxslt-dev
88 |
89 | Running
90 | ~~~~~~~
91 |
92 | Once you have ledger-autosync installed, you can download an OFX file
93 | from your bank and run ledger-autosync against it:
94 |
95 | ::
96 |
97 | $ ledger-autosync download.ofx
98 |
99 | This should print a number of transactions to stdout. If you add these
100 | transactions to your default ledger file (whatever is read when you
101 | run ``ledger`` without arguments), you should find that if you run
102 | ledger-autosync again, it should print no transactions. This is
103 | because of the deduplicating feature: only new transactions will be
104 | printed for insertion into your ledger files.
105 |
106 | Using the ofx protocol for automatic download
107 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
108 |
109 | ledger-autosync also supports using the OFX protocol to automatically
110 | connect to banks and download data. You can use the ofxclient program
111 | (which should have been installed with ledger-autosync) to set up
112 | banking:
113 |
114 | ::
115 |
116 | $ ofxclient
117 |
118 | When you have added your institution, quit ofxclient.
119 |
120 | (At least one user has reported being signed up for a pay service by
121 | setting up OFX direct connect. Although this seems unusual, please be
122 | aware of this.)
123 |
124 | Edit the generated ``~/ofxclient.ini`` file. Change the
125 | ``description`` field of your accounts to the name used in ledger.
126 | Optionally, move the ``~/ofxclient.ini`` file to your ``~/.config``
127 | directory.
128 |
129 | Run:
130 |
131 | ::
132 |
133 | ledger-autosync
134 |
135 | This will download a maximum of 90 days previous activity from your
136 | accounts. The output will be in ledger format and printed to stdout.
137 | Add this output to your ledger file. When that is done, you can call:
138 |
139 | ::
140 |
141 | ledger-autosync
142 |
143 | again, and it should print nothing to stdout, because you already have
144 | those transactions in your ledger.
145 |
146 | How it works
147 | ------------
148 |
149 | ledger-autosync stores a unique identifier as metadata with each
150 | transaction. (For OFX files, this is a unique ID provided by your
151 | institution for each transaction.) When syncing with your bank, it
152 | will check if the transaction exists by running the ledger or hledger
153 | command. If the transaction exists, it does nothing. If it does not
154 | exist, the transaction is printed to stdout.
155 |
156 | Payee matching
157 | ~~~~~~~~~~~~~~
158 |
159 | When generating transactions, ledger-autosync attempts to match
160 | previous payees to determine the appropriate accounts. For instance,
161 | if a previous payee was ``Grocery store``, and one posting was to the
162 | account ``Expenses:Food``, ledger-autosync will use that account again.
163 | If ledger-autosync can’t find a matching payee, it will use
164 | ``Expenses:Misc`` or the value of the ``--unknown-account`` argument.
165 |
166 | The matching is not very sophisticated: it only does exact matching on
167 | the payee, and it uses transaction with the matching payee. It is also
168 | not currently working with CSV files.
169 |
170 | If you prefer to modify the payees to make them shorter than what is
171 | generated by ledger-autosync, you can use the ``AutosyncPayee`` metadata
172 | field to indicate to ledger-autosync that it should use the longer
173 | form for matching. For instance:
174 |
175 | ::
176 |
177 | 2011/03/31 Grocery
178 | ; AutosyncPayee: Payment to Grocery store #12345 CALIFORNIA
179 | Assets:Bank -$0.01
180 | Expenses:Food $0.01
181 |
182 | would indicate to ledger-autosync that any payee with the name
183 | ``Payment to Grocery store #12345 CALIFORNIA`` should use the
184 | ``Expenses:Food`` account.
185 |
186 | ofxid/csvid metadata tag
187 | ~~~~~~~~~~~~~~~~~~~~~~~~
188 |
189 | ledger-autosync stores a metatag with every posting that it outputs to
190 | support deduplication. This metadata tag is either ``ofxid`` (for OFX
191 | imports) or ``csvid`` for CSV imports.
192 |
193 | Pre-1.0.0 versions of ledger-autosync put this metadata tag in a
194 | slightly incorrect place, associating the metadata tag with the
195 | transaction itself, and not simply one posting. This should not effect
196 | the usage of ledger-autosync, but if you would like to correct your
197 | ledger files, there is a small python script ``fix_ofxid.py`` included
198 | with ledger-autosync. It can be run as:
199 |
200 | ::
201 |
202 | python fix_ofxid.py
203 |
204 | and will print a corrected file to stdout.
205 |
206 | Syncing a CSV file
207 | ------------------
208 |
209 | If you have a CSV file, you may also be able to import it using a
210 | recent (installed via source) version of ledger-autosync.
211 | ledger-autosync can currently process CSV files as provided by Paypal,
212 | Amazon, or Mint. You can process the CSV file as follows:
213 |
214 | ::
215 |
216 | ledger-autosync /path/to/file.csv -a Assets:Paypal
217 |
218 | With Amazon and Paypal CSV files, each row includes a unique
219 | identifier, so ledger-autosync will be able to deduplicate against any
220 | previously imported entries in your ledger files.
221 |
222 | With Mint, a unique identifier based on the data in the row is
223 | generated and stored. If future downloads contain identical rows, they
224 | will be deduplicated. This method is probably not as robust as a
225 | method based on unique ids, but Mint does not provide a unique id, and
226 | it should be better than nothing. It is likely to generate false
227 | negatives: transactions that seem new, but are in fact old. It will
228 | not generate false positives: transactions that are not generated
229 | because they seem old.
230 |
231 | If you are a developer, you should fine it easy enough to add a new
232 | CSV format to ledger-autosync. See, for example, the ``MintConverter``
233 | class in the ``ledgerautosync/converter.py`` file in this repository.
234 | See below for how to add these as plugins.
235 |
236 | Assertions
237 | ----------
238 |
239 | If you supply the ``--assertions`` flag, ledger-autosync will also
240 | print out valid ledger assertions based on your bank balances at the
241 | time of the sync. These otherwise empty transactions tell ledger that
242 | your balance *should* be something at a given time, and if not, ledger
243 | will fail with an error.
244 |
245 | 401k and investment accounts
246 | ----------------------------
247 |
248 | If you have a 401k account, ledger-autosync can help you to track the
249 | state of it. You will need OFX files (or an OFX protocol connection as
250 | set up by ofxclient) provided by your 401k.
251 |
252 | In general, your 401k account will consist of buy transactions,
253 | transfers and reinvestments. The type will be printed in the payee
254 | line after a colon (``:``)
255 |
256 | The buy transactions are your contributions to the 401k. These will be
257 | printed as follows:
258 |
259 | ::
260 |
261 | 2016/01/29 401k: buymf
262 | Assets:Retirement:401k 1.12345 FOOBAR @ $123.123456
263 | ; ofxid: 1234
264 | Income:Salary -$138.32
265 |
266 | This means that you bought (contributed) $138.32 worth of FOOBAR (your
267 | investment fund) at the price of $123.123456. The money to buy the
268 | investment came from your income. In ledger-autosync, the
269 | ``Assets:Retirement:401k`` account is the one specified using the
270 | ``--account`` command line, or configured in your ``ofxclient.ini``.
271 | The ``Income:Salary`` is specified by the ``--unknown-account``
272 | option.
273 |
274 | If the transaction is a “transfer” transaction, this usually means
275 | either a fee or a change in your investment option:
276 |
277 | ::
278 |
279 | 2014/06/30 401k: transfer: out
280 | Assets:Retirement:401k -1.61374 FOOBAR @ $123.123456
281 | ; ofxid: 1234
282 | Transfer $198.69
283 |
284 | You will need to examine your statements to determine if this was a
285 | fee or a real transfer back into your 401k.
286 |
287 | Another type of transaction is a “reinvest” transaction:
288 |
289 | ::
290 |
291 | 2014/06/30 401k: reinvest
292 | Assets:Retirement:401k 0.060702 FOOBAR @ $123.123456
293 | ; ofxid: 1234
294 | Income:Interest -$7.47
295 |
296 | This probably indicates a reinvestment of dividends. ledger-autosync
297 | will print ``Income:Interest`` as the other account.
298 |
299 | resync
300 | ------
301 |
302 | By default, ledger-autosync will process transactions backwards, and
303 | stop when it sees a transaction that is already in ledger. To force it
304 | to process all transactions up to the ``--max`` days back in time
305 | (default: 90), use the ``--resync`` option. This can be useful when
306 | increasing the ``--max`` option. For instance, if you previously
307 | synchronized 90 days and now want to get 180 days of transactions,
308 | ledger-autosync would stop before going back to 180 days without the
309 | ``--resync`` option.
310 |
311 | payee format
312 | ------------
313 |
314 | By default, ledger-autosync attempts to generate a decent payee line
315 | (the information that follows the date in a ledger transaction).
316 | Unfortunately, because of differences in preference and in the format
317 | of OFX files, it is not always possible to generate the user’s
318 | preferred payee format. ledger-autosync supports a ``payee-format``
319 | option that can be used to generate your preferred payee line. This
320 | option is of the format ``Text {memo}``, where ``memo`` is a
321 | substitution based on the value of the transaction. Available
322 | substitutions are ``memo``, ``payee``, ``txntype``, ``account`` and
323 | ``tferaction``. For example:
324 |
325 | ::
326 |
327 | $ ledger-autosync --payee-format "Memo: {memo}"
328 | 2011/03/31 Memo: DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
329 |
330 | This option is also available for CSV conversion. For CSV files, you
331 | can substitution any of the values of the rows in the CSV file by
332 | name. For instance, for Paypal files:
333 |
334 | ::
335 |
336 | $ ledger-autosync --payee-format "{Name} ({To Email Address})" -a Paypal paypal.csv
337 | 2016/06/04 Jane Doe (someone@example.net)
338 |
339 | python bindings
340 | ---------------
341 |
342 | If the ledger python bindings are available, ledger-autosync can use
343 | them if you pass in the ``--python`` argument. Note, however, they can
344 | be buggy, which is why they are disabled by default
345 |
346 | Plugin support
347 | --------------
348 |
349 | ledger-autosync has support for plugins. By placing python files a
350 | directory named ``~/.config/ledger-autosync/plugins/`` it should be
351 | possible to automatically load python files from there. You may place
352 | ``CsvCconverter`` subclasses here, which will be selected based on the
353 | columns in the CSV file being parsed and the FIELDSET of the CSV
354 | converters. You may also place a single ``OfxConverter`` in the plugin
355 | directory, which will be used in place of the stock ``OfxConverter``.
356 |
357 | Below is an example CSV converter, starting with the input CSV file:
358 |
359 | ::
360 |
361 | "Date","Name","Amount","Balance"
362 | "11/30/2016","Dividend","$1.06","$1,000“
363 |
364 | The following converter in the file ``~/.config/ledger-autosync/plugins/my.py``:
365 |
366 | ::
367 |
368 | from ledgerautosync.converter import CsvConverter, Posting, Transaction, Amount
369 | import datetime
370 | import re
371 |
372 | class SomeConverter(CsvConverter):
373 | FIELDSET = set(["Date", "Name", "Amount", "Balance"])
374 |
375 | def __init__(self, *args, **kwargs):
376 | super(SomeConverter, self).__init__(*args, **kwargs)
377 |
378 | def convert(self, row):
379 | md = re.match(r"^(\(?)\$([0-9,\.]+)", row['Amount'])
380 | amount = md.group(2).replace(",", "")
381 | if md.group(1) == "(":
382 | reverse = True
383 | else:
384 | reverse = False
385 | if reverse:
386 | account = 'expenses'
387 | else:
388 | account = 'income'
389 | return Transaction(
390 | date=datetime.datetime.strptime(row['Date'], "%m/%d/%Y"),
391 | payee=row['Name'],
392 | postings=[Posting(self.name, Amount(amount, '$', reverse=reverse)),
393 | Posting(account, Amount(amount, '$', reverse=not(reverse)))])
394 |
395 | Running ``ledger-autosync file.csv -a assets:bank`` will generate:
396 |
397 | ::
398 |
399 | 2016/11/30 Dividend
400 | assets:bank $1.06
401 | income -$1.06
402 |
403 | To ignore a row you can return ``None`` from your ``convert`` method.
404 | ledger-autosync will produce no output for that row.
405 |
406 | For more examples, see
407 | https://gitlab.com/egh/ledger-autosync/blob/master/ledgerautosync/converter.py#L421
408 | or the `example plugins directory `_.
409 |
410 | If you develop a converter that you think will be generally
411 | useful, please consider submitting a pull request.
412 |
413 | Testing
414 | -------
415 |
416 | ledger-autosync uses pytest for tests. To test, run pytest in the project directory. This will test the ledger, hledger and ledger-python interfaces. If hledger or the ledger-python interface is not found, these tests will be skipped.
417 |
--------------------------------------------------------------------------------
/examples/plugins/aib.py:
--------------------------------------------------------------------------------
1 | # ledger-autosync plugin for CSV files from AIB, an Irish bank.
2 |
3 | import datetime
4 | from decimal import Decimal
5 | import re
6 |
7 | from ledgerautosync.converter import (
8 | Amount,
9 | Converter,
10 | CsvConverter,
11 | Posting,
12 | Transaction,
13 | )
14 |
15 |
16 | class AIBConverter(CsvConverter):
17 | FIELDSET = set(
18 | [
19 | "Posted Account",
20 | "Posted Transactions Date",
21 | "Description",
22 | "Debit Amount",
23 | "Credit Amount",
24 | "Balance",
25 | "Transaction Type",
26 | ]
27 | )
28 |
29 | def __init__(self, *args, **kwargs):
30 | super(AIBConverter, self).__init__(*args, **kwargs)
31 |
32 | def convert(self, row):
33 | meta = {"csvid": self.get_csv_id(row)}
34 | debit = Decimal(row["Debit Amount"]) if row["Debit Amount"] else None
35 | credit = Decimal(row["Credit Amount"]) if row["Credit Amount"] else None
36 | if debit and debit > 0:
37 | posting_from = Posting(
38 | self.name, Amount(debit, "€", reverse=True), metadata=meta
39 | )
40 | posting_to = posting_from.clone_inverted("Expenses:Misc")
41 | elif credit and credit > 0:
42 | posting_from = Posting("Assets:Other", Amount(credit, "€", reverse=True))
43 | posting_to = posting_from.clone_inverted(self.name, metadata=meta)
44 | else:
45 | return ""
46 |
47 | payee = re.sub(
48 | r"[A-Za-z]+('[A-Za-z]+)?",
49 | lambda word: word.group(0).capitalize(),
50 | row["Description"],
51 | )
52 |
53 | return Transaction(
54 | date=datetime.datetime.strptime(
55 | row["Posted Transactions Date"], "%d/%m/%y"
56 | ),
57 | cleared=True,
58 | date_format="%Y-%m-%d",
59 | payee=payee,
60 | postings=[posting_to, posting_from],
61 | )
62 |
--------------------------------------------------------------------------------
/examples/plugins/firstdirect.py:
--------------------------------------------------------------------------------
1 | # ledger-autosync plugin for CSV files from First Direct, a UK bank.
2 | # The currency is fixed to GBP for that reason.
3 |
4 | import datetime
5 | import re
6 |
7 | from ledgerautosync.converter import Amount, CsvConverter, Posting, Transaction
8 |
9 |
10 | class SomeConverter(CsvConverter):
11 | FIELDSET = set(["Date", "Description", "Amount"])
12 |
13 | def __init__(self, *args, **kwargs):
14 | super(SomeConverter, self).__init__(*args, **kwargs)
15 |
16 | def convert(self, row):
17 | amount = row["Amount"]
18 | return Transaction(
19 | date=datetime.datetime.strptime(row["Date"], "%d/%m/%Y"),
20 | payee=row["Description"].strip(),
21 | postings=[
22 | Posting(self.name, Amount(amount, "GBP")),
23 | Posting(self.unknownaccount, Amount(amount, "GBP", reverse=True)),
24 | ],
25 | )
26 |
--------------------------------------------------------------------------------
/examples/plugins/n26.py:
--------------------------------------------------------------------------------
1 | # ledger-autosync plugin for CSV files from N26, a Berlin-based online bank.
2 |
3 | import datetime
4 | from decimal import Decimal
5 | import re
6 |
7 | from ledgerautosync.converter import (
8 | Amount,
9 | Converter,
10 | CsvConverter,
11 | Posting,
12 | Transaction,
13 | )
14 |
15 |
16 | class N26Converter(CsvConverter):
17 | FIELDSET = set(
18 | [
19 | "Date",
20 | "Payee",
21 | "Account number",
22 | "Transaction type",
23 | "Payment reference",
24 | "Amount (EUR)",
25 | "Amount (Foreign Currency)",
26 | "Type Foreign Currency",
27 | "Exchange Rate",
28 | ]
29 | )
30 |
31 | def __init__(self, *args, **kwargs):
32 | super(N26Converter, self).__init__(*args, **kwargs)
33 |
34 | def mk_currency(self, currency):
35 | if currency == "USD":
36 | currency = "$"
37 | elif currency == "GBP":
38 | currency = "£"
39 | elif currency == "EUR":
40 | currency = "€"
41 | return currency
42 |
43 | def convert(self, row):
44 | amt = Decimal(row["Amount (EUR)"])
45 | curr_foreign = self.mk_currency(row["Type Foreign Currency"] or "EUR")
46 | amt_foreign = (
47 | Decimal(row["Amount (Foreign Currency)"])
48 | if row["Amount (Foreign Currency)"]
49 | else amt
50 | )
51 | if amt < 0:
52 | reverse = False
53 | acct_from = self.name
54 | curr_from = self.mk_currency("EUR")
55 | amt_from = Amount(amt, curr_from, reverse=reverse)
56 | acct_to = "Expenses:Misc"
57 | curr_to = curr_foreign
58 | amt_to = Amount(amt_foreign, curr_to, reverse=not reverse)
59 | else:
60 | reverse = True
61 | acct_from = "Assets:Other"
62 | curr_from = curr_foreign
63 | amt_from = Amount(amt_foreign, curr_from, reverse=reverse)
64 | acct_to = self.name
65 | curr_to = self.mk_currency("EUR")
66 | amt_to = Amount(amt, curr_to, reverse=not reverse)
67 |
68 | payee = re.sub(
69 | r"[A-Za-z]+('[A-Za-z]+)?",
70 | lambda word: word.group(0).capitalize(),
71 | row["Payee"],
72 | )
73 | meta = {"csvid": self.get_csv_id(row)}
74 |
75 | posting_from = Posting(
76 | acct_from, amt_from, metadata=meta if acct_from == self.name else {}
77 | )
78 | posting_to = Posting(
79 | acct_to, amt_to, metadata=meta if acct_to == self.name else {}
80 | )
81 |
82 | return Transaction(
83 | date=datetime.datetime.strptime(row["Date"], "%Y-%m-%d"),
84 | cleared=True,
85 | date_format="%Y-%m-%d",
86 | payee=payee,
87 | postings=[posting_to, posting_from],
88 | )
89 |
--------------------------------------------------------------------------------
/examples/plugins/revolut.py:
--------------------------------------------------------------------------------
1 | # ledger-autosync plugin for CSV files from Revolut, a Lithuania-based online bank.
2 |
3 | import datetime
4 | from decimal import Decimal
5 | import re
6 |
7 | from ledgerautosync.converter import (
8 | Amount,
9 | Converter,
10 | CsvConverter,
11 | Posting,
12 | Transaction,
13 | )
14 |
15 |
16 | class RevolutConverter(CsvConverter):
17 | FIELDSET = set(
18 | [
19 | "Type",
20 | "Product",
21 | "Started Date",
22 | "Completed Date",
23 | "Description",
24 | "Amount",
25 | "Fee",
26 | "Currency",
27 | "State",
28 | "Balance",
29 | ]
30 | )
31 |
32 | def __init__(self, *args, **kwargs):
33 | super(RevolutConverter, self).__init__(*args, **kwargs)
34 |
35 | def mk_currency(self, currency):
36 | if currency == "USD":
37 | currency = "$"
38 | elif currency == "GBP":
39 | currency = "£"
40 | elif currency == "EUR":
41 | currency = "€"
42 | return currency
43 |
44 | def convert(self, row):
45 | amt = Decimal(row["Amount"])
46 | if not amt:
47 | return ""
48 | currency = self.mk_currency(row["Currency"])
49 | cleared = row["State"] == "COMPLETED"
50 | if row["Type"] == "TOPUP":
51 | reverse = True
52 | acct_from = "Assets:Other"
53 | amt_from = Amount(amt, currency, reverse=reverse)
54 | acct_to = self.name
55 | amt_to = Amount(amt, currency, reverse=not reverse)
56 | else:
57 | reverse = False
58 | acct_from = self.name
59 | amt_from = Amount(amt, currency, reverse=reverse)
60 | acct_to = "Expenses:Misc"
61 | amt_to = Amount(amt, currency, reverse=not reverse)
62 |
63 | payee = row["Description"]
64 | meta = {"csvid": self.get_csv_id(row)}
65 |
66 | posting_from = Posting(
67 | acct_from, amt_from, metadata=meta if acct_from == self.name else {}
68 | )
69 | posting_fee = (
70 | Posting(
71 | "Expenses:Bank Charges",
72 | Amount(Decimal(row["Fee"]), currency, reverse=True),
73 | )
74 | if row["Fee"] != "0.00"
75 | else None
76 | )
77 | posting_to = Posting(
78 | acct_to, amt_to, metadata=meta if acct_to == self.name else {}
79 | )
80 |
81 | date = datetime.datetime.strptime(row["Started Date"], "%Y-%m-%d %H:%M:%S")
82 | aux_date = (
83 | datetime.datetime.strptime(row["Completed Date"], "%Y-%m-%d %H:%M:%S")
84 | if row["Completed Date"]
85 | else None
86 | )
87 | if aux_date and (date.date() == aux_date.date()):
88 | aux_date = None
89 |
90 | postings = [posting_to, posting_from]
91 | if posting_fee:
92 | postings.append(posting_fee)
93 | postings.append(posting_fee.clone_inverted(self.name))
94 | return Transaction(
95 | date=date,
96 | cleared=cleared,
97 | aux_date=aux_date,
98 | date_format="%Y-%m-%d",
99 | payee=payee,
100 | postings=postings,
101 | )
102 |
--------------------------------------------------------------------------------
/examples/plugins/wise.py:
--------------------------------------------------------------------------------
1 | # ledger-autosync plugin for CSV files from Wise, formerly TransferWise.
2 |
3 | # You must download and sync a CSV statement for all currencies that
4 | # cover the same time period. Since Wise lists the transactions in
5 | # reverse chronological order, you might want to run with --reverse flag.
6 | # The code assumes some text is in English. Adjust mk_currency() and
7 | # anything else to suit. No warranty, YMMV, etc.
8 |
9 | import datetime
10 | from decimal import Decimal
11 | import re
12 |
13 | from ledgerautosync.converter import (
14 | Amount,
15 | Converter,
16 | CsvConverter,
17 | Posting,
18 | Transaction,
19 | )
20 |
21 |
22 | class WiseConverter(CsvConverter):
23 | FIELDSET = set(
24 | [
25 | "TransferWise ID",
26 | "Date",
27 | "Amount",
28 | "Currency",
29 | "Description",
30 | "Payment Reference",
31 | "Running Balance",
32 | "Exchange From",
33 | "Exchange To",
34 | "Exchange Rate",
35 | "Payer Name",
36 | "Payee Name",
37 | "Payee Account Number",
38 | "Merchant",
39 | "Card Last Four Digits",
40 | "Card Holder Full Name",
41 | "Attachment",
42 | "Note",
43 | "Total fees",
44 | ]
45 | )
46 |
47 | def __init__(self, *args, **kwargs):
48 | super(WiseConverter, self).__init__(*args, **kwargs)
49 |
50 | def mk_currency(self, currency):
51 | if currency == "USD":
52 | currency = "$"
53 | elif currency == "GBP":
54 | currency = "£"
55 | elif currency == "EUR":
56 | currency = "€"
57 | return currency
58 |
59 | def mk_amount(self, amt, currency, reverse=False):
60 | currency = self.mk_currency(currency)
61 | return Amount(Decimal(amt), currency, reverse=reverse)
62 |
63 | def convert(self, row):
64 | tid = row["TransferWise ID"]
65 | checknum = int(tid.split("-")[1])
66 | amt = Decimal(row["Amount"])
67 | acct_from = self.name
68 | curr_from = self.mk_currency(row["Currency"])
69 | acct_to = "Expenses:Misc"
70 | curr_to = curr_from
71 | amt_from = Amount(amt, curr_from)
72 | amt_to = Amount(amt, curr_to, reverse=True)
73 |
74 | fee_not_included = (
75 | tid.startswith("CARD-") and row["Currency"] == "USD" and row["Exchange To"]
76 | )
77 |
78 | if row["Exchange To"]:
79 | rate = Decimal(row["Exchange Rate"])
80 | curr = self.mk_currency(row["Currency"])
81 | curr_from = self.mk_currency(row["Exchange From"])
82 | curr_to = self.mk_currency(row["Exchange To"])
83 | if curr == curr_from:
84 | amt_from = Amount(amt, curr_from)
85 | # Card transactions from USD to other currencies do not consider the fees in the exchange rate
86 | amt_to = Amount(
87 | (
88 | amt
89 | + (
90 | Decimal(row["Total fees"])
91 | if tid.startswith("CARD-")
92 | and not (row["Currency"] == "USD" and row["Exchange To"])
93 | else Decimal(0)
94 | )
95 | )
96 | * rate,
97 | curr_to,
98 | reverse=True,
99 | )
100 | acct_from = self.name
101 | else:
102 | # Do not import this exchange from this statement; instead use the statement for the matching "from" currency
103 | if tid.startswith("BALANCE-"):
104 | return ""
105 | amt_from = Amount(amt / rate, curr_from, reverse=True)
106 | amt_to = Amount(amt, curr_to)
107 | acct_from = self.name if tid.startswith("BALANCE-") else "Expenses:Misc"
108 | if tid.startswith("BALANCE-"):
109 | acct_to = self.name
110 |
111 | if row["Description"].startswith("Wise Charges for:"):
112 | acct_to = "Expenses:Bank Charges"
113 |
114 | if tid.startswith("TRANSFER-"):
115 | payee = row["Payee Name"] or row["Payer Name"]
116 | elif tid.startswith("BALANCE-"):
117 | payee = row["Description"]
118 | else:
119 | payee = row["Merchant"]
120 |
121 | meta = {"csvid": self.get_csv_id(row)}
122 |
123 | posting_from = Posting(acct_from, amt_from, metadata=meta)
124 | posting_to = Posting(acct_to, amt_to)
125 |
126 | return Transaction(
127 | date=datetime.datetime.strptime(row["Date"], "%d-%m-%Y"),
128 | cleared=True,
129 | date_format="%Y-%m-%d",
130 | checknum=checknum,
131 | payee=payee,
132 | postings=[posting_to, posting_from],
133 | )
134 |
135 | def get_csv_id(self, row):
136 | fmt = (
137 | "wise.fee.{}"
138 | if row["Description"].startswith("Wise Charges for:")
139 | else "wise.{}"
140 | )
141 | return fmt.format(Converter.clean_id(row["TransferWise ID"]))
142 |
--------------------------------------------------------------------------------
/fix_ofxid.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | import re
3 | import sys
4 |
5 | first_line = False
6 | ofxline = None
7 |
8 | with open(sys.argv[1]) as f:
9 | for line in f.readlines():
10 | md = re.match(r"^(19|20)[0-9][0-9]", line)
11 | if md is not None:
12 | # Mark the next line as the first line in a txn
13 | first_line = True
14 | else:
15 | if first_line:
16 | first_line = False
17 | # Check if there is an ofxid on this line
18 | md = re.match(r"^\s+; ofxid:", line)
19 | if md is not None:
20 | ofxline = line
21 | continue
22 | # In every case except the one above where we call next, print the line
23 | sys.stdout.write(line)
24 | # We had a misplaced ofxid last, print it now
25 | if ofxline:
26 | sys.stdout.write(ofxline)
27 | ofxline = None
28 |
--------------------------------------------------------------------------------
/fixtures/accented_characters_latin1.ofx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/egh/ledger-autosync/ffa3fc66050f3c194a7716905103146063d7cad4/fixtures/accented_characters_latin1.ofx
--------------------------------------------------------------------------------
/fixtures/amazon.csv:
--------------------------------------------------------------------------------
1 | Order Date,Order ID,Title,Category,ASIN/ISBN,UNSPSC Code,Website,Release Date,Condition,Seller,Seller Credentials,List Price Per Unit,Purchase Price Per Unit,Quantity,Payment Instrument Type,Purchase Order Number,PO Line Number,Ordering Customer Email,Shipment Date,Shipping Address Name,Shipping Address Street 1,Shipping Address Street 2,Shipping Address City,Shipping Address State,Shipping Address Zip,Order Status,Carrier Name & Tracking Number,Item Subtotal,Item Subtotal Tax,Item Total,Tax Exemption Applied,Tax Exemption Type,Exemption Opt-Out,Buyer Name,Currency,Group Name
2 | 01/29/16,123-4567890-1234567,"Best Soap Ever",Health and Beauty,"B001XYZX00","01234567",Amazon.com,,new,Amazon.com,,$10.00,$10.00,2,"MasterCard - 1234",,,me@mailinator.com,01/31/16,M.E.,123 Main St,,Springfield,CA,00000-0000,Shipped,ONTRAC(XYZ),$20.00,$1.90,$21.90,,,,ME,USD,
3 |
--------------------------------------------------------------------------------
/fixtures/amazon2.csv:
--------------------------------------------------------------------------------
1 | Order Date,Order ID,Title,Category,ASIN/ISBN,UNSPSC Code,Website,Release Date,Condition,Seller,Seller Credentials,List Price Per Unit,Purchase Price Per Unit,Quantity,Payment Instrument Type,Purchase Order Number,PO Line Number,Ordering Customer Email,Shipment Date,Shipping Address Name,Shipping Address Street 1,Shipping Address Street 2,Shipping Address City,Shipping Address State,Shipping Address Zip,Order Status,Carrier Name & Tracking Number,Item Subtotal,Item Subtotal Tax,Item Total,Tax Exemption Applied,Tax Exemption Type,Exemption Opt-Out,Buyer Name,Currency,Group Name
2 | 06/05/17,111-1111111-1111111,"Test "" double quote",Kitchen,"A00AA1A11A",,Amazon.com,,new,,,$0.00,$9.99,1,"MasterCard - 1234",,,me@example.com,06/06/17,Me,123 Main St,,Springfield,CA,00000-0000,Shipped,AMZN_US(TBA000000000000),$9.99,$0.00,$9.99,,,,Me,USD,
3 |
--------------------------------------------------------------------------------
/fixtures/apostrophe.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 |
15 | 0
16 | INFO
17 |
18 | 20130525225731.258
19 | ENG
20 | 20050531060000.000
21 |
22 | FAKE
23 | 1101
24 |
25 | 51123
26 | 9774652
27 |
28 |
29 |
30 |
31 | 0
32 |
33 | 0
34 | INFO
35 |
36 |
37 | USD
38 |
39 | 5472369148
40 | 1452687~7
41 | CHECKING
42 |
43 |
44 | 20000101070000.000
45 | 20140920170000.000
46 |
47 | PAYMENT
48 | 20140920170000[0:GMT]
49 | -58.73
50 | 201409206
51 | 201409206
52 | REPLACE
53 | TRADER JOE'S #541 QPS
54 |
55 |
56 |
57 | 100.99
58 | 20140920170000.000
59 |
60 |
61 | 75.99
62 | 20140920170000.000
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/fixtures/checking-dynamic-account.lgr:
--------------------------------------------------------------------------------
1 | 2011/01/01 AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
2 | Assets:Foo -$10.00
3 | Expenses:Bar
4 |
5 | 2011/01/02 Generic
6 | Assets:Foo -$20.00
7 | Expenses:Foo
8 |
9 | 2011/02/02 Generic
10 | Assets:Foo -$15.00
11 | Expenses:Bar
12 |
--------------------------------------------------------------------------------
/fixtures/checking-partial.lgr:
--------------------------------------------------------------------------------
1 | 2011/03/31 DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
2 | ; ofxid: 1101.1452687~7.0000486
3 | Foo $0.01
4 | Bar -$0.01
5 |
6 | 2011/04/05 AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
7 | ; ofxid: 1101.1452687~7.0000487
8 | Foo -$34.51
9 | Bar $34.51
10 |
--------------------------------------------------------------------------------
/fixtures/checking-payee-match.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 |
15 | 0
16 | INFO
17 |
18 | 20130525225731.258
19 | ENG
20 | 20050531060000.000
21 |
22 | FAKE
23 | 1101
24 |
25 | 51123
26 | 9774652
27 |
28 |
29 |
30 |
31 | 0
32 |
33 | 0
34 | INFO
35 |
36 |
37 | USD
38 |
39 | 5472369148
40 | 1452687~7
41 | CHECKING
42 |
43 |
44 | 20000101070000.000
45 | 20130525060000.000
46 |
47 | DEBIT
48 | 20110331120000.000
49 | -0.01
50 | 0000489
51 | Payment to MATCH PAYEE and so on and so forth
52 |
53 |
54 |
55 |
56 | 100.99
57 | 20130525225731.258
58 |
59 |
60 | 75.99
61 | 20130525225731.258
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/fixtures/checking.lgr:
--------------------------------------------------------------------------------
1 | 2011/03/31 DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
2 | ; ofxid: 1101.1452687~7.0000486
3 | Assets:Foo $0.01
4 | Income:Bar -$0.01
5 |
6 | 2011/04/05 AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
7 | ; ofxid: 1101.1452687~7.0000487
8 | Assets:Foo -$34.51
9 | Expenses:Bar $34.51
10 |
11 | 2011/04/07 RETURNED CHECK FEE, CHECK # 319 FOR $45.33 ON 04/07/11
12 | ; ofxid: 1102.1452687~7.0000488
13 | Assets:Foo -$25.00
14 | Expenses:Bar $25.00
15 |
16 | 2011/04/07 Empty
17 | ; ofxid: empty
18 | Assets:Foo -$0.00
19 | Expenses:Bar $0.00
20 |
21 | 2011/03/31 TEST SLASH
22 | ; ofxid: 1_2
23 | Assets:Foo $0.01
24 | Income:Bar -$0.01
25 |
26 | 2011/03/31 PAYEE TEST:COLON
27 | Assets:Foo $0.01
28 | Income:Bar -$0.01
29 |
30 | 2011/03/31 PAYEE TEST/SLASH
31 | Assets:Foo $0.01
32 | Income:Bar -$0.01
33 |
34 | 2011/03/31 PAYEE TEST,COMMA
35 | Assets:Foo $0.01
36 | Income:Bar -$0.01
37 |
38 | 2011/03/31 PAYEE TEST.PERIOD
39 | Assets:Foo $0.01
40 | Income:Bar -$0.01
41 |
42 | 2011/03/31 PAYEE TEST*STAR
43 | Assets:Foo $0.01
44 | Income:Bar -$0.01
45 |
46 | 2011/03/31 PAYEE TEST#HASH
47 | Assets:Foo $0.01
48 | Income:Bar -$0.01
49 |
50 | 2011/03/31 PAYEE TEST"QUOTE
51 | Assets:Foo $0.01
52 | Income:Bar -$0.01
53 |
54 | 2011/03/31 Match Payee
55 | ; AutosyncPayee: Payment to MATCH PAYEE and so on and so forth
56 | Assets:Foo -$0.01
57 | Expenses:Bar $0.01
58 |
--------------------------------------------------------------------------------
/fixtures/checking.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 |
15 | 0
16 | INFO
17 |
18 | 20130525225731.258
19 | ENG
20 | 20050531060000.000
21 |
22 | FAKE
23 | 1101
24 |
25 | 51123
26 | 9774652
27 |
28 |
29 |
30 |
31 | 0
32 |
33 | 0
34 | INFO
35 |
36 |
37 | USD
38 |
39 | 5472369148
40 | 1452687~7
41 | CHECKING
42 |
43 |
44 | 20000101070000.000
45 | 20130525060000.000
46 |
47 | CREDIT
48 | 20110331120000.000
49 | 0.01
50 | 0000486
51 | DIVIDEND EARNED FOR PERIOD OF 03
52 | DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
53 |
54 |
55 | DEBIT
56 | 20110405120000.000
57 | -34.51
58 | 0000487
59 | AUTOMATIC WITHDRAWAL, ELECTRIC BILL
60 | AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
61 |
62 |
63 | CHECK
64 | 20110407120000.000
65 | -25.00
66 | 0000488
67 | 319
68 | RETURNED CHECK FEE, CHECK # 319
69 | RETURNED CHECK FEE, CHECK # 319 FOR $45.33 ON 04/07/11
70 |
71 |
72 |
73 | 100.99
74 | 20130525225731.258
75 |
76 |
77 | 75.99
78 | 20130525225731.258
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/fixtures/checking_order.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 |
15 | 0
16 | INFO
17 |
18 | 20130525225731.258
19 | ENG
20 | 20050531060000.000
21 |
22 | FAKE
23 | 1101
24 |
25 | 51123
26 | 9774652
27 |
28 |
29 |
30 |
31 | 0
32 |
33 | 0
34 | INFO
35 |
36 |
37 | USD
38 |
39 | 5472369148
40 | 1452687~7
41 | CHECKING
42 |
43 |
44 | 20000101070000.000
45 | 20130525060000.000
46 |
47 | CHECK
48 | 20110407120000.000
49 | -25.00
50 | 0000488
51 | 319
52 | RETURNED CHECK FEE, CHECK # 319
53 | RETURNED CHECK FEE, CHECK # 319 FOR $45.33 ON 04/07/11
54 |
55 |
56 | DEBIT
57 | 20110405120000.000
58 | -34.51
59 | 0000487
60 | AUTOMATIC WITHDRAWAL, ELECTRIC BILL
61 | AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
62 |
63 |
64 | CREDIT
65 | 20110331120000.000
66 | 0.01
67 | 0000486
68 | DIVIDEND EARNED FOR PERIOD OF 03
69 | DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
70 |
71 |
72 |
73 | 100.99
74 | 20130525225731.258
75 |
76 |
77 | 75.99
78 | 20130525225731.258
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/fixtures/comments.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 |
15 | 0
16 | INFO
17 |
18 | 20130525225731.258
19 | ENG
20 | 20050531060000.000
21 |
22 | FAKE
23 | 1101
24 |
25 | 51123
26 | 9774652
27 |
28 |
29 |
30 |
31 | 0
32 |
33 | 0
34 | INFO
35 |
36 |
37 | USD
38 |
39 | 5472369148
40 | 1452687~7
41 | CHECKING
42 |
43 |
44 | 20000101070000.000
45 | 20161027160000.000
46 |
47 | CREDIT
48 | 20161027160000.000
49 | 123.45
50 | FITID20161027123.45ABCDE
51 | AMAZON MKTPLACE PMTS AMZN.COM/BI
52 |
53 |
54 | DEBIT
55 | 20161027160000.000
56 | -0
57 | FITID20161027-0.0ABCDE
58 | XXXEXTRASTUFFXXX
59 |
60 |
61 |
62 | 100.99
63 | 20161027160000.000
64 |
65 |
66 | 75.99
67 | 20161027160000.000
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/fixtures/cusip.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:d0a0380757f14baba37a454823980c7c
10 |
11 |
12 |
13 |
14 |
15 | 0
16 | INFO
17 | SUCCESS
18 |
19 | 20161008122549.359[-4:EDT]
20 | ENG
21 |
22 | fidelity.com
23 | 7776
24 |
25 |
26 |
27 |
28 |
29 | 4335f294b6fb406ba99d3bf473b89e8a
30 |
31 | 0
32 | INFO
33 | SUCCESS
34 |
35 |
36 | 20161008033008.000[-4:EDT]
37 | USD
38 |
39 | fidelity.com
40 | 123456789
41 |
42 |
43 | 20160908000000.000[-4:EDT]
44 | 20161008122549.439[-4:EDT]
45 |
46 |
47 |
48 | 123456789000987654321
49 | 20161005000000.000[-4:EDT]
50 | YOU BOUGHT PROSPECTUS
51 |
52 |
53 | 957904675
54 | CUSIP
55 |
56 | +0000000001000.00000
57 | 000000047.860000000
58 | +00000000000000.0000
59 | +00000000000000.0000
60 | -00000000047860.0000
61 |
62 | 1.00
63 | USD
64 |
65 | CASH
66 | CASH
67 |
68 | BUY
69 |
70 |
71 |
72 | 123456789000987654322
73 | 20160929000000.000[-4:EDT]
74 | DIVIDEND RECEIVED
75 |
76 |
77 | 957904675
78 | CUSIP
79 |
80 | DIV
81 | +00000000000005.2300
82 | CASH
83 | CASH
84 |
85 | 1.00
86 | USD
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | 957904675
95 | CUSIP
96 |
97 | CASH
98 | LONG
99 | 2000.00000
100 | 47.8600000
101 | +00000095720.00
102 | 20161008033008.000[-4:EDT]
103 |
104 | 1.0
105 | USD
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | 957904675
119 | CUSIP
120 |
121 | BLACKROCK HEALTH SCIENCES OPP PRT A
122 | SHSAX
123 | 47.8600000
124 | 20161008033008.000[-4:EDT]
125 |
126 | 1.000
127 | USD
128 |
129 |
130 | OTHER
131 | 20161008033008.000[-4:EDT]
132 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/fixtures/empty.lgr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/egh/ledger-autosync/ffa3fc66050f3c194a7716905103146063d7cad4/fixtures/empty.lgr
--------------------------------------------------------------------------------
/fixtures/fidelity-one-dtsettle.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:a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0
10 |
11 | 0INFOSUCCESS
20120908190849.317[-4:EDT]ENGfidelity.com7776a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a00INFOSUCCESS
20120908033034.000[-4:EDT]USDfidelity.com0123456789020120710000000.000[-4:EDT]20120908190849.555[-4:EDT]012345678902020112012072020120720000000.000[-4:EDT]YOU BOUGHT458140100CUSIP+0000000000100.00000000000025.635000000+00000000000007.9500+00000000000000.0000-00000000002571.45001.00USDCASHCASHBUY 012345678902090112012072720120727000000.000[-4:EDT]G7945E105CUSIP+0000000000128.00000000000039.390900000+00000000000007.9500+00000000000000.0000-00000000005049.99001.00USDCASHCASHBUY 012345678902090122012072720120727000000.000[-4:EDT]20120727000000.000[-4:EDT]YOU BOUGHT431571108CUSIP+0000000000115.00000000000017.250000000+00000000000007.9500+00000000000000.0000-00000000001991.70001.00USDCASHCASHBUY 012345678902130112012073120120731000000.000[-4:EDT]YOU BOUGHT19421R200CUSIP+0000000000069.00000000000014.469900000+00000000000007.9500+00000000000000.0000-00000000001006.37001.00USDCASHCASHBUY 012345678902130162012073120120731000000.000[-4:EDT]YOU BOUGHT98417P105CUSIP+0000000000386.00000000000002.588700000+00000000000007.9500+00000000000000.0000-00000000001007.19001.00USDCASHCASHBUY 012345678902350122012082020120820000000.000[-4:EDT]REINVESTMENT98417P105CUSIP+0000000000004.90900000000002.947400000+00000000000000.0000+00000000000000.0000-00000000000014.47001.00USDCASHCASHBUY 012345678902440112012083120120831000000.000[-4:EDT]REINVESTMENT19421R200CUSIP+0000000000001.57300000000014.257000000+00000000000000.0000+00000000000000.0000-00000000000022.43001.00USDCASHCASHBUY 012345678902480112012090120120901000000.000[-4:EDT]REINVESTMENT458140100CUSIP+0000000000000.91100000000024.705500000+00000000000000.0000+00000000000000.0000-00000000000022.50001.00USDCASHCASHBUY 012345678902130152012073120120731000000.000[-4:EDT]DIVIDEND RECEIVED78462F103CUSIPDIV+00000000000005.5300CASHCASH1.00USD 012345678902350132012082020120820000000.000[-4:EDT]DIVIDEND RECEIVED98417P105CUSIPDIV+00000000000015.4400CASHCASH1.00USD 012345678902440122012083120120831000000.000[-4:EDT]DIVIDEND RECEIVED19421R200CUSIPDIV+00000000000022.4300CASHCASH1.00USD 012345678902480122012090120120901000000.000[-4:EDT]DIVIDEND RECEIVED458140100CUSIPDIV+00000000000022.5000CASHCASH1.00USD 012345678902090132012072720120727000000.000[-4:EDT]YOU SOLD78462F103CUSIP-0000000000008.00000000000137.160000000+00000000000007.9500+00000000000000.0000+00000000001089.30001.00USDCASHCASHSELL 012345678902140142012080120120801000000.000[-4:EDT]IN LIEU OF FRX SHARE78462F103CUSIP-0000000000000.03500000000137.142857143+00000000000000.0000+00000000000000.0000+00000000000004.80001.00USDCASHCASHSELL DEP20120731000000.000[-4:EDT]+00000000000000.24000123456789021301320120731INTEREST EARNEDINTEREST EARNED1.00USD CASH OTHER20120820000000.000[-4:EDT]-00000000000000.97000123456789023501120120820LATE SETTLEMENT FEELATE SETTLEMENT FEE1.00USD CASH DEP20120831000000.000[-4:EDT]+00000000000000.16000123456789024401420120831INTEREST EARNEDINTEREST EARNED1.00USD CASH G7945E105CUSIPCASHLONG128.0000040.8700000+00000005231.3620120908033034.000[-4:EDT]1.0USD19421R200CUSIPCASHLONG70.5730014.3200000+00000001010.6020120908033034.000[-4:EDT]1.0USD431571108CUSIPCASHLONG115.0000018.9300000+00000002176.9520120908033034.000[-4:EDT]1.0USD458140100CUSIPCASHLONG100.9110024.1900000+00000002441.0320120908033034.000[-4:EDT]1.0USD756577102CUSIPCASHLONG50.0000059.1500000+00000002957.5020120908033034.000[-4:EDT]1.0USD98417P105CUSIPCASHLONG390.909002.8200000+00000001102.3620120908033034.000[-4:EDT]1.0USD18073.98+00000000000.00+00000000000.00+00000000000.00NetworthThe net market value of all long and short positions in the accountDOLLAR32993.7920120908033034.000[-4:EDT]1.000USDMargin EquityThe margin market value less any margin debit balanceDOLLAR0.020120908033034.000[-4:EDT]1.000USDMargin Equity PercentageMargin equity / market value of long and short positionsPERCENT0.020120908033034.000[-4:EDT]1.000USDCash Debit BalanceCash Debit BalanceDOLLAR0.020120908033034.000[-4:EDT]1.000USDTotal Money MarketsThe total value of all money market positions in the cash accountDOLLAR18073.9820120908033034.000[-4:EDT]1.000USDHouse SurplusEquity amount above house requirementsDOLLAR0.020120908033034.000[-4:EDT]1.000USDNYSE SurplusEquity amount above exchange requirementsDOLLAR0.020120908033034.000[-4:EDT]1.000USDFederal SurplusAmount above federal requirementsDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - EquitiesAmount of equities you can buy on margin without generating a margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - Municipal BondsAmount of municipal bonds you can buy on margin without generating a margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - Government BondsAmount of government bonds you can buy on margin without generating a margin calDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - Corporate BondsAmount you can buy of corporate bonds on margin with no margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDOption Market ValueThe market value of all options in the accountDOLLAR0.020120908033034.000[-4:EDT]1.000USDOption In The Money AmountThe in-the-money amount on covered optionsDOLLAR0.020120908033034.000[-4:EDT]1.000USDCash Market valueTotal value of all cash account positionsDOLLAR14919.820120908033034.000[-4:EDT]1.000USDMargin Market ValueTotal value of positions in margin less in-the-money amount of covered optionsDOLLAR0.020120908033034.000[-4:EDT]1.000USDShort Market ValueTotal value of short positions less in-the-money amount of covered put optionsDOLLAR0.020120908033034.000[-4:EDT]1.000USDAvailable to BorrowCash amount that can be borrowed without generating a margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDG7945E105CUSIPSEADRILL LTD USD2SDRL20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]19421R200CUSIPCOLLECTORS UNIVERSE INCCLCT20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]431571108CUSIPHILLENBRAND INC COMHI20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]458140100CUSIPINTEL CORPINTC20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]756577102CUSIPRED HAT INCRHT20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]98417P105CUSIPXINYUAN REAL ESTATE ADR EACH REPR 2 ORD SHSXIN20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]78462F103CUSIPSPDR S&P 500 ETF TRUST UNIT SER 1 S&PSPY20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]
12 |
--------------------------------------------------------------------------------
/fixtures/fidelity.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:a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0
10 |
11 | 0INFOSUCCESS
20120908190849.317[-4:EDT]ENGfidelity.com7776a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a00INFOSUCCESS
20120908033034.000[-4:EDT]USDfidelity.com0123456789020120710000000.000[-4:EDT]20120908190849.555[-4:EDT]012345678902020112012072020120720000000.000[-4:EDT]YOU BOUGHT458140100CUSIP+0000000000100.00000000000025.635000000+00000000000007.9500+00000000000000.0000-00000000002571.45001.00USDCASHCASHBUY 012345678902090112012072720120727000000.000[-4:EDT]G7945E105CUSIP+0000000000128.00000000000039.390900000+00000000000007.9500+00000000000000.0000-00000000005049.99001.00USDCASHCASHBUY 012345678902090122012072720120727000000.000[-4:EDT]YOU BOUGHT431571108CUSIP+0000000000115.00000000000017.250000000+00000000000007.9500+00000000000000.0000-00000000001991.70001.00USDCASHCASHBUY 012345678902130112012073120120731000000.000[-4:EDT]YOU BOUGHT19421R200CUSIP+0000000000069.00000000000014.469900000+00000000000007.9500+00000000000000.0000-00000000001006.37001.00USDCASHCASHBUY 012345678902130162012073120120731000000.000[-4:EDT]YOU BOUGHT98417P105CUSIP+0000000000386.00000000000002.588700000+00000000000007.9500+00000000000000.0000-00000000001007.19001.00USDCASHCASHBUY 012345678902350122012082020120820000000.000[-4:EDT]REINVESTMENT98417P105CUSIP+0000000000004.90900000000002.947400000+00000000000000.0000+00000000000000.0000-00000000000014.47001.00USDCASHCASHBUY 012345678902440112012083120120831000000.000[-4:EDT]REINVESTMENT19421R200CUSIP+0000000000001.57300000000014.257000000+00000000000000.0000+00000000000000.0000-00000000000022.43001.00USDCASHCASHBUY 012345678902480112012090120120901000000.000[-4:EDT]REINVESTMENT458140100CUSIP+0000000000000.91100000000024.705500000+00000000000000.0000+00000000000000.0000-00000000000022.50001.00USDCASHCASHBUY 012345678902130152012073120120731000000.000[-4:EDT]DIVIDEND RECEIVED78462F103CUSIPDIV+00000000000005.5300CASHCASH1.00USD 012345678902350132012082020120820000000.000[-4:EDT]DIVIDEND RECEIVED98417P105CUSIPDIV+00000000000015.4400CASHCASH1.00USD 012345678902440122012083120120831000000.000[-4:EDT]DIVIDEND RECEIVED19421R200CUSIPDIV+00000000000022.4300CASHCASH1.00USD 012345678902480122012090120120901000000.000[-4:EDT]DIVIDEND RECEIVED458140100CUSIPDIV+00000000000022.5000CASHCASH1.00USD 012345678902090132012072720120727000000.000[-4:EDT]YOU SOLD78462F103CUSIP-0000000000008.00000000000137.160000000+00000000000007.9500+00000000000000.0000+00000000001089.30001.00USDCASHCASHSELL 012345678902140142012080120120801000000.000[-4:EDT]IN LIEU OF FRX SHARE78462F103CUSIP-0000000000000.03500000000137.142857143+00000000000000.0000+00000000000000.0000+00000000000004.80001.00USDCASHCASHSELL DEP20120731000000.000[-4:EDT]+00000000000000.24000123456789021301320120731INTEREST EARNEDINTEREST EARNED1.00USD CASH OTHER20120820000000.000[-4:EDT]-00000000000000.97000123456789023501120120820LATE SETTLEMENT FEELATE SETTLEMENT FEE1.00USD CASH DEP20120831000000.000[-4:EDT]+00000000000000.16000123456789024401420120831INTEREST EARNEDINTEREST EARNED1.00USD CASH G7945E105CUSIPCASHLONG128.0000040.8700000+00000005231.3620120908033034.000[-4:EDT]1.0USD19421R200CUSIPCASHLONG70.5730014.3200000+00000001010.6020120908033034.000[-4:EDT]1.0USD431571108CUSIPCASHLONG115.0000018.9300000+00000002176.9520120908033034.000[-4:EDT]1.0USD458140100CUSIPCASHLONG100.9110024.1900000+00000002441.0320120908033034.000[-4:EDT]1.0USD756577102CUSIPCASHLONG50.0000059.1500000+00000002957.5020120908033034.000[-4:EDT]1.0USD98417P105CUSIPCASHLONG390.909002.8200000+00000001102.3620120908033034.000[-4:EDT]1.0USD18073.98+00000000000.00+00000000000.00+00000000000.00NetworthThe net market value of all long and short positions in the accountDOLLAR32993.7920120908033034.000[-4:EDT]1.000USDMargin EquityThe margin market value less any margin debit balanceDOLLAR0.020120908033034.000[-4:EDT]1.000USDMargin Equity PercentageMargin equity / market value of long and short positionsPERCENT0.020120908033034.000[-4:EDT]1.000USDCash Debit BalanceCash Debit BalanceDOLLAR0.020120908033034.000[-4:EDT]1.000USDTotal Money MarketsThe total value of all money market positions in the cash accountDOLLAR18073.9820120908033034.000[-4:EDT]1.000USDHouse SurplusEquity amount above house requirementsDOLLAR0.020120908033034.000[-4:EDT]1.000USDNYSE SurplusEquity amount above exchange requirementsDOLLAR0.020120908033034.000[-4:EDT]1.000USDFederal SurplusAmount above federal requirementsDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - EquitiesAmount of equities you can buy on margin without generating a margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - Municipal BondsAmount of municipal bonds you can buy on margin without generating a margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - Government BondsAmount of government bonds you can buy on margin without generating a margin calDOLLAR0.020120908033034.000[-4:EDT]1.000USDBuying Power - Corporate BondsAmount you can buy of corporate bonds on margin with no margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDOption Market ValueThe market value of all options in the accountDOLLAR0.020120908033034.000[-4:EDT]1.000USDOption In The Money AmountThe in-the-money amount on covered optionsDOLLAR0.020120908033034.000[-4:EDT]1.000USDCash Market valueTotal value of all cash account positionsDOLLAR14919.820120908033034.000[-4:EDT]1.000USDMargin Market ValueTotal value of positions in margin less in-the-money amount of covered optionsDOLLAR0.020120908033034.000[-4:EDT]1.000USDShort Market ValueTotal value of short positions less in-the-money amount of covered put optionsDOLLAR0.020120908033034.000[-4:EDT]1.000USDAvailable to BorrowCash amount that can be borrowed without generating a margin callDOLLAR0.020120908033034.000[-4:EDT]1.000USDG7945E105CUSIPSEADRILL LTD USD2SDRL20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]19421R200CUSIPCOLLECTORS UNIVERSE INCCLCT20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]431571108CUSIPHILLENBRAND INC COMHI20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]458140100CUSIPINTEL CORPINTC20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]756577102CUSIPRED HAT INCRHT20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]98417P105CUSIPXINYUAN REAL ESTATE ADR EACH REPR 2 ORD SHSXIN20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]78462F103CUSIPSPDR S&P 500 ETF TRUST UNIT SER 1 S&PSPY20120908033034.000[-4:EDT]1.000USD COMMON20120908033034.000[-4:EDT]
12 |
--------------------------------------------------------------------------------
/fixtures/fidelity_fee.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:a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0
10 |
11 |
12 |
13 |
14 |
15 | 0
16 | INFO
17 | SUCCESS
18 |
19 | 20120908190849.317[-4:EDT]
20 | ENG
21 |
22 | fidelity.com
23 | 7776
24 |
25 |
26 |
27 |
28 |
29 | a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0
30 |
31 | 0
32 | INFO
33 | SUCCESS
34 |
35 |
36 | 20120908033034.000[-4:EDT]
37 | USD
38 |
39 | fidelity.com
40 | 01234567890
41 |
42 |
43 | 20120710000000.000[-4:EDT]
44 | 20120908190849.555[-4:EDT]
45 |
46 |
47 |
48 | 0123456789021401420120801
49 | 20120801000000.000[-4:EDT]
50 | SELL
51 |
52 |
53 | 929042109
54 | CUSIP
55 |
56 | -100.0
57 | 32.3944
58 | 0.07
59 | 3239.37
60 | CASH
61 | CASH
62 |
63 | SELL
64 |
65 |
66 |
67 |
68 | 987654321
69 | 20200522160000.000[-5:EST]
70 | 20200526160000.000[-5:EST]
71 | SELL
72 |
73 |
74 | Z9977810Z
75 | CUSIP
76 |
77 | -1.0
78 | 8.27
79 | 1.0
80 | 0.02
81 | 825.98
82 | CASH
83 | CASH
84 |
85 | SELLTOCLOSE
86 | 0
87 |
88 |
89 |
90 | DEP
91 | 20120731000000.000[-4:EDT]
92 | +00000000000000.2400
93 | 0123456789021301320120731
94 | INTEREST EARNED
95 | INTEREST EARNED
96 |
97 | 1.00
98 | USD
99 |
100 |
101 | CASH
102 |
103 |
104 |
105 | OTHER
106 | 20120820000000.000[-4:EDT]
107 | -00000000000000.9700
108 | 0123456789023501120120820
109 | LATE SETTLEMENT FEE
110 | LATE SETTLEMENT FEE
111 |
112 | 1.00
113 | USD
114 |
115 |
116 | CASH
117 |
118 |
119 |
120 | DEP
121 | 20120831000000.000[-4:EDT]
122 | +00000000000000.1600
123 | 0123456789024401420120831
124 | INTEREST EARNED
125 | INTEREST EARNED
126 |
127 | 1.00
128 | USD
129 |
130 |
131 | CASH
132 |
133 |
134 |
135 |
136 |
137 |
138 | G7945E105
139 | CUSIP
140 |
141 | CASH
142 | LONG
143 | 128.00000
144 | 40.8700000
145 | +00000005231.36
146 | 20120908033034.000[-4:EDT]
147 |
148 | 1.0
149 | USD
150 |
151 |
152 |
153 |
154 |
155 |
156 | 19421R200
157 | CUSIP
158 |
159 | CASH
160 | LONG
161 | 70.57300
162 | 14.3200000
163 | +00000001010.60
164 | 20120908033034.000[-4:EDT]
165 |
166 | 1.0
167 | USD
168 |
169 |
170 |
171 |
172 |
173 |
174 | 431571108
175 | CUSIP
176 |
177 | CASH
178 | LONG
179 | 115.00000
180 | 18.9300000
181 | +00000002176.95
182 | 20120908033034.000[-4:EDT]
183 |
184 | 1.0
185 | USD
186 |
187 |
188 |
189 |
190 |
191 |
192 | 458140100
193 | CUSIP
194 |
195 | CASH
196 | LONG
197 | 100.91100
198 | 24.1900000
199 | +00000002441.03
200 | 20120908033034.000[-4:EDT]
201 |
202 | 1.0
203 | USD
204 |
205 |
206 |
207 |
208 |
209 |
210 | 756577102
211 | CUSIP
212 |
213 | CASH
214 | LONG
215 | 50.00000
216 | 59.1500000
217 | +00000002957.50
218 | 20120908033034.000[-4:EDT]
219 |
220 | 1.0
221 | USD
222 |
223 |
224 |
225 |
226 |
227 |
228 | 98417P105
229 | CUSIP
230 |
231 | CASH
232 | LONG
233 | 390.90900
234 | 2.8200000
235 | +00000001102.36
236 | 20120908033034.000[-4:EDT]
237 |
238 | 1.0
239 | USD
240 |
241 |
242 |
243 |
244 |
245 | 18073.98
246 | +00000000000.00
247 | +00000000000.00
248 | +00000000000.00
249 |
250 |
251 | Networth
252 | The net market value of all long and short positions in the account
253 | DOLLAR
254 | 32993.79
255 | 20120908033034.000[-4:EDT]
256 |
257 | 1.000
258 | USD
259 |
260 |
261 |
262 | Margin Equity
263 | The margin market value less any margin debit balance
264 | DOLLAR
265 | 0.0
266 | 20120908033034.000[-4:EDT]
267 |
268 | 1.000
269 | USD
270 |
271 |
272 |
273 | Margin Equity Percentage
274 | Margin equity / market value of long and short positions
275 | PERCENT
276 | 0.0
277 | 20120908033034.000[-4:EDT]
278 |
279 | 1.000
280 | USD
281 |
282 |
283 |
284 | Cash Debit Balance
285 | Cash Debit Balance
286 | DOLLAR
287 | 0.0
288 | 20120908033034.000[-4:EDT]
289 |
290 | 1.000
291 | USD
292 |
293 |
294 |
295 | Total Money Markets
296 | The total value of all money market positions in the cash account
297 | DOLLAR
298 | 18073.98
299 | 20120908033034.000[-4:EDT]
300 |
301 | 1.000
302 | USD
303 |
304 |
305 |
306 | House Surplus
307 | Equity amount above house requirements
308 | DOLLAR
309 | 0.0
310 | 20120908033034.000[-4:EDT]
311 |
312 | 1.000
313 | USD
314 |
315 |
316 |
317 | NYSE Surplus
318 | Equity amount above exchange requirements
319 | DOLLAR
320 | 0.0
321 | 20120908033034.000[-4:EDT]
322 |
323 | 1.000
324 | USD
325 |
326 |
327 |
328 | Federal Surplus
329 | Amount above federal requirements
330 | DOLLAR
331 | 0.0
332 | 20120908033034.000[-4:EDT]
333 |
334 | 1.000
335 | USD
336 |
337 |
338 |
339 | Buying Power - Equities
340 | Amount of equities you can buy on margin without generating a margin call
341 | DOLLAR
342 | 0.0
343 | 20120908033034.000[-4:EDT]
344 |
345 | 1.000
346 | USD
347 |
348 |
349 |
350 | Buying Power - Municipal Bonds
351 | Amount of municipal bonds you can buy on margin without generating a margin call
352 | DOLLAR
353 | 0.0
354 | 20120908033034.000[-4:EDT]
355 |
356 | 1.000
357 | USD
358 |
359 |
360 |
361 | Buying Power - Government Bonds
362 | Amount of government bonds you can buy on margin without generating a margin cal
363 | DOLLAR
364 | 0.0
365 | 20120908033034.000[-4:EDT]
366 |
367 | 1.000
368 | USD
369 |
370 |
371 |
372 | Buying Power - Corporate Bonds
373 | Amount you can buy of corporate bonds on margin with no margin call
374 | DOLLAR
375 | 0.0
376 | 20120908033034.000[-4:EDT]
377 |
378 | 1.000
379 | USD
380 |
381 |
382 |
383 | Option Market Value
384 | The market value of all options in the account
385 | DOLLAR
386 | 0.0
387 | 20120908033034.000[-4:EDT]
388 |
389 | 1.000
390 | USD
391 |
392 |
393 |
394 | Option In The Money Amount
395 | The in-the-money amount on covered options
396 | DOLLAR
397 | 0.0
398 | 20120908033034.000[-4:EDT]
399 |
400 | 1.000
401 | USD
402 |
403 |
404 |
405 | Cash Market value
406 | Total value of all cash account positions
407 | DOLLAR
408 | 14919.8
409 | 20120908033034.000[-4:EDT]
410 |
411 | 1.000
412 | USD
413 |
414 |
415 |
416 | Margin Market Value
417 | Total value of positions in margin less in-the-money amount of covered options
418 | DOLLAR
419 | 0.0
420 | 20120908033034.000[-4:EDT]
421 |
422 | 1.000
423 | USD
424 |
425 |
426 |
427 | Short Market Value
428 | Total value of short positions less in-the-money amount of covered put options
429 | DOLLAR
430 | 0.0
431 | 20120908033034.000[-4:EDT]
432 |
433 | 1.000
434 | USD
435 |
436 |
437 |
438 | Available to Borrow
439 | Cash amount that can be borrowed without generating a margin call
440 | DOLLAR
441 | 0.0
442 | 20120908033034.000[-4:EDT]
443 |
444 | 1.000
445 | USD
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 | G7945E105
459 | CUSIP
460 |
461 | SEADRILL LTD USD2
462 | SDRL
463 | 20120908033034.000[-4:EDT]
464 |
465 | 1.000
466 | USD
467 |
468 |
469 | COMMON
470 | 20120908033034.000[-4:EDT]
471 |
472 |
473 |
474 |
475 | 19421R200
476 | CUSIP
477 |
478 | COLLECTORS UNIVERSE INC
479 | CLCT
480 | 20120908033034.000[-4:EDT]
481 |
482 | 1.000
483 | USD
484 |
485 |
486 | COMMON
487 | 20120908033034.000[-4:EDT]
488 |
489 |
490 |
491 |
492 | 431571108
493 | CUSIP
494 |
495 | HILLENBRAND INC COM
496 | HI
497 | 20120908033034.000[-4:EDT]
498 |
499 | 1.000
500 | USD
501 |
502 |
503 | COMMON
504 | 20120908033034.000[-4:EDT]
505 |
506 |
507 |
508 |
509 | 458140100
510 | CUSIP
511 |
512 | INTEL CORP
513 | INTC
514 | 20120908033034.000[-4:EDT]
515 |
516 | 1.000
517 | USD
518 |
519 |
520 | COMMON
521 | 20120908033034.000[-4:EDT]
522 |
523 |
524 |
525 |
526 | 756577102
527 | CUSIP
528 |
529 | RED HAT INC
530 | RHT
531 | 20120908033034.000[-4:EDT]
532 |
533 | 1.000
534 | USD
535 |
536 |
537 | COMMON
538 | 20120908033034.000[-4:EDT]
539 |
540 |
541 |
542 |
543 | 98417P105
544 | CUSIP
545 |
546 | XINYUAN REAL ESTATE ADR EACH REPR 2 ORD SHS
547 | XIN
548 | 20120908033034.000[-4:EDT]
549 |
550 | 1.000
551 | USD
552 |
553 |
554 | COMMON
555 | 20120908033034.000[-4:EDT]
556 |
557 |
558 |
559 |
560 | 78462F103
561 | CUSIP
562 |
563 | SPDR S&P 500 ETF TRUST UNIT SER 1 S&P
564 | SPY
565 | 20120908033034.000[-4:EDT]
566 |
567 | 1.000
568 | USD
569 |
570 |
571 | COMMON
572 | 20120908033034.000[-4:EDT]
573 |
574 |
575 |
576 |
577 |
--------------------------------------------------------------------------------
/fixtures/income.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 |
15 | 0
16 | INFO
17 | SUCCESS
18 |
19 | 20150909084609.717[-6:MDT]
20 | ENG
21 |
22 | EXAMPLE
23 | 1234
24 |
25 | 1234
26 |
27 |
28 |
29 |
30 | 0
31 |
32 | 0
33 | INFO
34 | SUCCESS
35 |
36 |
37 | 20140630000000.000[-6:MDT]
38 | USD
39 |
40 | example.org
41 | 12345678.123456-01
42 |
43 |
44 | 20160908000000.000[-4:EDT]
45 | 20161008121253.321[-4:EDT]
46 |
47 |
48 | redacted
49 | 20161012000000.000[-4:EDT]
50 | DIVIDEND RECEIVED
51 |
52 |
53 | cusip_redacted
54 | CUSIP
55 |
56 | DIV
57 | +00000000001234.5600
58 | CASH
59 | CASH
60 |
61 | 1.00
62 | USD
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/fixtures/investment_401k.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 |
15 | 0
16 | INFO
17 | SUCCESS
18 |
19 | 20150909084609.717[-6:MDT]
20 | ENG
21 |
22 | EXAMPLE
23 | 1234
24 |
25 | 1234
26 |
27 |
28 |
29 |
30 | 0
31 |
32 | 0
33 | INFO
34 | SUCCESS
35 |
36 |
37 | 20140630000000.000[-6:MDT]
38 | USD
39 |
40 | example.org
41 | 12345678.123456-01
42 |
43 |
44 | 20140401000000.000[-6:MDT]
45 | 20140630000000.000[-6:MDT]
46 |
47 |
48 |
49 | 1
50 | 20140617000000.000[-6:MDT]
51 |
52 |
53 | FOO
54 | PRIVATE
55 |
56 | 8.846699
57 | 22.2908
58 | -197.2
59 | OTHER
60 | OTHER
61 |
62 | BUY
63 |
64 |
65 |
66 | 2
67 | 20140630000000.000[-6:MDT]
68 |
69 |
70 | BAR
71 | PRIVATE
72 |
73 | OTHER
74 | 6.800992
75 | IN
76 | LONG
77 | 29.214856
78 |
79 |
80 |
81 | 3
82 | 20140630000000.000[-6:MDT]
83 |
84 |
85 | BAZ
86 | PRIVATE
87 |
88 | OTHER
89 | -9.060702
90 | OUT
91 | LONG
92 | 21.928764
93 |
94 |
95 |
96 |
97 |
98 |
99 | FOO
100 | PRIVATE
101 |
102 | CASH
103 | LONG
104 | 17.604312
105 | 22.517211
106 | 396.4
107 | 20140630000000.000[-6:MDT]
108 |
109 |
110 |
111 |
112 |
113 | BAR
114 | PRIVATE
115 |
116 | CASH
117 | LONG
118 | 13.550983
119 | 29.214855
120 | 395.89
121 | 20140630000000.000[-6:MDT]
122 |
123 |
124 |
125 |
126 |
127 | BAZ
128 | PRIVATE
129 |
130 | CASH
131 | LONG
132 | 0.0
133 | 0.0
134 | 0.0
135 | 20140630000000.000[-6:MDT]
136 |
137 |
138 |
139 |
140 | 1000.00
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | BAR
151 | PRIVATE
152 |
153 | BAR Index Fund
154 | BAR
155 |
156 |
157 |
158 |
159 |
160 | FOO
161 | PRIVATE
162 |
163 | Foo Index Fund
164 | FOO
165 |
166 |
167 |
168 |
169 |
170 | BAZ
171 | PRIVATE
172 |
173 | Baz Fund
174 | BAZ
175 |
176 |
177 |
178 |
179 |
180 |
181 |
--------------------------------------------------------------------------------
/fixtures/mint.csv:
--------------------------------------------------------------------------------
1 | "Date","Description","Original Description","Amount","Transaction Type","Category","Account Name","Labels","Notes"
2 | "8/02/2016","Amazon","AMAZON MKTPLACE PMTS AMZN.COM/BILL WA","29.99","debit","Shopping","1234","",""
3 | "6/02/2016","Autopay Rautopay Auto","AUTOPAY 000000000000000RAUTOPAY AUTO-PMT","123.45","credit","Credit Card Payment","1234","",""
4 |
--------------------------------------------------------------------------------
/fixtures/multiple.lgr:
--------------------------------------------------------------------------------
1 | 2011/03/01 Baz
2 | Foo $21.01
3 | Bar
4 |
5 | 2011/04/01 Baz
6 | Foo $10.01
7 | Bar
8 |
--------------------------------------------------------------------------------
/fixtures/no-institution.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 |
15 | 0
16 | INFO
17 |
18 | 20130525225731.258
19 | ENG
20 | 20050531060000.000
21 | 51123
22 | 9774652
23 |
24 |
25 |
26 |
27 | 0
28 |
29 | 0
30 | INFO
31 |
32 |
33 | USD
34 |
35 | 5472369148
36 | 1452687~7
37 | CHECKING
38 |
39 |
40 | 20000101070000.000
41 | 20130525060000.000
42 |
43 | CREDIT
44 | 20110331120000.000
45 | 0.01
46 | 0000486
47 | DIVIDEND EARNED FOR PERIOD OF 03
48 | DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
49 |
50 |
51 | DEBIT
52 | 20110405120000.000
53 | -34.51
54 | 0000487
55 | AUTOMATIC WITHDRAWAL, ELECTRIC BILL
56 | AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
57 |
58 |
59 | CHECK
60 | 20110407120000.000
61 | -25.00
62 | 0000488
63 | 319
64 | RETURNED CHECK FEE, CHECK # 319
65 | RETURNED CHECK FEE, CHECK # 319 FOR $45.33 ON 04/07/11
66 |
67 |
68 |
69 | 100.99
70 | 20130525225731.258
71 |
72 |
73 | 75.99
74 | 20130525225731.258
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/fixtures/ofxclient.ini:
--------------------------------------------------------------------------------
1 | [FOO]
2 | account_type = SAVINGS
3 | description = Assets:Savings:Foo
4 | institution.id = FOOBANK
5 | institution.org = FOO
6 | institution.password = SECRET
7 | institution.url = https://foo.example.org
8 | institution.username = ME
9 | local_id = FOO
10 | number = 0123456789
11 | routing_number = 0123456789
12 |
13 | [BAR]
14 | account_type = CHECKING
15 | description = Assets:Checking:Bar
16 | institution.id = FOOBANK
17 | institution.org = FOO
18 | institution.password = SECRET
19 | institution.url = https://foo.example.org
20 | institution.username = ME
21 | local_id = BAR
22 | number = 0123456798
23 | routing_number = 0123456789
24 |
--------------------------------------------------------------------------------
/fixtures/paypal.csv:
--------------------------------------------------------------------------------
1 | Date, Time, Time Zone, Name, Type, Status, Currency, Gross, Fee, Net, From Email Address, To Email Address, Transaction ID, Counterparty Status, Shipping Address, Address Status, Item Title, Item ID, Shipping and Handling Amount, Insurance Amount, Sales Tax, Option 1 Name, Option 1 Value, Option 2 Name, Option 2 Value, Auction Site, Buyer ID, Item URL, Closing Date, Escrow Id, Invoice Id, Reference Txn ID, Invoice Number, Custom Number, Receipt ID, Balance, Contact Phone Number,
2 | "6/4/2016","10:46:49","PDT","Jane Doe","Recurring Payment Sent","Completed","USD","-20.00","0.00","-20.00","me@example.com","someone@example.net","XYZ1","Verified","John Doe, 123 Main St, Springfield, XX 00000, United States","Confirmed","My Friend","friend","","","","","","","","","","","","","","","","","","0.00","",
3 | "6/4/2016","10:46:49","PDT","Debit Card","Charge From Debit Card","Completed","USD","1,120.00","0.00","1,120.00","","","XYZ2","","","","","","","","","","","","","","","","","","","XYZ3","","","","20.00","",
4 |
--------------------------------------------------------------------------------
/fixtures/paypal.lgr:
--------------------------------------------------------------------------------
1 | 2016/06/04 Jane Doe someone@example.net My Friend ID: XYZ1, Recurring Payment Sent
2 | ; csvid: paypal.XYZ1
3 | Paypal -20.00 USD
4 | Expenses:Misc 20.00 USD
5 |
--------------------------------------------------------------------------------
/fixtures/paypal_alternate.csv:
--------------------------------------------------------------------------------
1 | Date, Time, Time Zone, Name, Type, Status, Amount, Receipt ID, Balance,
2 | "12/31/2016","23:59:59","PST","Some User","Payment Sent","Completed","-12.34","","0.00",
3 | "12/31/2016","23:59:59","PST","Bank Account","Add Funds from a Bank Account","Completed","12.34","","12.34",
4 |
--------------------------------------------------------------------------------
/fixtures/venmo.csv:
--------------------------------------------------------------------------------
1 | Username,ID,Datetime,Type,Status,Note,From,To,Amount (total),Amount (fee),Funding Source,Destination,Beginning Balance,Ending Balance,Statement Period Venmo Fees,Year to Date Venmo Fees,Disclaimer
2 | my_username,,,,,,,,,,,,"$0",,,,
3 | ,1,2020-01-01T13:00:00,Charge,Complete,foobar charge positive,Me,Friend A,+ $73.01,,,Venmo balance,,,,,
4 | ,2,2020-01-02T17:00:00,Payment,Complete,foobar payment positive,Friend B,My Name,+ $50.03,,,Venmo balance,,,,,
5 | ,3,2020-01-03T20:00:00,Payment,Complete,foobar payment negative,Me,Friend A,- $12.01,,Venmo balance,,,,,,
6 | ,4,2020-01-04T04:00:00,Charge,Complete,foobar charge negative,Friend B,Me,- $35.02,,Venmo balance,,,,,,
7 | ,,,,,,,,,,,,,"$76.01",,,
8 | ,,,,,,,,,,,,,,$0.00,,
9 | ,,,,,,,,,,,,,,,$0.00,
10 | ,,,,,,,,,,,,,,,,"In case of errors or questions about your
11 | electronic transfers:
12 | - Telephone us at 855-812-4430
13 | - Write the Venmo Error Resolution Department at
14 | 222 W. Merchandise Plaza, Suite 800, Chicago, IL 60654; or
15 | - Write to us through the Contact Us page
16 | (https://help.venmo.com/hc/en-us/requests/new)
17 | Contact us as soon as you can if you think your statement or
18 | receipt is wrong or if you need more information about
19 | a transfer on the statement or receipt. We must hear from
20 | you no later than 60 days after we sent you the FIRST
21 | statement on which the error or problem appeared.
22 | 1. Tell us your name and username or phone number.
23 | 2. Describe the error or the transfer you are unsure about,
24 | and explain as clearly as you can why you believe it is
25 | an error or why you need more information.
26 | 3. Tell us the dollar amount of the suspected error.
27 | We will investigate your complaint and will correct any
28 | error promptly. If we take more than 10 business days to do
29 | this, we will credit your account for the amount you think
30 | is in error, so that you will have the use of the money
31 | during the time it takes us to complete our investigation.
32 | In case of errors or questions about your transactions made
33 | with your Venmo Mastercard, please consult your
34 | Cardholder Agreement
35 | (https://venmo.com/legal/bancorp-cardholder-agreement).
36 | "
37 |
--------------------------------------------------------------------------------
/ledgerautosync/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 |
20 | class EmptyInstitutionException(Exception):
21 | def __init__(self, value):
22 | self.value = value
23 |
24 | def __str__(self):
25 | return repr(self.value)
26 |
27 |
28 | class LedgerAutosyncException(Exception):
29 | def __init__(self, value):
30 | self.value = value
31 |
32 | def __str__(self):
33 | return repr(self.value)
34 |
--------------------------------------------------------------------------------
/ledgerautosync/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Copyright (c) 2013-2021 Erik Hetzner
4 | # Portions Copyright (c) 2016 James S Blachly, MD
5 | #
6 | # This file is part of ledger-autosync
7 | #
8 | # ledger-autosync is free software: you can redistribute it and/or
9 | # modify it under the terms of the GNU General Public License as
10 | # published by the Free Software Foundation, either version 3 of the
11 | # License, or (at your option) any later version.
12 | #
13 | # ledger-autosync is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 | # General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with ledger-autosync. If not, see
20 | # .
21 |
22 |
23 | import argparse
24 | import importlib.util
25 | import logging
26 | import os
27 | import os.path
28 | import re
29 | import sys
30 | import traceback
31 |
32 | from ofxclient.config import OfxConfig
33 |
34 | from ledgerautosync import LedgerAutosyncException
35 | from ledgerautosync.converter import (
36 | ALL_AUTOSYNC_INITIAL,
37 | AUTOSYNC_INITIAL,
38 | UNKNOWN_BANK_ACCOUNT,
39 | OfxConverter,
40 | SecurityList,
41 | )
42 | from ledgerautosync.ledgerwrap import HLedger, Ledger, LedgerPython, mk_ledger
43 | from ledgerautosync.sync import CsvSynchronizer, OfxSynchronizer
44 |
45 |
46 | def find_ledger_file(ledgerrcpath=None):
47 | """Returns main ledger file path or raise exception if it cannot be \
48 | found."""
49 | if ledgerrcpath is None:
50 | ledgerrcpath = os.path.abspath(os.path.expanduser("~/.ledgerrc"))
51 | if "LEDGER_FILE" in os.environ:
52 | return os.path.abspath(os.path.expanduser(os.environ["LEDGER_FILE"]))
53 | elif os.path.exists(ledgerrcpath):
54 | # hacky
55 | ledgerrc = open(ledgerrcpath)
56 | for line in ledgerrc.readlines():
57 | md = re.match(r"--file\s+([^\s]+).*", line)
58 | if md is not None:
59 | return os.path.abspath(os.path.expanduser(md.group(1)))
60 | else:
61 | return None
62 |
63 |
64 | def print_results(converter, ofx, ledger, txns, args):
65 | """
66 | This function is the final common pathway of program:
67 |
68 | Print initial balance if requested;
69 | Print transactions surviving de-duplication filter;
70 | Print balance assertions if requested;
71 | Print commodity prices obtained from position statements
72 | """
73 |
74 | if args.initial:
75 | if not (
76 | ledger.check_transaction_by_id(
77 | "ofxid", converter.mk_ofxid(AUTOSYNC_INITIAL)
78 | )
79 | ) and not (ledger.check_transaction_by_id("ofxid", ALL_AUTOSYNC_INITIAL)):
80 | print(converter.format_initial_balance(ofx.account.statement))
81 | for txn in txns:
82 | print(converter.convert(txn).format(args.indent))
83 | if args.assertions:
84 | print(converter.format_balance(ofx.account.statement))
85 |
86 | # if OFX has positions use these to obtain commodity prices
87 | # and print "P" records to provide dated/timed valuations
88 | # Note that this outputs only the commodity price,
89 | # not your position (e.g. # shares), even though this is in the OFX record
90 | if hasattr(ofx.account.statement, "positions"):
91 | for pos in ofx.account.statement.positions:
92 | print(converter.format_position(pos))
93 |
94 |
95 | def make_ofx_converter(
96 | account,
97 | name,
98 | ledger,
99 | indent,
100 | fid,
101 | unknownaccount,
102 | payee_format,
103 | hardcodeaccount,
104 | shortenaccount,
105 | security_list,
106 | date_format,
107 | infer_account,
108 | ):
109 | klasses = OfxConverter.__subclasses__()
110 | if len(klasses) > 1:
111 | raise Exception(
112 | "I found more than 1 OfxConverter subclass, but only "
113 | "know how to handle 1. Remove extra subclasses from "
114 | "the plugin directory"
115 | )
116 | elif len(klasses) == 1:
117 | return klasses[0](
118 | account=account,
119 | name=name,
120 | ledger=ledger,
121 | indent=indent,
122 | fid=fid,
123 | unknownaccount=unknownaccount,
124 | payee_format=payee_format,
125 | hardcodeaccount=hardcodeaccount,
126 | shortenaccount=shortenaccount,
127 | security_list=security_list,
128 | date_format=date_format,
129 | infer_account=infer_account,
130 | )
131 | else:
132 | return OfxConverter(
133 | account=account,
134 | name=name,
135 | ledger=ledger,
136 | indent=indent,
137 | fid=fid,
138 | unknownaccount=unknownaccount,
139 | payee_format=payee_format,
140 | hardcodeaccount=hardcodeaccount,
141 | shortenaccount=shortenaccount,
142 | security_list=security_list,
143 | date_format=date_format,
144 | infer_account=infer_account,
145 | )
146 |
147 |
148 | def sync(ledger, accounts, args):
149 | sync = OfxSynchronizer(ledger, shortenaccount=args.shortenaccount)
150 | for acct in accounts:
151 | try:
152 | (ofx, txns) = sync.get_new_txns(acct, resync=args.resync, max_days=args.max)
153 | if ofx is not None:
154 | converter = make_ofx_converter(
155 | account=ofx.account,
156 | name=acct.description,
157 | ledger=ledger,
158 | indent=args.indent,
159 | fid=None,
160 | unknownaccount=args.unknownaccount,
161 | payee_format=args.payee_format,
162 | hardcodeaccount=None,
163 | shortenaccount=args.shortenaccount,
164 | security_list=SecurityList(ofx),
165 | date_format=args.date_format,
166 | infer_account=args.infer_account,
167 | )
168 | print_results(converter, ofx, ledger, txns, args)
169 | except KeyboardInterrupt:
170 | raise
171 | except BaseException:
172 | sys.stderr.write("Caught exception processing %s\n" % (acct.description))
173 | traceback.print_exc(file=sys.stderr)
174 |
175 |
176 | def import_ofx(ledger, args):
177 | sync = OfxSynchronizer(
178 | ledger, hardcodeaccount=args.hardcodeaccount, shortenaccount=args.shortenaccount
179 | )
180 | ofx = OfxSynchronizer.parse_file(args.PATH)
181 | txns = sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
182 | accountname = args.account
183 | if accountname is None:
184 | if ofx.account.institution is not None:
185 | accountname = "%s:%s" % (
186 | ofx.account.institution.organization,
187 | ofx.account.account_id,
188 | )
189 | else:
190 | accountname = UNKNOWN_BANK_ACCOUNT
191 |
192 | # build SecurityList (including indexing by CUSIP and ticker symbol)
193 | security_list = SecurityList(ofx)
194 |
195 | converter = make_ofx_converter(
196 | account=ofx.account,
197 | name=accountname,
198 | ledger=ledger,
199 | indent=args.indent,
200 | fid=args.fid,
201 | unknownaccount=args.unknownaccount,
202 | payee_format=args.payee_format,
203 | hardcodeaccount=args.hardcodeaccount,
204 | shortenaccount=args.shortenaccount,
205 | security_list=security_list,
206 | date_format=args.date_format,
207 | infer_account=args.infer_account,
208 | )
209 | print_results(converter, ofx, ledger, txns, args)
210 |
211 |
212 | def import_csv(ledger, args):
213 | if args.account is None:
214 | raise Exception("When importing a CSV file, you must specify an account name.")
215 | sync = CsvSynchronizer(
216 | ledger, payee_format=args.payee_format, date_format=args.date_format
217 | )
218 | txns = sync.parse_file(
219 | args.PATH, accountname=args.account, unknownaccount=args.unknownaccount
220 | )
221 | if args.reverse:
222 | txns = reversed(txns)
223 | for txn in txns:
224 | if txn is not None:
225 | print(txn.format(args.indent, args.assertions))
226 |
227 |
228 | def load_plugins(config_dir):
229 | plugin_dir = os.path.join(config_dir, "ledger-autosync", "plugins")
230 | if os.path.isdir(plugin_dir):
231 | for plugin in filter(
232 | re.compile(".py$", re.IGNORECASE).search, os.listdir(plugin_dir)
233 | ):
234 | # Quiet loader
235 | import ledgerautosync.plugins # noqa: F401
236 |
237 | file_path = os.path.join(plugin_dir, plugin)
238 | module_name = f"ledgerautosync.plugins.{os.path.splitext(plugin)[0]}"
239 | spec = importlib.util.spec_from_file_location(module_name, file_path)
240 | module = importlib.util.module_from_spec(spec)
241 | sys.modules[module_name] = module
242 | spec.loader.exec_module(module)
243 |
244 |
245 | def run(args=None, config=None):
246 | if args is None:
247 | args = sys.argv[1:]
248 |
249 | parser = argparse.ArgumentParser(description="Synchronize ledger.")
250 | parser.add_argument(
251 | "-m", "--max", type=int, default=90, help="maximum number of days to process"
252 | )
253 | parser.add_argument(
254 | "-r",
255 | "--resync",
256 | action="store_true",
257 | default=False,
258 | help="do not stop until max days reached",
259 | )
260 | parser.add_argument(
261 | "PATH",
262 | nargs="?",
263 | help="do not sync; import from OFX \
264 | file",
265 | )
266 | parser.add_argument(
267 | "-a",
268 | "--account",
269 | type=str,
270 | default=None,
271 | help="sync only the named account; \
272 | if importing from file, set account name for import",
273 | )
274 | parser.add_argument(
275 | "-l",
276 | "--ledger",
277 | type=str,
278 | default=None,
279 | help="specify ledger file to READ for syncing",
280 | )
281 | parser.add_argument(
282 | "--rules",
283 | type=str,
284 | default=None,
285 | help="specify rule file to READ for Payee matching",
286 | )
287 | parser.add_argument(
288 | "-L",
289 | "--no-ledger",
290 | dest="no_ledger",
291 | action="store_true",
292 | default=False,
293 | help="do not de-duplicate against a ledger file",
294 | )
295 | parser.add_argument(
296 | "-i",
297 | "--indent",
298 | type=int,
299 | default=4,
300 | help="number of spaces to use for indentation",
301 | )
302 | parser.add_argument(
303 | "--initial",
304 | action="store_true",
305 | default=False,
306 | help="create initial balance entries",
307 | )
308 | parser.add_argument(
309 | "--fid",
310 | type=int,
311 | default=None,
312 | help="pass in fid value for OFX files that do not \
313 | supply it",
314 | )
315 | parser.add_argument(
316 | "--hardcode-account",
317 | type=str,
318 | default=None,
319 | dest="hardcodeaccount",
320 | help="pass in hardcoded account number for OFX files \
321 | to maintain ledger files without real account numbers",
322 | )
323 | parser.add_argument(
324 | "--shorten-account",
325 | default=False,
326 | action="store_true",
327 | dest="shortenaccount",
328 | help="shorten all account numbers to last 4 digits \
329 | to maintain ledger files without full account numbers",
330 | )
331 | parser.add_argument(
332 | "--unknown-account",
333 | type=str,
334 | dest="unknownaccount",
335 | default=None,
336 | help="specify account name to use when one can't be \
337 | found by payee",
338 | )
339 | parser.add_argument(
340 | "--assertions",
341 | action="store_true",
342 | default=False,
343 | help="create balance assertion entries",
344 | )
345 | parser.add_argument(
346 | "-d", "--debug", action="store_true", default=False, help="enable debug logging"
347 | )
348 | parser.add_argument(
349 | "--hledger",
350 | action="store_true",
351 | default=False,
352 | help="force use of hledger (on by default if invoked \
353 | as hledger-autosync)",
354 | )
355 | parser.add_argument(
356 | "--payee-format",
357 | type=str,
358 | default=None,
359 | dest="payee_format",
360 | help="""Format string to use for generating the payee line. Substitutions
361 | can be written using {memo}, {payee}, {txntype}, {account} or
362 | {tferaction} for OFX. If the input file is a CSV file,
363 | substitutions are written using the CSV file column names
364 | between {}.""",
365 | )
366 | parser.add_argument(
367 | "--python",
368 | action="store_true",
369 | default=False,
370 | help="use the ledger python interface",
371 | )
372 | parser.add_argument(
373 | "--slow",
374 | action="store_true",
375 | default=False,
376 | help="use slow, but possibly more robust, method of \
377 | calling ledger (no subprocess)",
378 | )
379 | parser.add_argument(
380 | "--which",
381 | action="store_true",
382 | default=False,
383 | help="display which version of ledger (cli), hledger, \
384 | or ledger (python) will be used by ledger-autosync to check for previous \
385 | transactions",
386 | )
387 | parser.add_argument(
388 | "--reverse",
389 | action="store_true",
390 | default=False,
391 | help="print CSV transactions in reverse order",
392 | )
393 | parser.add_argument(
394 | "-o",
395 | "--ofxconfig",
396 | type=str,
397 | default=None,
398 | help="specify config file for ofxclient",
399 | )
400 | parser.add_argument(
401 | "-y",
402 | "--date-format",
403 | type=str,
404 | default=None,
405 | dest="date_format",
406 | help="""Format string to use for printing dates.
407 | See strftime for details on format string syntax. Default is "%%Y/%%m/%%d".""",
408 | )
409 | parser.add_argument(
410 | "--no-infer-account",
411 | dest="infer_account",
412 | action="store_false",
413 | default=True,
414 | help="disable inference of offset account from payee",
415 | )
416 | args = parser.parse_args(args)
417 | if sys.argv[0][-16:] == "hledger-autosync":
418 | args.hledger = True
419 |
420 | ledger_file = None
421 | if args.ledger and args.no_ledger:
422 | raise LedgerAutosyncException("You cannot specify a ledger file and -L")
423 | elif args.ledger:
424 | ledger_file = args.ledger
425 | else:
426 | ledger_file = find_ledger_file()
427 | if args.debug:
428 | logging.basicConfig(level=logging.DEBUG)
429 |
430 | if ledger_file is None:
431 | sys.stderr.write(
432 | "LEDGER_FILE environment variable not set, and no \
433 | .ledgerrc file found, and -l argument was not supplied: running with deduplication disabled. \
434 | All transactions will be printed!\n"
435 | )
436 | ledger = None
437 | elif args.no_ledger:
438 | ledger = None
439 | elif args.hledger:
440 | ledger = HLedger(ledger_file)
441 | elif args.python:
442 | ledger = LedgerPython(ledger_file=ledger_file)
443 | elif args.slow:
444 | ledger = Ledger(ledger_file=ledger_file, no_pipe=True)
445 | else:
446 | ledger = mk_ledger(ledger_file)
447 |
448 | if args.which:
449 | sys.stderr.write("ledger-autosync is using ")
450 | if isinstance(ledger, Ledger):
451 | sys.stderr.write("ledger (cli)\n")
452 | elif isinstance(ledger, HLedger):
453 | sys.stderr.write("hledger\n")
454 | elif isinstance(ledger, LedgerPython):
455 | sys.stderr.write("ledger.so (python)\n")
456 | exit()
457 |
458 | config_dir = os.environ.get(
459 | "XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")
460 | )
461 |
462 | if args.rules and os.path.exists(args.rules):
463 | with open(args.rules) as f:
464 | for line in f:
465 | regex, account = line.strip().split("\t")
466 | ledger.add_rule(re.compile(regex, re.IGNORECASE), account)
467 |
468 | load_plugins(config_dir)
469 |
470 | if args.PATH is None:
471 | if config is None:
472 | if args.ofxconfig is None:
473 | config_file = os.path.join(config_dir, "ofxclient.ini")
474 | else:
475 | config_file = args.ofxconfig
476 | if os.path.exists(config_file):
477 | config = OfxConfig(file_name=config_file)
478 | else:
479 | config = OfxConfig()
480 | accounts = config.accounts()
481 | if args.account:
482 | accounts = [acct for acct in accounts if acct.description == args.account]
483 | sync(ledger, accounts, args)
484 | else:
485 | _, file_extension = os.path.splitext(args.PATH.lower())
486 | if file_extension == ".csv":
487 | import_csv(ledger, args)
488 | else:
489 | import_ofx(ledger, args)
490 |
491 |
492 | if __name__ == "__main__":
493 | run()
494 |
--------------------------------------------------------------------------------
/ledgerautosync/ledgerwrap.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 |
20 | import csv
21 | import logging
22 | import os
23 | import re
24 | import subprocess
25 | from queue import Empty, Queue
26 | from shutil import which as find_executable
27 | from subprocess import PIPE, Popen
28 | from threading import Thread
29 |
30 | from ledgerautosync.converter import Converter
31 |
32 | csv.register_dialect("ledger", delimiter=",", quoting=csv.QUOTE_ALL, escapechar="\\")
33 |
34 |
35 | def mk_ledger(ledger_file):
36 | if Ledger.available():
37 | return Ledger(ledger_file)
38 | elif HLedger.available():
39 | return HLedger(ledger_file)
40 | elif LedgerPython.available():
41 | # string_read=True works around
42 | # http://bugs.ledger-cli.org/show_bug.cgi?id=973
43 | return LedgerPython(ledger_file, string_read=True)
44 | else:
45 | raise Exception("Neither ledger 3 nor hledger found!")
46 |
47 |
48 | class MetaLedger(object):
49 | @staticmethod
50 | def windows_clean(a):
51 | def clean_str(s):
52 | s = s.replace("%", "")
53 | s = s.replace(" ", "\\ ")
54 | s = s.replace("/", "\\/")
55 | return s
56 |
57 | return [clean_str(s) for s in a]
58 |
59 | # Return True if this ledgerlike interface is available
60 | @staticmethod
61 | def available():
62 | return False
63 |
64 | def add_payee(self, payee, account):
65 | if payee not in self.payees:
66 | self.payees[payee] = []
67 | if account not in self.payees[payee]:
68 | self.payees[payee].append(account)
69 |
70 | def filter_accounts(self, accts, exclude):
71 | accts_filtered = [a for a in accts if a != exclude]
72 | if accts_filtered:
73 | return accts_filtered[-1]
74 | else:
75 | return None
76 |
77 | def get_account_by_payee(self, payee, exclude):
78 | for regex, account in self.rules:
79 | if regex.match(payee):
80 | return account
81 |
82 | self.load_payees()
83 | return self.filter_accounts(self.payees.get(payee, []), exclude)
84 |
85 | def add_rule(self, regex, account):
86 | self.rules.append((regex, account))
87 |
88 | def __init__(self):
89 | self.payees = None
90 | self.rules = []
91 |
92 |
93 | class Ledger(MetaLedger):
94 | @staticmethod
95 | def available():
96 | return (find_executable("ledger") is not None) and (
97 | Popen(
98 | ["ledger", "--version"], stdout=PIPE, universal_newlines=True
99 | ).communicate()[0]
100 | ).startswith("Ledger 3")
101 |
102 | def __init__(self, ledger_file=None, no_pipe=True):
103 | if find_executable("ledger") is None:
104 | raise Exception("ledger was not found in $PATH")
105 | self._item = ""
106 |
107 | def enqueue_output(out, queue):
108 | buff = ""
109 | while buff is not None:
110 | buff = out.read(1)
111 | if buff is not None:
112 | self._item += buff
113 | if self._item.endswith("] "): # prompt
114 | queue.put(self._item[0:-2])
115 | self._item = ""
116 | out.close()
117 |
118 | self.use_pipe = (os.name == "posix") and not (no_pipe)
119 | self.args = ["ledger", "--args-only"]
120 | if ledger_file is not None:
121 | self.args += ["-f", ledger_file]
122 | if self.use_pipe:
123 | self.p = Popen(
124 | self.args,
125 | bufsize=1,
126 | stdin=PIPE,
127 | stdout=PIPE,
128 | universal_newlines=True,
129 | close_fds=True,
130 | )
131 | self.q = Queue()
132 | self.t = Thread(target=enqueue_output, args=(self.p.stdout, self.q))
133 | self.t.daemon = True # thread dies with the program
134 | self.t.start()
135 | # read output until prompt
136 | try:
137 | self.q.get(True, 5)
138 | except Empty:
139 | logging.error("Could not get prompt (]) from ledger!")
140 | logging.error("Received: %s" % (self._item))
141 | exit(1)
142 | super(Ledger, self).__init__()
143 |
144 | @staticmethod
145 | def pipe_quote(a):
146 | def quote(s):
147 | s = s.replace("/", "\\\\/")
148 | s = s.replace("%", "")
149 | if not (re.match(r"^\w+$", s)):
150 | s = '"%s"' % (s)
151 | return s
152 |
153 | return [quote(s) for s in a]
154 |
155 | def run(self, cmd):
156 | if self.use_pipe:
157 | self.p.stdin.write("csv ")
158 | self.p.stdin.write(" ".join(Ledger.pipe_quote(cmd)))
159 | self.p.stdin.write("\n")
160 | logging.debug(" ".join(Ledger.pipe_quote(cmd)))
161 | try:
162 | return csv.reader(self.q.get(True, 5), dialect="ledger")
163 | except Empty:
164 | logging.error("Could not get prompt from ledger!")
165 | exit(1)
166 | else:
167 | cmd = self.args + ["csv"] + cmd
168 | if os.name == "nt":
169 | cmd = MetaLedger.windows_clean(cmd)
170 | return csv.reader(
171 | subprocess.check_output(cmd, universal_newlines=True).splitlines(),
172 | dialect="ledger",
173 | )
174 |
175 | def check_transaction_by_id(self, key, value):
176 | q = ["-E", "meta", "%s=%s" % (key, Converter.clean_id(value))]
177 | try:
178 | next(self.run(q))
179 | return True
180 | except StopIteration:
181 | return False
182 |
183 | def load_payees(self):
184 | if self.payees is None:
185 | self.payees = {}
186 | r = self.run(["show", "--actual"])
187 | for line in r:
188 | self.add_payee(line[2], line[3])
189 |
190 | def get_autosync_payee(self, payee, account):
191 | q = [
192 | account,
193 | "--last",
194 | "1",
195 | "--format",
196 | "%(quoted(payee))\n",
197 | "--limit",
198 | 'tag("AutosyncPayee") == "%s"' % (payee),
199 | ]
200 | r = self.run(q)
201 | try:
202 | return next(r)[0]
203 | except StopIteration:
204 | return payee
205 |
206 |
207 | class LedgerPython(MetaLedger):
208 | @staticmethod
209 | def available():
210 | try:
211 | import ledger # noqa: F401
212 |
213 | return True
214 | except ImportError:
215 | return False
216 |
217 | def __init__(self, ledger_file=None, string_read=True):
218 | # sanity check for ledger python interface
219 | try:
220 | import ledger
221 | except ImportError:
222 | raise Exception("Ledger python interface not found!")
223 | if ledger_file is None:
224 | # TODO - better loading
225 | raise Exception
226 | else:
227 | if string_read:
228 | self.session = ledger.Session()
229 | self.journal = self.session.read_journal_from_string(
230 | open(ledger_file).read()
231 | )
232 | else:
233 | self.journal = ledger.read_journal(ledger_file)
234 |
235 | super(LedgerPython, self).__init__()
236 |
237 | def load_payees(self):
238 | if self.payees is None:
239 | self.payees = {}
240 | for xact in self.journal:
241 | for post in xact.posts():
242 | self.add_payee(xact.payee, post.reported_account().fullname())
243 |
244 | def check_transaction_by_id(self, key, value):
245 | q = self.journal.query('-E meta %s="%s"' % (key, Converter.clean_id(value)))
246 | return len(q) > 0
247 |
248 | def get_autosync_payee(self, payee, account):
249 | logging.error("payee lookup not implemented for LedgerPython, using raw payee")
250 | return payee
251 |
252 |
253 | class HLedger(MetaLedger):
254 | @staticmethod
255 | def available():
256 | return find_executable("hledger") is not None
257 |
258 | @staticmethod
259 | def quote(a):
260 | def quote_str(s):
261 | s = s.replace("(", "\\(")
262 | s = s.replace(")", "\\)")
263 | return s
264 |
265 | return [quote_str(s) for s in a]
266 |
267 | def __init__(self, ledger_file=None):
268 | if find_executable("hledger") is None:
269 | raise Exception("hledger was not found in $PATH")
270 | self.args = ["hledger"]
271 | if ledger_file is not None:
272 | self.args += ["-f", ledger_file]
273 | super(HLedger, self).__init__()
274 |
275 | def run(self, cmd):
276 | cmd = HLedger.quote(self.args + cmd)
277 | if os.name == "nt":
278 | cmd = MetaLedger.windows_clean(cmd)
279 | logging.debug(" ".join(cmd))
280 | return subprocess.check_output(cmd, universal_newlines=True)
281 |
282 | def check_transaction_by_id(self, key, value):
283 | cmd = ["reg", "tag:%s=%s" % (key, Converter.clean_id(value))]
284 | return self.run(cmd) != ""
285 |
286 | def load_payees(self):
287 | if self.payees is None:
288 | self.payees = {}
289 | cmd = ["reg", "-O", "csv", "--real"]
290 | r = csv.DictReader(self.run(cmd).splitlines())
291 | for line in r:
292 | self.add_payee(line["description"], line["account"])
293 |
294 | def get_autosync_payee(self, payee, account):
295 | logging.error("payee lookup not implemented for HLedger, using raw payee")
296 | return payee
297 |
--------------------------------------------------------------------------------
/ledgerautosync/plugins.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/egh/ledger-autosync/ffa3fc66050f3c194a7716905103146063d7cad4/ledgerautosync/plugins.py
--------------------------------------------------------------------------------
/ledgerautosync/sync.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 |
20 | import codecs
21 | import csv
22 | import logging
23 |
24 | from ofxparse import OfxParser, OfxParserException
25 |
26 | from ledgerautosync.converter import CsvConverter
27 |
28 |
29 | class Synchronizer(object):
30 | def __init__(self, lgr):
31 | self.lgr = lgr
32 |
33 |
34 | class OfxSynchronizer(Synchronizer):
35 | def __init__(self, lgr, hardcodeaccount=None, shortenaccount=None):
36 | self.hardcodeaccount = hardcodeaccount
37 | self.shortenaccount = shortenaccount
38 | super(OfxSynchronizer, self).__init__(lgr)
39 |
40 | @staticmethod
41 | def parse_file(path):
42 | with open(path, "rb") as ofx_file:
43 | return OfxParser.parse(ofx_file)
44 |
45 | def is_txn_synced(self, acctid, txn):
46 | if self.lgr is None:
47 | # User called with --no-ledger
48 | # All transactions are considered "synced" in this case.
49 | return False
50 | else:
51 | acctid_to_use = acctid
52 | txnid_to_use = txn.id
53 | if self.hardcodeaccount:
54 | acctid_to_use = self.hardcodeaccount
55 | txnid_to_use = txnid_to_use.replace(acctid, acctid_to_use)
56 | elif self.shortenaccount:
57 | acctid_to_use = acctid[-4:]
58 | txnid_to_use = txnid_to_use.replace(acctid, acctid_to_use)
59 | ofxid = "%s.%s" % (acctid_to_use, txnid_to_use)
60 | return self.lgr.check_transaction_by_id("ofxid", ofxid)
61 |
62 | # Filter out comment transactions. These have an amount of 0 and the same
63 | # datetime as the previous transactions.
64 | def filter_comment_txns(self, txns):
65 | last_txn = None
66 | retval = []
67 | for txn in txns:
68 | if (
69 | (last_txn is not None)
70 | and hasattr(txn, "amount")
71 | and (txn.amount == 0)
72 | and hasattr(last_txn, "date")
73 | and hasattr(txn, "date")
74 | and (last_txn.date == txn.date)
75 | ):
76 | # This is a comment transaction
77 | pass
78 | else:
79 | last_txn = txn
80 | retval.append(txn)
81 | return retval
82 |
83 | @staticmethod
84 | def extract_sort_key(txn):
85 | if hasattr(txn, "tradeDate"):
86 | return txn.tradeDate
87 | elif hasattr(txn, "date"):
88 | return txn.date
89 | elif hasattr(txn, "settleDate"):
90 | return txn.settleDate
91 | return None
92 |
93 | def filter(self, txns, acctid):
94 | if len(txns) == 0:
95 | sorted_txns = txns
96 | else:
97 | sorted_txns = sorted(txns, key=OfxSynchronizer.extract_sort_key)
98 | retval = [txn for txn in sorted_txns if not (self.is_txn_synced(acctid, txn))]
99 | return self.filter_comment_txns(retval)
100 |
101 | def get_new_txns(self, acct, max_days=999999, resync=False):
102 | if resync or (max_days < 7):
103 | days = max_days
104 | else:
105 | days = 7
106 | last_txns_len = 0
107 | while True:
108 | logging.debug(
109 | "Downloading %d days of transactions for %s (max_days=%d)."
110 | % (days, acct.description, max_days)
111 | )
112 | raw = acct.download(days=days)
113 |
114 | if raw.read() == "Server error occured. Received HttpStatusCode of 400":
115 | raise Exception("Error connecting to account %s" % (acct.description))
116 | raw.seek(0)
117 | ofx = None
118 | try:
119 | ofx = OfxParser.parse(raw)
120 | except OfxParserException as ex:
121 | if ex.message == "The ofx file is empty!":
122 | return (ofx, [])
123 | else:
124 | raise ex
125 | if ofx.signon is not None:
126 | if ofx.signon.severity == "ERROR":
127 | raise Exception(
128 | "Error returned from server for %s: %s"
129 | % (acct.description, ofx.signon.message)
130 | )
131 | if not (hasattr(ofx, "account")):
132 | # some banks return this for no txns
133 | if days >= max_days:
134 | logging.debug("Hit max days.")
135 | # return None to let the caller know that we don't
136 | # even have account info
137 | return (None, None)
138 | else:
139 | days = days * 2
140 | if days > max_days:
141 | days = max_days
142 | logging.debug("empty account: increasing days ago to %d." % (days))
143 | last_txns_len = 0
144 | else:
145 | txns = ofx.account.statement.transactions
146 | new_txns = self.filter(txns, ofx.account.account_id)
147 | logging.debug("txns: %d" % (len(txns)))
148 | logging.debug("new txns: %d" % (len(new_txns)))
149 | if (len(txns) > 0) and (last_txns_len == len(txns)):
150 | # not getting more txns than last time; we have
151 | # reached the beginning
152 | logging.debug("Not getting more txns than last time, done.")
153 | return (ofx, new_txns)
154 | elif (len(txns) > len(new_txns)) or (days >= max_days):
155 | # got more txns than were new or hit max_days, we've
156 | # reached a stopping point
157 | if days >= max_days:
158 | logging.debug("Hit max days.")
159 | else:
160 | logging.debug("Got some stale txns.")
161 | return (ofx, new_txns)
162 | else:
163 | # all txns were new, increase how far back we go
164 | days = days * 2
165 | if days > max_days:
166 | days = max_days
167 | logging.debug("Increasing days ago to %d." % (days))
168 | last_txns_len = len(txns)
169 |
170 |
171 | class CsvSynchronizer(Synchronizer):
172 | def __init__(self, lgr, payee_format=None, date_format=None):
173 | super(CsvSynchronizer, self).__init__(lgr)
174 | self.payee_format = payee_format
175 | self.date_format = date_format
176 |
177 | def is_row_synced(self, converter, row):
178 | if self.lgr is None:
179 | # User called with --no-ledger
180 | # All transactions are considered "synced" in this case.
181 | return False
182 | else:
183 | return self.lgr.check_transaction_by_id("csvid", converter.get_csv_id(row))
184 |
185 | def parse_file(self, path, accountname=None, unknownaccount=None):
186 | with open(path) as f:
187 | has_bom = f.read(3) == codecs.BOM_UTF8
188 | if not (has_bom):
189 | f.seek(0)
190 | else:
191 | f.seek(3)
192 | dialect = csv.Sniffer().sniff(f.readline())
193 | if not (has_bom):
194 | f.seek(0)
195 | else:
196 | f.seek(3)
197 | dialect.skipinitialspace = True
198 | reader = csv.DictReader(f, dialect=dialect)
199 | converter = CsvConverter.make_converter(
200 | set(reader.fieldnames),
201 | dialect,
202 | ledger=self.lgr,
203 | name=accountname,
204 | unknownaccount=unknownaccount,
205 | payee_format=self.payee_format,
206 | date_format=self.date_format,
207 | )
208 | # Create a new reader in case the converter modified the dialect
209 | if not (has_bom):
210 | f.seek(0)
211 | else:
212 | f.seek(3)
213 | reader = csv.DictReader(f, dialect=dialect)
214 | return [
215 | converter.convert(row)
216 | for row in reader
217 | if not (self.is_row_synced(converter, row))
218 | ]
219 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.isort]
2 | profile = "black"
3 | multi_line_output = 3
4 |
5 | [tool.pytest.ini_options]
6 | markers = [
7 | "ofx_file: ofx fixture file to use in test",
8 | "lgr_file: lgr fixture file to use in test",
9 | "csv_file: csv fixture file to use in test",
10 | "ledger_impls: List of Ledger implementations (as wrapper classes) that this test applies to. Default is all."
11 | ]
12 |
13 | [tool.poetry]
14 | name = "ledger-autosync"
15 | version = "1.2.0"
16 | description = "Automatically sync your bank's data with ledger"
17 | authors = ["Erik Hetzner "]
18 | license = "GPLv3"
19 | homepage = "https://github.com/egh/ledger-autosync"
20 | repository = "https://github.com/egh/ledger-autosync"
21 | keywords = ["ledger", "accounting"]
22 | readme = "README.rst"
23 | classifiers = [
24 | "Development Status :: 5 - Production/Stable",
25 | "Intended Audience :: End Users/Desktop",
26 | "Operating System :: OS Independent",
27 | "Topic :: Office/Business :: Financial :: Accounting",
28 | "Topic :: Office/Business :: Financial :: Investment",
29 | "Topic :: Office/Business :: Financial",
30 | ]
31 | packages = [
32 | { include = "ledgerautosync" }
33 | ]
34 | [tool.poetry.scripts]
35 | "ledger-autosync" = "ledgerautosync.cli:run"
36 | "hledger-autosync" = "ledgerautosync.cli:run"
37 |
38 | [tool.poetry.dependencies]
39 | python = ">=3.8"
40 | ofxclient = "*"
41 | ofxparse = "*"
42 | lxml = "*"
43 |
44 | [tool.poetry.dev-dependencies]
45 | pytest = "*"
46 | black = "*"
47 |
48 | [build-system]
49 | requires = ["poetry-core>=1.0.0"]
50 | build-backend = "poetry.core.masonry.api"
51 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [pycodestyle]
2 | max-line-length = 88
3 | extend-ignore = E203
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 |
20 | import os
21 |
22 | import pytest
23 | from ofxparse import OfxParser
24 |
25 | from ledgerautosync.ledgerwrap import HLedger, Ledger, LedgerPython
26 |
27 | LEDGER = [HLedger, Ledger, LedgerPython]
28 |
29 |
30 | @pytest.fixture(params=LEDGER)
31 | def ledger(request):
32 | lgr_name = request.node.get_closest_marker("lgr_file").args[0]
33 | ledger_impls = request.node.get_closest_marker("ledger_impls")
34 | kwargs = {}
35 | if request.param == Ledger:
36 | kwargs["no_pipe"] = True
37 | if not request.param.available():
38 | pytest.skip(f"{request.param} not found")
39 | if ledger_impls is not None:
40 | if request.param not in ledger_impls.args[0]:
41 | pytest.skip("Test not applicable for this ledger.")
42 | return request.param(os.path.join("fixtures", lgr_name), **kwargs)
43 |
44 |
45 | @pytest.fixture
46 | def ofx(request):
47 | ofx_name = request.node.get_closest_marker("ofx_file").args[0]
48 | with open(os.path.join("fixtures", ofx_name), "rb") as ofx_file:
49 | yield OfxParser.parse(ofx_file)
50 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 | import os.path
20 | import re
21 | import tempfile
22 | from io import StringIO
23 | from unittest.mock import Mock, call, patch
24 |
25 | import pytest
26 | from ofxclient.config import OfxConfig
27 |
28 | from ledgerautosync import LedgerAutosyncException
29 | from ledgerautosync.cli import find_ledger_file, run
30 |
31 |
32 | def test_run():
33 | config = OfxConfig(os.path.join("fixtures", "ofxclient.ini"))
34 | acct = config.accounts()[0]
35 | acct.download = Mock(
36 | side_effect=lambda *args, **kwargs: open(
37 | os.path.join("fixtures", "checking.ofx"), "rb"
38 | )
39 | )
40 | config.accounts = Mock(return_value=[acct])
41 | run(["-l", os.path.join("fixtures", "empty.lgr")], config)
42 | acct.download.assert_has_calls([call(days=7), call(days=14)])
43 | assert config.accounts.call_count == 1
44 |
45 |
46 | def test_run_csv_file():
47 | config = OfxConfig(os.path.join("fixtures", "ofxclient.ini"))
48 | run(
49 | [
50 | "-a",
51 | "Paypal",
52 | "-l",
53 | os.path.join("fixtures", "empty.lgr"),
54 | os.path.join("fixtures", "paypal.csv"),
55 | ],
56 | config,
57 | )
58 |
59 |
60 | def test_filter_account():
61 | config = OfxConfig(os.path.join("fixtures", "ofxclient.ini"))
62 | foo = next(
63 | acct for acct in config.accounts() if acct.description == "Assets:Savings:Foo"
64 | )
65 | bar = next(
66 | acct for acct in config.accounts() if acct.description == "Assets:Checking:Bar"
67 | )
68 | foo.download = Mock(
69 | side_effect=lambda *args, **kwargs: open(
70 | os.path.join("fixtures", "checking.ofx"), "rb"
71 | )
72 | )
73 | bar.download = Mock()
74 | config.accounts = Mock(return_value=[foo, bar])
75 | run(
76 | [
77 | "-l",
78 | os.path.join("fixtures", "checking.lgr"),
79 | "-a",
80 | "Assets:Savings:Foo",
81 | ],
82 | config,
83 | )
84 | foo.download.assert_has_calls([call(days=7)])
85 | bar.download.assert_not_called()
86 |
87 |
88 | def test_find_ledger_path():
89 | os.environ["LEDGER_FILE"] = "/tmp/foo"
90 | assert (
91 | find_ledger_file() == "/tmp/foo"
92 | ), "Should use LEDGER_FILE to find ledger path."
93 |
94 | (f, tmprcpath) = tempfile.mkstemp(".ledgerrc")
95 | os.close(f) # Who wants to deal with low-level file descriptors?
96 | with open(tmprcpath, "w") as f:
97 | f.write("--bar foo\n")
98 | f.write("--file /tmp/bar\n")
99 | f.write("--foo bar\n")
100 | assert (
101 | find_ledger_file(tmprcpath) == "/tmp/foo"
102 | ), "Should prefer LEDGER_FILE to --file arg in ledgerrc"
103 | del os.environ["LEDGER_FILE"]
104 | assert find_ledger_file(tmprcpath) == "/tmp/bar", "Should parse ledgerrc"
105 | os.unlink(tmprcpath)
106 |
107 |
108 | def test_no_ledger_arg():
109 | with pytest.raises(LedgerAutosyncException):
110 | config = OfxConfig(os.path.join("fixtures", "ofxclient.ini"))
111 | run(["-l", os.path.join("fixtures", "checking.lgr"), "-L"], config)
112 |
113 |
114 | def test_format_payee():
115 | with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
116 | run(
117 | [
118 | os.path.join("fixtures", "paypal.csv"),
119 | "-a",
120 | "Assets:Foo",
121 | "--payee-format",
122 | "GROSS:{Gross}",
123 | "-L",
124 | ]
125 | )
126 | assert re.search(r"GROSS:-20\.00", mock_stdout.getvalue())
127 |
128 |
129 | # def test_multi_account():
130 | # with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
131 | # run([os.path.join('fixtures', 'multi_account.ofx'), '-a', 'Assets:Foo'])
132 | # self.assertRegexpMatches(mock_stdout.getvalue(), r"GROSS:-20\.00")
133 |
134 |
135 | def test_no_ledger():
136 | config = OfxConfig(os.path.join("fixtures", "ofxclient.ini"))
137 | acct = config.accounts()[0]
138 | acct.download = Mock(
139 | side_effect=lambda *args, **kwargs: open(
140 | os.path.join("fixtures", "checking.ofx"), "rb"
141 | )
142 | )
143 | config.accounts = Mock(return_value=[acct])
144 | with patch("ledgerautosync.cli.find_ledger_file", return_value=None):
145 | with patch("sys.stderr", new_callable=StringIO) as mock_stdout:
146 | run([], config)
147 | assert (
148 | mock_stdout.getvalue()
149 | == "LEDGER_FILE environment variable not set, and no .ledgerrc file found, and -l argument was not supplied: running with deduplication disabled. All transactions will be printed!\n"
150 | )
151 |
--------------------------------------------------------------------------------
/tests/test_converter.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 | import csv
20 | import hashlib
21 | import os
22 | from decimal import Decimal
23 |
24 | import pytest
25 |
26 | from ledgerautosync.converter import (
27 | AmazonConverter,
28 | Amount,
29 | CsvConverter,
30 | MintConverter,
31 | PaypalAlternateConverter,
32 | PaypalConverter,
33 | Posting,
34 | )
35 |
36 |
37 | def test_format():
38 | assert (
39 | Posting("Foo", Amount(Decimal("10.00"), "$"), metadata={"foo": "bar"}).format(
40 | indent=2
41 | )
42 | == " Foo $10.00\n ; foo: bar\n"
43 | )
44 |
45 |
46 | def test_amount():
47 | assert (
48 | "$10.00" == Amount(Decimal("10.001"), "$").format()
49 | ), "Formats to 2 points precision by default"
50 | assert (
51 | "10.00 USD" == Amount(Decimal(10), "USD").format()
52 | ), "Longer commodity names come after"
53 | assert (
54 | "-$10.00" == Amount(Decimal(10), "$", reverse=True).format()
55 | ), "Reverse flag works."
56 | assert (
57 | '10.00 "ABC123"' == Amount(Decimal(10), "ABC123").format()
58 | ), "Currencies with numbers are quoted"
59 | assert (
60 | '10.00 "A BC"' == Amount(Decimal(10), "A BC").format()
61 | ), "Currencies with whitespace are quoted"
62 | assert "$10.001" == Amount(Decimal("10.001"), "$", unlimited=True).format()
63 |
64 |
65 | def test_get_csv_id():
66 | converter = CsvConverter(None)
67 | h = {"foo": "bar", "bar": "foo"}
68 | assert (
69 | converter.get_csv_id(h)
70 | == hashlib.md5("bar=foo\nfoo=bar\n".encode("utf-8")).hexdigest()
71 | )
72 |
73 |
74 | @pytest.fixture
75 | def reader(request):
76 | csv_name = request.node.get_closest_marker("csv_file").args[0]
77 | with open(os.path.join("fixtures", csv_name), "r") as csv_file:
78 | dialect = csv.Sniffer().sniff(csv_file.read(1024))
79 | csv_file.seek(0)
80 | dialect.skipinitialspace = True
81 | reader = csv.DictReader(csv_file, dialect=dialect)
82 | csv_file.seek(0)
83 | reader = csv.DictReader(csv_file, dialect=dialect)
84 | yield reader
85 |
86 |
87 | @pytest.fixture
88 | def converter(reader):
89 | return CsvConverter.make_converter(set(reader.fieldnames), reader.dialect, "Foo")
90 |
91 |
92 | @pytest.mark.csv_file("paypal.csv")
93 | def test_paypal_format(reader, converter):
94 | assert type(converter) == PaypalConverter
95 | assert (
96 | converter.convert(next(reader)).format()
97 | == """2016/06/04 Jane Doe someone@example.net My Friend ID: XYZ1, Recurring Payment Sent
98 | Foo -20.00 USD
99 | ; csvid: paypal.XYZ1
100 | Expenses:Misc 20.00 USD
101 | """
102 | )
103 | assert (
104 | converter.convert(next(reader)).format()
105 | == """2016/06/04 Debit Card ID: XYZ2, Charge From Debit Card
106 | Foo 1120.00 USD
107 | ; csvid: paypal.XYZ2
108 | Transfer:Paypal -1120.00 USD
109 | """
110 | )
111 |
112 |
113 | @pytest.mark.csv_file("paypal_alternate.csv")
114 | def test_paypal_alternate_format(reader, converter):
115 | assert type(converter) == PaypalAlternateConverter
116 | assert (
117 | converter.convert(next(reader)).format()
118 | == """2016/12/31 Some User: Payment Sent
119 | Foo -$12.34
120 | ; csvid: 1209a7bb0d17276248d463b71a6a8b8c
121 | Expenses:Misc $12.34
122 | """
123 | )
124 | assert (
125 | converter.convert(next(reader)).format()
126 | == """2016/12/31 Bank Account: Add Funds from a Bank Account
127 | Foo $12.34
128 | ; csvid: 581e62da71bab74c7ce61854c2b6b6a5
129 | Transfer:Paypal -$12.34
130 | """
131 | )
132 |
133 |
134 | def test_mk_amount_alternate():
135 | converter = PaypalAlternateConverter(None)
136 | row = {"Currency": "USD", "Amount": "12.34"}
137 | assert converter.mk_amount(row) == Amount(Decimal("12.34"), "USD")
138 |
139 |
140 | @pytest.mark.csv_file("amazon.csv")
141 | def test_amazon_format(reader, converter):
142 | assert type(converter) == AmazonConverter
143 | assert (
144 | converter.convert(next(reader)).format()
145 | == """2016/01/29 Best Soap Ever
146 | Foo $21.90
147 | ; csvid: amazon.123-4567890-1234567
148 | ; url: https://www.amazon.com/gp/css/summary/print.html/ref=od_aui_print_invoice?ie=UTF8&orderID=123-4567890-1234567
149 | Expenses:Misc -$21.90
150 | """
151 | )
152 |
153 |
154 | @pytest.mark.csv_file("amazon2.csv")
155 | def test_amazon2_format(reader, converter):
156 | assert type(converter) == AmazonConverter
157 | assert (
158 | converter.convert(next(reader)).format()
159 | == """2017/06/05 Test " double quote
160 | Foo $9.99
161 | ; csvid: amazon.111-1111111-1111111
162 | ; url: https://www.amazon.com/gp/css/summary/print.html/ref=od_aui_print_invoice?ie=UTF8&orderID=111-1111111-1111111
163 | Expenses:Misc -$9.99
164 | """
165 | )
166 |
167 |
168 | @pytest.mark.csv_file("mint.csv")
169 | def test_mint_format(reader):
170 | converter = CsvConverter.make_converter(set(reader.fieldnames), reader.dialect)
171 | assert type(converter) == MintConverter
172 | assert (
173 | converter.convert(next(reader)).format()
174 | == """2016/08/02 Amazon
175 | 1234 $29.99
176 | ; csvid: mint.a7c028a73d76956453dab634e8e5bdc1
177 | Expenses:Shopping -$29.99
178 | """
179 | )
180 | assert (
181 | converter.convert(next(reader)).format()
182 | == """2016/06/02 Autopay Rautopay Auto
183 | 1234 -$123.45
184 | ; csvid: mint.a404e70594502dd62bfc6f15d80b7cd7
185 | Credit Card Payment $123.45
186 | """
187 | )
188 |
--------------------------------------------------------------------------------
/tests/test_ledger.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 |
20 | import os
21 | import os.path
22 | import tempfile
23 |
24 | import pytest
25 |
26 | from ledgerautosync.ledgerwrap import Ledger, LedgerPython
27 |
28 |
29 | @pytest.mark.lgr_file("checking.lgr")
30 | def test_check_transaction(ledger):
31 | assert ledger.check_transaction_by_id("ofxid", "1101.1452687~7.0000486")
32 |
33 |
34 | @pytest.mark.lgr_file("checking.lgr")
35 | def test_nonexistent_transaction(ledger):
36 | assert not ledger.check_transaction_by_id("ofxid", "FOO")
37 |
38 |
39 | @pytest.mark.lgr_file("checking.lgr")
40 | def test_empty_transaction(ledger):
41 | assert ledger.check_transaction_by_id("ofxid", "empty")
42 |
43 |
44 | @pytest.mark.lgr_file("checking.lgr")
45 | def test_get_account_by_payee(ledger):
46 | account = ledger.get_account_by_payee(
47 | "AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )", exclude="Assets:Foo"
48 | )
49 | assert account == "Expenses:Bar"
50 |
51 |
52 | @pytest.mark.lgr_file("checking-dynamic-account.lgr")
53 | def test_get_ambiguous_account_by_payee(ledger):
54 | account = ledger.get_account_by_payee("Generic", exclude="Assets:Foo")
55 | # shoud use the latest
56 | assert account == "Expenses:Bar"
57 |
58 |
59 | @pytest.mark.lgr_file("checking.lgr")
60 | def test_ofx_payee_quoting(ledger):
61 | payees = [
62 | "PAYEE TEST/SLASH",
63 | "PAYEE TEST,COMMA",
64 | "PAYEE TEST:COLON",
65 | "PAYEE TEST*STAR",
66 | "PAYEE TEST#HASH",
67 | "PAYEE TEST.PERIOD",
68 | ]
69 | for payee in payees:
70 | assert (
71 | ledger.get_account_by_payee(payee, ["Assets:Foo"]) is not None
72 | ), "Did not find %s in %s" % (payee, ledger)
73 |
74 |
75 | # TODO Broken on current hledger
76 | @pytest.mark.lgr_file("checking.lgr")
77 | @pytest.mark.ledger_impls([Ledger, LedgerPython])
78 | def test_ofx_payee_quote_quote(ledger):
79 | payees = [
80 | 'PAYEE TEST"QUOTE',
81 | ]
82 | for payee in payees:
83 | assert (
84 | ledger.get_account_by_payee(payee, ["Assets:Foo"]) is not None
85 | ), "Did not find %s in %s" % (payee, ledger)
86 |
87 |
88 | @pytest.mark.lgr_file("checking.lgr")
89 | def test_ofx_id_quoting(ledger):
90 | assert (
91 | ledger.check_transaction_by_id("ofxid", "1/2") is True
92 | ), "Did not find 1/2 in %s" % (ledger)
93 |
94 |
95 | @pytest.mark.lgr_file("checking.lgr")
96 | def test_load_payees(ledger):
97 | ledger.load_payees()
98 | assert ledger.payees["PAYEE TEST:COLON"] == ["Assets:Foo", "Income:Bar"]
99 |
100 |
101 | @pytest.mark.lgr_file("empty.lgr")
102 | def test_load_payees_with_empty_ledger(ledger):
103 | ledger.load_payees()
104 |
105 |
106 | # class TestLedger(LedgerTest, TestCase):
107 | # def setUp(ledger):
108 | # self.empty_lgr = Ledger(os.path.join("fixtures", "empty.lgr"), no_pipe=True)
109 | # ledger = Ledger(self.ledger_path, no_pipe=True)
110 | # self.dynamic_lgr = Ledger(self.dynamic_ledger_path, no_pipe=True)
111 |
112 |
113 | @pytest.mark.lgr_file("checking.lgr")
114 | @pytest.mark.ledger_impls([Ledger])
115 | def test_args_only(ledger):
116 | (f, tmprcpath) = tempfile.mkstemp(".ledgerrc")
117 | os.close(f) # Who wants to deal with low-level file descriptors?
118 | # Create an init file that will narrow the test data to a period that
119 | # contains no trasnactions
120 | with open(tmprcpath, "w") as f:
121 | f.write("--period 2012")
122 | # If the command returns no trasnactions, as we would expect if we
123 | # parsed the init file, then this will throw an exception.
124 | next(ledger.run([""]))
125 | os.unlink(tmprcpath)
126 |
--------------------------------------------------------------------------------
/tests/test_ofx_formatter.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 | import re
20 |
21 | import pytest
22 |
23 | from ledgerautosync.converter import OfxConverter, SecurityList
24 | from ledgerautosync.ledgerwrap import Ledger
25 |
26 |
27 | def clean_posting(posting):
28 | return re.sub(" +", " ", posting)
29 |
30 |
31 | def make_clean_posting(converter, txn):
32 | return clean_posting(converter.convert(txn).format())
33 |
34 |
35 | @pytest.mark.ofx_file("checking.ofx")
36 | def test_checking(ofx):
37 | converter = OfxConverter(account=ofx.account, name="Foo")
38 | assert (
39 | make_clean_posting(converter, ofx.account.statement.transactions[0])
40 | == """2011/03/31 DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
41 | Foo $0.01
42 | ; ofxid: 1101.1452687~7.0000486
43 | Expenses:Misc -$0.01
44 | """
45 | )
46 | assert (
47 | make_clean_posting(converter, ofx.account.statement.transactions[1])
48 | == """2011/04/05 AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
49 | Foo -$34.51
50 | ; ofxid: 1101.1452687~7.0000487
51 | Expenses:Misc $34.51
52 | """
53 | )
54 |
55 | assert (
56 | make_clean_posting(converter, ofx.account.statement.transactions[2])
57 | == """2011/04/07 (319) RETURNED CHECK FEE, CHECK # 319 FOR $45.33 ON 04/07/11
58 | Foo -$25.00
59 | ; ofxid: 1101.1452687~7.0000488
60 | Expenses:Misc $25.00
61 | """
62 | )
63 |
64 |
65 | @pytest.mark.ofx_file("checking.ofx")
66 | def test_indent(ofx):
67 | converter = OfxConverter(account=ofx.account, name="Foo", indent=4)
68 | # testing indent, so do not use the string collapsing version of assert
69 | assert (
70 | converter.convert(ofx.account.statement.transactions[0]).format()
71 | == """2011/03/31 DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
72 | Foo $0.01
73 | ; ofxid: 1101.1452687~7.0000486
74 | Expenses:Misc -$0.01
75 | """
76 | )
77 |
78 |
79 | @pytest.mark.ofx_file("checking.ofx")
80 | def test_shortenaccount(ofx):
81 | converter = OfxConverter(
82 | account=ofx.account, name="Foo", indent=4, shortenaccount=True
83 | )
84 | # testing indent, so do not use the string collapsing version of assert
85 | assert (
86 | converter.convert(ofx.account.statement.transactions[0]).format()
87 | == """2011/03/31 DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
88 | Foo $0.01
89 | ; ofxid: 1101.87~7.0000486
90 | Expenses:Misc -$0.01
91 | """
92 | )
93 |
94 |
95 | @pytest.mark.ofx_file("checking.ofx")
96 | def test_hardcodeaccount(ofx):
97 | converter = OfxConverter(
98 | account=ofx.account, name="Foo", indent=4, hardcodeaccount="9999"
99 | )
100 | # testing indent, so do not use the string collapsing version of assert
101 | assert (
102 | converter.convert(ofx.account.statement.transactions[0]).format()
103 | == """2011/03/31 DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
104 | Foo $0.01
105 | ; ofxid: 1101.9999.0000486
106 | Expenses:Misc -$0.01
107 | """
108 | )
109 |
110 |
111 | @pytest.mark.ofx_file("fidelity.ofx")
112 | def test_investments(ofx):
113 | converter = OfxConverter(
114 | account=ofx.account,
115 | name="Foo",
116 | security_list=SecurityList(ofx),
117 | )
118 | assert (
119 | make_clean_posting(converter, ofx.account.statement.transactions[0])
120 | == """2012/07/20 YOU BOUGHT
121 | Foo 100.00000 INTC @ $25.635000000
122 | ; ofxid: 7776.01234567890.0123456789020201120120720
123 | Assets:Unknown -$2563.50
124 | Expenses:Commission $7.95
125 | """
126 | )
127 | # test no payee/memo
128 | assert (
129 | make_clean_posting(converter, ofx.account.statement.transactions[1])
130 | == """2012/07/27 Foo: buystock
131 | Foo 128.00000 SDRL @ $39.390900000
132 | ; ofxid: 7776.01234567890.0123456789020901120120727
133 | Assets:Unknown -$5042.04
134 | Expenses:Commission $7.95
135 | """
136 | )
137 |
138 |
139 | @pytest.mark.ofx_file("fidelity_fee.ofx")
140 | def test_fee(ofx):
141 | """Test that fees are parsed correctly.
142 |
143 | In this case we have a 7-cent fee. We need to make sure that
144 | the net sale price which shows up is the gross price of 3239.44
145 | minus 7 cents which equals 3239.37 and that the 7 cent fee
146 | shows up as an extra posting.
147 | """
148 | converter = OfxConverter(
149 | account=ofx.account, name="Foo", security_list=SecurityList(ofx)
150 | )
151 | # test fee
152 | assert (
153 | make_clean_posting(converter, ofx.account.statement.transactions[1])
154 | == """2012/08/01 SELL
155 | Foo -100.0 "929042109" @ $32.3944
156 | ; ofxid: 7776.01234567890.0123456789021401420120801
157 | Assets:Unknown $3239.37
158 | Expenses:Fees $0.07
159 | """
160 | )
161 | # test fee and comission
162 | assert (
163 | make_clean_posting(converter, ofx.account.statement.transactions[0])
164 | == """2020/05/22=2020/05/26 SELL
165 | Foo -1.0 "Z9977810Z" @ $8.27
166 | ; ofxid: 7776.01234567890.987654321
167 | Assets:Unknown $8.25
168 | Expenses:Fees $0.02
169 | Expenses:Commission $1.00
170 | """
171 | )
172 |
173 |
174 | @pytest.mark.ofx_file("checking.ofx")
175 | @pytest.mark.lgr_file("checking-dynamic-account.lgr")
176 | def test_dynamic_account(ofx, ledger):
177 | converter = OfxConverter(account=ofx.account, name="Assets:Foo", ledger=ledger)
178 | assert (
179 | make_clean_posting(converter, ofx.account.statement.transactions[1])
180 | == """2011/04/05 AUTOMATIC WITHDRAWAL, ELECTRIC BILL WEB(S )
181 | Assets:Foo -$34.51
182 | ; ofxid: 1101.1452687~7.0000487
183 | Expenses:Bar $34.51
184 | """
185 | )
186 |
187 |
188 | @pytest.mark.ofx_file("checking.ofx")
189 | @pytest.mark.lgr_file("checking.lgr")
190 | def test_balance_assertion(ofx, ledger):
191 | converter = OfxConverter(account=ofx.account, name="Assets:Foo", ledger=ledger)
192 | assert (
193 | clean_posting(converter.format_balance(ofx.account.statement))
194 | == """2013/05/25 * --Autosync Balance Assertion
195 | Assets:Foo $0.00 = $100.99
196 | """
197 | )
198 |
199 |
200 | @pytest.mark.ofx_file("checking.ofx")
201 | @pytest.mark.lgr_file("checking.lgr")
202 | def test_initial_balance(ofx, ledger):
203 | converter = OfxConverter(account=ofx.account, name="Assets:Foo", ledger=ledger)
204 | assert (
205 | clean_posting(converter.format_initial_balance(ofx.account.statement))
206 | == """2000/01/01 * --Autosync Initial Balance
207 | Assets:Foo $160.49
208 | ; ofxid: 1101.1452687~7.autosync_initial
209 | Assets:Equity -$160.49
210 | """
211 | )
212 |
213 |
214 | @pytest.mark.ofx_file("checking.ofx")
215 | def test_unknownaccount(ofx):
216 | converter = OfxConverter(
217 | account=ofx.account, name="Foo", unknownaccount="Expenses:Unknown"
218 | )
219 | assert (
220 | make_clean_posting(converter, ofx.account.statement.transactions[0])
221 | == """2011/03/31 DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%
222 | Foo $0.01
223 | ; ofxid: 1101.1452687~7.0000486
224 | Expenses:Unknown -$0.01
225 | """
226 | )
227 |
228 |
229 | @pytest.mark.ofx_file("fidelity.ofx")
230 | def test_quote_commodity(ofx):
231 | converter = OfxConverter(
232 | account=ofx.account, name="Foo", security_list=SecurityList(ofx)
233 | )
234 | assert (
235 | make_clean_posting(converter, ofx.account.statement.transactions[0])
236 | == """2012/07/20 YOU BOUGHT
237 | Foo 100.00000 INTC @ $25.635000000
238 | ; ofxid: 7776.01234567890.0123456789020201120120720
239 | Assets:Unknown -$2563.50
240 | Expenses:Commission $7.95
241 | """
242 | )
243 |
244 |
245 | # Check that txns are parsed.
246 | @pytest.mark.ofx_file("investment_401k.ofx")
247 | def test_transfer_txn(ofx):
248 | converter = OfxConverter(
249 | account=ofx.account,
250 | name="Foo",
251 | unknownaccount="Expenses:Unknown",
252 | )
253 | if len(ofx.account.statement.transactions) > 2:
254 | # older versions of ofxparse would skip these transactions
255 | if hasattr(ofx.account.statement.transactions[2], "tferaction"):
256 | # unmerged pull request
257 | assert (
258 | make_clean_posting(converter, ofx.account.statement.transactions[2])
259 | == """2014/06/30 Foo: transfer: out
260 | Foo -9.060702 BAZ @ $21.928764
261 | ; ofxid: 1234.12345678.123456-01.3
262 | Transfer $198.69
263 | """
264 | )
265 | else:
266 | assert (
267 | make_clean_posting(converter, ofx.account.statement.transactions[2])
268 | == """2014/06/30 Foo: transfer
269 | Foo -9.060702 BAZ @ $21.928764
270 | ; ofxid: 1234.12345678.123456-01.3
271 | Transfer $198.69
272 | """
273 | )
274 |
275 |
276 | @pytest.mark.ofx_file("cusip.ofx")
277 | def test_position(ofx):
278 | converter = OfxConverter(
279 | account=ofx.account,
280 | name="Foo",
281 | indent=4,
282 | unknownaccount="Expenses:Unknown",
283 | security_list=SecurityList(ofx),
284 | )
285 | assert (
286 | converter.format_position(ofx.account.statement.positions[0])
287 | == """P 2016/10/08 07:30:08 SHSAX 47.8600000
288 | """
289 | )
290 |
291 |
292 | @pytest.mark.ofx_file("income.ofx")
293 | def test_dividend(ofx):
294 | converter = OfxConverter(account=ofx.account, name="Foo")
295 | assert (
296 | make_clean_posting(converter, ofx.account.statement.transactions[0])
297 | == """2016/10/12 DIVIDEND RECEIVED
298 | ; dividend_from: cusip_redacted
299 | Foo $1234.56
300 | ; ofxid: 1234.12345678.123456-01.redacted
301 | Income:Dividends -$1234.56
302 | """
303 | )
304 |
305 |
306 | @pytest.mark.ofx_file("checking.ofx")
307 | def test_checking_custom_payee(ofx):
308 | converter = OfxConverter(account=ofx.account, name="Foo", payee_format="{memo}")
309 | assert (
310 | converter.format_payee(ofx.account.statement.transactions[0])
311 | == "DIVIDEND EARNED FOR PERIOD OF 03/01/2011 THROUGH 03/31/2011 ANNUAL PERCENTAGE YIELD EARNED IS 0.05%"
312 | )
313 | converter = OfxConverter(account=ofx.account, name="Foo", payee_format="{payee}")
314 | assert (
315 | converter.format_payee(ofx.account.statement.transactions[0])
316 | == "DIVIDEND EARNED FOR PERIOD OF 03"
317 | )
318 | converter = OfxConverter(account=ofx.account, name="Foo", payee_format="{account}")
319 | assert converter.format_payee(ofx.account.statement.transactions[0]) == "Foo"
320 | converter = OfxConverter(
321 | account=ofx.account, name="Foo", payee_format=" {account} "
322 | )
323 | assert converter.format_payee(ofx.account.statement.transactions[0]) == "Foo"
324 |
325 |
326 | @pytest.mark.ofx_file("investment_401k.ofx")
327 | def test_investments_custom_payee(ofx):
328 | converter = OfxConverter(account=ofx.account, name="Foo", payee_format="{txntype}")
329 | assert converter.format_payee(ofx.account.statement.transactions[1]) == "transfer"
330 | converter = OfxConverter(
331 | account=ofx.account, name="Foo", payee_format="{tferaction}"
332 | )
333 | assert converter.format_payee(ofx.account.statement.transactions[1]) == "in"
334 |
335 |
336 | @pytest.mark.ofx_file("checking-payee-match.ofx")
337 | @pytest.mark.lgr_file("checking.lgr")
338 | @pytest.mark.ledger_impls([Ledger])
339 | def test_payee_match(ofx, ledger):
340 | converter = OfxConverter(account=ofx.account, name="Foo", ledger=ledger)
341 | assert (
342 | make_clean_posting(converter, ofx.account.statement.transactions[0])
343 | == """2011/03/31 Match Payee
344 | Foo -$0.01
345 | ; ofxid: 1101.1452687~7.0000489
346 | Expenses:Bar $0.01
347 | """
348 | )
349 |
--------------------------------------------------------------------------------
/tests/test_sync.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2013-2021 Erik Hetzner
2 | #
3 | # This file is part of ledger-autosync
4 | #
5 | # ledger-autosync is free software: you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # ledger-autosync is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with ledger-autosync. If not, see
17 | # .
18 |
19 |
20 | import os
21 | import os.path
22 | from unittest.mock import Mock
23 |
24 | from ofxparse import OfxParser
25 |
26 | from ledgerautosync.ledgerwrap import Ledger
27 | from ledgerautosync.sync import CsvSynchronizer, OfxSynchronizer
28 |
29 |
30 | def test_fresh_sync():
31 | ledger = Ledger(os.path.join("fixtures", "empty.lgr"))
32 | sync = OfxSynchronizer(ledger)
33 | with open(os.path.join("fixtures", "checking.ofx"), "rb") as ofx_file:
34 | ofx = OfxParser.parse(ofx_file)
35 | txns1 = ofx.account.statement.transactions
36 | txns2 = sync.filter(txns1, ofx.account.account_id)
37 | assert txns1 == txns2
38 |
39 |
40 | def test_sync_order():
41 | ledger = Ledger(os.path.join("fixtures", "empty.lgr"))
42 | sync = OfxSynchronizer(ledger)
43 | with open(os.path.join("fixtures", "checking_order.ofx"), "rb") as ofx_file:
44 | ofx = OfxParser.parse(ofx_file)
45 | txns = sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
46 | assert txns[0].date < txns[1].date
47 | assert txns[1].date < txns[2].date
48 |
49 |
50 | def test_fully_synced():
51 | ledger = Ledger(os.path.join("fixtures", "checking.lgr"))
52 | sync = OfxSynchronizer(ledger)
53 | ofx = OfxSynchronizer.parse_file(os.path.join("fixtures", "checking.ofx"))
54 | txns = sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
55 | assert txns == []
56 |
57 |
58 | def test_partial_sync():
59 | ledger = Ledger(os.path.join("fixtures", "checking-partial.lgr"))
60 | sync = OfxSynchronizer(ledger)
61 | ofx = OfxSynchronizer.parse_file(os.path.join("fixtures", "checking.ofx"))
62 | txns = sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
63 | assert len(txns) == 1
64 |
65 |
66 | def test_no_new_txns():
67 | ledger = Ledger(os.path.join("fixtures", "checking.lgr"))
68 | acct = Mock()
69 | acct.download = Mock(
70 | return_value=open(os.path.join("fixtures", "checking.ofx"), "rb")
71 | )
72 | sync = OfxSynchronizer(ledger)
73 | assert len(sync.get_new_txns(acct, 7, 7)[1]) == 0
74 |
75 |
76 | def test_all_new_txns():
77 | ledger = Ledger(os.path.join("fixtures", "empty.lgr"))
78 | acct = Mock()
79 | acct.download = Mock(
80 | return_value=open(os.path.join("fixtures", "checking.ofx"), "rb")
81 | )
82 | sync = OfxSynchronizer(ledger)
83 | assert len(sync.get_new_txns(acct, 7, 7)[1]) == 3
84 |
85 |
86 | def test_comment_txns():
87 | ledger = Ledger(os.path.join("fixtures", "empty.lgr"))
88 | sync = OfxSynchronizer(ledger)
89 | ofx = OfxSynchronizer.parse_file(os.path.join("fixtures", "comments.ofx"))
90 | txns = sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
91 | assert len(txns) == 1
92 |
93 |
94 | def test_sync_no_ledger():
95 | acct = Mock()
96 | acct.download = Mock(
97 | return_value=open(os.path.join("fixtures", "checking.ofx"), "rb")
98 | )
99 | sync = OfxSynchronizer(None)
100 | assert len(sync.get_new_txns(acct, 7, 7)[1]) == 3
101 |
102 |
103 | def test_paypal_fresh_sync():
104 | ledger = Ledger(os.path.join("fixtures", "empty.lgr"))
105 | sync = CsvSynchronizer(ledger)
106 | assert 2 == len(sync.parse_file(os.path.join("fixtures", "paypal.csv")))
107 |
108 |
109 | def test_sync_paypal_no_ledger():
110 | sync = CsvSynchronizer(None)
111 | assert 2 == len(sync.parse_file(os.path.join("fixtures", "paypal.csv")))
112 |
113 |
114 | def test_paypal_partial_sync():
115 | ledger = Ledger(os.path.join("fixtures", "paypal.lgr"))
116 | sync = CsvSynchronizer(ledger)
117 | assert 1 == len(sync.parse_file(os.path.join("fixtures", "paypal.csv")))
118 |
--------------------------------------------------------------------------------
/tests/test_weird_ofx.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright (c) 2013-2021 Erik Hetzner
3 | #
4 | # This file is part of ledger-autosync
5 | #
6 | # ledger-autosync is free software: you can redistribute it and/or
7 | # modify it under the terms of the GNU General Public License as
8 | # published by the Free Software Foundation, either version 3 of the
9 | # License, or (at your option) any later version.
10 | #
11 | # ledger-autosync is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 | # General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with ledger-autosync. If not, see
18 | # .
19 |
20 |
21 | import os.path
22 |
23 | import pytest
24 | from ofxclient.config import OfxConfig
25 |
26 | from ledgerautosync import EmptyInstitutionException
27 | from ledgerautosync.cli import run
28 | from ledgerautosync.converter import OfxConverter
29 | from ledgerautosync.sync import OfxSynchronizer
30 |
31 |
32 | @pytest.fixture
33 | def ofx_sync(ledger):
34 | return OfxSynchronizer(ledger)
35 |
36 |
37 | @pytest.mark.lgr_file("empty.lgr")
38 | def test_no_institution_no_fid(ledger):
39 | with pytest.raises(EmptyInstitutionException):
40 | config = OfxConfig(os.path.join("fixtures", "ofxclient.ini"))
41 | run(
42 | [
43 | os.path.join("fixtures", "no-institution.ofx"),
44 | "-l",
45 | os.path.join("fixtures", "empty.lgr"),
46 | "-a",
47 | "Assets:Savings:Foo",
48 | ],
49 | config,
50 | )
51 |
52 |
53 | @pytest.mark.ofx_file("no-institution.ofx")
54 | @pytest.mark.lgr_file("empty.lgr")
55 | def test_no_institution(ofx, ofx_sync):
56 | txns = ofx_sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
57 | assert len(txns) == 3
58 |
59 |
60 | @pytest.mark.ofx_file("no-institution.ofx")
61 | @pytest.mark.lgr_file("empty.lgr")
62 | def test_no_institution_no_accountname(ofx):
63 | with pytest.raises(EmptyInstitutionException):
64 | OfxConverter(account=ofx.account, name=None)
65 |
66 |
67 | @pytest.mark.ofx_file("apostrophe.ofx")
68 | @pytest.mark.lgr_file("empty.lgr")
69 | def test_apostrophe(ofx, ofx_sync):
70 | txns = ofx_sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
71 | assert len(txns) == 1
72 |
73 |
74 | @pytest.mark.ofx_file("fidelity-one-dtsettle.ofx")
75 | @pytest.mark.lgr_file("empty.lgr")
76 | def test_one_settleDate(ofx, ofx_sync):
77 | txns = ofx_sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
78 | assert len(txns) == 17
79 |
80 |
81 | @pytest.mark.ofx_file("accented_characters_latin1.ofx")
82 | @pytest.mark.lgr_file("empty.lgr")
83 | def test_accented_characters_latin1(ofx, ofx_sync):
84 | txns = ofx_sync.filter(ofx.account.statement.transactions, ofx.account.account_id)
85 | converter = OfxConverter(account=ofx.account, name="Foo")
86 | assert converter.format_payee(txns[0]) == "Virement Interac à: Jean"
87 | assert len(txns) == 1
88 |
--------------------------------------------------------------------------------