summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAldrik Ramaekers <aldrikboy@gmail.com>2025-10-11 09:41:14 +0200
committerAldrik Ramaekers <aldrikboy@gmail.com>2025-10-11 09:41:14 +0200
commitd83e5e8cd66f05ca7e6aa9fc645788313d89dfe7 (patch)
tree987e9c3acc0232e2df2109d776f410bee591eacd
parentcf5dfa405fa3d9b480794f7f2c32e325fdfd134c (diff)
multi currency invoice handling for tax report
-rw-r--r--docs/CHANGES.rst4
-rw-r--r--include/administration.hpp3
-rw-r--r--libs/greatest/greatest.h11
-rw-r--r--src/administration.cpp11
-rw-r--r--src/countries/nl.cpp115
-rw-r--r--src/ui/ui_tax.cpp28
-rw-r--r--tests/nl_tax_tests.cpp35
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