-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tax report generation given export from cointracking.info
- Loading branch information
1 parent
b997efb
commit d8997dd
Showing
9 changed files
with
720 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"namn": "Förnamn Efternamn", | ||
"personnummer": "YYYYMMDD-NNNN", | ||
"postnummer": "NNNNN", | ||
"postort": "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": ...} | ||
... | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.