Skip to content

Commit

Permalink
Merge branch 'master' into feature/amazon-mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
jaraco authored Feb 18, 2024
2 parents 4cd5884 + a923dd8 commit c9f200b
Show file tree
Hide file tree
Showing 26 changed files with 1,117 additions and 32 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.idea/
example*.log
examples/.ipynb_checkpoints/*
.*.sw*

# C extensions
*.so
Expand Down
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,38 +80,41 @@ usage: csv2ofx [options] <source> <dest>
description: csv2ofx converts a csv file to ofx and qif

positional arguments:
source the source csv file (defaults to stdin)
dest the output file (defaults to stdout)
source the source csv file (default: stdin)
dest the output file (default: stdout)

optional arguments:
-h, --help show this help message and exit
-a TYPE, --account TYPE
default account type 'CHECKING' for OFX and 'Bank' for QIF.
-e DATE, --end DATE end date
-e DATE, --end DATE end date (default: today)
-B BALANCE, --ending-balance BALANCE
ending balance (default: None)
-l LANGUAGE, --language LANGUAGE
the language
the language (default: ENG)
-s DATE, --start DATE
the start date
-y, --dayfirst interpret the first value in ambiguous dates (e.g. 01/05/09) as the day
-m MAPPING, --mapping MAPPING
the account mapping
-m MAPPING_NAME, --mapping MAPPING_NAME
the account mapping (default: default)
-x FILE_PATH, --custom FILE_PATH
path to a custom mapping file
-c FIELD_NAME, --collapse FIELD_NAME
field used to combine transactions within a split for double entry statements
-C ROWS, --chunksize ROWS
number of rows to process at a time (default: 2 ** 14)
-r ROWS, --first-row ROWS
number of initial rows to skip (default: 0)
-r ROWS, --last-row ROWS
the final rows to process, negative values count from the end (default: inf)
the first row to process (zero based)
-R ROWS, --last-row ROWS
the last row to process (zero based, negative values count from the end)
-O COLS, --first-col COLS
number of initial cols to skip (default: 0)
the first column to process (zero based)
-L, --list-mappings list the available mappings
-V, --version show version and exit
-q, --qif enables 'QIF' output instead of 'OFX'
-M, --ms-money enables MS Money compatible 'OFX' output
-o, --overwrite overwrite destination file if it exists
-D SERVER_DATE, --server-date SERVER_DATE
-D DATE, --server-date DATE
OFX server date (default: source file mtime)
-E ENCODING, --encoding ENCODING
File encoding (default: utf-8)
Expand Down
12 changes: 11 additions & 1 deletion csv2ofx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
md5 = lambda content: hashlib.md5(content.encode("utf-8")).hexdigest()


class BalanceError(Exception):
"""Raised if no ending balance when MS Money compatible output requested"""
pass


class Content(object): # pylint: disable=too-many-instance-attributes
"""A transaction holding object"""

Expand Down Expand Up @@ -76,6 +81,7 @@ def __init__(self, mapping=None, **kwargs):
self.parse_fmt = kwargs.get("parse_fmt")
self.dayfirst = kwargs.get("dayfirst")
self.filter = kwargs.get("filter")
self.ms_money = kwargs.get("ms_money")
self.split_account = None
self.inv_split_account = None
self.id = None
Expand Down Expand Up @@ -226,7 +232,7 @@ def transaction_data(self, trxn): # pylint: disable=too-many-locals
... 'split_account': 'Checking', 'type': 'DEBIT',
... 'category': '', 'amount': Decimal('-1000.00'),
... 'memo': 'description notes', 'inv_split_account': None,
... 'x_action': ''}
... 'x_action': '', 'balance': None}
True
"""
account = self.get("account", trxn)
Expand All @@ -251,6 +257,9 @@ def transaction_data(self, trxn): # pylint: disable=too-many-locals
symbol = self.get("symbol", trxn, "")
price = Decimal(self.get("price", trxn, 0))
invest = shares or (symbol and symbol != "N/A") or "invest" in category
balance = self.get("balance", trxn)
if balance is not None:
balance = utils.convert_amount(balance)

if invest:
amount = abs(amount)
Expand Down Expand Up @@ -288,6 +297,7 @@ def transaction_data(self, trxn): # pylint: disable=too-many-locals
"id": self.get("id", trxn, check_num) or md5(details),
"check_num": check_num,
"type": _type,
"balance": balance,
}

def gen_trxns(self, groups, collapse=False):
Expand Down
21 changes: 19 additions & 2 deletions csv2ofx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from dateutil.parser import parse
from meza.io import read_csv, IterStringIO, write

from . import utils
from . import BalanceError, utils
from .ofx import OFX
from .qif import QIF

Expand Down Expand Up @@ -77,6 +77,13 @@
help="end date (default: today)",
default=str(dt.now()),
)
parser.add_argument(
"-B",
"--ending-balance",
metavar="BALANCE",
type=float,
help="ending balance (default: None)",
)
parser.add_argument(
"-l", "--language", help="the language (default: ENG)", default="ENG"
)
Expand Down Expand Up @@ -157,6 +164,13 @@
action="store_true",
default=False,
)
parser.add_argument(
"-M",
"--ms-money",
help="enables MS Money compatible 'OFX' output",
action="store_true",
default=False,
)
parser.add_argument(
"-o",
"--overwrite",
Expand Down Expand Up @@ -216,6 +230,7 @@ def run(): # noqa: C901
"def_type": args.account_type or "Bank" if args.qif else "CHECKING",
"start": parse(args.start, dayfirst=args.dayfirst) if args.start else None,
"end": parse(args.end, dayfirst=args.dayfirst) if args.end else None,
"ms_money": args.ms_money,
}

cont = QIF(mapping, **okwargs) if args.qif else OFX(mapping, **okwargs)
Expand Down Expand Up @@ -248,7 +263,7 @@ def run(): # noqa: C901
server_date = dt.fromtimestamp(mtime)

header = cont.header(date=server_date, language=args.language)
footer = cont.footer(date=server_date)
footer = cont.footer(date=server_date, balance=args.ending_balance)
filtered = filter(None, [header, body, footer])
content = it.chain.from_iterable(filtered)
kwargs = {
Expand Down Expand Up @@ -277,6 +292,8 @@ def run(): # noqa: C901
# csv2ofx called with no arguments or broken mapping
msg = "Possible mapping problem: %s. " % str(err)
parser.print_help()
except BalanceError as err:
msg = "%s. Try again with `--ending-balance` option." % err
except Exception: # pylint: disable=broad-except
msg = 1
traceback.print_exc()
Expand Down
29 changes: 29 additions & 0 deletions csv2ofx/mappings/schwabchecking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import (
absolute_import, division, print_function, unicode_literals)

from operator import itemgetter

# Schwab Bank includes three lines of text in their CSV which otherwise break
# date parsing. Filtering by lines with no "Type" (and faking out the date for
# those lines) avoids the problem.

# Note: Ideally, we could get account_id from the first line of the CSV.

mapping = {
'first_row': 1,
'has_header': True,
'filter': lambda tr: tr.get('Type'),
'is_split': False,
'bank': 'Charles Schwab Bank, N.A.',
'bank_id': '121202211',
'account_id': '12345', # Change to your actual account number if desired
'currency': 'USD',
'account': 'Charles Schwab Checking',
'date': lambda tr: tr.get('Date') if tr.get('Type') else "1-1-1970",
'check_num': itemgetter('Check #'),
'payee': itemgetter('Description'),
'desc': itemgetter('Description'),
'type': lambda tr: 'debit' if tr.get('Withdrawal (-)') != '' else 'credit',
'amount': lambda tr: tr.get('Deposit (+)') or tr.get('Withdrawal (-)'),
'balance': itemgetter('RunningBalance'),
}
Loading

0 comments on commit c9f200b

Please sign in to comment.