diff --git a/crypto_tax/__init__.py b/crypto_tax/__init__.py index 47a661c..7f0790c 100644 --- a/crypto_tax/__init__.py +++ b/crypto_tax/__init__.py @@ -30,6 +30,14 @@ import fin_data from .data import TaxReport, BoughtAndSold, BoughtAndNotYetSold, LedgerEntry +try: + import logging_color + + logging_color.monkey_patch() +except ImportError as e: + logging.warning('Color logging not enabled') + del e + logger = logging.getLogger(__name__) FIN_DB = fin_data.FinancialDatabase(enable_kucoin=True, enable_nationalbanken_dk=True) @@ -44,7 +52,7 @@ def compute_fifo(transfers: list) -> TaxReport: ledger_entries = [] prices_bought_for: dict[Asset, deque[BoughtAndNotYetSold]] = {} - if True: + if False: prices_bought_for[MPC] = deque() # TODO: prices_bought_for[MPC].append(BoughtAndNotYetSold(AssetAmount(MPC, Decimal(80)), datetime.datetime(2024, 4,1,1,1,1,1))) @@ -55,7 +63,7 @@ def compute_fifo(transfers: list) -> TaxReport: return sum((p.amount for p in prices_bought_for[asset]), start=AssetAmount.ZERO) def sell(amount_to_sell: AssetAmount, executed_time: datetime.datetime, variant: str = 'SELL'): - logger.info('%s: %s', variant, amount_to_sell) + logger.debug('%s: %s', variant, amount_to_sell) amount = amount_to_sell while amount.amount > 0: if len(prices_bought_for[amount.asset]) == 0: @@ -108,8 +116,6 @@ def compute_fifo(transfers: list) -> TaxReport: for transfer in transfers: - logger.info(transfer) - # Sell if _input := transfer.input: variant = 'WITHDRAW' if transfer.output is None else 'SELL' diff --git a/crypto_tax/__main__.py b/crypto_tax/__main__.py index e909e7b..d8b35b4 100644 --- a/crypto_tax/__main__.py +++ b/crypto_tax/__main__.py @@ -36,8 +36,6 @@ def main(): #TRANSFERS += list(KUCOIN_CLIENT._get_double_registers()) tax_report = compute_fifo(TRANSFERS) - logger.info('-'*80) - output_path = Path('./output/report.xlsx') output_excel.produce_excel_report(tax_report, output_path) diff --git a/crypto_tax/output_excel.py b/crypto_tax/output_excel.py index ffe3aed..6be8265 100644 --- a/crypto_tax/output_excel.py +++ b/crypto_tax/output_excel.py @@ -15,14 +15,16 @@ from pathlib import Path import openpyxl import openpyxl.styles +import openpyxl.worksheet.table +from openpyxl.utils import get_column_letter TAX_TYPES: dict[fin_defs.Asset, str] = { fin_defs.DKK: 'Fiat', fin_defs.EUR: 'Fiat', - fin_defs.BTC: 'Krypto-valuta', - fin_defs.MPC: 'Krypto-valuta', - fin_defs.WELL_KNOWN_SYMBOLS['MATIC']: 'Krypto-valuta', - fin_defs.USDT: 'Stable Coin (Financiel Kontrakt)', + fin_defs.BTC: 'Kryptovaluta', + fin_defs.MPC: 'Kryptovaluta', + fin_defs.WELL_KNOWN_SYMBOLS['MATIC']: 'Kryptovaluta', + fin_defs.USDT: 'Stable Coin', } NOW = datetime.datetime.now(tz=datetime.UTC) @@ -34,19 +36,34 @@ def mult_a(a, b): return 'Unknown' return a * b -BOLD_FONT = openpyxl.styles.Font(bold=True) +FONT_BOLD = openpyxl.styles.Font(bold=True) +FONT_HEADER = openpyxl.styles.Font(size=20) ALIGN_CENTER = openpyxl.styles.Alignment(horizontal="center", vertical="center") +ALIGN_WRAP = openpyxl.styles.Alignment(wrap_text=True) +BORDER_BOTTOM = openpyxl.styles.Border(bottom=openpyxl.styles.Side(border_style='medium', color='FF000000')) +BORDER_SUM = openpyxl.styles.Border(top=openpyxl.styles.Side(border_style='thin', color='FF000000'), + bottom=openpyxl.styles.Side(border_style='double', color='FF000000')) def add_headers(sheet, column_headers): sheet.append(column_headers) - for row in sheet['A1:J1']: + end_column = get_column_letter(len(column_headers)) + sheet.row_dimensions[1].height = 30 + for row in sheet[f'A1:{end_column}1']: for cell in row: - cell.font = BOLD_FONT + cell.font = FONT_BOLD + cell.alignment = ALIGN_CENTER + cell.border = BORDER_BOTTOM + +def set_number_format(sheet, range): + for row in sheet[range]: + for cell in row: + cell.number_format = "0.00" def write_current_assets(sheet, tax_report: TaxReport): - column_headers = ['Værdipapir', 'Mængde', 'Estimeret værdi (DKK)', + column_headers = ['Værdipapir', 'Mængde', 'Kurs', 'Værdi (DKK)', 'Beskatningstype'] add_headers(sheet, column_headers) + row_idx = 2 for asset, positions in tax_report.current_assets.items(): total_amount: Decimal = sum((p.amount.amount for p in positions), start=Decimal(0)) exchange_rate_asset_to_dkk = tax_report.exchange_rate_at_time(asset, fin_defs.DKK, NOW) @@ -54,12 +71,21 @@ def write_current_assets(sheet, tax_report: TaxReport): row = [ asset.raw_short_name(), total_amount, - mult_a(total_amount, exchange_rate_asset_to_dkk), + exchange_rate_asset_to_dkk, + f'=B{row_idx}*C{row_idx}', TAX_TYPES[asset], ] sheet.append(row) + row_idx += 1 del asset, positions, row, total_amount + sheet.column_dimensions['A'].width = 16 + sheet.column_dimensions['B'].width = 16 + sheet.column_dimensions['C'].width = 15 + sheet.column_dimensions['D'].width = 15 + sheet.column_dimensions['E'].width = 15 + set_number_format(sheet, 'C1:D6') + def write_ledger_sheet(sheet, tax_report: TaxReport): assets = list({e.amount.asset for e in tax_report.ledger_entries}) @@ -68,7 +94,6 @@ def write_ledger_sheet(sheet, tax_report: TaxReport): 'Type', 'Værdipapir', 'Mængde', - 'Balance', 'Kontoudbyder', '' ] + [a.raw_short_name() for a in assets] @@ -84,64 +109,183 @@ def write_ledger_sheet(sheet, tax_report: TaxReport): ledger_entry.type, ledger_entry.amount.asset.raw_short_name(), ledger_entry.amount.amount, - ledger_entry.balance.amount, ledger_entry.account_provider, '', - ] + [(ledger_entry.balance.amount if ledger_entry.balance.asset == a else '') for a in assets] + ] + [(ledger_entry.balance.amount if ledger_entry.amount.asset == a else '') for a in assets] sheet.append(row) - row_idx+= 1 - if entry_time != prev_entry_time: + if entry_time != prev_entry_time and prev_entry_time is not None: # TODO: sheet.merge_cells(f'A{start_of_same_entry}:A{row_idx-1}') sheet[f'A{start_of_same_entry}'].alignment = ALIGN_CENTER start_of_same_entry = row_idx - prev_entry_time = entry_time + prev_entry_time = entry_time + row_idx+= 1 del ledger_entry, row + # Styling + sheet.column_dimensions['A'].width = 20 + sheet.column_dimensions['B'].width = 12 + sheet.column_dimensions['C'].width = 13 + sheet.column_dimensions['D'].width = 12 + sheet.column_dimensions['E'].width = 12 + sheet.column_dimensions['F'].width = 16 -def write_fifo_sheet(sheet, tax_report: TaxReport): +def write_fifo_sheet(sheet, bought_and_sold_for: list, exchange_rate_at_time) -> tuple[str,str]: # TODO: Account for transfers column_headers = [ - 'Værdipapir', 'Mængde', 'Anskaffelsesværdi (DKK)', 'Købsdato (UTC)', - 'Salgsværdi (DKK)', 'Salgsdato (UTC)', 'Profit (DKK)', 'Tab (DKK)', - 'Beskatningstype', + 'Købsdato (UTC)', + 'Salgsdato (UTC)', + 'Værdipapir', + 'Mængde', + 'Anskaffelseskurs', + 'Salgskurs', + 'Anskaffelsesværdi (DKK)', + 'Salgsværdi (DKK)', + 'Profit (DKK)', 'Tab (DKK)', ] add_headers(sheet, column_headers) row_idx = 2 - for fifo_entry in tax_report.bought_and_sold_for: + for fifo_entry in bought_and_sold_for: asset = fifo_entry.amount.asset - exchange_rate_dkk_bought = tax_report.exchange_rate_at_time(asset, fin_defs.DKK, fifo_entry.time_bought) + exchange_rate_dkk_bought = exchange_rate_at_time(asset, fin_defs.DKK, fifo_entry.time_bought) #assert exchange_rate_dkk_bought is not None, asset - exchange_rate_dkk_sold = tax_report.exchange_rate_at_time(asset, fin_defs.DKK, fifo_entry.time_sold) + exchange_rate_dkk_sold = exchange_rate_at_time(asset, fin_defs.DKK, fifo_entry.time_sold) #assert exchange_rate_dkk_bought is not None, asset row = [ + fifo_entry.time_bought.replace(tzinfo=None), + fifo_entry.time_sold.replace(tzinfo=None), asset.raw_short_name(), fifo_entry.amount.amount, - mult_a(fifo_entry.amount.amount , exchange_rate_dkk_bought), - fifo_entry.time_bought.replace(tzinfo=None), - mult_a(fifo_entry.amount.amount , exchange_rate_dkk_sold), - fifo_entry.time_sold.replace(tzinfo=None), - f'=max(0, E{row_idx} - C{row_idx})', - f'=max(0, C{row_idx} - E{row_idx})', - TAX_TYPES[asset], + exchange_rate_dkk_bought, + exchange_rate_dkk_sold, + f'=F{row_idx}*D{row_idx}', + f'=E{row_idx}*D{row_idx}', + f'=max(0, G{row_idx} - H{row_idx})', + f'=max(0, H{row_idx} - G{row_idx})', ] sheet.append(row) row_idx+= 1 del fifo_entry, asset, row + # Sums + sum_profit = f'I{row_idx}' + sum_loss = f'J{row_idx}' + sheet.append([ + '', + '', + '', + '', + '', + '', + '', + '', + f'=sum(I2:I{row_idx-1})', + f'=sum(J2:J{row_idx-1})' + ]) + for row in sheet[f'A{row_idx}:{sum_loss}']: + for cell in row: + cell.font = FONT_BOLD + cell.border = BORDER_SUM + + # Styling + sheet.column_dimensions['A'].width = 20 + sheet.column_dimensions['B'].width = 20 + sheet.column_dimensions['C'].width = 16 + sheet.column_dimensions['D'].width = 10 + sheet.column_dimensions['E'].width = 15 + sheet.column_dimensions['F'].width = 15 + sheet.column_dimensions['G'].width = 26 + sheet.column_dimensions['H'].width = 20 + sheet.column_dimensions['I'].width = 15 + sheet.column_dimensions['J'].width = 15 + set_number_format(sheet, f'G1:J{row_idx}') + + return (sum_profit, sum_loss) + +def write_overview(sheet, tax_report: TaxReport, sums): + sheet['A1'] = 'Krypto-Skatterapport 2024' + sheet.merge_cells('A1:B1') + sheet['A1'].font = FONT_HEADER + sheet['A1'].alignment = ALIGN_CENTER + sheet['A2'] = 'Jon Michael Aanes' + sheet.merge_cells('A2:B2') + sheet['A2'].font = FONT_HEADER + sheet['A2'].alignment = ALIGN_CENTER + + # Forklaring + sheet['A4'] = 'Denne rapport afdækker aktiviteten på en række krypto-børser, med henblik på at give en komplet oversigt af handler og nuværende beholdning.' + sheet.merge_cells('A4:B4') + sheet['A4'].alignment = ALIGN_WRAP + + sheet['A6'] = 'Rapporten er splittet op I følgende ark:' + sheet.merge_cells('A6:B6') + sheet['A7'] = 'Overview' + sheet['B7'] = 'Denne oversigt' + sheet['A8'] = 'Current Assets' + sheet['B8'] = 'Afdækker nuværende beholdning.' + sheet['A9'] = 'Ledger' + sheet['B9'] = 'Afdækker alle handler og overførsler.' + sheet['A10'] = 'FIFO Stablecoin' + sheet['B10'] = 'Beregner profit og tab for stablecoins ved FIFO-pricippet.' + sheet['A11'] = 'FIFO Kryptovaluta' + sheet['B11'] = 'Beregner profit og tab for kryptovaluta ved FIFO-pricippet.' + sheet['A12'] = 'FIFO Fiat' + sheet['B12'] = 'Beregner profit og tab for udenlandsk valuta ved FIFO-pricippet.' + + # Beregnet kolonner + sheet['D6'] = 'Beregnet Skatte Kolonnner' # TODO + sheet.merge_cells('D6:E6') + sheet['A4'].alignment = ALIGN_CENTER + sheet['D7'] = 'Kolonne' # TODO + sheet['E7'] = 'Værdi' + sheet['D8'] = 'Profit Krypto' + sheet['E8'] = '=$\'FIFO Kryptovaluta\'.'+sums['Kryptovaluta'][0] + sheet['D9'] = 'Tab Krypto' + sheet['E9'] = '=$\'FIFO Kryptovaluta\'.'+sums['Kryptovaluta'][1] + sheet['D10'] = 'Profit Stable Coin' + sheet['E10'] = '=$\'FIFO Stable Coin\'.'+sums['Stable Coin'][0] + sheet['D11'] = 'Profit Stable Coin' + sheet['E11'] = '=$\'FIFO Stable Coin\'.'+sums['Stable Coin'][1] + + + # Disclaimer + #sheet['A13'] = 'Bemærk at rapporten kan være ufyldtesgørende, af forskellige årsager, inklusiv: Fejl i data fra krypto-børser' + + # Styling + sheet.column_dimensions['A'].width = 20 + sheet.column_dimensions['B'].width = 70 + sheet.row_dimensions[4].height = 60 + + def produce_excel_report(report: TaxReport, output_path: Path): workbook = openpyxl.Workbook() - workbook.active.title = 'Current Assets' - write_current_assets(workbook.active, report) + # Oversigt + workbook.active.title = 'Oversigt' + workbook.active.page_setup.fitToWidth = 1 + # Current assets + sheet_ledger = workbook.create_sheet( 'Current Assets') + workbook.active.page_setup.fitToWidth = 1 + write_current_assets(sheet_ledger, report) + + # Ledger sheet_ledger = workbook.create_sheet('Ledger') + sheet_ledger.page_setup.fitToWidth = 1 write_ledger_sheet(sheet_ledger , report) - sheet_fifo = workbook.create_sheet('FIFO') - write_fifo_sheet(sheet_fifo, report) + # FIFO + sums = {} + for tax_type in set(TAX_TYPES.values()): + sheet_fifo = workbook.create_sheet('FIFO '+tax_type) + sheet_fifo.page_setup.fitToWidth = 1 + bought_and_sold_for = [entry for entry in report.bought_and_sold_for if TAX_TYPES[entry.amount.asset] == tax_type] + sums[tax_type] = write_fifo_sheet(sheet_fifo, bought_and_sold_for, report.exchange_rate_at_time) + del tax_type, sheet_fifo + + write_overview(workbook.active, report, sums) workbook.save(output_path)