diff options
| author | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-09-27 14:23:56 +0200 |
|---|---|---|
| committer | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-09-27 14:23:56 +0200 |
| commit | fa088bb60692ba02d30d39affa9a31d9e2b688e2 (patch) | |
| tree | a2d51585c0f86407709b834bd7a82a5672d2c17b | |
| parent | aa7b5ef6ab4f45d2e8e0caa7942db31fc60b3861 (diff) | |
ai service settings
| -rw-r--r-- | docs/CHANGES.rst | 3 | ||||
| -rw-r--r-- | include/administration.hpp | 18 | ||||
| -rw-r--r-- | include/file_templates.hpp | 5 | ||||
| -rw-r--r-- | src/administration.cpp | 13 | ||||
| -rw-r--r-- | src/administration_reader.cpp | 6 | ||||
| -rw-r--r-- | src/administration_writer.cpp | 5 | ||||
| -rw-r--r-- | src/locales/en.cpp | 6 | ||||
| -rw-r--r-- | src/ui/imgui_extensions.cpp | 6 | ||||
| -rw-r--r-- | src/ui/ui_settings.cpp | 41 | ||||
| -rw-r--r-- | tests/administration_rw_tests.cpp | 68 |
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 |
