/* * 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 "config.hpp" #include "memops.hpp" #include "logger.hpp" #include "ui.hpp" #include "locales.hpp" #include "strops.hpp" #include "administration_reader.hpp" #include "administration_writer.hpp" #include "tinyfiledialogs.h" #include "file_templates.hpp" static mtx_t _save_file_mutex; static bool _is_writing = false; static write_completed_event _write_completed_ev = 0; bool administration_writer::is_writing() { return _is_writing; } void administration_writer::set_write_completed_event_callback(write_completed_event ev) { _write_completed_ev = ev; } static void on_administration_data_changed() { _is_writing = true; auto* func = new auto([]() { administration_writer::save_administration_info_blocking(); _is_writing = false; if (_write_completed_ev) _write_completed_ev(); }); auto trampoline = [](void* arg) -> int { auto* f = static_cast(arg); (*f)(); return 0; }; thrd_t thr; thrd_create(&thr, trampoline, 0); #ifdef _TESTING_MODE_ thrd_join(thr, 0); #endif } static void on_administration_data_deleted(char id[MAX_LEN_ID]) { _is_writing = true; char* id_copy = (char*)memops::alloc(MAX_LEN_ID); strops::copy(id_copy, id, MAX_LEN_ID); auto* func = new auto([](void* arg) { char* id = (char*)arg; administration_writer::delete_entry(id); administration_writer::save_administration_info_blocking(); memops::unalloc(arg); _is_writing = false; if (_write_completed_ev) _write_completed_ev(); }); auto trampoline = [](void* arg) -> int { auto* f = static_cast(arg); (*f)(arg); return 0; }; thrd_t thr; thrd_create(&thr, trampoline, (void*)id_copy); #ifdef _TESTING_MODE_ thrd_join(thr, 0); #endif } static void on_invoice_changed(invoice* inv) { _is_writing = true; invoice inv_copy = administration::invoice_create_copy(inv); invoice* inv_copy2 = (invoice*)memops::alloc(sizeof(invoice)); memops::copy(inv_copy2, &inv_copy, sizeof(invoice)); auto* func = new auto([](void* arg) { invoice* inv = (invoice*)arg; administration_writer::save_invoice_blocking(*inv); administration_writer::save_administration_info_blocking(); administration::invoice_destroy(inv); _is_writing = false; if (_write_completed_ev) _write_completed_ev(); }); auto trampoline = [](void* arg) -> int { auto* f = static_cast(arg); (*f)(arg); return 0; }; thrd_t thr; thrd_create(&thr, trampoline, (void*)inv_copy2); #ifdef _TESTING_MODE_ thrd_join(thr, 0); #endif } static void on_contact_changed_changed(contact* cc) { _is_writing = true; contact* cc_copy = (contact*)memops::alloc(sizeof(contact)); memops::copy(cc_copy, cc, sizeof(contact)); auto* func = new auto([](void* arg) { contact* cc = (contact*)arg; administration_writer::save_contact_blocking(*cc); administration_writer::save_administration_info_blocking(); memops::unalloc(arg); _is_writing = false; if (_write_completed_ev) _write_completed_ev(); }); auto trampoline = [](void* arg) -> int { auto* f = static_cast(arg); (*f)(arg); return 0; }; thrd_t thr; thrd_create(&thr, trampoline, (void*)cc_copy); #ifdef _TESTING_MODE_ thrd_join(thr, 0); #endif } static void on_taxrate_changed_changed(tax_rate* rate) { _is_writing = true; tax_rate* rate_copy = (tax_rate*)memops::alloc(sizeof(tax_rate)); memops::copy(rate_copy, rate, sizeof(tax_rate)); auto* func = new auto([](void* arg) { tax_rate* rate = (tax_rate*)arg; administration_writer::save_tax_rate_blocking(*rate); administration_writer::save_administration_info_blocking(); memops::unalloc(arg); _is_writing = false; if (_write_completed_ev) _write_completed_ev(); }); auto trampoline = [](void* arg) -> int { auto* f = static_cast(arg); (*f)(arg); return 0; }; thrd_t thr; thrd_create(&thr, trampoline, (void*)rate_copy); #ifdef _TESTING_MODE_ thrd_join(thr, 0); #endif } static void on_costcenter_changed_changed(cost_center* cc) { _is_writing = true; cost_center* cc_copy = (cost_center*)memops::alloc(sizeof(cost_center)); memops::copy(cc_copy, cc, sizeof(cost_center)); auto* func = new auto([](void* arg) { cost_center* cc = (cost_center*)arg; administration_writer::save_cost_center_blocking(*cc); memops::unalloc(arg); _is_writing = false; if (_write_completed_ev) _write_completed_ev(); }); auto trampoline = [](void* arg) -> int { auto* f = static_cast(arg); (*f)(arg); return 0; }; thrd_t thr; thrd_create(&thr, trampoline, (void*)cc_copy); #ifdef _TESTING_MODE_ thrd_join(thr, 0); #endif } static void on_project_changed_changed(project* pp) { _is_writing = true; project* pp_copy = (project*)memops::alloc(sizeof(project)); memops::copy(pp_copy, pp, sizeof(project)); auto* func = new auto([](void* arg) { project* pp = (project*)arg; administration_writer::save_project_blocking(*pp); administration_writer::save_administration_info_blocking(); memops::unalloc(arg); _is_writing = false; if (_write_completed_ev) _write_completed_ev(); }); auto trampoline = [](void* arg) -> int { auto* f = static_cast(arg); (*f)(arg); return 0; }; thrd_t thr; thrd_create(&thr, trampoline, (void*)pp_copy); #ifdef _TESTING_MODE_ thrd_join(thr, 0); #endif } bool administration_writer::create() { administration::set_administration_data_changed_event_callback(on_administration_data_changed); administration::set_data_deleted_event_callback(on_administration_data_deleted); administration::set_invoice_changed_event_callback(on_invoice_changed); administration::set_contact_changed_event_callback(on_contact_changed_changed); administration::set_taxrate_changed_event_callback(on_taxrate_changed_changed); administration::set_costcenter_changed_event_callback(on_costcenter_changed_changed); administration::set_project_changed_event_callback(on_project_changed_changed); return mtx_init(&_save_file_mutex, mtx_plain|mtx_recursive) == thrd_success; } void administration_writer::destroy() { mtx_destroy(&_save_file_mutex); } static char* copy_template(const char* template_str, int* buf_size) { size_t template_size = strops::length(template_str); size_t buf_length = template_size*5; // Ballpark file content size. char* file_content = (char*)memops::alloc(buf_length); memops::zero(file_content, buf_length); memops::copy(file_content, template_str, template_size); *buf_size = (int)buf_length; return file_content; } static bool zip_entry_exists(char* entry) { mtx_lock(&_save_file_mutex); struct zip_t *zip_read = zip_open(administration::get_file_path(), 0, 'r'); int result = zip_entry_open(zip_read, entry); zip_entry_close(zip_read); zip_close(zip_read); mtx_unlock(&_save_file_mutex); return result == 0; } static bool delete_entry_by_name(char* entry) { STOPWATCH_START; mtx_lock(&_save_file_mutex); bool result = 1; struct zip_t *zip_write = zip_open(administration::get_file_path(), 0, 'a'); if (!zip_write) zip_write = zip_open(administration::get_file_path(), 0, 'w'); char* indices[1] = {entry}; if (zip_entries_delete(zip_write, indices, 1) < 0) result = 0; zip_close(zip_write); if (result) logger::info("Deleted entry '%s' in %.3fms.", entry, STOPWATCH_TIME); else logger::error("Failed to delete entry '%s'.", entry); mtx_unlock(&_save_file_mutex); return result; } bool administration_writer::delete_entry(char* id) { char final_path[50]; strops::format(final_path, 50, "%s.xml", id); return delete_entry_by_name(final_path); } static bool write_to_zip(char* entry_to_replace, char* orig_content, int final_length) { mtx_lock(&_save_file_mutex); bool result = 1; bool entry_exists = zip_entry_exists(entry_to_replace); if (entry_exists) delete_entry_by_name(entry_to_replace); struct zip_t *zip_write = zip_open(administration::get_file_path(), 0, 'a'); if (!zip_write) zip_write = zip_open(administration::get_file_path(), 0, 'w'); zip_entry_open(zip_write, entry_to_replace); if (zip_entry_write(zip_write, orig_content, final_length) < 0) result = 0; zip_entry_close(zip_write); zip_close(zip_write); #if WRITE_DELAY_SEC != 0 struct timespec time; time.tv_sec = WRITE_DELAY_SEC; time.tv_nsec = 0; thrd_sleep(&time, NULL); #endif mtx_unlock(&_save_file_mutex); return result; } ///////////////////////////// //// Invoices ///////////////////////////// static char* get_eas_id_for_contact(contact contact) { if (contact.type == contact_type::CONTACT_CONSUMER) { return "[CONSUMER]"; } // https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/ char* country_code = contact.address.country_code; // Countries using tax identification numbers. if (strops::equals(country_code, "AT")) return contact.taxid; // Austria if (strops::equals(country_code, "BE")) return contact.taxid; // Belgium if (strops::equals(country_code, "BG")) return contact.taxid; // Bulgaria if (strops::equals(country_code, "CY")) return contact.taxid; // Cyprus if (strops::equals(country_code, "CZ")) return contact.taxid; // Czech Republic if (strops::equals(country_code, "DE")) return contact.taxid; // Germany if (strops::equals(country_code, "EE")) return contact.taxid; // Estonia if (strops::equals(country_code, "FR")) return contact.taxid; // France if (strops::equals(country_code, "GR")) return contact.taxid; // Greece if (strops::equals(country_code, "HR")) return contact.taxid; // Croatia if (strops::equals(country_code, "HU")) return contact.taxid; // Hungary if (strops::equals(country_code, "IE")) return contact.taxid; // Ireland if (strops::equals(country_code, "LU")) return contact.taxid; // Luxembourg if (strops::equals(country_code, "LV")) return contact.taxid; // Latvia if (strops::equals(country_code, "MT")) return contact.taxid; // Malta if (strops::equals(country_code, "PL")) return contact.taxid; // Poland if (strops::equals(country_code, "PT")) return contact.taxid; // Portugal if (strops::equals(country_code, "RO")) return contact.taxid; // Romania if (strops::equals(country_code, "SI")) return contact.taxid; // Slovenia if (strops::equals(country_code, "SK")) return contact.taxid; // Slovakia if (strops::equals(country_code, "ES")) return contact.taxid; // Spain // Countries using business identification numbers. if (strops::equals(country_code, "NL")) return contact.businessid; // Netherlands if (strops::equals(country_code, "SE")) return contact.businessid; // Sweden if (strops::equals(country_code, "LT")) return contact.businessid; // Lithuania if (strops::equals(country_code, "IT")) return contact.businessid; // Italy if (strops::equals(country_code, "FI")) return contact.businessid; // Finland if (strops::equals(country_code, "DK")) return contact.businessid; // Denmark return contact.businessid; // Unknown country code } static char* get_eas_scheme_for_contact(contact contact) { if (contact.type == contact_type::CONTACT_CONSUMER) { return "0203"; // Hack } address addr = contact.address; // https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/ char* country_code = addr.country_code; if (strops::equals(country_code, "AT")) return "9914"; // Austria if (strops::equals(country_code, "BE")) return "9925"; // Belgium if (strops::equals(country_code, "BG")) return "9926"; // Bulgaria if (strops::equals(country_code, "CY")) return "9928"; // Cyprus if (strops::equals(country_code, "CZ")) return "9929"; // Czech Republic if (strops::equals(country_code, "DE")) return "9930"; // Germany if (strops::equals(country_code, "DK")) return "0096"; // Denmark if (strops::equals(country_code, "EE")) return "9931"; // Estonia if (strops::equals(country_code, "FR")) return "9957"; // France if (strops::equals(country_code, "GR")) return "9933"; // Greece if (strops::equals(country_code, "HR")) return "9934"; // Croatia if (strops::equals(country_code, "HU")) return "9910"; // Hungary if (strops::equals(country_code, "IE")) return "9935"; // Ireland if (strops::equals(country_code, "FI")) return "0212"; // Finland if (strops::equals(country_code, "IT")) return "0208"; // Italy if (strops::equals(country_code, "LT")) return "0200"; // Lithuania if (strops::equals(country_code, "LU")) return "9938"; // Luxembourg if (strops::equals(country_code, "LV")) return "9939"; // Latvia if (strops::equals(country_code, "MT")) return "9943"; // Malta if (strops::equals(country_code, "NL")) return "0106"; // Netherlands if (strops::equals(country_code, "PL")) return "9945"; // Poland if (strops::equals(country_code, "PT")) return "9946"; // Portugal if (strops::equals(country_code, "RO")) return "9947"; // Romania if (strops::equals(country_code, "SE")) return "0007"; // Sweden if (strops::equals(country_code, "SI")) return "9949"; // Slovenia if (strops::equals(country_code, "SK")) return "9950"; // Slovakia if (strops::equals(country_code, "ES")) return "9920"; // Spain return "0203"; // Hack } bool isEmptyTag(const char *start, const char *end) { const char *ptr = start; while (ptr < end) { if (*ptr != ' ' && *ptr != '\n' && *ptr != '\t') { return false; } ptr++; } return true; } void _remove_empty_xml_tags(char* file_content, int depth) { for (int i = 0; i < depth; i++) { char *read = file_content; char *write = file_content; while (*read) { if (*read == '<') { char *tagStart = read; if (*(tagStart+1) != '/') { char *tagEnd = strchr(tagStart, '>'); if (!tagEnd) break; // malformed XML char *nextTagStart = strchr(tagEnd, '<'); if (*(nextTagStart+1) == '/') { // Check for empty tag char *closeTagStart = strstr(tagEnd + 1, "'); if (closeTagEnd) { read = closeTagEnd + 1; // skip entire empty tag continue; } } } } } *write++ = *read++; } *write = '\0'; // terminate the modified string } } static const char* _get_file_extension(const char *path) { const char *dot = strrchr(path, '.'); if (!dot || dot == path) return ""; return dot; } static void _add_document_to_zip(invoice* inv) { document* doc = &inv->document; if (strops::empty(doc->copy_path) && !strops::empty(doc->original_path)) { char copy_path[MAX_LEN_PATH]; strops::format(copy_path, MAX_LEN_PATH, "documents/%s%s", inv->sequential_number, _get_file_extension(doc->original_path)); FILE* orig_file = fopen(doc->original_path, "rb"); if (orig_file == NULL) { logger::error("ERROR: original document file path does not exist."); return; } fseek(orig_file, 0L, SEEK_END); long sz = ftell(orig_file); fseek(orig_file, 0, SEEK_SET); char* file_copy = (char*)memops::alloc(sz); fread(file_copy, sz, 1, orig_file); file_copy[sz-1] = 0; fclose(orig_file); if (write_to_zip(copy_path, file_copy, sz)) { strops::copy(doc->copy_path, copy_path, MAX_LEN_PATH); logger::info("Made copy of '%s' to '%s'.", doc->original_path, doc->copy_path); } else { logger::error("ERROR: failed to make copy of original document '%s'.", doc->original_path); } memops::unalloc(file_copy); } } bool administration_writer::save_invoice_blocking(invoice inv) { STOPWATCH_START; bool result = 1; int buf_length = 150000; // Ballpark file content size. char* file_content = (char*)memops::alloc(buf_length); memops::zero(file_content, buf_length); memops::copy(file_content, file_template::peppol_invoice_template, strops::length(file_template::peppol_invoice_template)); struct tm *tm_info = 0; char date_buffer[11]; // "YYYY-MM-DD" + null terminator _add_document_to_zip(&inv); strops::replace(file_content, buf_length, "{{INVOICE_ID}}", inv.id); strops::replace(file_content, buf_length, "{{INVOICE_SEQUENCE_ID}}", inv.sequential_number); strops::replace(file_content, buf_length, "{{CURRENCY}}", inv.currency); strops::replace(file_content, buf_length, "{{PROJECT_ID}}", inv.project_id); strops::replace(file_content, buf_length, "{{COST_CENTER_ID}}", inv.cost_center_id); strops::replace(file_content, buf_length, "{{INVOICE_DOCUMENT_COPY}}", inv.document.copy_path); strops::replace(file_content, buf_length, "{{INVOICE_DOCUMENT_ORIG}}", inv.document.original_path); strops::replace_int32(file_content, buf_length, "{{INVOICE_STATUS}}", (s32)inv.status); // Supplier data strops::replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_SCHEME}}", get_eas_scheme_for_contact(inv.supplier)); strops::replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_ID}}", get_eas_id_for_contact(inv.supplier)); strops::replace(file_content, buf_length, "{{SUPPLIER_ID}}", inv.supplier.id); strops::replace(file_content, buf_length, "{{SUPPLIER_NAME}}", inv.supplier.name); strops::replace(file_content, buf_length, "{{SUPPLIER_STREET}}", inv.supplier.address.address1); strops::replace(file_content, buf_length, "{{SUPPLIER_STREET2}}", inv.supplier.address.address2); strops::replace(file_content, buf_length, "{{SUPPLIER_CITY}}", inv.supplier.address.city); strops::replace(file_content, buf_length, "{{SUPPLIER_POSTAL}}", inv.supplier.address.postal); strops::replace(file_content, buf_length, "{{SUPPLIER_REGION}}", inv.supplier.address.region); strops::replace(file_content, buf_length, "{{SUPPLIER_COUNTRY}}", inv.supplier.address.country_code); strops::replace(file_content, buf_length, "{{SUPPLIER_VAT_ID}}", inv.supplier.taxid); strops::replace(file_content, buf_length, "{{SUPPLIER_LEGAL_NAME}}", inv.supplier.name); strops::replace(file_content, buf_length, "{{SUPPLIER_BUSINESS_ID}}", inv.supplier.businessid); strops::replace(file_content, buf_length, "{{SUPPLIER_PHONE_NUMBER}}", inv.supplier.phone_number); strops::replace(file_content, buf_length, "{{SUPPLIER_EMAIL}}", inv.supplier.email); // Customer data strops::replace(file_content, buf_length, "{{CUSTOMER_ENDPOINT_SCHEME}}", get_eas_scheme_for_contact(inv.customer)); strops::replace(file_content, buf_length, "{{CUSTOMER_ENDPOINT_ID}}", get_eas_id_for_contact(inv.customer)); strops::replace(file_content, buf_length, "{{CUSTOMER_ID}}", inv.customer.id); strops::replace(file_content, buf_length, "{{CUSTOMER_NAME}}", inv.customer.name); strops::replace(file_content, buf_length, "{{CUSTOMER_STREET}}", inv.customer.address.address1); strops::replace(file_content, buf_length, "{{CUSTOMER_STREET2}}", inv.customer.address.address2); strops::replace(file_content, buf_length, "{{CUSTOMER_CITY}}", inv.customer.address.city); strops::replace(file_content, buf_length, "{{CUSTOMER_POSTAL}}", inv.customer.address.postal); strops::replace(file_content, buf_length, "{{CUSTOMER_REGION}}", inv.customer.address.region); strops::replace(file_content, buf_length, "{{CUSTOMER_COUNTRY}}", inv.customer.address.country_code); strops::replace(file_content, buf_length, "{{CUSTOMER_VAT_ID}}", inv.customer.taxid); strops::replace(file_content, buf_length, "{{CUSTOMER_LEGAL_NAME}}", inv.customer.name); strops::replace(file_content, buf_length, "{{CUSTOMER_BUSINESS_ID}}", inv.customer.businessid); strops::replace(file_content, buf_length, "{{CUSTOMER_PHONE_NUMBER}}", inv.customer.phone_number); strops::replace(file_content, buf_length, "{{CUSTOMER_EMAIL}}", inv.customer.email); // Delivery data tm_info = localtime(&inv.delivered_at); strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); strops::replace(file_content, buf_length, "{{DELIVERY_DATE}}", date_buffer); strops::replace(file_content, buf_length, "{{DELIVERY_NAME}}", inv.addressee.name); strops::replace(file_content, buf_length, "{{DELIVERY_STREET}}", inv.addressee.address.address1); strops::replace(file_content, buf_length, "{{DELIVERY_STREET2}}", inv.addressee.address.address2); strops::replace(file_content, buf_length, "{{DELIVERY_CITY}}", inv.addressee.address.city); strops::replace(file_content, buf_length, "{{DELIVERY_POSTAL}}", inv.addressee.address.postal); strops::replace(file_content, buf_length, "{{DELIVERY_REGION}}", inv.addressee.address.region); strops::replace(file_content, buf_length, "{{DELIVERY_COUNTRY}}", inv.addressee.address.country_code); // Payment means strops::replace_int32(file_content, buf_length, "{{PAYMENT_TYPE}}", inv.payment_means.payment_method); strops::replace(file_content, buf_length, "{{RECIPIENT_IBAN}}", inv.payment_means.payee_bank_account); strops::replace(file_content, buf_length, "{{RECIPIENT_NAME}}", inv.payment_means.payee_account_name); strops::replace(file_content, buf_length, "{{RECIPIENT_BIC}}", inv.payment_means.service_provider_id); strops::replace(file_content, buf_length, "{{SENDER_IBAN}}", inv.payment_means.payer_bank_account); // Tax breakdown strops::replace_float(file_content, buf_length, "{{TOTAL_TAX_AMOUNT}}", inv.tax, 2); { // Create tax subtotal list. tax_rate* tax_rate_buffer = (tax_rate*)memops::alloc(sizeof(tax_rate)*administration::billing_item_count(&inv)); u32 tax_rate_count = administration::invoice_get_tax_rates(&inv, tax_rate_buffer); u32 tax_subtotal_list_buffer_size = (u32)strops::length(file_template::peppol_invoice_tax_subtotal_template) * 2 * tax_rate_count; // Ballpark list size. char* tax_subtotal_list_buffer = (char*)memops::alloc(tax_subtotal_list_buffer_size); memops::zero(tax_subtotal_list_buffer, tax_subtotal_list_buffer_size); u32 tax_subtotal_list_buffer_cursor = 0; for (u32 i = 0; i < tax_rate_count; i++) { int tax_entry_buf_length = 0; char* tax_entry_file_content = copy_template(file_template::peppol_invoice_tax_subtotal_template, &tax_entry_buf_length); tax_subtotal subtotal; administration::invoice_get_subtotal_for_tax_rate(&inv, tax_rate_buffer[i], &subtotal); strops::replace(tax_entry_file_content, tax_entry_buf_length, "{{CURRENCY}}", inv.currency); strops::replace_float(tax_entry_file_content, tax_entry_buf_length, "{{TAXABLE_AMOUNT}}", subtotal.net, 2); strops::replace_float(tax_entry_file_content, tax_entry_buf_length, "{{TAX_AMOUNT}}", subtotal.tax, 2); strops::replace(tax_entry_file_content, tax_entry_buf_length, "{{TAX_CATEGORY}}", tax_rate_buffer[i].category_code); strops::replace_float(tax_entry_file_content, tax_entry_buf_length, "{{TAX_PERCENT}}", tax_rate_buffer[i].rate, 2); u32 content_len = (u32)strops::length(tax_entry_file_content); memops::copy(tax_subtotal_list_buffer+tax_subtotal_list_buffer_cursor, tax_entry_file_content, content_len); tax_subtotal_list_buffer_cursor += content_len; } tax_subtotal_list_buffer[tax_subtotal_list_buffer_cursor] = 0; strops::replace(file_content, buf_length, "{{TAX_SUBTOTAL_LIST}}", tax_subtotal_list_buffer); memops::unalloc(tax_subtotal_list_buffer); memops::unalloc(tax_rate_buffer); } // Totals strops::replace_float(file_content, buf_length, "{{LINE_EXTENSION_AMOUNT}}", inv.net - inv.allowance, 2); strops::replace_float(file_content, buf_length, "{{TAX_EXCLUSIVE_AMOUNT}}", inv.net, 2); strops::replace_float(file_content, buf_length, "{{TAX_INCLUSIVE_AMOUNT}}", inv.total, 2); strops::replace_float(file_content, buf_length, "{{PAYABLE_AMOUNT}}", inv.total, 2); // Invoice lines { billing_item* billing_item_buffer = (billing_item*)memops::alloc(sizeof(billing_item) * administration::billing_item_count(&inv)); u32 billing_item_count = administration::billing_item_get_all_for_invoice(&inv, billing_item_buffer); u32 billing_item_list_buffer_size = (u32)strops::length(file_template::peppol_invoice_line_template) * 2 * billing_item_count; // Ballpark list size. char* billing_item_list_buffer = (char*)memops::alloc(billing_item_list_buffer_size); memops::zero(billing_item_list_buffer, billing_item_list_buffer_size); u32 billing_item_list_buffer_cursor = 0; for (u32 i = 0; i < billing_item_count; i++) { int billing_item_buf_length = 0; char* billing_item_file_content = copy_template(file_template::peppol_invoice_line_template, &billing_item_buf_length); billing_item bi = billing_item_buffer[i]; tax_rate rate; administration::tax_rate_get_by_internal_code(&rate, bi.tax_internal_code); strops::replace(billing_item_file_content, billing_item_buf_length, "{{CURRENCY}}", bi.currency); strops::replace(billing_item_file_content, billing_item_buf_length, "{{LINE_ID}}", bi.id); strops::replace(billing_item_file_content, billing_item_buf_length, "{{LINE_TAX_ID}}", bi.tax_internal_code); strops::replace(billing_item_file_content, billing_item_buf_length, "{{ITEM_NAME}}", bi.description); strops::replace(billing_item_file_content, billing_item_buf_length, "{{LINE_TAX_CATEGORY}}", rate.category_code); strops::replace_float(billing_item_file_content, billing_item_buf_length, "{{LINE_TAX_PERCENT}}", rate.rate, 2); strops::replace_float(billing_item_file_content, billing_item_buf_length, "{{LINE_AMOUNT}}", bi.net, 2); // line amount = net_per_item * items_count - discount strops::replace_float(billing_item_file_content, billing_item_buf_length, "{{QUANTITY}}", bi.amount, 2); strops::replace_float(billing_item_file_content, billing_item_buf_length, "{{UNIT_PRICE}}", bi.net_per_item, 2); // unit price before discount strops::replace(billing_item_file_content, billing_item_buf_length, "{{UNIT_CODE}}", bi.amount_is_percentage ? "%" : "X"); if (bi.discount_is_percentage) { strops::replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", bi.discount, 2); strops::replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_BASE_AMOUNT}}", bi.net + bi.allowance, 2); // Total net before discount. } else { strops::replace(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", ""); strops::replace(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_BASE_AMOUNT}}", ""); } strops::replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL}}", bi.allowance, 2); u32 content_len = (u32)strops::length(billing_item_file_content); memops::copy(billing_item_list_buffer+billing_item_list_buffer_cursor, billing_item_file_content, content_len); billing_item_list_buffer_cursor += content_len; } billing_item_list_buffer[billing_item_list_buffer_cursor] = 0; strops::replace(file_content, buf_length, "{{INVOICE_LINE_LIST}}", billing_item_list_buffer); memops::unalloc(billing_item_list_buffer); memops::unalloc(billing_item_buffer); } // Dates tm_info = localtime(&inv.issued_at); strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); strops::replace(file_content, buf_length, "{{ISSUE_DATE}}", date_buffer); tm_info = localtime(&inv.expires_at); strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); strops::replace(file_content, buf_length, "{{DUE_DATE}}", date_buffer); _remove_empty_xml_tags(file_content, 5); //// Write to Disk. char final_path[50]; strops::format(final_path, 50, "%s.xml", inv.id); int final_length = (int)strops::length(file_content); if (!xml_string_is_valid((uint8_t*)file_content, final_length)) result = 0; else if (!write_to_zip(final_path, file_content, final_length)) result = 0; memops::unalloc(file_content); if (result) logger::info("Saved invoice '%s' in %.3fms.", inv.sequential_number, STOPWATCH_TIME); else logger::error("Failed to save invoice '%s'.", inv.sequential_number); return result; } ///////////////////////////// //// Projects ///////////////////////////// bool administration_writer::save_project_blocking(project project) { STOPWATCH_START; bool result = 1; int buf_length = 0; char* file_content = copy_template(file_template::project_save_template, &buf_length); struct tm *tm_info = 0; char date_buffer[11]; // "YYYY-MM-DD" + null terminator strops::replace(file_content, buf_length, "{{PROJECT_ID}}", project.id); strops::replace(file_content, buf_length, "{{PROJECT_DESCRIPTION}}", project.description); strops::replace_int32(file_content, buf_length, "{{PROJECT_STATE}}", project.state); tm_info = gmtime(&project.start_date); strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); strops::replace(file_content, buf_length, "{{PROJECT_STARTDATE}}", date_buffer); tm_info = gmtime(&project.end_date); strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); strops::replace(file_content, buf_length, "{{PROJECT_ENDDATE}}", date_buffer); //// Write to Disk. char final_path[50]; strops::format(final_path, 50, "%s.xml", project.id); int final_length = (int)strops::length(file_content); if (!xml_string_is_valid((uint8_t*)file_content, final_length)) result = 0; else if (!write_to_zip(final_path, file_content, final_length)) result = 0; memops::unalloc(file_content); if (result) logger::info("Saved project '%s' in %.3fms.", project.description, STOPWATCH_TIME); else logger::error("Failed to save project '%s'.", project.description); return result; } ///////////////////////////// //// Cost centers ///////////////////////////// bool administration_writer::save_cost_center_blocking(cost_center cost) { STOPWATCH_START; bool result = 1; int buf_length = 0; char* file_content = copy_template(file_template::costcenter_save_template, &buf_length); strops::replace(file_content, buf_length, "{{COSTCENTER_ID}}", cost.id); strops::replace(file_content, buf_length, "{{COSTCENTER_CODE}}", cost.code); strops::replace(file_content, buf_length, "{{COSTCENTER_DESCRIPTION}}", cost.description); //// Write to Disk. char final_path[50]; strops::format(final_path, 50, "%s.xml", cost.id); int final_length = (int)strops::length(file_content); if (!xml_string_is_valid((uint8_t*)file_content, final_length)) result = 0; else if (!write_to_zip(final_path, file_content, final_length)) result = 0; memops::unalloc(file_content); if (result) logger::info("Saved cost center '%s' in %.3fms.", cost.code, STOPWATCH_TIME); else logger::error("Failed to save cost center '%s'.", cost.code); return result; } bool administration_writer::save_all_cost_centers_blocking() { bool result = 1; u32 num_costcenters = administration::cost_center_count(); u32 buffer_size = sizeof(cost_center) * num_costcenters; cost_center* costcenter_buffer = (cost_center*)memops::alloc(buffer_size); num_costcenters = administration::cost_center_get_all(costcenter_buffer); for (u32 i = 0; i < num_costcenters; i++) { cost_center c = costcenter_buffer[i]; if (!administration_writer::save_cost_center_blocking(c)) result = 0; } memops::unalloc(costcenter_buffer); return result; } ///////////////////////////// //// Tax rates ///////////////////////////// bool administration_writer::save_tax_rate_blocking(tax_rate rate) { STOPWATCH_START; bool result = 1; int buf_length = 0; char* file_content = copy_template(file_template::taxrate_save_template, &buf_length); strops::replace(file_content, buf_length, "{{TAXBRACKET_INTERNAL_CODE}}", rate.internal_code); strops::replace_float(file_content, buf_length, "{{TAXBRACKET_RATE}}", rate.rate, 2); strops::replace_int32(file_content, buf_length, "{{TAXBRACKET_TYPE}}", static_cast(rate.type)); strops::replace(file_content, buf_length, "{{TAXBRACKET_CATEGORY}}", rate.category_code); char tax_sections_buffer[MAX_LEN_LONG_DESC]; char* write_cursor = tax_sections_buffer; for (u32 i = 0; i < rate.tax_section_count; i++) { write_cursor += strops::format(write_cursor, MAX_LEN_LONG_DESC, "%s##", rate.tax_sections[i]); } strops::replace(file_content, buf_length, "{{TAXBRACKET_SECTIONS}}", tax_sections_buffer); //// Write to Disk. char final_path[50]; strops::format(final_path, 50, "T/%s.xml", rate.internal_code); int final_length = (int)strops::length(file_content); if (!xml_string_is_valid((uint8_t*)file_content, final_length)) result = 0; else if (!write_to_zip(final_path, file_content, final_length)) result = 0; memops::unalloc(file_content); if (result) logger::info("Saved tax rate '%s' in %.3fms.", rate.internal_code, STOPWATCH_TIME); else logger::error("Failed to save tax rate '%s'.", rate.internal_code); return result; } ///////////////////////////// //// Contacts ///////////////////////////// bool administration_writer::save_contact_blocking(contact c) { STOPWATCH_START; bool result = 1; int buf_length = 0; char* file_content = copy_template(file_template::contact_save_template, &buf_length); strops::replace(file_content, buf_length, "{{CONTACT_ID}}", c.id); strops::replace(file_content, buf_length, "{{CONTACT_NAME}}", c.name); strops::replace_int32(file_content, buf_length, "{{CONTACT_TYPE}}", c.type); strops::replace(file_content, buf_length, "{{CONTACT_TAXID}}", c.taxid); strops::replace(file_content, buf_length, "{{CONTACT_BUSINESSID}}", c.businessid); strops::replace(file_content, buf_length, "{{CONTACT_EMAIL}}", c.email); strops::replace(file_content, buf_length, "{{CONTACT_PHONENUMBER}}", c.phone_number); strops::replace(file_content, buf_length, "{{CONTACT_BANKACCOUNT}}", c.bank_account); strops::replace(file_content, buf_length, "{{CONTACT_ADDRESS1}}", c.address.address1); strops::replace(file_content, buf_length, "{{CONTACT_ADDRESS2}}", c.address.address2); strops::replace(file_content, buf_length, "{{CONTACT_COUNTRY}}", c.address.country_code); strops::replace(file_content, buf_length, "{{CONTACT_CITY}}", c.address.city); strops::replace(file_content, buf_length, "{{CONTACT_POSTAL}}", c.address.postal); strops::replace(file_content, buf_length, "{{CONTACT_REGION}}", c.address.region); char final_path[50]; strops::format(final_path, 50, "%s.xml", c.id); int final_length = (int)strops::length(file_content); if (!xml_string_is_valid((uint8_t*)file_content, final_length)) result = 0; else if (!write_to_zip(final_path, file_content, final_length)) result = 0; memops::unalloc(file_content); if (result) logger::info("Saved contact '%s' in %.3fms.", c.name, STOPWATCH_TIME); else logger::error("Failed to save contact '%s'.", c.name); return result; } ///////////////////////////// //// Administration info ///////////////////////////// bool administration_writer::save_administration_info_blocking() { STOPWATCH_START; bool result = 1; int buf_length = 0; char* file_content = copy_template(file_template::administration_save_template, &buf_length); strops::replace_int32(file_content, buf_length, "{{NEXT_ID}}", administration::get_next_id()); strops::replace_int32(file_content, buf_length, "{{NEXT_SEQUENCE_NUMBER}}", administration::get_next_sequence_number()); strops::replace(file_content, buf_length, "{{PROGRAM_VERSION}}", config::PROGRAM_VERSION); ai_service ai_service = administration::get_ai_service(); strops::replace_int32(file_content, buf_length, "{{AI_SERVICE_PROVIDER}}", (s32)ai_service.provider); strops::replace(file_content, buf_length, "{{AI_SERVICE_PUBLIC_KEY}}", ai_service.api_key_public); strops::replace(file_content, buf_length, "{{AI_SERVICE_MODEL}}", ai_service.model_name); //// Write to Disk. int final_length = (int)strops::length(file_content); if (!xml_string_is_valid((uint8_t*)file_content, final_length)) result = 0; else if (!write_to_zip(ADMIN_FILE_INFO, file_content, final_length)) result = 0; memops::unalloc(file_content); if (result) logger::info("Saved administration info in %.3fms.", STOPWATCH_TIME); else logger::error("Failed to save administration info."); return result; }