summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/administration.cpp4
-rw-r--r--src/administration_reader.cpp18
-rw-r--r--src/ai_providers/openAI.cpp252
-rw-r--r--src/ai_service.cpp135
-rw-r--r--src/ui/ui_expenses.cpp2
5 files changed, 312 insertions, 99 deletions
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 <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 <fstream>
+#include <iostream>
+#include <string>
+
+#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\": \"<Invoice xmlns=\\\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\\\" xmlns:cac=\\\"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2\\\" xmlns:cbc=\\\"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2\\\">\\n <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>\\n <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>\\n <cbc:ID>586928</cbc:ID>\\n <cbc:IssueDate>2025-03-17</cbc:IssueDate>\\n <cbc:DueDate>2025-03-24</cbc:DueDate>\\n <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>\\n <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>\\n <cac:DespatchDocumentReference>\\n <cbc:ID>699607</cbc:ID>\\n </cac:DespatchDocumentReference>\\n <cac:AdditionalDocumentReference>\\n <cbc:ID>AR385893</cbc:ID>\\n <cbc:DocumentDescription>Jouw bestelling : 420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31</cbc:DocumentDescription>\\n </cac:AdditionalDocumentReference>\\n <cac:OrderReference>\\n <cbc:ID>420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31</cbc:ID>\\n </cac:OrderReference>\\n <cac:ProjectReference>\\n <cbc:ID></cbc:ID>\\n </cac:ProjectReference>\\n <cbc:AccountingCost></cbc:AccountingCost>\\n <cac:AccountingSupplierParty>\\n <cac:Party>\\n <cbc:EndpointID schemeID=\\\"\\\"></cbc:EndpointID>\\n <cac:PartyIdentification>\\n <cbc:ID schemeID=\\\"ZZZ\\\">R.C le mans B 302 494 224</cbc:ID>\\n </cac:PartyIdentification>\\n <cac:PartyName>\\n <cbc:Name>Visserie-service</cbc:Name>\\n </cac:PartyName>\\n <cac:PostalAddress>\\n <cbc:StreetName>Z.A Nord</cbc:StreetName>\\n <cbc:AdditionalStreetName></cbc:AdditionalStreetName>\\n <cbc:CityName>Parce sur Sarthe</cbc:CityName>\\n <cbc:PostalZone>72300</cbc:PostalZone>\\n <cbc:CountrySubentity></cbc:CountrySubentity>\\n <cac:Country>\\n <cbc:IdentificationCode>FR</cbc:IdentificationCode>\\n </cac:Country>\\n </cac:PostalAddress>\\n <cac:PartyTaxScheme>\\n <cbc:CompanyID>FR57 302 494 224</cbc:CompanyID>\\n <cac:TaxScheme>\\n <cbc:ID>VAT</cbc:ID>\\n </cac:TaxScheme>\\n </cac:PartyTaxScheme>\\n <cac:PartyLegalEntity>\\n <cbc:RegistrationName>Visserie Service SAS</cbc:RegistrationName>\\n </cac:PartyLegalEntity>\\n <cac:Contact>\\n <cbc:Name>AMELIE L</cbc:Name>\\n <cbc:Telephone>02.43.62.09.08</cbc:Telephone>\\n <cbc:ElectronicMail>klantenservice@schroeven-express.nl</cbc:ElectronicMail>\\n </cac:Contact>\\n </cac:Party>\\n </cac:AccountingSupplierParty>\\n <cac:AccountingCustomerParty>\\n <cac:Party>\\n <cbc:EndpointID schemeID=\\\"\\\"></cbc:EndpointID>\\n <cac:PartyIdentification>\\n <cbc:ID schemeID=\\\"ZZZ\\\">cl585187</cbc:ID>\\n </cac:PartyIdentification>\\n <cac:PartyName>\\n <cbc:Name>ALDRIK RAMAEKERS</cbc:Name>\\n </cac:PartyName>\\n <cac:PostalAddress>\\n <cbc:StreetName>KEERDERSTRAAT 81</cbc:StreetName>\\n <cbc:AdditionalStreetName></cbc:AdditionalStreetName>\\n <cbc:CityName>MAASTRICHT</cbc:CityName>\\n <cbc:PostalZone>6226X</cbc:PostalZone>\\n <cbc:CountrySubentity></cbc:CountrySubentity>\\n <cac:Country>\\n <cbc:IdentificationCode>NL</cbc:IdentificationCode>\\n </cac:Country>\\n </cac:PostalAddress>\\n <cac:PartyTaxScheme>\\n <cbc:CompanyID></cbc:CompanyID>\\n <cac:TaxScheme>\\n <cbc:ID>VAT</cbc:ID>\\n </cac:TaxScheme>\\n </cac:PartyTaxScheme>\\n <cac:PartyLegalEntity>\\n <cbc:RegistrationName>ALDRIK RAMAEKERS</cbc:RegistrationName>\\n </cac:PartyLegalEntity>\\n <cac:Contact>\\n <cbc:Name>A RAMAEKERS</cbc:Name>\\n <cbc:Telephone>31618260377</cbc:Telephone>\\n <cbc:ElectronicMail>aldrikboy@gmail.com</cbc:ElectronicMail>\\n </cac:Contact>\\n </cac:Party>\\n </cac:AccountingCustomerParty>\\n <cac:Delivery>\\n <cbc:ActualDeliveryDate>2025-03-17</cbc:ActualDeliveryDate>\\n <cac:DeliveryLocation>\\n <cac:Address>\\n <cbc:StreetName>KEERDERSTRAAT 81</cbc:StreetName>\\n <cbc:AdditionalStreetName></cbc:AdditionalStreetName>\\n <cbc:CityName>MAASTRICHT</cbc:CityName>\\n <cbc:PostalZone>6226X</cbc:PostalZone>\\n <cbc:CountrySubentity></cbc:CountrySubentity>\\n <cac:Country>\\n <cbc:IdentificationCode>NL</cbc:IdentificationCode>\\n </cac:Country>\\n </cac:Address>\\n </cac:DeliveryLocation>\\n <cac:DeliveryParty>\\n <cac:PartyName>\\n <cbc:Name>ALDRIK RAMAEKERS</cbc:Name>\\n </cac:PartyName>\\n </cac:DeliveryParty>\\n </cac:Delivery>\\n <cac:PaymentMeans>\\n <cbc:PaymentMeansCode></cbc:PaymentMeansCode>\\n <cbc:PaymentID>586928</cbc:PaymentID>\\n <cac:PayeeFinancialAccount>\\n <cbc:ID>FR76 1790 6001 1272 5017 0700 137</cbc:ID>\\n <cbc:Name>Visserie Service SAS</cbc:Name>\\n <cac:FinancialInstitutionBranch>\\n <cac:FinancialInstitution>\\n <cbc:ID>AGRIFRPP879</cbc:ID>\\n </cac:FinancialInstitution>\\n </cac:FinancialInstitutionBranch>\\n </cac:PayeeFinancialAccount>\\n <cac:PayerFinancialAccount>\\n <cbc:ID></cbc:ID>\\n </cac:PayerFinancialAccount>\\n </cac:PaymentMeans>\\n <cac:TaxTotal>\\n <cbc:TaxAmount currencyID=\\\"EUR\\\">2.59</cbc:TaxAmount>\\n <cac:TaxSubtotal>\\n <cbc:TaxableAmount currencyID=\\\"EUR\\\">12.36</cbc:TaxableAmount>\\n <cbc:TaxAmount currencyID=\\\"EUR\\\">2.59</cbc:TaxAmount>\\n <cac:TaxCategory>\\n <cbc:ID></cbc:ID>\\n <cbc:Percent>21</cbc:Percent>\\n <cac:TaxScheme>\\n <cbc:ID>VAT</cbc:ID>\\n </cac:TaxScheme>\\n </cac:TaxCategory>\\n </cac:TaxSubtotal>\\n </cac:TaxTotal>\\n <cac:LegalMonetaryTotal>\\n <cbc:LineExtensionAmount currencyID=\\\"EUR\\\">6.95</cbc:LineExtensionAmount>\\n <cbc:TaxExclusiveAmount currencyID=\\\"EUR\\\">12.36</cbc:TaxExclusiveAmount>\\n <cbc:TaxInclusiveAmount currencyID=\\\"EUR\\\">14.95</cbc:TaxInclusiveAmount>\\n <cbc:PayableAmount currencyID=\\\"EUR\\\">14.95</cbc:PayableAmount>\\n </cac:LegalMonetaryTotal>\\n <cac:InvoiceLine>\\n <cbc:ID>1</cbc:ID>\\n <cbc:InvoicedQuantity unitCode=\\\"\\\">500</cbc:InvoicedQuantity>\\n <cbc:LineExtensionAmount currencyID=\\\"EUR\\\">6.95</cbc:LineExtensionAmount>\\n <cac:AllowanceCharge>\\n <cbc:ChargeIndicator>false</cbc:ChargeIndicator>\\n <cbc:AllowanceChargeReason>Discount</cbc:AllowanceChargeReason>\\n <cbc:MultiplierFactorNumeric></cbc:MultiplierFactorNumeric>\\n <cbc:Amount currencyID=\\\"EUR\\\"></cbc:Amount>\\n <cbc:BaseAmount currencyID=\\\"EUR\\\"></cbc:BaseAmount>\\n </cac:AllowanceCharge>\\n <cac:Item>\\n <cbc:Name>Metalen schroeven RVS A2 gefreesde kop Pozi N\\u00b01 M2X4 DIN 965 ISO 7046, VS0109, VS0110</cbc:Name>\\n <cac:AdditionalItemProperty>\\n <cbc:Name>Internal Tax Rate ID</cbc:Name>\\n <cbc:Value></cbc:Value>\\n </cac:AdditionalItemProperty>\\n <cac:ClassifiedTaxCategory>\\n <cbc:ID></cbc:ID>\\n <cbc:Percent>21</cbc:Percent>\\n <cac:TaxScheme>\\n <cbc:ID>VAT</cbc:ID>\\n </cac:TaxScheme>\\n </cac:ClassifiedTaxCategory>\\n </cac:Item>\\n <cac:Price>\\n <cbc:PriceAmount currencyID=\\\"EUR\\\">1.39</cbc:PriceAmount>\\n </cac:Price>\\n </cac:InvoiceLine>\\n</Invoice>\"\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;