diff options
| author | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-08-16 11:10:29 +0200 |
|---|---|---|
| committer | Aldrik Ramaekers <aldrikboy@gmail.com> | 2025-08-16 11:10:29 +0200 |
| commit | 05bc81cd42c5aeff7cfb6cf6b18f88792e7c16c9 (patch) | |
| tree | 7bc9c0439507b4531ed78578e441b12cba4dda4e /src | |
| parent | f67e92f55b6223f2806c3d5ef1cbe2a638920562 (diff) | |
invoice form work
Diffstat (limited to 'src')
| -rw-r--r-- | src/administration.cpp | 195 | ||||
| -rw-r--r-- | src/ui/ui_invoices.cpp | 263 | ||||
| -rw-r--r-- | src/ui/ui_main.cpp | 4 |
3 files changed, 439 insertions, 23 deletions
diff --git a/src/administration.cpp b/src/administration.cpp index 56d8663..2e16775 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -262,6 +262,12 @@ static void administration_create_debug_data() ADD_PROJECT("Retail store #1"); ADD_PROJECT("Retail store #2"); ADD_PROJECT("Kayak rental"); + + // Company info. + strops_copy(g_administration.company_info.name, "Aldrik Ramaekers", sizeof(g_administration.company_info.name)); + strops_copy(g_administration.company_info.address.address1, "Keerderstraat 81", sizeof(g_administration.company_info.address.address1)); + strops_copy(g_administration.company_info.address.address2, "6226XW Maastricht", sizeof(g_administration.company_info.address.address2)); + strops_copy(g_administration.company_info.address.country_code, "NL", sizeof(g_administration.company_info.address.country_code)); } void administration_create() @@ -269,11 +275,6 @@ void administration_create() g_administration.next_id = 1; g_administration.next_sequence_number = 1; - strops_copy(g_administration.company_info.name, "Aldrik Ramaekers", sizeof(g_administration.company_info.name)); - strops_copy(g_administration.company_info.address.address1, "Keerderstraat 81", sizeof(g_administration.company_info.address.address1)); - strops_copy(g_administration.company_info.address.address2, "6226XW Maastricht", sizeof(g_administration.company_info.address.address2)); - strops_copy(g_administration.company_info.address.country_code, "NL", sizeof(g_administration.company_info.address.country_code)); - list_init(&g_administration.contacts); list_init(&g_administration.projects); list_init(&g_administration.tax_brackets); @@ -408,6 +409,25 @@ u32 administration_get_project_count() return list_size(&g_administration.projects); } +u32 administration_get_billing_items_count(invoice* invoice) +{ + return list_size(&invoice->billing_items); +} + +u32 administration_get_all_billing_items_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; +} + u32 administration_get_all_projects(project* buffer) { u32 write_cursor = 0; @@ -543,6 +563,23 @@ bool administration_add_tax_bracket(country_tax_bracket data) return true; } +u32 administration_get_tax_brackets_for_country(country_tax_bracket* buffer, char* country_code) +{ + assert(buffer); + + u32 write_cursor = 0; + + list_iterator_start(&g_administration.tax_brackets); + while (list_iterator_hasnext(&g_administration.tax_brackets)) { + country_tax_bracket c = *(country_tax_bracket *)list_iterator_next(&g_administration.tax_brackets); + + if (strcmp(c.country_code, country_code) == 0 || strcmp(c.country_code, "00") == 0) buffer[write_cursor++] = c; + } + list_iterator_stop(&g_administration.tax_brackets); + + return write_cursor; +} + u32 administration_get_tax_brackets(country_tax_bracket* buffer) { assert(buffer); @@ -576,8 +613,6 @@ bool administration_update_tax_bracket(country_tax_bracket data) return false; } - - u32 administration_get_cost_center_count() { return list_size(&g_administration.cost_centers); @@ -689,6 +724,68 @@ static time_t administration_get_default_invoice_expire_duration() return (30 * 24 * 60 * 60); // 30 days } +billing_item administration_create_empty_billing_item() +{ + billing_item item; + memset(&item, 0, sizeof(billing_item)); + item.amount = 1; + return item; +} + +bool administration_add_billing_item_to_invoice(invoice* invoice, billing_item item) +{ + billing_item* tb = (billing_item*)malloc(sizeof(billing_item)); + memcpy(tb, &item, sizeof(billing_item)); + snprintf(tb->id, sizeof(tb->id), "B/%d", administration_create_id()); + strops_copy(tb->invoice_id, invoice->id, sizeof(tb->invoice_id)); + list_append(&invoice->billing_items, tb); + strops_copy(tb->currency, invoice->currency, CURRENCY_LENGTH); // Set billing item currency to invoice currency. + + g_administration.next_id++; + + return true; +} + +static char* administration_get_default_currency_for_country(char* country_code) +{ + if (country_code == NULL || strlen(country_code) != 2) + return "EUR"; // default + + // Non-euro EU currencies + if (strcmp(country_code, "BG") == 0) return "BGN"; // Bulgaria + else if (strcmp(country_code, "CZ") == 0) return "CZK"; // Czechia + else if (strcmp(country_code, "DK") == 0) return "DKK"; // Denmark + else if (strcmp(country_code, "HU") == 0) return "HUF"; // Hungary + else if (strcmp(country_code, "PL") == 0) return "PLN"; // Poland + else if (strcmp(country_code, "RO") == 0) return "RON"; // Romania + else if (strcmp(country_code, "SE") == 0) return "SEK"; // Sweden + + // Eurozone members + else if (strcmp(country_code, "AT") == 0) return "EUR"; // Austria + else if (strcmp(country_code, "BE") == 0) return "EUR"; // Belgium + else if (strcmp(country_code, "CY") == 0) return "EUR"; // Cyprus + else if (strcmp(country_code, "DE") == 0) return "EUR"; // Germany + else if (strcmp(country_code, "EE") == 0) return "EUR"; // Estonia + else if (strcmp(country_code, "ES") == 0) return "EUR"; // Spain + else if (strcmp(country_code, "FI") == 0) return "EUR"; // Finland + else if (strcmp(country_code, "FR") == 0) return "EUR"; // France + else if (strcmp(country_code, "GR") == 0) return "EUR"; // Greece + else if (strcmp(country_code, "HR") == 0) return "EUR"; // Croatia + else if (strcmp(country_code, "IE") == 0) return "EUR"; // Ireland + else if (strcmp(country_code, "IT") == 0) return "EUR"; // Italy + else if (strcmp(country_code, "LT") == 0) return "EUR"; // Lithuania + else if (strcmp(country_code, "LU") == 0) return "EUR"; // Luxembourg + else if (strcmp(country_code, "LV") == 0) return "EUR"; // Latvia + else if (strcmp(country_code, "MT") == 0) return "EUR"; // Malta + else if (strcmp(country_code, "NL") == 0) return "EUR"; // Netherlands + else if (strcmp(country_code, "PT") == 0) return "EUR"; // Portugal + else if (strcmp(country_code, "SI") == 0) return "EUR"; // Slovenia + else if (strcmp(country_code, "SK") == 0) return "EUR"; // Slovakia + + // Default fallback + return "EUR"; +} + invoice administration_create_empty_invoice() { invoice result; @@ -698,6 +795,8 @@ invoice administration_create_empty_invoice() result.issued_at = time(NULL); result.delivered_at = time(NULL); result.expires_at = time(NULL) + administration_get_default_invoice_expire_duration(); + list_init(&result.billing_items); // @leak + strops_copy(result.currency, administration_get_default_currency_for_country(g_administration.company_info.address.country_code), CURRENCY_LENGTH); return result; } @@ -715,4 +814,86 @@ project administration_create_empty_project() memset(&result, 0, sizeof(project)); snprintf(result.id, sizeof(result.id), "P/%d", administration_create_id()); return result; +} + +static bool administration_get_tax_bracket_by_id(country_tax_bracket* buffer, char* id) +{ + assert(buffer); + + list_iterator_start(&g_administration.tax_brackets); + while (list_iterator_hasnext(&g_administration.tax_brackets)) { + country_tax_bracket c = *(country_tax_bracket *)list_iterator_next(&g_administration.tax_brackets); + if (strcmp(c.id, id) == 0) + { + *buffer = c; + list_iterator_stop(&g_administration.tax_brackets); + return true; + } + } + list_iterator_stop(&g_administration.tax_brackets); + + return false; +} + +static void administration_recalculate_billing_item_totals(billing_item* item) +{ + if (item->amount_is_percentage) + { + item->net = item->net_per_item * (item->amount / 100.0f); + } + else + { + item->net = item->net_per_item * item->amount; + } + + if (item->discount != 0) + { + if (item->discount_is_percentage) + { + item->net -= item->net * (item->discount / 100.0f); + } + else + { + item->net -= item->discount; + } + } + + country_tax_bracket bracket; + if (administration_get_tax_bracket_by_id(&bracket, item->tax_bracket_id)) + { + item->tax = item->net * (bracket.rate/100.0f); + } + + item->total = item->net + item->tax; +} + +bool administration_update_billing_item_of_invoice(invoice* invoice, billing_item item) +{ + list_iterator_start(&invoice->billing_items); + while (list_iterator_hasnext(&invoice->billing_items)) { + billing_item* c = (billing_item *)list_iterator_next(&invoice->billing_items); + + if (strcmp(c->id, item.id) == 0) { + memcpy(c, &item, sizeof(billing_item)); + administration_recalculate_billing_item_totals(c); + list_iterator_stop(&invoice->billing_items); + return true; + } + } + list_iterator_stop(&invoice->billing_items); + + return false; +} + +void administration_invoice_set_currency(invoice* invoice, char* currency) +{ + strops_copy(invoice->currency, currency, CURRENCY_LENGTH); + + 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, CURRENCY_LENGTH); + } + list_iterator_stop(&invoice->billing_items); }
\ No newline at end of file diff --git a/src/ui/ui_invoices.cpp b/src/ui/ui_invoices.cpp index 2b5a834..036d85b 100644 --- a/src/ui/ui_invoices.cpp +++ b/src/ui/ui_invoices.cpp @@ -16,21 +16,136 @@ void ui_draw_address_form(address* buffer); static view_state current_view_state = view_state::LIST; static invoice active_invoice = {0}; +cost_center* cost_center_list_buffer = 0; +country_tax_bracket* tax_bracket_list_buffer = 0; + void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false, bool* on_autocomplete = 0); void ui_setup_invoices() { current_view_state = view_state::LIST; active_invoice = administration_create_empty_invoice(); + + u32 costcenter_count = administration_get_cost_center_count(); + cost_center_list_buffer = (cost_center*) malloc(sizeof(cost_center) * costcenter_count); // @leak + + u32 tax_bracket_count = administration_get_tax_bracket_count(); + tax_bracket_list_buffer = (country_tax_bracket*) malloc(sizeof(country_tax_bracket) * tax_bracket_count); // @leak +} + +// TODO move custom ui functions to helpers.cpp + +void draw_tax_bracket_selector(char* tax_bracket_id) +{ + country_tax_bracket* selected_tax_bracket = NULL; + + country_tax_bracket* buffer = tax_bracket_list_buffer; + u32 tax_bracket_count = administration_get_tax_brackets_for_country(buffer, administration_get_company_info().address.country_code); + + // Select tax bracket by given id. + if (strlen(tax_bracket_id) > 0) + { + for (u32 i = 0; i < tax_bracket_count; i++) + { + if (strcmp(buffer[i].id, tax_bracket_id) == 0) + { + selected_tax_bracket = &buffer[i]; + break; + } + } + } + + int selected_tax_bracket_index = -1; + char rate_str_buf[20]; + rate_str_buf[0] = 0; + if (selected_tax_bracket) + { + if (strcmp(selected_tax_bracket->country_code, "00") == 0) snprintf(rate_str_buf, 20, "%s", localize(selected_tax_bracket->description)); + else snprintf(rate_str_buf, 20, "%s/%.1f%%", selected_tax_bracket->country_code, selected_tax_bracket->rate); + } + + if (ImGui::BeginCombo("##Tax Bracket", rate_str_buf)) + { + for (u32 n = 0; n < tax_bracket_count; n++) + { + bool is_selected = selected_tax_bracket && strcmp(selected_tax_bracket->id, buffer[n].id) == 0; + + if (strcmp(buffer[n].country_code, "00") == 0) snprintf(rate_str_buf, 20, "%s", localize(buffer[n].description)); + else snprintf(rate_str_buf, 20, "%s/%.1f%%", buffer[n].country_code, buffer[n].rate); + + if (ImGui::Selectable(rate_str_buf, is_selected)) { + selected_tax_bracket_index = n; + } + } + ImGui::EndCombo(); + } + + if (selected_tax_bracket_index != -1) { + strops_copy(tax_bracket_id, buffer[selected_tax_bracket_index].id, 16); + } +} + +bool draw_currency_selector(char* currency) +{ + int currentCurrency = 0; + bool result = false; + + // Top 15 most traded currencies + all EU official currencies + const char* currencies[] = { + // Top 15 + "EUR", "USD", "JPY", "GBP", "AUD", "CAD", "CHF", + "CNY", "HKD", "NZD", "SEK", "KRW", "SGD", "NOK", "MXN", + + // Additional EU currencies + "BGN", // Bulgarian Lev + "CZK", // Czech Koruna + "DKK", // Danish Krone + "HUF", // Hungarian Forint + "PLN", // Polish Zloty + "RON", // Romanian Leu + // "HRK", // Croatian Kuna (legacy, replaced by EUR in 2023) + }; + int currency_count = sizeof(currencies) / sizeof(char*); + + if (strlen(currency) > 0) + { + for (int i = 0; i < currency_count; i++) + { + if (strcmp(currencies[i], currency) == 0) + { + currentCurrency = i; + break; + } + } + } + + ImGui::SetNextItemWidth(100.0f); + if (ImGui::BeginCombo("##currency", currencies[currentCurrency])) + { + for (int n = 0; n < IM_ARRAYSIZE(currencies); n++) + { + bool isSelected = (currentCurrency == n); + if (ImGui::Selectable(currencies[n], isSelected)) + { + result = true; + strops_copy(currency, currencies[n], CURRENCY_LENGTH); + } + + if (isSelected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + return result; } void draw_costcenter_selector(char* costcenter_id) { cost_center* selected_costcenter = NULL; - u32 costcenter_count = administration_get_cost_center_count(); - cost_center* buffer = (cost_center*) malloc(sizeof(cost_center) * costcenter_count); - costcenter_count = administration_get_cost_centers(buffer); + cost_center* buffer = cost_center_list_buffer; + u32 costcenter_count = administration_get_cost_centers(buffer); // Select cost center by given id. if (strlen(costcenter_id) > 0) @@ -61,8 +176,6 @@ void draw_costcenter_selector(char* costcenter_id) if (selected_costcenter_index != -1) { strops_copy(costcenter_id, buffer[selected_costcenter_index].id, 16); } - - free(buffer); } void draw_project_selector(char* project_id) @@ -106,9 +219,105 @@ void draw_project_selector(char* project_id) free(buffer); } +static void draw_invoice_items_form(invoice* invoice) +{ + u32 invoice_items = administration_get_billing_items_count(invoice); + billing_item* buffer = (billing_item*)malloc(sizeof(billing_item) * invoice_items); + administration_get_all_billing_items_for_invoice(invoice, buffer); + + if (ImGui::BeginTable("TableBillingItems", 8, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + + ImGui::TableSetupColumn("Amount", ImGuiTableColumnFlags_WidthFixed, 80); + ImGui::TableSetupColumn("Description"); + ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Discount", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Net", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Tax %", ImGuiTableColumnFlags_WidthFixed, 170); + ImGui::TableSetupColumn("Tax", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthFixed, 100); + + //ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 100); + ImGui::TableHeadersRow(); + + for (u32 i = 0; i < invoice_items; i++) + { + billing_item item = buffer[i]; + + ImGui::TableNextRow(); + + ImGui::PushID(i); + + ImGui::TableSetColumnIndex(0); + ImGui::InputFloat("##amount", &item.amount, 0.0f, 0.0f, "%.0f"); + ImGui::SameLine(); + + // Toggle between X and % + { + const char* items[] = { "X", "%" }; + if (ImGui::BeginCombo("Mode", items[item.amount_is_percentage])) { + for (int n = 0; n < 2; n++) { + bool is_selected = (n == (int)item.amount_is_percentage); + if (ImGui::Selectable(items[n], is_selected)) { + item.amount_is_percentage = n; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + } + + ImGui::TableSetColumnIndex(1); + ImGui::InputText("##desc", item.description, IM_ARRAYSIZE(item.description)); + + ImGui::TableSetColumnIndex(2); + ImGui::InputFloat("##price", &item.net_per_item, 0.0f, 0.0f, "%.2f"); + + ImGui::TableSetColumnIndex(3); + ImGui::InputFloat("##discount", &item.discount, 0.0f, 0.0f, "%.0f"); + ImGui::SameLine(); + + // Toggle between currency and % + { + const char* items[] = { item.currency, "%" }; + if (ImGui::BeginCombo("Mode##discountMode", items[item.discount_is_percentage])) { + for (int n = 0; n < 2; n++) { + bool is_selected = (n == (int)item.discount_is_percentage); + if (ImGui::Selectable(items[n], is_selected)) { + item.discount_is_percentage = n; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + } + + ImGui::TableSetColumnIndex(4); + ImGui::Text("%.2f %s", item.net, item.currency); + + ImGui::TableSetColumnIndex(5); + draw_tax_bracket_selector(item.tax_bracket_id); + + ImGui::TableSetColumnIndex(6); + ImGui::Text("%.2f %s", item.tax, item.currency); + + ImGui::TableSetColumnIndex(7); + ImGui::Text("%.2f %s", item.total, item.currency); + + ImGui::PopID(); + + administration_update_billing_item_of_invoice(invoice, item); + } + + ImGui::EndTable(); + } +} + void draw_invoice_form(invoice* buffer, bool viewing_only = false) { - //float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::BeginDisabled(); // 1. Identifier @@ -122,7 +331,7 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false) // 3. Supplier (you) ImGui::Text("Supplier: %s", buffer->supplier.name); - // 6. Invoice issued at + // 4. Invoice issued at ImGui::BeginDisabled(); tm issued_at_date = *gmtime(&buffer->issued_at); if (ImGui::DatePicker("##issuedAt", issued_at_date)) @@ -133,7 +342,7 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false) ImGui::Text("Invoice issued at"); ImGui::EndDisabled(); - // 7. Invoice expires at + // 5. Invoice expires at ImGui::BeginDisabled(); tm expires_at_date = *gmtime(&buffer->expires_at); if (ImGui::DatePicker("##expiresAt", expires_at_date)) @@ -144,7 +353,7 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false) ImGui::Text("Invoice expires at"); ImGui::EndDisabled(); - // 8. Product/service delivered at + // 6. Product/service delivered at tm delivered_at_date = *gmtime(&buffer->delivered_at); if (ImGui::DatePicker("##deliveredAt", delivered_at_date)) { @@ -155,7 +364,7 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false) ImGui::Separator(); - // 4. Customer information + // 7. Customer information ImGui::Text("Billing information"); bool on_autocomplete; draw_contact_form_ex(&buffer->customer, false, true, &on_autocomplete); @@ -164,7 +373,7 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false) strops_copy(buffer->customer_id, buffer->customer.id, sizeof(buffer->customer_id)); } - // 5. (optional) shipping address. + // 8. (optional) shipping address. static bool shipping_is_billing_addr = true; ImGui::Checkbox("Shipping information is billing information", &shipping_is_billing_addr); if (!shipping_is_billing_addr) { @@ -180,9 +389,33 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false) // 10. Cost center selection draw_costcenter_selector(buffer->cost_center_id); - //ImGui::SetNextItemWidth(widthAvailable*0.5f); - //ImGui::InputTextWithHint("Invoice number", "Invoice number", buffer->sequential_number, IM_ARRAYSIZE(buffer->sequential_number)); - //ImGui::SameLine();ui_helper_draw_required_tag(); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); + + // 11. New billing item button. + if (ImGui::Button(localize("+ Billing item"))) + { + billing_item item = administration_create_empty_billing_item(); + administration_add_billing_item_to_invoice(buffer, item); + } + + // 12. Dropdown for invoice currency. + ImGui::SameLine(); + ImGui::Text("| Currency: "); + ImGui::SameLine(); + if (draw_currency_selector(buffer->currency)) + { + administration_invoice_set_currency(buffer, buffer->currency); + } + + // 13. Invoice items form + draw_invoice_items_form(buffer); + + ImGui::Separator(); + + // 14. Totals overview. if (viewing_only) ImGui::EndDisabled(); } @@ -193,7 +426,7 @@ void draw_invoices_list() if (ImGui::Button(localize("form.create"))) { current_view_state = view_state::CREATE; - active_invoice = administration_create_empty_invoice(); + active_invoice = administration_create_empty_invoice(); // @leak active_invoice.supplier = administration_get_company_info(); strops_copy(active_invoice.supplier_id, active_invoice.supplier.id, sizeof(active_invoice.supplier_id)); } diff --git a/src/ui/ui_main.cpp b/src/ui/ui_main.cpp index 05a98b4..b4617d1 100644 --- a/src/ui/ui_main.cpp +++ b/src/ui/ui_main.cpp @@ -16,7 +16,7 @@ typedef enum END } dashboard_view_state; -static dashboard_view_state dashboard_state = dashboard_view_state::INVOICES; +static dashboard_view_state dashboard_state = dashboard_view_state::END; void (*drawcalls[dashboard_view_state::END])(void) = { ui_draw_invoices, 0, @@ -45,6 +45,8 @@ static void set_dashboard_state(dashboard_view_state state) void ui_draw_main() { + if (dashboard_state == dashboard_view_state::END) set_dashboard_state(dashboard_view_state::INVOICES); + // @localize if (ImGui::BeginMainMenuBar()) { |
