From 38019a9693375ac6719ffec43bff63774e142387 Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Sat, 13 Sep 2025 18:47:01 +0200 Subject: invoice peppol work --- .gitignore | 3 +- docs/CHANGES.rst | 9 +-- include/administration.hpp | 12 +--- include/file_templates.hpp | 109 ++++++++++++++++++++++++++----- libs/ImGuiDatePicker/ImGuiDatePicker.cpp | 2 +- src/administration.cpp | 1 + src/administration_writer.cpp | 65 ++++++++++++++++-- src/ui/ui_settings.cpp | 2 +- 8 files changed, 165 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index 9fafbe8..b822716 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ imgui.ini -build/* \ No newline at end of file +build/* +example.openbook \ No newline at end of file diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index 7c7ef64..e9881ec 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -9,8 +9,7 @@ TODO: - read invoice from Holodeck instance - send invoice via Holodeck instance - View invoice history for contacts -- View invoice history for projects -- Create monthly and quarterly results (per project). +- View invoice history for projects - Create quarterly tax reports for NL. - Make sure invoices/expenses cannot be added when company info is empty/invalid. - net negative billing items should not also have discounts. @@ -19,7 +18,9 @@ TODO: - View local business number / vat number naming on contact form. e.g. when country is austria: "Österreichische Umsatzsteuer-Identifikationsnummer" - add allowances to invoice items - add accounting cost to invoice -- if an invoice currency != default currency, final amount should be manually set as the total is always represented in the default currency. +- outgoing invoices should always have default currency +- if an incomming invoice currency != default currency, final amount should be manually set as the total is always represented in the default currency. +- ICP reports v0.1 (master) @@ -31,5 +32,5 @@ v0.1 (master) - Generate default VAT rates - Generate default cost centers - Reading & writing administration files -- Archive invoices in Peppol BIS 3.0 format. +- Archive invoices in Peppol BIS 3.0 format - Quarterly income statement reports \ No newline at end of file diff --git a/include/administration.hpp b/include/administration.hpp index fe3d7b2..95d4622 100644 --- a/include/administration.hpp +++ b/include/administration.hpp @@ -109,18 +109,15 @@ typedef struct float amount; bool amount_is_percentage; char description[MAX_LEN_LONG_DESC]; - float net_per_item; + float net_per_item; // Net per item before discount. float discount; bool discount_is_percentage; float allowance; // Total discount. - float net; + float net; // Total net, with discount. char currency[MAX_LEN_CURRENCY]; // 3 letter code char tax_bracket_id[MAX_LEN_ID]; // T/[id] float tax; float total; - - // TODO uninplemented - char tax_section[MAX_LEN_TAX_SECTION]; } billing_item; /** @@ -257,11 +254,6 @@ typedef struct invoice_status status; bool is_outgoing; // Outgoing or incomming invoice. payment_information payment_means; - - bool is_intra_community; // TODO uninplemented - time_t payment_on_account_date; // TODO uninplemented - char tax_representative[MAX_LEN_LONG_DESC]; // TODO uninplemented - char corrected_sequential_number[MAX_LEN_ID]; // TODO uninplemented // Used for forms, not stored on disk. Filled when retrieved. contact supplier; diff --git a/include/file_templates.hpp b/include/file_templates.hpp index cb89050..af2f373 100644 --- a/include/file_templates.hpp +++ b/include/file_templates.hpp @@ -64,23 +64,37 @@ const char* peppol_invoice_tax_subtotal_template = " \n"; const char* peppol_invoice_line_template = -" \n" -" {{LINE_ID}}\n" -" {{QUANTITY}}\n" -" {{LINE_AMOUNT}}\n" -" \n" -" {{ITEM_NAME}}\n" -" \n" -" {{LINE_TAX_CATEGORY}}\n" -" {{LINE_TAX_PERCENT}}\n" -" \n" -" VAT\n" -" \n" -" \n" -" \n" -" \n" -" {{UNIT_PRICE}}\n" -" \n" +" \n" +" {{LINE_ID}}\n" +" {{QUANTITY}}\n" +" {{LINE_AMOUNT}}\n" + +" \n" +" {{TAX_BRACKET_ID}}\n" +" \n" + +" \n" +" false\n" +" Discount\n" +" {{ALLOWANCE_IS_PERCENTAGE}}\n" +" {{DISCOUNT_TOTAL}}\n" +" {{DISCOUNT_BASE_AMOUNT}}\n" +" \n" + +" \n" +" {{ITEM_NAME}}\n" +" \n" +" {{LINE_TAX_CATEGORY}}\n" +" {{LINE_TAX_PERCENT}}\n" +" \n" +" VAT\n" +" \n" +" \n" +" \n" + +" \n" +" {{UNIT_PRICE}}\n" +" \n" " \n"; const char *peppol_invoice_template = @@ -94,12 +108,25 @@ const char *peppol_invoice_template = "\n" " {{INVOICE_ID}}\n" " {{ISSUE_DATE}}\n" +" {{DUE_DATE}}\n" " 380\n" " {{CURRENCY}}\n" "\n" +" \n" +" {{INVOICE_DOCUMENT}}\n" +" \n" +"\n" +" \n" +" {{PROJECT_ID}}\n" +" \n" +" {{COST_CENTER_CODE}}\n" +"\n" " \n" " \n" " {{SUPPLIER_ENDPOINT_ID}}\n" +" \n" +" {{SUPPLIER_ID}}\n" +" \n" " \n" " {{SUPPLIER_NAME}}\n" " \n" @@ -119,12 +146,27 @@ const char *peppol_invoice_template = " VAT\n" " \n" " \n" +"\n" +" \n" +" {{SUPPLIER_LEGAL_NAME}}\n" +" {{SUPPLIER_BUSINESS_ID}}\n" +" \n" +"\n" +" \n" +" {{SUPPLIER_NAME}}\n" +" {{SUPPLIER_PHONE_NUMBER}}\n" +" {{SUPPLIER_EMAIL}}\n" +" \n" +"\n" " \n" " \n" "\n" " \n" " \n" " {{CUSTOMER_ENDPOINT_ID}}\n" +" \n" +" {{CUSTOMER_ID}}\n" +" \n" " \n" " {{CUSTOMER_NAME}}\n" " \n" @@ -144,9 +186,42 @@ const char *peppol_invoice_template = " VAT\n" " \n" " \n" +"\n" +" \n" +" {{CUSTOMER_LEGAL_NAME}}\n" +" {{CUSTOMER_BUSINESS_ID}}\n" +" \n" +"\n" +" \n" +" {{CUSTOMER_NAME}}\n" +" {{CUSTOMER_PHONE_NUMBER}}\n" +" {{CUSTOMER_EMAIL}}\n" +" \n" +"\n" " \n" " \n" "\n" +"\n" +" {{DELIVERY_DATE}}\n" +" \n" +" \n" +" {{DELIVERY_STREET}}\n" +" {{DELIVERY_STREET2}}\n" +" {{DELIVERY_CITY}}\n" +" {{DELIVERY_POSTAL}}\n" +" {{DELIVERY_REGION}}\n" +" \n" +" {{DELIVERY_COUNTRY}}\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" {{DELIVERY_NAME}}\n" +" \n" +" \n" +"\n" +"\n" " \n" " {{PAYMENT_TYPE}}\n" " {{INVOICE_ID}}\n" diff --git a/libs/ImGuiDatePicker/ImGuiDatePicker.cpp b/libs/ImGuiDatePicker/ImGuiDatePicker.cpp index 8c81b2b..207a902 100644 --- a/libs/ImGuiDatePicker/ImGuiDatePicker.cpp +++ b/libs/ImGuiDatePicker/ImGuiDatePicker.cpp @@ -361,7 +361,7 @@ namespace ImGui if (Button(std::to_string(day).c_str(), ImVec2(GetContentRegionAvail().x, GetTextLineHeightWithSpacing() + 5.0f))) { - v = EncodeTimePoint(day, month, year); + v = EncodeTimePoint(day+1, month, year); res = true; CloseCurrentPopup(); } diff --git a/src/administration.cpp b/src/administration.cpp index 49b2559..2a18747 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -799,6 +799,7 @@ void administration_company_info_import(contact data) 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, administration_get_default_currency_for_country(g_administration.company_info.address.country_code), MAX_LEN_CURRENCY); diff --git a/src/administration_writer.cpp b/src/administration_writer.cpp index f8b4302..779a5f1 100644 --- a/src/administration_writer.cpp +++ b/src/administration_writer.cpp @@ -229,7 +229,7 @@ bool administration_writer_save_invoice_blocking(invoice inv) STOPWATCH_START; bool result = 1; - int buf_length = 150000; // Ballpark file content size. + int buf_length = 150000; // Ballpark file content size. char* file_content = (char*)malloc(buf_length); memset(file_content, 0, buf_length); memcpy(file_content, peppol_invoice_template, strlen(peppol_invoice_template)); @@ -237,12 +237,31 @@ bool administration_writer_save_invoice_blocking(invoice inv) struct tm *tm_info = 0; char date_buffer[11]; // "YYYY-MM-DD" + null terminator + // properties not stored from invoice: + // - id (can be retrieved from filename) + // - invoice allowance (can be recalculated from billing items) + + // properties not stored from supplier/customer/addressee: + // - type + // - bank account + // These can all be retrieved from existing contacts. + + // properties not stored from billing item: + // - invoice_id (not necessary) + // - discount (can be recalculated from line_amount - (quantity * unit_price) ) + // - tax (can be recalculated) + // - total (can be recalculated) + strops_replace(file_content, buf_length, "{{INVOICE_ID}}", inv.sequential_number); strops_replace(file_content, buf_length, "{{CURRENCY}}", inv.currency); + strops_replace(file_content, buf_length, "{{PROJECT_ID}}", inv.project_id); + strops_replace(file_content, buf_length, "{{COST_CENTER_CODE}}", inv.cost_center_id); + strops_replace(file_content, buf_length, "{{INVOICE_DOCUMENT}}", inv.document); // Supplier data strops_replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_SCHEME}}", administration_writer_get_eas_scheme_for_address(inv.supplier.address)); strops_replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_ID}}", administration_writer_get_eas_id_for_contact(inv.supplier)); + strops_replace(file_content, buf_length, "{{SUPPLIER_ID}}", inv.supplier.id); strops_replace(file_content, buf_length, "{{SUPPLIER_NAME}}", inv.supplier.name); strops_replace(file_content, buf_length, "{{SUPPLIER_STREET}}", inv.supplier.address.address1); strops_replace(file_content, buf_length, "{{SUPPLIER_STREET2}}", inv.supplier.address.address2); @@ -251,10 +270,15 @@ bool administration_writer_save_invoice_blocking(invoice inv) strops_replace(file_content, buf_length, "{{SUPPLIER_REGION}}", inv.supplier.address.region); strops_replace(file_content, buf_length, "{{SUPPLIER_COUNTRY}}", inv.supplier.address.country_code); strops_replace(file_content, buf_length, "{{SUPPLIER_VAT_ID}}", inv.supplier.taxid); + strops_replace(file_content, buf_length, "{{SUPPLIER_LEGAL_NAME}}", inv.supplier.name); + strops_replace(file_content, buf_length, "{{SUPPLIER_BUSINESS_ID}}", inv.supplier.businessid); + strops_replace(file_content, buf_length, "{{SUPPLIER_PHONE_NUMBER}}", inv.supplier.phone_number); + strops_replace(file_content, buf_length, "{{SUPPLIER_EMAIL}}", inv.supplier.email); // Customer data strops_replace(file_content, buf_length, "{{CUSTOMER_ENDPOINT_SCHEME}}", administration_writer_get_eas_scheme_for_address(inv.customer.address)); strops_replace(file_content, buf_length, "{{CUSTOMER_ENDPOINT_ID}}", administration_writer_get_eas_id_for_contact(inv.customer)); + strops_replace(file_content, buf_length, "{{CUSTOMER_ID}}", inv.customer.id); strops_replace(file_content, buf_length, "{{CUSTOMER_NAME}}", inv.customer.name); strops_replace(file_content, buf_length, "{{CUSTOMER_STREET}}", inv.customer.address.address1); strops_replace(file_content, buf_length, "{{CUSTOMER_STREET2}}", inv.customer.address.address2); @@ -263,6 +287,22 @@ bool administration_writer_save_invoice_blocking(invoice inv) strops_replace(file_content, buf_length, "{{CUSTOMER_REGION}}", inv.customer.address.region); strops_replace(file_content, buf_length, "{{CUSTOMER_COUNTRY}}", inv.customer.address.country_code); strops_replace(file_content, buf_length, "{{CUSTOMER_VAT_ID}}", inv.customer.taxid); + strops_replace(file_content, buf_length, "{{CUSTOMER_LEGAL_NAME}}", inv.customer.name); + strops_replace(file_content, buf_length, "{{CUSTOMER_BUSINESS_ID}}", inv.customer.businessid); + strops_replace(file_content, buf_length, "{{CUSTOMER_PHONE_NUMBER}}", inv.customer.phone_number); + strops_replace(file_content, buf_length, "{{CUSTOMER_EMAIL}}", inv.customer.email); + + // Delivery data + tm_info = localtime(&inv.delivered_at); + strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); + strops_replace(file_content, buf_length, "{{DELIVERY_DATE}}", date_buffer); + strops_replace(file_content, buf_length, "{{DELIVERY_NAME}}", inv.addressee.name); + strops_replace(file_content, buf_length, "{{DELIVERY_STREET}}", inv.addressee.address.address1); + strops_replace(file_content, buf_length, "{{DELIVERY_STREET2}}", inv.addressee.address.address2); + strops_replace(file_content, buf_length, "{{DELIVERY_CITY}}", inv.addressee.address.city); + strops_replace(file_content, buf_length, "{{DELIVERY_POSTAL}}", inv.addressee.address.postal); + strops_replace(file_content, buf_length, "{{DELIVERY_REGION}}", inv.addressee.address.region); + strops_replace(file_content, buf_length, "{{DELIVERY_COUNTRY}}", inv.addressee.address.country_code); // Payment means strops_replace_int32(file_content, buf_length, "{{PAYMENT_TYPE}}", inv.payment_means.payment_method); @@ -270,7 +310,7 @@ bool administration_writer_save_invoice_blocking(invoice inv) strops_replace(file_content, buf_length, "{{SUPPLIER_BIC}}", inv.payment_means.service_provider_id); // Tax breakdown - strops_replace_float(file_content, buf_length, "{{TOTAL_TAX_AMOUNT}}", inv.total, 2); + strops_replace_float(file_content, buf_length, "{{TOTAL_TAX_AMOUNT}}", inv.tax, 2); { // Create tax subtotal list. country_tax_bracket* tax_bracket_buffer = (country_tax_bracket*)malloc(sizeof(country_tax_bracket)*administration_billing_item_count(&inv)); u32 tax_bracket_count = administration_invoice_get_tax_brackets(&inv, tax_bracket_buffer); @@ -337,10 +377,23 @@ bool administration_writer_save_invoice_blocking(invoice inv) strops_replace(billing_item_file_content, billing_item_buf_length, "{{ITEM_NAME}}", bi.description); strops_replace(billing_item_file_content, billing_item_buf_length, "{{LINE_TAX_CATEGORY}}", bracket.category_code); strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{LINE_TAX_PERCENT}}", bracket.rate, 2); - strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{LINE_AMOUNT}}", bi.net, 2); + strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{LINE_AMOUNT}}", bi.net, 2); // line amount = net_per_item * items_count - discount strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{QUANTITY}}", bi.amount, 2); - strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{UNIT_PRICE}}", bi.net_per_item, 2); + strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{UNIT_PRICE}}", bi.net_per_item, 2); // unit price before discount strops_replace(billing_item_file_content, billing_item_buf_length, "{{UNIT_CODE}}", bi.amount_is_percentage ? "%" : "X"); + strops_replace(billing_item_file_content, billing_item_buf_length, "{{TAX_BRACKET_ID}}", bi.tax_bracket_id); + + if (bi.discount_is_percentage) { + strops_replace(billing_item_file_content, billing_item_buf_length, "{{ALLOWANCE_IS_PERCENTAGE}}", + "{{DISCOUNT_TOTAL_PERCENTAGE}}"); + strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", bi.allowance / (bi.net + bi.allowance), 2); + } + else { + strops_replace(billing_item_file_content, billing_item_buf_length, "{{ALLOWANCE_IS_PERCENTAGE}}", ""); + } + + strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL}}", bi.allowance, 2); + strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_BASE_AMOUNT}}", bi.net + bi.allowance, 2); // Total net before discount. u32 content_len = (u32)strlen(billing_item_file_content); memcpy(billing_item_list_buffer+billing_item_list_buffer_cursor, billing_item_file_content, content_len); @@ -360,6 +413,10 @@ bool administration_writer_save_invoice_blocking(invoice inv) strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); strops_replace(file_content, buf_length, "{{ISSUE_DATE}}", date_buffer); + tm_info = localtime(&inv.expires_at); + strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); + strops_replace(file_content, buf_length, "{{DUE_DATE}}", date_buffer); + //// Write to Disk. char final_path[50]; snprintf(final_path, 50, "%s.xml", inv.id); diff --git a/src/ui/ui_settings.cpp b/src/ui/ui_settings.cpp index ae05b1a..dfe6b96 100644 --- a/src/ui/ui_settings.cpp +++ b/src/ui/ui_settings.cpp @@ -61,7 +61,7 @@ static void ui_draw_vat_rates() country_tax_bracket c = tax_brackets[i]; // Set to false for shared rates. - bool can_be_modified = true; + bool can_be_modified = false; // Check for fixed rates shared accross countries. if (strcmp(c.country_code, "00") == 0) -- cgit v1.2.3-70-g09d2