From a2299b0bae21c8f05f091732a78fc250cbd5e016 Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Sun, 28 Sep 2025 17:41:50 +0200 Subject: openAI invoice importing v0.1 --- docs/CHANGES.rst | 18 +++ include/administration.hpp | 4 +- include/administration_reader.hpp | 3 +- include/ai_service.hpp | 10 ++ run.bat | 4 +- src/administration.cpp | 4 +- src/administration_reader.cpp | 18 ++- src/ai_providers/openAI.cpp | 252 ++++++++++++++++++++++++++++++++++++++ src/ai_service.cpp | 135 +++++++------------- src/ui/ui_expenses.cpp | 2 +- 10 files changed, 346 insertions(+), 104 deletions(-) create mode 100644 src/ai_providers/openAI.cpp diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index 157b56e..1a46011 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -1,7 +1,25 @@ .. _changes: TODO: +for invoice importing using AI: + 4. find billing item tax rate based on paid tax vs total + 6. set document original and copy path (save file to zip first) + 7. create a new invoice ID + + 8. call ai_document_to_invoice in a thread & create a new UI page to show loading screen. + The user should not be able to add/update existing invoices but they can navigate to other screens + and come back to the loading screen. +- retrieve available balance from AI api & show in settings/services. +- let user choose the model to use in settings/services/ai +- real error logging for OpenAI and importing in general +- when importing an invoice: do not accept invoices for unsupported countries (yet) + +- log_set_depth function so data can be grouped +- move string function out of openAI.cpp into strops.cpp +- log elapsed time for ai requests - refactor _add functions to use _import functions +- _import functions should not check for validity and should never fail because of invalid data +- invalid invoices should be marked in the UI - 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). - invoice sequential number should be modifyable & checked for uniqueness (for external invoices being imported) diff --git a/include/administration.hpp b/include/administration.hpp index ec64833..3d6a88c 100644 --- a/include/administration.hpp +++ b/include/administration.hpp @@ -27,8 +27,8 @@ #define MAX_LEN_FILENAME 255 #define MAX_LEN_PATH 4096 #define MAX_LEN_CURRENCY 8 -#define MAX_LEN_SHORT_DESC 32 -#define MAX_LEN_LONG_DESC 64 +#define MAX_LEN_SHORT_DESC 64 +#define MAX_LEN_LONG_DESC 256 #define MAX_LEN_EMAIL 64 #define MAX_LEN_PHONE 16 #define MAX_LEN_BANK 35 diff --git a/include/administration_reader.hpp b/include/administration_reader.hpp index 7765680..d650855 100644 --- a/include/administration_reader.hpp +++ b/include/administration_reader.hpp @@ -25,4 +25,5 @@ bool administration_reader_import_tax_rate(char* buffer, size_t buffer_size); bool administration_reader_import_cost_center(char* buffer, size_t buffer_size); bool administration_reader_import_project(char* buffer, size_t buffer_size); bool administration_reader_import_contact(char* buffer, size_t buffer_size); -bool administration_reader_import_invoice(char* buffer, size_t buffer_size); \ No newline at end of file +bool administration_reader_import_invoice(char* buffer, size_t buffer_size); +bool administration_reader_read_invoice_from_xml(invoice* result, char* buffer, size_t buffer_size); \ No newline at end of file diff --git a/include/ai_service.hpp b/include/ai_service.hpp index b481b58..cf0b67a 100644 --- a/include/ai_service.hpp +++ b/include/ai_service.hpp @@ -16,6 +16,8 @@ #pragma once +#include "administration.hpp" + typedef struct { time_t started_at; @@ -23,4 +25,12 @@ typedef struct char* result; } ai_request; +typedef struct +{ + bool (*upload_file)(char* file_path, char* file_id, size_t file_id_len); + bool (*query_with_file)(char* query, size_t query_length, char* file_id, char** response); +} ai_provider_impl; + +extern ai_provider_impl _chatgpt_api_provider; + ai_request* ai_document_to_invoice(char* file_path); \ No newline at end of file diff --git a/run.bat b/run.bat index 954ac4f..f99c70f 100644 --- a/run.bat +++ b/run.bat @@ -28,13 +28,13 @@ set LIB_SOURCES=libs\imgui-1.92.1\backends\imgui_impl_dx11.cpp^ libs\xml.c\src\*.c^ libs\timer_lib\*.c^ libs\tinyfiledialogs\tinyfiledialogs.c -@set SOURCES= src\*.cpp src\ui\*.cpp src\locales\*.cpp +@set SOURCES= src\*.cpp src\ui\*.cpp src\locales\*.cpp src\ai_providers\*.cpp @set LIBS=opengl32.lib Advapi32.lib Shell32.lib Ole32.lib User32.lib Pathcch.lib D3D11.lib Comdlg32.lib Kernel32.lib /LIBPATH:"libs/openssl-3.6.0-beta1/x64/lib" libssl.lib libcrypto.lib @set FLAGS=/nologo /Ob0 /MD /Oy- /Zi /FS /W4 /EHsc /utf-8 @set INCLUDE_DIRS=/I"libs/imgui-1.92.1" /I"libs/imgui-1.92.1/backends" /I"/" /I"libs/openssl-3.6.0-beta1/x64/include" /I"libs/cpp-httplib" /I"libs/timer_lib" /I"libs/greatest" /I"libs/simclist-1.5" /I"libs/tinyfiledialogs" /I"libs/zip/src" /I"libs/xml.c/src" /I"libs/" /Iinclude @set DEFINITIONS=/D_BUILD_DATE_=\"%date%\" /D_COMMIT_=\"%COMMIT_ID%\" /D_PLATFORM_=\"win64\" -if "%1"=="-t" @set SOURCES= tests\main.cpp src\administration.cpp src\administration_writer.cpp src\administration_reader.cpp src\strops.cpp src\log.cpp src\locales.cpp src\locales\*.cpp src\ui\helpers.cpp +if "%1"=="-t" @set SOURCES= tests\main.cpp src\administration.cpp src\administration_writer.cpp src\administration_reader.cpp src\strops.cpp src\log.cpp src\locales.cpp src\locales\*.cpp src\ai_providers\*.cpp src\ui\helpers.cpp if "%1"=="-t" @set OUT_EXE=accounting_tests cl %FLAGS% %INCLUDE_DIRS% %DEFINITIONS% %SOURCES% %LIB_SOURCES% /Fe%OUT_DIR%/%OUT_EXE%.exe /Fd%OUT_DIR%/vc140.pdb /Fo%OUT_DIR%/ /link %LIBS% diff --git a/src/administration.cpp b/src/administration.cpp index e1405b8..231953b 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -1558,8 +1558,8 @@ a_err administration_invoice_update(invoice* inv) a_err administration_invoice_import(invoice* inv) { - a_err result = administration_invoice_is_valid(inv); - if (result != A_ERR_SUCCESS) return result; + //a_err result = administration_invoice_is_valid(inv); + //if (result != A_ERR_SUCCESS) return result; inv->is_triangulation = !(memcmp(&inv->addressee.address, &inv->customer.address, sizeof(address)) == 0); diff --git a/src/administration_reader.cpp b/src/administration_reader.cpp index b8b4958..f316f40 100644 --- a/src/administration_reader.cpp +++ b/src/administration_reader.cpp @@ -123,14 +123,13 @@ bool administration_reader_open_existing(char* file_path) return true; } -bool administration_reader_import_invoice(char* buffer, size_t buffer_size) +bool administration_reader_read_invoice_from_xml(invoice* result, char* buffer, size_t buffer_size) { - STOPWATCH_START; - xml_document* document = xml_parse_document((uint8_t *)buffer, buffer_size); if (!document) return false; struct xml_node* root = xml_document_root(document); + if (!root) return false; invoice data = administration_invoice_create_empty(); xml_get_str(root, data.id, MAX_LEN_ID, "cbc:ID"); @@ -238,8 +237,19 @@ bool administration_reader_import_invoice(char* buffer, size_t buffer_size) free(child_name); } - + + *result = data; + return true; +} + +bool administration_reader_import_invoice(char* buffer, size_t buffer_size) +{ + STOPWATCH_START; + + invoice data; + if (!administration_reader_read_invoice_from_xml(&data, buffer, buffer_size)) return false; a_err result = administration_invoice_import(&data); + if (result == A_ERR_SUCCESS) { log_info("Loaded invoice '%s' in %.3fms.", data.sequential_number, STOPWATCH_TIME); } diff --git a/src/ai_providers/openAI.cpp b/src/ai_providers/openAI.cpp new file mode 100644 index 0000000..5dd2c50 --- /dev/null +++ b/src/ai_providers/openAI.cpp @@ -0,0 +1,252 @@ +/* +* Copyright (c) 2025 Aldrik Ramaekers +* +* 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 +#include +#include + +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include "log.hpp" +#include "ai_service.hpp" + +static char *extract_json_value(const char *json, const char *key, char *out, size_t out_size, int skip = 0) { + char pattern[128]; + snprintf(pattern, sizeof(pattern), "\"%s\"", key); + const char *pos = strstr(json, pattern); + while(skip > 0) { + pos = strstr(pos+1, pattern); + skip--; + } + if (!pos) return NULL; + pos = strchr(pos, ':'); + if (!pos) return NULL; + pos++; + + // Skip whitespace and quotes + while (*pos == ' ' || *pos == '\"') pos++; + + size_t i = 0; + while (*pos && !(*pos == '\"' && *(pos-1) != '\\') && i < out_size - 1) { + out[i++] = *pos++; + } + out[i] = '\0'; + return out; +} + +static const char* get_filename(const char* path) { + const char* filename = strrchr(path, '/'); // for Unix-style paths + if (filename) return filename + 1; // skip the '/' + filename = strrchr(path, '\\'); // for Windows-style paths + if (filename) return filename + 1; + return path; // no slashes found, path itself is filename +} + +static char *escape_quotes(const char *input, size_t buffer_size) { + if (!input) return NULL; + + char *result = (char*)malloc(buffer_size + 100); // Ballpark + if (!result) return NULL; + + const char *src = input; + char *dst = result; + + while (*src) { + if (*src == '"') { + *dst++ = '\\'; + *dst++ = '"'; + } + else if (*src == '\n') { + // empty + } + else { + *dst++ = *src; + } + src++; + } + *dst = '\0'; + + return result; +} + +static char *unescape_quotes(char *input) { + if (!input) return NULL; + + char *src = input; + char *dst = input; + + while (*src) { + if (*src == '\\' && *(src+1) == '"') { + src++; + } + else if (*src == '\\' && *(src+1) == 'n') { + src++;src++; + } + *dst++ = *src++; + } + *dst = '\0'; + + return input; +} + +static bool _openAI_query_with_file(char* query, size_t query_length, char* file_id, char** response) +{ + #define TESTING_IMPORT + + #ifndef TESTING_IMPORT + const char *api_key = administration_get_ai_service().api_key_public; + + httplib::SSLClient cli("api.openai.com", 443); + //cli.enable_server_certificate_verification(false); + + char* query_escaped = escape_quotes(query, query_length); + free(query); + + size_t body_size = query_length + 200; + char* body = (char*)malloc(body_size); + snprintf(body, body_size, + "{\"model\":\"gpt-5-nano\", \"input\": [ { \"role\": \"user\", \"content\": [ { \"type\": \"input_file\", \"file_id\": \"%s\" }, " + "{ \"type\": \"input_text\", \"text\": \"%s\" } ] } ] }", file_id, query_escaped); + + httplib::Headers headers; + headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + httplib::Result res = cli.Post("/v1/responses", headers, body, "application/json"); + free(body); + + if (!res || res->status != 200) { + log_error("ERROR Failed to query API."); + return 0; + } + + char* response_body = (char*)res->body.c_str(); + #else + char* response_body = "{\n \"id\": \"resp_68d9482030fc8196930b43b6b28feeb104e98afee829eee0\",\n \"object\": \"response\",\n \"created_at\": 1759070240,\n \"status\": \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": \"developer\"\n },\n \"error\": null,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-5-2025-08-07\",\n \"output\": [\n {\n \"id\": \"rs_68d94821d1f0819694533a6ed7ed6b2904e98afee829eee0\",\n \"type\": \"reasoning\",\n \"summary\": []\n },\n {\n \"id\": \"msg_68d948a09e0c819696782e09c6b7626104e98afee829eee0\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"\\n urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0\\n urn:fdc:peppol.eu:2017:poacc:billing:01:1.0\\n 586928\\n 2025-03-17\\n 2025-03-24\\n 380\\n EUR\\n \\n 699607\\n \\n \\n AR385893\\n Jouw bestelling : 420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31\\n \\n \\n 420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n R.C le mans B 302 494 224\\n \\n \\n Visserie-service\\n \\n \\n Z.A Nord\\n \\n Parce sur Sarthe\\n 72300\\n \\n \\n FR\\n \\n \\n \\n FR57 302 494 224\\n \\n VAT\\n \\n \\n \\n Visserie Service SAS\\n \\n \\n AMELIE L\\n 02.43.62.09.08\\n klantenservice@schroeven-express.nl\\n \\n \\n \\n \\n \\n \\n \\n cl585187\\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n KEERDERSTRAAT 81\\n \\n MAASTRICHT\\n 6226X\\n \\n \\n NL\\n \\n \\n \\n \\n \\n VAT\\n \\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n A RAMAEKERS\\n 31618260377\\n aldrikboy@gmail.com\\n \\n \\n \\n \\n 2025-03-17\\n \\n \\n KEERDERSTRAAT 81\\n \\n MAASTRICHT\\n 6226X\\n \\n \\n NL\\n \\n \\n \\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n \\n \\n \\n 586928\\n \\n FR76 1790 6001 1272 5017 0700 137\\n Visserie Service SAS\\n \\n \\n AGRIFRPP879\\n \\n \\n \\n \\n \\n \\n \\n \\n 2.59\\n \\n 12.36\\n 2.59\\n \\n \\n 21\\n \\n VAT\\n \\n \\n \\n \\n \\n 6.95\\n 12.36\\n 14.95\\n 14.95\\n \\n \\n 1\\n 500\\n 6.95\\n \\n false\\n Discount\\n \\n \\n \\n \\n \\n Metalen schroeven RVS A2 gefreesde kop Pozi N\\u00b01 M2X4 DIN 965 ISO 7046, VS0109, VS0110\\n \\n Internal Tax Rate ID\\n \\n \\n \\n \\n 21\\n \\n VAT\\n \\n \\n \\n \\n 1.39\\n \\n \\n\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n"; + #endif + + *response = (char*)malloc(100000); + memset(*response, 0, 100000); + strncpy(*response, response_body, 100000); + + extract_json_value(*response, "text", *response, 100000); + *response = unescape_quotes(*response); + + return 1; +} + +static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_len) +{ + const char *api_key = administration_get_ai_service().api_key_public; + const char *filename = get_filename(file_path); + + FILE* orig_file = fopen(file_path, "rb"); + if (orig_file == NULL) { + log_error("ERROR: file to upload could not be opened."); + return 0; + } + + fseek(orig_file, 0L, SEEK_END); + long sz = ftell(orig_file); + fseek(orig_file, 0, SEEK_SET); + + httplib::SSLClient cli("api.openai.com", 443); + //cli.enable_server_certificate_verification(false); + + char body[512]; + snprintf(body, sizeof(body), "{\"filename\":\"%s\",\"purpose\":\"user_data\", \"bytes\": %d, \"mime_type\": \"application/pdf\", \"expires_after\": { \"anchor\": \"created_at\", \"seconds\": 3600 } }", filename, sz); + + httplib::Headers headers; + headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + httplib::Result res = cli.Post("/v1/uploads", headers, body, "application/json"); + if (!res || res->status != 200) { + log_error("ERROR Failed to create upload."); + fclose(orig_file); + return 0; + } + + char upload_id[128]; + extract_json_value(res->body.c_str(), "id", upload_id, sizeof(upload_id)); + size_t part_size = 64000000; // 64mb + log_info("Created upload %s with part size %zu.", upload_id, part_size); + + char *buffer = (char*)malloc(part_size); + + char completion_body[1048]; + snprintf(completion_body, sizeof(completion_body), "{\"part_ids\": ["); + + int part_number = 0; + while (1) { + size_t read_bytes = fread(buffer, 1, part_size, orig_file); + if (read_bytes == 0) break; + + httplib::Headers part_headers; + part_headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + std::string chunk(buffer, read_bytes); + + httplib::UploadFormDataItems items = { + {"data", chunk, filename, "application/octet-stream"} + }; + + char path[256]; + snprintf(path, sizeof(path), "/v1/uploads/%s/parts?part_number=%d", upload_id, part_number); + + httplib::Result part_res = cli.Post(path, part_headers, items); + + if (!part_res || part_res->status != 200) { + log_error("Failed to upload part %d.", part_number); + free(buffer); + fclose(orig_file); + return 0; + } + else { + char part_id[128]; + extract_json_value(part_res->body.c_str(), "id", part_id, sizeof(part_id)); + if (part_number == 0) snprintf(completion_body+strlen(completion_body), sizeof(completion_body)-strlen(completion_body), "\"%s\"", part_id); + if (part_number != 0) snprintf(completion_body+strlen(completion_body), sizeof(completion_body)-strlen(completion_body), ", \"%s\"", part_id); + } + + log_info("Uploaded part %d\n", part_number); + part_number++; + } + + snprintf(completion_body+strlen(completion_body), sizeof(completion_body)-strlen(completion_body), "]}"); + + free(buffer); + fclose(orig_file); + + // ---------- Step 3: Complete upload ---------- + + httplib::Result complete_res = cli.Post((std::string("/v1/uploads/") + upload_id + "/complete").c_str(), + headers, completion_body, "application/json"); + + + if (!complete_res || complete_res->status != 200) { + log_error("ERROR Failed to complete upload."); + return 0; + } + + char* completion_body_response = (char*)complete_res->body.c_str(); + extract_json_value(completion_body_response, "id", file_id, file_id_len, 1); + + return 1; +} + +ai_provider_impl _chatgpt_api_provider = { + _openAI_upload_file, + _openAI_query_with_file, +}; \ No newline at end of file diff --git a/src/ai_service.cpp b/src/ai_service.cpp index 2553f61..e271fcb 100644 --- a/src/ai_service.cpp +++ b/src/ai_service.cpp @@ -22,121 +22,72 @@ #include "httplib.h" #include "log.hpp" #include "ai_service.hpp" +#include "strops.hpp" +#include "administration_reader.hpp" +ai_provider_impl _ai_get_impl() +{ + ai_provider provider = administration_get_ai_service().provider; -// ---- Utility: simple JSON value extractor (very naive) ---- -char *extract_json_value(const char *json, const char *key, char *out, size_t out_size) { - char pattern[128]; - snprintf(pattern, sizeof(pattern), "\"%s\"", key); - const char *pos = strstr(json, pattern); - if (!pos) return NULL; - pos = strchr(pos, ':'); - if (!pos) return NULL; - pos++; - - // Skip whitespace and quotes - while (*pos == ' ' || *pos == '\"') pos++; - - size_t i = 0; - while (*pos && *pos != '\"' && *pos != ',' && *pos != '}' && i < out_size - 1) { - out[i++] = *pos++; + switch(provider) + { + case AI_PROVIDER_OPENAI: return _chatgpt_api_provider; + default: assert(0); break; } - out[i] = '\0'; - return out; -} -// ---- Read file chunk ---- -size_t read_chunk(FILE *fp, char *buffer, size_t chunk_size) { - return fread(buffer, 1, chunk_size, fp); + return ai_provider_impl {0}; } -const char* get_filename(const char* path) { - const char* filename = strrchr(path, '/'); // for Unix-style paths - if (filename) return filename + 1; // skip the '/' - filename = strrchr(path, '\\'); // for Windows-style paths - if (filename) return filename + 1; - return path; // no slashes found, path itself is filename -} +extern const char* peppol_invoice_template; +extern const char* peppol_invoice_line_template; ai_request* ai_document_to_invoice(char* file_path) { - const char *api_key = administration_get_ai_service().api_key_public; - const char *filename = get_filename(file_path); - - FILE* orig_file = fopen(file_path, "rb"); - if (orig_file == NULL) { - log_error("ERROR: file to upload could not be opened."); - return 0; - } + ai_provider_impl impl = _ai_get_impl(); - fseek(orig_file, 0L, SEEK_END); - long sz = ftell(orig_file); - fseek(orig_file, 0, SEEK_SET); - - httplib::SSLClient cli("api.openai.com", 443); - cli.enable_server_certificate_verification(false); - - char body[512]; - snprintf(body, sizeof(body), "{\"filename\":\"%s\",\"purpose\":\"user_data\", \"bytes\": %d, \"mime_type\": \"application/pdf\", \"expires_after\": { \"anchor\": \"created_at\", \"seconds\": 3600 } }", filename, sz); - - httplib::Headers headers; - headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); - - httplib::Result res = cli.Post("/v1/uploads", headers, body, "application/json"); - if (!res || res->status != 200) { - log_error("ERROR Failed to create upload."); - fclose(orig_file); + char file_id[100]; + if (!impl.upload_file(file_path, file_id, 100)) { return 0; } - char upload_id[128]; - extract_json_value(res->body.c_str(), "id", upload_id, sizeof(upload_id)); - size_t part_size = 64000000; // 64mb - log_info("Created upload %s with part size %zu.", upload_id, part_size); + size_t query_buffer_len = 50000; + char* template_buffer = (char*)malloc(query_buffer_len); + memset(template_buffer, 0, query_buffer_len); - char *buffer = (char*)malloc(part_size); + strncpy(template_buffer, peppol_invoice_template, query_buffer_len); + strops_replace(template_buffer, 50000, "{{INVOICE_LINE_LIST}}", peppol_invoice_line_template); - int part_number = 0; - while (1) { - size_t read_bytes = read_chunk(orig_file, buffer, part_size); - if (read_bytes == 0) break; + char* ai_query = + "\n\nI have provided a file containing an invoice. Fill in the above Peppol 3.0 template with the information from the invoice." + "Do not add any fields to the template. If you can't find data for a given field, leave it empty. Do not make up any information." + "Only return the filled out template in valid XML format. Nothing else.\n"; - httplib::Headers part_headers; - part_headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); - part_headers.insert(std::make_pair("Content-Type", "multipart/form-data")); + size_t query_len = strlen(template_buffer); + strncpy(template_buffer + query_len, ai_query, query_buffer_len - query_len); - std::string chunk(buffer, read_bytes); + char* response; + if (!impl.query_with_file(template_buffer, query_buffer_len, file_id, &response)) { + return 0; + } - httplib::UploadFormDataItems items = { - {"data", chunk, filename, "application/pdf"} - }; + invoice inv; + if (!administration_reader_read_invoice_from_xml(&inv, response, strlen(response))) { + return false; + } - char path[256]; - snprintf(path, sizeof(path), "/v1/uploads/%s/parts?part_number=%d", upload_id, part_number); + invoice tmp = administration_invoice_create_empty(); - httplib::Result part_res = cli.Post(path, part_headers, items); + inv.status = invoice_status::INVOICE_RECEIVED; + strops_copy(inv.id, tmp.id, MAX_LEN_ID); // TODO next_id is not being incremented + strops_copy(inv.customer.id, MY_COMPANY_ID, MAX_LEN_ID); // TODO param for incomming/exporting necessary - if (!part_res || part_res->status != 200) { - log_error("Failed to upload part %d.", part_number); - free(buffer); - fclose(orig_file); - return 0; - } + strops_copy(inv.document.original_path, file_path, MAX_LEN_PATH); + strops_copy(inv.document.copy_path, "", MAX_LEN_PATH); - log_info("Uploaded part %d\n", part_number); - part_number++; - } + a_err result = administration_invoice_import(&inv); - free(buffer); - fclose(orig_file); - - // ---------- Step 3: Complete upload ---------- - httplib::Result complete_res = cli.Post((std::string("/v1/uploads/") + upload_id + "/complete").c_str(), - headers, "", "application/json"); - if (!complete_res || complete_res->status != 200) { - log_error("ERROR Failed to complete upload."); - return 0; - } + free(template_buffer); + free(response); return 0; } \ No newline at end of file diff --git a/src/ui/ui_expenses.cpp b/src/ui/ui_expenses.cpp index 790394a..153e6b7 100644 --- a/src/ui/ui_expenses.cpp +++ b/src/ui/ui_expenses.cpp @@ -176,7 +176,7 @@ static void ui_draw_expenses_list() char import_file_path[MAX_LEN_PATH] = {0}; ImGui::SameLine(); if (ImGui::FormInvoiceFileSelector("+ Import", import_file_path)) { // @localize - current_view_state = view_state::CREATE; + //current_view_state = view_state::CREATE; active_invoice = administration_invoice_create_empty(); // @leak active_invoice.customer = administration_company_info_get(); active_invoice.is_outgoing = 0; -- cgit v1.2.3-70-g09d2