summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/CHANGES.rst3
-rw-r--r--include/administration.hpp8
-rw-r--r--include/file_templates.hpp3
-rw-r--r--include/ui.hpp1
-rw-r--r--src/administration_reader.cpp3
-rw-r--r--src/administration_writer.cpp57
-rw-r--r--src/ui/imgui_extensions.cpp42
-rw-r--r--src/ui/ui_expenses.cpp6
-rw-r--r--src/ui/ui_invoices.cpp6
9 files changed, 114 insertions, 15 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst
index 64bd317..4ac2eed 100644
--- a/docs/CHANGES.rst
+++ b/docs/CHANGES.rst
@@ -1,10 +1,11 @@
.. _changes:
TODO:
+- write tests for invoices using multiple currencies
- refactor _add functions to use _import functions
- write tests that check error handling for corrupt files. (e.g. references to tax rates, project and cost center that failed to load)
- it is possible a referenced tax rate is loaded after an invoice is loaded. This means all invoices need to be recalculated after file load. (try to write a test for this).
-- invoice sequential number should be modifyable (for external invoices being imported)
+- invoice sequential number should be modifyable & checked for uniqueness (for external invoices being imported)
- allow cost centers to be deleted that have 0 references
- Send invoice by email
- create invoice from PDF file
diff --git a/include/administration.hpp b/include/administration.hpp
index b3f453e..e926aa6 100644
--- a/include/administration.hpp
+++ b/include/administration.hpp
@@ -251,12 +251,18 @@ typedef struct
typedef struct
{
+ char original_path[MAX_LEN_PATH];
+ char copy_path[MAX_LEN_PATH];
+} document;
+
+typedef struct
+{
char id[MAX_LEN_ID]; // I/[id]
char sequential_number[MAX_LEN_SEQ_NUM]; // INV0000000000 - INV9999999999
time_t issued_at;
time_t expires_at;
time_t delivered_at;
- char document[MAX_LEN_PATH]; // path to copy of document for incomming invoice.
+ document document; // path to copy of document for incomming invoice.
char project_id[MAX_LEN_ID]; // Optional.
char cost_center_id[MAX_LEN_ID]; // For incomming invoices. optional.
list_t billing_items;
diff --git a/include/file_templates.hpp b/include/file_templates.hpp
index 4ed3611..8571368 100644
--- a/include/file_templates.hpp
+++ b/include/file_templates.hpp
@@ -134,7 +134,8 @@ const char *peppol_invoice_template =
" </cac:DespatchDocumentReference>\n"
"\n"
" <cac:AdditionalDocumentReference>\n"
-" <cbc:ID>{{INVOICE_DOCUMENT}}</cbc:ID>\n"
+" <cbc:ID>{{INVOICE_DOCUMENT_COPY}}</cbc:ID>\n"
+" <cbc:DocumentDescription>{{INVOICE_DOCUMENT_ORIG}}</cbc:DocumentDescription>\n"
" </cac:AdditionalDocumentReference>\n"
"\n"
" <cac:OrderReference>\n"
diff --git a/include/ui.hpp b/include/ui.hpp
index ebfa210..955964b 100644
--- a/include/ui.hpp
+++ b/include/ui.hpp
@@ -98,6 +98,7 @@ namespace ImGui
void InputTextWithError(const char* text, char* buffer, size_t buf_size, bool has_error);
int TextInputWithAutocomplete(const char* hint, char* buffer, size_t buf_size, char** suggestions, int suggestion_count, bool has_error);
+ bool FormFileSelector(char* buffer);
void FormContactAutocomplete(contact* buffer, bool has_error);
void FormInputTextWithErrorHint(const char* hint, char* buffer, size_t buf_size, bool has_error);
void FormCountryCombo(char* buffer, size_t buf_size);
diff --git a/src/administration_reader.cpp b/src/administration_reader.cpp
index bf8f8db..9c20a27 100644
--- a/src/administration_reader.cpp
+++ b/src/administration_reader.cpp
@@ -144,7 +144,8 @@ bool administration_reader_import_invoice(char* buffer, size_t buffer_size)
data.delivered_at = xml_get_date_x(root, "cac:Delivery", "cbc:ActualDeliveryDate", 0);
// References
- xml_get_str_x(root, data.document, MAX_LEN_PATH, "cac:AdditionalDocumentReference", "cbc:ID", 0);
+ xml_get_str_x(root, data.document.copy_path, MAX_LEN_PATH, "cac:AdditionalDocumentReference", "cbc:ID", 0);
+ xml_get_str_x(root, data.document.original_path, MAX_LEN_PATH, "cac:AdditionalDocumentReference", "cbc:DocumentDescription", 0);
xml_get_str_x(root, data.project_id, MAX_LEN_ID, "cac:ProjectReference", "cbc:ID", 0);
xml_get_str(root, data.cost_center_id, MAX_LEN_ID, "cac:AccountingCost");
diff --git a/src/administration_writer.cpp b/src/administration_writer.cpp
index b08723b..0cc4cd8 100644
--- a/src/administration_writer.cpp
+++ b/src/administration_writer.cpp
@@ -286,6 +286,49 @@ void _remove_empty_xml_tags(char* file_content, int depth)
}
}
+static const char* _get_file_extension(const char *path) {
+ const char *dot = strrchr(path, '.');
+ if (!dot || dot == path) return "";
+ return dot;
+}
+
+static void _add_document_to_zip(invoice* inv)
+{
+ document* doc = &inv->document;
+
+ if (strlen(doc->copy_path) == 0 && strlen(doc->original_path) != 0)
+ {
+ char copy_path[MAX_LEN_PATH];
+ snprintf(copy_path, MAX_LEN_PATH, "documents/%s%s", inv->sequential_number, _get_file_extension(doc->original_path));
+
+ FILE* orig_file = fopen(doc->original_path, "rb");
+ if (orig_file == NULL) {
+ log_error("ERROR: original document file path does not exist.");
+ return;
+ }
+
+ fseek(orig_file, 0L, SEEK_END);
+ long sz = ftell(orig_file);
+ fseek(orig_file, 0, SEEK_SET);
+
+ char* file_copy = (char*)malloc(sz);
+ fread(file_copy, sz, 1, orig_file);
+ file_copy[sz-1] = 0;
+
+ fclose(orig_file);
+
+ if (administration_writer_write_to_zip(copy_path, file_copy, sz)) {
+ strops_copy(doc->copy_path, copy_path, MAX_LEN_PATH);
+ log_info("Made copy of '%s' to '%s'.", doc->original_path, doc->copy_path);
+ }
+ else {
+ log_error("ERROR: failed to make copy of original document '%s'.", doc->original_path);
+ }
+
+ free(file_copy);
+ }
+}
+
bool administration_writer_save_invoice_blocking(invoice inv)
{
STOPWATCH_START;
@@ -299,21 +342,15 @@ 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 supplier/customer/addressee:
- // - type
- // These can all be retrieved from existing contacts.
-
- // properties not stored from billing item:
- // - discount (can be recalculated from line_amount - (quantity * unit_price) )
- // - tax (can be recalculated)
- // - total (can be recalculated)
+ _add_document_to_zip(&inv);
strops_replace(file_content, buf_length, "{{INVOICE_ID}}", inv.id);
strops_replace(file_content, buf_length, "{{INVOICE_SEQUENCE_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_ID}}", inv.cost_center_id);
- strops_replace(file_content, buf_length, "{{INVOICE_DOCUMENT}}", inv.document);
+ strops_replace(file_content, buf_length, "{{INVOICE_DOCUMENT_COPY}}", inv.document.copy_path);
+ strops_replace(file_content, buf_length, "{{INVOICE_DOCUMENT_ORIG}}", inv.document.original_path);
strops_replace_int32(file_content, buf_length, "{{INVOICE_STATUS}}", (s32)inv.status);
// Supplier data
@@ -524,9 +561,7 @@ bool administration_writer_save_project_blocking(project project)
struct tm *tm_info = 0;
char date_buffer[11]; // "YYYY-MM-DD" + null terminator
-
-
strops_replace(file_content, buf_length, "{{PROJECT_ID}}", project.id);
strops_replace(file_content, buf_length, "{{PROJECT_DESCRIPTION}}", project.description);
strops_replace_int32(file_content, buf_length, "{{PROJECT_STATE}}", project.state);
diff --git a/src/ui/imgui_extensions.cpp b/src/ui/imgui_extensions.cpp
index 8485dbf..483f5f6 100644
--- a/src/ui/imgui_extensions.cpp
+++ b/src/ui/imgui_extensions.cpp
@@ -6,6 +6,7 @@
#include "config.hpp"
#include "locales.hpp"
#include "administration.hpp"
+#include "tinyfiledialogs.h"
namespace ImGui
{
@@ -42,6 +43,47 @@ namespace ImGui
}
}
+ bool FormFileSelector(char* buffer)
+ {
+ bool result = false;
+ float widthAvailable = ImGui::GetContentRegionAvail().x;
+ ImGui::SetNextItemWidth(widthAvailable*0.5f);
+
+ if (ImGui::Button("Select file..."))
+ {
+ // You can adjust filters, title, default path
+ const char *filterPatterns[] = { "*.png", "*.jpg", "*.pdf", "*" };
+ const char *file = tinyfd_openFileDialog(
+ "Choose a file", // dialog title
+ NULL, // default path
+ 4, // number of filter patterns
+ filterPatterns, // filter patterns array
+ NULL, // single filter description (can be NULL)
+ 0); // allowMultiple (0 = single)
+ if (file)
+ {
+ strops_copy(buffer, file, MAX_LEN_PATH);
+ buffer[MAX_LEN_PATH-1] = '\0';
+ result = true;
+ }
+ }
+
+ if (buffer[0] != '\0')
+ {
+ ImGui::SameLine();
+ if (ImGui::Button("Clear"))
+ {
+ buffer[0] = '\0';
+ result = true;
+ }
+ ImGui::SameLine();
+ ImGui::TextWrapped("Selected: %s", buffer);
+
+ }
+
+ return result;
+ }
+
void FormCountryCombo(char* buffer, size_t buf_size)
{
const char* selected_country = 0;
diff --git a/src/ui/ui_expenses.cpp b/src/ui/ui_expenses.cpp
index 9094d30..92f9c7c 100644
--- a/src/ui/ui_expenses.cpp
+++ b/src/ui/ui_expenses.cpp
@@ -85,6 +85,12 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false)
ImGui::Separator();
+ if (ImGui::FormFileSelector(buffer->document.original_path)) {
+ buffer->document.copy_path[0] = 0;
+ }
+
+ ImGui::Separator();
+
ImGui::Text(localize("invoice.form.supplier"));
draw_contact_form_ex(&buffer->supplier, false, true);
diff --git a/src/ui/ui_invoices.cpp b/src/ui/ui_invoices.cpp
index 0ef3fb2..fdeafc6 100644
--- a/src/ui/ui_invoices.cpp
+++ b/src/ui/ui_invoices.cpp
@@ -206,6 +206,12 @@ static void draw_invoice_form(invoice* buffer, bool viewing_only = false)
ImGui::Separator();
+ if (ImGui::FormFileSelector(buffer->document.original_path)) {
+ buffer->document.copy_path[0] = 0;
+ }
+
+ ImGui::Separator();
+
ImGui::Text(localize("invoice.form.billinginformation"));
draw_contact_form_ex(&buffer->customer, false, true);