summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAldrik Ramaekers <aldrikboy@gmail.com>2025-09-27 14:23:56 +0200
committerAldrik Ramaekers <aldrikboy@gmail.com>2025-09-27 14:23:56 +0200
commitfa088bb60692ba02d30d39affa9a31d9e2b688e2 (patch)
treea2d51585c0f86407709b834bd7a82a5672d2c17b
parentaa7b5ef6ab4f45d2e8e0caa7942db31fc60b3861 (diff)
ai service settings
-rw-r--r--docs/CHANGES.rst3
-rw-r--r--include/administration.hpp18
-rw-r--r--include/file_templates.hpp5
-rw-r--r--src/administration.cpp13
-rw-r--r--src/administration_reader.cpp6
-rw-r--r--src/administration_writer.cpp5
-rw-r--r--src/locales/en.cpp6
-rw-r--r--src/ui/imgui_extensions.cpp6
-rw-r--r--src/ui/ui_settings.cpp41
-rw-r--r--tests/administration_rw_tests.cpp68
10 files changed, 163 insertions, 8 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst
index 4ac2eed..157b56e 100644
--- a/docs/CHANGES.rst
+++ b/docs/CHANGES.rst
@@ -1,7 +1,6 @@
.. _changes:
TODO:
-- write tests for invoices using multiple currencies
- refactor _add functions to use _import functions
- write tests that check error handling for corrupt files. (e.g. references to tax rates, project and cost center that failed to load)
- it is possible a referenced tax rate is loaded after an invoice is loaded. This means all invoices need to be recalculated after file load. (try to write a test for this).
@@ -16,8 +15,6 @@ TODO:
- View invoice history for contacts
- View invoice history for projects
- Create quarterly tax reports for NL.
-- net negative billing items should not also have discounts.
-- net negative billing items should set tax to 0.
- 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 e926aa6..145bb5d 100644
--- a/include/administration.hpp
+++ b/include/administration.hpp
@@ -35,6 +35,7 @@
#define MAX_LEN_TAXID 32
#define MAX_LEN_BUSINESSID 32
#define MAX_LEN_TAX_SECTION 16
+#define MAX_LEN_API_KEY 64
#define MAX_LEN_INCOME_STATEMENT_REPORT_QUARTERS 400
#define MAX_LEN_QUARTERLY_REPORT_PROJECTS 200
@@ -336,6 +337,18 @@ typedef void (*taxrate_changed_event)(tax_rate* rate);
typedef void (*costcenter_changed_event)(cost_center* cost_center);
typedef void (*project_changed_event)(project* project);
+typedef enum
+{
+ AI_PROVIDER_OPENAI = 0,
+} ai_provider;
+
+typedef struct
+{
+ ai_provider provider;
+ char api_key_public[MAX_LEN_API_KEY];
+ char api_key_private[MAX_LEN_API_KEY];
+} ai_service;
+
typedef struct
{
contact company_info; // Company info used for invoices. User cannot create invoices when this is empty/invalid.
@@ -354,6 +367,9 @@ typedef struct
u32 invoice_count;
u32 expense_count;
list_t invoices;
+
+ // Service providers.
+ ai_service ai_service;
} administration;
// Add/Update result codes.
@@ -409,10 +425,12 @@ s32 administration_get_next_id();
s32 administration_get_next_sequence_number();
char* administration_get_currency_symbol_for_currency(char* code);
char* administration_get_default_currency();
+ai_service administration_get_ai_service();
void administration_set_file_path(char* path);
void administration_set_next_id(s32 nr);
void administration_set_next_sequence_number(s32 nr);
+void administration_set_ai_service(ai_service provider);
void administration_create_income_statement(income_statement* statement);
bool administration_can_create_invoices();
diff --git a/include/file_templates.hpp b/include/file_templates.hpp
index 8571368..226ae31 100644
--- a/include/file_templates.hpp
+++ b/include/file_templates.hpp
@@ -63,6 +63,11 @@ const char* administration_save_template =
" <NextId>{{NEXT_ID}}</NextId>\n"
" <NextSequenceNumber>{{NEXT_SEQUENCE_NUMBER}}</NextSequenceNumber>\n"
" <ProgramVersion>{{PROGRAM_VERSION}}</ProgramVersion>\n"
+" <AIService>\n"
+" <Provider>{{AI_SERVICE_PROVIDER}}</Provider>\n"
+" <PublicKey>{{AI_SERVICE_PUBLIC_KEY}}</PublicKey>\n"
+" <PrivateKey>{{AI_SERVICE_PRIVATE_KEY}}</PrivateKey>\n"
+" </AIService>\n"
"</Administration>";
const char* peppol_invoice_tax_subtotal_template =
diff --git a/src/administration.cpp b/src/administration.cpp
index c451f9b..e1405b8 100644
--- a/src/administration.cpp
+++ b/src/administration.cpp
@@ -305,6 +305,8 @@ void administration_create()
list_init(&g_administration.cost_centers);
strops_copy(g_administration.path, "", sizeof(g_administration.path));
+ memset(&g_administration.ai_service, 0, sizeof(ai_service));
+
log_info("Setup took %.3fms.", STOPWATCH_TIME);
}
@@ -361,6 +363,17 @@ void administration_create_default(char* save_file)
// Other functions.
// =======================
+ai_service administration_get_ai_service()
+{
+ return g_administration.ai_service;
+}
+
+void administration_set_ai_service(ai_service provider)
+{
+ g_administration.ai_service = provider;
+ if (data_changed_event_callback) data_changed_event_callback();
+}
+
void administration_set_next_id(s32 nr)
{
g_administration.next_id = nr;
diff --git a/src/administration_reader.cpp b/src/administration_reader.cpp
index 9c20a27..b8b4958 100644
--- a/src/administration_reader.cpp
+++ b/src/administration_reader.cpp
@@ -386,6 +386,12 @@ bool administration_reader_import_administration_info(char* buffer, size_t buffe
administration_set_next_id(xml_get_s32(root, "NextId"));
administration_set_next_sequence_number(xml_get_s32(root, "NextSequenceNumber"));
+ ai_service ai_service;
+ ai_service.provider = (ai_provider)xml_get_s32_x(root, "AIService", "Provider", 0);
+ xml_get_str_x(root, ai_service.api_key_public, MAX_LEN_API_KEY, "AIService", "PublicKey", 0);
+ xml_get_str_x(root, ai_service.api_key_private, MAX_LEN_API_KEY, "AIService", "PrivateKey", 0);
+ administration_set_ai_service(ai_service);
+
log_info("Loaded administration info in %.3fms. next_id=%d next_sequence_number=%d",
STOPWATCH_TIME, administration_get_next_id(), administration_get_next_sequence_number());
diff --git a/src/administration_writer.cpp b/src/administration_writer.cpp
index 0cc4cd8..8bcf741 100644
--- a/src/administration_writer.cpp
+++ b/src/administration_writer.cpp
@@ -785,6 +785,11 @@ bool administration_writer_save_all_administration_info_blocking()
strops_replace_int32(file_content, buf_length, "{{NEXT_SEQUENCE_NUMBER}}", administration_get_next_sequence_number());
strops_replace(file_content, buf_length, "{{PROGRAM_VERSION}}", PROGRAM_VERSION);
+ ai_service ai_service = administration_get_ai_service();
+ strops_replace_int32(file_content, buf_length, "{{AI_SERVICE_PROVIDER}}", (s32)ai_service.provider);
+ strops_replace(file_content, buf_length, "{{AI_SERVICE_PUBLIC_KEY}}", ai_service.api_key_public);
+ strops_replace(file_content, buf_length, "{{AI_SERVICE_PRIVATE_KEY}}", ai_service.api_key_private);
+
//// Write to Disk.
int final_length = (int)strlen(file_content);
if (!xml_parse_document((uint8_t*)file_content, final_length)) result = 0;
diff --git a/src/locales/en.cpp b/src/locales/en.cpp
index 36b9876..0152928 100644
--- a/src/locales/en.cpp
+++ b/src/locales/en.cpp
@@ -139,10 +139,16 @@ locale_entry en_locales[] = {
{"settings.table.company", "Company"},
{"settings.table.vatrates", "VAT Rates"},
{"settings.table.costcenters", "Cost Centers"},
+ {"settings.table.services", "Services"},
{"settings.vat.table.country", "Country"},
{"settings.vat.table.rates", "Rates"},
{"settings.costcenters.table.code", "Code"},
{"settings.costcenters.table.description", "Description"},
+ {"settings.services.ai_service", "AI Service"},
+ {"settings.services.ai_service.provider", "Provider"},
+
+ {"settings.services.ai_service.privkey", "Public key"},
+ {"settings.services.ai_service.pubkey", "Private key"},
// Invoice/expense strings.
{"invoice.form.costcenter", "Cost center"},
diff --git a/src/ui/imgui_extensions.cpp b/src/ui/imgui_extensions.cpp
index 483f5f6..4dc1fb9 100644
--- a/src/ui/imgui_extensions.cpp
+++ b/src/ui/imgui_extensions.cpp
@@ -49,12 +49,12 @@ namespace ImGui
float widthAvailable = ImGui::GetContentRegionAvail().x;
ImGui::SetNextItemWidth(widthAvailable*0.5f);
- if (ImGui::Button("Select file..."))
+ if (ImGui::Button("Select file...")) // @localize
{
// You can adjust filters, title, default path
const char *filterPatterns[] = { "*.png", "*.jpg", "*.pdf", "*" };
const char *file = tinyfd_openFileDialog(
- "Choose a file", // dialog title
+ "Choose a file", // dialog title // @localize
NULL, // default path
4, // number of filter patterns
filterPatterns, // filter patterns array
@@ -77,7 +77,7 @@ namespace ImGui
result = true;
}
ImGui::SameLine();
- ImGui::TextWrapped("Selected: %s", buffer);
+ ImGui::TextWrapped("Selected: %s", buffer); // @localize
}
diff --git a/src/ui/ui_settings.cpp b/src/ui/ui_settings.cpp
index 892e580..1f9fe5a 100644
--- a/src/ui/ui_settings.cpp
+++ b/src/ui/ui_settings.cpp
@@ -35,6 +35,7 @@ u32 cost_center_count;
cost_center* cost_centers = 0;
static int select_company_tab = 0;
+static ai_service new_service;
void ui_destroy_settings()
{
@@ -54,6 +55,8 @@ void ui_setup_settings()
cost_center_count = administration_cost_center_count();
cost_centers = (cost_center*)malloc(cost_center_count * sizeof(cost_center));
administration_cost_center_get_all(cost_centers);
+
+ new_service = administration_get_ai_service();
}
static void ui_draw_vat_rates()
@@ -330,6 +333,39 @@ static void ui_draw_cost_centers()
}
}
+static void ui_draw_services()
+{
+ // AI service
+ if (ImGui::CollapsingHeader(localize("settings.services.ai_service")))
+ {
+ char* services[1] = {
+ "OpenAI"
+ };
+
+ if (ImGui::BeginCombo(localize("settings.services.ai_service.provider"), services[new_service.provider]))
+ {
+ for (u32 n = 0; n < 1; n++)
+ {
+ bool is_selected = n == (uint32_t)new_service.provider;
+ if (ImGui::Selectable(services[n], is_selected)) {
+ new_service.provider = (ai_provider)n;
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ ImGui::InputTextWithHint(localize("settings.services.ai_service.pubkey"), localize("settings.services.ai_service.pubkey"),
+ new_service.api_key_public, sizeof(new_service.api_key_public));
+
+ ImGui::InputTextWithHint(localize("settings.services.ai_service.privkey"), localize("settings.services.ai_service.privkey"),
+ new_service.api_key_private, sizeof(new_service.api_key_private));
+
+ if (ImGui::Button(localize("form.save"))) {
+ administration_set_ai_service(new_service);
+ }
+ }
+}
+
void ui_draw_settings()
{
if (ImGui::BeginTabBar("SettingsTabBar"))
@@ -360,6 +396,11 @@ void ui_draw_settings()
ui_draw_cost_centers();
ImGui::EndTabItem();
}
+ if (ImGui::BeginTabItem(localize("settings.table.services")))
+ {
+ ui_draw_services();
+ ImGui::EndTabItem();
+ }
ImGui::EndTabBar();
}
} \ No newline at end of file
diff --git a/tests/administration_rw_tests.cpp b/tests/administration_rw_tests.cpp
index e1582f7..cf95efe 100644
--- a/tests/administration_rw_tests.cpp
+++ b/tests/administration_rw_tests.cpp
@@ -144,16 +144,28 @@ TEST _administration_rw_info(void)
administration_writer_create();
s32 next_id, next_sequence_number;
+ ai_service ais;
administration_create_empty(test_file_path);
{
next_id = administration_get_next_id();
next_sequence_number = administration_get_next_sequence_number();
+
+ ai_service ss = {0};
+ ss.provider = AI_PROVIDER_OPENAI;
+ strops_copy(ss.api_key_public, "123", sizeof(ss.api_key_public));
+ strops_copy(ss.api_key_private, "321", sizeof(ss.api_key_private));
+ administration_set_ai_service(ss);
+
+ ais = administration_get_ai_service();
}
administration_reader_open_existing(test_file_path);
{
ASSERT_EQ(next_id, administration_get_next_id());
ASSERT_EQ(next_sequence_number, administration_get_next_sequence_number());
+
+ ai_service rs = administration_get_ai_service();
+ ASSERT_MEM_EQ(&ais, &rs, sizeof(ai_service));
}
PASS();
@@ -167,7 +179,8 @@ TEST _assert_invoices_are_equal(invoice inv, invoice invr)
ASSERT_EQ(invr.issued_at, inv.issued_at);
ASSERT_EQ(invr.expires_at, inv.expires_at);
ASSERT_EQ(invr.delivered_at, inv.delivered_at);
- ASSERT_STR_EQ(invr.document, inv.document);
+ ASSERT_STR_EQ(invr.document.original_path, inv.document.original_path);
+ ASSERT_STR_EQ(invr.document.copy_path, inv.document.copy_path);
ASSERT_STR_EQ(invr.project_id, inv.project_id);
ASSERT_STR_EQ(invr.cost_center_id, inv.cost_center_id);
@@ -302,7 +315,6 @@ TEST _administration_rw_b2c_invoice(void)
PASS();
}
-
TEST _administration_rw_b2b2c_invoice(void)
{
u32 count;
@@ -346,6 +358,57 @@ TEST _administration_rw_b2b2c_invoice(void)
PASS();
}
+TEST _administration_rw_b2c_2currency_invoice(void)
+{
+ u32 count;
+ invoice inv;
+ invoice invr;
+
+ administration_writer_create();
+
+ administration_create_default(test_file_path);
+ {
+ contact pw1 = _create_nl_business();
+ contact pw2 = _create_nl_consumer();
+
+ inv = administration_invoice_create_empty();
+ inv.supplier = pw1;
+ inv.customer = pw2;
+ 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_invoice_set_currency(&inv, "USD");
+
+ administration_billing_item_add_to_invoice(&inv, _create_bi3(&inv));
+ administration_billing_item_add_to_invoice(&inv, _create_bi4(&inv));
+ administration_billing_item_add_to_invoice(&inv, _create_bi5(&inv));
+ administration_billing_item_add_to_invoice(&inv, _create_bi6(&inv));
+
+ // Finals are set manually for multi currency invoices.
+ inv.total = 20.0f;
+ inv.tax = 10.0f;
+ inv.net = 10.0f;
+ inv.allowance = 5.0f;
+
+ count = administration_invoice_count();
+ ASSERT_EQ(administration_invoice_add(&inv), A_ERR_SUCCESS);
+ ASSERT_EQ(count+1, administration_invoice_count());
+ }
+
+ administration_reader_open_existing(test_file_path);
+ {
+ ASSERT_EQ(count+1, administration_invoice_count());
+ ASSERT_EQ(A_ERR_SUCCESS, administration_invoice_get_by_id(&invr, inv.id));
+
+ CHECK_CALL(_assert_invoices_are_equal(inv, invr));
+ }
+
+ PASS();
+}
+
SUITE(administration_rw) {
SET_SETUP(setup_cb, NULL);
SET_TEARDOWN(teardown_cb, NULL);
@@ -358,4 +421,5 @@ SUITE(administration_rw) {
RUN_TEST(_administration_rw_b2b_invoice);
RUN_TEST(_administration_rw_b2c_invoice);
RUN_TEST(_administration_rw_b2b2c_invoice);
+ RUN_TEST(_administration_rw_b2c_2currency_invoice);
} \ No newline at end of file