diff options
| author | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-10-05 20:28:18 +0200 |
|---|---|---|
| committer | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-10-05 20:28:18 +0200 |
| commit | 1c53bd3ac83cc7a985983ac656bc2599276808a4 (patch) | |
| tree | 47b890b696649fcf85462d71058fb66c078bdca0 | |
| parent | 8aa66a6c6c0d8984b7d2668c03bad5a3b29e3a33 (diff) | |
country data, working on tax reports
| -rw-r--r-- | docs/CHANGES.rst | 3 | ||||
| -rw-r--r-- | include/administration.hpp | 26 | ||||
| -rw-r--r-- | include/config.hpp | 3 | ||||
| -rw-r--r-- | include/countries.hpp | 42 | ||||
| -rw-r--r-- | run.bat | 2 | ||||
| -rw-r--r-- | src/administration.cpp | 92 | ||||
| -rw-r--r-- | src/countries.cpp | 88 | ||||
| -rw-r--r-- | src/countries/nl.cpp | 100 | ||||
| -rw-r--r-- | src/logger.cpp | 2 |
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 @@ -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" |
