summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ai_providers/openAI.cpp78
-rw-r--r--src/importer.cpp263
-rw-r--r--src/strops.cpp4
-rw-r--r--src/ui/ui_expenses.cpp9
-rw-r--r--src/ui/ui_settings.cpp2
5 files changed, 279 insertions, 77 deletions
diff --git a/src/ai_providers/openAI.cpp b/src/ai_providers/openAI.cpp
index fa2cc05..64e7a6a 100644
--- a/src/ai_providers/openAI.cpp
+++ b/src/ai_providers/openAI.cpp
@@ -14,6 +14,8 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
+#include <threads.h>
+
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include "httplib.h"
@@ -22,9 +24,77 @@
#include "logger.hpp"
#include "importer.hpp"
+static bool _openAI_batch_query_with_file(char** queries, size_t query_count, char* file_id, invoice* buffer, importer::batch_query_response_handler response_handler)
+{
+ const char *api_key = administration::get_ai_service().api_key_public;
+ httplib::SSLClient cli("api.openai.com", 443);
+
+ thrd_t threads[20];
+
+ for (u32 i = 0; i < query_count; i++)
+ {
+ auto* func = new auto([&api_key, &cli, i, &file_id, &response_handler, &buffer, &queries]() {
+ char* query_escaped = strops::prep_str_for_json(queries[i], 1000);
+
+ size_t body_size = 1000; // Ballpark
+ char* body = (char*)memops::alloc(body_size);
+ strops::format(body, body_size,
+ "{ \"model\":\"%s\", "
+ " \"input\": [ "
+ " { \"role\": \"user\", "
+ " \"content\": [ "
+ " { \"type\": \"input_file\", \"file_id\": \"%s\" }, "
+ " { \"type\": \"input_text\", \"text\": \"%s\" } "
+ " ] "
+ " }"
+ "], "
+ " \"text\": { \"format\": { \"type\": \"json_object\" } } "
+ "}", administration::get_ai_service().model_name, 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");
+ memops::unalloc(body);
+
+ if (!res || res->status != 200) {
+ logger::error("ERROR Failed to query API.");
+ logger::error(res->body.c_str());
+ return 0;
+ }
+
+ char* response_body = (char*)res->body.c_str();
+ char* response = (char*)memops::alloc(5000);
+ memops::zero(response, 5000);
+ strops::copy(response, response_body, 5000);
+
+ strops::get_json_value(response, "text", response, 5000);
+ strops::unprep_str_from_json(response);
+
+ response_handler(buffer, response);
+
+ memops::unalloc(response);
+ memops::unalloc(query_escaped);
+ return 1;
+ });
+
+ auto trampoline = [](void* arg) -> int {
+ auto* f = static_cast<decltype(func)>(arg);
+ (*f)();
+ delete f;
+ return 0;
+ };
+
+ thrd_create(&threads[i], trampoline, func);
+ }
+
+ for (u32 i = 0; i < query_count; i++) thrd_join(threads[i], nullptr);
+
+ return 1;
+}
+
static bool _openAI_query_with_file(char* query, size_t query_length, char* file_id, char** response)
{
- #if 1
const char *api_key = administration::get_ai_service().api_key_public;
httplib::SSLClient cli("api.openai.com", 443);
@@ -58,11 +128,6 @@ static bool _openAI_query_with_file(char* query, size_t query_length, char* file
strops::get_json_value(*response, "text", *response, 100000);
*response = strops::unprep_str_from_json(*response);
- #else
- *response = (char*)memops::alloc(100000);
- memops::zero(*response, 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;
}
@@ -205,5 +270,6 @@ importer::ai_provider_impl _chatgpt_api_provider = {
"gpt-5-nano",
_openAI_upload_file,
_openAI_query_with_file,
+ _openAI_batch_query_with_file,
_openAI_get_available_models,
}; \ No newline at end of file
diff --git a/src/importer.cpp b/src/importer.cpp
index 194913a..4f78d20 100644
--- a/src/importer.cpp
+++ b/src/importer.cpp
@@ -34,20 +34,182 @@ importer::ai_provider_impl importer::get_ai_provider_implementation(ai_provider
switch(provider)
{
case AI_PROVIDER_OPENAI: return _chatgpt_api_provider;
- case AI_PROVIDER_DEEPSEEK: return _deepseek_api_provider;
+ //case AI_PROVIDER_DEEPSEEK: return _deepseek_api_provider;
default: assert(0); break;
}
return importer::ai_provider_impl {0};
}
-static int _ai_document_to_invoice_t(void *arg) {
+static void _batch_query_response_handler(invoice* buffer, char* json)
+{
+ int alloc_size = 1000;
+ char* rb = (char*)memops::alloc(alloc_size);
+ strops::get_json_value(json, "query_id", rb, alloc_size);
+
+ if (strops::equals(rb, "-1")) return; // ignore
+
+ else if (strops::equals(rb, "1")) {
+ strops::get_json_value(json, "sequential_number", rb, alloc_size);
+ strops::copy(buffer->sequential_number, rb, sizeof(buffer->sequential_number));
+ }
+ else if (strops::equals(rb, "2")) {
+ strops::get_json_value(json, "issued_at", rb, alloc_size);
+ buffer->issued_at = strtoll(rb, NULL, 10);
+ }
+ else if (strops::equals(rb, "3")) {
+ strops::get_json_value(json, "expires_at", rb, alloc_size);
+ buffer->expires_at = strtoll(rb, NULL, 10);
+ }
+ else if (strops::equals(rb, "4")) {
+ strops::get_json_value(json, "currency_code", rb, alloc_size);
+ administration::invoice_set_currency(buffer, rb);
+ }
+ else if (strops::equals(rb, "5")) {
+ strops::get_json_value(json, "address1", rb, alloc_size);
+ strops::copy(buffer->supplier.address.address1, rb, sizeof(buffer->supplier.address.address1));
+
+ strops::get_json_value(json, "address2", rb, alloc_size);
+ strops::copy(buffer->supplier.address.address2, rb, sizeof(buffer->supplier.address.address2));
+
+ strops::get_json_value(json, "city", rb, alloc_size);
+ strops::copy(buffer->supplier.address.city, rb, sizeof(buffer->supplier.address.city));
+
+ strops::get_json_value(json, "postal", rb, alloc_size);
+ strops::copy(buffer->supplier.address.postal, rb, sizeof(buffer->supplier.address.postal));
+
+ strops::get_json_value(json, "region", rb, alloc_size);
+ strops::copy(buffer->supplier.address.region, rb, sizeof(buffer->supplier.address.region));
+
+ strops::get_json_value(json, "country_code", rb, alloc_size);
+ strops::copy(buffer->supplier.address.country_code, rb, sizeof(buffer->supplier.address.country_code));
+
+ strops::get_json_value(json, "is_business", rb, alloc_size);
+ buffer->supplier.type = (strops::equals(rb, "true")) ? contact_type::CONTACT_BUSINESS : contact_type::CONTACT_CONSUMER;
+
+ strops::get_json_value(json, "name", rb, alloc_size);
+ strops::copy(buffer->supplier.name, rb, sizeof(buffer->supplier.name));
+
+ strops::get_json_value(json, "taxid", rb, alloc_size);
+ strops::copy(buffer->supplier.taxid, rb, sizeof(buffer->supplier.taxid));
+
+ strops::get_json_value(json, "businessid", rb, alloc_size);
+ strops::copy(buffer->supplier.businessid, rb, sizeof(buffer->supplier.businessid));
+
+ strops::get_json_value(json, "email", rb, alloc_size);
+ strops::copy(buffer->supplier.email, rb, sizeof(buffer->supplier.email));
+
+ strops::get_json_value(json, "phone_number", rb, alloc_size);
+ strops::copy(buffer->supplier.phone_number, rb, sizeof(buffer->supplier.phone_number));
+
+ strops::get_json_value(json, "bank_account", rb, alloc_size);
+ strops::copy(buffer->supplier.bank_account, rb, sizeof(buffer->supplier.bank_account));
+ }
+ else if (strops::equals(rb, "6")) {
+ strops::get_json_value(json, "address1", rb, alloc_size);
+ strops::copy(buffer->customer.address.address1, rb, sizeof(buffer->customer.address.address1));
+
+ strops::get_json_value(json, "address2", rb, alloc_size);
+ strops::copy(buffer->customer.address.address2, rb, sizeof(buffer->customer.address.address2));
+
+ strops::get_json_value(json, "city", rb, alloc_size);
+ strops::copy(buffer->customer.address.city, rb, sizeof(buffer->customer.address.city));
+
+ strops::get_json_value(json, "postal", rb, alloc_size);
+ strops::copy(buffer->customer.address.postal, rb, sizeof(buffer->customer.address.postal));
+
+ strops::get_json_value(json, "region", rb, alloc_size);
+ strops::copy(buffer->customer.address.region, rb, sizeof(buffer->customer.address.region));
+
+ strops::get_json_value(json, "country_code", rb, alloc_size);
+ strops::copy(buffer->customer.address.country_code, rb, sizeof(buffer->customer.address.country_code));
+
+ strops::get_json_value(json, "is_business", rb, alloc_size);
+ buffer->customer.type = (strops::equals(rb, "true")) ? contact_type::CONTACT_BUSINESS : contact_type::CONTACT_CONSUMER;
+
+ strops::get_json_value(json, "name", rb, alloc_size);
+ strops::copy(buffer->customer.name, rb, sizeof(buffer->customer.name));
+
+ strops::get_json_value(json, "taxid", rb, alloc_size);
+ strops::copy(buffer->customer.taxid, rb, sizeof(buffer->customer.taxid));
+
+ strops::get_json_value(json, "businessid", rb, alloc_size);
+ strops::copy(buffer->customer.businessid, rb, sizeof(buffer->customer.businessid));
+
+ strops::get_json_value(json, "email", rb, alloc_size);
+ strops::copy(buffer->customer.email, rb, sizeof(buffer->customer.email));
+
+ strops::get_json_value(json, "phone_number", rb, alloc_size);
+ strops::copy(buffer->customer.phone_number, rb, sizeof(buffer->customer.phone_number));
+
+ strops::get_json_value(json, "bank_account", rb, alloc_size);
+ strops::copy(buffer->customer.bank_account, rb, sizeof(buffer->customer.bank_account));
+ }
+ else if (strops::equals(rb, "7")) {
+ strops::get_json_value(json, "address1", rb, alloc_size);
+ strops::copy(buffer->addressee.address.address1, rb, sizeof(buffer->addressee.address.address1));
+
+ strops::get_json_value(json, "address2", rb, alloc_size);
+ strops::copy(buffer->addressee.address.address2, rb, sizeof(buffer->addressee.address.address2));
+
+ strops::get_json_value(json, "city", rb, alloc_size);
+ strops::copy(buffer->addressee.address.city, rb, sizeof(buffer->addressee.address.city));
+
+ strops::get_json_value(json, "postal", rb, alloc_size);
+ strops::copy(buffer->addressee.address.postal, rb, sizeof(buffer->addressee.address.postal));
+
+ strops::get_json_value(json, "region", rb, alloc_size);
+ strops::copy(buffer->addressee.address.region, rb, sizeof(buffer->addressee.address.region));
+
+ strops::get_json_value(json, "country_code", rb, alloc_size);
+ strops::copy(buffer->addressee.address.country_code, rb, sizeof(buffer->addressee.address.country_code));
+
+ strops::get_json_value(json, "name", rb, alloc_size);
+ strops::copy(buffer->addressee.name, rb, sizeof(buffer->addressee.name));
+ }
+ else if (strops::equals(rb, "8")) {
+ strops::get_json_value(json, "item_count", rb, alloc_size);
+ u32 item_count = strtol(rb, NULL, 10);
+
+ for (u32 i = 0; i < item_count; i++)
+ {
+ billing_item item = administration::billing_item_create_empty();
+ strops::get_json_value(json, "amount", rb, alloc_size, i);
+ item.amount = strtof(rb, NULL);
+
+ strops::get_json_value(json, "amount_is_percentage", rb, alloc_size, i);
+ item.amount_is_percentage = strops::equals(rb, "true");
+
+ strops::get_json_value(json, "description", rb, alloc_size, i);
+ strops::copy(item.description, rb, sizeof(item.description));
+
+ strops::get_json_value(json, "price_per_item", rb, alloc_size, i);
+ item.net_per_item = strtof(rb, NULL);
+
+ strops::get_json_value(json, "discount", rb, alloc_size, i);
+ item.discount = strtof(rb, NULL);
+
+ strops::get_json_value(json, "discount_is_percentage", rb, alloc_size, i);
+ item.discount_is_percentage = strops::equals(rb, "true");
+
+ administration::billing_item_add_to_invoice(buffer, item);
+ }
+
+ }
+
+ memops::unalloc(rb);
+
+ return;
+}
+
+static int _ai_document_to_invoice_t(void *arg)
+{
importer::invoice_request* request = (importer::invoice_request*)arg;
char* file_path = request->file_path;
importer::ai_provider_impl impl = importer::get_ai_provider_implementation(administration::get_ai_service().provider);
request->status = importer::status::IMPORT_UPLOADING_FILE;
-
+
char file_id[100];
if (!impl.upload_file(file_path, file_id, 100)) {
request->status = importer::status::IMPORT_DONE;
@@ -55,68 +217,49 @@ static int _ai_document_to_invoice_t(void *arg) {
return 0;
}
- request->status = importer::status::IMPORT_QUERYING;
-
- size_t query_buffer_len = 50000;
- char* template_buffer = (char*)memops::alloc(query_buffer_len);
- memops::zero(template_buffer, query_buffer_len);
-
- strops::copy(template_buffer, file_template::peppol_invoice_template, query_buffer_len);
- strops::replace(template_buffer, 50000, "{{INVOICE_LINE_LIST}}", file_template::peppol_invoice_line_template);
-
- 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.\n"
- "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.\n"
- "Only return the filled out template in valid XML format. Nothing else.\n\n"
- "{{LINE_AMOUNT}} equals the net paid amount for an order line.\n"
- "{{UNIT_PRICE}} is the net price per unit in an order line.\n"
- "{{QUANTITY}} is the amount of units per order line. If this is not defined, default to 1.\n"
- "If {{UNIT_PRICE}} is less than 1.00 and {{QUANTITY}} is more than 10, {{QUANTITY}} should equal 1, {{UNIT_PRICE}} should equal {{LINE_AMOUNT}} and {{ITEM_NAME}} should include the original {{QUANTITY}}.\n" // High quantity, small price, might result in incorrect unit price. e.g. 700x resistor for 2,00 total.
- "{{UNIT_CODE}} should always be 'X' unless you know for sure know the line item amount is defined as a percentage, in which case it should be '%'.\n"
- "Every invoice line will atleast have {{LINE_AMOUNT}} and {{ITEM_NAME}} defined.\n"
- "{{LINE_TAX_PERCENT}} is the tax rate for the line item. This could also be described as VAT rate. Often an invoice only has 1 tax rate defined intstead of per line item.\n"
- "{{LINE_TAX_ID}} should be set to the country code and tax rate, in the format 'CC/PP' where CC is the 2 letter country code and PP is the tax rate as a number with 2 decimals.\n"
- "If a line item is taxted with vat reverse Charge, {{LINE_TAX_ID}} should be set to '00/AE'.\n"
- "If a line item is exempt from Tax, {{LINE_TAX_ID}} should be set to '00/E'.\n"
- "If a line item is categorized as zero rated goods, {{LINE_TAX_ID}} should be set to '00/Z'.\n"
- "If a line item is a service outside scope of tax, {{LINE_TAX_ID}} should be set to '00/O'.\n"
- "If a line item is VAT exempt for EEA intra-community supply of goods and services, {{LINE_TAX_ID}} should be set to '00/K'.\n"
- "All of there tax rates can be declared as per line item, or per invoice.\n"
- "If you can find the tax rate for 1 line item but not another, assume they are taxed at the same rate and their {{LINE_TAX_ID}} should match.\n"
- "If shipping costs are provided, these should also be added to the result as a cac:InvoiceLine.\n"
- "{{INVOICE_SEQUENCE_ID}} should be set to the provided invoice id or invoice number. This is always defined.\n"
- "{{ISSUE_DATE}} is the date the invoice was issued and should be stored in format 'YYYY-MM-DD'.\n"
- "{{DUE_DATE}} is the date the invoice is due and should be stored in format 'YYYY-MM-DD'. If the due date is not defined, {{DUE_DATE}} should equal 0.\n"
- "{{DELIVERY_DATE}} might be defined and should be stored in format 'YYYY-MM-DD'. If the delivery date is not defined, {{DELIVERY_DATE}} should equal 0.\n"
- "cac:AccountingSupplierParty contains all information of the supplier. This information might be under the section 'Supplier', 'Seller', 'Sold by' or something similar.\n"
- "cac:AccountingCustomerParty contains all information of the customer. This information might be under the section 'Customer', 'Ordered by', 'Billing address' or something similar.\n"
- "cac:Delivery contains the delivery address for physical goods. This information might be under the section 'Shipping address', 'Shipped to' or something similar. If this is not explicitly set, leave this section empty.\n"
- ;
-
- size_t query_len = strops::length(template_buffer);
- strops::copy(template_buffer + query_len, ai_query, query_buffer_len - query_len);
-
request->status = importer::status::IMPORT_WAITING_FOR_RESPONSE;
- char* response;
- if (!impl.query_with_file(template_buffer, query_buffer_len, file_id, &response)) {
+ char* queries[] = {
+ "What is the invoice number/ID? Return json containing sequential_number (string), query_id = 1 (string)",
+ "When was this invoice issued? Return json containing issued_at (time_t value), query_id = 2 (string). If not found, issued_at = 0",
+ "When does this invoice expire? Return json containing expires_at (time_t value), query_id = 3 (string). If not found, expires_at = 0",
+ "What currency is this invoice issued in? Look for a currency symbol in one of the billed items. Return json containing currency_code (3 letter code string), query_id = 4 (string)",
+
+ "Who sent the invoice? This information might be under the section 'From, 'Supplier', 'Seller', 'Sold by' or something similar. Return json containing query_id = 5 (string), "
+ " address1 (string, address line 1), address2 (string, address line 2), "
+ " city (string), postal (string), region (string), country_code (string, 2 letter code), is_business (string, 'true' or 'false'), "
+ " name (string), taxid (string, tax identifier number), businessid (string, business identifier number), email (string), "
+ " phone_number (string), bank_account (string)",
+
+ "Who received the invoice? This information might be under the section 'Customer', 'Ordered by', 'Billing address' or something similar. Return json containing query_id = 6 (string), "
+ " address1 (string, address line 1), address2 (string, address line 2), "
+ " city (string), postal (string), region (string), country_code (string, 2 letter code), is_business (string, 'true' or 'false'), "
+ " name (string), taxid (string, tax identifier number), businessid (string, business identifier number), email (string), "
+ " phone_number (string), bank_account (string)",
+
+ "Who received the product? This information might be under the section 'Delivered to', 'shipping address' or something similar. Return json containing query_id = 7 (string), "
+ " address1 (string, address line 1), address2 (string, address line 2), "
+ " city (string), postal (string), region (string), country_code (string, 2 letter code), name (string). "
+ " If the delivery address is not provided, return query_id = -1 (string)",
+
+ "Give me a list of billed items in json format. Return query_id = 8 (string), item_count (string), items (array). For each item, get: "
+ " amount (string, number of items in billing line, default to 1), amount_is_percentage (string, 'true' or 'false'), description (string), price_per_item (string, price with 2 decimals, no currency symbol), "
+ " discount (string, price with 2 decimals, no currency symbol), discount_is_percentage (string, 'true' or 'false')"
+ };
+
+ invoice inv = administration::invoice_create_empty();
+
+ if (!impl.batch_query_with_file(queries, sizeof(queries) / sizeof(char*), file_id, &inv, _batch_query_response_handler)) {
request->status = importer::status::IMPORT_DONE;
request->error = I_ERR_FAILED_QUERY;
return 0;
}
- invoice inv;
- if (!administration_reader::read_invoice_from_xml(&inv, response, strops::length(response))) {
- request->status = importer::status::IMPORT_DONE;
- request->error = I_ERR_FAILED_IMPORT;
- return 0;
- }
-
inv.status = invoice_status::INVOICE_RECEIVED;
// Set customer or supplier depending on incomming or outgoing.
contact my_info = administration::company_info_get();
- memops::copy(&inv.customer, &my_info, sizeof(contact));
+ //memops::copy(&inv.customer, &my_info, sizeof(contact));
strops::copy(inv.customer.id, MY_COMPANY_ID, MAX_LEN_ID);
// Project and cost centers cannot be interpreted from file so are set to 0.
@@ -127,18 +270,6 @@ static int _ai_document_to_invoice_t(void *arg) {
strops::copy(inv.document.original_path, file_path, MAX_LEN_PATH);
strops::copy(inv.document.copy_path, "", MAX_LEN_PATH);
- // Set dates.
- if (inv.expires_at == 0) {
- inv.expires_at = inv.issued_at + administration::get_default_invoice_expire_duration();
- }
-
- if (inv.delivered_at == 0) {
- inv.delivered_at = inv.issued_at;
- }
-
- memops::unalloc(template_buffer);
- memops::unalloc(response);
-
request->status = importer::status::IMPORT_DONE;
request->result = administration::invoice_create_copy(&inv);
return 0;
diff --git a/src/strops.cpp b/src/strops.cpp
index 2dfbf16..798e57e 100644
--- a/src/strops.cpp
+++ b/src/strops.cpp
@@ -162,10 +162,10 @@ namespace strops {
pos++;
// Skip whitespace and quotes
- while (*pos == ' ' || *pos == '\"') pos++;
+ while (*pos == ' ' || (*pos == '\"' && *(pos+1) != '\"')) pos++;
size_t i = 0;
- while (*pos && !(*pos == '\"' && *(pos-1) != '\\') && i < out_size - 1) {
+ while (*pos /*&& *pos != '}'*/ && !(*pos == '\"' && *(pos-1) != '\\') && i < out_size - 1) {
out[i++] = *pos++;
}
out[i] = '\0';
diff --git a/src/ui/ui_expenses.cpp b/src/ui/ui_expenses.cpp
index 560eb90..6a4eebd 100644
--- a/src/ui/ui_expenses.cpp
+++ b/src/ui/ui_expenses.cpp
@@ -64,7 +64,7 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
if (viewing_only) ImGui::BeginDisabled();
ImGui::Text("%s: %s", locale::get("invoice.form.invoicenumber"), buffer->sequential_number);
- ImGui::Text("%s: %s", locale::get("invoice.form.billedTo"), buffer->customer.name);
+ //ImGui::Text("%s: %s", locale::get("invoice.form.billedTo"), buffer->customer.name);
tm issued_at_date = *gmtime(&buffer->issued_at);
if (ImGui::DatePicker("##issuedAt", issued_at_date))
@@ -98,6 +98,11 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
ImGui::Separator();
+ ImGui::Text(locale::get("invoice.form.billinginformation"));
+ draw_contact_form_ex(&buffer->customer, false, true);
+
+ ImGui::Separator();
+
ImGui::Text(locale::get("invoice.form.supplier"));
draw_contact_form_ex(&buffer->supplier, false, true);
@@ -181,7 +186,7 @@ static void draw_expenses_list()
char import_file_path[MAX_LEN_PATH] = {0};
ImGui::SameLine();
- if (ImGui::FormInvoiceFileSelector("+ Import", import_file_path)) { // @locale::get
+ if (ImGui::FormInvoiceFileSelector("+ Import", import_file_path)) { // @localize
current_view_state = ui::view_state::VIEW_IMPORT_REQUEST;
active_invoice = administration::invoice_create_empty(); // @leak
active_invoice.customer = administration::company_info_get();
diff --git a/src/ui/ui_settings.cpp b/src/ui/ui_settings.cpp
index c1a27a2..51beafe 100644
--- a/src/ui/ui_settings.cpp
+++ b/src/ui/ui_settings.cpp
@@ -249,7 +249,7 @@ static void draw_services()
if (ImGui::BeginCombo(locale::get("settings.services.ai_service.provider"), ai_service_names[new_service.provider]))
{
- for (u32 n = 0; n < 2; n++)
+ for (u32 n = 0; n < AI_PROVIDER_END; n++)
{
bool is_selected = n == (uint32_t)new_service.provider;
if (ImGui::Selectable(ai_service_names[n], is_selected)) {