Skip to content

Commit

Permalink
Tax report generation given export from cointracking.info
Browse files Browse the repository at this point in the history
  • Loading branch information
bitsofwinter committed Apr 6, 2018
1 parent b997efb commit d8997dd
Show file tree
Hide file tree
Showing 9 changed files with 720 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__pycache__
.idea
.mypy_cache
.DS_Store
*.pyc
venv
out
data/personal_details.json
data/trades.csv
data/fees.json
data/stocks.json
163 changes: 163 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Swedish cryptocurrency tax reporting script

## About

This is a tool to convert your cryptocurrency trade history to the K4 documents needed
for tax reporting to Skatteverket.

Using [cointracking.info](https://cointracking.info?ref=D611015) is currently the
only supported way to import trades. This site does not yet support doing tax
reports using average cost basis which is what is required in Sweden but
it is still very useful for the actual trade data import.

Besides adding support for average cost basis this script can also generate files
which are compatible with Skatteverket. There is either PDF output for printing and
sending by mail or SRU-output which can be imported on skatteverket.se.

## Supporting the development

Please consider supporting the development of this tool by either using
the referral link to [cointracking.info](https://cointracking.info?ref=D611015)
or by donating to one of the adresses below. Using the referral link
will give you a 10% discount if you decide to buy a Pro or Unlimited account.

* BTC: `3KTLVpWjRGuJNBmjsKo4HGDG1G5SCesej3`
* ETH: `0x05125B8E6598AbDDe21c7D01008a10F6107Ce004`

## Limitations

The sru format is currently limited in that it doesn't allow
decimals, this is a limitation with skatteverket.se. The
recommendation from Skatteverket is to round to whole numbers
even if that results in 0 BTC or similar being reported.

If the result is a report with a lot of zero amounts of cryptos
reported then you should mention why it looks like this in
"Övriga upplysningar" on the tax report.

## Liability

I'm not taking any responsibility for that this tool will generate a
correct tax report. I am using the tool for my own tax reporting though
so making it correct is a priority to me. You will however have to
take responsibility yourself for the tax report you send to
Skatteverket, this means you should perform a sanity check of some sort
on the generated K4 documents to make sure it looks reasonable.

## Setup

### Windows

There is a packaged version for Windows under releases which can be used.
Change the example command lines below from `python report.py` to
`report.exe` instead if using it.

### macOS

There is a packaged version for macOS under releases which can be used.
Change the example command lines below from `python report.py` to
`./report` instead if using it.

### Other (or if you prefer setting up python yourself)

Python 3.6 is required.

The following python packages are needed for pdf generation.

* pdfrw
* reportlab

Python virtualenv can be setup using

```
virtualenv venv -p python3.6
. ./venv/bin/activate
pip install -r requirements.txt
```

## Input data

### data/personal_details.json

This file should have the following format.

```
{
"namn": "Full name",
"personnummer": "YYYYMMDD-NNNN",
"postnummer": "NNNNN",
"postort": "City"
}
```

### data/trades.csv

To get the data for this file you first need to have your complete trade history
on [cointracking.info](https://cointracking.info?ref=D611015). Then go to the
Trade Prices-page and download a CSV report from that page and store it at
`data/trades.csv`.

The script currently uses a cost basis of 0 for any entries marked as 'Gift/Tip'
on cointracking, this is to be able to report received coins in forks under this
type on cointracking.

### data/stocks.json (optional)

If you have any stock trades which need to be reported in section A on the K4 then you can
enter them in `data/stocks.json`. See `data/stocks_template.json` for the format.

## Running

### Options

```
usage: report.py [-h] [--trades TRADES] [--format {pdf,sru}] [--decimal-sru]
[--exclude-groups [EXCLUDE_GROUPS [EXCLUDE_GROUPS ...]]]
[--coin-report]
year
Swedish cryptocurrency tax reporting script
positional arguments:
year Tax year to create report for
optional arguments:
-h, --help show this help message and exit
--trades TRADES Read trades from csv file
--format {pdf,sru} The file format of the generated report
--decimal-sru Report decimal amounts in sru mode (not supported by
Skatteverket yet)
--exclude-groups [EXCLUDE_GROUPS [EXCLUDE_GROUPS ...]]
Exclude cointracking group from report
--coin-report Generate report of remaining coins and their cost
basis at end of year
```

### Example

#### Generate report for 2017 in sru format.

```
python report.py 2017
```

Generated sru files can be found in the ```out``` folder.

Generated sru files can be tested for errors at [https://www.skatteverket.se/filoverforing]

#### Generate report for 2017 in pdf format.

```
python report.py --format=pdf 2017
```

Generated pdf files can be found in the ```out``` folder.

#### Merging the generated pdf files

Merging the pdf files can be done with Ghostscript. It might make printing a bit easier.

```
cd out
gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=merged.pdf k4_no*.pdf
```
6 changes: 6 additions & 0 deletions data/personal_details_template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"namn": "Förnamn Efternamn",
"personnummer": "YYYYMMDD-NNNN",
"postnummer": "NNNNN",
"postort": ""
}
7 changes: 7 additions & 0 deletions data/stocks_template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"_comment": "Trades which should be reported under section A on K4",
"trades": [
{"name": ..., "amount": ..., "income": ..., "costbase": ...}
...
]
}
134 changes: 134 additions & 0 deletions k4page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from datetime import datetime
import os


class K4Section:
def __init__(self, lines, sums):
self.lines = lines
self.sums = sums


class K4Page:
def __init__(self, year, personal_details, page_number,
section_a, section_c, section_d):
self._year = year
self._personal_details = personal_details
self._page_number = page_number
self._section_a = section_a
self._section_c = section_c
self._section_d = section_d

def generate_sru_lines(self):
k4_page_number_field = 7014

blankettkod = f"K4-{self._year}P4"
now = datetime.now()
generated_date = now.strftime("%Y%m%d")
generated_time = now.strftime("%H%M%S")

lines = []
lines.append(f"#BLANKETT {blankettkod}")
lines.append(
f"#IDENTITET {self._personal_details.personnummer.replace('-', '')} {generated_date} {generated_time}")
lines.append(f"#NAMN {self._personal_details.namn}")
lines.append(f"#UPPGIFT {k4_page_number_field} {self._page_number}")

def generate_section(section, k4_base_field_code, k4_field_code_line_offset, k4_sum_field_codes):
for (line_index, fields) in enumerate(section.lines):
for (field_index, field) in enumerate(fields):
if field:
field_code = k4_base_field_code + k4_field_code_line_offset * line_index + field_index
lines.append(f"#UPPGIFT {field_code} {field}")
for (index, field) in enumerate(section.sums):
if field:
field_code = k4_sum_field_codes[index]
lines.append(f"#UPPGIFT {field_code} {field}")

if self._section_a and self._section_a.lines:
generate_section(self._section_a, 3100, 10, [3300, 3301, 3304, 3305])
if self._section_c and self._section_c.lines:
generate_section(self._section_c, 3310, 10, [3400, 3401, 3403, 3404])
if self._section_d and self._section_d.lines:
generate_section(self._section_d, 3410, 10, [3500, 3501, 3503, 3504])

lines.append("#BLANKETTSLUT")

return lines

def generate_pdf(self, destination_folder):
import io
import pdfrw
from reportlab.pdfgen import canvas

field_x_positions = [58, 122, 217, 302, 388, 475]
field_char_widths = [10, 14, 12, 12, 12, 12]
field_yoffset = 24
sumfield_addition_yoffset = 10

def generate_section(pdf, section, maxlines, ystart):
for (y, fields) in enumerate(section.lines):
for (x, field) in enumerate(fields):
ys = ystart - field_yoffset * y
if field:
pdf.drawString(
x=field_x_positions[x],
y=ys,
text=field[:field_char_widths[x]]
)
for (x, field) in enumerate(section.sums):
if field:
pdf.drawString(
x=field_x_positions[x + 2],
y=ystart - field_yoffset * maxlines - sumfield_addition_yoffset,
text=field[:field_char_widths[x]]
)

def generate_page_1_overlay():
data = io.BytesIO()
pdf = canvas.Canvas(data)
pdf.setFont("Helvetica", 10)
now = datetime.now()
pdf.drawString(x=434, y=744, text=now.strftime("%Y-%m-%d"))
pdf.drawString(x=434, y=708, text=str(self._page_number))
pdf.drawString(x=46, y=660, text=self._personal_details.namn)
pdf.drawString(x=434, y=660, text=self._personal_details.personnummer)
if self._section_a and self._section_a.lines:
generate_section(pdf, self._section_a, 9, 588)
pdf.save()
data.seek(0)
return data

def generate_page_2_overlay():
data = io.BytesIO()
pdf = canvas.Canvas(data)
pdf.setFont("Helvetica", 10)
pdf.drawString(x=434, y=792, text=self._personal_details.personnummer)
if self._section_c and self._section_c.lines:
generate_section(pdf, self._section_c, 7, 720)
if self._section_d and self._section_d.lines:
generate_section(pdf, self._section_d, 7, 360)
pdf.save()
data.seek(0)
return data

def merge(overlay_canvases, template_path):
template_pdf = pdfrw.PdfReader(template_path)
overlay_pdfs = [pdfrw.PdfReader(x) for x in overlay_canvases]
for page, data in zip(template_pdf.pages, overlay_pdfs):
overlay = pdfrw.PageMerge().add(data.pages[0])[0]
pdfrw.PageMerge(page).add(overlay).render()
form = io.BytesIO()
pdfrw.PdfWriter().write(form, template_pdf)
form.seek(0)
return form

def save(form, filename):
with open(filename, 'wb') as f:
f.write(form.read())

if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
pagestr = "%02d" % self._page_number

form = merge([generate_page_1_overlay(), generate_page_2_overlay()], template_path='docs/K4.pdf')
save(form, filename=f"{destination_folder}/k4_no{pagestr}.pdf")
51 changes: 51 additions & 0 deletions report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import argparse
import datetime
import os
from enum import Enum

from taxdata import PersonalDetails, Fees, Trades, TaxEvent
import tax


class Format(Enum):
pdf = 'pdf'
sru = 'sru'

def __str__(self):
return self.value

parser = argparse.ArgumentParser(description='Swedish cryptocurrency tax reporting script')
parser.add_argument('year', type=int,
help='Tax year to create report for')
parser.add_argument('--trades', help='Read trades from csv file', default='data/trades.csv')
parser.add_argument('--format', type=Format, choices=list(Format), default=Format.sru,
help='The file format of the generated report')
parser.add_argument('--decimal-sru', help='Report decimal amounts in sru mode (not supported by Skatteverket yet)', action='store_true')
parser.add_argument('--exclude-groups', nargs='*', help='Exclude cointracking group from report')
parser.add_argument('--coin-report', help='Generate report of remaining coins and their cost basis at end of year', action='store_true')
opts = parser.parse_args()

personal_details = PersonalDetails.read_from("data/personal_details.json")
trades = Trades.read_from(opts.trades)
stock_tax_events = TaxEvent.read_stock_tax_events_from("data/stocks.json") if os.path.exists("data/stocks.json") else None

tax_events = tax.compute_tax(trades,
datetime.datetime(year=opts.year,month=1,day=1,hour=0, minute=0),
datetime.datetime(year=opts.year,month=12,day=31,hour=23, minute=59),
exclude_groups=opts.exclude_groups if opts.exclude_groups else [],
coin_report_filename="out/coin_report.csv" if opts.coin_report else None
)

if opts.format == Format.sru and not opts.decimal_sru:
tax_events = tax.convert_to_integer_amounts(tax_events)

tax_events = tax.convert_sek_to_integer_amounts(tax_events)

pages = tax.generate_k4_pages(opts.year, personal_details, tax_events, stock_tax_events=stock_tax_events)

if opts.format == Format.sru:
tax.generate_k4_sru(pages, personal_details, "out")
elif opts.format == Format.pdf:
tax.generate_k4_pdf(pages, "out")

tax.output_totals(tax_events, stock_tax_events=stock_tax_events)
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pdfrw==0.4
Pillow==5.0.0
reportlab==3.4.0
Loading

0 comments on commit d8997dd

Please sign in to comment.