/* * 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 "memops.hpp" #include "logger.hpp" #include "strops.hpp" #include "countries.hpp" #include "administration.hpp" #include "administration_writer.hpp" static ledger g_administration; static data_changed_event data_changed_event_callback = 0; static data_deleted_event data_deleted_event_callback = 0; static invoice_changed_event invoice_changed_event_callback = 0; static contact_changed_event contact_changed_event_callback = 0; static taxrate_changed_event taxrate_changed_event_callback = 0; static costcenter_changed_event costcenter_changed_event_callback = 0; static project_changed_event project_changed_event_callback = 0; static s32 create_id() { return g_administration.next_id; } static int compare_tax_countries(const void *a, const void *b) { tax_rate *objA = (tax_rate *)a; tax_rate *objB = (tax_rate *)b; return strcmp(objA->country_code, objB->country_code); } time_t administration::get_default_invoice_expire_duration() // TODO depricated { return (30 * 24 * 60 * 60); // 30 days } static char* get_default_currency_for_country(char* country_code); static void create_default_tax_rates() { #define ADD_BRACKET(_country, _rate, _code)\ {\ tax_rate* tb = (tax_rate*)memops::alloc(sizeof(tax_rate));\ strops::format(tb->id, sizeof(tb->id), "T/%d", create_id());\ memops::copy(tb->country_code, _country, sizeof(tb->country_code));\ tb->rate = _rate;\ memops::copy(tb->category_code, _code, sizeof(tb->category_code));\ list_append(&g_administration.tax_rates, tb);\ g_administration.next_id++;\ if (taxrate_changed_event_callback) taxrate_changed_event_callback(tb);\ if (data_changed_event_callback) data_changed_event_callback();\ } // General rates shared between countries. // Category options: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/ // TODO unimplemented category L, M, B ADD_BRACKET("00", 0.0f, "AE"); // Vat Reverse Charge ADD_BRACKET("00", 0.0f, "E"); // Exempt from Tax ADD_BRACKET("00", 0.0f, "Z"); // Zero rated goods ADD_BRACKET("00", 0.0f, "G"); // Free export item, VAT not charged ADD_BRACKET("00", 0.0f, "O"); // Services outside scope of tax ADD_BRACKET("00", 0.0f, "K"); // VAT exempt for EEA intra-community supply of goods and services // Austria ADD_BRACKET("AT", 20.0f, "S"); ADD_BRACKET("AT", 10.0f, "S"); ADD_BRACKET("AT", 13.0f, "S"); // Belgium ADD_BRACKET("BE", 21.0f, "S"); ADD_BRACKET("BE", 6.0f, "S"); ADD_BRACKET("BE", 12.0f, "S"); // Bulgaria ADD_BRACKET("BG", 20.0f, "S"); ADD_BRACKET("BG", 9.0f, "S"); // Cyprus ADD_BRACKET("CY", 19.0f, "S"); ADD_BRACKET("CY", 5.0f, "S"); ADD_BRACKET("CY", 9.0f, "S"); // Czechia ADD_BRACKET("CZ", 21.0f, "S"); ADD_BRACKET("CZ", 12.0f, "S"); // Croatia ADD_BRACKET("HR", 25.0f, "S"); ADD_BRACKET("HR", 5.0f, "S"); ADD_BRACKET("HR", 13.0f, "S"); // Denmark ADD_BRACKET("DK", 25.0f, "S"); // Estonia ADD_BRACKET("EE", 22.0f, "S"); ADD_BRACKET("EE", 9.0f, "S"); // Finland ADD_BRACKET("FI", 25.5f, "S"); ADD_BRACKET("FI", 10.0f, "S"); ADD_BRACKET("FI", 14.0f, "S"); // France ADD_BRACKET("FR", 20.0f, "S"); ADD_BRACKET("FR", 5.5f, "S"); ADD_BRACKET("FR", 10.0f, "S"); ADD_BRACKET("FR", 2.1f, "S"); // Germany ADD_BRACKET("DE", 19.0f, "S"); ADD_BRACKET("DE", 7.0f, "S"); // Greece ADD_BRACKET("GR", 24.0f, "S"); ADD_BRACKET("GR", 6.0f, "S"); ADD_BRACKET("GR", 13.0f, "S"); // Hungary ADD_BRACKET("HU", 27.0f, "S"); ADD_BRACKET("HU", 5.0f, "S"); ADD_BRACKET("HU", 18.0f, "S"); // Ireland ADD_BRACKET("IE", 23.0f, "S"); ADD_BRACKET("IE", 9.0f, "S"); ADD_BRACKET("IE", 13.5f, "S"); ADD_BRACKET("IE", 4.8f, "S"); // Italy ADD_BRACKET("IT", 22.0f, "S"); ADD_BRACKET("IT", 5.0f, "S"); ADD_BRACKET("IT", 10.0f, "S"); ADD_BRACKET("IT", 4.0f, "S"); // Latvia ADD_BRACKET("LV", 21.0f, "S"); ADD_BRACKET("LV", 5.0f, "S"); ADD_BRACKET("LV", 12.0f, "S"); // Lithuania ADD_BRACKET("LT", 21.0f, "S"); ADD_BRACKET("LT", 5.0f, "S"); ADD_BRACKET("LT", 9.0f, "S"); // Luxembourg ADD_BRACKET("LU", 17.0f, "S"); ADD_BRACKET("LU", 8.0f, "S"); ADD_BRACKET("LU", 14.0f, "S"); ADD_BRACKET("LU", 3.0f, "S"); // Malta ADD_BRACKET("MT", 18.0f, "S"); ADD_BRACKET("MT", 5.0f, "S"); ADD_BRACKET("MT", 7.0f, "S"); // Netherlands ADD_BRACKET("NL", 21.0f, "S"); ADD_BRACKET("NL", 9.0f, "S"); // Poland ADD_BRACKET("PL", 23.0f, "S"); ADD_BRACKET("PL", 5.0f, "S"); ADD_BRACKET("PL", 8.0f, "S"); // Portugal ADD_BRACKET("PT", 23.0f, "S"); ADD_BRACKET("PT", 6.0f, "S"); ADD_BRACKET("PT", 13.0f, "S"); // Romania ADD_BRACKET("RO", 19.0f, "S"); ADD_BRACKET("RO", 5.0f, "S"); ADD_BRACKET("RO", 9.0f, "S"); // Slovakia ADD_BRACKET("SK", 23.0f, "S"); ADD_BRACKET("SK", 5.0f, "S"); ADD_BRACKET("SK", 19.0f, "S"); // Slovenia ADD_BRACKET("SI", 22.0f, "S"); ADD_BRACKET("SI", 5.0f, "S"); ADD_BRACKET("SI", 9.5f, "S"); // Spain ADD_BRACKET("ES", 21.0f, "S"); ADD_BRACKET("ES", 10.0f, "S"); ADD_BRACKET("ES", 4.0f, "S"); // Sweden ADD_BRACKET("SE", 25.0f, "S"); ADD_BRACKET("SE", 6.0f, "S"); ADD_BRACKET("SE", 12.0f, "S"); list_attributes_comparator(&g_administration.tax_rates, compare_tax_countries); list_sort(&g_administration.tax_rates, -1); } static void create_default_cost_centers() { #define ADD_COSTCENTER(_description, _code)\ {\ cost_center* tb = (cost_center*)memops::alloc(sizeof(cost_center));\ strops::format(tb->id, sizeof(tb->id), "E/%d", create_id());\ memops::copy(tb->description, _description, sizeof(tb->description));\ memops::copy(tb->code, _code, sizeof(tb->code));\ list_append(&g_administration.cost_centers, tb);\ g_administration.next_id++;\ if (costcenter_changed_event_callback) costcenter_changed_event_callback(tb);\ if (data_changed_event_callback) data_changed_event_callback();\ } 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 s32 create_sequence_number() { return g_administration.next_sequence_number; } // Callback functions. // ======================= void administration::set_data_changed_event_callback(data_changed_event ev) { data_changed_event_callback = ev; } void administration::set_data_deleted_event_callback(data_deleted_event ev) { data_deleted_event_callback = ev; } void administration::set_invoice_changed_event_callback(invoice_changed_event ev) { invoice_changed_event_callback = ev; } void administration::set_contact_changed_event_callback(contact_changed_event ev) { contact_changed_event_callback = ev; } void administration::set_taxrate_changed_event_callback(taxrate_changed_event ev) { taxrate_changed_event_callback = ev; } void administration::set_costcenter_changed_event_callback(costcenter_changed_event ev) { costcenter_changed_event_callback = ev; } void administration::set_project_changed_event_callback(project_changed_event ev) { project_changed_event_callback = ev; } // Setup functions. // ======================= static bool is_initialized = false; void administration_create() { STOPWATCH_START; logger::clear(); is_initialized = true; list_init(&g_administration.invoices); list_init(&g_administration.contacts); list_init(&g_administration.projects); list_init(&g_administration.tax_rates); list_init(&g_administration.cost_centers); strops::copy(g_administration.path, "", sizeof(g_administration.path)); memset(&g_administration.ai_service, 0, sizeof(ai_service)); logger::info("Setup took %.3fms.", STOPWATCH_TIME); } static void administration_destroy_list(list_t *list) { list_iterator_start(list); while (list_iterator_hasnext(list)) { void* c = (void *)list_iterator_next(list); memops::unalloc(c); } list_iterator_stop(list); list_destroy(list); } void administration_destroy() { is_initialized = false; administration_destroy_list(&g_administration.invoices); administration_destroy_list(&g_administration.contacts); administration_destroy_list(&g_administration.projects); administration_destroy_list(&g_administration.tax_rates); administration_destroy_list(&g_administration.cost_centers); } void administration::create_from_file(char* save_file) { if (is_initialized) administration_destroy(); administration_create(); strops::copy(g_administration.path, save_file, sizeof(g_administration.path)); strops::copy(g_administration.program_version, config::PROGRAM_VERSION, sizeof(g_administration.program_version)); } void administration::create_empty(char* save_file) { if (is_initialized) administration_destroy(); administration_create(); g_administration.next_id = 2; g_administration.next_sequence_number = 1; strops::copy(g_administration.path, save_file, sizeof(g_administration.path)); strops::copy(g_administration.program_version, config::PROGRAM_VERSION, sizeof(g_administration.program_version)); administration::company_info_set(administration::contact_create_empty()); } void administration::create_default(char* save_file) { administration::create_empty(save_file); create_default_tax_rates(); create_default_cost_centers(); //administration::create_debug_data(); } // Other functions. // ======================= ai_service administration::get_ai_service() { return g_administration.ai_service; } void administration::set_ai_service(ai_service provider) { g_administration.ai_service = provider; if (data_changed_event_callback) data_changed_event_callback(); } void administration::set_next_id(s32 nr) { g_administration.next_id = nr; } void administration::set_next_sequence_number(s32 nr) { g_administration.next_sequence_number = nr; } s32 administration::get_next_id() { return g_administration.next_id; } s32 administration::get_next_sequence_number() { return g_administration.next_sequence_number; } bool administration::can_create_invoices() { return administration::contact_is_valid(g_administration.company_info) == A_ERR_SUCCESS; } char* administration::get_default_currency() { return g_administration.default_currency; } char* administration::get_currency_symbol_for_currency(char* code) { // Major European currencies if (strcmp(code, "EUR") == 0) return "€"; // Euro if (strcmp(code, "GBP") == 0) return "£"; // British Pound if (strcmp(code, "CHF") == 0) return "CHF"; // Swiss Franc (no special sign, usually "CHF") if (strcmp(code, "NOK") == 0) return "kr"; // Norwegian Krone if (strcmp(code, "SEK") == 0) return "kr"; // Swedish Krona if (strcmp(code, "DKK") == 0) return "kr"; // Danish Krone if (strcmp(code, "ISK") == 0) return "kr"; // Icelandic Króna if (strcmp(code, "CZK") == 0) return "Kč"; // Czech Koruna if (strcmp(code, "PLN") == 0) return "zł"; // Polish Złoty if (strcmp(code, "HUF") == 0) return "Ft"; // Hungarian Forint if (strcmp(code, "RON") == 0) return "lei"; // Romanian Leu if (strcmp(code, "BGN") == 0) return "лв"; // Bulgarian Lev if (strcmp(code, "HRK") == 0) return "kn"; // Croatian Kuna (before Euro, now EUR since 2023) if (strcmp(code, "RSD") == 0) return "дин"; // Serbian Dinar if (strcmp(code, "MKD") == 0) return "ден"; // Macedonian Denar if (strcmp(code, "ALL") == 0) return "L"; // Albanian Lek if (strcmp(code, "MDL") == 0) return "L"; // Moldovan Leu if (strcmp(code, "BYN") == 0) return "Br"; // Belarusian Ruble if (strcmp(code, "UAH") == 0) return "₴"; // Ukrainian Hryvnia if (strcmp(code, "RUB") == 0) return "₽"; // Russian Ruble if (strcmp(code, "TRY") == 0) return "₺"; // Turkish Lira // Unknown currency return "?"; } static void time_t_to_quarter(time_t time, u16* year, u8* quarter) { struct tm *lt = localtime(&time); //sprinf(buffer, "%dQ%d", lt->tm_mon / 3, lt->tm_year / 100); *year = (u16)((1900+lt->tm_year) % 100); *quarter = (u8)(lt->tm_mon / 3); } #if 0 static void administration_debug_print_income_statement(income_statement* statement) { for (u32 i = 0; i < statement->quarter_count; i++) { quarterly_report report = statement->quarters[i]; printf("=== %dQ%d ===\n", report.quarter+1, report.year); printf("general revenue: %.2f\n", report.uncategorized_revenue); printf("general taxes: %.2f\n", report.uncategorized_taxes); printf("general expenses: %.2f\n", report.uncategorized_expenses); for (u32 x = 0; x < report.report_count; x++) { project_report pr = report.reports[x]; project proj; administration::project_get_by_id(&proj, pr.project_id); printf("PROJECT: %s\n", proj.description); printf(" revenue: %.2f\n", pr.revenue); printf(" taxes: %.2f\n", pr.taxes); printf(" expenses: %.2f\n", pr.expenses_total); for (u32 y = 0; y < pr.expense_count; y++) { project_expense expense = pr.expenses[y]; cost_center costcenter; administration::cost_center_get_by_id(&costcenter, expense.cost_center_id); printf(" %s: %.2f\n", costcenter.code, expense.total); } } printf("\n"); } } #endif void administration::create_tax_statement(tax_statement* statement) { STOPWATCH_START; char* country_code = company_info_get().address.country_code; assert(statement); statement->quarter_count = 0; u32 invoice_count = administration::invoice_count(); if (invoice_count == 0) return; invoice* invoice_buffer = (invoice*)memops::alloc(sizeof(invoice)*invoice_count); invoice_count = administration::invoice_get_all(invoice_buffer); // Find oldest and youngest invoice. time_t oldest = INT64_MAX; time_t youngest = 0; for (u32 i = 0; i < invoice_count; i++) { if (invoice_buffer[i].delivered_at < oldest) oldest = invoice_buffer[i].delivered_at; if (invoice_buffer[i].delivered_at > youngest) youngest = invoice_buffer[i].delivered_at; } u16 oldest_year; u8 oldest_quarter; time_t_to_quarter(oldest, &oldest_year, &oldest_quarter); oldest_quarter = 0; u16 youngest_year; u8 youngest_quarter; time_t_to_quarter(youngest, &youngest_year, &youngest_quarter); youngest_quarter = 3; u32 num_quarters = (youngest_quarter+1 + (youngest_year*4)) - (oldest_quarter + (oldest_year*4)); assert(num_quarters <= MAX_LEN_INCOME_STATEMENT_REPORT_QUARTERS); assert(num_quarters % 4 == 0); // Generate quarters. for (u32 i = 0; i < num_quarters; i++) { tax_report quarter; quarter.year = oldest_year + (u16)((oldest_quarter+i) / 4); quarter.quarter = (u8)((oldest_quarter + (i)) % 4); quarter.line_count = 0; quarter.is_empty = 1; strops::format(quarter.quarter_str, MAX_LEN_SHORT_DESC, "%dQ%d", quarter.quarter+1, quarter.year); country::fill_tax_report_with_categories(country_code, &quarter); } // Fill quarters. for (u32 i = 0; i < invoice_count; i++) { invoice* inv = &invoice_buffer[i]; u16 yy; u8 qq; time_t_to_quarter(inv->issued_at, &yy, &qq); u32 report_index = (qq + (yy*4)) - (oldest_quarter + (oldest_year*4)); tax_report* quarter = &statement->reports[report_index]; assert(yy == quarter->year && qq == quarter->quarter); quarter->is_empty = 0; billing_item* item_buffer = (billing_item*)memops::alloc(sizeof(billing_item)*billing_item_count(inv)); u32 invoice_items = administration::billing_item_get_all_for_invoice(inv, item_buffer); for (u32 x = 0; x < invoice_items; x++) { char* tax_category = country::get_tax_category_for_billing_item(country_code, inv, &item_buffer[x]); for (u32 t = 0; t < quarter->line_count; t++) { if (strops::equals(quarter->lines[t].tax_category, tax_category)) { quarter->lines[t].total_net += inv->net; quarter->lines[t].total_tax += inv->tax; } } memops::unalloc(item_buffer); } } memops::unalloc(invoice_buffer); logger::info("Created tax statement in %.3fms.", STOPWATCH_TIME); } void administration::create_income_statement(income_statement* statement) { STOPWATCH_START; assert(statement); statement->quarter_count = 0; u32 invoice_count = administration::invoice_count(); if (invoice_count == 0) return; invoice* invoice_buffer = (invoice*)memops::alloc(sizeof(invoice)*invoice_count); invoice_count = administration::invoice_get_all(invoice_buffer); // Find oldest and youngest invoice. time_t oldest = INT64_MAX; time_t youngest = 0; for (u32 i = 0; i < invoice_count; i++) { if (invoice_buffer[i].delivered_at < oldest) oldest = invoice_buffer[i].delivered_at; if (invoice_buffer[i].delivered_at > youngest) youngest = invoice_buffer[i].delivered_at; } u16 oldest_year; u8 oldest_quarter; time_t_to_quarter(oldest, &oldest_year, &oldest_quarter); oldest_quarter = 0; u16 youngest_year; u8 youngest_quarter; time_t_to_quarter(youngest, &youngest_year, &youngest_quarter); youngest_quarter = 3; u32 num_quarters = (youngest_quarter+1 + (youngest_year*4)) - (oldest_quarter + (oldest_year*4)); assert(num_quarters <= MAX_LEN_INCOME_STATEMENT_REPORT_QUARTERS); assert(num_quarters % 4 == 0); u32 costcenter_count = 0; u32 project_count = 0; // Generate quarters. for (u32 i = 0; i < num_quarters; i++) { quarterly_report quarter; quarter.year = oldest_year + (u16)((oldest_quarter+i) / 4); quarter.quarter = (u8)((oldest_quarter + (i)) % 4); quarter.uncategorized_expenses = 0.0f; quarter.uncategorized_revenue = 0.0f; quarter.uncategorized_taxes = 0.0f; quarter.report_count = 0; quarter.is_empty = 1; quarter.profit = 0.0f; strops::format(quarter.quarter_str, MAX_LEN_SHORT_DESC, "%dQ%d", quarter.quarter+1, quarter.year); project_count = administration::project_count(); project* project_buffer = (project*)memops::alloc(sizeof(project)*project_count); project_count = administration::project_get_all(project_buffer); assert(project_count <= MAX_LEN_QUARTERLY_REPORT_PROJECTS); costcenter_count = administration::cost_center_count(); cost_center* costcenter_buffer = (cost_center*)memops::alloc(sizeof(cost_center)*costcenter_count); costcenter_count = administration::cost_center_get_all(costcenter_buffer); assert(costcenter_count <= MAX_LEN_PROJECT_REPORT_COSTCENTERS); for (u32 x = 0; x < project_count; x++) { project_report report; report.expenses_total = 0.0f; report.revenue = 0.0f; report.taxes = 0.0f; report.expense_count = 0; strops::copy(report.project_id, project_buffer[x].id, MAX_LEN_ID); strops::copy(report.description, project_buffer[x].description, MAX_LEN_LONG_DESC); for (u32 y = 0; y < costcenter_count; y++) { project_expense expense; strops::copy(expense.cost_center_id, costcenter_buffer[y].id, MAX_LEN_ID); strops::copy(expense.description, costcenter_buffer[y].description, MAX_LEN_LONG_DESC); expense.total = 0.0f; expense.expense_used_in_project = true; report.expenses[report.expense_count++] = expense; } quarter.reports[quarter.report_count++] = report; } statement->quarters[statement->quarter_count++] = quarter; memops::unalloc(costcenter_buffer); memops::unalloc(project_buffer); } // Fill quarters. for (u32 i = 0; i < invoice_count; i++) { invoice* inv = &invoice_buffer[i]; u16 yy; u8 qq; time_t_to_quarter(inv->issued_at, &yy, &qq); u32 report_index = (qq + (yy*4)) - (oldest_quarter + (oldest_year*4)); quarterly_report* quarter = &statement->quarters[report_index]; assert(yy == quarter->year && qq == quarter->quarter); quarter->is_empty = 0; if (strcmp(inv->project_id, "") == 0) { if (inv->is_outgoing) { quarter->uncategorized_revenue += inv->total; quarter->uncategorized_taxes += inv->tax; quarter->profit += inv->net; } else { quarter->uncategorized_expenses += inv->total; quarter->profit -= inv->total; } } else { int project_report_index = -1; for (u32 x = 0; x < quarter->report_count; x++) { if (strcmp(quarter->reports[x].project_id, inv->project_id) == 0) { project_report_index = x; break; } } assert(project_report_index != -1); project_report* report = &quarter->reports[project_report_index]; if (inv->is_outgoing) { report->revenue += inv->total; report->taxes += inv->tax; quarter->profit += inv->net; } else { report->expenses_total += inv->total; quarter->profit -= inv->total; if (strcmp(inv->cost_center_id, "") != 0) { int expense_report_index = -1; for (u32 x = 0; x < report->expense_count; x++) { if (strcmp(report->expenses[x].cost_center_id, inv->cost_center_id) == 0) { expense_report_index = x; break; } } assert(expense_report_index != -1); project_expense* expense = &report->expenses[expense_report_index]; expense->total += inv->total; } } } } // Remove unused cost centers from projects. int years = num_quarters / 4; for (int i = 0; i < years; i++) { for (u32 y = 0; y < costcenter_count; y++) { for (u32 x = 0; x < project_count; x++) { bool used = false; for (u32 q = 0; q < 4; q++) { quarterly_report* quarter = &statement->quarters[i*4+q]; if (quarter->reports[x].expenses[y].total != 0.0f) used = true; } for (u32 q = 0; q < 4; q++) { quarterly_report* quarter = &statement->quarters[i*4+q]; quarter->reports[x].expenses[y].expense_used_in_project = used; } } } } //administration_debug_print_income_statement(statement); memops::unalloc(invoice_buffer); logger::info("Created income statement in %.3fms.", STOPWATCH_TIME); } void administration::set_file_path(char* path) { strops::copy(g_administration.path, path, MAX_LEN_PATH); } char* administration::get_file_path() { return strlen(g_administration.path) == 0 ? NULL : g_administration.path; } contact administration::company_info_get() { return g_administration.company_info; } void administration::company_info_import(contact data) { g_administration.company_info = data; strops::copy(g_administration.default_currency, get_default_currency_for_country(g_administration.company_info.address.country_code), MAX_LEN_CURRENCY); } void administration::company_info_set(contact data) { strops::copy(data.id, MY_COMPANY_ID, sizeof(data.id)); g_administration.company_info = data; strops::copy(g_administration.default_currency, get_default_currency_for_country(g_administration.company_info.address.country_code), MAX_LEN_CURRENCY); if (contact_changed_event_callback) contact_changed_event_callback(&data); if (data_changed_event_callback) data_changed_event_callback(); } // Contact functions. // ======================= a_err administration::contact_import(contact data) { a_err result = administration::contact_is_valid(data); if (result != A_ERR_SUCCESS) return result; if (strcmp(data.id, MY_COMPANY_ID) == 0) { administration::company_info_import(data); return A_ERR_SUCCESS; } contact* new_contact = (contact*)memops::alloc(sizeof(contact)); if (!new_contact) return A_ERR_GENERIC; memops::copy((void*)new_contact, (void*)&data, sizeof(contact)); if (!list_append(&g_administration.contacts, new_contact)) { return A_ERR_GENERIC; } return A_ERR_SUCCESS; } a_err administration::contact_add(contact data) { a_err result = administration::contact_is_valid(data); if (result != A_ERR_SUCCESS) return result; contact* new_contact = (contact*)memops::alloc(sizeof(contact)); if (!new_contact) return A_ERR_GENERIC; memops::copy((void*)new_contact, (void*)&data, sizeof(contact)); if (!list_append(&g_administration.contacts, new_contact)) { return A_ERR_GENERIC; } g_administration.next_id++; if (contact_changed_event_callback) contact_changed_event_callback(new_contact); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } a_err administration::contact_update(contact data) { a_err result = administration::contact_is_valid(data); if (result != A_ERR_SUCCESS) return result; 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) { memops::copy(c, &data, sizeof(data)); if (contact_changed_event_callback) contact_changed_event_callback(c); if (data_changed_event_callback) data_changed_event_callback(); list_iterator_stop(&g_administration.contacts); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.contacts); return A_ERR_NOT_FOUND; } a_err 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); if (list_delete(&g_administration.contacts, c) != 0) return A_ERR_GENERIC; if (data_deleted_event_callback) data_deleted_event_callback(c->id); memops::unalloc(c); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.contacts); return A_ERR_NOT_FOUND; } 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; } a_err 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 A_ERR_SUCCESS; } a_err result = A_ERR_NOT_FOUND; 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.contacts); *buffer = c; result = A_ERR_SUCCESS; } } 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::contains(c.name, name)) { buffer[write_cursor++] = c; if (write_cursor >= buf_size) break; } } list_iterator_stop(&g_administration.contacts); return write_cursor; } a_err administration::addressee_is_valid(delivery_info data) { a_err result = A_ERR_SUCCESS; if (strlen(data.name) == 0) result |= A_ERR_MISSING_NAME; if (strlen(data.address.city) == 0) result |= A_ERR_MISSING_CITY; if (strlen(data.address.postal) == 0) result |= A_ERR_MISSING_POSTAL; if (strlen(data.address.address1) == 0) result |= A_ERR_MISSING_ADDRESS1; if (strlen(data.address.country_code) == 0) result |= A_ERR_MISSING_COUNTRYCODE; return result; } a_err administration::contact_is_valid(contact data) { a_err result = A_ERR_SUCCESS; if (strlen(data.name) == 0) result |= A_ERR_MISSING_NAME; if (strlen(data.email) == 0) result |= A_ERR_MISSING_EMAIL; if (strlen(data.address.city) == 0) result |= A_ERR_MISSING_CITY; if (strlen(data.address.postal) == 0) result |= A_ERR_MISSING_POSTAL; if (strlen(data.address.address1) == 0) result |= A_ERR_MISSING_ADDRESS1; if (strlen(data.address.country_code) == 0) result |= A_ERR_MISSING_COUNTRYCODE; if (data.type == contact_type::CONTACT_BUSINESS) { if (strlen(data.taxid) == 0) result |= A_ERR_MISSING_TAXID; if (strlen(data.businessid) == 0) result |= A_ERR_MISSING_BUSINESSID; } return result; } contact administration::contact_create_empty() { contact result; memset(&result, 0, sizeof(contact)); strops::format(result.id, sizeof(result.id), "C/%d", 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); } a_err administration::project_get_by_id(project* buffer, char* id) { assert(buffer); 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, id) == 0) { *buffer = c; list_iterator_stop(&g_administration.projects); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.projects); return A_ERR_NOT_FOUND; } 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); } a_err administration::project_is_valid(project data) { if (strlen(data.description) == 0) return A_ERR_MISSING_DESCRIPTION; return A_ERR_SUCCESS; } 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 ""; } a_err administration::project_import(project data) { a_err result = administration::project_is_valid(data); if (result != A_ERR_SUCCESS) return result; project* new_project = (project*)memops::alloc(sizeof(project)); if (!new_project) return A_ERR_GENERIC; memops::copy((void*)new_project, (void*)&data, sizeof(project)); if (!list_append(&g_administration.projects, new_project)) { return A_ERR_GENERIC; } return A_ERR_SUCCESS; } a_err administration::project_add(project data) { a_err result = administration::project_is_valid(data); if (result != A_ERR_SUCCESS) return result; project* new_project = (project*)memops::alloc(sizeof(project)); if (!new_project) return A_ERR_GENERIC; memops::copy((void*)new_project, (void*)&data, sizeof(project)); if (!list_append(&g_administration.projects, new_project)) { return A_ERR_GENERIC; } g_administration.next_id++; if (project_changed_event_callback) project_changed_event_callback(new_project); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } a_err administration::project_update(project data) { a_err result = administration::project_is_valid(data); if (result != A_ERR_SUCCESS) return result; 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) { memops::copy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.projects); if (project_changed_event_callback) project_changed_event_callback(c); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.projects); return A_ERR_NOT_FOUND; } a_err 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); if (list_delete(&g_administration.projects, c) != 0) return A_ERR_GENERIC; if (data_deleted_event_callback) data_deleted_event_callback(c->id); memops::unalloc(c); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.projects); return A_ERR_NOT_FOUND; } project administration::project_create_empty() { project result; memset(&result, 0, sizeof(project)); result.state = project_state::PROJECT_RUNNING; result.start_date = time(NULL); result.start_date -= (result.start_date % 86400); result.end_date = 0; strops::format(result.id, sizeof(result.id), "P/%d", create_id()); return result; } // Tax rate functions. // ======================= tax_rate administration::tax_rate_create_empty() { tax_rate result; memset(&result, 0, sizeof(tax_rate)); strops::format(result.id, sizeof(result.id), "T/%d", create_id()); strops::format(result.category_code, sizeof(result.category_code), "S"); // S = standard rate. return result; } a_err administration::tax_rate_get_by_shorthandle(tax_rate* buffer, char* handle) { assert(buffer); list_iterator_start(&g_administration.tax_rates); while (list_iterator_hasnext(&g_administration.tax_rates)) { tax_rate c = *(tax_rate *)list_iterator_next(&g_administration.tax_rates); char compare_str[20]; strops::format(compare_str, 20, "%s/%.2f", c.country_code, c.rate); if (strcmp(compare_str, handle) == 0) { *buffer = c; list_iterator_stop(&g_administration.tax_rates); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.tax_rates); return A_ERR_NOT_FOUND; } a_err administration::tax_rate_get_by_id(tax_rate* buffer, char* id) { assert(buffer); list_iterator_start(&g_administration.tax_rates); while (list_iterator_hasnext(&g_administration.tax_rates)) { tax_rate c = *(tax_rate *)list_iterator_next(&g_administration.tax_rates); if (strcmp(c.id, id) == 0) { *buffer = c; list_iterator_stop(&g_administration.tax_rates); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.tax_rates); return A_ERR_NOT_FOUND; } u32 administration::tax_rate_count() { return list_size(&g_administration.tax_rates); } a_err administration::tax_rate_import(tax_rate data) { tax_rate* tb = (tax_rate*)memops::alloc(sizeof(tax_rate)); if (!tb) return A_ERR_GENERIC; memops::copy((void*)tb, (void*)&data, sizeof(tax_rate)); if (!list_append(&g_administration.tax_rates, tb)) { return A_ERR_GENERIC; } list_attributes_comparator(&g_administration.tax_rates, compare_tax_countries); list_sort(&g_administration.tax_rates, -1); return A_ERR_SUCCESS; } a_err administration::tax_rate_add(tax_rate data) { tax_rate* tb = (tax_rate*)memops::alloc(sizeof(tax_rate)); if (!tb) return A_ERR_GENERIC; memops::copy((void*)tb, (void*)&data, sizeof(tax_rate)); if (!list_append(&g_administration.tax_rates, tb)) { return A_ERR_GENERIC; } g_administration.next_id++; list_attributes_comparator(&g_administration.tax_rates, compare_tax_countries); list_sort(&g_administration.tax_rates, -1); if (taxrate_changed_event_callback) taxrate_changed_event_callback(&data); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } u32 administration::tax_rate_get_by_country(tax_rate* buffer, u32 code_count, char** tax_country_codes) { assert(buffer); u32 write_cursor = 0; list_iterator_start(&g_administration.tax_rates); while (list_iterator_hasnext(&g_administration.tax_rates)) { tax_rate c = *(tax_rate *)list_iterator_next(&g_administration.tax_rates); if (strcmp(c.country_code, "00") == 0) { buffer[write_cursor++] = c; continue; } for (u32 x = 0; x < code_count; x++) { if (strcmp(c.country_code, tax_country_codes[x]) == 0) { buffer[write_cursor++] = c; continue; } } } list_iterator_stop(&g_administration.tax_rates); return write_cursor; } u32 administration::tax_rate_get_all(tax_rate* buffer) { assert(buffer); u32 write_cursor = 0; list_iterator_start(&g_administration.tax_rates); while (list_iterator_hasnext(&g_administration.tax_rates)) { tax_rate c = *(tax_rate *)list_iterator_next(&g_administration.tax_rates); buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.tax_rates); return write_cursor; } a_err administration::tax_rate_update(tax_rate data) { list_iterator_start(&g_administration.tax_rates); while (list_iterator_hasnext(&g_administration.tax_rates)) { tax_rate* c = (tax_rate *)list_iterator_next(&g_administration.tax_rates); if (strcmp(c->id, data.id) == 0) { memops::copy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.tax_rates); if (taxrate_changed_event_callback) taxrate_changed_event_callback(c); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.tax_rates); return A_ERR_NOT_FOUND; } // Cost center functions. // ======================= u32 administration::cost_center_count() { return list_size(&g_administration.cost_centers); } cost_center administration::cost_center_create_empty() { cost_center cc; memset(&cc, 0, sizeof(cost_center)); strops::format(cc.id, sizeof(cc.id), "E/%d", create_id()); return cc; } a_err administration::cost_center_get_by_id(cost_center* buffer, char* id) { assert(buffer); 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, id) == 0) { *buffer = c; list_iterator_stop(&g_administration.cost_centers); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.cost_centers); return A_ERR_NOT_FOUND; } 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 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; } a_err administration::cost_center_is_valid(cost_center data) { cost_center lookup; a_err result = A_ERR_SUCCESS; if (strlen(data.code) == 0) result |= A_ERR_MISSING_CODE; if (strlen(data.description) == 0) result |= A_ERR_MISSING_DESCRIPTION; if (get_cost_center_by_code(data.code, &lookup)) result |= A_ERR_CODE_EXISTS; return result; } a_err administration::cost_center_import(cost_center data) { a_err result = administration::cost_center_is_valid(data); if (result != A_ERR_SUCCESS) return result; cost_center* tb = (cost_center*)memops::alloc(sizeof(cost_center)); if (!tb) return A_ERR_GENERIC; memops::copy(tb, &data, sizeof(cost_center)); if (!list_append(&g_administration.cost_centers, tb)) { return A_ERR_GENERIC; } return A_ERR_SUCCESS; } a_err administration::cost_center_add(cost_center data) { a_err result = administration::cost_center_is_valid(data); if (result != A_ERR_SUCCESS) return result; cost_center* tb = (cost_center*)memops::alloc(sizeof(cost_center)); if (!tb) return A_ERR_GENERIC; memops::copy((void*)tb, (void*)&data, sizeof(cost_center)); if (!list_append(&g_administration.cost_centers, tb)) { return A_ERR_GENERIC; } g_administration.next_id++; if (costcenter_changed_event_callback) costcenter_changed_event_callback(tb); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } a_err administration::cost_center_update(cost_center data) { a_err result = administration::cost_center_is_valid(data); if (result != A_ERR_CODE_EXISTS) return result; 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) { memops::copy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.cost_centers); if (costcenter_changed_event_callback) costcenter_changed_event_callback(c); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.cost_centers); return A_ERR_NOT_FOUND; } // Invoice functions. // ======================= static char* 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)); strops::format(result.id, sizeof(result.id), "I/%d", create_id()); strops::format(result.sequential_number, sizeof(result.id), "INV%010d", create_sequence_number()); result.issued_at = time(NULL); result.issued_at -= (result.issued_at % 86400); result.delivered_at = result.issued_at; result.expires_at = result.issued_at + administration::get_default_invoice_expire_duration(); list_init(&result.billing_items); // @leak strops::copy(result.currency, 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->orig_tax = 0.0f; invoice->orig_total = 0.0f; invoice->orig_net = 0.0f; invoice->orig_allowance = 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->orig_tax += c->tax; invoice->orig_total += c->total; invoice->orig_net += c->net; invoice->orig_allowance += c->allowance; } list_iterator_stop(&invoice->billing_items); if (strcmp(invoice->currency, administration::get_default_currency()) == 0) { invoice->tax = invoice->orig_tax; invoice->total = invoice->orig_total; invoice->net = invoice->orig_net; invoice->allowance = invoice->orig_allowance; } } 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); } a_err administration::invoice_is_valid(invoice* invoice) { a_err result = A_ERR_SUCCESS; if (list_size(&invoice->billing_items) == 0) result |= A_ERR_MISSING_BILLING_ITEMS; if (invoice->is_triangulation && administration::addressee_is_valid(invoice->addressee) != A_ERR_SUCCESS) result |= A_ERR_INVALID_ADDRESSEE; if (administration::contact_is_valid(invoice->customer) != A_ERR_SUCCESS) result |= A_ERR_INVALID_CUSTOMER; if (administration::contact_is_valid(invoice->supplier) != A_ERR_SUCCESS) result |= A_ERR_INVALID_SUPPLIER; 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 (administration::billing_item_is_valid(*c) != A_ERR_SUCCESS) result |= A_ERR_INVALID_BILLING_ITEM; } list_iterator_stop(&invoice->billing_items); return result; } a_err 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); if (list_delete(&g_administration.invoices, c) != 0) return A_ERR_GENERIC; if (data_deleted_event_callback) data_deleted_event_callback(c->id); if (inv->is_outgoing) g_administration.invoice_count--; else g_administration.expense_count--; memops::unalloc(c); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.invoices); return A_ERR_NOT_FOUND; } static void invoice_set_addressee(invoice* inv, contact contact) { memops::copy(&inv->addressee.name, &contact.name, sizeof(inv->addressee.name)); memops::copy(&inv->addressee.address.address1, &contact.address.address1, sizeof(inv->addressee.address.address1)); memops::copy(&inv->addressee.address.address2, &contact.address.address2, sizeof(inv->addressee.address.address2)); memops::copy(&inv->addressee.address.city, &contact.address.city, sizeof(inv->addressee.address.city)); memops::copy(&inv->addressee.address.postal, &contact.address.postal, sizeof(inv->addressee.address.postal)); memops::copy(&inv->addressee.address.region, &contact.address.region, sizeof(inv->addressee.address.region)); memops::copy(&inv->addressee.address.country_code, &contact.address.country_code, sizeof(inv->addressee.address.country_code)); } a_err administration::invoice_update(invoice* inv) { a_err result = administration::invoice_is_valid(inv); if (result != A_ERR_SUCCESS) return result; // Addressee is same as customer. if (!inv->is_triangulation) invoice_set_addressee(inv, inv->customer); 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) { memops::copy(c, inv, sizeof(invoice)); list_iterator_stop(&g_administration.invoices); if (invoice_changed_event_callback) invoice_changed_event_callback(c); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.invoices); return A_ERR_NOT_FOUND; } a_err administration::invoice_import(invoice* inv) { //a_err result = administration::invoice_is_valid(inv); //if (result != A_ERR_SUCCESS) return result; inv->is_triangulation = !(memcmp(&inv->addressee.address, &inv->customer.address, sizeof(address)) == 0); inv->issued_at -= (inv->issued_at % 86400); inv->delivered_at -= (inv->delivered_at % 86400); inv->expires_at -= (inv->expires_at % 86400); inv->is_outgoing = strcmp(inv->supplier.id, MY_COMPANY_ID) == 0; invoice copy = administration::invoice_create_copy(inv); // Create copy to make copy of billing item list. invoice* new_inv = (invoice*)memops::alloc(sizeof(invoice)); if (!new_inv) return A_ERR_GENERIC; memops::copy(new_inv, ©, sizeof(invoice)); if (!list_append(&g_administration.invoices, new_inv)) { return A_ERR_GENERIC; } return A_ERR_SUCCESS; } a_err administration::invoice_add(invoice* inv) { a_err result = administration::invoice_is_valid(inv); if (result != A_ERR_SUCCESS) return result; if (!inv->is_triangulation) invoice_set_addressee(inv, inv->customer); inv->issued_at -= (inv->issued_at % 86400); inv->delivered_at -= (inv->delivered_at % 86400); inv->expires_at -= (inv->expires_at % 86400); inv->is_outgoing = strcmp(inv->supplier.id, MY_COMPANY_ID) == 0; inv->payment_means.payment_method = PAYMENT_METHOD_STANDING_AGREEMENT; strops::copy(inv->payment_means.payee_bank_account, inv->supplier.bank_account, sizeof(inv->payment_means.payee_bank_account)); strops::copy(inv->payment_means.payee_account_name, inv->supplier.name, sizeof(inv->payment_means.payee_account_name)); strops::copy(inv->payment_means.service_provider_id, "", sizeof(inv->payment_means.service_provider_id)); strops::copy(inv->payment_means.payer_bank_account, inv->customer.bank_account, sizeof(inv->payment_means.payer_bank_account)); invoice copy = administration::invoice_create_copy(inv); // Create copy to make copy of billing item list. invoice* new_inv = (invoice*)memops::alloc(sizeof(invoice)); if (!new_inv) return A_ERR_GENERIC; memops::copy(new_inv, ©, sizeof(invoice)); if (!list_append(&g_administration.invoices, new_inv)) { return A_ERR_GENERIC; } g_administration.next_id++; g_administration.next_sequence_number++; if (inv->is_outgoing) g_administration.invoice_count++; else g_administration.expense_count++; if (invoice_changed_event_callback) invoice_changed_event_callback(new_inv); if (data_changed_event_callback) data_changed_event_callback(); return A_ERR_SUCCESS; } invoice administration::invoice_create_copy(invoice* inv) { invoice new_inv = administration::invoice_create_empty(); list_t billing_items = new_inv.billing_items; memops::copy((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*)memops::alloc(sizeof(billing_item)); memops::copy(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 administration::invoice_get_incomming_count() { return g_administration.expense_count; } u32 administration::invoice_get_outgoing_count() { return g_administration.invoice_count; } a_err administration::invoice_get_by_id(invoice* buffer, char* id) { a_err result = A_ERR_NOT_FOUND; 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, id) == 0) { list_iterator_stop(&g_administration.invoices); *buffer = c; result = A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.invoices); return result; } u32 administration::invoice_get_all(invoice* buffer) { assert(buffer); u32 write_cursor = 0; list_iterator_start(&g_administration.invoices); while (list_iterator_hasnext(&g_administration.invoices)) { invoice c = *(invoice *)list_iterator_next(&g_administration.invoices); buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.invoices); return write_cursor; } bool administration::invoice_get_subtotal_for_tax_rate(invoice* invoice, tax_rate rate, tax_subtotal* buffer) { bool result = false; buffer->tax = 0.0f; buffer->total = 0.0f; buffer->net = 0.0f; buffer->allowance = 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); if (strcmp(c->tax_rate_id, rate.id) == 0) { result = true; buffer->tax += c->tax; buffer->total += c->total; buffer->net += c->net; buffer->allowance += c->allowance; } } list_iterator_stop(&invoice->billing_items); return result; } u32 administration::invoice_get_tax_rates(invoice* invoice, tax_rate* 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); tax_rate rate; a_err found = administration::tax_rate_get_by_id(&rate, c.tax_rate_id); if (found == A_ERR_SUCCESS) { bool exists = false; for (u32 i = 0; i < write_cursor; i++) { if (strcmp(buffer[i].id, c.tax_rate_id) == 0) { exists = true; break; } } if (!exists) { buffer[write_cursor++] = rate; } } } list_iterator_stop(&invoice->billing_items); return write_cursor; } static u32 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 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 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->allowance = item->net * (item->discount / 100.0f); item->net -= item->allowance; } else { item->allowance = item->discount; item->net -= item->allowance; } } tax_rate rate; if (administration::tax_rate_get_by_id(&rate, item->tax_rate_id) == A_ERR_SUCCESS) { item->tax = item->net * (rate.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; } a_err 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); if (list_delete(&invoice->billing_items, c) != 0) return A_ERR_GENERIC; memops::unalloc(c); return A_ERR_SUCCESS; } } list_iterator_stop(&invoice->billing_items); return A_ERR_NOT_FOUND; } a_err 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) { memops::copy(c, &item, sizeof(billing_item)); list_iterator_stop(&invoice->billing_items); administration_recalculate_billing_item_totals(c); administration_recalculate_invoice_totals(invoice); return A_ERR_SUCCESS; } } list_iterator_stop(&invoice->billing_items); return A_ERR_NOT_FOUND; } a_err administration::billing_item_is_valid(billing_item item) { a_err result = A_ERR_SUCCESS; if (strlen(item.description) == 0) result |= A_ERR_MISSING_DESCRIPTION; if (strlen(item.tax_rate_id) == 0) result |= A_ERR_MISSING_TAX_RATE; return result; } a_err administration::billing_item_import_to_invoice(invoice* invoice, billing_item item) { if (administration::billing_item_count(invoice) >= MAX_BILLING_ITEMS) return A_ERR_MAX_ITEMS_REACHED; billing_item* tb = (billing_item*)memops::alloc(sizeof(billing_item)); if (!tb) return A_ERR_GENERIC; memops::copy(tb, &item, sizeof(billing_item)); administration_recalculate_billing_item_totals(tb); if (!list_append(&invoice->billing_items, tb)) { return A_ERR_GENERIC; } administration_recalculate_invoice_totals(invoice); return A_ERR_SUCCESS; } a_err administration::billing_item_add_to_invoice(invoice* invoice, billing_item item) { if (administration::billing_item_count(invoice) >= MAX_BILLING_ITEMS) return A_ERR_MAX_ITEMS_REACHED; billing_item* tb = (billing_item*)memops::alloc(sizeof(billing_item)); if (!tb) return A_ERR_GENERIC; memops::copy(tb, &item, sizeof(billing_item)); strops::format(tb->id, sizeof(tb->id), "B/%d", create_id()); strops::copy(tb->currency, invoice->currency, MAX_LEN_CURRENCY); // Set billing item currency to invoice currency. administration_recalculate_billing_item_totals(tb); if (!list_append(&invoice->billing_items, tb)) { return A_ERR_GENERIC; } administration_recalculate_invoice_totals(invoice); g_administration.next_id++; return A_ERR_SUCCESS; } billing_item administration::billing_item_create_empty() { billing_item item; memset(&item, 0, sizeof(billing_item)); item.amount = 1; return item; }