Skip to content

Commit

Permalink
feat: configurable balance assertion dates
Browse files Browse the repository at this point in the history
  • Loading branch information
redstreet committed Sep 9, 2023
1 parent 601929c commit 3b57229
Show file tree
Hide file tree
Showing 25 changed files with 390 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from beancount_reds_importers.libreader import csvreader
from beancount_reds_importers.libtransactionbuilder import banking
import datetime


class Importer(csvreader.Importer, banking.Importer):
Expand Down Expand Up @@ -50,7 +49,6 @@ def prepare_table(self, rdr):
def get_balance_statement(self, file=None):
"""Return the balance on the first and last dates"""

max_date = self.get_max_transaction_date()
if max_date:
date = max_date + datetime.timedelta(days=1)
date = self.get_balance_assertion_date()
if date:
yield banking.Balance(date, self.rdr.namedtuples()[0].balance, self.currency)
7 changes: 2 additions & 5 deletions beancount_reds_importers/importers/stanchart/scbbank.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from beancount_reds_importers.libreader import csvreader
from beancount_reds_importers.libtransactionbuilder import banking
import datetime
import re
from beancount.core.number import D

Expand Down Expand Up @@ -51,8 +50,8 @@ def prepare_raw_file(self, rdr):

def get_balance_statement(self, file=None):
"""Return the balance on the first and last dates"""
max_date = self.get_max_transaction_date()
if max_date:
date = self.get_balance_assertion_date
if date:
rdr = self.read_raw(file)
rdr = self.prepare_raw_file(rdr)
col_labels = self.balance_column_labels_line.split(',')
Expand All @@ -64,8 +63,6 @@ def get_balance_statement(self, file=None):
while '' in rdr.header():
rdr = rdr.cutout('')

date = max_date + datetime.timedelta(days=1)

row = rdr.namedtuples()[0]
amount = row.Current_Balance
units, debitcredit = amount.split()
Expand Down
7 changes: 2 additions & 5 deletions beancount_reds_importers/importers/stanchart/scbcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from beancount_reds_importers.libreader import csvreader
from beancount_reds_importers.libtransactionbuilder import banking
import datetime
import re
from beancount.core.number import D

Expand Down Expand Up @@ -67,14 +66,12 @@ def prepare_raw_file(self, rdr):

def get_balance_statement(self, file=None):
"""Return the balance on the first and last dates"""
max_date = self.get_max_transaction_date()
if max_date:
date = self.get_balance_assertion_date()
if date:
balance_row = self.get_row_by_label(file, 'Current Balance')
currency, amount = balance_row[1], balance_row[2]
units, debitcredit = amount.split()
if debitcredit != 'CR':
units = '-' + units

date = max_date + datetime.timedelta(days=1)

yield banking.Balance(date, D(units), currency)
7 changes: 2 additions & 5 deletions beancount_reds_importers/importers/unitedoverseas/uobbank.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from beancount_reds_importers.libreader import xlsreader
from beancount_reds_importers.libtransactionbuilder import banking
import datetime
import re
from beancount.core.number import D

Expand Down Expand Up @@ -52,11 +51,9 @@ def prepare_raw_file(self, rdr):

def get_balance_statement(self, file=None):
"""Return the balance on the first and last dates"""
max_date = self.get_max_transaction_date()
if max_date:
date = self.get_balance_assertion_date()
if date:
row = self.rdr.namedtuples()[0]
date = max_date + datetime.timedelta(days=1)

# Get currency from input file
currency = self.get_row_by_label(file, 'Account Number:')[2]

Expand Down
8 changes: 2 additions & 6 deletions beancount_reds_importers/importers/unitedoverseas/uobcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from beancount_reds_importers.libreader import xlsreader
from beancount_reds_importers.libtransactionbuilder import banking
import datetime
import re
from beancount.core.number import D

Expand Down Expand Up @@ -69,11 +68,8 @@ def prepare_raw_file(self, rdr):

def get_balance_statement(self, file=None):
"""Return the balance on the first and last dates"""
max_date = self.get_max_transaction_date()
if max_date:
date = self.get_balance_assertion_date
if date:
balance_row = self.get_row_by_label(file, 'Statement Balance:')
units, currency = balance_row[1], balance_row[2]

date = max_date + datetime.timedelta(days=1)

yield banking.Balance(date, -1 * D(str(units)), currency)
44 changes: 32 additions & 12 deletions beancount_reds_importers/libreader/ofxreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,31 +76,51 @@ def get_available_cash(self, settlement_fund_balance=0):
return available_cash - settlement_fund_balance
return None

def get_balance_assertion_date(self):
def get_ofx_end_date(self):
end_date = self.ofx_account.statement.end_date
# convert end_date from utc to local timezone
end_date = end_date.replace(tzinfo=datetime.timezone.utc).astimezone().date()
return end_date

def get_smart_date(self):
""" We find the statement's end date from the OFX file. However, banks and credit cards
typically have pending transactions that are not included in downloads. When we download
the next statement, new transactions may appear prior to the balance assertion date that we
generate for this statement. To attempt to avoid this, we set the balance assertion date to
either two days before the statement's end date or the last transaction's date, whichever
is later.
Finally, we add an additional day, since Beancount balance assertions are defined to occur
on the beginning of the assertion date.
"""

end_date = self.ofx_account.statement.end_date
# convert end_date from utc to local timezone
end_date = end_date.replace(tzinfo=datetime.timezone.utc).astimezone().date()
end_date -= datetime.timedelta(days=getattr(self, 'balance_assertion_date_fudge', 2))
end_date = self.get_ofx_end_date()
end_date -= datetime.timedelta(days=self.config.get('balance_assertion_date_fudge', 2))

max_transaction_date = self.get_max_transaction_date()
max_transaction_date = max_transaction_date if max_transaction_date else datetime.date.min
return_date = max(end_date, max_transaction_date)

# As defined by Beancount
return_date += datetime.timedelta(days=1)
return return_date

def get_balance_assertion_date(self):
""" Choices for the date of the generated balance assertion can be specified in
self.config['balance_assertion_date_type'], which can be:
- 'smart': smart date (default)
- 'ofx_date': date specified in ofx file
- 'last_transaction': max transaction date
- 'today': today's date
If you want something else, simply override this method in individual importer
Finally, we add an additional day, since Beancount balance assertions are defined to occur
on the beginning of the assertion date.
"""

date_type_map = {'smart': self.get_smart_date,
'ofx_date': self.get_ofx_end_date,
'last_transaction': self.get_max_transaction_date,
'today': datetime.date.today}
date_type = self.config.get('balance_assertion_date_type', 'smart')
return_date = date_type_map[date_type]()

return return_date + datetime.timedelta(days=1) # Next day, as defined by Beancount

def get_max_transaction_date(self):
"""
Here, we find the last transaction's date. If we use the ofx download date (if our source is ofx), we
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from os import path
from beancount.ingest import regression_pytest as regtest
from beancount_reds_importers.importers import ally


@regtest.with_importer(
ally.Importer({
"account_number": "23456",
"main_account": "Assets:Banks:Checking",
"balance_assertion_date_type": "last_transaction",
})
)
@regtest.with_testdir(path.dirname(__file__))
class TestSmart(regtest.ImporterTestBase):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>
0</CODE>
<SEVERITY>
INFO</SEVERITY>
</STATUS>
<DTSERVER>
20150102170000.000[+0:UTC]</DTSERVER>
<LANGUAGE>
ENG</LANGUAGE>
<FI>
<ORG>
Great Bank of Orangeland</ORG>
<FID>
666</FID>
</FI>
</SONRS>
</SIGNONMSGSRSV1>
<BANKMSGSRSV1>
<STMTTRNRS>
<TRNUID>
5678</TRNUID>
<STATUS>
<CODE>
0</CODE>
<SEVERITY>
INFO</SEVERITY>
</STATUS>
<STMTRS>
<CURDEF>
USD</CURDEF>
<BANKACCTFROM>
<BANKID>
123456789</BANKID>
<ACCTID>
23456</ACCTID>
<ACCTTYPE>
CHECKING</ACCTTYPE>
</BANKACCTFROM>


<BANKTRANLIST>
<DTSTART>
20190503000000</DTSTART>
<DTEND>
20190901000000</DTEND>

<STMTTRN>
<TRNTYPE>
XFER</TRNTYPE>
<DTPOSTED>
20190109121112</DTPOSTED>
<TRNAMT>
-55.00</TRNAMT>
<FITID>
21302970191741</FITID>
<NAME>
Transfer to savings account</NAME>
</STMTTRN>
</BANKTRANLIST>

<STMTTRN>
<TRNTYPE>
XFER</TRNTYPE>
<DTPOSTED>
20190109121112</DTPOSTED>
<TRNAMT>
-1000</TRNAMT>
<FITID>
21302970191741</FITID>
<NAME>
Check 23400</NAME>
</STMTTRN>
</BANKTRANLIST>


<LEDGERBAL>
<BALAMT>
150.65</BALAMT>
<DTASOF>
20150101000000.000[+0:UTC]</DTASOF>
</LEDGERBAL>
</STMTRS>
</STMTTRNRS>
</BANKMSGSRSV1>
</OFX>

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

2019-01-09 * "Transfer to savings account"
Assets:Banks:Checking -55.00 USD

2019-01-09 * "Check 23400"
Assets:Banks:Checking -1000 USD

2019-01-10 balance Assets:Banks:Checking 150.65 USD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Assets:Banks:Checking
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2019-09-01T00:00:00
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
transactions.qfx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from os import path
from beancount.ingest import regression_pytest as regtest
from beancount_reds_importers.importers import ally


@regtest.with_importer(
ally.Importer({
"account_number": "23456",
"main_account": "Assets:Banks:Checking",
"balance_assertion_date_type": "ofx_date",
})
)
@regtest.with_testdir(path.dirname(__file__))
class TestSmart(regtest.ImporterTestBase):
pass
Loading

0 comments on commit 3b57229

Please sign in to comment.