summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAldrik Ramaekers <aldrikboy@gmail.com>2025-10-05 20:28:18 +0200
committerAldrik Ramaekers <aldrikboy@gmail.com>2025-10-05 20:28:18 +0200
commit1c53bd3ac83cc7a985983ac656bc2599276808a4 (patch)
tree47b890b696649fcf85462d71058fb66c078bdca0
parent8aa66a6c6c0d8984b7d2668c03bad5a3b29e3a33 (diff)
country data, working on tax reports
-rw-r--r--docs/CHANGES.rst3
-rw-r--r--include/administration.hpp26
-rw-r--r--include/config.hpp3
-rw-r--r--include/countries.hpp42
-rw-r--r--run.bat2
-rw-r--r--src/administration.cpp92
-rw-r--r--src/countries.cpp88
-rw-r--r--src/countries/nl.cpp100
-rw-r--r--src/logger.cpp2
9 files changed, 350 insertions, 8 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst
index 93fd296..c7a020b 100644
--- a/docs/CHANGES.rst
+++ b/docs/CHANGES.rst
@@ -5,11 +5,11 @@ for invoice importing using AI:
1. all address data should be editable because import is not perfect
2. file path should not be editable as it is imported
+- create invoice PDF for NL https://goedestartbelastingdienst.nl/wiki/view/50bdccd8-f9a0-4297-b57f-3a6651cbe05c/factuureisen
- toggle on invoice form wether price in inclusive of tax.
- retrieve available balance from AI api & show in settings/services.
- let user choose the model to use in settings/services/ai
- real error logging for OpenAI and importing in general
-- when importing an invoice: do not accept invoices for unsupported countries (yet)
- write tests for strops.hpp
- log_set_depth function so data can be grouped
- log elapsed time for ai requests
@@ -21,7 +21,6 @@ for invoice importing using AI:
- invoice sequential number should be modifyable & checked for uniqueness (for external invoices being imported)
- allow cost centers to be deleted that have 0 references
- Send invoice by email
-- create invoice from PDF file
- create invoice from image of receipt
- create invoice from UBL file
- read invoice from Holodeck instance
diff --git a/include/administration.hpp b/include/administration.hpp
index 75f5b52..54f8aac 100644
--- a/include/administration.hpp
+++ b/include/administration.hpp
@@ -37,6 +37,8 @@
#define MAX_LEN_TAX_SECTION 16
#define MAX_LEN_API_KEY 256
+#define MAX_LEN_TAX_REPORT_LINES 50
+#define MAX_LEN_TAX_REPORT_QUARTERS 400
#define MAX_LEN_INCOME_STATEMENT_REPORT_QUARTERS 400
#define MAX_LEN_QUARTERLY_REPORT_PROJECTS 200
#define MAX_LEN_PROJECT_REPORT_COSTCENTERS 50
@@ -326,6 +328,29 @@ typedef struct
quarterly_report quarters[MAX_LEN_INCOME_STATEMENT_REPORT_QUARTERS];
} income_statement;
+typedef struct
+{
+ char tax_category[MAX_LEN_SHORT_DESC];
+ float total_net;
+ float total_tax;
+} tax_line;
+
+typedef struct
+{
+ bool is_empty;
+ u16 year; // 00-99
+ u8 quarter; // 0-3
+ char quarter_str[MAX_LEN_SHORT_DESC];
+ u32 line_count;
+ tax_line lines[MAX_LEN_TAX_REPORT_LINES];
+} tax_report;
+
+typedef struct
+{
+ u32 quarter_count;
+ tax_report reports[MAX_LEN_TAX_REPORT_QUARTERS];
+} tax_statement;
+
// Administration callback functions.
// These are called when adding/updating/deleting entries.
// These are NOT called when using import functions.
@@ -438,6 +463,7 @@ namespace administration {
void set_ai_service(ai_service provider);
void create_income_statement(income_statement* statement);
+ void create_tax_statement(tax_statement* statement);
bool can_create_invoices();
// Contact functions.
diff --git a/include/config.hpp b/include/config.hpp
index 5fe4a2c..aecec75 100644
--- a/include/config.hpp
+++ b/include/config.hpp
@@ -31,7 +31,8 @@
namespace config {
static const char* PROGRAM_VERSION = "0.1.0"; // major.minor.patch
-
+
+ // TODO get rid of this and use country iter
static const char* country_codes[] = {
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
diff --git a/include/countries.hpp b/include/countries.hpp
new file mode 100644
index 0000000..0849ed6
--- /dev/null
+++ b/include/countries.hpp
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2025 Aldrik Ramaekers <aldrik.ramaekers@gmail.com>
+*
+* Permission to use, copy, modify, and/or distribute this software for any
+* purpose with or without fee is hereby granted, provided that the above
+* copyright notice and this permission notice appear in all copies.
+*
+* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#pragma once
+
+#include "administration.hpp"
+
+typedef struct
+{
+ char* country_code;
+ bool is_EU;
+ time_t (*get_default_invoice_expire_duration)();
+ void (*fill_tax_report_with_categories)(tax_report* report);
+ char* (*get_tax_category_for_billing_item)(invoice* inv, billing_item* item);
+} country_impl;
+
+namespace country {
+
+ s32 get_count();
+ char* get_code_by_index(s32 index);
+
+ time_t get_default_invoice_expire_duration(char* country_code);
+
+ bool is_EU(char* country_code);
+ bool tax_is_implemented(char* country_code);
+ void fill_tax_report_with_categories(char* country_code, tax_report* report);
+ char* get_tax_category_for_billing_item(char* country_code, invoice* inv, billing_item* item);
+
+} \ No newline at end of file
diff --git a/run.bat b/run.bat
index a6f535c..00eeb4a 100644
--- a/run.bat
+++ b/run.bat
@@ -34,7 +34,7 @@ set LIB_SOURCES=libs\imgui-1.92.1\backends\imgui_impl_dx11.cpp^
@set INCLUDE_DIRS=/I"libs/imgui-1.92.1" /I"libs/imgui-1.92.1/backends" /I"/" /I"libs/openssl-3.6.0-beta1/x64/include" /I"libs/cpp-httplib" /I"libs/timer_lib" /I"libs/greatest" /I"libs/simclist-1.5" /I"libs/tinyfiledialogs" /I"libs/zip/src" /I"libs/xml.c/src" /I"libs/" /Iinclude
@set DEFINITIONS=/D_BUILD_DATE_=\"%date%\" /D_COMMIT_=\"%COMMIT_ID%\" /D_PLATFORM_=\"win64\" /D_CRT_SECURE_NO_WARNINGS
-if "%1"=="-t" @set SOURCES= tests\main.cpp src\administration.cpp src\administration_writer.cpp src\administration_reader.cpp src\strops.cpp src\logger.cpp src\locales.cpp src\locales\*.cpp src\ai_providers\*.cpp src\ui\helpers.cpp
+if "%1"=="-t" @set SOURCES= tests\main.cpp src\administration.cpp src\administration_writer.cpp src\administration_reader.cpp src\strops.cpp src\logger.cpp src\locales.cpp src\locales\*.cpp src\ai_providers\*.cpp src\ui\helpers.cpp src\importer.cpp src\memops.cpp src\countries.cpp
if "%1"=="-t" @set OUT_EXE=accounting_tests
cl %FLAGS% %INCLUDE_DIRS% %DEFINITIONS% %SOURCES% %LIB_SOURCES% /Fe%OUT_DIR%/%OUT_EXE%.exe /Fd%OUT_DIR%/vc140.pdb /Fo%OUT_DIR%/ /link %LIBS%
diff --git a/src/administration.cpp b/src/administration.cpp
index 9790281..9adce55 100644
--- a/src/administration.cpp
+++ b/src/administration.cpp
@@ -20,6 +20,7 @@
#include "memops.hpp"
#include "logger.hpp"
#include "strops.hpp"
+#include "countries.hpp"
#include "administration.hpp"
#include "administration_writer.hpp"
@@ -45,7 +46,7 @@ static int compare_tax_countries(const void *a, const void *b)
return strcmp(objA->country_code, objB->country_code);
}
-time_t administration::get_default_invoice_expire_duration()
+time_t administration::get_default_invoice_expire_duration() // TODO depricated
{
return (30 * 24 * 60 * 60); // 30 days
}
@@ -475,6 +476,93 @@ static void administration_debug_print_income_statement(income_statement* statem
}
#endif
+void administration::create_tax_statement(tax_statement* statement)
+{
+ STOPWATCH_START;
+
+ char* country_code = company_info_get().address.country_code;
+
+ assert(statement);
+ statement->quarter_count = 0;
+
+ u32 invoice_count = administration::invoice_count();
+ if (invoice_count == 0) return;
+ invoice* invoice_buffer = (invoice*)memops::alloc(sizeof(invoice)*invoice_count);
+ invoice_count = administration::invoice_get_all(invoice_buffer);
+
+ // Find oldest and youngest invoice.
+ time_t oldest = INT64_MAX;
+ time_t youngest = 0;
+ for (u32 i = 0; i < invoice_count; i++)
+ {
+ if (invoice_buffer[i].delivered_at < oldest) oldest = invoice_buffer[i].delivered_at;
+ if (invoice_buffer[i].delivered_at > youngest) youngest = invoice_buffer[i].delivered_at;
+ }
+
+ u16 oldest_year;
+ u8 oldest_quarter;
+ time_t_to_quarter(oldest, &oldest_year, &oldest_quarter);
+ oldest_quarter = 0;
+
+ u16 youngest_year;
+ u8 youngest_quarter;
+ time_t_to_quarter(youngest, &youngest_year, &youngest_quarter);
+ youngest_quarter = 3;
+
+ u32 num_quarters = (youngest_quarter+1 + (youngest_year*4)) - (oldest_quarter + (oldest_year*4));
+ assert(num_quarters <= MAX_LEN_INCOME_STATEMENT_REPORT_QUARTERS);
+ assert(num_quarters % 4 == 0);
+
+ // Generate quarters.
+ for (u32 i = 0; i < num_quarters; i++)
+ {
+ tax_report quarter;
+ quarter.year = oldest_year + (u16)((oldest_quarter+i) / 4);
+ quarter.quarter = (u8)((oldest_quarter + (i)) % 4);
+ quarter.line_count = 0;
+ quarter.is_empty = 1;
+ strops::format(quarter.quarter_str, MAX_LEN_SHORT_DESC, "%dQ%d", quarter.quarter+1, quarter.year);
+
+ country::fill_tax_report_with_categories(country_code, &quarter);
+ }
+
+ // Fill quarters.
+ for (u32 i = 0; i < invoice_count; i++)
+ {
+ invoice* inv = &invoice_buffer[i];
+
+ u16 yy;
+ u8 qq;
+ time_t_to_quarter(inv->issued_at, &yy, &qq);
+
+ u32 report_index = (qq + (yy*4)) - (oldest_quarter + (oldest_year*4));
+
+ tax_report* quarter = &statement->reports[report_index];
+ assert(yy == quarter->year && qq == quarter->quarter);
+ quarter->is_empty = 0;
+
+ billing_item* item_buffer = (billing_item*)memops::alloc(sizeof(billing_item)*billing_item_count(inv));
+ u32 invoice_items = administration::billing_item_get_all_for_invoice(inv, item_buffer);
+ for (u32 x = 0; x < invoice_items; x++)
+ {
+ char* tax_category = country::get_tax_category_for_billing_item(country_code, inv, &item_buffer[x]);
+ for (u32 t = 0; t < quarter->line_count; t++)
+ {
+ if (strops::equals(quarter->lines[t].tax_category, tax_category))
+ {
+ quarter->lines[t].total_net += inv->net;
+ quarter->lines[t].total_tax += inv->tax;
+ }
+ }
+
+ memops::unalloc(item_buffer);
+ }
+ }
+
+ memops::unalloc(invoice_buffer);
+ logger::info("Created tax statement in %.3fms.", STOPWATCH_TIME);
+}
+
void administration::create_income_statement(income_statement* statement)
{
STOPWATCH_START;
@@ -573,7 +661,7 @@ void administration::create_income_statement(income_statement* statement)
u16 yy;
u8 qq;
- time_t_to_quarter(inv->delivered_at, &yy, &qq);
+ time_t_to_quarter(inv->issued_at, &yy, &qq);
u32 report_index = (qq + (yy*4)) - (oldest_quarter + (oldest_year*4));
diff --git a/src/countries.cpp b/src/countries.cpp
new file mode 100644
index 0000000..d4bcae1
--- /dev/null
+++ b/src/countries.cpp
@@ -0,0 +1,88 @@
+/*
+* Copyright (c) 2025 Aldrik Ramaekers <aldrik.ramaekers@gmail.com>
+*
+* Permission to use, copy, modify, and/or distribute this software for any
+* purpose with or without fee is hereby granted, provided that the above
+* copyright notice and this permission notice appear in all copies.
+*
+* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "strops.hpp"
+#include "countries.hpp"
+
+#include "countries/nl.cpp"
+
+static country_impl country_map[] = {
+ _nl_country_impl,
+ // Add new locales here.
+};
+static const u32 country_map_count = sizeof(country_map) / sizeof(country_map[0]);
+
+s32 country::get_count()
+{
+ return country_map_count;
+}
+
+char* country::get_code_by_index(s32 index)
+{
+ return country_map[index].country_code;
+}
+
+static s32 get_index_by_country_code(char* country_code)
+{
+ for (u32 i = 0; i < country_map_count; i++)
+ {
+ if (strops::equals(country_map[i].country_code, country_code)) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+bool country::is_EU(char* country_code)
+{
+ s32 index = get_index_by_country_code(country_code);
+ if (index == -1) return 0;
+
+ return country_map[index].is_EU;
+}
+
+time_t country::get_default_invoice_expire_duration(char* country_code)
+{
+ s32 index = get_index_by_country_code(country_code);
+ if (index == -1) return 0;
+
+ return country_map[index].get_default_invoice_expire_duration();
+}
+
+bool country::tax_is_implemented(char* country_code)
+{
+ s32 index = get_index_by_country_code(country_code);
+ if (index == -1) return false;
+
+ return country_map[index].fill_tax_report_with_categories &&
+ country_map[index].get_tax_category_for_billing_item;
+}
+
+void country::fill_tax_report_with_categories(char* country_code, tax_report* report)
+{
+ s32 index = get_index_by_country_code(country_code);
+ assert(index != -1);
+
+ country_map[index].fill_tax_report_with_categories(report);
+}
+
+char* country::get_tax_category_for_billing_item(char* country_code, invoice* inv, billing_item* item)
+{
+ s32 index = get_index_by_country_code(country_code);
+ assert(index != -1);
+
+ return country_map[index].get_tax_category_for_billing_item(inv, item);
+} \ No newline at end of file
diff --git a/src/countries/nl.cpp b/src/countries/nl.cpp
new file mode 100644
index 0000000..85005d6
--- /dev/null
+++ b/src/countries/nl.cpp
@@ -0,0 +1,100 @@
+/*
+* Copyright (c) 2025 Aldrik Ramaekers <aldrik.ramaekers@gmail.com>
+*
+* Permission to use, copy, modify, and/or distribute this software for any
+* purpose with or without fee is hereby granted, provided that the above
+* copyright notice and this permission notice appear in all copies.
+*
+* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#include "countries.hpp"
+
+time_t _nl_get_default_invoice_expire_duration()
+{
+ return (15 * 24 * 60 * 60); // 15 days
+}
+
+void _nl_fill_tax_report_with_categories(tax_report* report)
+{
+ report->lines[report->line_count++] = tax_line {"1a", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"1b", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"1c", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"1d", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"1e", 0.0f, 0.0f};
+
+ report->lines[report->line_count++] = tax_line {"2a", 0.0f, 0.0f};
+
+ report->lines[report->line_count++] = tax_line {"3a", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"3b", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"3c", 0.0f, 0.0f};
+
+ report->lines[report->line_count++] = tax_line {"4a", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"4b", 0.0f, 0.0f};
+
+ report->lines[report->line_count++] = tax_line {"5a", 0.0f, 0.0f};
+ report->lines[report->line_count++] = tax_line {"5b", 0.0f, 0.0f};
+
+ report->lines[report->line_count++] = tax_line {"Total", 0.0f, 0.0f};
+}
+
+char* _nl_get_tax_category_for_billing_item(invoice* inv, billing_item* item)
+{
+ // https://goedestartbelastingdienst.nl/wiki/view/7494ecb4-f6d2-4f85-a200-5a3ee5d45b75/btw-aangifte-het-invullen-van-de-verschillende-rubrieken
+
+ tax_rate rate;
+ a_err err = administration::tax_rate_get_by_id(&rate, item->tax_rate_id);
+ if (err != A_ERR_SUCCESS) return 0;
+
+
+ /*
+ We moeten 5a ook nog berekenen, dus misschien tax_report meegeven en hier alles doen wat nodig is?
+ we moeten ook nog het uiteindelijke bedrag uitregenen 5a-5b dus dat moet ook hier gebeuren.
+
+ */
+
+ // Outgoing = 1 + 3
+ if (inv->is_outgoing) {
+
+ if (strops::equals(inv->customer.address.country_code, "NL"))
+ {
+ if (rate.rate == 21.0f) return "1a";
+ else if (rate.rate == 9.0f) return "1b";
+ // TODO 1c
+ else if (rate.rate > 0.0f) return "1d";
+ else if (rate.rate == 0.0f) return "1e";
+ }
+ else if (!country::is_EU(inv->customer.address.country_code)) return "3a";
+ else return "3b";
+ // TODO 3c
+
+ }
+
+ // Incomming = 2 + 4 + 5
+ else {
+
+ if (strops::equals(inv->customer.address.country_code, "NL")) {
+ if (strops::equals(rate.category_code, "AE")) return "2a"; // NL reverse charge.
+ else return "5b";
+ }
+
+ if (!country::is_EU(inv->customer.address.country_code)) return "4a";
+ else return "4b";
+
+ }
+ return 0;
+}
+
+country_impl _nl_country_impl = {
+ "NL",
+ true,
+ _nl_get_default_invoice_expire_duration,
+ _nl_fill_tax_report_with_categories,
+ _nl_get_tax_category_for_billing_item,
+}; \ No newline at end of file
diff --git a/src/logger.cpp b/src/logger.cpp
index 0035948..e2c33e2 100644
--- a/src/logger.cpp
+++ b/src/logger.cpp
@@ -14,8 +14,6 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
-#include <stdio.h>
-#include <stdarg.h>
#include <time.h>
#include "timer.h"