summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/CHANGES.rst2
-rw-r--r--docs/README.rst2
-rw-r--r--include/administration.hpp1
-rw-r--r--include/config.hpp7
-rw-r--r--include/file_templates.hpp8
-rw-r--r--include/ui.hpp18
-rw-r--r--src/administration.cpp13
-rw-r--r--src/administration_writer.cpp34
-rw-r--r--src/config.cpp24
-rw-r--r--src/ui/helpers.cpp74
-rw-r--r--src/ui/imgui_extensions.cpp414
-rw-r--r--src/ui/ui_contacts.cpp163
-rw-r--r--src/ui/ui_expenses.cpp123
-rw-r--r--src/ui/ui_invoices.cpp193
-rw-r--r--src/ui/ui_projects.cpp17
-rw-r--r--tests/peppol_write_tests.cpp86
16 files changed, 620 insertions, 559 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst
index 6611e50..8701d77 100644
--- a/docs/CHANGES.rst
+++ b/docs/CHANGES.rst
@@ -1,6 +1,8 @@
.. _changes:
TODO:
+- get rid of billing item properties: invoice_id and tax_rate_id. replace tax_rate_id with copy of tax rate
+- get rid of customer_id and supplier_id. Just check for existing contact entries when finishing invoice and create new contact if necessary.
- validate billing items in invoice add/update
- project start and end date should be stored as YYYY-MM-DD
- Send invoice by email
diff --git a/docs/README.rst b/docs/README.rst
index c92bb7a..fcd228d 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -38,6 +38,8 @@ This section lists all supported countries. If the country you operate from is n
3. Dependencies
---------------
+Changes/patches have been made to most of these libraries.
+
- ImGui 1.92.1 (https://github.com/ocornut/imgui)
- simclist 1.5 (https://mij.oltrelinux.com/devel/simclist/)
- ImGuiDatePicker (https://github.com/DnA-IntRicate/ImGuiDatePicker)
diff --git a/include/administration.hpp b/include/administration.hpp
index cfdb293..0d15195 100644
--- a/include/administration.hpp
+++ b/include/administration.hpp
@@ -367,6 +367,7 @@ typedef struct
#define A_ERR_MISSING_BUSINESSID (1ULL << 14)
#define A_ERR_MAX_ITEMS_REACHED (1ULL << 15)
#define A_ERR_MISSING_CODE (1ULL << 16)
+#define A_ERR_MISSING_EMAIL (1ULL << 17)
typedef uint32_t a_err;
diff --git a/include/config.hpp b/include/config.hpp
index 2284c8a..9598841 100644
--- a/include/config.hpp
+++ b/include/config.hpp
@@ -29,4 +29,9 @@
#define u8 uint8_t
#define u16 uint16_t
#define u32 uint32_t
-#define u64 uint64_t \ No newline at end of file
+#define u64 uint64_t
+
+extern const char* country_codes[];
+extern s32 country_count;
+
+#define COLOR_ERROR_OUTLINE IM_COL32(255, 0, 0, 80) \ No newline at end of file
diff --git a/include/file_templates.hpp b/include/file_templates.hpp
index 1980d97..9064abf 100644
--- a/include/file_templates.hpp
+++ b/include/file_templates.hpp
@@ -84,14 +84,10 @@ const char* peppol_invoice_line_template =
" <cbc:InvoicedQuantity unitCode=\"{{UNIT_CODE}}\">{{QUANTITY}}</cbc:InvoicedQuantity>\n"
" <cbc:LineExtensionAmount currencyID=\"{{CURRENCY}}\">{{LINE_AMOUNT}}</cbc:LineExtensionAmount>\n"
-" <cac:OrderLineReference>\n"
-" <cbc:LineID>{{TAX_BRACKET_ID}}</cbc:LineID>\n"
-" </cac:OrderLineReference>\n"
-
" <cac:AllowanceCharge>\n"
" <cbc:ChargeIndicator>false</cbc:ChargeIndicator>\n"
" <cbc:AllowanceChargeReason>Discount</cbc:AllowanceChargeReason>\n"
-" {{ALLOWANCE_IS_PERCENTAGE}}\n"
+" <cbc:MultiplierFactorNumeric>{{DISCOUNT_TOTAL_PERCENTAGE}}</cbc:MultiplierFactorNumeric>\n"
" <cbc:Amount currencyID=\"{{CURRENCY}}\">{{DISCOUNT_TOTAL}}</cbc:Amount>\n"
" <cbc:BaseAmount currencyID=\"{{CURRENCY}}\">{{DISCOUNT_BASE_AMOUNT}}</cbc:BaseAmount>\n"
" </cac:AllowanceCharge>\n"
@@ -168,7 +164,6 @@ const char *peppol_invoice_template =
"\n"
" <cac:PartyLegalEntity>\n"
" <cbc:RegistrationName>{{SUPPLIER_LEGAL_NAME}}</cbc:RegistrationName>\n"
-" <cbc:CompanyID schemeID=\"ZZZ\">{{SUPPLIER_BUSINESS_ID}}</cbc:CompanyID>\n"
" </cac:PartyLegalEntity>\n"
"\n"
" <cac:Contact>\n"
@@ -208,7 +203,6 @@ const char *peppol_invoice_template =
"\n"
" <cac:PartyLegalEntity>\n"
" <cbc:RegistrationName>{{CUSTOMER_LEGAL_NAME}}</cbc:RegistrationName>\n"
-" <cbc:CompanyID schemeID=\"ZZZ\">{{CUSTOMER_BUSINESS_ID}}</cbc:CompanyID>\n"
" </cac:PartyLegalEntity>\n"
"\n"
" <cac:Contact>\n"
diff --git a/include/ui.hpp b/include/ui.hpp
index cdffa58..d6c5fad 100644
--- a/include/ui.hpp
+++ b/include/ui.hpp
@@ -17,6 +17,7 @@
#pragma once
#include "imgui.h"
+#include "administration.hpp"
#define STATUS_TEXT_LEN 64
#define STATUS_DURATION 4.0f
@@ -61,8 +62,6 @@ typedef struct
extern ImFont* fontBold;
extern ImFont* fontBig;
-void ui_helper_draw_required_tag();
-
ui_status ui_get_status();
void ui_set_status_loading(bool loading);
void ui_set_status_error(const char* txt);
@@ -94,4 +93,17 @@ void ui_destroy_expenses();
void ui_destroy_earnings();
// Custom imgui widgets.
-int TextInputWithAutocomplete(const char* label, const char* hint, char* buffer, size_t buf_size, char** suggestions, int suggestion_count); \ No newline at end of file
+namespace ImGui
+{
+ int TextInputWithAutocomplete(const char* hint, char* buffer, size_t buf_size, char** suggestions, int suggestion_count, bool has_error);
+
+ void FormContactAutocomplete(contact* buffer, bool has_error);
+ void FormInputTextWithErrorHint(const char* hint, void* data, char* buffer, size_t buf_size, bool has_error);
+ void FormCountryCombo(void* data, char* buffer, size_t buf_size);
+ void FormContactTypeCombo(contact_type* type);
+ void FormCostCenterCombo(char* costcenter_id);
+ void FormProjectCombo(char* project_id);
+ void FormTaxRateCombo(char* tax_rate_id, char* orig_country, char* dest_country);
+ bool FormCurrencyCombo(char* currency);
+ void FormToggleCombo(bool *buffer, char* option1, char* option2);
+} \ No newline at end of file
diff --git a/src/administration.cpp b/src/administration.cpp
index f8d53f9..09bb560 100644
--- a/src/administration.cpp
+++ b/src/administration.cpp
@@ -126,8 +126,8 @@ static void administration_get_random_billing_items(invoice* inv)
tax_rate buffer[20];
- char* country_codes[1] = {inv->supplier.address.country_code};
- u32 rate_count = administration_tax_rate_get_by_country(buffer, 1, country_codes);
+ char* tax_country_codes[1] = {inv->supplier.address.country_code};
+ u32 rate_count = administration_tax_rate_get_by_country(buffer, 1, tax_country_codes);
tax_rate rand_rate = buffer[rand() % rate_count];
strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
@@ -1045,6 +1045,7 @@ a_err administration_contact_is_valid(contact data)
{
a_err result = A_ERR_SUCCESS;
if (strlen(data.name) == 0) result |= A_ERR_MISSING_NAME;
+ if (strlen(data.email) == 0) result |= A_ERR_MISSING_EMAIL;
if (strlen(data.address.city) == 0) result |= A_ERR_MISSING_CITY;
if (strlen(data.address.postal) == 0) result |= A_ERR_MISSING_POSTAL;
if (strlen(data.address.address1) == 0) result |= A_ERR_MISSING_ADDRESS1;
@@ -1326,7 +1327,7 @@ a_err administration_tax_rate_add(tax_rate data)
return A_ERR_SUCCESS;
}
-u32 administration_tax_rate_get_by_country(tax_rate* buffer, u32 code_count, char** country_codes)
+u32 administration_tax_rate_get_by_country(tax_rate* buffer, u32 code_count, char** tax_country_codes)
{
assert(buffer);
@@ -1342,7 +1343,7 @@ u32 administration_tax_rate_get_by_country(tax_rate* buffer, u32 code_count, cha
}
for (u32 x = 0; x < code_count; x++) {
- if (strcmp(c.country_code, country_codes[x]) == 0) {
+ if (strcmp(c.country_code, tax_country_codes[x]) == 0) {
buffer[write_cursor++] = c;
continue;
}
@@ -1708,7 +1709,7 @@ static void administration_invoice_set_refs(invoice* inv)
}
else
{
- // Store customer id in invoice, (only id is stored to disk, supplier field is filled on load).
+ // Store customer id in invoice.
strops_copy(inv->customer_id, inv->customer.id, sizeof(inv->customer_id));
}
}
@@ -1772,7 +1773,7 @@ a_err administration_invoice_add(invoice* inv)
memcpy(new_inv, &copy, sizeof(invoice));
- new_inv->payment_means.payment_method = PAYMENT_METHOD_DEBIT_TRANSFER;
+ new_inv->payment_means.payment_method = PAYMENT_METHOD_STANDING_AGREEMENT;
strops_copy(new_inv->payment_means.payee_bank_account, inv->customer.bank_account, sizeof(new_inv->payment_means.payee_bank_account));
strops_copy(new_inv->payment_means.payee_account_name, inv->customer.name, sizeof(new_inv->payment_means.payee_account_name));
strops_copy(new_inv->payment_means.service_provider_id, "", sizeof(new_inv->payment_means.service_provider_id));
diff --git a/src/administration_writer.cpp b/src/administration_writer.cpp
index 4722f7c..e7d9e07 100644
--- a/src/administration_writer.cpp
+++ b/src/administration_writer.cpp
@@ -155,8 +155,13 @@ static bool administration_writer_write_to_zip(char* entry_to_replace, char* ori
/////////////////////////////
//// Invoices
/////////////////////////////
+
static char* administration_writer_get_eas_id_for_contact(contact contact)
{
+ if (contact.type == contact_type::CONTACT_CONSUMER) {
+ return "[CONSUMER]";
+ }
+
// https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/
char* country_code = contact.address.country_code;
@@ -176,7 +181,6 @@ static char* administration_writer_get_eas_id_for_contact(contact contact)
if (strcmp(country_code, "LU") == 0) return contact.taxid; // Luxembourg
if (strcmp(country_code, "LV") == 0) return contact.taxid; // Latvia
if (strcmp(country_code, "MT") == 0) return contact.taxid; // Malta
- if (strcmp(country_code, "NL") == 0) return contact.taxid; // Netherlands
if (strcmp(country_code, "PL") == 0) return contact.taxid; // Poland
if (strcmp(country_code, "PT") == 0) return contact.taxid; // Portugal
if (strcmp(country_code, "RO") == 0) return contact.taxid; // Romania
@@ -185,6 +189,7 @@ static char* administration_writer_get_eas_id_for_contact(contact contact)
if (strcmp(country_code, "ES") == 0) return contact.taxid; // Spain
// Countries using business identification numbers.
+ if (strcmp(country_code, "NL") == 0) return contact.businessid; // Netherlands
if (strcmp(country_code, "SE") == 0) return contact.businessid; // Sweden
if (strcmp(country_code, "LT") == 0) return contact.businessid; // Lithuania
if (strcmp(country_code, "IT") == 0) return contact.businessid; // Italy
@@ -193,8 +198,14 @@ static char* administration_writer_get_eas_id_for_contact(contact contact)
return NULL; // Unknown country code
}
-static char* administration_writer_get_eas_scheme_for_address(address addr)
+static char* administration_writer_get_eas_scheme_for_contact(contact contact)
{
+ if (contact.type == contact_type::CONTACT_CONSUMER) {
+ return "0203"; // Hack
+ }
+
+ address addr = contact.address;
+
// https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/
char* country_code = addr.country_code;
if (strcmp(country_code, "AT") == 0) return "9914"; // Austria
@@ -216,7 +227,7 @@ static char* administration_writer_get_eas_scheme_for_address(address addr)
if (strcmp(country_code, "LU") == 0) return "9938"; // Luxembourg
if (strcmp(country_code, "LV") == 0) return "9939"; // Latvia
if (strcmp(country_code, "MT") == 0) return "9943"; // Malta
- if (strcmp(country_code, "NL") == 0) return "9944"; // Netherlands
+ if (strcmp(country_code, "NL") == 0) return "0106"; // Netherlands
if (strcmp(country_code, "PL") == 0) return "9945"; // Poland
if (strcmp(country_code, "PT") == 0) return "9946"; // Portugal
if (strcmp(country_code, "RO") == 0) return "9947"; // Romania
@@ -309,7 +320,7 @@ bool administration_writer_save_invoice_blocking(invoice inv)
strops_replace(file_content, buf_length, "{{INVOICE_DOCUMENT}}", inv.document);
// Supplier data
- strops_replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_SCHEME}}", administration_writer_get_eas_scheme_for_address(inv.supplier.address));
+ strops_replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_SCHEME}}", administration_writer_get_eas_scheme_for_contact(inv.supplier));
strops_replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_ID}}", administration_writer_get_eas_id_for_contact(inv.supplier));
strops_replace(file_content, buf_length, "{{SUPPLIER_ID}}", inv.supplier.id);
strops_replace(file_content, buf_length, "{{SUPPLIER_NAME}}", inv.supplier.name);
@@ -326,7 +337,7 @@ bool administration_writer_save_invoice_blocking(invoice inv)
strops_replace(file_content, buf_length, "{{SUPPLIER_EMAIL}}", inv.supplier.email);
// Customer data
- strops_replace(file_content, buf_length, "{{CUSTOMER_ENDPOINT_SCHEME}}", administration_writer_get_eas_scheme_for_address(inv.customer.address));
+ strops_replace(file_content, buf_length, "{{CUSTOMER_ENDPOINT_SCHEME}}", administration_writer_get_eas_scheme_for_contact(inv.customer));
strops_replace(file_content, buf_length, "{{CUSTOMER_ENDPOINT_ID}}", administration_writer_get_eas_id_for_contact(inv.customer));
strops_replace(file_content, buf_length, "{{CUSTOMER_ID}}", inv.customer.id);
strops_replace(file_content, buf_length, "{{CUSTOMER_NAME}}", inv.customer.name);
@@ -431,19 +442,18 @@ bool administration_writer_save_invoice_blocking(invoice inv)
strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{QUANTITY}}", bi.amount, 2);
strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{UNIT_PRICE}}", bi.net_per_item, 2); // unit price before discount
strops_replace(billing_item_file_content, billing_item_buf_length, "{{UNIT_CODE}}", bi.amount_is_percentage ? "%" : "X");
- strops_replace(billing_item_file_content, billing_item_buf_length, "{{TAX_BRACKET_ID}}", bi.tax_rate_id);
-
+ //strops_replace(billing_item_file_content, billing_item_buf_length, "{{TAX_BRACKET_ID}}", bi.tax_rate_id);
+
if (bi.discount_is_percentage) {
- strops_replace(billing_item_file_content, billing_item_buf_length, "{{ALLOWANCE_IS_PERCENTAGE}}",
- "<cbc:MultiplierFactorNumeric>{{DISCOUNT_TOTAL_PERCENTAGE}}</cbc:MultiplierFactorNumeric>");
- strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", bi.allowance / (bi.net + bi.allowance), 2);
+ strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", (bi.allowance / (bi.net + bi.allowance)) * 100.0f, 2);
+ strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_BASE_AMOUNT}}", bi.net + bi.allowance, 2); // Total net before discount.
}
else {
- strops_replace(billing_item_file_content, billing_item_buf_length, "{{ALLOWANCE_IS_PERCENTAGE}}", "");
+ strops_replace(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", "");
+ strops_replace(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_BASE_AMOUNT}}", "");
}
strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL}}", bi.allowance, 2);
- strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_BASE_AMOUNT}}", bi.net + bi.allowance, 2); // Total net before discount.
u32 content_len = (u32)strlen(billing_item_file_content);
memcpy(billing_item_list_buffer+billing_item_list_buffer_cursor, billing_item_file_content, content_len);
diff --git a/src/config.cpp b/src/config.cpp
new file mode 100644
index 0000000..9ab8e90
--- /dev/null
+++ b/src/config.cpp
@@ -0,0 +1,24 @@
+/*
+* 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 "config.hpp"
+
+const char* country_codes[] = {
+ // "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
+ /*"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT",*/ "NL",
+ // "PL", "PT", "RO", "SK", "SI", "ES", "SE"
+};
+s32 country_count = sizeof(country_codes) / sizeof(country_codes[0]); \ No newline at end of file
diff --git a/src/ui/helpers.cpp b/src/ui/helpers.cpp
index 07302b7..2447270 100644
--- a/src/ui/helpers.cpp
+++ b/src/ui/helpers.cpp
@@ -93,77 +93,3 @@ ui_status ui_get_status()
{
return current_status;
}
-
-void ui_helper_draw_required_tag()
-{
- ImDrawList* draw_list = ImGui::GetWindowDrawList();
-
- const char* text = localize("form.required");
- ImVec2 text_pos = ImGui::GetCursorScreenPos();
- ImVec2 text_size = ImGui::CalcTextSize(text);
- text_pos.y += text_size.y/4.0f;
-
- ImVec4 bg_color = ImVec4(0.9f, 0.235f, 0.235f, 0.4f); // Red background
- ImVec4 text_color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White text
- float rounding = 2.0f;
- float padding = 2.0f;
-
- // Background rectangle
- ImVec2 bg_min = ImVec2(text_pos.x - padding, text_pos.y - padding);
- ImVec2 bg_max = ImVec2(text_pos.x + text_size.x + padding, text_pos.y + text_size.y + padding);
- draw_list->AddRectFilled(bg_min, bg_max, ImColor(bg_color), rounding);
-
- // Foreground text
- ImGui::PushStyleColor(ImGuiCol_Text, text_color);
- ImGui::TextUnformatted(text);
- ImGui::PopStyleColor();
-}
-
-int TextInputWithAutocomplete(const char* label, const char* hint, char* buffer, size_t buf_size,
- char* suggestions[], int suggestion_count)
-{
- int result = -1;
- static bool is_open = false;
- ImGui::InputTextWithHint(label, hint, buffer, buf_size);
-
- if (buffer[0] == '\0' && is_open) is_open = false;
- if (suggestion_count == 0 && is_open) is_open = false;
-
- bool is_active = ImGui::IsItemActive();
- if (is_active && buffer[0] != '\0' && suggestion_count > 0)
- {
- is_open = true;
- }
-
- if (is_open) {
- ImGui::BeginChild("autocomplete_popup", ImVec2(0, 10.0f + suggestion_count*23.0f), true);
- {
- ImVec2 win_pos = ImGui::GetWindowPos();
- ImVec2 win_size = ImGui::GetWindowSize();
- ImVec2 mouse_pos = ImGui::GetMousePos();
-
- bool mouse_clicked_outside = !is_active && ImGui::IsMouseClicked(0) &&
- (mouse_pos.x < win_pos.x || mouse_pos.x > win_pos.x + win_size.x ||
- mouse_pos.y < win_pos.y || mouse_pos.y > win_pos.y + win_size.y);
-
- if (mouse_clicked_outside) is_open = false;
-
- for (int i = 0; i < suggestion_count; ++i)
- {
- ImGui::PushID(i);
- if (ImGui::Selectable(suggestions[i]))
- {
- // Copy selected suggestion to buffer
- strops_copy(buffer, suggestions[i], buf_size);
- buffer[buf_size - 1] = '\0';
- result = i;
- is_open = false;
- }
- ImGui::PopID();
- }
-
- ImGui::EndChild();
- }
- }
- return result;
-}
diff --git a/src/ui/imgui_extensions.cpp b/src/ui/imgui_extensions.cpp
new file mode 100644
index 0000000..5fdc3a7
--- /dev/null
+++ b/src/ui/imgui_extensions.cpp
@@ -0,0 +1,414 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "ui.hpp"
+#include "strops.hpp"
+#include "config.hpp"
+#include "locales.hpp"
+#include "administration.hpp"
+
+namespace ImGui
+{
+ void FormInputTextWithErrorHint(const char* hint, void* data, char* buffer, size_t buf_size, bool has_error)
+ {
+ float widthAvailable = ImGui::GetContentRegionAvail().x;
+ ImGui::SetNextItemWidth(widthAvailable*0.5f);
+
+ char id[MAX_LEN_LONG_DESC];
+ snprintf(id, sizeof(id), "%s##%p", hint, data);
+
+ if (has_error) {
+ ImGui::PushStyleColor(ImGuiCol_Border, COLOR_ERROR_OUTLINE);
+ ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f);
+ }
+ ImGui::InputTextWithHint(id, hint, buffer, buf_size);
+ if (has_error) {
+ ImGui::PopStyleVar();
+ ImGui::PopStyleColor();
+
+ ImGui::SameLine();
+ ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::CalcTextSize("*").x);
+ ImGui::TextColored(ImVec4(1, 0, 0, 1), "*");
+ }
+ }
+
+ void FormCountryCombo(void* data, char* buffer, size_t buf_size)
+ {
+ const char* selected_country = 0;
+ char id[MAX_LEN_LONG_DESC];
+ float widthAvailable = ImGui::GetContentRegionAvail().x;
+ ImGui::SetNextItemWidth(widthAvailable*0.5f);
+
+ const char* countries[30];
+ for (int x = 0; x < country_count; x++)
+ {
+ char locale_str[20];
+ snprintf(locale_str, 20, "country.%s", country_codes[x]);
+ countries[x] = localize(locale_str);
+ }
+
+ for (int i = 0; i < country_count; i++)
+ {
+ if (strcmp(country_codes[i], buffer) == 0)
+ {
+ selected_country = countries[i];
+ break;
+ }
+ }
+ bool has_a_selection = selected_country != 0;
+
+ int selected_country_index = -1;
+ snprintf(id, sizeof(id), "%s##%p", localize("contact.form.country"), data);
+
+ if (!has_a_selection) {
+ ImGui::PushStyleColor(ImGuiCol_Border, COLOR_ERROR_OUTLINE);
+ ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f);
+ }
+
+ if (ImGui::BeginCombo(id, selected_country))
+ {
+ for (int n = 0; n < country_count; n++)
+ {
+ bool is_selected = (selected_country == countries[n]);
+ if (ImGui::Selectable(countries[n], is_selected)) {
+ selected_country = countries[n];
+ selected_country_index = n;
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ if (!has_a_selection) {
+ ImGui::PopStyleVar();
+ ImGui::PopStyleColor();
+
+ ImGui::SameLine();
+ ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::CalcTextSize("*").x);
+ ImGui::TextColored(ImVec4(1, 0, 0, 1), "*");
+ }
+
+ if (selected_country_index != -1) {
+ strops_copy(buffer, country_codes[selected_country_index], buf_size);
+ }
+ }
+
+ void FormContactTypeCombo(contact_type* type)
+ {
+ float widthAvailable = ImGui::GetContentRegionAvail().x;
+ ImGui::SetNextItemWidth(widthAvailable*0.5f);
+ const char* customer_types[2] = { localize("contact.form.type.business"), localize("contact.form.type.consumer") };
+ int currentItem = static_cast<int>(*type);
+ if (ImGui::Combo(localize("contact.form.type"), &currentItem, customer_types, IM_ARRAYSIZE(customer_types)))
+ {
+ *type = static_cast<contact_type>(currentItem);
+ }
+ }
+
+
+ int TextInputWithAutocomplete(const char* hint, char* buffer, size_t buf_size, char* suggestions[], int suggestion_count, bool has_error)
+ {
+ int result = -1;
+ static bool is_open = false;
+
+ if (has_error) {
+ ImGui::PushStyleColor(ImGuiCol_Border, COLOR_ERROR_OUTLINE);
+ ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f);
+ }
+ ImGui::InputTextWithHint(hint, hint, buffer, buf_size);
+ if (has_error) {
+ ImGui::PopStyleVar();
+ ImGui::PopStyleColor();
+
+ ImGui::SameLine();
+ ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::CalcTextSize("*").x);
+ ImGui::TextColored(ImVec4(1, 0, 0, 1), "*");
+ }
+
+
+ if (buffer[0] == '\0' && is_open) is_open = false;
+ if (suggestion_count == 0 && is_open) is_open = false;
+
+ bool is_active = ImGui::IsItemActive();
+ if (is_active && buffer[0] != '\0' && suggestion_count > 0)
+ {
+ is_open = true;
+ }
+
+ if (is_open) {
+ ImGui::BeginChild("autocomplete_popup", ImVec2(0, 10.0f + suggestion_count*23.0f), true);
+ {
+ ImVec2 win_pos = ImGui::GetWindowPos();
+ ImVec2 win_size = ImGui::GetWindowSize();
+ ImVec2 mouse_pos = ImGui::GetMousePos();
+
+ bool mouse_clicked_outside = !is_active && ImGui::IsMouseClicked(0) &&
+ (mouse_pos.x < win_pos.x || mouse_pos.x > win_pos.x + win_size.x ||
+ mouse_pos.y < win_pos.y || mouse_pos.y > win_pos.y + win_size.y);
+
+ if (mouse_clicked_outside) is_open = false;
+
+ for (int i = 0; i < suggestion_count; ++i)
+ {
+ ImGui::PushID(i);
+ if (ImGui::Selectable(suggestions[i]))
+ {
+ // Copy selected suggestion to buffer
+ strops_copy(buffer, suggestions[i], buf_size);
+ buffer[buf_size - 1] = '\0';
+ result = i;
+ is_open = false;
+ }
+ ImGui::PopID();
+ }
+
+ ImGui::EndChild();
+ }
+ }
+ return result;
+ }
+
+ void FormContactAutocomplete(contact* buffer, bool has_error)
+ {
+ float widthAvailable = ImGui::GetContentRegionAvail().x;
+ ImGui::SetNextItemWidth(widthAvailable*0.5f);
+
+ contact autocomplete_list[5];
+ int autocomplete_count = administration_contact_get_autocompletions(autocomplete_list, 5, buffer->name);
+ char* autocomplete_strings[5];
+
+ for (int i = 0; i < autocomplete_count; i++)
+ {
+ autocomplete_strings[i] = (char*)malloc(200);
+ snprintf(autocomplete_strings[i], 200, "%s (%s %s)", autocomplete_list[i].name, autocomplete_list[i].address.address1, autocomplete_list[i].address.address2);
+ }
+
+ int autocomplete_index = ImGui::TextInputWithAutocomplete(localize("contact.form.fullname"),
+ buffer->name, IM_ARRAYSIZE(buffer->name), (char**)autocomplete_strings, autocomplete_count, has_error);
+
+ if (autocomplete_index != -1)
+ {
+ memcpy(buffer, &autocomplete_list[autocomplete_index], sizeof(contact));
+ }
+
+ for (int i = 0; i < autocomplete_count; i++)
+ {
+ free(autocomplete_strings[i]);
+ }
+ }
+
+ void FormCostCenterCombo(char* costcenter_id)
+ {
+ u32 costcenter_count = administration_cost_center_count();
+ cost_center* buffer = (cost_center*) malloc(sizeof(cost_center) * costcenter_count);
+
+ cost_center* selected_costcenter = NULL;
+ costcenter_count = administration_cost_center_get_all(buffer);
+
+ // Select cost center by given id.
+ if (strlen(costcenter_id) > 0)
+ {
+ for (u32 i = 0; i < costcenter_count; i++)
+ {
+ if (strcmp(buffer[i].id, costcenter_id) == 0)
+ {
+ selected_costcenter = &buffer[i];
+ break;
+ }
+ }
+ }
+
+ int selected_costcenter_index = -1;
+ if (ImGui::BeginCombo(localize("invoice.form.costcenter"), selected_costcenter == NULL ? NULL : localize(selected_costcenter->description)))
+ {
+ for (u32 n = 0; n < costcenter_count; n++)
+ {
+ bool is_selected = selected_costcenter && strcmp(selected_costcenter->id, buffer[n].id) == 0;
+ if (ImGui::Selectable(localize(buffer[n].description), is_selected)) {
+ selected_costcenter_index = n;
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ if (selected_costcenter_index != -1) {
+ strops_copy(costcenter_id, buffer[selected_costcenter_index].id, MAX_LEN_ID);
+ }
+
+ free(buffer);
+ }
+
+ void FormProjectCombo(char* project_id)
+ {
+ u32 project_count = administration_project_count();
+ project* buffer = (project*) malloc(sizeof(project) * project_count);
+
+ project* selected_project = NULL;
+ project_count = administration_project_get_all(buffer);
+
+ // Select project by given id.
+ if (strlen(project_id) > 0)
+ {
+ for (u32 i = 0; i < project_count; i++)
+ {
+ if (strcmp(buffer[i].id, project_id) == 0)
+ {
+ selected_project = &buffer[i];
+ break;
+ }
+ }
+ }
+
+ int selected_project_index = -1;
+ if (ImGui::BeginCombo(localize("invoice.form.project"), selected_project == NULL ? NULL : selected_project->description))
+ {
+ for (u32 n = 0; n < project_count; n++)
+ {
+ bool is_selected = selected_project && strcmp(selected_project->id, buffer[n].id) == 0;
+ if (ImGui::Selectable(buffer[n].description, is_selected)) {
+ selected_project_index = n;
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ if (selected_project_index != -1) {
+ strops_copy(project_id, buffer[selected_project_index].id, MAX_LEN_ID);
+ }
+
+ free(buffer);
+ }
+
+ void FormTaxRateCombo(char* tax_rate_id, char* orig_country, char* dest_country)
+ {
+ u32 tax_rate_count = administration_tax_rate_count();
+ tax_rate* buffer = (tax_rate*) malloc(sizeof(tax_rate) * tax_rate_count);
+
+ tax_rate* selected_tax_rate = NULL;
+ char* tax_country_codes[2] = {orig_country, dest_country};
+ tax_rate_count = administration_tax_rate_get_by_country(buffer, strcmp(orig_country, dest_country) == 0 ? 1 : 2, tax_country_codes);
+
+ // Select tax rate by given id.
+ if (strlen(tax_rate_id) > 0)
+ {
+ for (u32 i = 0; i < tax_rate_count; i++)
+ {
+ if (strcmp(buffer[i].id, tax_rate_id) == 0)
+ {
+ selected_tax_rate = &buffer[i];
+ break;
+ }
+ }
+ }
+
+ int selected_tax_rate_index = -1;
+ char rate_str_buf[40];
+ rate_str_buf[0] = 0;
+ if (selected_tax_rate)
+ {
+ if (strcmp(selected_tax_rate->country_code, "00") == 0) {
+ char category_code_desc[MAX_LEN_LONG_DESC];
+ snprintf(category_code_desc, MAX_LEN_LONG_DESC, "taxcategory.%s", selected_tax_rate->category_code);
+ snprintf(rate_str_buf, 40, "%s", localize(category_code_desc));
+ }
+ else snprintf(rate_str_buf, 40, "%s/%.1f%%", selected_tax_rate->country_code, selected_tax_rate->rate);
+ }
+
+ if (ImGui::BeginCombo("##Tax Bracket", rate_str_buf))
+ {
+ for (u32 n = 0; n < tax_rate_count; n++)
+ {
+ bool is_selected = selected_tax_rate && strcmp(selected_tax_rate->id, buffer[n].id) == 0;
+
+ if (strcmp(buffer[n].country_code, "00") == 0) {
+ char category_code_desc[MAX_LEN_LONG_DESC];
+ snprintf(category_code_desc, MAX_LEN_LONG_DESC, "taxcategory.%s", buffer[n].category_code);
+ snprintf(rate_str_buf, 40, "%s", localize(category_code_desc));
+ }
+ else snprintf(rate_str_buf, 40, "%s/%.1f%%", buffer[n].country_code, buffer[n].rate);
+
+ if (ImGui::Selectable(rate_str_buf, is_selected)) {
+ selected_tax_rate_index = n;
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ if (selected_tax_rate_index != -1) {
+ strops_copy(tax_rate_id, buffer[selected_tax_rate_index].id, MAX_LEN_ID);
+ }
+
+ free(buffer);
+ }
+
+ bool FormCurrencyCombo(char* currency)
+ {
+ int currentCurrency = 0;
+ bool result = false;
+
+ // Top 15 most traded currencies + all EU official currencies
+ const char* currencies[] = {
+ // Top 15
+ "EUR", "USD", "JPY", "GBP", "AUD", "CAD", "CHF",
+ "CNY", "HKD", "NZD", "SEK", "KRW", "SGD", "NOK", "MXN",
+
+ // Additional EU currencies
+ "BGN", // Bulgarian Lev
+ "CZK", // Czech Koruna
+ "DKK", // Danish Krone
+ "HUF", // Hungarian Forint
+ "PLN", // Polish Zloty
+ "RON", // Romanian Leu
+ // "HRK", // Croatian Kuna (legacy, replaced by EUR in 2023)
+ };
+ int currency_count = sizeof(currencies) / sizeof(char*);
+
+ if (strlen(currency) > 0)
+ {
+ for (int i = 0; i < currency_count; i++)
+ {
+ if (strcmp(currencies[i], currency) == 0)
+ {
+ currentCurrency = i;
+ break;
+ }
+ }
+ }
+
+ ImGui::SetNextItemWidth(100.0f);
+ if (ImGui::BeginCombo("##currency", currencies[currentCurrency]))
+ {
+ for (int n = 0; n < IM_ARRAYSIZE(currencies); n++)
+ {
+ bool isSelected = (currentCurrency == n);
+ if (ImGui::Selectable(currencies[n], isSelected))
+ {
+ result = true;
+ strops_copy(currency, currencies[n], MAX_LEN_CURRENCY);
+ }
+
+ if (isSelected)
+ ImGui::SetItemDefaultFocus();
+ }
+ ImGui::EndCombo();
+ }
+
+ return result;
+ }
+
+ void FormToggleCombo(bool *buffer, char* option1, char* option2)
+ {
+ const char* items[] = { option1, option2 };
+ if (ImGui::BeginCombo("Mode", items[*buffer])) {
+ for (int n = 0; n < 2; n++) {
+ bool is_selected = (n == (int)*buffer);
+ if (ImGui::Selectable(items[n], is_selected)) {
+ *buffer = n;
+ }
+ if (is_selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ }
+ ImGui::EndCombo();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/ui/ui_contacts.cpp b/src/ui/ui_contacts.cpp
index 92dd21a..b471f58 100644
--- a/src/ui/ui_contacts.cpp
+++ b/src/ui/ui_contacts.cpp
@@ -18,6 +18,7 @@
#include <stdlib.h>
#include "strops.hpp"
+#include "config.hpp"
#include "ui.hpp"
#include "imgui.h"
#include "administration.hpp"
@@ -29,76 +30,15 @@ static contact selected_for_removal;
static contact active_contact;
-void ui_draw_address_form(address* buffer)
+void ui_draw_address_form(address* buffer, a_err last_err)
{
- const char* selected_country = NULL;
- float widthAvailable = ImGui::GetContentRegionAvail().x;
-
- char id[MAX_LEN_ADDRESS];
-
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- snprintf(id, sizeof(id), "%s##%p", localize("contact.form.address1"), buffer);
- ImGui::InputTextWithHint(id, localize("contact.form.address1"), buffer->address1, IM_ARRAYSIZE(buffer->address1));
- ImGui::SameLine();ui_helper_draw_required_tag();
-
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- snprintf(id, sizeof(id), "%s##%p", localize("contact.form.address2"), buffer);
- ImGui::InputTextWithHint(id, localize("contact.form.address2"), buffer->address2, IM_ARRAYSIZE(buffer->address2));
- //ImGui::SameLine();ui_helper_draw_required_tag();
-
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- snprintf(id, sizeof(id), "%s##%p", localize("contact.form.city"), buffer);
- ImGui::InputTextWithHint(id, localize("contact.form.city"), buffer->city, IM_ARRAYSIZE(buffer->city));
- ImGui::SameLine();ui_helper_draw_required_tag();
-
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- snprintf(id, sizeof(id), "%s##%p", localize("contact.form.postal"), buffer);
- ImGui::InputTextWithHint(id, localize("contact.form.postal"), buffer->postal, IM_ARRAYSIZE(buffer->postal));
- ImGui::SameLine();ui_helper_draw_required_tag();
-
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- snprintf(id, sizeof(id), "%s##%p", localize("contact.form.region"), buffer);
- ImGui::InputTextWithHint(id, localize("contact.form.region"), buffer->region, IM_ARRAYSIZE(buffer->region));
- ImGui::SameLine();ui_helper_draw_required_tag();
-
- // 5. Country dropdown.
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- const char* countries[] = { localize("country.AT"),localize("country.BE"),localize("country.BG"),localize("country.HR"),localize("country.CY"),localize("country.CZ"),localize("country.DK"),localize("country.EE"),localize("country.FI"),localize("country.FR"),localize("country.DE"),localize("country.GR"),localize("country.HU"),localize("country.IE"),localize("country.IT"),localize("country.LV"),localize("country.LT"),localize("country.LU"),localize("country.MT"),localize("country.NL"),localize("country.PL"),localize("country.PT"),localize("country.RO"),localize("country.SK"),localize("country.SI"),localize("country.ES"),localize("country.SE") };
- const char* country_codes[] = {
- "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
- "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
- "PL", "PT", "RO", "SK", "SI", "ES", "SE"
- };
- s32 country_count = sizeof(countries) / sizeof(countries[0]);
- if (selected_country == 0) {
- for (int i = 0; i < country_count; i++)
- {
- if (strcmp(country_codes[i], buffer->country_code) == 0)
- {
- selected_country = countries[i];
- break;
- }
- }
- }
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.address1"), buffer, buffer->address1, IM_ARRAYSIZE(buffer->address1), last_err & A_ERR_MISSING_ADDRESS1);
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.address2"), buffer, buffer->address2, IM_ARRAYSIZE(buffer->address2), 0);
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.city"), buffer, buffer->city, IM_ARRAYSIZE(buffer->city), last_err & A_ERR_MISSING_CITY);
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.postal"), buffer, buffer->postal, IM_ARRAYSIZE(buffer->postal), last_err & A_ERR_MISSING_POSTAL);
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.region"), buffer, buffer->region, IM_ARRAYSIZE(buffer->region), 0);
- int selected_country_index = -1;
- snprintf(id, sizeof(id), "%s##%p", localize("contact.form.country"), buffer);
- if (ImGui::BeginCombo(id, selected_country))
- {
- for (int n = 0; n < IM_ARRAYSIZE(countries); n++)
- {
- bool is_selected = (selected_country == countries[n]);
- if (ImGui::Selectable(countries[n], is_selected)) {
- selected_country = countries[n];
- selected_country_index = n;
- }
- }
- ImGui::EndCombo();
- }
- if (selected_country_index != -1) {
- strops_copy(buffer->country_code, country_codes[selected_country_index], IM_ARRAYSIZE(buffer->country_code));
- }
- ImGui::SameLine();ui_helper_draw_required_tag();
+ ImGui::FormCountryCombo(buffer, buffer->country_code, IM_ARRAYSIZE(buffer->country_code));
}
void ui_setup_contacts()
@@ -108,89 +48,34 @@ void ui_setup_contacts()
memset(&selected_for_removal, 0, sizeof(contact));
}
-void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false, bool* on_autocomplete = 0)
+void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false)
{
+ a_err last_err = administration_contact_is_valid(*buffer);
+
ImGui::PushID(buffer);
ImGui::Spacing();
- float widthAvailable = ImGui::GetContentRegionAvail().x;
-
ImGui::BeginDisabled();
- // 1. Identifier
- //ImGui::SetNextItemWidth(widthAvailable*0.2f);
- //ImGui::InputText(localize("contact.form.identifier"), buffer->id, IM_ARRAYSIZE(buffer->id));
-
if (!viewing_only) ImGui::EndDisabled();
- // 2. Full name
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- if (with_autocomplete) {
- contact autocomplete_list[5];
- int autocomplete_count = administration_contact_get_autocompletions(autocomplete_list, 5, buffer->name);
- char* autocomplete_strings[5];
-
- for (int i = 0; i < autocomplete_count; i++)
- {
- autocomplete_strings[i] = (char*)malloc(200);
- snprintf(autocomplete_strings[i], 200, "%s (%s %s)", autocomplete_list[i].name, autocomplete_list[i].address.address1, autocomplete_list[i].address.address2);
- }
-
- int autocomplete_index = TextInputWithAutocomplete(localize("contact.form.fullname"), localize("contact.form.fullname"),
- buffer->name, IM_ARRAYSIZE(buffer->name), (char**)autocomplete_strings, autocomplete_count);
-
- if (on_autocomplete) {
- *on_autocomplete = autocomplete_index != -1;
- }
+ if (with_autocomplete) ImGui::FormContactAutocomplete(buffer, last_err & A_ERR_MISSING_NAME);
+ else ImGui::FormInputTextWithErrorHint(localize("contact.form.fullname"), buffer, buffer->name, IM_ARRAYSIZE(buffer->name), last_err & A_ERR_MISSING_NAME);
- if (autocomplete_index != -1)
- {
- memcpy(buffer, &autocomplete_list[autocomplete_index], sizeof(contact));
- }
+ ui_draw_address_form(&buffer->address, last_err);
- for (int i = 0; i < autocomplete_count; i++)
- {
- free(autocomplete_strings[i]);
- }
- }
- else ImGui::InputTextWithHint(localize("contact.form.fullname"), localize("contact.form.fullname"), buffer->name, IM_ARRAYSIZE(buffer->name));
- ImGui::SameLine();ui_helper_draw_required_tag();
-
- // 3. Address line 1, 4. address line 2, 5. country
- ui_draw_address_form(&buffer->address);
-
- // 6. Contact type dropdown.
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- const char* customer_types[2] = { localize("contact.form.type.business"), localize("contact.form.type.consumer") };
- int currentItem = static_cast<int>(buffer->type);
- if (ImGui::Combo(localize("contact.form.type"), &currentItem, customer_types, IM_ARRAYSIZE(customer_types)))
- {
- buffer->type = static_cast<contact_type>(currentItem);
- }
+ ImGui::FormContactTypeCombo(&buffer->type);
// Fields only required for businesses.
if (buffer->type == contact_type::CONTACT_BUSINESS)
{
- // 7. Tax number
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- ImGui::InputTextWithHint(localize("contact.form.taxnumber"), localize("contact.form.taxnumber"), buffer->taxid, IM_ARRAYSIZE(buffer->taxid));
-
- // 8. Business number / Chamber of commerce
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- ImGui::InputTextWithHint(localize("contact.form.businessnumber"), localize("contact.form.businessnumber"), buffer->businessid, IM_ARRAYSIZE(buffer->businessid));
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.taxnumber"), buffer, buffer->taxid, IM_ARRAYSIZE(buffer->taxid), last_err & A_ERR_MISSING_TAXID);
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.businessnumber"), buffer, buffer->businessid, IM_ARRAYSIZE(buffer->businessid), last_err & A_ERR_MISSING_BUSINESSID);
}
- // 9. Email
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- ImGui::InputTextWithHint(localize("contact.form.email"), localize("contact.form.email"), buffer->email, IM_ARRAYSIZE(buffer->email));
-
- // 10. Phone number
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- ImGui::InputTextWithHint(localize("contact.form.phonenumber"), localize("contact.form.phonenumber"), buffer->phone_number, IM_ARRAYSIZE(buffer->phone_number));
-
- // 11. Bank account.
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- ImGui::InputTextWithHint(localize("contact.form.bankaccount"), localize("contact.form.bankaccount"), buffer->bank_account, IM_ARRAYSIZE(buffer->bank_account));
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.email"), buffer, buffer->email, IM_ARRAYSIZE(buffer->email), last_err & A_ERR_MISSING_EMAIL);
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.phonenumber"), buffer, buffer->phone_number, IM_ARRAYSIZE(buffer->phone_number), 0);
+ ImGui::FormInputTextWithErrorHint(localize("contact.form.bankaccount"), buffer, buffer->bank_account, IM_ARRAYSIZE(buffer->bank_account), 0);
if (viewing_only) ImGui::EndDisabled();
ImGui::PopID();
@@ -198,7 +83,7 @@ void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_
void draw_contact_form(contact* buffer, bool viewing_only = false)
{
- draw_contact_form_ex(buffer, viewing_only, false, 0);
+ draw_contact_form_ex(buffer, viewing_only, false);
}
@@ -313,7 +198,8 @@ static void ui_draw_contacts_create()
draw_contact_form(&active_contact);
- bool can_save = administration_contact_is_valid(active_contact) == A_ERR_SUCCESS;
+ a_err contact_validation_err = administration_contact_is_valid(active_contact);
+ bool can_save = contact_validation_err == A_ERR_SUCCESS;
if (!can_save) ImGui::BeginDisabled();
// Save button
ImGui::Spacing();
@@ -332,7 +218,8 @@ static void ui_draw_contacts_update()
draw_contact_form(&active_contact);
- bool can_save = administration_contact_is_valid(active_contact) == A_ERR_SUCCESS;
+ a_err contact_validation_err = administration_contact_is_valid(active_contact);
+ bool can_save = contact_validation_err == A_ERR_SUCCESS;
if (!can_save) ImGui::BeginDisabled();
// Save button
ImGui::Spacing();
diff --git a/src/ui/ui_expenses.cpp b/src/ui/ui_expenses.cpp
index b195442..7d94eaf 100644
--- a/src/ui/ui_expenses.cpp
+++ b/src/ui/ui_expenses.cpp
@@ -32,94 +32,13 @@ static view_state current_view_state = view_state::LIST;
static invoice active_invoice = {0};
static invoice selected_for_removal = {0};
-static cost_center* cost_center_list_buffer = 0;
-static tax_rate* tax_rate_list_buffer = 0;
-static project* project_list_buffer = 0;
static billing_item* invoice_items_buffer = 0;
-void ui_draw_address_form(address* buffer);
-void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false, bool* on_autocomplete = 0);
-void draw_tax_rate_selector(char* tax_rate_id, tax_rate* buffer, char* orig_country, char* dest_country);
-bool draw_currency_selector(char* currency);
-void draw_invoice_items_form(invoice* invoice, bool is_outgoing);
-
-void draw_costcenter_selector(char* costcenter_id, cost_center* buffer)
-{
- cost_center* selected_costcenter = NULL;
- u32 costcenter_count = administration_cost_center_get_all(buffer);
-
- // Select cost center by given id.
- if (strlen(costcenter_id) > 0)
- {
- for (u32 i = 0; i < costcenter_count; i++)
- {
- if (strcmp(buffer[i].id, costcenter_id) == 0)
- {
- selected_costcenter = &buffer[i];
- break;
- }
- }
- }
-
- int selected_costcenter_index = -1;
- if (ImGui::BeginCombo(localize("invoice.form.costcenter"), selected_costcenter == NULL ? NULL : localize(selected_costcenter->description)))
- {
- for (u32 n = 0; n < costcenter_count; n++)
- {
- bool is_selected = selected_costcenter && strcmp(selected_costcenter->id, buffer[n].id) == 0;
- if (ImGui::Selectable(localize(buffer[n].description), is_selected)) {
- selected_costcenter_index = n;
- }
- }
- ImGui::EndCombo();
- }
-
- if (selected_costcenter_index != -1) {
- strops_copy(costcenter_id, buffer[selected_costcenter_index].id, MAX_LEN_ID);
- }
-}
-
-void draw_project_selector(char* project_id, project* buffer)
-{
- project* selected_project = NULL;
- u32 project_count = administration_project_get_all(buffer);
-
- // Select project by given id.
- if (strlen(project_id) > 0)
- {
- for (u32 i = 0; i < project_count; i++)
- {
- if (strcmp(buffer[i].id, project_id) == 0)
- {
- selected_project = &buffer[i];
- break;
- }
- }
- }
-
- int selected_project_index = -1;
- if (ImGui::BeginCombo(localize("invoice.form.project"), selected_project == NULL ? NULL : selected_project->description))
- {
- for (u32 n = 0; n < project_count; n++)
- {
- bool is_selected = selected_project && strcmp(selected_project->id, buffer[n].id) == 0;
- if (ImGui::Selectable(buffer[n].description, is_selected)) {
- selected_project_index = n;
- }
- }
- ImGui::EndCombo();
- }
-
- if (selected_project_index != -1) {
- strops_copy(project_id, buffer[selected_project_index].id, MAX_LEN_ID);
- }
-}
+void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false);
+void draw_invoice_items_form(invoice* invoice);
void ui_destroy_expenses()
{
- free(cost_center_list_buffer);
- free(tax_rate_list_buffer);
- free(project_list_buffer);
free(invoice_items_buffer);
}
@@ -128,15 +47,6 @@ void ui_setup_expenses()
current_view_state = view_state::LIST;
active_invoice = administration_invoice_create_empty();
- u32 costcenter_count = administration_cost_center_count();
- cost_center_list_buffer = (cost_center*) malloc(sizeof(cost_center) * costcenter_count);
-
- u32 tax_rate_count = administration_tax_rate_count();
- tax_rate_list_buffer = (tax_rate*) malloc(sizeof(tax_rate) * tax_rate_count);
-
- u32 project_count = administration_project_count();
- project_list_buffer = (project*) malloc(sizeof(project) * project_count);
-
u32 invoice_items_count = MAX_BILLING_ITEMS;
invoice_items_buffer = (billing_item*)malloc(sizeof(billing_item) * invoice_items_count);
}
@@ -145,17 +55,9 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
{
if (viewing_only) ImGui::BeginDisabled();
- // 1. Identifier
- //ImGui::SetNextItemWidth(widthAvailable*0.2f);
- //ImGui::InputText(localize("contact.form.identifier"), buffer->id, IM_ARRAYSIZE(buffer->id));
-
- // 2. Sequential number
ImGui::Text("%s: %s", localize("invoice.form.invoicenumber"), buffer->sequential_number);
-
- // 3. Billed to (you)
ImGui::Text("%s: %s", localize("invoice.form.billedTo"), buffer->customer.name);
- // 4. Invoice issued at
tm issued_at_date = *gmtime(&buffer->issued_at);
if (ImGui::DatePicker("##issuedAt", issued_at_date))
{
@@ -164,7 +66,6 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
ImGui::SameLine();
ImGui::Text(localize("invoice.form.issuedat"));
- // 5. Invoice expires at
tm expires_at_date = *gmtime(&buffer->expires_at);
if (ImGui::DatePicker("##expiresAt", expires_at_date))
{
@@ -173,7 +74,6 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
ImGui::SameLine();
ImGui::Text(localize("invoice.form.expiresat"));
- // 6. Product/service delivered at
tm delivered_at_date = *gmtime(&buffer->delivered_at);
if (ImGui::DatePicker("##deliveredAt", delivered_at_date))
{
@@ -184,31 +84,26 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
ImGui::Separator();
- // 7. Supplier information
ImGui::Text(localize("invoice.form.supplier"));
- draw_contact_form_ex(&buffer->supplier, false, true, 0);
+ draw_contact_form_ex(&buffer->supplier, false, true);
- // 8. (optional) shipping address.
ImGui::Checkbox(localize("invoice.form.triangulation"), &buffer->is_triangulation);
if (buffer->is_triangulation) {
ImGui::Spacing();
ImGui::Text(localize("invoice.form.shippinginformation"));
- draw_contact_form_ex(&buffer->addressee, 0,0,0);
+ draw_contact_form_ex(&buffer->addressee, 0,0);
}
ImGui::Separator();
- // 9. Project selection
- draw_project_selector(buffer->project_id, project_list_buffer);
+ ImGui::FormProjectCombo(buffer->project_id);
+ ImGui::FormCostCenterCombo(buffer->cost_center_id);
- // 10. Cost center selection
- draw_costcenter_selector(buffer->cost_center_id, cost_center_list_buffer);
ImGui::Separator();
ImGui::Spacing();
ImGui::Spacing();
ImGui::Spacing();
- // 11. New billing item button.
bool max_items_reached = administration_billing_item_count(buffer) >= MAX_BILLING_ITEMS;
if (max_items_reached) ImGui::BeginDisabled();
if (ImGui::Button(localize(localize("invoice.form.add"))))
@@ -218,17 +113,15 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
}
if (max_items_reached) ImGui::EndDisabled();
- // 12. Dropdown for invoice currency.
ImGui::SameLine();
ImGui::Text("| %s: ", localize("invoice.form.currency"));
ImGui::SameLine();
- if (draw_currency_selector(buffer->currency))
+ if (ImGui::FormCurrencyCombo(buffer->currency))
{
administration_invoice_set_currency(buffer, buffer->currency);
}
- // 13. Invoice items form
- draw_invoice_items_form(buffer, false);
+ draw_invoice_items_form(buffer);
if (viewing_only) ImGui::EndDisabled();
}
diff --git a/src/ui/ui_invoices.cpp b/src/ui/ui_invoices.cpp
index c3c6b8c..f061e3f 100644
--- a/src/ui/ui_invoices.cpp
+++ b/src/ui/ui_invoices.cpp
@@ -33,19 +33,13 @@ static view_state current_view_state = view_state::LIST;
static invoice active_invoice = {0};
static invoice selected_for_removal = {0};
-static tax_rate* tax_rate_list_buffer = 0;
static billing_item* invoice_items_buffer = 0;
-static project* project_list_buffer = 0;
-void ui_draw_address_form(address* buffer);
-void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false, bool* on_autocomplete = 0);
-void draw_project_selector(char* project_id, project* buffer);
+void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false);
void ui_destroy_invoices()
{
- free(tax_rate_list_buffer);
free(invoice_items_buffer);
- free(project_list_buffer);
}
void ui_setup_invoices()
@@ -53,129 +47,11 @@ void ui_setup_invoices()
current_view_state = view_state::LIST;
active_invoice = administration_invoice_create_empty();
- u32 tax_rate_count = administration_tax_rate_count();
- tax_rate_list_buffer = (tax_rate*) malloc(sizeof(tax_rate) * tax_rate_count);
-
u32 invoice_items_count = MAX_BILLING_ITEMS;
invoice_items_buffer = (billing_item*)malloc(sizeof(billing_item) * invoice_items_count);
-
- u32 project_count = administration_project_count();
- project_list_buffer = (project*) malloc(sizeof(project) * project_count);
-}
-
-void draw_tax_rate_selector(char* tax_rate_id, tax_rate* buffer, char* orig_country, char* dest_country)
-{
- tax_rate* selected_tax_rate = NULL;
- char* country_codes[2] = {orig_country, dest_country};
- u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, country_codes);
-
- // Select tax rate by given id.
- if (strlen(tax_rate_id) > 0)
- {
- for (u32 i = 0; i < tax_rate_count; i++)
- {
- if (strcmp(buffer[i].id, tax_rate_id) == 0)
- {
- selected_tax_rate = &buffer[i];
- break;
- }
- }
- }
-
- int selected_tax_rate_index = -1;
- char rate_str_buf[40];
- rate_str_buf[0] = 0;
- if (selected_tax_rate)
- {
- if (strcmp(selected_tax_rate->country_code, "00") == 0) {
- char category_code_desc[MAX_LEN_LONG_DESC];
- snprintf(category_code_desc, MAX_LEN_LONG_DESC, "taxcategory.%s", selected_tax_rate->category_code);
- snprintf(rate_str_buf, 40, "%s", localize(category_code_desc));
- }
- else snprintf(rate_str_buf, 40, "%s/%.1f%%", selected_tax_rate->country_code, selected_tax_rate->rate);
- }
-
- if (ImGui::BeginCombo("##Tax Bracket", rate_str_buf))
- {
- for (u32 n = 0; n < tax_rate_count; n++)
- {
- bool is_selected = selected_tax_rate && strcmp(selected_tax_rate->id, buffer[n].id) == 0;
-
- if (strcmp(buffer[n].country_code, "00") == 0) {
- char category_code_desc[MAX_LEN_LONG_DESC];
- snprintf(category_code_desc, MAX_LEN_LONG_DESC, "taxcategory.%s", buffer[n].category_code);
- snprintf(rate_str_buf, 40, "%s", localize(category_code_desc));
- }
- else snprintf(rate_str_buf, 40, "%s/%.1f%%", buffer[n].country_code, buffer[n].rate);
-
- if (ImGui::Selectable(rate_str_buf, is_selected)) {
- selected_tax_rate_index = n;
- }
- }
- ImGui::EndCombo();
- }
-
- if (selected_tax_rate_index != -1) {
- strops_copy(tax_rate_id, buffer[selected_tax_rate_index].id, MAX_LEN_ID);
- }
}
-bool draw_currency_selector(char* currency)
-{
- int currentCurrency = 0;
- bool result = false;
-
- // Top 15 most traded currencies + all EU official currencies
- const char* currencies[] = {
- // Top 15
- "EUR", "USD", "JPY", "GBP", "AUD", "CAD", "CHF",
- "CNY", "HKD", "NZD", "SEK", "KRW", "SGD", "NOK", "MXN",
-
- // Additional EU currencies
- "BGN", // Bulgarian Lev
- "CZK", // Czech Koruna
- "DKK", // Danish Krone
- "HUF", // Hungarian Forint
- "PLN", // Polish Zloty
- "RON", // Romanian Leu
- // "HRK", // Croatian Kuna (legacy, replaced by EUR in 2023)
- };
- int currency_count = sizeof(currencies) / sizeof(char*);
-
- if (strlen(currency) > 0)
- {
- for (int i = 0; i < currency_count; i++)
- {
- if (strcmp(currencies[i], currency) == 0)
- {
- currentCurrency = i;
- break;
- }
- }
- }
-
- ImGui::SetNextItemWidth(100.0f);
- if (ImGui::BeginCombo("##currency", currencies[currentCurrency]))
- {
- for (int n = 0; n < IM_ARRAYSIZE(currencies); n++)
- {
- bool isSelected = (currentCurrency == n);
- if (ImGui::Selectable(currencies[n], isSelected))
- {
- result = true;
- strops_copy(currency, currencies[n], MAX_LEN_CURRENCY);
- }
-
- if (isSelected)
- ImGui::SetItemDefaultFocus();
- }
- ImGui::EndCombo();
- }
-
- return result;
-}
-
-void draw_invoice_items_form(invoice* invoice, bool is_outgoing)
+void draw_invoice_items_form(invoice* invoice)
{
billing_item* buffer = invoice_items_buffer;
u32 invoice_items = administration_billing_item_get_all_for_invoice(invoice, buffer);
@@ -212,22 +88,7 @@ void draw_invoice_items_form(invoice* invoice, bool is_outgoing)
ImGui::InputFloat("##amount", &item.amount, 0.0f, 0.0f, "%.0f");
ImGui::SameLine();
- // Toggle between X and %
- {
- const char* items[] = { "X", "%" };
- if (ImGui::BeginCombo("Mode", items[item.amount_is_percentage])) {
- for (int n = 0; n < 2; n++) {
- bool is_selected = (n == (int)item.amount_is_percentage);
- if (ImGui::Selectable(items[n], is_selected)) {
- item.amount_is_percentage = n;
- }
- if (is_selected) {
- ImGui::SetItemDefaultFocus();
- }
- }
- ImGui::EndCombo();
- }
- }
+ ImGui::FormToggleCombo(&item.amount_is_percentage, "X", "%");
ImGui::TableSetColumnIndex(2);
ImGui::PushItemWidth(-1);
@@ -243,31 +104,15 @@ void draw_invoice_items_form(invoice* invoice, bool is_outgoing)
ImGui::InputFloat("##discount", &item.discount, 0.0f, 0.0f, "%.2f");
ImGui::SameLine();
- // Toggle between currency and %
- {
- const char* items[] = { item.currency, "%" };
- if (ImGui::BeginCombo("Mode##discountMode", items[item.discount_is_percentage])) {
- for (int n = 0; n < 2; n++) {
- bool is_selected = (n == (int)item.discount_is_percentage);
- if (ImGui::Selectable(items[n], is_selected)) {
- item.discount_is_percentage = n;
- }
- if (is_selected) {
- ImGui::SetItemDefaultFocus();
- }
- }
- ImGui::EndCombo();
- }
- }
+ ImGui::FormToggleCombo(&item.amount_is_percentage, item.currency, "%");
ImGui::TableSetColumnIndex(5);
ImGui::Text("%.2f %s", item.net, item.currency);
ImGui::TableSetColumnIndex(6);
ImGui::PushItemWidth(-1);
- draw_tax_rate_selector(item.tax_rate_id, tax_rate_list_buffer,
- administration_company_info_get().address.country_code,
- is_outgoing ? invoice->customer.address.country_code : invoice->supplier.address.country_code);
+ ImGui::FormTaxRateCombo(item.tax_rate_id, invoice->customer.address.country_code, invoice->supplier.address.country_code);
+
ImGui::PopItemWidth();
ImGui::TableSetColumnIndex(7);
@@ -329,17 +174,9 @@ static void draw_invoice_form(invoice* buffer, bool viewing_only = false)
{
ImGui::BeginDisabled();
- // 1. Identifier
- //ImGui::SetNextItemWidth(widthAvailable*0.2f);
- //ImGui::InputText(localize("contact.form.identifier"), buffer->id, IM_ARRAYSIZE(buffer->id));
-
- // 2. Sequential number
ImGui::Text("%s: %s", localize("invoice.form.invoicenumber"), buffer->sequential_number);
-
- // 3. Supplier (you)
ImGui::Text("%s: %s", localize("invoice.form.supplier"), buffer->supplier.name);
- // 4. Invoice issued at
tm issued_at_date = *gmtime(&buffer->issued_at);
if (ImGui::DatePicker("##issuedAt", issued_at_date))
{
@@ -348,7 +185,6 @@ static void draw_invoice_form(invoice* buffer, bool viewing_only = false)
ImGui::SameLine();
ImGui::Text(localize("invoice.form.issuedat"));
- // 5. Invoice expires at
tm expires_at_date = *gmtime(&buffer->expires_at);
if (ImGui::DatePicker("##expiresAt", expires_at_date))
{
@@ -358,7 +194,6 @@ static void draw_invoice_form(invoice* buffer, bool viewing_only = false)
ImGui::Text(localize("invoice.form.expiresat"));
if (!viewing_only) ImGui::EndDisabled();
- // 6. Product/service delivered at
tm delivered_at_date = *gmtime(&buffer->delivered_at);
if (ImGui::DatePicker("##deliveredAt", delivered_at_date))
{
@@ -369,27 +204,23 @@ static void draw_invoice_form(invoice* buffer, bool viewing_only = false)
ImGui::Separator();
- // 7. Customer information
ImGui::Text(localize("invoice.form.billinginformation"));
- draw_contact_form_ex(&buffer->customer, false, true, 0);
+ draw_contact_form_ex(&buffer->customer, false, true);
- // 8. (optional) shipping address.
ImGui::Checkbox(localize("invoice.form.triangulation"), &buffer->is_triangulation);
if (buffer->is_triangulation) {
ImGui::Spacing();
ImGui::Text(localize("invoice.form.shippinginformation"));
- draw_contact_form_ex(&buffer->addressee, 0,0,0);
+ draw_contact_form_ex(&buffer->addressee, 0,0);
}
ImGui::Separator();
- // 9. Project selection
- draw_project_selector(buffer->project_id, project_list_buffer);
+ ImGui::FormProjectCombo(buffer->project_id);
ImGui::Spacing();
ImGui::Spacing();
ImGui::Spacing();
- // 11. New billing item button.
bool max_items_reached = administration_billing_item_count(buffer) >= MAX_BILLING_ITEMS;
if (max_items_reached) ImGui::BeginDisabled();
if (ImGui::Button(localize(localize("invoice.form.add"))))
@@ -399,17 +230,15 @@ static void draw_invoice_form(invoice* buffer, bool viewing_only = false)
}
if (max_items_reached) ImGui::EndDisabled();
- // 12. Dropdown for invoice currency.
ImGui::SameLine();
ImGui::Text("| %s: ", localize("invoice.form.currency"));
ImGui::SameLine();
- if (draw_currency_selector(buffer->currency))
+ if (ImGui::FormCurrencyCombo(buffer->currency))
{
administration_invoice_set_currency(buffer, buffer->currency);
}
- // 13. Invoice items form
- draw_invoice_items_form(buffer, true);
+ draw_invoice_items_form(buffer);
if (viewing_only) ImGui::EndDisabled();
}
diff --git a/src/ui/ui_projects.cpp b/src/ui/ui_projects.cpp
index dddf881..37cf5d8 100644
--- a/src/ui/ui_projects.cpp
+++ b/src/ui/ui_projects.cpp
@@ -35,8 +35,10 @@ void ui_setup_projects()
static void draw_project_form()
{
+ float widthAvailable = ImGui::GetContentRegionAvail().x;
+ bool viewing_only = (current_view_state == view_state::VIEW);
static const char* selected_country = NULL;
-
+
if (ImGui::Button(localize("form.back"))) {
current_view_state = view_state::LIST;
active_project = administration_project_create_empty();
@@ -44,20 +46,15 @@ static void draw_project_form()
return;
}
ImGui::Spacing();
-
- bool viewing_only = (current_view_state == view_state::VIEW);
-
ImGui::BeginDisabled();
-
- float widthAvailable = ImGui::GetContentRegionAvail().x;
+
+ a_err last_err = administration_project_is_valid(active_project);
ImGui::SetNextItemWidth(widthAvailable*0.2f);
ImGui::InputText(localize("project.form.identifier"), active_project.id, IM_ARRAYSIZE(active_project.id));
if (!viewing_only) ImGui::EndDisabled();
-
- ImGui::SetNextItemWidth(widthAvailable*0.5f);
- ImGui::InputTextWithHint(localize("project.form.description"), localize("project.form.description"), active_project.description, IM_ARRAYSIZE(active_project.description));
- ImGui::SameLine();ui_helper_draw_required_tag();
+
+ ImGui::FormInputTextWithErrorHint(localize("project.form.description"), &active_project, active_project.description, IM_ARRAYSIZE(active_project.description), last_err & A_ERR_MISSING_DESCRIPTION);
if (viewing_only) ImGui::EndDisabled();
diff --git a/tests/peppol_write_tests.cpp b/tests/peppol_write_tests.cpp
index 8e8c102..e77f128 100644
--- a/tests/peppol_write_tests.cpp
+++ b/tests/peppol_write_tests.cpp
@@ -1,20 +1,38 @@
+#include <malloc.h>
+
#include "zip.h"
#include "xml.h"
+char* validation_file = "libs\\PEPPOL-EN16931-UBL.sch";
+char* result_file = "build\\invoice_report.xml";
+char* extract_dir = "build\\extracted";
+
static contact _create_nl_business()
{
contact pw = administration_contact_create_empty();
strops_copy(pw.name, "John Johnsson", sizeof(pw.name));
strops_copy(pw.address.address1, "Address 1", sizeof(pw.address.address1));
- strops_copy(pw.address.country_code, "FR", sizeof(pw.address.country_code));
- strops_copy(pw.address.city, "Paris", sizeof(pw.address.city));
- strops_copy(pw.address.postal, "12345", sizeof(pw.address.postal));
+ strops_copy(pw.address.country_code, "NL", sizeof(pw.address.country_code));
+ strops_copy(pw.address.city, "Amsterdam", sizeof(pw.address.city));
+ strops_copy(pw.address.postal, "1234AA", sizeof(pw.address.postal));
strops_copy(pw.taxid, "T123", sizeof(pw.taxid));
strops_copy(pw.businessid, "B321", sizeof(pw.businessid));
pw.type = contact_type::CONTACT_BUSINESS;
return pw;
}
+static contact _create_nl_consumer()
+{
+ contact pw = administration_contact_create_empty();
+ strops_copy(pw.name, "Hans Klok", sizeof(pw.name));
+ strops_copy(pw.address.address1, "Address 2", sizeof(pw.address.address1));
+ strops_copy(pw.address.country_code, "NL", sizeof(pw.address.country_code));
+ strops_copy(pw.address.city, "Amsterdam", sizeof(pw.address.city));
+ strops_copy(pw.address.postal, "4321AA", sizeof(pw.address.postal));
+ pw.type = contact_type::CONTACT_CONSUMER;
+ return pw;
+}
+
static billing_item _create_bi1(invoice *inv)
{
billing_item item = administration_billing_item_create_empty();
@@ -38,10 +56,14 @@ static bool _test_peppol_file(char* id)
{
bool result = 1;
- zip_extract(test_file_path, "build\\extracted", 0, 0);
- system("java -jar libs\\schxslt-cli.jar -d build\\extracted\\I\\2.xml -s libs\\PEPPOL-EN16931-UBL.sch -o build\\invoice_report.xml > NUL 2>&1");
+ zip_extract(test_file_path, extract_dir, 0, 0);
- FILE* file = fopen("build\\invoice_report.xml", "r");
+ char command[200];
+ snprintf(command, 200, "java -jar libs\\schxslt-cli.jar -d %s\\%s.xml -s %s -o %s > NUL 2>&1",
+ extract_dir, id, validation_file, result_file);
+ system(command);
+
+ FILE* file = fopen(result_file, "r");
struct xml_document* document = xml_open_document(file);
struct xml_node* root = xml_document_root(document);
@@ -56,7 +78,28 @@ static bool _test_peppol_file(char* id)
if (strcmp(name, "svrl:failed-assert") == 0) {
struct xml_node* text_node = xml_easy_child(node, (uint8_t *)"svrl:text", 0);
char* content = (char*)xml_easy_content(text_node);
- printf("FAILURE: %s\n", content);
+
+ size_t num_attributes = xml_node_attributes(node);
+ for (int x = 0; x < num_attributes; x++)
+ {
+ struct xml_string* attr_name = xml_node_attribute_name(node, x);
+ size_t b_length = xml_string_length(attr_name);
+ uint8_t* b_buffer = (uint8_t*)alloca((b_length + 1) * sizeof(uint8_t));
+ xml_string_copy(attr_name, b_buffer, b_length);
+ b_buffer[b_length] = 0;
+
+ if (strcmp((char*)b_buffer, "location") == 0) {
+ struct xml_string* attr_content = xml_node_attribute_content(node, x);
+
+ size_t a_length = xml_string_length(attr_content);
+ uint8_t* a_buffer = (uint8_t*)alloca((a_length + 1) * sizeof(uint8_t));
+ xml_string_copy(attr_content, a_buffer, a_length);
+ a_buffer[a_length] = 0;
+
+ printf("FAILURE: %s @ %s\n", content, (char*)a_buffer);
+ }
+ }
+
result = 0;
}
}
@@ -67,10 +110,10 @@ static bool _test_peppol_file(char* id)
//////////////////
// Netherlands
//////////////////
-TEST _peppol_write_outgoing_nl2nl_b2b(void)
+TEST _peppol_write_nl2nl_b2b(void)
{
administration_writer_create();
- administration_create_empty(test_file_path);
+ administration_create_default(test_file_path);
invoice inv = administration_invoice_create_empty();
inv.supplier = _create_nl_business();
@@ -82,11 +125,32 @@ TEST _peppol_write_outgoing_nl2nl_b2b(void)
inv.expires_at = inv.issued_at + 1000;
administration_billing_item_add_to_invoice(&inv, _create_bi1(&inv));
- administration_invoice_add(&inv);
+ ASSERT_EQ(administration_invoice_add(&inv), A_ERR_SUCCESS);
+
+ if (_test_peppol_file(inv.id)) { PASS(); } else { FAIL(); }
+}
+
+TEST _peppol_write_nl2nl_b2c(void)
+{
+ administration_writer_create();
+ administration_create_default(test_file_path);
+
+ invoice inv = administration_invoice_create_empty();
+ inv.supplier = _create_nl_business();
+ inv.customer = _create_nl_consumer();
+ inv.is_outgoing = 1;
+ inv.status = invoice_status::INVOICE_CONCEPT;
+ inv.issued_at = time(NULL);
+ inv.delivered_at = inv.issued_at;
+ inv.expires_at = inv.issued_at + 1000;
+
+ administration_billing_item_add_to_invoice(&inv, _create_bi1(&inv));
+ ASSERT_EQ(administration_invoice_add(&inv), A_ERR_SUCCESS);
if (_test_peppol_file(inv.id)) { PASS(); } else { FAIL(); }
}
SUITE(peppol_write) {
- RUN_TEST(_peppol_write_outgoing_nl2nl_b2b);
+ RUN_TEST(_peppol_write_nl2nl_b2b);
+ RUN_TEST(_peppol_write_nl2nl_b2c);
} \ No newline at end of file