From a4834d32be9e732698097a8e0b1f7b5e812ceab3 Mon Sep 17 00:00:00 2001 From: Ivan Kold Date: Mon, 5 Jul 2021 20:32:42 -0700 Subject: [PATCH] Export transactions, 8949 sales and some calculations as a ODS spreadsheet --- CoinTaxes.py | 14 ++-- Pipfile | 2 + Pipfile.lock | 144 +++++++++++++++++++++++++++++++++++++---- config.yml | 8 ++- formats/spreadsheet.py | 68 +++++++++++++++++++ 5 files changed, 216 insertions(+), 20 deletions(-) create mode 100644 formats/spreadsheet.py diff --git a/CoinTaxes.py b/CoinTaxes.py index 87dc409..73b8ccc 100755 --- a/CoinTaxes.py +++ b/CoinTaxes.py @@ -3,9 +3,10 @@ import os import yaml +import copy import exchanges -from formats import fill_8949, turbo_tax +from formats import fill_8949, turbo_tax, spreadsheet def get_exchange(name, config): @@ -120,7 +121,7 @@ def main(): """Main function to collect information and create the forms.""" parser = argparse.ArgumentParser(description='CoinTaxes', formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--input', default='config.yml', required=True, + parser.add_argument('--input', default='config.yml', help='Configuration file to read.') parser.add_argument('--output', default='output', help='Directory to output autogenerated files.') @@ -160,8 +161,8 @@ def main(): # Get the full order information to be used on form 8949 full_orders = fill_8949.get_cost_basis( - sells_sorted, - buys_sorted, + copy.deepcopy(sells_sorted), # pass a copy to preserve the original + copy.deepcopy(buys_sorted), # pass a copy to preserve the original basis_type='highest', tax_year=config['year'] ) @@ -174,8 +175,11 @@ def main(): turbo_tax.make_txf(full_orders, year=config['year']) # Make the 8949 forms - fill_8949.make_pdf(full_orders, "test", config['name'], config['social'], config['year']) + if 'fill_8949' in config and config['fill_8949']: + fill_8949.make_pdf(full_orders, "test", config['name'], config['social'], config['year']) + if 'spreadsheet' in config and config['spreadsheet']: + spreadsheet.make_spreadsheet(full_orders, buys_sorted, sells_sorted, year=config['year']) if __name__ == '__main__': main() diff --git a/Pipfile b/Pipfile index a2543fc..90a1084 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,8 @@ six = "==1.10.0" websocket-client = "==0.40.0" pyyaml = "*" pycryptodome = "==3.6.6" +pyexcel = "0.6.6" +pyexcel-ods3 = "0.6.0" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index c12b45d..662a003 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "41e6e18bc7e23b77fae6254d481b34908d58f83734985f9bdb8d4b0c6e6b889a" + "sha256": "5e9c0cced908ab248521cf4906657e48bc7b2e9b6432d608419237401981a28d" }, "pipfile-spec": 6, "requires": { @@ -18,8 +18,11 @@ "default": { "bintrees": { "hashes": [ + "sha256:18f552b1e41d0d2ad0d9e384caebc617ea676359901704ae475c922bd85d7fad", "sha256:3270aa3d541906382d823eb4406bf3b9ad5d2731c12c58f22cb2e12475addb90", + "sha256:4188dcc69397b2513aef1ece1b3ea99e0663c75ed7bee7814f671a462819fd31", "sha256:60675e6602cef094abcd38bf4aecc067d78ae2d5e1645615c789724542d11270", + "sha256:70f1a1621850a614cea3c24bda0d54e63c46dd30243206764e23748a75eedd61", "sha256:730ed144319c82edff3b4d151a70aae7371054e1f3bfed4d44db87ccbebe8c7f", "sha256:94e604f709151d0e678e06baa269fc748ae48667678ec23eb2b6704d743aa34f" ], @@ -56,6 +59,65 @@ "index": "pypi", "version": "==0.0.3" }, + "lml": { + "hashes": [ + "sha256:57a085a29bb7991d70d41c6c3144c560a8e35b4c1030ffb36d85fa058773bcc5", + "sha256:ec06e850019942a485639c8c2a26bdb99eae24505bee7492b649df98a0bed101" + ], + "version": "==0.1.0" + }, + "lxml": { + "hashes": [ + "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d", + "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3", + "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2", + "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae", + "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f", + "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927", + "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3", + "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7", + "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59", + "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f", + "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade", + "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96", + "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468", + "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b", + "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4", + "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354", + "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83", + "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04", + "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16", + "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791", + "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a", + "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51", + "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1", + "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a", + "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f", + "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee", + "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec", + "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969", + "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28", + "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a", + "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa", + "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106", + "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d", + "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617", + "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4", + "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92", + "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0", + "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4", + "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24", + "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2", + "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e", + "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0", + "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654", + "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2", + "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23", + "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.6.3" + }, "pycryptodome": { "hashes": [ "sha256:0f027d5da3f3c4c0167f3ccf4a1f56674248120656099df35098dfaf3edff0fb", @@ -92,6 +154,37 @@ "index": "pypi", "version": "==3.6.6" }, + "pyexcel": { + "hashes": [ + "sha256:39b0bb8f033d9b5523b126cf5a5259d1990ea82b8a23c8eab7aa5e23116803df", + "sha256:6aa6ece9cd3eda52c05d6687bb6c63580fc950bf48a8ca67df090193326d27b9" + ], + "index": "pypi", + "version": "==0.6.6" + }, + "pyexcel-ezodf": { + "hashes": [ + "sha256:972eeea9b0e4bab60dfc5cdcb7378cc7ba5e070a0b7282746c0182c5de011ff1", + "sha256:a74ac7636a015fff31d35c5350dc5ad347ba98ecb453de4dbcbb9a9168434e8c" + ], + "version": "==0.3.4" + }, + "pyexcel-io": { + "hashes": [ + "sha256:00f15f4bae2947de49b3206f2600f78780008e044380f7aafe0ce52969cda4ca", + "sha256:3d7e78ad3344e4755726e1b1fa605b0e95cb76acfc4e6ff0ca0fc2e8303ded38" + ], + "markers": "python_version >= '3.6'", + "version": "==0.6.4" + }, + "pyexcel-ods3": { + "hashes": [ + "sha256:d043da2a42e3daf737bf8c42378a5fe47a056ff7b348d819eded416854dc4346", + "sha256:eda986eedcb90386907dd65ea2e9f5ff7387a8848cd6c16d9d6762523c560346" + ], + "index": "pypi", + "version": "==0.6.0" + }, "python-dateutil": { "hashes": [ "sha256:3220490fb9741e2342e1cf29a503394fdac874bc39568288717ee67047ff29df", @@ -102,20 +195,38 @@ }, "pyyaml": { "hashes": [ - "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", - "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", - "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", - "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", - "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", - "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", - "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", - "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", - "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", - "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], "index": "pypi", - "version": "==3.13" + "version": "==5.4.1" }, "requests": { "hashes": [ @@ -133,6 +244,13 @@ "index": "pypi", "version": "==1.10.0" }, + "texttable": { + "hashes": [ + "sha256:ce0faf21aa77d806bbff22b107cc22cce68dc9438f97a2df32c93e9afa4ce436", + "sha256:f802f2ef8459058736264210f716c757cbf85007a30886d8541aa8c3404f1dda" + ], + "version": "==1.6.3" + }, "websocket-client": { "hashes": [ "sha256:40ac14a0c54e14d22809a5c8d553de5a2ae45de3c60105fae53bcb281b3fe6fb" diff --git a/config.yml b/config.yml index 509188c..2c01550 100644 --- a/config.yml +++ b/config.yml @@ -3,9 +3,13 @@ name: 'nitrocode' # Full hyphen delimited social security number or leave empty social: '000-00-0000' # tax year -year: 2017 -# use turbo tax? +year: 2018 +# generate turbo tax file? txf: true +# generate PDFs of Form 8949? +fill_8949: false +# generate a spreadsheet with all data and some calculations? +spreadsheet: true # view only api keys # comment out unused exchanges by prefixing lines with a pound sign # diff --git a/formats/spreadsheet.py b/formats/spreadsheet.py new file mode 100644 index 0000000..0e0c302 --- /dev/null +++ b/formats/spreadsheet.py @@ -0,0 +1,68 @@ +import os +import datetime +import pyexcel as p +from pyexcel._compact import OrderedDict + +def make_spreadsheet(full_orders, buys_sorted, sells_sorted, output_dir='output', year=2017): + out_file = os.path.join(output_dir, '_'.join(['Transactions', 'Crypto', str(year)])) + '.ods' + book = OrderedDict() + + # "Transactions" sheet + trans_sheet = [] + book['Transactions'] = trans_sheet + trans_sheet.append([ 'Order Time UTC', 'product', 'currency', 'currency_pair', 'buysell', 'cost', 'amount', 'cost_per_coin' ]) +# 'order_time', 'product', 'currency', 'currency_pair', 'buysell', 'cost', 'amount', 'cost_per_coin' + buys_sells_sorted = sorted(buys_sorted+sells_sorted, key=lambda order: order['order_time']) + for order in buys_sells_sorted: + trans_sheet.append([ + order['order_time'].isoformat(), + order['product'], + order['currency'], + order['currency_pair'], + order['buysell'], + order['cost'], + order['amount'], + order['cost_per_coin'] + ]) + + # "8949" sheet + form_8949_sheet = [] + book['8949'] = form_8949_sheet + form_8949_sheet.append([ 'Description', 'Date bought', 'Date sold', 'Proceeds', 'Cost basis', 'Gain/Loss' ]) + + # Full order is [description, date acquired, date sold, proceeds, cost basis, gain/loss] (populated in fill_8949.py) + DESC, DATE_ACQ, DATE_SOLD, PROCEEDS, COST_BASIS, GAIN_LOSS = range(5+1) + form_8949_sales_by_month = { month_num: { 'first_idx': -1, 'last_idx': -1, 'proceeds': 0, 'gain_loss': 0 } for month_num in range(1,12+1) } + total_8949_proceeds = 0 + total_8949_gain_loss = 0 + + for idx, full_order in enumerate(full_orders): + form_8949_sheet.append(full_order) + # caclulate start/end indices of sales by month + sale_dt = datetime.datetime.strptime(full_order[DATE_SOLD], '%m/%d/%Y') + month_sales = form_8949_sales_by_month[sale_dt.month] + if month_sales['first_idx'] == -1: + month_sales['first_idx'] = idx + + month_sales['last_idx'] = idx + month_sales['proceeds'] += full_order[PROCEEDS] + total_8949_proceeds += full_order[PROCEEDS] + month_sales['gain_loss'] += full_order[GAIN_LOSS] + total_8949_gain_loss += full_order[GAIN_LOSS] + + # "Calculated" sheet + calc_sheet = [] + book['Calculated'] = calc_sheet + calc_sheet.append([ 'Total 8949 gain/loss:', "=SUM($'8949'.F2:F%d)" % (len(form_8949_sheet)), total_8949_gain_loss ]) + calc_sheet.append([ 'Total 8949 proceeds:', "=SUM($'8949'.D2:D%d)" % (len(form_8949_sheet)), total_8949_proceeds ]) + for month_num in range(1, 12+1): + month_sales = form_8949_sales_by_month[month_num] + month_first_idx = month_sales['first_idx'] + month_last_idx = month_sales['last_idx'] + if month_first_idx != -1: + calc_sheet.append([ 'Total 8949 proceeds in month #%d:' % (month_num), "=SUM($'8949'.D%d:D%d)" % (month_first_idx+2, month_last_idx+2), month_sales['proceeds'] ]) + # calc_sheet.append([ 'Total trans amounts:', "=SUM($'Transactions'.F2:F%d)" % (len(buys_sells_sorted)) ]) + + p.save_book_as(bookdict=book, dest_file_name=out_file) + print("Saved spreadsheet as %s" %(out_file) ) +