I am trying to build something on the side and so I've gotten my company number together with VAT registration. That means I can make purchases with VAT reverse charge. In Denmark, VAT is 25%. Excluding that means 1/4th cut in price which for a self-funded hustle is very appreciated.
Pre history
When I first registered, I was using a bank that supported holding balances in both DKK, EUR and USD. That made it a pain to do proper book keeping because of mixed currencies. I tried Dinero.dk which was nice but it freaked out when I tried setting currencies other than Danish Krone. Their support referred me to VISMA, the more serious platform. However I quickly found that it was prohibitively complicated and demanding for my very simple case.
As time proven, I settled for an Excel sheet. Basic columns such as total paid, vat amount, currency etc. Then I did my first submission of VAT, it is called Moms in Danish. I made some formulas and got what I wanted. But it was too much hassle than I would have preferred. I did it again and with the turn of 2024 I knew I will automate this as every real developer would do.
Code
Something that changed is that this year, 2024, I decided to pay using Danish Krones no matter the destination currency (aka, to leverage the perks of credit cards). This removed the previous challenge and made it even simpler. Then I wrote the following code to fetch the csv of my records in Google Sheets and calculate.
More fluff of mine
Ironically enough, even though I am better versed in English than in Danish at the time being, I find certain explanations in Danish easier to understand. Speaking of which, I got to really acknowledge how great Denmark is for making it easy to understand the taxes.
Both the documentation on the website provided in multiple languages and the helpful phone line, is what makes me pay taxes with the smile. Seriously good job by SKAT and that's one of places Denmark really stands out.
Finally the code
So all in all, this is the script I came up with.
import { parse } from 'csv-parse/sync';
const { SHEETS_CSV_URL, FROM_DATE, TO_DATE } = Bun.env;
if (!SHEETS_CSV_URL) {
console.error('SHEETS_CSV_URL is required');
process.exit(1);
}
if (!FROM_DATE) {
console.error('FROM_DATE is required');
process.exit(1);
}
if (!TO_DATE) {
console.error('TO_DATE is required');
process.exit(1);
}
const csvString = await (await fetch(SHEETS_CSV_URL)).text();
const records = parse(csvString, {
cast: true,
cast_date: true,
columns: true,
skip_empty_lines: true,
});
type Record = {
invoiceId: string;
date: Date;
name: string;
type: 'A - Services' | 'A - Goods' | 'B - Services';
inEu: boolean;
inDk: boolean;
grandTotal: number;
vatRate: number;
baseValue: number;
vatValue: number;
};
const filteredRecords = records.filter((r: Record) => {
const date = r.date;
return date >= new Date(FROM_DATE) && date <= new Date(TO_DATE);
});
const tax = {
'vat-in-dk': 0,
'vat-on-goods-purchased-outside-denmark': 0,
'vat-on-services-purchased-outside-denmark-subject-to-a-reverse-charge': 0,
'vat-on-services-purchased-outside-denmark-outside-eu': 0,
'eu-sales-with-vat': 0,
'eu-sales-without-vat': 0,
'vat-paid': 0,
'vat-collected': 0,
'box-a-services': 0,
'box-a-goods': 0,
'box-b-services': 0,
'box-b-goods': 0,
'box-c-services': 0,
};
tax['vat-in-dk'] = filteredRecords
.filter((r: Record) => r.inDk)
.filter((r: Record) => r.type === 'A - Services' || r.type === 'A - Goods')
.reduce((acc: number, r: Record) => acc + r.vatValue, 0);
let vatOf25estimatedForReverseCharge = 0;
tax['vat-on-goods-purchased-outside-denmark'] = filteredRecords
.filter((r: Record) => !r.inDk)
.filter((r: Record) => r.type === 'A - Goods')
.reduce((acc: number, r: Record) => {
let value = r.vatValue;
if (r.vatRate === 0) {
value = r.grandTotal * 0.25;
vatOf25estimatedForReverseCharge += value;
}
return acc + value;
}, 0);
tax['vat-on-services-purchased-outside-denmark-subject-to-a-reverse-charge'] =
filteredRecords
.filter((r: Record) => !r.inDk && r.inEu)
.filter((r: Record) => r.type === 'A - Services')
.filter((r: Record) => r.vatRate === 0)
.reduce((acc: number, r: Record) => {
let value = r.vatValue;
if (r.vatRate === 0) {
value = r.grandTotal * 0.25;
vatOf25estimatedForReverseCharge += value;
}
return acc + value;
}, 0);
tax['vat-on-services-purchased-outside-denmark-outside-eu'] = filteredRecords
.filter((r: Record) => !r.inDk && !r.inEu)
.filter((r: Record) => r.type === 'A - Services')
.reduce((acc: number, r: Record) => {
let value = r.vatValue;
if (r.vatRate === 0) {
value = r.grandTotal * 0.25;
vatOf25estimatedForReverseCharge += value;
}
return acc + value;
}, 0);
console.info({ vatOf25estimatedForReverseCharge });
tax['vat-paid'] =
tax['vat-in-dk'] +
tax['vat-on-goods-purchased-outside-denmark'] +
tax[
'vat-on-services-purchased-outside-denmark-subject-to-a-reverse-charge'
] +
tax['vat-on-services-purchased-outside-denmark-outside-eu'];
tax['box-a-goods'] = filteredRecords
.filter((r: Record) => r.inEu && !r.inDk)
.filter((r: Record) => r.type === 'A - Goods')
.reduce((acc: number, r: Record) => acc + r.grandTotal, 0);
tax['box-a-services'] = filteredRecords
.filter((r: Record) => r.inEu && !r.inDk)
.filter((r: Record) => r.type === 'A - Services')
.reduce((acc: number, r: Record) => acc + r.grandTotal, 0);
tax['box-b-services'] = filteredRecords
.filter((r: Record) => r.inEu && !r.inDk)
.filter((r: Record) => r.type === 'B - Services')
.reduce((acc: number, r: Record) => acc + r.grandTotal, 0);
tax['eu-sales-with-vat'] = filteredRecords
.filter((r: Record) => r.inEu && !r.inDk)
.filter((r: Record) => r.type === 'B - Services')
.filter((r: Record) => r.vatRate > 0)
.reduce((acc: number, r: Record) => acc + r.grandTotal, 0);
tax['eu-sales-without-vat'] = filteredRecords
.filter((r: Record) => r.inEu && !r.inDk)
.filter((r: Record) => r.type === 'B - Services')
.filter((r: Record) => r.vatRate === 0)
.reduce((acc: number, r: Record) => acc + r.baseValue, 0);
Object.keys(tax).forEach((key) => {
tax[key] = Math.round(tax[key]);
});
const dansk = {
'Moms af varekøb i udlandet (både EU og lande uden for EU)':
tax['vat-on-goods-purchased-outside-denmark'],
'Moms af ydelseskøb i udlandet med omvendt betalingspligt':
tax[
'vat-on-services-purchased-outside-denmark-subject-to-a-reverse-charge'
],
Købsmoms: tax['vat-paid'],
'Rubrik A - varer': tax['box-a-goods'],
'Rubrik A - ydelser': tax['box-a-services'],
'Rubrik B - ydelser': tax['box-b-services'],
'EU-salg med moms': tax['eu-sales-with-vat'],
'EU-salg uden moms': tax['eu-sales-without-vat'],
};
console.dir(dansk);
I like TypeScript for how easy it is throw something together and just run the code. I like Bun that it allows me to execute TypeScript with no need to transpile first.
This script will print all the numbers that I need to fill out in the SKAT form and that is soo so nice. If you're doing the same, you're welcome to use it too. Just make sure to match the columns.
On GitHub
I also put it up in the repo here https://github.com/flexchar/bun-scripts/tree/main/skat. I haven't figured out the ultimate structure for these seldom used scripts.
PS. If you notice that something is not quite right, please do let me know. At the end of the day, it is a little challenge to interpret the taxation rules. :)