diff options
| -rw-r--r-- | docs/CHANGES.rst | 4 | ||||
| -rw-r--r-- | include/import_service.hpp (renamed from include/ai_service.hpp) | 28 | ||||
| -rw-r--r-- | include/ui.hpp | 1 | ||||
| -rw-r--r-- | libs/imgui-1.92.1/imgui.cpp | 40 | ||||
| -rw-r--r-- | libs/imgui-1.92.1/imgui.h | 4 | ||||
| -rw-r--r-- | libs/xml.c/src/xml.c | 6 | ||||
| -rw-r--r-- | src/ai_providers/openAI.cpp | 20 | ||||
| -rw-r--r-- | src/import_service.cpp (renamed from src/ai_service.cpp) | 71 | ||||
| -rw-r--r-- | src/locales/en.cpp | 11 | ||||
| -rw-r--r-- | src/main.cpp | 2 | ||||
| -rw-r--r-- | src/ui/ui_expenses.cpp | 61 |
11 files changed, 218 insertions, 30 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index b93080c..2d1bbf1 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -2,13 +2,11 @@ TODO: for invoice importing using AI: + 1. my address data should be editable because import is not perfect 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 diff --git a/include/ai_service.hpp b/include/import_service.hpp index cf0b67a..516257e 100644 --- a/include/ai_service.hpp +++ b/include/import_service.hpp @@ -18,12 +18,30 @@ #include "administration.hpp" +typedef uint32_t i_err; + +#define I_ERR_SUCCESS 0 +#define I_ERR_FAILED_UPLOAD 1 +#define I_ERR_FAILED_QUERY 2 +#define I_ERR_FAILED_IMPORT 3 + +typedef enum +{ + IMPORT_STARTING, + IMPORT_UPLOADING_FILE, + IMPORT_QUERYING, + IMPORT_WAITING_FOR_RESPONSE, + IMPORT_DONE, +} import_status; + typedef struct { time_t started_at; - bool finished; - char* result; -} ai_request; + invoice result; + char file_path[MAX_LEN_PATH]; + i_err error; + import_status status; +} import_invoice_request; typedef struct { @@ -33,4 +51,6 @@ typedef struct extern ai_provider_impl _chatgpt_api_provider; -ai_request* ai_document_to_invoice(char* file_path);
\ No newline at end of file +const char* import_error_to_str(i_err error); +const char* import_status_to_str(import_status status); +import_invoice_request* ai_document_to_invoice(char* file_path);
\ No newline at end of file diff --git a/include/ui.hpp b/include/ui.hpp index 33f6d18..931f302 100644 --- a/include/ui.hpp +++ b/include/ui.hpp @@ -47,6 +47,7 @@ typedef enum EDIT, CREATE, VIEW, + VIEW_IMPORT_REQUEST, } view_state; typedef struct diff --git a/libs/imgui-1.92.1/imgui.cpp b/libs/imgui-1.92.1/imgui.cpp index 99819ff..b41a167 100644 --- a/libs/imgui-1.92.1/imgui.cpp +++ b/libs/imgui-1.92.1/imgui.cpp @@ -16677,6 +16677,46 @@ void ImGui::DebugBreakButtonTooltip(bool keyboard_only, const char* description_ EndTooltip(); } +// From https://github.com/ocornut/imgui/issues/1901#issuecomment-444929973 +void ImGui::LoadingIndicatorCircle(const char* label, const float indicator_radius, + const ImVec4& main_color, const ImVec4& backdrop_color, + const int circle_count, const float speed) { + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) { + return; + } + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + + const ImVec2 pos = window->DC.CursorPos; + const float circle_radius = indicator_radius / 15.0f; + const float updated_indicator_radius = indicator_radius - 4.0f * circle_radius; + const ImRect bb(pos, ImVec2(pos.x + indicator_radius * 2.0f, pos.y + indicator_radius * 2.0f)); + ItemSize(bb); + if (!ItemAdd(bb, id)) { + return; + } + const double t = g.Time; + const auto degree_offset = 2.0f * IM_PI / circle_count; + for (int i = 0; i < circle_count; ++i) { + const auto x = updated_indicator_radius * sin(degree_offset * i); + const auto y = updated_indicator_radius * cos(degree_offset * i); + + #define max(a,b) (((a)>(b))?(a):(b)) + + const auto growth = max(0.0f, sin(t * speed - i * degree_offset)); + ImVec4 color; + color.x = (float)(main_color.x * growth + backdrop_color.x * (1.0f - growth)); + color.y = (float)(main_color.y * growth + backdrop_color.y * (1.0f - growth)); + color.z = (float)(main_color.z * growth + backdrop_color.z * (1.0f - growth)); + color.w = 1.0f; + window->DrawList->AddCircleFilled(ImVec2((float)(pos.x + indicator_radius + x), + (float)(pos.y + indicator_radius - y)), + (float)(circle_radius + growth * circle_radius), GetColorU32(color)); + } +} + // Special button that doesn't take focus, doesn't take input owner, and can be activated without a click etc. // In order to reduce interferences with the contents we are trying to debug into. bool ImGui::DebugBreakButton(const char* label, const char* description_of_location) diff --git a/libs/imgui-1.92.1/imgui.h b/libs/imgui-1.92.1/imgui.h index a2b2a1f..29c89b9 100644 --- a/libs/imgui-1.92.1/imgui.h +++ b/libs/imgui-1.92.1/imgui.h @@ -375,6 +375,10 @@ IM_MSVC_RUNTIME_CHECKS_RESTORE namespace ImGui { + IMGUI_API void LoadingIndicatorCircle(const char* label, const float indicator_radius, + const ImVec4& main_color, const ImVec4& backdrop_color, + const int circle_count, const float speed); + // Context creation and access // - Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between contexts. // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() diff --git a/libs/xml.c/src/xml.c b/libs/xml.c/src/xml.c index 3601aa6..ffebdb5 100644 --- a/libs/xml.c/src/xml.c +++ b/libs/xml.c/src/xml.c @@ -1269,7 +1269,8 @@ char* xml_get_str(struct xml_node* root, char* buffer, size_t bufsize, char* chi memset(buffer, 0, bufsize); struct xml_string* str = xml_node_content(node); - xml_string_copy(str, (uint8_t *)buffer, xml_string_length(str)); + xml_string_copy(str, (uint8_t *)buffer, bufsize); + buffer[bufsize-1] = 0; return buffer; } @@ -1284,7 +1285,8 @@ char* xml_get_str_x(struct xml_node* root, char* buffer, size_t bufsize, char* c memset(buffer, 0, bufsize); struct xml_string* str = xml_node_content(node); - xml_string_copy(str, (uint8_t *)buffer, xml_string_length(str)); + xml_string_copy(str, (uint8_t *)buffer, bufsize); + buffer[bufsize-1] = 0; return buffer; } diff --git a/src/ai_providers/openAI.cpp b/src/ai_providers/openAI.cpp index e005a80..7675e8b 100644 --- a/src/ai_providers/openAI.cpp +++ b/src/ai_providers/openAI.cpp @@ -24,15 +24,13 @@ #include "httplib.h" #include "strops.hpp" #include "log.hpp" -#include "ai_service.hpp" +#include "import_service.hpp" static bool _openAI_query_with_file(char* query, size_t query_length, char* file_id, char** response) { - //#define TESTING_IMPORT - - #ifndef TESTING_IMPORT + #if 0 const char *api_key = administration_get_ai_service().api_key_public; - + httplib::SSLClient cli("api.openai.com", 443); //cli.enable_server_certificate_verification(false); @@ -57,16 +55,17 @@ static bool _openAI_query_with_file(char* query, size_t query_length, char* file } 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); strops_get_json_value(*response, "text", *response, 100000); *response = strops_unprep_str_from_json(*response); + #else + *response = (char*)malloc(100000); + memset(*response, 0, 100000); + strops_copy(*response, "<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\"> <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID> <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID> <cbc:ID>492043632</cbc:ID> <cbc:IssueDate>2024-09-01</cbc:IssueDate> <cbc:DueDate>2024-09-01</cbc:DueDate> <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode> <cbc:DocumentCurrencyCode>USD</cbc:DocumentCurrencyCode> <cac:DespatchDocumentReference> <cbc:ID>Final invoice</cbc:ID> </cac:DespatchDocumentReference> <cac:AdditionalDocumentReference> <cbc:ID></cbc:ID> <cbc:DocumentDescription></cbc:DocumentDescription> </cac:AdditionalDocumentReference> <cac:OrderReference> <cbc:ID></cbc:ID> </cac:OrderReference> <cac:ProjectReference> <cbc:ID>do:team:67840ecb-44e2-472e-bc45-801bd4e1f1fe</cbc:ID> </cac:ProjectReference> <cbc:AccountingCost></cbc:AccountingCost> <cac:AccountingSupplierParty> <cac:Party> <cbc:EndpointID schemeID=""></cbc:EndpointID> <cac:PartyIdentification> <cbc:ID schemeID=\"ZZZ\"></cbc:ID> </cac:PartyIdentification> <cac:PartyName> <cbc:Name>DigitalOcean LLC</cbc:Name> </cac:PartyName> <cac:PostalAddress> <cbc:StreetName>101 Avenue of the Americas</cbc:StreetName> <cbc:AdditionalStreetName>2nd Floor</cbc:AdditionalStreetName> <cbc:CityName>New York</cbc:CityName> <cbc:PostalZone>10013</cbc:PostalZone> <cbc:CountrySubentity>NY</cbc:CountrySubentity> <cac:Country> <cbc:IdentificationCode>US</cbc:IdentificationCode> </cac:Country> </cac:PostalAddress> <cac:PartyTaxScheme> <cbc:CompanyID>EU528002224</cbc:CompanyID> <cac:TaxScheme> <cbc:ID>VAT</cbc:ID> </cac:TaxScheme> </cac:PartyTaxScheme> <cac:PartyLegalEntity> <cbc:RegistrationName>DigitalOcean LLC</cbc:RegistrationName> </cac:PartyLegalEntity> <cac:Contact> <cbc:Name></cbc:Name> <cbc:Telephone></cbc:Telephone> <cbc:ElectronicMail></cbc:ElectronicMail> </cac:Contact> </cac:Party> </cac:AccountingSupplierParty> <cac:AccountingCustomerParty> <cac:Party> <cbc:EndpointID schemeID=""></cbc:EndpointID> <cac:PartyIdentification> <cbc:ID schemeID=\"ZZZ\"></cbc:ID> </cac:PartyIdentification> <cac:PartyName> <cbc:Name>My Team</cbc:Name> </cac:PartyName> <cac:PostalAddress> <cbc:StreetName>Keerderstraat 81</cbc:StreetName> <cbc:AdditionalStreetName></cbc:AdditionalStreetName> <cbc:CityName>Maastricht</cbc:CityName> <cbc:PostalZone>6226 XW</cbc:PostalZone> <cbc:CountrySubentity>LI</cbc:CountrySubentity> <cac:Country> <cbc:IdentificationCode>NL</cbc:IdentificationCode> </cac:Country> </cac:PostalAddress> <cac:PartyTaxScheme> <cbc:CompanyID></cbc:CompanyID> <cac:TaxScheme> <cbc:ID>VAT</cbc:ID> </cac:TaxScheme> </cac:PartyTaxScheme> <cac:PartyLegalEntity> <cbc:RegistrationName></cbc:RegistrationName> </cac:PartyLegalEntity> <cac:Contact> <cbc:Name></cbc:Name> <cbc:Telephone></cbc:Telephone> <cbc:ElectronicMail>aldrikboy@gmail.com</cbc:ElectronicMail> </cac:Contact> </cac:Party> </cac:AccountingCustomerParty> <cac:Delivery> <cbc:ActualDeliveryDate></cbc:ActualDeliveryDate> <cac:DeliveryLocation> <cac:Address> <cbc:StreetName></cbc:StreetName> <cbc:AdditionalStreetName></cbc:AdditionalStreetName> <cbc:CityName></cbc:CityName> <cbc:PostalZone></cbc:PostalZone> <cbc:CountrySubentity></cbc:CountrySubentity> <cac:Country> <cbc:IdentificationCode></cbc:IdentificationCode> </cac:Country> </cac:Address> </cac:DeliveryLocation> <cac:DeliveryParty> <cac:PartyName> <cbc:Name></cbc:Name> </cac:PartyName> </cac:DeliveryParty> </cac:Delivery> <cac:PaymentMeans> <cbc:PaymentMeansCode></cbc:PaymentMeansCode> <cbc:PaymentID>492043632</cbc:PaymentID> <cac:PayeeFinancialAccount> <cbc:ID></cbc:ID> <cbc:Name></cbc:Name> <cac:FinancialInstitutionBranch> <cac:FinancialInstitution> <cbc:ID></cbc:ID> </cac:FinancialInstitution> </cac:FinancialInstitutionBranch> </cac:PayeeFinancialAccount> <cac:PayerFinancialAccount> <cbc:ID></cbc:ID> </cac:PayerFinancialAccount> </cac:PaymentMeans> <cac:TaxTotal> <cbc:TaxAmount currencyID=\"USD\">3.49</cbc:TaxAmount> <cac:TaxSubtotal> <cbc:TaxableAmount currencyID=\"USD\">15.60</cbc:TaxableAmount> <cbc:TaxAmount currencyID=\"USD\">3.28</cbc:TaxAmount> <cac:TaxCategory> <cac:TaxScheme> <cbc:ID>VAT</cbc:ID> </cac:TaxScheme> </cac:TaxCategory> </cac:TaxSubtotal> <cac:TaxSubtotal> <cbc:TaxableAmount currencyID=\"USD\">1.00</cbc:TaxableAmount> <cbc:TaxAmount currencyID=\"USD\">0.21</cbc:TaxAmount> <cac:TaxCategory> <cac:TaxScheme> <cbc:ID>VAT</cbc:ID> </cac:TaxScheme> </cac:TaxCategory> </cac:TaxSubtotal> </cac:TaxTotal> <cac:LegalMonetaryTotal> <cbc:LineExtensionAmount currencyID=\"USD\">16.60</cbc:LineExtensionAmount> <cbc:TaxExclusiveAmount currencyID=\"USD\">16.60</cbc:TaxExclusiveAmount> <cbc:TaxInclusiveAmount currencyID=\"USD\">20.09</cbc:TaxInclusiveAmount> <cbc:PayableAmount currencyID=\"USD\">20.09</cbc:PayableAmount> </cac:LegalMonetaryTotal> <cac:InvoiceLine> <cbc:ID>1</cbc:ID> <cbc:InvoicedQuantity unitCode=""></cbc:InvoicedQuantity> <cbc:LineExtensionAmount currencyID=\"USD\">16.60</cbc:LineExtensionAmount> <cac:AllowanceCharge> <cbc:ChargeIndicator>false</cbc:ChargeIndicator> <cbc:AllowanceChargeReason>Discount</cbc:AllowanceChargeReason> <cbc:MultiplierFactorNumeric></cbc:MultiplierFactorNumeric> <cbc:Amount currencyID=\"USD\"></cbc:Amount> <cbc:BaseAmount currencyID=\"USD\"></cbc:BaseAmount> </cac:AllowanceCharge> <cac:Item> <cbc:Name>Product Usage Charges</cbc:Name> <cac:AdditionalItemProperty> <cbc:Name>Internal Tax Rate ID</cbc:Name> <cbc:Value></cbc:Value> </cac:AdditionalItemProperty> <cac:ClassifiedTaxCategory> <cbc:ID></cbc:ID> <cbc:Percent></cbc:Percent> <cac:TaxScheme> <cbc:ID>VAT</cbc:ID> </cac:TaxScheme> </cac:ClassifiedTaxCategory> </cac:Item> <cac:Price> <cbc:PriceAmount currencyID=\"USD\"></cbc:PriceAmount> </cac:Price> </cac:InvoiceLine></Invoice>", 100000); + #endif return 1; } @@ -98,6 +97,7 @@ static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_l httplib::Result res = cli.Post("/v1/uploads", headers, body, "application/json"); if (!res || res->status != 200) { log_error("ERROR Failed to create upload."); + log_error(res->body.c_str()); fclose(orig_file); return 0; } @@ -133,6 +133,7 @@ static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_l if (!part_res || part_res->status != 200) { log_error("Failed to upload part %d.", part_number); + log_error(part_res->body.c_str()); free(buffer); fclose(orig_file); return 0; @@ -161,6 +162,7 @@ static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_l if (!complete_res || complete_res->status != 200) { log_error("ERROR Failed to complete upload."); + log_error(complete_res->body.c_str()); return 0; } diff --git a/src/ai_service.cpp b/src/import_service.cpp index 82d81d6..b7a519c 100644 --- a/src/ai_service.cpp +++ b/src/import_service.cpp @@ -19,13 +19,15 @@ #include <fstream> #include <iostream> #include <string> +#include <threads.h> #define CPPHTTPLIB_OPENSSL_SUPPORT #include "httplib.h" #include "log.hpp" -#include "ai_service.hpp" +#include "import_service.hpp" #include "strops.hpp" #include "administration_reader.hpp" +#include "locales.hpp" ai_provider_impl _ai_get_impl() { @@ -43,15 +45,22 @@ ai_provider_impl _ai_get_impl() extern const char* peppol_invoice_template; extern const char* peppol_invoice_line_template; -ai_request* ai_document_to_invoice(char* file_path) -{ +static int _ai_document_to_invoice_t(void *arg) { + import_invoice_request* request = (import_invoice_request*)arg; + char* file_path = request->file_path; ai_provider_impl impl = _ai_get_impl(); + request->status = import_status::IMPORT_UPLOADING_FILE; + char file_id[100]; if (!impl.upload_file(file_path, file_id, 100)) { + request->status = import_status::IMPORT_DONE; + request->error = I_ERR_FAILED_UPLOAD; return 0; } + request->status = import_status::IMPORT_QUERYING; + size_t query_buffer_len = 50000; char* template_buffer = (char*)malloc(query_buffer_len); memset(template_buffer, 0, query_buffer_len); @@ -67,29 +76,77 @@ ai_request* ai_document_to_invoice(char* file_path) size_t query_len = strlen(template_buffer); strncpy(template_buffer + query_len, ai_query, query_buffer_len - query_len); + request->status = import_status::IMPORT_WAITING_FOR_RESPONSE; + char* response; if (!impl.query_with_file(template_buffer, query_buffer_len, file_id, &response)) { + request->status = import_status::IMPORT_DONE; + request->error = I_ERR_FAILED_QUERY; return 0; } invoice inv; if (!administration_reader_read_invoice_from_xml(&inv, response, strlen(response))) { - return false; + request->status = import_status::IMPORT_DONE; + request->error = I_ERR_FAILED_IMPORT; + return 0; } invoice tmp = administration_invoice_create_empty(); 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 + contact my_info = administration_company_info_get(); + memcpy(&inv.customer, &my_info, sizeof(contact)); + strops_copy(inv.customer.id, MY_COMPANY_ID, MAX_LEN_ID); strops_copy(inv.document.original_path, file_path, MAX_LEN_PATH); strops_copy(inv.document.copy_path, "", MAX_LEN_PATH); - a_err result = administration_invoice_import(&inv); - free(template_buffer); free(response); + request->status = import_status::IMPORT_DONE; + request->result = administration_invoice_create_copy(&inv); return 0; +} + +import_invoice_request* ai_document_to_invoice(char* file_path) +{ + import_invoice_request* result = (import_invoice_request*)malloc(sizeof(import_invoice_request)); + result->started_at = time(NULL); + result->error = I_ERR_SUCCESS; + result->status = import_status::IMPORT_STARTING; + strops_copy(result->file_path, file_path, MAX_LEN_PATH); + + thrd_t thr; + if (thrd_create(&thr, _ai_document_to_invoice_t, result) != thrd_success) { + return 0; + } + + return result; +} + +const char* import_status_to_str(import_status status) +{ + switch(status) + { + case import_status::IMPORT_STARTING: return localize("import.status.starting"); + case import_status::IMPORT_UPLOADING_FILE: return localize("import.status.uploading_file"); + case import_status::IMPORT_QUERYING: return localize("import.status.querying"); + case import_status::IMPORT_WAITING_FOR_RESPONSE: return localize("import.status.waiting_for_result"); + case import_status::IMPORT_DONE: return localize("import.status.done"); + } + return ""; +} + +const char* import_error_to_str(i_err error) +{ + switch(error) + { + case I_ERR_FAILED_UPLOAD: return localize("import.error.upload"); + case I_ERR_FAILED_QUERY: return localize("import.error.query"); + case I_ERR_FAILED_IMPORT: return localize("import.error.import"); + } + return ""; }
\ No newline at end of file diff --git a/src/locales/en.cpp b/src/locales/en.cpp index 0152928..d311b36 100644 --- a/src/locales/en.cpp +++ b/src/locales/en.cpp @@ -196,6 +196,17 @@ locale_entry en_locales[] = { {"statement.tax", "Tax"}, {"statement.expenses", "Expenses"}, {"statement.profit", "Profit"}, + + // Import service. + {"import.status.starting","Starting import"}, + {"import.status.uploading_file","Uploading file"}, + {"import.status.querying","Querying AI provider"}, + {"import.status.waiting_for_result","Waiting for result"}, + {"import.status.done","Import completed"}, + + {"import.error.upload","Failure: Upload failed"}, + {"import.error.query","Failure: Querying service failed"}, + {"import.error.import","Failure: Failed to import result from service"}, }; const int en_locale_count = sizeof(en_locales) / sizeof(en_locales[0]);
\ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index bdddd98..8475008 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -129,7 +129,7 @@ int main(int argc, char** argv) io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf"); //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\seguisym.ttf"); fontBold = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeuib.ttf"); - fontBig = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeuib.ttf", 36); + fontBig = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeuib.ttf", 30); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf"); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf"); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf"); diff --git a/src/ui/ui_expenses.cpp b/src/ui/ui_expenses.cpp index 153e6b7..c02cdce 100644 --- a/src/ui/ui_expenses.cpp +++ b/src/ui/ui_expenses.cpp @@ -27,7 +27,9 @@ #include "administration.hpp" #include "administration_writer.hpp" #include "locales.hpp" -#include "ai_service.hpp" +#include "import_service.hpp" + +static import_invoice_request* active_import_request = 0; static view_state current_view_state = view_state::LIST; static invoice active_invoice = {0}; @@ -46,7 +48,13 @@ void ui_destroy_expenses() void ui_setup_expenses() { - current_view_state = view_state::LIST; + if (active_import_request != 0) { + current_view_state = view_state::VIEW_IMPORT_REQUEST; + } + else { + current_view_state = view_state::LIST; + } + active_invoice = administration_invoice_create_empty(); u32 invoice_items_count = MAX_BILLING_ITEMS; @@ -176,13 +184,13 @@ 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::VIEW_IMPORT_REQUEST; active_invoice = administration_invoice_create_empty(); // @leak active_invoice.customer = administration_company_info_get(); active_invoice.is_outgoing = 0; active_invoice.status = invoice_status::INVOICE_RECEIVED; - ai_document_to_invoice(import_file_path); + active_import_request = ai_document_to_invoice(import_file_path); } if (current_page >= max_page-1) current_page = max_page-1; @@ -336,6 +344,50 @@ static void ui_draw_expense_view() draw_expense_form(&active_invoice, true); } +static void ui_draw_import_request() +{ + assert(active_import_request); + + if (active_import_request->status == import_status::IMPORT_DONE) { + if (active_import_request->error == I_ERR_SUCCESS) { + active_invoice = active_import_request->result; + current_view_state = view_state::CREATE; + active_import_request = 0; + return; + } + else { + if (ImGui::Button(localize("form.back"))) { + current_view_state = view_state::LIST; + active_import_request = 0; + return; + } + } + } + + ImGui::PushFont(fontBig); + + ImVec2 windowSize = ImGui::GetWindowSize(); + float radius = 60.0f; + + const char* text = import_status_to_str(active_import_request->status); + if (active_import_request->error != I_ERR_SUCCESS) text = import_error_to_str(active_import_request->error); + ImVec2 textSize = ImGui::CalcTextSize(text); + ImGui::SetCursorPos(ImVec2((windowSize.x - textSize.x) * 0.5f, + (windowSize.y) * 0.5f - radius - 40.0f)); + ImGui::Text(text); + + if (active_import_request->error == I_ERR_SUCCESS) { + ImGui::SetCursorPos(ImVec2((windowSize.x - radius*2) * 0.5f, + (windowSize.y - radius*2) * 0.5f)); + + const ImVec4 col = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + const ImVec4 bg = ImGui::GetStyleColorVec4(ImGuiCol_Button); + ImGui::LoadingIndicatorCircle("##loadingAnim", radius, bg, col, 10, 4.0f); + } + + ImGui::PopFont(); +} + void ui_draw_expenses() { switch(current_view_state) @@ -344,5 +396,6 @@ void ui_draw_expenses() case view_state::CREATE: ui_draw_expense_create(); break; case view_state::EDIT: ui_draw_expense_update(); break; case view_state::VIEW: ui_draw_expense_view(); break; + case view_state::VIEW_IMPORT_REQUEST: ui_draw_import_request(); break; } }
\ No newline at end of file |
