summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAldrik Ramaekers <aldrikboy@gmail.com>2025-08-16 12:57:53 +0200
committerAldrik Ramaekers <aldrikboy@gmail.com>2025-08-16 12:57:53 +0200
commit12afa4c63e642452676f77830ec0383a6132883e (patch)
tree63e23c4892e2d399e23040016e0f10cffd01d341
parent05bc81cd42c5aeff7cfb6cf6b18f88792e7c16c9 (diff)
working on invoice form
-rw-r--r--docs/README.rst3
-rw-r--r--include/administration.hpp26
-rw-r--r--src/administration.cpp79
-rw-r--r--src/ui/ui_contacts.cpp3
-rw-r--r--src/ui/ui_invoices.cpp101
5 files changed, 160 insertions, 52 deletions
diff --git a/docs/README.rst b/docs/README.rst
index a9d4c18..469fab4 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -20,13 +20,14 @@ What OpenBooks **can** do:
- OpenBooks can export/email invoices in PDF, UBL and Peppol format.
- OpenBooks can send and receive invoices over the Peppol network (using a locally run `Holodeck <https://holodeck-b2b.org/>`_ instance).
- OpenBooks can create tax reports and audit files (see supported countries list).
+- OpenBooks can create Intra-Community transactions declaration (ICP) reports.
What OpenBooks **can't** do:
- Intrastat reporting
- Sustainability Reporting (CSRD)
- PoS Fiscalization
-- Submit tax and audit reports
+- Automatically submit tax, audit or ICP reports
2. Supported countries
----------------------
diff --git a/include/administration.hpp b/include/administration.hpp
index fd55978..af449bb 100644
--- a/include/administration.hpp
+++ b/include/administration.hpp
@@ -62,6 +62,9 @@ typedef struct
typedef enum
{
+ INVOICE_CONCEPT,
+ INVOICE_SENT,
+ INVOICE_REMINDED,
INVOICE_PAID,
INVOICE_EXPIRED,
INVOICE_CANCELLED,
@@ -84,14 +87,11 @@ typedef struct
float net;
char currency[CURRENCY_LENGTH];
char tax_bracket_id[16];
+ float tax;
+ float total;
// todo
char tax_section[16];
- float tax;
- float total;
- bool is_intra_community;
- bool is_triangulation;
- char internal_code[64];
} billing_item;
typedef struct
@@ -100,26 +100,27 @@ typedef struct
char sequential_number[16]; // INV0000000000 - INV9999999999
char customer_id[16];
char supplier_id[16];
- address shipping_address;
+ contact addressee;
time_t issued_at;
time_t expires_at;
time_t delivered_at;
char document[255]; // path to copy of document for incomming invoice.
char project_id[16];
char cost_center_id[16];
-
list_t billing_items;
float total;
float tax;
float net;
- invoice_status status;
char currency[CURRENCY_LENGTH];
-
+ bool is_triangulation;
+
+ bool is_intra_community; // TODO
+ invoice_status status; // TODO
time_t payment_on_account_date; // TODO
char tax_representative[64]; // TODO
char corrected_sequential_number[16]; // TODO
- // Not stored.
+ // Used for forms, not stored on disk. Filled when retrieved.
contact supplier;
contact customer;
} invoice;
@@ -184,9 +185,12 @@ bool administration_update_cost_center(cost_center data);
invoice administration_create_empty_invoice();
void administration_invoice_set_currency(invoice* invoice, char* currency);
+bool administration_is_invoice_valid(invoice* invoice);
+bool administration_add_invoice(invoice* invoice);
billing_item administration_create_empty_billing_item();
u32 administration_get_all_billing_items_for_invoice(invoice* invoice, billing_item* buffer);
u32 administration_get_billing_items_count(invoice* invoice);
bool administration_add_billing_item_to_invoice(invoice* invoice, billing_item item);
-bool administration_update_billing_item_of_invoice(invoice* invoice, billing_item item); \ No newline at end of file
+bool administration_update_billing_item_of_invoice(invoice* invoice, billing_item item);
+bool administration_remove_billing_item_from_invoice(invoice* invoice, billing_item item); \ No newline at end of file
diff --git a/src/administration.cpp b/src/administration.cpp
index 2e16775..6ed7d63 100644
--- a/src/administration.cpp
+++ b/src/administration.cpp
@@ -268,6 +268,8 @@ static void administration_create_debug_data()
strops_copy(g_administration.company_info.address.address1, "Keerderstraat 81", sizeof(g_administration.company_info.address.address1));
strops_copy(g_administration.company_info.address.address2, "6226XW Maastricht", sizeof(g_administration.company_info.address.address2));
strops_copy(g_administration.company_info.address.country_code, "NL", sizeof(g_administration.company_info.address.country_code));
+ strops_copy(g_administration.company_info.taxid, "123", sizeof(g_administration.company_info.taxid));
+ strops_copy(g_administration.company_info.businessid, "123", sizeof(g_administration.company_info.businessid));
}
void administration_create()
@@ -275,6 +277,7 @@ void administration_create()
g_administration.next_id = 1;
g_administration.next_sequence_number = 1;
+ list_init(&g_administration.invoices);
list_init(&g_administration.contacts);
list_init(&g_administration.projects);
list_init(&g_administration.tax_brackets);
@@ -291,6 +294,7 @@ void administration_create()
void administration_destroy()
{
+ list_destroy(&g_administration.invoices);
list_destroy(&g_administration.contacts);
list_destroy(&g_administration.projects);
list_destroy(&g_administration.tax_brackets);
@@ -835,6 +839,23 @@ static bool administration_get_tax_bracket_by_id(country_tax_bracket* buffer, ch
return false;
}
+static void administration_recalculate_invoice_totals(invoice* invoice)
+{
+ invoice->tax = 0.0f;
+ invoice->total = 0.0f;
+ invoice->net = 0.0f;
+
+ list_iterator_start(&invoice->billing_items);
+ while (list_iterator_hasnext(&invoice->billing_items)) {
+ billing_item* c = (billing_item *)list_iterator_next(&invoice->billing_items);
+
+ invoice->tax += c->tax;
+ invoice->total += c->total;
+ invoice->net += c->net;
+ }
+ list_iterator_stop(&invoice->billing_items);
+}
+
static void administration_recalculate_billing_item_totals(billing_item* item)
{
if (item->amount_is_percentage)
@@ -867,6 +888,23 @@ static void administration_recalculate_billing_item_totals(billing_item* item)
item->total = item->net + item->tax;
}
+bool administration_remove_billing_item_from_invoice(invoice* invoice, billing_item item)
+{
+ list_iterator_start(&invoice->billing_items);
+ while (list_iterator_hasnext(&invoice->billing_items)) {
+ billing_item* c = (billing_item *)list_iterator_next(&invoice->billing_items);
+
+ if (strcmp(c->id, item.id) == 0) {
+ list_iterator_stop(&invoice->billing_items);
+ list_delete(&invoice->billing_items, c);
+ return true;
+ }
+ }
+ list_iterator_stop(&invoice->billing_items);
+
+ return false;
+}
+
bool administration_update_billing_item_of_invoice(invoice* invoice, billing_item item)
{
list_iterator_start(&invoice->billing_items);
@@ -874,9 +912,11 @@ bool administration_update_billing_item_of_invoice(invoice* invoice, billing_ite
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);
+ memcpy(c, &item, sizeof(billing_item));
list_iterator_stop(&invoice->billing_items);
+
+ administration_recalculate_billing_item_totals(c);
+ administration_recalculate_invoice_totals(invoice);
return true;
}
}
@@ -896,4 +936,39 @@ void administration_invoice_set_currency(invoice* invoice, char* currency)
strops_copy(c->currency, currency, CURRENCY_LENGTH);
}
list_iterator_stop(&invoice->billing_items);
+}
+
+bool administration_is_invoice_valid(invoice* invoice)
+{
+ if (list_size(&invoice->billing_items) == 0) return false;
+ if (invoice->is_triangulation && !administration_is_contact_valid(invoice->addressee)) return false;
+ if (!administration_is_contact_valid(invoice->customer)) return false;
+ if (!administration_is_contact_valid(invoice->supplier)) return false;
+
+ return true;
+}
+
+bool administration_add_invoice(invoice* inv)
+{
+ if (!administration_is_invoice_valid(inv)) return false;
+
+ // Invoice is valid but customer id is unset means we need to register a new contact.
+ if (strcmp(inv->customer_id, ""))
+ {
+ contact new_contact = administration_create_empty_contact();
+ strops_copy(inv->customer.id, new_contact.id, sizeof(new_contact.id));
+
+ memcpy(&new_contact, &inv->customer, sizeof(contact));
+ administration_create_contact(new_contact);
+ }
+
+ invoice* new_inv = (invoice*)malloc(sizeof(invoice));
+ memcpy((void*)new_inv, (void*)inv, sizeof(invoice));
+
+ list_append(&g_administration.invoices, new_inv);
+
+ g_administration.next_id++;
+ g_administration.next_sequence_number++;
+
+ return true;
} \ No newline at end of file
diff --git a/src/ui/ui_contacts.cpp b/src/ui/ui_contacts.cpp
index 7d03ba6..eca6ee3 100644
--- a/src/ui/ui_contacts.cpp
+++ b/src/ui/ui_contacts.cpp
@@ -77,6 +77,8 @@ void ui_setup_contacts()
void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false, bool* on_autocomplete = 0)
{
+ ImGui::PushID(buffer);
+
ImGui::Spacing();
float widthAvailable = ImGui::GetContentRegionAvail().x;
@@ -152,6 +154,7 @@ void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_
ImGui::InputTextWithHint(localize("contact.form.bankaccount"), localize("contact.form.bankaccount"), buffer->bank_account, IM_ARRAYSIZE(buffer->bank_account));
if (viewing_only) ImGui::EndDisabled();
+ ImGui::PopID();
}
void draw_contact_form(contact* buffer, bool viewing_only = false)
diff --git a/src/ui/ui_invoices.cpp b/src/ui/ui_invoices.cpp
index 036d85b..8c0943c 100644
--- a/src/ui/ui_invoices.cpp
+++ b/src/ui/ui_invoices.cpp
@@ -225,14 +225,15 @@ static void draw_invoice_items_form(invoice* 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)) {
+ if (ImGui::BeginTable("TableBillingItems", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
+ ImGui::TableSetupColumn("##actions", ImGuiTableColumnFlags_WidthFixed, 20);
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, 120);
ImGui::TableSetupColumn("Tax", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Total", ImGuiTableColumnFlags_WidthFixed, 100);
@@ -246,8 +247,14 @@ static void draw_invoice_items_form(invoice* invoice)
ImGui::TableNextRow();
ImGui::PushID(i);
+
+ ImGui::TableSetColumnIndex(0);
+ if (ImGui::Button("X"))
+ {
+ administration_remove_billing_item_from_invoice(invoice, item);
+ }
- ImGui::TableSetColumnIndex(0);
+ ImGui::TableSetColumnIndex(1);
ImGui::InputFloat("##amount", &item.amount, 0.0f, 0.0f, "%.0f");
ImGui::SameLine();
@@ -268,13 +275,17 @@ static void draw_invoice_items_form(invoice* invoice)
}
}
- ImGui::TableSetColumnIndex(1);
+ ImGui::TableSetColumnIndex(2);
+ ImGui::PushItemWidth(-1);
ImGui::InputText("##desc", item.description, IM_ARRAYSIZE(item.description));
+ ImGui::PopItemWidth();
- ImGui::TableSetColumnIndex(2);
+ ImGui::TableSetColumnIndex(3);
+ ImGui::PushItemWidth(-1);
ImGui::InputFloat("##price", &item.net_per_item, 0.0f, 0.0f, "%.2f");
+ ImGui::PopItemWidth();
- ImGui::TableSetColumnIndex(3);
+ ImGui::TableSetColumnIndex(4);
ImGui::InputFloat("##discount", &item.discount, 0.0f, 0.0f, "%.0f");
ImGui::SameLine();
@@ -295,16 +306,18 @@ static void draw_invoice_items_form(invoice* invoice)
}
}
- ImGui::TableSetColumnIndex(4);
+ ImGui::TableSetColumnIndex(5);
ImGui::Text("%.2f %s", item.net, item.currency);
- ImGui::TableSetColumnIndex(5);
+ ImGui::TableSetColumnIndex(6);
+ ImGui::PushItemWidth(-1);
draw_tax_bracket_selector(item.tax_bracket_id);
+ ImGui::PopItemWidth();
- ImGui::TableSetColumnIndex(6);
+ ImGui::TableSetColumnIndex(7);
ImGui::Text("%.2f %s", item.tax, item.currency);
- ImGui::TableSetColumnIndex(7);
+ ImGui::TableSetColumnIndex(8);
ImGui::Text("%.2f %s", item.total, item.currency);
ImGui::PopID();
@@ -312,6 +325,18 @@ static void draw_invoice_items_form(invoice* invoice)
administration_update_billing_item_of_invoice(invoice, item);
}
+ ImGui::TableNextRow();
+ ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(70, 70, 70, 255));
+
+ ImGui::TableSetColumnIndex(5);
+ ImGui::Text("%.2f %s", invoice->net, invoice->currency);
+
+ ImGui::TableSetColumnIndex(7);
+ ImGui::Text("%.2f %s", invoice->tax, invoice->currency);
+
+ ImGui::TableSetColumnIndex(8);
+ ImGui::Text("%.2f %s", invoice->total, invoice->currency);
+
ImGui::EndTable();
}
}
@@ -323,7 +348,6 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false)
// 1. Identifier
//ImGui::SetNextItemWidth(widthAvailable*0.2f);
//ImGui::InputText(localize("contact.form.identifier"), buffer->id, IM_ARRAYSIZE(buffer->id));
- if (!viewing_only) ImGui::EndDisabled();
// 2. Sequential number
ImGui::Text("Invoice number: %s", buffer->sequential_number);
@@ -332,7 +356,6 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false)
ImGui::Text("Supplier: %s", buffer->supplier.name);
// 4. Invoice issued at
- ImGui::BeginDisabled();
tm issued_at_date = *gmtime(&buffer->issued_at);
if (ImGui::DatePicker("##issuedAt", issued_at_date))
{
@@ -340,10 +363,8 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false)
}
ImGui::SameLine();
ImGui::Text("Invoice issued at");
- ImGui::EndDisabled();
// 5. Invoice expires at
- ImGui::BeginDisabled();
tm expires_at_date = *gmtime(&buffer->expires_at);
if (ImGui::DatePicker("##expiresAt", expires_at_date))
{
@@ -374,22 +395,21 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false)
}
// 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) {
+ ImGui::Checkbox("Shipping information differs from billing information (triangulation)", &buffer->is_triangulation);
+ if (buffer->is_triangulation) {
ImGui::Spacing();
ImGui::Text("Shipping information");
- ui_draw_address_form(&buffer->shipping_address);
+ draw_contact_form_ex(&buffer->addressee, 0,0,0);
}
ImGui::Separator();
// 9. Project selection
- draw_project_selector(buffer->project_id);
+ //draw_project_selector(buffer->project_id);
// 10. Cost center selection
- draw_costcenter_selector(buffer->cost_center_id);
+ //draw_costcenter_selector(buffer->cost_center_id);
+ //ImGui::Separator();
- ImGui::Separator();
ImGui::Spacing();
ImGui::Spacing();
ImGui::Spacing();
@@ -412,12 +432,8 @@ void draw_invoice_form(invoice* buffer, bool viewing_only = false)
// 13. Invoice items form
draw_invoice_items_form(buffer);
-
- ImGui::Separator();
-
- // 14. Totals overview.
- if (viewing_only) ImGui::EndDisabled();
+ //if (viewing_only) ImGui::EndDisabled();
}
void draw_invoices_list()
@@ -432,23 +448,32 @@ void draw_invoices_list()
}
}
+static void ui_draw_invoice_create()
+{
+ if (ImGui::Button(localize("form.back"))) {
+ current_view_state = view_state::LIST;
+ }
+
+ draw_invoice_form(&active_invoice);
+
+ bool can_save = administration_is_invoice_valid(&active_invoice);
+ if (!can_save) ImGui::BeginDisabled();
+
+ ImGui::Spacing();
+ if (ImGui::Button(localize("form.save"))) {
+ administration_add_invoice(&active_invoice);
+ current_view_state = view_state::LIST;
+ }
+
+ if (!can_save) ImGui::EndDisabled();
+}
+
void ui_draw_invoices()
{
switch(current_view_state)
{
case view_state::LIST: draw_invoices_list(); break;
- case view_state::CREATE:
-
- if (ImGui::Button(localize("form.back"))) {
- current_view_state = view_state::LIST;
- }
- draw_invoice_form(&active_invoice);
-
- //if () {
- //administration_create_invoice(active_invoice);
- //current_view_state = view_state::LIST;
- //}
- break;
+ case view_state::CREATE: ui_draw_invoice_create(); break;
case view_state::EDIT: break;
case view_state::VIEW: break;
}