diff options
| author | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-10-11 09:41:14 +0200 |
|---|---|---|
| committer | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-10-11 09:41:14 +0200 |
| commit | d83e5e8cd66f05ca7e6aa9fc645788313d89dfe7 (patch) | |
| tree | 987e9c3acc0232e2df2109d776f410bee591eacd | |
| parent | cf5dfa405fa3d9b480794f7f2c32e325fdfd134c (diff) | |
multi currency invoice handling for tax report
| -rw-r--r-- | docs/CHANGES.rst | 4 | ||||
| -rw-r--r-- | include/administration.hpp | 3 | ||||
| -rw-r--r-- | libs/greatest/greatest.h | 11 | ||||
| -rw-r--r-- | src/administration.cpp | 11 | ||||
| -rw-r--r-- | src/countries/nl.cpp | 115 | ||||
| -rw-r--r-- | src/ui/ui_tax.cpp | 28 | ||||
| -rw-r--r-- | tests/nl_tax_tests.cpp | 35 |
7 files changed, 143 insertions, 64 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index a31ff8a..74e4f6a 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -1,8 +1,5 @@ .. _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 @@ -30,7 +27,6 @@ for invoice importing using AI: - send invoice via Holodeck instance - View invoice history for contacts - View invoice history for projects -- Create quarterly tax reports for NL. - validate data within administration on save to make sure it is valid for transmissions. (e.g. rules of https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-AccountingSupplierParty/cac-Party/cbc-EndpointID/) - View local business number / vat number naming on contact form. e.g. when country is austria: "Österreichische Umsatzsteuer-Identifikationsnummer" - ICP reports diff --git a/include/administration.hpp b/include/administration.hpp index f2ca9e4..ebb30d0 100644 --- a/include/administration.hpp +++ b/include/administration.hpp @@ -334,6 +334,8 @@ typedef struct char tax_category[MAX_LEN_SHORT_DESC]; float total_net; float total_tax; + bool show_net; + bool show_tax; } tax_line; typedef struct @@ -567,6 +569,7 @@ namespace administration { a_err billing_item_update_in_invoice(invoice* invoice, billing_item item); a_err billing_item_remove_from_invoice(invoice* invoice, billing_item item); + tax_subtotal billing_item_convert_to_default_currency(invoice* invoice, billing_item item); a_err billing_item_is_valid(billing_item item); u32 billing_item_get_all_for_invoice(invoice* invoice, billing_item* buffer); diff --git a/libs/greatest/greatest.h b/libs/greatest/greatest.h index 58f12b5..06fcca9 100644 --- a/libs/greatest/greatest.h +++ b/libs/greatest/greatest.h @@ -17,6 +17,8 @@ #ifndef GREATEST_H #define GREATEST_H +#include <math.h> + #if defined(__cplusplus) && !defined(GREATEST_NO_EXTERN_CPLUSPLUS) extern "C" { #endif @@ -436,6 +438,8 @@ typedef enum greatest_test_res { GREATEST_ASSERT_FALSEm(#COND, COND) #define GREATEST_ASSERT_EQ(EXP, GOT) \ GREATEST_ASSERT_EQm(#EXP " != " #GOT, EXP, GOT) +#define GREATEST_ASSERT_FEQ(EXP, GOT) \ + GREATEST_ASSERT_FEQm(#EXP " != " #GOT, EXP, GOT) #define GREATEST_ASSERT_NEQ(EXP, GOT) \ GREATEST_ASSERT_NEQm(#EXP " == " #GOT, EXP, GOT) #define GREATEST_ASSERT_GT(EXP, GOT) \ @@ -500,6 +504,12 @@ typedef enum greatest_test_res { #define GREATEST_ASSERT_LTm(MSG,E,G) GREATEST__REL(<, MSG,E,G) #define GREATEST_ASSERT_LTEm(MSG,E,G) GREATEST__REL(<=, MSG,E,G) +#define GREATEST_ASSERT_FEQm(MSG, EXP, GOT) \ + do { \ + greatest_info.assertions++; \ + if (fabs(EXP - GOT) > 0.001f) { GREATEST_FAILm(MSG); } \ + } while (0) + /* Fail if EXP != GOT (equality comparison by ==). * Warning: FMT, EXP, and GOT will be evaluated more * than once on failure. */ @@ -1208,6 +1218,7 @@ greatest_run_info greatest_info #define ASSERTm GREATEST_ASSERTm #define ASSERT_FALSE GREATEST_ASSERT_FALSE #define ASSERT_EQ GREATEST_ASSERT_EQ +#define ASSERT_FEQ GREATEST_ASSERT_FEQ #define ASSERT_NEQ GREATEST_ASSERT_NEQ #define ASSERT_GT GREATEST_ASSERT_GT #define ASSERT_GTE GREATEST_ASSERT_GTE diff --git a/src/administration.cpp b/src/administration.cpp index 5675cb4..495f847 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -2036,6 +2036,17 @@ a_err administration::billing_item_update_in_invoice(invoice* invoice, billing_i return A_ERR_NOT_FOUND; } +tax_subtotal administration::billing_item_convert_to_default_currency(invoice* invoice, billing_item item) +{ + tax_subtotal result = {0}; + if (invoice->net != 0.0f) result.net = item.net / (invoice->orig_net / invoice->net); + if (invoice->tax != 0.0f) result.tax = item.tax / (invoice->orig_tax / invoice->tax); + if (invoice->total != 0.0f) result.total = item.total / (invoice->orig_total / invoice->total); + if (invoice->allowance != 0.0f) result.allowance = item.allowance / (invoice->orig_allowance / invoice->allowance); + + return result; +} + a_err administration::billing_item_is_valid(billing_item item) { a_err result = A_ERR_SUCCESS; diff --git a/src/countries/nl.cpp b/src/countries/nl.cpp index bc45b59..cf79ee2 100644 --- a/src/countries/nl.cpp +++ b/src/countries/nl.cpp @@ -26,30 +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 {"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}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1", "", 0.0f, 0.0f, false, false}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1a", "1a", 0.0f, 0.0f, true, true}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1b", "1b", 0.0f, 0.0f, true, true}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1c", "1c", 0.0f, 0.0f, true, true}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1d", "1d", 0.0f, 0.0f, true, true}; + report->lines[report->line_count++] = tax_line {"taxes.nl.1e", "1e", 0.0f, 0.0f, true, false}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.2", "", 0.0f, 0.0f, false, false}; + report->lines[report->line_count++] = tax_line {"taxes.nl.2a", "2a", 0.0f, 0.0f, true, true}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.3", "", 0.0f, 0.0f, false, false}; + report->lines[report->line_count++] = tax_line {"taxes.nl.3a", "3a", 0.0f, 0.0f, true, false}; + report->lines[report->line_count++] = tax_line {"taxes.nl.3b", "3b", 0.0f, 0.0f, true, false}; + report->lines[report->line_count++] = tax_line {"taxes.nl.3c", "3c", 0.0f, 0.0f, true, false}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.4", "", 0.0f, 0.0f, false, false}; + report->lines[report->line_count++] = tax_line {"taxes.nl.4a", "4a", 0.0f, 0.0f, true, true}; + report->lines[report->line_count++] = tax_line {"taxes.nl.4b", "4b", 0.0f, 0.0f, true, true}; + + report->lines[report->line_count++] = tax_line {"taxes.nl.5", "", 0.0f, 0.0f, false, false}; + report->lines[report->line_count++] = tax_line {"taxes.nl.5a", "5a", 0.0f, 0.0f, false, true}; + report->lines[report->line_count++] = tax_line {"taxes.nl.5b", "5b", 0.0f, 0.0f, false, true}; + + report->lines[report->line_count++] = tax_line {"taxes.total", "5c", 0.0f, 0.0f, false, true}; } bool _nl_add_billing_item_to_tax_report(tax_report* report, invoice* inv, billing_item* item) @@ -60,6 +60,8 @@ bool _nl_add_billing_item_to_tax_report(tax_report* report, invoice* inv, billin a_err err = administration::tax_rate_get_by_id(&rate, item->tax_rate_id); if (err != A_ERR_SUCCESS) return 0; + tax_subtotal totals = administration::billing_item_convert_to_default_currency(inv, *item); + if (inv->is_outgoing) { tax_line* a5 = administration::get_tax_line_from_report(report, "5a"); // Total owed. @@ -68,42 +70,42 @@ bool _nl_add_billing_item_to_tax_report(tax_report* report, invoice* inv, billin { if (rate.rate == 21.0f) { tax_line* tl = administration::get_tax_line_from_report(report, "1a"); - tl->total_net += item->net; - tl->total_tax += item->tax; + tl->total_net += totals.net; + tl->total_tax += totals.tax; - a5->total_tax += item->tax; + a5->total_tax += totals.tax; } else if (rate.rate == 9.0f) { - tax_line* tl = administration::get_tax_line_from_report(report, "1b"); - tl->total_net += item->net; - tl->total_tax += item->tax; + tax_line* tl = administration::get_tax_line_from_report(report, "1b"); + tl->total_net += totals.net; + tl->total_tax += totals.tax; - a5->total_tax += item->tax; + a5->total_tax += totals.tax; } // TODO 1c else if (rate.rate > 0.0f) { tax_line* tl = administration::get_tax_line_from_report(report, "1d"); - tl->total_net += item->net; - tl->total_tax += item->tax; + tl->total_net += totals.net; + tl->total_tax += totals.tax; - a5->total_tax += item->tax; + a5->total_tax += totals.tax; } else if (rate.rate == 0.0f) { - tax_line* tl = administration::get_tax_line_from_report(report, "1e"); - tl->total_net += item->net; - tl->total_tax += item->tax; + tax_line* tl = administration::get_tax_line_from_report(report, "1e"); + tl->total_net += totals.net; + tl->total_tax += totals.tax; - a5->total_tax += item->tax; + a5->total_tax += totals.tax; } } else if (!country::is_EU(inv->customer.address.country_code)) { tax_line* tl = administration::get_tax_line_from_report(report, "3a"); - tl->total_net += item->net; + tl->total_net += totals.net; // Tax is paid to country of customer. } else { tax_line* tl = administration::get_tax_line_from_report(report, "3b"); - tl->total_net += item->net; + tl->total_net += totals.net; // Tax is paid to country of customer. } // TODO 3c @@ -117,34 +119,31 @@ bool _nl_add_billing_item_to_tax_report(tax_report* report, invoice* inv, billin if (strops::equals(rate.category_code, "AE")) { // NL reverse charge. tax_line* tl = administration::get_tax_line_from_report(report, "2a"); + tl->total_net += totals.net; + tl->total_tax += totals.net * 0.21f; // TODO fr? - tl->total_net += item->net; - tl->total_tax += item->net * 0.21f; // TODO fr? - - a5->total_tax += item->net * 0.21f; - b5->total_tax += item->net * 0.21f; + a5->total_tax += totals.net * 0.21f; + b5->total_tax += totals.net * 0.21f; } else { - b5->total_tax += item->tax; + b5->total_tax += totals.tax; } } else if (!country::is_EU(inv->supplier.address.country_code)) { tax_line* tl = administration::get_tax_line_from_report(report, "4a"); + tl->total_net += totals.net; + tl->total_tax += totals.net * 0.21f; - tl->total_net += item->net; - tl->total_tax += item->net * 0.21f; - - a5->total_tax += item->net * 0.21f; - b5->total_tax += item->net * 0.21f; + a5->total_tax += totals.net * 0.21f; + b5->total_tax += totals.net * 0.21f; } else { tax_line* tl = administration::get_tax_line_from_report(report, "4b"); + tl->total_net += totals.net; + tl->total_tax += totals.net * 0.21f; - tl->total_net += item->net; - tl->total_tax += item->net * 0.21f; - - a5->total_tax += item->net * 0.21f; - b5->total_tax += item->net * 0.21f; + a5->total_tax += totals.net * 0.21f; + b5->total_tax += totals.net * 0.21f; } } @@ -158,7 +157,9 @@ float _nl_calculate_tax_report_final(tax_report* report) 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); + if (total->total_tax < 0.0f) total->total_tax = (float)ceil(total->total_tax); + else total->total_tax = (float)floor(total->total_tax); + return total->total_tax; } time_t _nl_get_invoice_date_to_use_for_tax_report(invoice* inv) diff --git a/src/ui/ui_tax.cpp b/src/ui/ui_tax.cpp index 37eeaee..8a119e4 100644 --- a/src/ui/ui_tax.cpp +++ b/src/ui/ui_tax.cpp @@ -66,9 +66,9 @@ void ui::draw_tax_report() { 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::TableSetupColumn("##names", ImGuiTableColumnFlags_WidthFixed, 60); + ImGui::TableSetupColumn(report.quarter_str, ImGuiTableColumnFlags_WidthFixed, 120); + ImGui::TableSetupColumn("##tax", ImGuiTableColumnFlags_WidthFixed, 120); ImGui::TableHeadersRow(); ImGui::PopFont(); @@ -85,6 +85,7 @@ void ui::draw_tax_report() ImGui::TableSetColumnIndex(0); ImGui::Text("%s", locale::get(line.tax_description)); ImGui::TableSetColumnIndex(1); ImGui::Text("%s", locale::get(line.tax_category)); + #if 0 if (!strops::equals(line.tax_category, "")) { ImGui::TableSetColumnIndex(2); ImGui::Text("%.2f %s", line.total_net, currency_symbol); @@ -102,6 +103,27 @@ void ui::draw_tax_report() } } } + #else + ImGui::TableSetColumnIndex(2); + if (line.show_net) ImGui::Text("%.2f %s", line.total_net, currency_symbol); + else ImGui::Text(""); + + ImGui::TableSetColumnIndex(3); + if (line.show_tax) { + 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); + } + } + } + else ImGui::Text(""); + #endif if (bold) ImGui::PopFont(); } diff --git a/tests/nl_tax_tests.cpp b/tests/nl_tax_tests.cpp index bd7b51d..31382ed 100644 --- a/tests/nl_tax_tests.cpp +++ b/tests/nl_tax_tests.cpp @@ -106,9 +106,44 @@ TEST _nl_tax_1e(void) PASS(); } +TEST _nl_tax_2currency(void) +{ + administration::create_default(test_file_path); + administration::company_info_set(_create_nl_business()); + + invoice inv = _create_nl_b2b_inv_outgoing(); + administration::invoice_set_currency(&inv, "USD"); + + administration::billing_item_add_to_invoice(&inv, _create_bi(1, 20.0f, "NL/21.00")); + administration::billing_item_add_to_invoice(&inv, _create_bi(1, 30.0f, "NL/21.00")); + + float eur_usd_exchange_rate = 1.2f; + + inv.net = 50.0f / eur_usd_exchange_rate; + inv.tax = inv.net * 0.21f; + inv.allowance = 0.0f; + inv.total = inv.net + inv.tax; + + ASSERT_EQ(administration::invoice_add(&inv), A_ERR_SUCCESS); + + tax_statement statement; + administration::create_tax_statement(&statement); + ASSERT_EQ(statement.report_count, 1); + + tax_line* tl = administration::get_tax_line_from_report(&statement.reports[0], "1a"); + GREATEST_ASSERT_FEQ(tl->total_net, inv.net); + GREATEST_ASSERT_FEQ(tl->total_tax, inv.tax); + + tax_line* tl2 = administration::get_tax_line_from_report(&statement.reports[0], "5a"); + GREATEST_ASSERT_FEQ(tl2->total_tax, inv.tax); + + PASS(); +} + SUITE(nl_tax_statement) { RUN_TEST(_nl_tax_1a); RUN_TEST(_nl_tax_1b); RUN_TEST(_nl_tax_1d); RUN_TEST(_nl_tax_1e); + RUN_TEST(_nl_tax_2currency); }
\ No newline at end of file |
