#include #include #include #include #include #include "strops.hpp" #include "administration.hpp" #include "administration_writer.hpp" administration g_administration; static s32 administration_create_id() { return g_administration.next_id; } #define ADD_BRACKET(_country, _rate, _description)\ {\ country_tax_bracket* tb = (country_tax_bracket*)malloc(sizeof(country_tax_bracket));\ snprintf(tb->id, sizeof(tb->id), "T/%d", administration_create_id());\ memcpy(tb->country_code, _country, sizeof(tb->country_code));\ tb->rate = _rate;\ memcpy(tb->description, _description, sizeof(tb->description));\ list_append(&g_administration.tax_brackets, tb);\ g_administration.next_id++;\ } static int compare_tax_countries(const void *a, const void *b) { country_tax_bracket *objA = (country_tax_bracket *)a; country_tax_bracket *objB = (country_tax_bracket *)b; return strcmp(objA->country_code, objB->country_code); } static invoice_status administation_get_random_invoice_status() { return (invoice_status)(rand() % invoice_status::INVOICE_END); } static void administation_get_random_project(char* project_id) { int size = list_size(&g_administration.projects); if (size > 0) { int index = rand() % size; project *val = (project *) list_get_at(&g_administration.projects, index); strops_copy(project_id, val->id, MAX_LEN_ID); } } static void administation_get_random_cost_center(char* cost_center_id) { int size = list_size(&g_administration.cost_centers); if (size > 0) { int index = rand() % size; cost_center *val = (cost_center *) list_get_at(&g_administration.cost_centers, index); strops_copy(cost_center_id, val->id, MAX_LEN_ID); } } static contact* administation_get_random_contact() { int size = list_size(&g_administration.contacts); if (size > 0) { int index = rand() % size; contact *val = (contact *) list_get_at(&g_administration.contacts, index); return val; } return 0; } static time_t administration_get_default_invoice_expire_duration() { return (30 * 24 * 60 * 60); // 30 days } static void administration_recalculate_billing_item_totals(billing_item* item); static void administation_get_random_billing_items(invoice* inv) { int amount = 1 + rand() % 20; for (int i = 0; i < amount; i++) { billing_item item = administration_billing_item_create_empty(); item.amount = 1 + (float)(rand() % 5); item.amount_is_percentage = (float)(rand() % 2); item.description[MAX_LEN_LONG_DESC]; item.net_per_item = (float)(rand() % 100); item.discount = (float)(rand() % 30); item.discount_is_percentage = (float)(rand() % 2); administration_recalculate_billing_item_totals(&item); if (item.net < item.discount) item.discount = 0.0f; country_tax_bracket buffer[20]; u32 bracket_count = administration_tax_bracket_get_by_country(buffer, inv->supplier.address.country_code); country_tax_bracket rand_bracket = buffer[rand() % bracket_count]; strops_copy(item.tax_bracket_id, rand_bracket.id, MAX_LEN_ID); administration_billing_item_add_to_invoice(inv, item); } } static void administration_create_default_tax_brackets() { // General brackets shared between countries. ADD_BRACKET("00", 0.0f, "tax.reverse_charge"); ADD_BRACKET("00", 0.0f, "tax.exempt"); // Austria ADD_BRACKET("AT", 20.0f, "tax.standard"); ADD_BRACKET("AT", 10.0f, "tax.reduced"); ADD_BRACKET("AT", 13.0f, "tax.reduced"); // Belgium ADD_BRACKET("BE", 21.0f, "tax.standard"); ADD_BRACKET("BE", 6.0f, "tax.reduced"); ADD_BRACKET("BE", 12.0f, "tax.reduced"); // Bulgaria ADD_BRACKET("BG", 20.0f, "tax.standard"); ADD_BRACKET("BG", 9.0f, "tax.reduced"); // Cyprus ADD_BRACKET("CY", 19.0f, "tax.standard"); ADD_BRACKET("CY", 5.0f, "tax.reduced"); ADD_BRACKET("CY", 9.0f, "tax.reduced"); // Czechia ADD_BRACKET("CZ", 21.0f, "tax.standard"); ADD_BRACKET("CZ", 12.0f, "tax.reduced"); // Croatia ADD_BRACKET("HR", 25.0f, "tax.standard"); ADD_BRACKET("HR", 5.0f, "tax.reduced"); ADD_BRACKET("HR", 13.0f, "tax.reduced"); // Denmark ADD_BRACKET("DK", 25.0f, "tax.standard"); // Estonia ADD_BRACKET("EE", 22.0f, "tax.standard"); ADD_BRACKET("EE", 9.0f, "tax.reduced"); // Finland ADD_BRACKET("FI", 25.5f, "tax.standard"); ADD_BRACKET("FI", 10.0f, "tax.reduced"); ADD_BRACKET("FI", 14.0f, "tax.reduced"); // France ADD_BRACKET("FR", 20.0f, "tax.standard"); ADD_BRACKET("FR", 5.5f, "tax.reduced"); ADD_BRACKET("FR", 10.0f, "tax.reduced"); ADD_BRACKET("FR", 2.1f, "tax.superReduced"); // Germany ADD_BRACKET("DE", 19.0f, "tax.standard"); ADD_BRACKET("DE", 7.0f, "tax.reduced"); // Greece ADD_BRACKET("GR", 24.0f, "tax.standard"); ADD_BRACKET("GR", 6.0f, "tax.reduced"); ADD_BRACKET("GR", 13.0f, "tax.reduced"); // Hungary ADD_BRACKET("HU", 27.0f, "tax.standard"); ADD_BRACKET("HU", 5.0f, "tax.reduced"); ADD_BRACKET("HU", 18.0f, "tax.reduced"); // Ireland ADD_BRACKET("IE", 23.0f, "tax.standard"); ADD_BRACKET("IE", 9.0f, "tax.reduced"); ADD_BRACKET("IE", 13.5f, "tax.reduced"); ADD_BRACKET("IE", 4.8f, "tax.superReduced"); // Italy ADD_BRACKET("IT", 22.0f, "tax.standard"); ADD_BRACKET("IT", 5.0f, "tax.reduced"); ADD_BRACKET("IT", 10.0f, "tax.reduced"); ADD_BRACKET("IT", 4.0f, "tax.superReduced"); // Latvia ADD_BRACKET("LV", 21.0f, "tax.standard"); ADD_BRACKET("LV", 5.0f, "tax.reduced"); ADD_BRACKET("LV", 12.0f, "tax.reduced"); // Lithuania ADD_BRACKET("LT", 21.0f, "tax.standard"); ADD_BRACKET("LT", 5.0f, "tax.reduced"); ADD_BRACKET("LT", 9.0f, "tax.reduced"); // Luxembourg ADD_BRACKET("LU", 17.0f, "tax.standard"); ADD_BRACKET("LU", 8.0f, "tax.reduced"); ADD_BRACKET("LU", 14.0f, "tax.reduced"); ADD_BRACKET("LU", 3.0f, "tax.superReduced"); // Malta ADD_BRACKET("MT", 18.0f, "tax.standard"); ADD_BRACKET("MT", 5.0f, "tax.reduced"); ADD_BRACKET("MT", 7.0f, "tax.reduced"); // Netherlands ADD_BRACKET("NL", 21.0f, "tax.standard"); ADD_BRACKET("NL", 9.0f, "tax.reduced"); // Poland ADD_BRACKET("PL", 23.0f, "tax.standard"); ADD_BRACKET("PL", 5.0f, "tax.reduced"); ADD_BRACKET("PL", 8.0f, "tax.reduced"); // Portugal ADD_BRACKET("PT", 23.0f, "tax.standard"); ADD_BRACKET("PT", 6.0f, "tax.reduced"); ADD_BRACKET("PT", 13.0f, "tax.reduced"); // Romania ADD_BRACKET("RO", 19.0f, "tax.standard"); ADD_BRACKET("RO", 5.0f, "tax.reduced"); ADD_BRACKET("RO", 9.0f, "tax.reduced"); // Slovakia ADD_BRACKET("SK", 23.0f, "tax.standard"); ADD_BRACKET("SK", 5.0f, "tax.reduced"); ADD_BRACKET("SK", 19.0f, "tax.reduced"); // Slovenia ADD_BRACKET("SI", 22.0f, "tax.standard"); ADD_BRACKET("SI", 5.0f, "tax.reduced"); ADD_BRACKET("SI", 9.5f, "tax.reduced"); // Spain ADD_BRACKET("ES", 21.0f, "tax.standard"); ADD_BRACKET("ES", 10.0f, "tax.reduced"); ADD_BRACKET("ES", 4.0f, "tax.superReduced"); // Sweden ADD_BRACKET("SE", 25.0f, "tax.standard"); ADD_BRACKET("SE", 6.0f, "tax.reduced"); ADD_BRACKET("SE", 12.0f, "tax.reduced"); list_attributes_comparator(&g_administration.tax_brackets, compare_tax_countries); list_sort(&g_administration.tax_brackets, -1); } static void administration_create_default_cost_centers() { #define ADD_COSTCENTER(_description, _code)\ {\ cost_center* tb = (cost_center*)malloc(sizeof(cost_center));\ snprintf(tb->id, sizeof(tb->id), "E/%d", administration_create_id());\ memcpy(tb->description, _description, sizeof(tb->description));\ memcpy(tb->code, _code, sizeof(tb->code));\ list_append(&g_administration.cost_centers, tb);\ g_administration.next_id++;\ } ADD_COSTCENTER("costcenter.general_expenses", "GENE"); ADD_COSTCENTER("costcenter.administration_general_management", "ADMN"); ADD_COSTCENTER("costcenter.finance_accounting", "FINC"); ADD_COSTCENTER("costcenter.information_technology", "INFO"); ADD_COSTCENTER("costcenter.sales_marketing", "SALE"); ADD_COSTCENTER("costcenter.operations_production", "OPER"); ADD_COSTCENTER("costcenter.supply_chain_logistics", "SUPP"); ADD_COSTCENTER("costcenter.research_development", "RDEV"); ADD_COSTCENTER("costcenter.facilities_maintenance", "FACL"); ADD_COSTCENTER("costcenter.customer_service_support", "CUST"); ADD_COSTCENTER("costcenter.other_specialized", "OTHR"); } static void administration_create_debug_data() { srand((unsigned) time(NULL)); #define ADD_CONSUMER(_name, _addr1, _addr2, _cc)\ {contact _c = administration_contact_create_empty();\ strops_copy(_c.name, _name, sizeof(_c.name));\ strops_copy(_c.address.address1, _addr1, sizeof(_c.address.address1));\ strops_copy(_c.address.address2, _addr2, sizeof(_c.address.address2));\ strops_copy(_c.address.country_code, _cc, sizeof(_c.address.country_code));\ _c.type = contact_type::CONTACT_CONSUMER;\ administration_contact_add(_c);}; #define ADD_BUSINESS(_name, _addr1, _addr2, _cc, _tc, _bc)\ {contact _c = administration_contact_create_empty();\ strops_copy(_c.name, _name, sizeof(_c.name));\ strops_copy(_c.address.address1, _addr1, sizeof(_c.address.address1));\ strops_copy(_c.address.address2, _addr2, sizeof(_c.address.address2));\ strops_copy(_c.address.country_code, _cc, sizeof(_c.address.country_code));\ strops_copy(_c.taxid, _tc, sizeof(_c.taxid));\ strops_copy(_c.businessid, _bc, sizeof(_c.businessid));\ _c.type = contact_type::CONTACT_BUSINESS;\ administration_contact_add(_c);}; #define ADD_PROJECT(_name)\ {project _c = administration_project_create_empty();\ strops_copy(_c.description, _name, sizeof(_c.description));\ administration_project_add(_c);}; strops_copy(g_administration.path, "C:\\Users\\aldri\\Downloads\\test.openbooks", sizeof(g_administration.path)); ADD_CONSUMER("Emma Müller", "Hauptstraße 12", "10115 Berlin", "DE"); ADD_CONSUMER("Luca Rossi", "Via Roma 45", "00184 Roma", "IT"); ADD_CONSUMER("Sofia Garcia", "Calle Mayor 7", "28013 Madrid", "ES"); ADD_CONSUMER("Jean Dupont", "10 Rue de la Paix", "75002 Paris", "FR"); ADD_CONSUMER("Anna Nowak", "ul. Kwiatowa 3", "00-001 Warszawa", "PL"); ADD_CONSUMER("Mikkel Jensen", "Østergade 8", "8000 Aarhus", "DK"); ADD_CONSUMER("Maria Svensson", "Kungsgatan 15", "111 22 Stockholm", "SE"); ADD_CONSUMER("Péter Kovács", "Fő utca 25", "1051 Budapest", "HU"); ADD_CONSUMER("Lucas Silva", "Rua Augusta 100", "1250-001 Lisboa", "PT"); ADD_CONSUMER("Isabelle Lefevre", "5 Place Stanislas", "54000 Nancy", "FR"); ADD_BUSINESS("Schmidt & Co GmbH", "Friedrichstraße 45", "10117 Berlin", "DE", "DE123456789", "HRB123456"); ADD_BUSINESS("Bianchi Srl", "Corso Venezia 12", "20121 Milano", "IT", "IT987654321", "MI1234567"); ADD_BUSINESS("Fernández y Asociados", "Gran Vía 20", "28013 Madrid", "ES", "ES456789123", "CIFB123456"); ADD_BUSINESS("Martin & Partners", "12 Avenue Victor Hugo", "75016 Paris", "FR", "FR321654987", "SIRET123456"); ADD_BUSINESS("Zielińska Consulting", "ul. Marszałkowska 10", "00-590 Warszawa", "PL", "PL789123456", "REGON123456"); ADD_BUSINESS("Sørensen ApS", "Strøget 3", "1460 København", "DK", "DK654321789", "CVR12345678"); ADD_BUSINESS("Johansson AB", "Drottninggatan 22", "111 51 Stockholm", "SE", "SE987654321", "OrgNr1234567"); ADD_BUSINESS("Nagy Kft.", "Andrássy út 60", "1062 Budapest", "HU", "HU123987654", "Cégjegyzékszám123"); ADD_BUSINESS("Santos Lda.", "Avenida da Liberdade 50", "1250-142 Lisboa", "PT", "PT321789654", "NIPC123456789"); ADD_BUSINESS("Dupuis SARL", "8 Rue Saint-Denis", "75001 Paris", "FR", "FR456123789", "SIREN123456"); ADD_BUSINESS("Müller & Söhne GmbH", "Leipziger Platz 8", "10117 Berlin", "DE", "DE654987321", "HRB987654"); ADD_BUSINESS("Romano Srl", "Via Garibaldi 14", "16124 Genova", "IT", "IT321654987", "GE1239876"); ADD_BUSINESS("López Asociados", "Plaza del Pilar 6", "50003 Zaragoza", "ES", "ES789321654", "CIFC654321"); ADD_BUSINESS("Laurent & Fils", "15 Boulevard Haussmann", "75009 Paris", "FR", "FR987321654", "SIRET654987"); ADD_BUSINESS("Kowalczyk Sp. z o.o.", "ul. Piotrkowska 55", "90-001 Łódź", "PL", "PL123456789", "REGON654321"); ADD_BUSINESS("Nielsen ApS", "Nørregade 12", "1165 København", "DK", "DK789456123", "CVR87654321"); ADD_BUSINESS("Lindberg AB", "Vasagatan 18", "111 20 Stockholm", "SE", "SE456789123", "OrgNr7654321"); ADD_BUSINESS("Szabó Kft.", "Kossuth Lajos tér 1", "1055 Budapest", "HU", "HU987123654", "Cégjegyzékszám654321"); ADD_BUSINESS("Costa Lda.", "Rua do Ouro 24", "1100-063 Lisboa", "PT", "PT654123987", "NIPC987654321"); ADD_BUSINESS("Moreau SARL", "3 Place de la République", "75011 Paris", "FR", "FR321456987", "SIREN789123"); ADD_PROJECT("eCommerce"); ADD_PROJECT("Retail store #1"); ADD_PROJECT("Retail store #2"); ADD_PROJECT("Kayak rental"); // Company info. snprintf(g_administration.company_info.id, sizeof(g_administration.company_info.id), "C/%d", administration_create_id()); strops_copy(g_administration.company_info.name, "Aldrik Ramaekers", sizeof(g_administration.company_info.name)); strops_copy(g_administration.company_info.address.address1, "Keerderstraat 81", sizeof(g_administration.company_info.address.address1)); strops_copy(g_administration.company_info.address.address2, "6226XW Maastricht", sizeof(g_administration.company_info.address.address2)); strops_copy(g_administration.company_info.address.country_code, "NL", sizeof(g_administration.company_info.address.country_code)); strops_copy(g_administration.company_info.taxid, "123", sizeof(g_administration.company_info.taxid)); strops_copy(g_administration.company_info.businessid, "123", sizeof(g_administration.company_info.businessid)); g_administration.next_id++; // Invoices #define ADD_INVOICE(_outgoing)\ {\ invoice inv = administration_invoice_create_empty();\ if (_outgoing) inv.supplier = administration_company_info_get(); else inv.supplier = *administation_get_random_contact();\ if (_outgoing) inv.customer = *administation_get_random_contact(); else inv.customer = administration_company_info_get();\ administation_get_random_project(inv.project_id);\ administation_get_random_cost_center(inv.cost_center_id);\ inv.is_outgoing = _outgoing;\ inv.status = administation_get_random_invoice_status();\ inv.issued_at = time(NULL) - (86400 * (rand() % 720));\ inv.delivered_at = inv.issued_at;\ inv.expires_at = inv.issued_at + administration_get_default_invoice_expire_duration();\ administation_get_random_billing_items(&inv);\ administration_invoice_add(&inv);\ } // Create about 2 years of random data. for (int i = 0; i < 410; i++) ADD_INVOICE(1); for (int i = 0; i < 700; i++) ADD_INVOICE(0); } static s32 administration_create_sequence_number() { return g_administration.next_sequence_number; } // Setup functions. // ======================= void administration_create() { g_administration.next_id = 1; g_administration.next_sequence_number = 1; list_init(&g_administration.invoices); list_init(&g_administration.contacts); list_init(&g_administration.projects); list_init(&g_administration.tax_brackets); list_init(&g_administration.cost_centers); strops_copy(g_administration.path, "", sizeof(g_administration.path)); administration_create_default_tax_brackets(); administration_create_default_cost_centers(); administration_create_debug_data(); administration_writer_save_all(); } static void administration_destroy_list(list_t *list) { list_iterator_start(list); while (list_iterator_hasnext(list)) { void* c = (void *)list_iterator_next(list); free(c); } list_iterator_stop(list); list_destroy(list); } void administration_destroy() { administration_destroy_list(&g_administration.invoices); administration_destroy_list(&g_administration.contacts); administration_destroy_list(&g_administration.projects); administration_destroy_list(&g_administration.tax_brackets); administration_destroy_list(&g_administration.cost_centers); } // General functions. // ======================= char* administration_file_path_get() { return g_administration.path; } contact administration_company_info_get() { return g_administration.company_info; } void administration_company_info_set(contact data) { g_administration.company_info = data; } administration* administation_get() { return &g_administration; } // Contact functions. // ======================= bool administration_contact_add(contact data) { if (!administration_contact_is_valid(data)) return false; contact* new_contact = (contact*)malloc(sizeof(contact)); memcpy((void*)new_contact, (void*)&data, sizeof(contact)); list_append(&g_administration.contacts, new_contact); g_administration.next_id++; return true; } bool administration_contact_can_be_deleted(contact data) { (void)data; // TODO return true; } bool administration_contact_update(contact data) { if (!administration_contact_is_valid(data)) return false; list_iterator_start(&g_administration.contacts); while (list_iterator_hasnext(&g_administration.contacts)) { contact* c = (contact *)list_iterator_next(&g_administration.contacts); if (strcmp(c->id, data.id) == 0) { memcpy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.contacts); return true; } } list_iterator_stop(&g_administration.contacts); return false; } bool administration_contact_remove(contact data) { list_iterator_start(&g_administration.contacts); while (list_iterator_hasnext(&g_administration.contacts)) { contact* c = (contact *)list_iterator_next(&g_administration.contacts); if (strcmp(c->id, data.id) == 0) { list_iterator_stop(&g_administration.contacts); list_delete(&g_administration.contacts, c); free(c); return true; } } list_iterator_stop(&g_administration.contacts); return false; } u32 administration_contact_count() { return list_size(&g_administration.contacts); } u32 administration_contact_get_all(contact* buffer) { u32 write_cursor = 0; list_iterator_start(&g_administration.contacts); while (list_iterator_hasnext(&g_administration.contacts)) { contact c = *(contact *)list_iterator_next(&g_administration.contacts); buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.contacts); return write_cursor; } u32 administration_contact_get_partial_list(u32 page_index, u32 page_size, contact* buffer) { assert(buffer); u32 write_cursor = 0; u32 read_start = page_index * page_size; list_iterator_start(&g_administration.contacts); while (list_iterator_hasnext(&g_administration.contacts)) { contact c = *(contact *)list_iterator_next(&g_administration.contacts); if (g_administration.contacts.iter_pos <= read_start) continue; buffer[write_cursor++] = c; if (write_cursor >= page_size) break; } list_iterator_stop(&g_administration.contacts); return write_cursor; } bool administration_contact_get_by_id(contact* buffer, char* id) { // Include company info in contact lookup because this might be // used in forms. if (strcmp(id, g_administration.company_info.id) == 0) { *buffer = g_administration.company_info; return true; } bool result = false; list_iterator_start(&g_administration.contacts); while (list_iterator_hasnext(&g_administration.contacts)) { contact c = *(contact *)list_iterator_next(&g_administration.contacts); if (strcmp(c.id, id) == 0) { list_iterator_stop(&g_administration.projects); *buffer = c; result = true; } } list_iterator_stop(&g_administration.contacts); return result; } int administration_contact_get_autocompletions(contact* buffer, int buf_size, char* name) { int write_cursor = 0; if (name[0] == '\0') return 0; list_iterator_start(&g_administration.contacts); while (list_iterator_hasnext(&g_administration.contacts)) { contact c = *(contact *)list_iterator_next(&g_administration.contacts); if (strops_stristr(c.name, name)) { buffer[write_cursor++] = c; if (write_cursor >= buf_size) break; } } list_iterator_stop(&g_administration.contacts); return write_cursor; } bool administration_contact_is_valid(contact data) { if (data.type == contact_type::CONTACT_CONSUMER) { return strlen(data.name) > 0 && strlen(data.address.address1) > 0 && strlen(data.address.address2) > 0 && strlen(data.address.country_code) > 0; } else if (data.type == contact_type::CONTACT_BUSINESS) { return strlen(data.name) > 0 && strlen(data.address.address1) > 0 && strlen(data.address.address2) > 0 && strlen(data.address.country_code) > 0 && strlen(data.taxid) > 0 && strlen(data.businessid); } return false; } contact administration_contact_create_empty() { contact result; memset(&result, 0, sizeof(contact)); snprintf(result.id, sizeof(result.id), "C/%d", administration_create_id()); return result; } bool administration_contact_equals(contact c1, contact c2) { return memcmp(&c1, &c2, sizeof(contact)) == 0; } // Project functions. // ======================= u32 administration_project_count() { return list_size(&g_administration.projects); } u32 administration_project_get_all(project* buffer) { u32 write_cursor = 0; list_iterator_start(&g_administration.projects); while (list_iterator_hasnext(&g_administration.projects)) { project c = *(project *)list_iterator_next(&g_administration.projects); buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.projects); return write_cursor; } u32 administration_project_get_partial_list(u32 page_index, u32 page_size, project* buffer) { assert(buffer); u32 write_cursor = 0; u32 read_start = page_index * page_size; list_iterator_start(&g_administration.projects); while (list_iterator_hasnext(&g_administration.projects)) { project c = *(project *)list_iterator_next(&g_administration.projects); if (g_administration.projects.iter_pos <= read_start) continue; buffer[write_cursor++] = c; if (write_cursor >= page_size) break; } list_iterator_stop(&g_administration.projects); return write_cursor; } void administration_project_cancel(project data) { data.end_date = time(NULL); data.state = project_state::PROJECT_CANCELLED; administration_project_update(data); } bool administration_project_is_valid(project data) { return strlen(data.description) > 0; } char* administration_project_get_status_string(project data) { switch(data.state) { case project_state::PROJECT_RUNNING: return "project.state.running"; case project_state::PROJECT_PAUSED: return "project.state.paused"; case project_state::PROJECT_CANCELLED: return "project.state.cancelled"; default: assert(0); break; } return ""; } bool administration_project_add(project data) { if (!administration_project_is_valid(data)) return false; data.state = project_state::PROJECT_RUNNING; data.start_date = time(NULL); data.end_date = 0; project* new_project = (project*)malloc(sizeof(project)); memcpy((void*)new_project, (void*)&data, sizeof(project)); list_append(&g_administration.projects, new_project); g_administration.next_id++; return true; } bool administration_project_update(project data) { if (!administration_project_is_valid(data)) return false; list_iterator_start(&g_administration.projects); while (list_iterator_hasnext(&g_administration.projects)) { project* c = (project *)list_iterator_next(&g_administration.projects); if (strcmp(c->id, data.id) == 0) { memcpy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.projects); return true; } } list_iterator_stop(&g_administration.projects); return false; } bool administration_project_remove(project data) { list_iterator_start(&g_administration.projects); while (list_iterator_hasnext(&g_administration.projects)) { project* c = (project *)list_iterator_next(&g_administration.projects); if (strcmp(c->id, data.id) == 0) { list_iterator_stop(&g_administration.projects); list_delete(&g_administration.projects, c); free(c); return true; } } list_iterator_stop(&g_administration.projects); return false; } project administration_project_create_empty() { project result; memset(&result, 0, sizeof(project)); snprintf(result.id, sizeof(result.id), "P/%d", administration_create_id()); return result; } // Tax bracket functions. // ======================= static bool administration_get_tax_bracket_by_id(country_tax_bracket* buffer, char* id) { assert(buffer); list_iterator_start(&g_administration.tax_brackets); while (list_iterator_hasnext(&g_administration.tax_brackets)) { country_tax_bracket c = *(country_tax_bracket *)list_iterator_next(&g_administration.tax_brackets); if (strcmp(c.id, id) == 0) { *buffer = c; list_iterator_stop(&g_administration.tax_brackets); return true; } } list_iterator_stop(&g_administration.tax_brackets); return false; } u32 administration_tax_bracket_count() { return list_size(&g_administration.tax_brackets); } bool administration_tax_bracket_add(country_tax_bracket data) { ADD_BRACKET(data.country_code, data.rate, ""); list_attributes_comparator(&g_administration.tax_brackets, compare_tax_countries); list_sort(&g_administration.tax_brackets, -1); return true; } u32 administration_tax_bracket_get_by_country(country_tax_bracket* buffer, char* country_code) { assert(buffer); u32 write_cursor = 0; list_iterator_start(&g_administration.tax_brackets); while (list_iterator_hasnext(&g_administration.tax_brackets)) { country_tax_bracket c = *(country_tax_bracket *)list_iterator_next(&g_administration.tax_brackets); if (strcmp(c.country_code, country_code) == 0 || strcmp(c.country_code, "00") == 0) buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.tax_brackets); return write_cursor; } u32 administration_tax_bracket_get_all(country_tax_bracket* buffer) { assert(buffer); u32 write_cursor = 0; list_iterator_start(&g_administration.tax_brackets); while (list_iterator_hasnext(&g_administration.tax_brackets)) { country_tax_bracket c = *(country_tax_bracket *)list_iterator_next(&g_administration.tax_brackets); buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.tax_brackets); return write_cursor; } bool administration_tax_bracket_update(country_tax_bracket data) { list_iterator_start(&g_administration.tax_brackets); while (list_iterator_hasnext(&g_administration.tax_brackets)) { country_tax_bracket* c = (country_tax_bracket *)list_iterator_next(&g_administration.tax_brackets); if (strcmp(c->id, data.id) == 0) { memcpy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.tax_brackets); return true; } } list_iterator_stop(&g_administration.tax_brackets); return false; } // Cost center functions. // ======================= u32 administration_cost_center_count() { return list_size(&g_administration.cost_centers); } u32 administration_cost_center_get_all(cost_center* buffer) { assert(buffer); u32 write_cursor = 0; list_iterator_start(&g_administration.cost_centers); while (list_iterator_hasnext(&g_administration.cost_centers)) { cost_center c = *(cost_center *)list_iterator_next(&g_administration.cost_centers); buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.cost_centers); return write_cursor; } static bool administration_get_cost_center_by_code(char* code, cost_center* buffer) { bool result = false; list_iterator_start(&g_administration.cost_centers); while (list_iterator_hasnext(&g_administration.cost_centers)) { cost_center c = *(cost_center *)list_iterator_next(&g_administration.cost_centers); *buffer = c; if (strcmp(code, c.code) == 0) { result = true; break; } } list_iterator_stop(&g_administration.cost_centers); return result; } bool administration_cost_center_verify_description(char* text) { return strlen(text) != 0; } bool administration_cost_center_verify_code(char* code) { if (strlen(code) == 0) return false; cost_center cost_center; bool found = administration_get_cost_center_by_code(code, &cost_center); return !found; } bool administration_cost_center_add(cost_center data) { cost_center cs; bool found = administration_get_cost_center_by_code(data.code, &cs); if (found) return false; cost_center* tb = (cost_center*)malloc(sizeof(cost_center)); snprintf(tb->id, sizeof(tb->id), "E/%d", administration_create_id()); memcpy(tb->description, data.description, sizeof(tb->description)); memcpy(tb->code, data.code, sizeof(tb->code)); list_append(&g_administration.cost_centers, tb); g_administration.next_id++; return true; } bool administration_cost_center_update(cost_center data) { list_iterator_start(&g_administration.cost_centers); while (list_iterator_hasnext(&g_administration.cost_centers)) { cost_center* c = (cost_center *)list_iterator_next(&g_administration.cost_centers); if (strcmp(c->id, data.id) == 0) { memcpy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.cost_centers); return true; } } list_iterator_stop(&g_administration.cost_centers); return false; } // Invoice functions. // ======================= static char* administration_get_default_currency_for_country(char* country_code) { if (country_code == NULL || strlen(country_code) != 2) return "EUR"; // default // Non-euro EU currencies if (strcmp(country_code, "BG") == 0) return "BGN"; // Bulgaria else if (strcmp(country_code, "CZ") == 0) return "CZK"; // Czechia else if (strcmp(country_code, "DK") == 0) return "DKK"; // Denmark else if (strcmp(country_code, "HU") == 0) return "HUF"; // Hungary else if (strcmp(country_code, "PL") == 0) return "PLN"; // Poland else if (strcmp(country_code, "RO") == 0) return "RON"; // Romania else if (strcmp(country_code, "SE") == 0) return "SEK"; // Sweden // Eurozone members else if (strcmp(country_code, "AT") == 0) return "EUR"; // Austria else if (strcmp(country_code, "BE") == 0) return "EUR"; // Belgium else if (strcmp(country_code, "CY") == 0) return "EUR"; // Cyprus else if (strcmp(country_code, "DE") == 0) return "EUR"; // Germany else if (strcmp(country_code, "EE") == 0) return "EUR"; // Estonia else if (strcmp(country_code, "ES") == 0) return "EUR"; // Spain else if (strcmp(country_code, "FI") == 0) return "EUR"; // Finland else if (strcmp(country_code, "FR") == 0) return "EUR"; // France else if (strcmp(country_code, "GR") == 0) return "EUR"; // Greece else if (strcmp(country_code, "HR") == 0) return "EUR"; // Croatia else if (strcmp(country_code, "IE") == 0) return "EUR"; // Ireland else if (strcmp(country_code, "IT") == 0) return "EUR"; // Italy else if (strcmp(country_code, "LT") == 0) return "EUR"; // Lithuania else if (strcmp(country_code, "LU") == 0) return "EUR"; // Luxembourg else if (strcmp(country_code, "LV") == 0) return "EUR"; // Latvia else if (strcmp(country_code, "MT") == 0) return "EUR"; // Malta else if (strcmp(country_code, "NL") == 0) return "EUR"; // Netherlands else if (strcmp(country_code, "PT") == 0) return "EUR"; // Portugal else if (strcmp(country_code, "SI") == 0) return "EUR"; // Slovenia else if (strcmp(country_code, "SK") == 0) return "EUR"; // Slovakia // Default fallback return "EUR"; } invoice administration_invoice_create_empty() { invoice result; memset(&result, 0, sizeof(invoice)); snprintf(result.id, sizeof(result.id), "I/%d", administration_create_id()); snprintf(result.sequential_number, sizeof(result.id), "INV%010d", administration_create_sequence_number()); result.issued_at = time(NULL); result.delivered_at = time(NULL); result.expires_at = time(NULL) + administration_get_default_invoice_expire_duration(); list_init(&result.billing_items); // @leak strops_copy(result.currency, administration_get_default_currency_for_country(g_administration.company_info.address.country_code), MAX_LEN_CURRENCY); return result; } static void administration_recalculate_invoice_totals(invoice* invoice) { invoice->tax = 0.0f; invoice->total = 0.0f; invoice->net = 0.0f; list_iterator_start(&invoice->billing_items); while (list_iterator_hasnext(&invoice->billing_items)) { billing_item* c = (billing_item *)list_iterator_next(&invoice->billing_items); invoice->tax += c->tax; invoice->total += c->total; invoice->net += c->net; } list_iterator_stop(&invoice->billing_items); } void administration_invoice_set_currency(invoice* invoice, char* currency) { strops_copy(invoice->currency, currency, MAX_LEN_CURRENCY); list_iterator_start(&invoice->billing_items); while (list_iterator_hasnext(&invoice->billing_items)) { billing_item* c = (billing_item *)list_iterator_next(&invoice->billing_items); strops_copy(c->currency, currency, MAX_LEN_CURRENCY); } list_iterator_stop(&invoice->billing_items); } bool administration_invoice_is_valid(invoice* invoice) { if (list_size(&invoice->billing_items) == 0) return false; if (invoice->is_triangulation && !administration_contact_is_valid(invoice->addressee)) return false; if (!administration_contact_is_valid(invoice->customer)) return false; if (!administration_contact_is_valid(invoice->supplier)) return false; return true; } bool administration_invoice_remove(invoice* inv) { list_iterator_start(&g_administration.invoices); while (list_iterator_hasnext(&g_administration.invoices)) { invoice* c = (invoice *)list_iterator_next(&g_administration.invoices); if (strcmp(c->id, inv->id) == 0) { list_iterator_stop(&g_administration.invoices); administration_destroy_list(&c->billing_items); list_delete(&g_administration.invoices, c); if (inv->is_outgoing) g_administration.invoice_count--; else g_administration.expense_count--; free(c); return true; } } list_iterator_stop(&g_administration.invoices); return false; } static void administration_invoice_set_refs(invoice* inv) { // This function makes sure that contact info referenced in the IDs actually // matches the contact info stored in administation, if not we make new contacts. // Check if supplier info is equal to contact stored in supplier id, in case we autocomplete and edit data after, // this should be handled as a new contact. contact lookup_buffer; if (administration_contact_get_by_id(&lookup_buffer, inv->supplier.id)) { if (!administration_contact_equals(lookup_buffer, inv->supplier)) { // Zero id so new contact is created for supplier. inv->supplier_id[0] = '\0'; inv->supplier.id[0] = '\0'; } else { // Store supplier id in invoice, (only id is stored to disk, supplier field is filled on load). strops_copy(inv->supplier_id, inv->supplier.id, sizeof(inv->supplier_id)); } } // Supplier is valid but supplier id is unset means we need to register a new contact. if (strcmp(inv->supplier_id, "") == 0) { contact new_contact = administration_contact_create_empty(); strops_copy(inv->supplier_id, new_contact.id, sizeof(new_contact.id)); strops_copy(inv->supplier.id, new_contact.id, sizeof(new_contact.id)); memcpy(&new_contact, &inv->supplier, sizeof(contact)); administration_contact_add(new_contact); inv->supplier = new_contact; } // Check if customer info is equal to contact stored in customer id, in case we autocomplete and edit data after, // this should be handled as a new contact. if (administration_contact_get_by_id(&lookup_buffer, inv->customer.id)) { if (!administration_contact_equals(lookup_buffer, inv->customer)) { // Zero id so new contact is created for supplier. inv->customer_id[0] = '\0'; inv->customer.id[0] = '\0'; } else { // Store customer id in invoice, (only id is stored to disk, supplier field is filled on load). strops_copy(inv->customer_id, inv->customer.id, sizeof(inv->customer_id)); } } // Invoice is valid but customer id is unset means we need to register a new contact. if (strcmp(inv->customer_id, "") == 0) { contact new_contact = administration_contact_create_empty(); strops_copy(inv->customer_id, new_contact.id, sizeof(new_contact.id)); strops_copy(inv->customer.id, new_contact.id, sizeof(new_contact.id)); memcpy(&new_contact, &inv->customer, sizeof(contact)); administration_contact_add(new_contact); inv->customer = new_contact; } // Addressee is same as customer. if (!inv->is_triangulation) { memcpy(&inv->addressee, &inv->customer, sizeof(contact)); } } bool administration_invoice_update(invoice* inv) { if (!administration_invoice_is_valid(inv)) return false; administration_invoice_set_refs(inv); list_iterator_start(&g_administration.invoices); while (list_iterator_hasnext(&g_administration.invoices)) { invoice* c = (invoice *)list_iterator_next(&g_administration.invoices); if (strcmp(c->id, inv->id) == 0) { memcpy(c, inv, sizeof(invoice)); list_iterator_stop(&g_administration.invoices); return true; } } list_iterator_stop(&g_administration.invoices); return false; } bool administration_invoice_add(invoice* inv) { if (!administration_invoice_is_valid(inv)) return false; administration_invoice_set_refs(inv); invoice copy = administration_invoice_create_copy(inv); // Create copy to make copy of billing item list. invoice* new_inv = (invoice*)malloc(sizeof(invoice)); memcpy(new_inv, ©, sizeof(invoice)); list_append(&g_administration.invoices, new_inv); g_administration.next_id++; g_administration.next_sequence_number++; if (inv->is_outgoing) g_administration.invoice_count++; else g_administration.expense_count++; return true; } invoice administration_invoice_create_copy(invoice* inv) { invoice new_inv = administration_invoice_create_empty(); list_t billing_items = new_inv.billing_items; memcpy((void*)&new_inv, (void*)inv, sizeof(invoice)); new_inv.billing_items = billing_items; list_iterator_start(&inv->billing_items); while (list_iterator_hasnext(&inv->billing_items)) { billing_item* c = (billing_item *)list_iterator_next(&inv->billing_items); billing_item* item_copy = (billing_item*)malloc(sizeof(billing_item)); memcpy(item_copy, c, sizeof(billing_item)); list_append(&new_inv.billing_items, item_copy); } list_iterator_stop(&inv->billing_items); return new_inv; } u32 administration_invoice_count() { return list_size(&g_administration.invoices); } u32 administation_invoice_get_incomming_count() { return g_administration.expense_count; } u32 administation_invoice_get_outgoing_count() { return g_administration.invoice_count; } static u32 administration_invoice_get_partial_list(u32 page_index, u32 page_size, invoice* buffer, bool want_outgoing) { assert(buffer); u32 write_cursor = 0; u32 read_start = page_index * page_size; u32 read_cursor = 0; list_iterator_start(&g_administration.invoices); while (list_iterator_hasnext(&g_administration.invoices)) { invoice c = *(invoice *)list_iterator_next(&g_administration.invoices); if (c.is_outgoing != want_outgoing) continue; // Continue without incrementing read cursor read_cursor++; if (read_cursor <= read_start) continue; buffer[write_cursor++] = c; if (write_cursor >= page_size) break; } list_iterator_stop(&g_administration.invoices); return write_cursor; } u32 administration_invoice_get_partial_list_outgoing(u32 page_index, u32 page_size, invoice* buffer) { return administration_invoice_get_partial_list(page_index, page_size, buffer, 1); } u32 administration_invoice_get_partial_list_incomming(u32 page_index, u32 page_size, invoice* buffer) { return administration_invoice_get_partial_list(page_index, page_size, buffer, 0); } char* administration_invoice_get_status_string(invoice* invoice) { switch(invoice->status) { case invoice_status::INVOICE_CONCEPT: return "invoice.status.concept"; case invoice_status::INVOICE_SENT: return "invoice.status.sent"; case invoice_status::INVOICE_REMINDED: return "invoice.status.reminded"; case invoice_status::INVOICE_PAID: return "invoice.status.paid"; case invoice_status::INVOICE_EXPIRED: return "invoice.status.expired"; case invoice_status::INVOICE_CANCELLED: return "invoice.status.cancelled"; case invoice_status::INVOICE_REFUNDED: return "invoice.status.refunded"; case invoice_status::INVOICE_CORRECTED: return "invoice.status.corrected"; case invoice_status::INVOICE_RECEIVED: return "invoice.status.received"; default: assert(0); break; } return ""; } // Billing item functions. // ======================= static void administration_recalculate_billing_item_totals(billing_item* item) { if (item->amount_is_percentage) { item->net = item->net_per_item * (item->amount / 100.0f); } else { item->net = item->net_per_item * item->amount; } if (item->discount != 0) { if (item->discount_is_percentage) { item->net -= item->net * (item->discount / 100.0f); } else { item->net -= item->discount; } } country_tax_bracket bracket; if (administration_get_tax_bracket_by_id(&bracket, item->tax_bracket_id)) { item->tax = item->net * (bracket.rate/100.0f); } item->total = item->net + item->tax; } u32 administration_billing_item_count(invoice* invoice) { return list_size(&invoice->billing_items); } u32 administration_billing_item_get_all_for_invoice(invoice* invoice, billing_item* buffer) { u32 write_cursor = 0; list_iterator_start(&invoice->billing_items); while (list_iterator_hasnext(&invoice->billing_items)) { billing_item c = *(billing_item *)list_iterator_next(&invoice->billing_items); buffer[write_cursor++] = c; } list_iterator_stop(&invoice->billing_items); return write_cursor; } bool administration_billing_item_remove_from_invoice(invoice* invoice, billing_item item) { list_iterator_start(&invoice->billing_items); while (list_iterator_hasnext(&invoice->billing_items)) { billing_item* c = (billing_item *)list_iterator_next(&invoice->billing_items); if (strcmp(c->id, item.id) == 0) { list_iterator_stop(&invoice->billing_items); list_delete(&invoice->billing_items, c); free(c); return true; } } list_iterator_stop(&invoice->billing_items); return false; } bool administration_billing_item_update_in_invoice(invoice* invoice, billing_item item) { list_iterator_start(&invoice->billing_items); while (list_iterator_hasnext(&invoice->billing_items)) { billing_item* c = (billing_item *)list_iterator_next(&invoice->billing_items); if (strcmp(c->id, item.id) == 0) { memcpy(c, &item, sizeof(billing_item)); list_iterator_stop(&invoice->billing_items); administration_recalculate_billing_item_totals(c); administration_recalculate_invoice_totals(invoice); return true; } } list_iterator_stop(&invoice->billing_items); return false; } bool administration_billing_item_add_to_invoice(invoice* invoice, billing_item item) { if (administration_billing_item_count(invoice) >= MAX_BILLING_ITEMS) return false; billing_item* tb = (billing_item*)malloc(sizeof(billing_item)); memcpy(tb, &item, sizeof(billing_item)); snprintf(tb->id, sizeof(tb->id), "B/%d", administration_create_id()); strops_copy(tb->invoice_id, invoice->id, sizeof(tb->invoice_id)); strops_copy(tb->currency, invoice->currency, MAX_LEN_CURRENCY); // Set billing item currency to invoice currency. administration_recalculate_billing_item_totals(tb); administration_recalculate_invoice_totals(invoice); list_append(&invoice->billing_items, tb); g_administration.next_id++; return true; } billing_item administration_billing_item_create_empty() { billing_item item; memset(&item, 0, sizeof(billing_item)); item.amount = 1; return item; }