Skip to content

Commit

Permalink
Features/transaction notes (BittyTax#65)
Browse files Browse the repository at this point in the history
Transaction notes feature branch

An optional note can now be added per transaction, taken from the last column after the timestamp.

This note shoud describe what the transaction is for, for example, for a deposit or withdrawal it could indicate which wallet the transfer is going to/from, to describe what the spend was for, or what the income was from.

The transaction note will appear in the logging to help find any issues.

The note will also be used as a description field in the income tax report to make tracking income sources easier.

The new note field appears in the transaction records output file, this will be an empty field except for the following parsers:
Electrum, HandCash, Qt Wallet, Trezor where the original transaction label/note is copied into the transaction record note.
  • Loading branch information
nanonano authored Feb 15, 2021
1 parent 3e757bd commit 55cba2d
Show file tree
Hide file tree
Showing 15 changed files with 129 additions and 64 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Change Log
## [Unreleased]
Important:- A new Note field has been added to the end of the transaction record format (column M), this is used to add a description to a transaction. It is recommended that you add the additional Note column to all your existing transaction records.
### Fixed
- Accounting tool: "xlrd.biffh.XLRDError: Excel xlsx file; not supported" Exception. ([#36](https://github.com/BittyTax/BittyTax/issues/36))
- Coinbase parser: added support for Convert transactions ([#46](https://github.com/BittyTax/BittyTax/issues/46))
Expand All @@ -23,6 +24,9 @@
- Accounting tool: warning given if disposal detected between transfers.
- Accounting tool: integrity check (audit balances against section 104 pools).
- Accounting tool: skip integrity check (--skipint) option added.
- Accounting tool: new Note field added to transaction record format.
- Accounting tool: note field added to income report.
- Conversion tool: note field added to the Excel and CSV output.
### Changed
- Conversion tool: UnknownAddressError exception changed to generic DataFilenameError.
- Binance parser: use filename to determine if deposits or withdrawals.
Expand All @@ -36,6 +40,10 @@
- Accounting tool: same day pooling debug now only shows the pooled transactions.
- Accounting tool: section 104 debug also shows matched transactions.
- Crypto.com parser: added "campaign_reward" transaction type. ([#64](https://github.com/BittyTax/BittyTax/issues/64))
- Elecrum parser: Note field is mapped from 'label'.
- HandCash parser: Note field is mapped from 'note'.
- Qt Wallet parser: Note field is mapped from 'Label'.
- Trezor parser: Note field is mapped from 'Address Label'.
### Removed
- Accounting tool: skip audit (-s or --skipaudit) option removed.
- Accounting tool: updated transactions debug removed.
Expand Down
3 changes: 2 additions & 1 deletion bittytax/conv/out_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, t_type, timestamp,
buy_quantity=None, buy_asset='', buy_value=None,
sell_quantity=None, sell_asset='', sell_value=None,
fee_quantity=None, fee_asset='', fee_value=None,
wallet=''):
wallet='', note=''):

self.t_type = t_type
self.buy_quantity = Decimal(buy_quantity) if buy_quantity is not None else None
Expand All @@ -38,6 +38,7 @@ def __init__(self, t_type, timestamp,
self.fee_value = Decimal(fee_value) if fee_value is not None else None
self.wallet = wallet
self.timestamp = timestamp
self.note = note

@staticmethod
def format_quantity(quantity):
Expand Down
5 changes: 3 additions & 2 deletions bittytax/conv/output_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class OutputBase(object):
'Buy Quantity', 'Buy Asset', 'Buy Value',
'Sell Quantity', 'Sell Asset', 'Sell Value',
'Fee Quantity', 'Fee Asset', 'Fee Value',
'Wallet', 'Timestamp']
'Wallet', 'Timestamp', 'Note']

RECAP_OUT_HEADER = ['Type', 'Date',
'InOrBuyAmount', 'InOrBuyCurrency',
Expand Down Expand Up @@ -172,7 +172,8 @@ def _to_bittytax_csv(tr):
'{0:f}'.format(tr.fee_value.normalize()) if tr.fee_value is not None \
else None,
tr.wallet,
OutputCsv._format_timestamp(tr.timestamp)]
OutputCsv._format_timestamp(tr.timestamp),
tr.note]

@staticmethod
def _to_recap_csv(tr):
Expand Down
15 changes: 10 additions & 5 deletions bittytax/conv/output_excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ def add_row(self, data_row, row_num):
self._xl_value(data_row.t_record.fee_value, row_num, 9)
self._xl_wallet(data_row.t_record.wallet, row_num, 10)
self._xl_timestamp(data_row.t_record.timestamp, row_num, 11)
self._xl_note(data_row.t_record.note, row_num, 12)

if sys.version_info[0] < 3:
in_row = [r.decode('utf8') for r in data_row.in_row]
Expand All @@ -215,13 +216,13 @@ def add_row(self, data_row, row_num):
# Add original data
for col_num, col_data in enumerate(in_row):
if data_row.failure and data_row.failure.col_num == col_num:
self.worksheet.write(row_num, 12 + col_num, col_data,
self.output.format_in_data_err)
self.worksheet.write(row_num, len(self.output.BITTYTAX_OUT_HEADER) + col_num,
col_data, self.output.format_in_data_err)
else:
self.worksheet.write(row_num, 12 + col_num, col_data,
self.output.format_in_data)
self.worksheet.write(row_num, len(self.output.BITTYTAX_OUT_HEADER) + col_num,
col_data, self.output.format_in_data)

self._autofit_calc(12 + col_num, len(col_data))
self._autofit_calc(len(self.output.BITTYTAX_OUT_HEADER) + col_num, len(col_data))

def _xl_type(self, t_type, row_num, col_num):
if t_type in self.BUY_LIST:
Expand Down Expand Up @@ -291,6 +292,10 @@ def _xl_timestamp(self, timestamp, row_num, col_num):
self.output.format_timestamp)
self._autofit_calc(col_num, len(self.output.DATE_FORMAT))

def _xl_note(self, note, row_num, col_num):
self.worksheet.write_string(row_num, col_num, note)
self._autofit_calc(col_num, len(note) if note else self.MAX_COL_WIDTH)

def _autofit_calc(self, col_num, width):
if width > self.MAX_COL_WIDTH:
width = self.MAX_COL_WIDTH
Expand Down
12 changes: 8 additions & 4 deletions bittytax/conv/parsers/electrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ def parse_electrum2(data_row, _parser, _filename):
data_row.timestamp,
buy_quantity=Decimal(in_row[2]),
buy_asset=config.args.cryptoasset,
wallet=WALLET)
wallet=WALLET,
note=in_row[1])
else:
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_WITHDRAWAL,
data_row.timestamp,
sell_quantity=abs(Decimal(in_row[2])),
sell_asset=config.args.cryptoasset,
wallet=WALLET)
wallet=WALLET,
note=in_row[1])

def parse_electrum(data_row, _parser, _filename):
in_row = data_row.in_row
Expand All @@ -42,13 +44,15 @@ def parse_electrum(data_row, _parser, _filename):
data_row.timestamp,
buy_quantity=Decimal(in_row[3]),
buy_asset=config.args.cryptoasset,
wallet=WALLET)
wallet=WALLET,
note=in_row[1])
else:
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_WITHDRAWAL,
data_row.timestamp,
sell_quantity=abs(Decimal(in_row[3])),
sell_asset=config.args.cryptoasset,
wallet=WALLET)
wallet=WALLET,
note=in_row[1])

DataParser(DataParser.TYPE_WALLET,
"Electrum",
Expand Down
6 changes: 4 additions & 2 deletions bittytax/conv/parsers/handcash.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def parse_handcash(data_row, parser, _filename):
data_row.timestamp,
buy_quantity=Decimal(in_row[5]) / 10 ** 8,
buy_asset="BSV",
wallet=WALLET)
wallet=WALLET,
note=in_row[3])
elif in_row[0] == "send":
if participants[0]["type"] == "user":
t_type = TransactionOutRecord.TYPE_GIFT_SENT
Expand All @@ -38,7 +39,8 @@ def parse_handcash(data_row, parser, _filename):
sell_asset="BSV",
fee_quantity=Decimal(in_row[4]) / 10 ** 8,
fee_asset="BSV",
wallet=WALLET)
wallet=WALLET,
note=in_row[3])
else:
raise UnexpectedTypeError(0, parser.in_header[0], in_row[0])

Expand Down
15 changes: 10 additions & 5 deletions bittytax/conv/parsers/qtwallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,33 +41,38 @@ def parse_qt_wallet(data_row, parser, _filename):
data_row.timestamp,
buy_quantity=amount,
buy_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[3])
elif in_row[2] == "Sent to":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_WITHDRAWAL,
data_row.timestamp,
sell_quantity=amount,
sell_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[3])
elif in_row[2] == "Mined":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_MINING,
data_row.timestamp,
buy_quantity=amount,
buy_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[3])
elif in_row[2] == "Payment to yourself":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_WITHDRAWAL,
data_row.timestamp,
sell_quantity=Decimal(0),
sell_asset=symbol,
fee_quantity=amount,
fee_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[3])
elif in_row[2] == "Name operation":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_SPEND,
data_row.timestamp,
sell_quantity=amount,
sell_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[3])
else:
raise UnexpectedTypeError(2, parser.in_header[2], in_row[2])

Expand Down
9 changes: 6 additions & 3 deletions bittytax/conv/parsers/trezor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def parse_trezor(data_row, parser, filename):
buy_asset=symbol,
fee_quantity=Decimal(in_row[7])-Decimal(in_row[6]),
fee_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[4])
elif in_row[5] == "OUT":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_WITHDRAWAL,
data_row.timestamp,
Expand All @@ -41,15 +42,17 @@ def parse_trezor(data_row, parser, filename):
fee_quantity=abs(Decimal(in_row[7]))
-Decimal(in_row[6]),
fee_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[4])
elif in_row[5] == "SELF":
data_row.t_record = TransactionOutRecord(TransactionOutRecord.TYPE_WITHDRAWAL,
data_row.timestamp,
sell_quantity=Decimal(0),
sell_asset=symbol,
fee_quantity=abs(Decimal(in_row[7])),
fee_asset=symbol,
wallet=WALLET)
wallet=WALLET,
note=in_row[4])
else:
raise UnexpectedTypeError(5, parser.in_header[5], in_row[5])

Expand Down
2 changes: 1 addition & 1 deletion bittytax/export_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class ExportRecords:
'Buy Quantity', 'Buy Asset', 'Buy Value',
'Sell Quantity', 'Sell Asset', 'Sell Value',
'Fee Quantity', 'Fee Asset', 'Fee Value',
'Wallet', 'Timestamp']
'Wallet', 'Timestamp', 'Note']

def __init__(self, transaction_records):
self.transaction_records = transaction_records
Expand Down
9 changes: 7 additions & 2 deletions bittytax/import_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class TransactionRow(object):
'Buy Quantity', 'Buy Asset', 'Buy Value',
'Sell Quantity', 'Sell Asset', 'Sell Value',
'Fee Quantity', 'Fee Asset', 'Fee Value',
'Wallet', 'Timestamp']
'Wallet', 'Timestamp', 'Note']

BUY_TYPES = (TransactionRecord.TYPE_DEPOSIT,
TransactionRecord.TYPE_MINING,
Expand Down Expand Up @@ -207,8 +207,13 @@ def parse(self):
# A fee spend is normally a disposal unless it's part of a transfer
fee.disposal = False

if len(self.row) == len(self.HEADER):
note = self.row[12]
else:
note = ''

self.t_record = TransactionRecord(t_type, buy, sell, fee, self.row[10],
self.parse_timestamp(self.row[11]))
self.parse_timestamp(self.row[11]), note)

@staticmethod
def parse_timestamp(timestamp_str):
Expand Down
33 changes: 24 additions & 9 deletions bittytax/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,31 @@ class TransactionRecord(object):

cnt = 0

def __init__(self, t_type, buy, sell, fee, wallet, timestamp):
def __init__(self, t_type, buy, sell, fee, wallet, timestamp, note):
self.tid = None
self.t_type = t_type
self.buy = buy
self.sell = sell
self.fee = fee
self.wallet = wallet
self.timestamp = timestamp
self.note = note

if self.buy:
self.buy.t_record = self
self.buy.timestamp = self.timestamp.astimezone(config.TZ_LOCAL)
self.buy.wallet = self.wallet
self.buy.note = self.note
if self.sell:
self.sell.t_record = self
self.sell.timestamp = self.timestamp.astimezone(config.TZ_LOCAL)
self.sell.wallet = self.wallet
self.sell.note = self.note
if self.fee:
self.fee.t_record = self
self.fee.timestamp = self.timestamp.astimezone(config.TZ_LOCAL)
self.fee.wallet = self.wallet
self.fee.note = self.note

def set_tid(self):
if self.tid is None:
Expand Down Expand Up @@ -87,12 +91,17 @@ def _format_value(value):
config.CCY)
return ''

@staticmethod
def _format_note(note):
if note:
return "'%s' " % note
return ''

@staticmethod
def _format_timestamp(timestamp):
if timestamp.microsecond:
return timestamp.strftime('%Y-%m-%dT%H:%M:%S.%f %Z')
else:
return timestamp.strftime('%Y-%m-%dT%H:%M:%S %Z')
return timestamp.strftime('%Y-%m-%dT%H:%M:%S %Z')

@staticmethod
def _format_decimal(decimal):
Expand All @@ -111,7 +120,7 @@ def __lt__(self, other):

def __str__(self):
if self.buy and self.sell:
return "%s %s %s%s <- %s %s%s%s '%s' %s [TID:%s]" % (
return "%s %s %s%s <- %s %s%s%s '%s' %s %s[TID:%s]" % (
self.t_type,
self._format_quantity(self.buy.quantity),
self.buy.asset,
Expand All @@ -122,26 +131,29 @@ def __str__(self):
self._format_fee(),
self.wallet,
self._format_timestamp(self.timestamp),
self._format_note(self.note),
self.tid[0])
elif self.buy:
return "%s %s %s%s%s '%s' %s [TID:%s]" % (
return "%s %s %s%s%s '%s' %s %s[TID:%s]" % (
self.t_type,
self._format_quantity(self.buy.quantity),
self.buy.asset,
self._format_value(self.buy.cost),
self._format_fee(),
self.wallet,
self._format_timestamp(self.timestamp),
self._format_note(self.note),
self.tid[0])
elif self.sell:
return "%s %s %s%s%s '%s' %s [TID:%s]" % (
return "%s %s %s%s%s '%s' %s %s[TID:%s]" % (
self.t_type,
self._format_quantity(self.sell.quantity),
self.sell.asset,
self._format_value(self.sell.proceeds),
self._format_fee(),
self.wallet,
self._format_timestamp(self.timestamp),
self._format_note(self.note),
self.tid[0])

return ''
Expand All @@ -159,7 +171,8 @@ def to_csv(self):
self.fee.asset if self.fee else '',
self._format_decimal(self.fee.proceeds) if self.fee else '',
self.wallet,
self._format_timestamp(self.timestamp)]
self._format_timestamp(self.timestamp),
self.note]
elif self.buy:
return [self.t_type,
self._format_decimal(self.buy.quantity),
Expand All @@ -172,7 +185,8 @@ def to_csv(self):
self.fee.asset if self.fee else '',
self._format_decimal(self.fee.proceeds) if self.fee else '',
self.wallet,
self._format_timestamp(self.timestamp)]
self._format_timestamp(self.timestamp),
self.note]
elif self.sell:
return [self.t_type,
'',
Expand All @@ -185,6 +199,7 @@ def to_csv(self):
self.fee.asset if self.fee else '',
self._format_decimal(self.fee.proceeds) if self.fee else '',
self.wallet,
self._format_timestamp(self.timestamp)]
self._format_timestamp(self.timestamp),
self.note]

return []
Loading

0 comments on commit 55cba2d

Please sign in to comment.