/* * 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 administration_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; } time_t administration::get_default_invoice_expire_duration() // TODO depricated { return (30 * 24 * 60 * 60); // 30 days } static void administration_recalculate_billing_item_totals(billing_item* item); static char* get_default_currency_for_country(char* country_code); 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);\ } 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_administration_data_changed_event_callback(data_changed_event ev) { administration_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.activities); list_init(&g_administration.all_tax_rates); list_init(&g_administration.cost_centers); strops::copy(g_administration.path, "", sizeof(g_administration.path)); memops::zero(&g_administration.ai_service, sizeof(ai_service)); // Load all tax rates. for (s32 i = 0; i < country::get_count(); i++) { if (!country::tax_is_implemented(country::get_code_by_index(i))) continue; tax_rate* tax_rates = (tax_rate*)memops::alloc(sizeof(tax_rate) * 400); u32 tax_rate_count = country::get_available_tax_rates(country::get_code_by_index(i), tax_rates, 400); for (u32 x = 0; x < tax_rate_count; x++) { tax_rate* rate = (tax_rate*)memops::alloc(sizeof(tax_rate)); memops::copy(rate, &tax_rates[x], sizeof(tax_rate)); list_append(&g_administration.all_tax_rates, rate); } } 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); } static void administration_destroy_invoices() { list_iterator_start(&g_administration.invoices); while (list_iterator_hasnext(&g_administration.invoices)) { invoice* c = (invoice *)list_iterator_next(&g_administration.invoices); administration_destroy_list(&c->billing_items); } list_iterator_stop(&g_administration.invoices); } int sort_invoice_time_t(const void *a, const void *b) { invoice* inv1 = (invoice*)a; invoice* inv2 = (invoice*)b; return (int)(inv1->issued_at - inv2->issued_at); } void administration::sort_data() { list_attributes_comparator(&g_administration.invoices, sort_invoice_time_t); list_sort(&g_administration.invoices, 1); } void administration::destroy() { is_initialized = false; administration_destroy_invoices(); administration_destroy_list(&g_administration.invoices); administration_destroy_list(&g_administration.contacts); administration_destroy_list(&g_administration.projects); administration_destroy_list(&g_administration.activities); administration_destroy_list(&g_administration.tax_rates); administration_destroy_list(&g_administration.all_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_cost_centers(); } // 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 (administration_data_changed_event_callback) administration_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() // TODO rename to be more generic as it is used for more than 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 (strops::equals(code, "EUR")) return "€"; // Euro if (strops::equals(code, "GBP")) return "£"; // British Pound if (strops::equals(code, "CHF")) return "CHF"; // Swiss Franc (no special sign, usually "CHF") if (strops::equals(code, "NOK")) return "kr"; // Norwegian Krone if (strops::equals(code, "SEK")) return "kr"; // Swedish Krona if (strops::equals(code, "DKK")) return "kr"; // Danish Krone if (strops::equals(code, "ISK")) return "kr"; // Icelandic Króna if (strops::equals(code, "CZK")) return "Kč"; // Czech Koruna if (strops::equals(code, "PLN")) return "zł"; // Polish Złoty if (strops::equals(code, "HUF")) return "Ft"; // Hungarian Forint if (strops::equals(code, "RON")) return "lei"; // Romanian Leu if (strops::equals(code, "BGN")) return "лв"; // Bulgarian Lev if (strops::equals(code, "HRK")) return "kn"; // Croatian Kuna (before Euro, now EUR since 2023) if (strops::equals(code, "RSD")) return "дин"; // Serbian Dinar if (strops::equals(code, "MKD")) return "ден"; // Macedonian Denar if (strops::equals(code, "ALL")) return "L"; // Albanian Lek if (strops::equals(code, "MDL")) return "L"; // Moldovan Leu if (strops::equals(code, "BYN")) return "Br"; // Belarusian Ruble if (strops::equals(code, "UAH")) return "₴"; // Ukrainian Hryvnia if (strops::equals(code, "RUB")) return "₽"; // Russian Ruble if (strops::equals(code, "TRY")) 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->report_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 = country::get_invoice_date_to_use_for_tax_report(country_code, &invoice_buffer[i]); if (invoice_buffer[i].delivered_at > youngest) youngest = country::get_invoice_date_to_use_for_tax_report(country_code, &invoice_buffer[i]); } 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); statement->reports[statement->report_count++] = quarter; } // Fill quarters. for (u32 i = 0; i < invoice_count; i++) { invoice* inv = &invoice_buffer[i]; u16 yy; u8 qq; time_t_to_quarter(country::get_invoice_date_to_use_for_tax_report(country_code, inv), &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++) { bool success = country::add_billing_item_to_tax_report(country_code, quarter, inv, &item_buffer[x]); if (!success) logger::error("Failed to add billing item to tax report: %s - %s.", inv->sequential_number, item_buffer[x].description); } country::calculate_tax_report_final(country_code, quarter); memops::unalloc(item_buffer); } memops::unalloc(invoice_buffer); logger::info("Created tax statement in %.3fms.", STOPWATCH_TIME); } tax_line* administration::get_tax_line_from_report(tax_report* quarter, char* category) { for (u32 t = 0; t < quarter->line_count; t++) { if (strops::equals(quarter->lines[t].tax_category, category)) { return &quarter->lines[t]; } } return 0; } 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->delivered_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 (strops::equals(inv->project_id, "")) { 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 (strops::equals(quarter->reports[x].project_id, inv->project_id)) { 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 (!strops::equals(inv->cost_center_id, "")) { int expense_report_index = -1; for (u32 x = 0; x < report->expense_count; x++) { if (strops::equals(report->expenses[x].cost_center_id, inv->cost_center_id)) { 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 strops::empty(g_administration.path) ? 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); } // Contact functions. // ======================= a_err administration::contact_import(contact data) { if (strops::equals(data.id, MY_COMPANY_ID)) { 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); 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 (strops::equals(c->id, data.id)) { memops::copy(c, &data, sizeof(data)); if (contact_changed_event_callback) contact_changed_event_callback(c); 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 (strops::equals(c->id, data.id)) { 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(contact_filter* filter) { if (!filter) return list_size(&g_administration.contacts); u32 count = 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, filter->name_filter)) continue; count++; } list_iterator_stop(&g_administration.contacts); return count; } 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, contact_filter* filter) { assert(buffer); u32 write_cursor = 0; u32 read_start = page_index * page_size; u32 read_cursor = 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, filter->name_filter)) continue; if (++read_cursor <= 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 (strops::equals(id, g_administration.company_info.id)) { *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 (strops::equals(c.id, id)) { 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 (strops::empty(data.name)) result |= A_ERR_MISSING_NAME; if (strops::empty(data.address.city)) result |= A_ERR_MISSING_CITY; if (strops::empty(data.address.postal)) result |= A_ERR_MISSING_POSTAL; if (strops::empty(data.address.address1)) result |= A_ERR_MISSING_ADDRESS1; if (strops::empty(data.address.country_code)) result |= A_ERR_MISSING_COUNTRYCODE; return result; } a_err administration::contact_is_valid(contact data) { a_err result = A_ERR_SUCCESS; if (strops::empty(data.name)) result |= A_ERR_MISSING_NAME; if (strops::empty(data.email)) result |= A_ERR_MISSING_EMAIL; if (strops::empty(data.address.city)) result |= A_ERR_MISSING_CITY; if (strops::empty(data.address.postal)) result |= A_ERR_MISSING_POSTAL; if (strops::empty(data.address.address1)) result |= A_ERR_MISSING_ADDRESS1; if (strops::empty(data.address.country_code)) result |= A_ERR_MISSING_COUNTRYCODE; if (data.type == contact_type::CONTACT_BUSINESS) { if (strops::empty(data.taxid)) result |= A_ERR_MISSING_TAXID; if (strops::empty(data.businessid)) result |= A_ERR_MISSING_BUSINESSID; } return result; } contact administration::contact_create_empty() { contact result; memops::zero(&result, sizeof(contact)); strops::format(result.id, sizeof(result.id), "C/%d", create_id()); return result; } bool administration::contact_equals(contact c1, contact c2) { return memops::equals(&c1, &c2, sizeof(contact)); } // 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 (strops::equals(c.id, id)) { *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 (strops::empty(data.description)) 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) { 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); 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 (strops::equals(c->id, data.id)) { memops::copy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.projects); if (project_changed_event_callback) project_changed_event_callback(c); 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 (strops::equals(c->id, data.id)) { 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; memops::zero(&result, 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; memops::zero(&result, sizeof(tax_rate)); return result; } a_err administration::tax_rate_disable(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 (strops::equals(c->internal_code, data.internal_code)) { list_iterator_stop(&g_administration.tax_rates); if (list_delete(&g_administration.tax_rates, c) != 0) return A_ERR_GENERIC; char filename[MAX_LEN_PATH]; strops::format(filename, sizeof(filename), "T/%s", c->internal_code); if (data_deleted_event_callback) data_deleted_event_callback(filename); memops::unalloc(c); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.tax_rates); return A_ERR_NOT_FOUND; } a_err administration::tax_rate_is_enabled(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 (strops::equals(c.internal_code, data.internal_code)) { 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_internal_code(tax_rate* buffer, char* id) { assert(buffer); list_iterator_start(&g_administration.all_tax_rates); while (list_iterator_hasnext(&g_administration.all_tax_rates)) { tax_rate c = *(tax_rate *)list_iterator_next(&g_administration.all_tax_rates); if (strops::equals(c.internal_code, id)) { *buffer = c; list_iterator_stop(&g_administration.all_tax_rates); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.all_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; } return A_ERR_SUCCESS; } a_err administration::tax_rate_enable(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++; if (taxrate_changed_event_callback) taxrate_changed_event_callback(&data); return A_ERR_SUCCESS; } u32 administration::tax_rate_get_all(tax_rate* buffer, tax_rate_type type) { 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 (c.type == type) buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.tax_rates); return write_cursor; } // 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; memops::zero(&cc, 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 (strops::equals(c.id, id)) { *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 (strops::equals(code, c.code)) { 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 (strops::empty(data.code)) result |= A_ERR_MISSING_CODE; if (strops::empty(data.description)) 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) { 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); 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 (strops::equals(c->id, data.id)) { memops::copy(c, &data, sizeof(data)); list_iterator_stop(&g_administration.cost_centers); if (costcenter_changed_event_callback) costcenter_changed_event_callback(c); 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 || strops::length(country_code) != 2) return "EUR"; // default // Non-euro EU currencies if (strops::equals(country_code, "BG")) return "BGN"; // Bulgaria else if (strops::equals(country_code, "CZ")) return "CZK"; // Czechia else if (strops::equals(country_code, "DK")) return "DKK"; // Denmark else if (strops::equals(country_code, "HU")) return "HUF"; // Hungary else if (strops::equals(country_code, "PL")) return "PLN"; // Poland else if (strops::equals(country_code, "RO")) return "RON"; // Romania else if (strops::equals(country_code, "SE")) return "SEK"; // Sweden // Eurozone members else if (strops::equals(country_code, "AT")) return "EUR"; // Austria else if (strops::equals(country_code, "BE")) return "EUR"; // Belgium else if (strops::equals(country_code, "CY")) return "EUR"; // Cyprus else if (strops::equals(country_code, "DE")) return "EUR"; // Germany else if (strops::equals(country_code, "EE")) return "EUR"; // Estonia else if (strops::equals(country_code, "ES")) return "EUR"; // Spain else if (strops::equals(country_code, "FI")) return "EUR"; // Finland else if (strops::equals(country_code, "FR")) return "EUR"; // France else if (strops::equals(country_code, "GR")) return "EUR"; // Greece else if (strops::equals(country_code, "HR")) return "EUR"; // Croatia else if (strops::equals(country_code, "IE")) return "EUR"; // Ireland else if (strops::equals(country_code, "IT")) return "EUR"; // Italy else if (strops::equals(country_code, "LT")) return "EUR"; // Lithuania else if (strops::equals(country_code, "LU")) return "EUR"; // Luxembourg else if (strops::equals(country_code, "LV")) return "EUR"; // Latvia else if (strops::equals(country_code, "MT")) return "EUR"; // Malta else if (strops::equals(country_code, "NL")) return "EUR"; // Netherlands else if (strops::equals(country_code, "PT")) return "EUR"; // Portugal else if (strops::equals(country_code, "SI")) return "EUR"; // Slovenia else if (strops::equals(country_code, "SK")) return "EUR"; // Slovakia // Default fallback return "EUR"; } bool administration::invoice_has_intra_community_services(invoice* invoice) { // TODO return false; } void administration::invoice_destroy(invoice* invoice) { administration_destroy_list(&invoice->billing_items); } invoice administration::invoice_create_empty() { invoice result; memops::zero(&result, 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); 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); administration_recalculate_billing_item_totals(c); 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 (strops::equals(invoice->currency, administration::get_default_currency())) { 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 (strops::equals(c->id, inv->id)) { 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 (strops::equals(c->id, inv->id)) { memops::copy(c, inv, sizeof(invoice)); list_iterator_stop(&g_administration.invoices); if (invoice_changed_event_callback) invoice_changed_event_callback(c); administration::activity_add(ACTIVITY_USER, c->id, "activity.update_invoice", 0); return A_ERR_SUCCESS; } } list_iterator_stop(&g_administration.invoices); return A_ERR_NOT_FOUND; } a_err administration::invoice_import(invoice* inv) { inv->is_triangulation = !memops::equals(&inv->addressee.address, &inv->customer.address, sizeof(address)); inv->issued_at -= (inv->issued_at % 86400); inv->delivered_at -= (inv->delivered_at % 86400); inv->expires_at -= (inv->expires_at % 86400); inv->is_outgoing = strops::equals(inv->supplier.id, MY_COMPANY_ID); 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 = strops::equals(inv->supplier.id, MY_COMPANY_ID); 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_prepend(&g_administration.invoices, new_inv)) { return A_ERR_GENERIC; } administration::activity_add(ACTIVITY_USER, inv->id, "activity.add_invoice", 0); 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); 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 (strops::equals(c.id, id)) { 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 (strops::equals(c->tax_internal_code, rate.internal_code)) { 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_internal_code(&rate, c.tax_internal_code); if (found == A_ERR_SUCCESS) { bool exists = false; for (u32 i = 0; i < write_cursor; i++) { if (strops::equals(buffer[i].internal_code, c.tax_internal_code)) { 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_internal_code(&rate, item->tax_internal_code) == 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 (strops::equals(c->id, item.id)) { 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 (strops::equals(c->id, item.id)) { 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; } tax_subtotal administration::billing_item_convert_to_default_currency(invoice* invoice, billing_item item) { tax_subtotal result = {0}; if (invoice->net != 0.0f) result.net = item.net / (invoice->orig_net / invoice->net); if (invoice->tax != 0.0f) result.tax = item.tax / (invoice->orig_tax / invoice->tax); if (invoice->total != 0.0f) result.total = item.total / (invoice->orig_total / invoice->total); if (invoice->allowance != 0.0f) result.allowance = item.allowance / (invoice->orig_allowance / invoice->allowance); return result; } a_err administration::billing_item_is_valid(billing_item item) { a_err result = A_ERR_SUCCESS; if (strops::empty(item.description)) result |= A_ERR_MISSING_DESCRIPTION; if (strops::empty(item.tax_internal_code)) 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; memops::zero(&item, sizeof(billing_item)); item.amount = 1; return item; } // Activity functions. // =================== a_err administration::activity_add(char* user, char* ref_id, char* message, ...) { activity* new_activity = (activity*)memops::alloc(sizeof(activity)); strops::copy(new_activity->user_name, user, MAX_LEN_SHORT_DESC); strops::copy(new_activity->ref_id, ref_id, MAX_LEN_ID); strops::copy(new_activity->message, message, MAX_LEN_LONG_DESC); new_activity->timestamp = time(NULL); va_list args; va_start(args, message); char* param; u32 param_count = 0; do { param = va_arg(args, char*); if (param != 0) { strops::copy(new_activity->params[param_count++], param, MAX_LEN_LONG_DESC); } } while (param && param_count < ACTIVITY_MAX_PARAMS); va_end(args); if (!list_prepend(&g_administration.activities, new_activity)) { return A_ERR_GENERIC; } return A_ERR_SUCCESS; } u32 administration::activity_get_all_for_object(activity* buffer, char* ref_id) { u32 write_cursor = 0; list_iterator_start(&g_administration.activities); while (list_iterator_hasnext(&g_administration.activities)) { activity c = *(activity *)list_iterator_next(&g_administration.activities); if (strops::equals(c.ref_id, ref_id)) buffer[write_cursor++] = c; } list_iterator_stop(&g_administration.activities); return write_cursor; }