From cf5dfa405fa3d9b480794f7f2c32e325fdfd134c Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Fri, 10 Oct 2025 23:11:18 +0200 Subject: tax statement UI --- docs/CHANGES.rst | 3 ++ include/administration.hpp | 3 +- include/ui.hpp | 3 ++ src/administration.cpp | 4 +- src/countries/nl.cpp | 50 ++++++++++---------- src/locales/en.cpp | 26 +++++++++++ src/ui/ui_main.cpp | 6 +-- src/ui/ui_tax.cpp | 111 +++++++++++++++++++++++++++++++++++++++++++++ tests/nl_tax_tests.cpp | 8 ++-- 9 files changed, 181 insertions(+), 33 deletions(-) create mode 100644 src/ui/ui_tax.cpp diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index c7a020b..a31ff8a 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -1,5 +1,8 @@ .. _changes: +INCORRECT DATA: +- invoices should only accept the default currency. if importing an invoice with another currency, it should be converted manually. (due to tax reporting mixing currencies atm.) + TODO: for invoice importing using AI: 1. all address data should be editable because import is not perfect diff --git a/include/administration.hpp b/include/administration.hpp index 69c0c0a..f2ca9e4 100644 --- a/include/administration.hpp +++ b/include/administration.hpp @@ -330,6 +330,7 @@ typedef struct typedef struct { + char tax_description[MAX_LEN_LONG_DESC]; char tax_category[MAX_LEN_SHORT_DESC]; float total_net; float total_tax; @@ -347,7 +348,7 @@ typedef struct typedef struct { - u32 quarter_count; + u32 report_count; tax_report reports[MAX_LEN_TAX_REPORT_QUARTERS]; } tax_statement; diff --git a/include/ui.hpp b/include/ui.hpp index 9304f78..0f71c1e 100644 --- a/include/ui.hpp +++ b/include/ui.hpp @@ -77,6 +77,7 @@ namespace ui { void draw_expenses(); void draw_earnings(); void draw_log(); + void draw_tax_report(); // Setup calls. void setup_invoices(); @@ -85,12 +86,14 @@ namespace ui { void setup_settings(); void setup_expenses(); void setup_earnings(); + void setup_tax_report(); // Destroy calls. void destroy_invoices(); void destroy_settings(); void destroy_expenses(); void destroy_earnings(); + void destroy_tax_report(); } diff --git a/src/administration.cpp b/src/administration.cpp index 0ad9684..5675cb4 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -484,7 +484,7 @@ void administration::create_tax_statement(tax_statement* statement) char* country_code = company_info_get().address.country_code; assert(statement); - statement->quarter_count = 0; + statement->report_count = 0; u32 invoice_count = administration::invoice_count(); if (invoice_count == 0) return; @@ -526,7 +526,7 @@ void administration::create_tax_statement(tax_statement* statement) country::fill_tax_report_with_categories(country_code, &quarter); - statement->reports[statement->quarter_count++] = quarter; + statement->reports[statement->report_count++] = quarter; } // Fill quarters. diff --git a/src/countries/nl.cpp b/src/countries/nl.cpp index 9c8a20b..bc45b59 100644 --- a/src/countries/nl.cpp +++ b/src/countries/nl.cpp @@ -26,25 +26,30 @@ time_t _nl_get_default_invoice_expire_duration() 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}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1", "", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1a", "1a", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1b", "1b", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1c", "1c", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1d", "1d", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1e", "1e", 0.0f, 0.0f}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.2", "", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.2a", "2a", 0.0f, 0.0f}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.3", "", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.3a", "3a", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.3b", "3b", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.3c", "3c", 0.0f, 0.0f}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.4", "", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.4a", "4a", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.4b", "4b", 0.0f, 0.0f}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.5", "", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.5a", "5a", 0.0f, 0.0f}; + report->lines[report->line_count++] = tax_line {"taxes.nl.5b", "5b", 0.0f, 0.0f}; + + report->lines[report->line_count++] = tax_line {"taxes.total", "5c", 0.0f, 0.0f}; } bool _nl_add_billing_item_to_tax_report(tax_report* report, invoice* inv, billing_item* item) @@ -108,7 +113,7 @@ bool _nl_add_billing_item_to_tax_report(tax_report* report, invoice* inv, billin tax_line* a5 = administration::get_tax_line_from_report(report, "5a"); // Total owed. tax_line* b5 = administration::get_tax_line_from_report(report, "5b"); // Input tax. - if (strops::equals(inv->customer.address.country_code, "NL")) { + if (strops::equals(inv->supplier.address.country_code, "NL")) { if (strops::equals(rate.category_code, "AE")) { // NL reverse charge. tax_line* tl = administration::get_tax_line_from_report(report, "2a"); @@ -123,8 +128,7 @@ bool _nl_add_billing_item_to_tax_report(tax_report* report, invoice* inv, billin b5->total_tax += item->tax; } } - - if (!country::is_EU(inv->customer.address.country_code)) { + else if (!country::is_EU(inv->supplier.address.country_code)) { tax_line* tl = administration::get_tax_line_from_report(report, "4a"); tl->total_net += item->net; @@ -151,7 +155,7 @@ float _nl_calculate_tax_report_final(tax_report* report) { tax_line* a5 = administration::get_tax_line_from_report(report, "5a"); // Total owed. tax_line* b5 = administration::get_tax_line_from_report(report, "5b"); // Input tax. - tax_line* total = administration::get_tax_line_from_report(report, "Total"); + tax_line* total = administration::get_tax_line_from_report(report, "5c"); total->total_tax = a5->total_tax - b5->total_tax; return (float)ceil(total->total_tax); diff --git a/src/locales/en.cpp b/src/locales/en.cpp index e7ad41b..e68e930 100644 --- a/src/locales/en.cpp +++ b/src/locales/en.cpp @@ -208,6 +208,32 @@ locale_entry en_locales[] = { {"import.error.upload","Failure: Upload failed"}, {"import.error.query","Failure: Querying service failed"}, {"import.error.import","Failure: Failed to import result from service"}, + + // Tax statement strings. + {"taxes.total", "Total"}, + + {"taxes.nl.1", "Domestic supplies and services"}, + {"taxes.nl.1a", "Supplies/services taxed at the high rate"}, + {"taxes.nl.1b", "Supplies/services taxed at the reduced rate"}, + {"taxes.nl.1c", "Supplies/services taxed at other rates (excluding 0%)"}, + {"taxes.nl.1d", "Private use"}, + {"taxes.nl.1e", "Supplies/services taxed at 0% or not taxed with you"}, + + {"taxes.nl.2", "Reverse-charge arrangements within the Netherlands"}, + {"taxes.nl.2a", "Supplies/services for which VAT has been reverse-charged to you"}, + + {"taxes.nl.3", "Supplies and services to or in other countries"}, + {"taxes.nl.3a", "Supplies to countries outside the EU (exports)"}, + {"taxes.nl.3b", "Supplies to or services in EU countries"}, + {"taxes.nl.3c", "Intra-EU distance sales and installation supplies"}, + + {"taxes.nl.4", "Services supplied to you from abroad"}, + {"taxes.nl.4a", "Supplies/services received from countries outside the EU (imports)"}, + {"taxes.nl.4b", "Supplies/services received from countries within the EU (intra-EU acquisitions)"}, + + {"taxes.nl.5", "Input tax and small businesses scheme (KOR)"}, + {"taxes.nl.5a", "VAT due (sections 1 to 4)"}, + {"taxes.nl.5b", "Input tax (VAT deductible)"}, }; int en_locale_count = sizeof(en_locales) / sizeof(en_locales[0]); \ No newline at end of file diff --git a/src/ui/ui_main.cpp b/src/ui/ui_main.cpp index 9e6aeb8..cf30840 100644 --- a/src/ui/ui_main.cpp +++ b/src/ui/ui_main.cpp @@ -28,7 +28,7 @@ void (*drawcalls[ui::main_state::UI_END])(void) = { ui::draw_expenses, ui::draw_contacts, ui::draw_earnings, - 0, + ui::draw_tax_report, ui::draw_projects, ui::draw_settings, ui::draw_log, @@ -39,7 +39,7 @@ void (*setupcalls[ui::main_state::UI_END])(void) = { ui::setup_expenses, ui::setup_contacts, ui::setup_earnings, - 0, + ui::setup_tax_report, ui::setup_projects, ui::setup_settings, 0, @@ -50,7 +50,7 @@ void (*destroycalls[ui::main_state::UI_END])(void) = { ui::destroy_expenses, 0, ui::destroy_earnings, - 0, + ui::destroy_tax_report, 0, ui::destroy_settings, 0, diff --git a/src/ui/ui_tax.cpp b/src/ui/ui_tax.cpp new file mode 100644 index 0000000..37eeaee --- /dev/null +++ b/src/ui/ui_tax.cpp @@ -0,0 +1,111 @@ +/* +* Copyright (c) 2025 Aldrik Ramaekers +* +* 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 "ui.hpp" +#include "imgui.h" +#include "memops.hpp" +#include "strops.hpp" +#include "locales.hpp" +#include "administration.hpp" + +tax_statement* statement = 0; + +void ui::setup_tax_report() +{ + statement = (tax_statement*)memops::alloc(sizeof(tax_statement)); + administration::create_tax_statement(statement); +} + +void ui::destroy_tax_report() +{ + memops::unalloc(statement); +} + +void ui::draw_tax_report() +{ + static s32 current_page = 0; + s32 max_page = statement->report_count; + + if (current_page >= max_page-1) current_page = max_page-1; + if (current_page < 0) current_page = 0; + + bool enable_prev = current_page > 0; + if (!enable_prev) ImGui::BeginDisabled(); + if (ImGui::Button(locale::get("ui.prev")) && current_page > 0) current_page--; + if (!enable_prev) ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::Text("(%d/%d)", current_page+1, max_page); + + ImGui::SameLine(); + bool enable_next = current_page < max_page-1; + if (!enable_next) ImGui::BeginDisabled(); + if (ImGui::Button(locale::get("ui.next")) && current_page < max_page-1) current_page++; + if (!enable_next) ImGui::EndDisabled(); + + ImGui::Spacing(); + + char* currency_symbol = administration::get_currency_symbol_for_currency(administration::get_default_currency()); + tax_report report = statement->reports[current_page]; + + if (ImGui::BeginTable("QuarterlyTaxTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Hideable)) + { + ImGui::PushFont(fontBold); + ImGui::TableSetupColumn("##desc", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("##names", ImGuiTableColumnFlags_WidthFixed, 150); + ImGui::TableSetupColumn(report.quarter_str, ImGuiTableColumnFlags_WidthFixed, 150); + ImGui::TableSetupColumn("##tax", ImGuiTableColumnFlags_WidthFixed, 150); + + ImGui::TableHeadersRow(); + ImGui::PopFont(); + + for (u32 x = 0; x < report.line_count; x++) + { + tax_line line = report.lines[x]; + bool is_last = x == (report.line_count-1); + bool bold = is_last || strops::equals(line.tax_category, ""); + + if (bold) ImGui::PushFont(fontBold); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); ImGui::Text("%s", locale::get(line.tax_description)); + ImGui::TableSetColumnIndex(1); ImGui::Text("%s", locale::get(line.tax_category)); + + if (!strops::equals(line.tax_category, "")) { + ImGui::TableSetColumnIndex(2); ImGui::Text("%.2f %s", line.total_net, currency_symbol); + + + ImGui::TableSetColumnIndex(3); + if (!is_last) ImGui::Text("%.2f %s", line.total_tax, currency_symbol); + else { + if (line.total_tax < 0.0f) { + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(235, 64, 52, 255)); + ImGui::Text("(%.0f %s)", line.total_tax, currency_symbol); + ImGui::PopStyleColor(); + } + else { + ImGui::Text("%.0f %s", line.total_tax, currency_symbol); + } + } + } + + if (bold) ImGui::PopFont(); + } + + ImGui::EndTable(); + } +} \ No newline at end of file diff --git a/tests/nl_tax_tests.cpp b/tests/nl_tax_tests.cpp index e6fda94..bd7b51d 100644 --- a/tests/nl_tax_tests.cpp +++ b/tests/nl_tax_tests.cpp @@ -13,7 +13,7 @@ TEST _nl_tax_1a(void) tax_statement statement; administration::create_tax_statement(&statement); - ASSERT_EQ(statement.quarter_count, 1); + ASSERT_EQ(statement.report_count, 1); tax_line* tl = administration::get_tax_line_from_report(&statement.reports[0], "1a"); ASSERT_EQ(tl->total_net, 50.0f); @@ -38,7 +38,7 @@ TEST _nl_tax_1b(void) tax_statement statement; administration::create_tax_statement(&statement); - ASSERT_EQ(statement.quarter_count, 1); + ASSERT_EQ(statement.report_count, 1); tax_line* tl = administration::get_tax_line_from_report(&statement.reports[0], "1b"); ASSERT_EQ(tl->total_net, 25.0f); @@ -69,7 +69,7 @@ TEST _nl_tax_1d(void) tax_statement statement; administration::create_tax_statement(&statement); - ASSERT_EQ(statement.quarter_count, 1); + ASSERT_EQ(statement.report_count, 1); tax_line* tl = administration::get_tax_line_from_report(&statement.reports[0], "1d"); ASSERT_EQ(tl->total_net, 45.0f); @@ -94,7 +94,7 @@ TEST _nl_tax_1e(void) tax_statement statement; administration::create_tax_statement(&statement); - ASSERT_EQ(statement.quarter_count, 1); + ASSERT_EQ(statement.report_count, 1); tax_line* tl = administration::get_tax_line_from_report(&statement.reports[0], "1e"); ASSERT_EQ(tl->total_net, 80.0f); -- cgit v1.2.3-70-g09d2