diff options
| author | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-10-04 15:54:30 +0200 |
|---|---|---|
| committer | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-10-04 15:54:30 +0200 |
| commit | 2855642dd16cea260f3b32351f0529328a0bcb15 (patch) | |
| tree | a375d2c57e1089ba54746371674e48c9216bb8e4 /src/importer.cpp | |
| parent | b8e049f02a16dc8398b2a954b561c84beda423c7 (diff) | |
namespacing locale, config, file_templates
Diffstat (limited to 'src/importer.cpp')
| -rw-r--r-- | src/importer.cpp | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/src/importer.cpp b/src/importer.cpp new file mode 100644 index 0000000..404ee8c --- /dev/null +++ b/src/importer.cpp @@ -0,0 +1,188 @@ +/* +* 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. +*/ + +#define _CRT_SECURE_NO_WARNINGS + +#include <fstream> +#include <iostream> +#include <string> +#include <threads.h> + +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include "log.hpp" +#include "importer.hpp" +#include "strops.hpp" +#include "administration_reader.hpp" +#include "locales.hpp" +#include "file_templates.hpp" + +ai_provider_impl _ai_get_impl() +{ + ai_provider provider = administration_get_ai_service().provider; + + switch(provider) + { + case AI_PROVIDER_OPENAI: return _chatgpt_api_provider; + case AI_PROVIDER_DEEPSEEK: return _deepseek_api_provider; + default: assert(0); break; + } + + return ai_provider_impl {0}; +} + +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); + + strncpy(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 = 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))) { + request->status = import_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(); + memcpy(&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. + strops_copy(inv.project_id, "", MAX_LEN_ID); + strops_copy(inv.cost_center_id, "", MAX_LEN_ID); + + // Set document references and save copy to disk. + 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; + } + + 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 locale::get("import.status.starting"); + case import_status::IMPORT_UPLOADING_FILE: return locale::get("import.status.uploading_file"); + case import_status::IMPORT_QUERYING: return locale::get("import.status.querying"); + case import_status::IMPORT_WAITING_FOR_RESPONSE: return locale::get("import.status.waiting_for_result"); + case import_status::IMPORT_DONE: return locale::get("import.status.done"); + } + return ""; +} + +const char* import_error_to_str(i_err error) +{ + switch(error) + { + case I_ERR_FAILED_UPLOAD: return locale::get("import.error.upload"); + case I_ERR_FAILED_QUERY: return locale::get("import.error.query"); + case I_ERR_FAILED_IMPORT: return locale::get("import.error.import"); + } + return ""; +}
\ No newline at end of file |
