/* * Copyright (c) 2025 Aldrik Ramaekers * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include "logger.hpp" #include "strops.hpp" #include "memops.hpp" #include "administration_reader.hpp" #include "administration_writer.hpp" #include "tinyfiledialogs.h" bool administration_reader::open_new() { // @locale::get char const * lFilterPatterns[1] = { "*.openbook" }; char* save_path = tinyfd_saveFileDialog("Select destination", NULL, 1, lFilterPatterns, NULL); if (!save_path) return false; administration::create_default(save_path); return true; } bool administration_reader::save_new() { // @locale::get char const * lFilterPatterns[1] = { "*.openbook" }; char* save_path = tinyfd_saveFileDialog("Select destination", NULL, 1, lFilterPatterns, NULL); if (!save_path) return false; administration::set_file_path(save_path); administration_writer::save_all_async(); return true; } bool administration_reader::open_existing(char* file_path) { if (file_path == NULL) { // @locale::get char const * lFilterPatterns[1] = { "*.openbook" }; file_path = tinyfd_openFileDialog("Select save file", NULL, 1, lFilterPatterns, NULL, 0); if (!file_path) return false; } STOPWATCH_START; administration::create_from_file(file_path); zip_t* zip = zip_open(file_path, 0, 'r'); size_t i, n = zip_entries_total(zip); for (i = 0; i < n; ++i) { zip_entry_openbyindex(zip, i); { const char *name = zip_entry_name(zip); int isdir = zip_entry_isdir(zip); if (isdir) continue; unsigned long long size = zip_entry_size(zip); char* buffer = (char*)memops::alloc(size+1); memops::zero(buffer, size+1); zip_entry_read(zip, (void**)&buffer, (size_t*)&size); if (strlen(name) == 0) continue; if (strcmp(name, ADMIN_FILE_INFO) == 0) { administration_reader::import_administration_info(buffer, (size_t)size); } else if (strops::is_prefixed("T/", name)) { administration_reader::import_tax_rate(buffer, (size_t)size); } else if (strops::is_prefixed("E/", name)) { administration_reader::import_cost_center(buffer, (size_t)size); } else if (strops::is_prefixed("P/", name)) { administration_reader::import_project(buffer, (size_t)size); } else if (strops::is_prefixed("C/", name)) { administration_reader::import_contact(buffer, (size_t)size); } else if (strops::is_prefixed("I/", name)) { administration_reader::import_invoice(buffer, (size_t)size); } memops::unalloc(buffer); } zip_entry_close(zip); } zip_close(zip); logger::info("Imported '%s' in %.3fms.", file_path, STOPWATCH_TIME); return true; } bool administration_reader::read_invoice_from_xml(invoice* result, char* buffer, size_t buffer_size) { 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"); xml_get_str_x(root, data.sequential_number, MAX_LEN_ID, "cac:OrderReference", "cbc:ID", 0); xml_get_str(root, data.currency, MAX_LEN_CURRENCY, "cbc:DocumentCurrencyCode"); data.status = (invoice_status)xml_get_s32_x(root, "cac:DespatchDocumentReference", "cbc:ID", 0); // Dates data.issued_at = xml_get_date_x(root, "cbc:IssueDate", 0); data.expires_at = xml_get_date_x(root, "cbc:DueDate", 0); data.delivered_at = xml_get_date_x(root, "cac:Delivery", "cbc:ActualDeliveryDate", 0); // References xml_get_str_x(root, data.document.copy_path, MAX_LEN_PATH, "cac:AdditionalDocumentReference", "cbc:ID", 0); xml_get_str_x(root, data.document.original_path, MAX_LEN_PATH, "cac:AdditionalDocumentReference", "cbc:DocumentDescription", 0); xml_get_str_x(root, data.project_id, MAX_LEN_ID, "cac:ProjectReference", "cbc:ID", 0); xml_get_str(root, data.cost_center_id, MAX_LEN_ID, "cac:AccountingCost"); // Payment means data.payment_means.payment_method = (payment_method)xml_get_s32_x(root, "cac:PaymentMeans", "cbc:PaymentMeansCode", 0); xml_get_str_x(root, data.payment_means.payee_bank_account, MAX_LEN_BANK, "cac:PaymentMeans", "cac:PayeeFinancialAccount", "cbc:ID", 0); xml_get_str_x(root, data.payment_means.payee_account_name, MAX_LEN_LONG_DESC, "cac:PaymentMeans", "cac:PayeeFinancialAccount", "cbc:Name", 0); xml_get_str_x(root, data.payment_means.service_provider_id, MAX_LEN_ID, "cac:PaymentMeans", "cac:PayeeFinancialAccount", "cac:FinancialInstitutionBranch", "cac:FinancialInstitution", "cbc:ID", 0); xml_get_str_x(root, data.payment_means.payer_bank_account, MAX_LEN_BANK, "cac:PaymentMeans", "cac:PayerFinancialAccount", "cbc:ID", 0); // Totals data.tax = xml_get_float_x(root, "cac:TaxTotal", "cbc:TaxAmount", 0); data.total = xml_get_float_x(root, "cac:LegalMonetaryTotal", "cbc:TaxInclusiveAmount", 0); data.net = xml_get_float_x(root, "cac:LegalMonetaryTotal", "cbc:TaxExclusiveAmount", 0); data.allowance = data.net - xml_get_float_x(root, "cac:LegalMonetaryTotal", "cbc:LineExtensionAmount", 0); // Supplier xml_get_str_x(root, data.supplier.id, MAX_LEN_ID, "cac:AccountingSupplierParty", "cac:Party", "cac:Contact", "cbc:Name", 0); strops::copy(data.supplier.bank_account, data.payment_means.payee_bank_account, MAX_LEN_BANK); xml_get_str_x(root, data.supplier.name, MAX_LEN_LONG_DESC, "cac:AccountingSupplierParty", "cac:Party", "cac:PartyName", "cbc:Name", 0); xml_get_str_x(root, data.supplier.address.address1, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:StreetName", 0); xml_get_str_x(root, data.supplier.address.address2, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:AdditionalStreetName", 0); xml_get_str_x(root, data.supplier.address.city, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:CityName", 0); xml_get_str_x(root, data.supplier.address.postal, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:PostalZone", 0); xml_get_str_x(root, data.supplier.address.region, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:CountrySubentity", 0); xml_get_str_x(root, data.supplier.address.country_code, MAX_LEN_COUNTRY_CODE, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cac:Country", "cbc:IdentificationCode", 0); xml_get_str_x(root, data.supplier.taxid, MAX_LEN_TAXID, "cac:AccountingSupplierParty", "cac:Party", "cac:PartyTaxScheme", "cbc:CompanyID", 0); xml_get_str_x(root, data.supplier.businessid, MAX_LEN_BUSINESSID, "cac:AccountingSupplierParty", "cac:Party", "cac:PartyIdentification", "cbc:ID", 0); xml_get_str_x(root, data.supplier.phone_number, MAX_LEN_PHONE, "cac:AccountingSupplierParty", "cac:Party", "cac:Contact", "cbc:Telephone", 0); xml_get_str_x(root, data.supplier.email, MAX_LEN_EMAIL, "cac:AccountingSupplierParty", "cac:Party", "cac:Contact", "cbc:ElectronicMail", 0); // Customer xml_get_str_x(root, data.customer.id, MAX_LEN_ID, "cac:AccountingCustomerParty", "cac:Party", "cac:Contact", "cbc:Name", 0); strops::copy(data.customer.bank_account, data.payment_means.payer_bank_account, MAX_LEN_BANK); xml_get_str_x(root, data.customer.name, MAX_LEN_LONG_DESC, "cac:AccountingCustomerParty", "cac:Party", "cac:PartyName", "cbc:Name", 0); xml_get_str_x(root, data.customer.address.address1, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:StreetName", 0); xml_get_str_x(root, data.customer.address.address2, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:AdditionalStreetName", 0); xml_get_str_x(root, data.customer.address.city, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:CityName", 0); xml_get_str_x(root, data.customer.address.postal, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:PostalZone", 0); xml_get_str_x(root, data.customer.address.region, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:CountrySubentity", 0); xml_get_str_x(root, data.customer.address.country_code, MAX_LEN_COUNTRY_CODE, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cac:Country", "cbc:IdentificationCode", 0); xml_get_str_x(root, data.customer.taxid, MAX_LEN_TAXID, "cac:AccountingCustomerParty", "cac:Party", "cac:PartyTaxScheme", "cbc:CompanyID", 0); xml_get_str_x(root, data.customer.businessid, MAX_LEN_BUSINESSID, "cac:AccountingCustomerParty", "cac:Party", "cac:PartyIdentification", "cbc:ID", 0); xml_get_str_x(root, data.customer.phone_number, MAX_LEN_PHONE, "cac:AccountingCustomerParty", "cac:Party", "cac:Contact", "cbc:Telephone", 0); xml_get_str_x(root, data.customer.email, MAX_LEN_EMAIL, "cac:AccountingCustomerParty", "cac:Party", "cac:Contact", "cbc:ElectronicMail", 0); char customer_endpoint_id[50]; xml_get_str_x(root, customer_endpoint_id, 50, "cac:AccountingCustomerParty", "cac:Party", "cbc:EndpointID", 0); if (strcmp(customer_endpoint_id, "[CONSUMER]") == 0) data.customer.type = contact_type::CONTACT_CONSUMER; // Addressee xml_get_str_x(root, data.addressee.name, MAX_LEN_LONG_DESC, "cac:Delivery", "cac:DeliveryParty", "cac:PartyName", "cbc:Name", 0); xml_get_str_x(root, data.addressee.address.address1, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:StreetName", 0); xml_get_str_x(root, data.addressee.address.address2, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:AdditionalStreetName", 0); xml_get_str_x(root, data.addressee.address.city, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:CityName", 0); xml_get_str_x(root, data.addressee.address.postal, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:PostalZone", 0); xml_get_str_x(root, data.addressee.address.region, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:CountrySubentity", 0); xml_get_str_x(root, data.addressee.address.country_code, MAX_LEN_COUNTRY_CODE, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cac:Country", "cbc:IdentificationCode", 0); size_t child_count = xml_node_children(root); for (size_t x = 0; x < child_count; x++) { xml_node* child = xml_node_child(root, x); char* child_name = (char*)xml_easy_name(child); if (strcmp(child_name, "cac:InvoiceLine") == 0) { billing_item bi = {0}; xml_get_str_x(child, bi.id, MAX_LEN_ID, "cbc:ID", 0); xml_get_str_x(child, bi.tax_internal_code, MAX_LEN_ID, "cac:Item", "cac:AdditionalItemProperty", "cbc:Value", 0); bi.amount = xml_get_float_x(child, "cbc:InvoicedQuantity", 0); bi.net_per_item = xml_get_float_x(child, "cac:Price", "cbc:PriceAmount", 0); bi.net = xml_get_float_x(child, "cbc:LineExtensionAmount", 0); bi.discount = xml_get_float_x(child, "cac:AllowanceCharge", "cbc:Amount", 0); xml_get_str_x(child, bi.description, MAX_LEN_LONG_DESC, "cac:Item", "cbc:Name", 0); char percentage_buffer[5] = {0}; xml_get_str_attribute(child, percentage_buffer, 5, "unitCode", "cbc:InvoicedQuantity", 0); bi.amount_is_percentage = strcmp(percentage_buffer, "%") == 0; xml_get_str_attribute(child, bi.currency, 5, "currencyID", "cbc:LineExtensionAmount", 0); bi.discount_is_percentage = xml_get_node_x(child, "cac:AllowanceCharge", "cbc:MultiplierFactorNumeric", 0) != 0; if (bi.discount_is_percentage) { bi.discount = xml_get_float_x(child, "cac:AllowanceCharge", "cbc:MultiplierFactorNumeric", 0); } // Import service could set tax rate id to shorthandle. tax_rate tax_rate; if (administration::tax_rate_get_by_internal_code(&tax_rate, bi.tax_internal_code) == A_ERR_NOT_FOUND) { strops::copy(bi.tax_internal_code, "", MAX_LEN_SHORT_DESC); } administration::billing_item_import_to_invoice(&data, bi); } memops::unalloc(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) { logger::info("Loaded invoice '%s' in %.3fms.", data.sequential_number, STOPWATCH_TIME); } else { logger::aerr(result); logger::error("ERROR loading invoice '%s'.", data.sequential_number); } return result == A_ERR_SUCCESS; } bool administration_reader::import_contact(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); contact data = {0}; xml_get_str(root, data.id, MAX_LEN_ID, "Id"); xml_get_str(root, data.name, MAX_LEN_LONG_DESC, "Name"); data.type = (contact_type)xml_get_s32(root, "Type"); xml_get_str(root, data.taxid, MAX_LEN_TAXID, "TaxId"); xml_get_str(root, data.businessid, MAX_LEN_BUSINESSID, "BusinessId"); xml_get_str(root, data.email, MAX_LEN_EMAIL, "Email"); xml_get_str(root, data.phone_number, MAX_LEN_PHONE, "PhoneNumber"); xml_get_str(root, data.bank_account, MAX_LEN_BANK, "BankAccount"); struct xml_node* node_address = xml_easy_child(root, (uint8_t *)"Address", 0); xml_get_str(node_address, data.address.address1, MAX_LEN_ADDRESS, "AddressLine1"); xml_get_str(node_address, data.address.address2, MAX_LEN_ADDRESS, "AddressLine2"); xml_get_str(node_address, data.address.country_code, MAX_LEN_COUNTRY_CODE, "CountryCode"); xml_get_str(node_address, data.address.city, MAX_LEN_ADDRESS, "City"); xml_get_str(node_address, data.address.postal, MAX_LEN_ADDRESS, "Postal"); xml_get_str(node_address, data.address.region, MAX_LEN_ADDRESS, "Region"); a_err result = administration::contact_import(data); if (result == A_ERR_SUCCESS) { logger::info("Loaded contact '%s' in %.3fms.", data.name, STOPWATCH_TIME); } else { logger::aerr(result); logger::error("ERROR loading contact '%s'.", data.name); } return result; } bool administration_reader::import_project(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); project data = {0}; xml_get_str(root, data.id, MAX_LEN_ID, "Id"); xml_get_str(root, data.description, MAX_LEN_LONG_DESC, "Description"); data.state = (project_state)xml_get_s32(root, "State"); data.start_date = xml_get_date_x(root, "StartDate", 0); data.end_date = xml_get_date_x(root, "EndDate", 0); a_err result = administration::project_import(data); if (result == A_ERR_SUCCESS) { logger::info("Loaded project in %.3fms. id=%s description=%s state=%d started=%lld end=%lld", STOPWATCH_TIME, data.id, data.description, data.state, data.start_date, data.end_date); } else { logger::aerr(result); logger::error("ERROR loading project '%s'.", data.id); } return result; } bool administration_reader::import_cost_center(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); cost_center data = {0}; xml_get_str(root, data.id, MAX_LEN_ID, "Id"); xml_get_str(root, data.code, MAX_LEN_CODE, "Code"); xml_get_str(root, data.description, MAX_LEN_LONG_DESC, "Description"); a_err result = administration::cost_center_import(data); if (result == A_ERR_SUCCESS) { logger::info("Loaded cost center in %.3fms. id=%s code=%s description=%s", STOPWATCH_TIME, data.id, data.code, data.description); } else { logger::aerr(result); logger::error("ERROR loading cost center '%s'.", data.id); } return result; } bool administration_reader::import_tax_rate(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); tax_rate data = {0}; xml_get_str(root, data.internal_code, MAX_LEN_ID, "Id"); xml_get_str(root, data.category_code, MAX_LEN_CODE, "Category"); data.rate = xml_get_float(root, "Rate"); data.type = static_cast(xml_get_s32(root, "Type")); char tsb[MAX_LEN_LONG_DESC]; xml_get_str(root, tsb, MAX_LEN_LONG_DESC, "TaxSections"); for (char *p = strops::tokenize(tsb,"##"); p != NULL; p = strtok(NULL, "##")) { if (strlen(p) > 0) strops::copy(data.tax_sections[data.tax_section_count++], p, MAX_LEN_SHORT_DESC); } a_err result = administration::tax_rate_import(data); if (result == A_ERR_SUCCESS) { logger::info("Loaded tax rate info in %.3fms. internal_code=%s", STOPWATCH_TIME, data.internal_code); } else { logger::aerr(result); logger::error("ERROR loading tax rate '%s'.", data.internal_code); } return result; } bool administration_reader::import_administration_info(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); administration::set_next_id(xml_get_s32(root, "NextId")); administration::set_next_sequence_number(xml_get_s32(root, "NextSequenceNumber")); ai_service ai_service; ai_service.provider = (ai_provider)xml_get_s32_x(root, "AIService", "Provider", 0); xml_get_str_x(root, ai_service.api_key_public, MAX_LEN_API_KEY, "AIService", "PublicKey", 0); xml_get_str_x(root, ai_service.model_name, MAX_LEN_SHORT_DESC, "AIService", "Model", 0); administration::set_ai_service(ai_service); logger::info("Loaded administration info in %.3fms. next_id=%d next_sequence_number=%d", STOPWATCH_TIME, administration::get_next_id(), administration::get_next_sequence_number()); return true; }