summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/CHANGES.rst10
-rw-r--r--include/administration.hpp5
-rw-r--r--include/administration_reader.hpp3
-rw-r--r--include/file_templates.hpp33
-rw-r--r--libs/xml.c/src/xml.c12
-rw-r--r--libs/xml.c/src/xml.h2
-rw-r--r--src/administration.cpp92
-rw-r--r--src/administration_reader.cpp246
-rw-r--r--src/administration_writer.cpp25
-rw-r--r--tests/administration_rw_tests.cpp119
-rw-r--r--tests/administration_validation_tests.cpp8
-rw-r--r--tests/main.cpp2
-rw-r--r--tests/peppol_write_tests.cpp123
-rw-r--r--tests/test_helper.cpp223
14 files changed, 717 insertions, 186 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst
index 2bb8399..8d07a81 100644
--- a/docs/CHANGES.rst
+++ b/docs/CHANGES.rst
@@ -1,8 +1,16 @@
.. _changes:
TODO:
+- move xml functions out of administration reader into xml.c
+- 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)
-- get rid of customer_id and supplier_id. Just check for existing contact entries when finishing invoice and create new contact if necessary.
+- log load failures
+- 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).
+
+- get rid of customer_id and supplier_id.
+This also means we can get rid of administration_invoice_set_refs which simplifies things alot. Contacts should just be a list of possible contacts
+that the user can select from and create manually if they want. They should not be automatically created.
+
- project start and end date should be stored as YYYY-MM-DD
- Send invoice by email
- create invoice from PDF file
diff --git a/include/administration.hpp b/include/administration.hpp
index e2ff80d..130993f 100644
--- a/include/administration.hpp
+++ b/include/administration.hpp
@@ -270,7 +270,6 @@ typedef struct
bool is_outgoing; // Outgoing or incomming invoice.
payment_information payment_means;
- // Used for forms, not stored on disk. Filled when retrieved.
contact supplier;
contact customer;
} invoice;
@@ -474,6 +473,7 @@ u32 administration_invoice_count();
u32 administration_invoice_get_incomming_count();
u32 administration_invoice_get_outgoing_count();
invoice administration_invoice_create_empty();
+a_err administration_invoice_import(invoice* invoice);
a_err administration_invoice_add(invoice* invoice);
a_err administration_invoice_update(invoice* invoice);
a_err administration_invoice_remove(invoice* invoice);
@@ -486,7 +486,7 @@ char* administration_invoice_get_status_string(invoice* invoice);
u32 administration_invoice_get_partial_list_outgoing(u32 page_index, u32 page_size, invoice* buffer);
u32 administration_invoice_get_partial_list_incomming(u32 page_index, u32 page_size, invoice* buffer);
u32 administration_invoice_get_all(invoice* buffer);
-
+a_err administration_invoice_get_by_id(invoice* buffer, char* id);
u32 administration_invoice_get_tax_rates(invoice* invoice, tax_rate* buffer);
bool administration_invoice_get_subtotal_for_tax_rate(invoice* invoice, tax_rate rate, tax_subtotal* buffer);
@@ -494,6 +494,7 @@ bool administration_invoice_get_subtotal_for_tax_rate(invoice* invoice, tax_r
// =======================
u32 administration_billing_item_count(invoice* invoice);
billing_item administration_billing_item_create_empty();
+a_err administration_billing_item_import_to_invoice(invoice* invoice, billing_item item);
a_err administration_billing_item_add_to_invoice(invoice* invoice, billing_item item);
a_err administration_billing_item_update_in_invoice(invoice* invoice, billing_item item);
a_err administration_billing_item_remove_from_invoice(invoice* invoice, billing_item item);
diff --git a/include/administration_reader.hpp b/include/administration_reader.hpp
index cbd69ba..7765680 100644
--- a/include/administration_reader.hpp
+++ b/include/administration_reader.hpp
@@ -24,4 +24,5 @@ bool administration_reader_import_administration_info(char* buffer, size_t buffe
bool administration_reader_import_tax_rate(char* buffer, size_t buffer_size);
bool administration_reader_import_cost_center(char* buffer, size_t buffer_size);
bool administration_reader_import_project(char* buffer, size_t buffer_size);
-bool administration_reader_import_contact(char* buffer, size_t buffer_size); \ No newline at end of file
+bool administration_reader_import_contact(char* buffer, size_t buffer_size);
+bool administration_reader_import_invoice(char* buffer, size_t buffer_size); \ No newline at end of file
diff --git a/include/file_templates.hpp b/include/file_templates.hpp
index 9064abf..4ed3611 100644
--- a/include/file_templates.hpp
+++ b/include/file_templates.hpp
@@ -94,6 +94,12 @@ const char* peppol_invoice_line_template =
" <cac:Item>\n"
" <cbc:Name>{{ITEM_NAME}}</cbc:Name>\n"
+
+" <cac:AdditionalItemProperty>\n"
+" <cbc:Name>Internal Tax Rate ID</cbc:Name>\n"
+" <cbc:Value>{{LINE_TAX_ID}}</cbc:Value>\n"
+" </cac:AdditionalItemProperty>\n"
+
" <cac:ClassifiedTaxCategory>\n"
" <cbc:ID>{{LINE_TAX_CATEGORY}}</cbc:ID>\n"
" <cbc:Percent>{{LINE_TAX_PERCENT}}</cbc:Percent>\n"
@@ -123,24 +129,28 @@ const char *peppol_invoice_template =
" <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>\n"
" <cbc:DocumentCurrencyCode>{{CURRENCY}}</cbc:DocumentCurrencyCode>\n"
"\n"
+" <cac:DespatchDocumentReference>\n"
+" <cbc:ID>{{INVOICE_STATUS}}</cbc:ID>\n"
+" </cac:DespatchDocumentReference>\n"
+"\n"
" <cac:AdditionalDocumentReference>\n"
" <cbc:ID>{{INVOICE_DOCUMENT}}</cbc:ID>\n"
" </cac:AdditionalDocumentReference>\n"
"\n"
" <cac:OrderReference>\n"
-" <cbc:ID>{{INVOICE_ID}}</cbc:ID>\n"
+" <cbc:ID>{{INVOICE_SEQUENCE_ID}}</cbc:ID>\n"
" </cac:OrderReference>\n"
"\n"
" <cac:ProjectReference>\n"
" <cbc:ID>{{PROJECT_ID}}</cbc:ID>\n"
" </cac:ProjectReference>\n"
-" <cbc:AccountingCost>{{COST_CENTER_CODE}}</cbc:AccountingCost>\n"
+" <cbc:AccountingCost>{{COST_CENTER_ID}}</cbc:AccountingCost>\n"
"\n"
" <cac:AccountingSupplierParty>\n"
" <cac:Party>\n"
" <cbc:EndpointID schemeID=\"{{SUPPLIER_ENDPOINT_SCHEME}}\">{{SUPPLIER_ENDPOINT_ID}}</cbc:EndpointID>\n"
" <cac:PartyIdentification>\n"
-" <cbc:ID schemeID=\"ZZZ\">{{SUPPLIER_ID}}</cbc:ID>\n"
+" <cbc:ID schemeID=\"ZZZ\">{{SUPPLIER_BUSINESS_ID}}</cbc:ID>\n"
" </cac:PartyIdentification>\n"
" <cac:PartyName>\n"
" <cbc:Name>{{SUPPLIER_NAME}}</cbc:Name>\n"
@@ -167,7 +177,7 @@ const char *peppol_invoice_template =
" </cac:PartyLegalEntity>\n"
"\n"
" <cac:Contact>\n"
-" <cbc:Name>{{SUPPLIER_NAME}}</cbc:Name>\n"
+" <cbc:Name>{{SUPPLIER_ID}}</cbc:Name>\n"
" <cbc:Telephone>{{SUPPLIER_PHONE_NUMBER}}</cbc:Telephone>\n"
" <cbc:ElectronicMail>{{SUPPLIER_EMAIL}}</cbc:ElectronicMail>\n"
" </cac:Contact>\n"
@@ -179,7 +189,7 @@ const char *peppol_invoice_template =
" <cac:Party>\n"
" <cbc:EndpointID schemeID=\"{{CUSTOMER_ENDPOINT_SCHEME}}\">{{CUSTOMER_ENDPOINT_ID}}</cbc:EndpointID>\n"
" <cac:PartyIdentification>\n"
-" <cbc:ID schemeID=\"ZZZ\">{{CUSTOMER_ID}}</cbc:ID>\n"
+" <cbc:ID schemeID=\"ZZZ\">{{CUSTOMER_BUSINESS_ID}}</cbc:ID>\n"
" </cac:PartyIdentification>\n"
" <cac:PartyName>\n"
" <cbc:Name>{{CUSTOMER_NAME}}</cbc:Name>\n"
@@ -206,7 +216,7 @@ const char *peppol_invoice_template =
" </cac:PartyLegalEntity>\n"
"\n"
" <cac:Contact>\n"
-" <cbc:Name>{{CUSTOMER_NAME}}</cbc:Name>\n"
+" <cbc:Name>{{CUSTOMER_ID}}</cbc:Name>\n"
" <cbc:Telephone>{{CUSTOMER_PHONE_NUMBER}}</cbc:Telephone>\n"
" <cbc:ElectronicMail>{{CUSTOMER_EMAIL}}</cbc:ElectronicMail>\n"
" </cac:Contact>\n"
@@ -238,14 +248,21 @@ const char *peppol_invoice_template =
" <cac:PaymentMeans>\n"
" <cbc:PaymentMeansCode>{{PAYMENT_TYPE}}</cbc:PaymentMeansCode>\n"
" <cbc:PaymentID>{{INVOICE_ID}}</cbc:PaymentID>\n"
+
" <cac:PayeeFinancialAccount>\n"
-" <cbc:ID>{{SUPPLIER_IBAN}}</cbc:ID>\n"
+" <cbc:ID>{{RECIPIENT_IBAN}}</cbc:ID>\n"
+" <cbc:Name>{{RECIPIENT_NAME}}</cbc:Name>\n"
" <cac:FinancialInstitutionBranch>\n"
" <cac:FinancialInstitution>\n"
-" <cbc:ID>{{SUPPLIER_BIC}}</cbc:ID>\n"
+" <cbc:ID>{{RECIPIENT_BIC}}</cbc:ID>\n"
" </cac:FinancialInstitution>\n"
" </cac:FinancialInstitutionBranch>\n"
" </cac:PayeeFinancialAccount>\n"
+
+" <cac:PayerFinancialAccount>\n"
+" <cbc:ID>{{SENDER_IBAN}}</cbc:ID>\n"
+" </cac:PayerFinancialAccount>\n"
+
" </cac:PaymentMeans>\n"
"\n"
" <cac:TaxTotal>\n"
diff --git a/libs/xml.c/src/xml.c b/libs/xml.c/src/xml.c
index 9f8bf24..2eda83e 100644
--- a/libs/xml.c/src/xml.c
+++ b/libs/xml.c/src/xml.c
@@ -1019,15 +1019,13 @@ struct xml_string* xml_node_attribute_content(struct xml_node* node, size_t attr
/**
* [PUBLIC API]
*/
-struct xml_node* xml_easy_child(struct xml_node* node, uint8_t const* child_name, ...) {
+ struct xml_node* xml_easy_vchild(struct xml_node* node, uint8_t const* child_name, va_list arguments) {
/* Find children, one by one
*/
struct xml_node* current = node;
- va_list arguments;
- va_start(arguments, child_name);
-
+ //va_start(arguments, child_name);
/* Descent to current.child
*/
@@ -1080,6 +1078,12 @@ struct xml_node* xml_easy_child(struct xml_node* node, uint8_t const* child_name
return current;
}
+struct xml_node* xml_easy_child(struct xml_node* node, uint8_t const* child_name, ...) {
+
+ va_list arguments;
+ va_start(arguments, child_name);
+ return xml_easy_vchild(node, child_name, arguments);
+}
/**
diff --git a/libs/xml.c/src/xml.h b/libs/xml.c/src/xml.h
index 688a4be..b5ce81f 100644
--- a/libs/xml.c/src/xml.h
+++ b/libs/xml.c/src/xml.h
@@ -154,7 +154,7 @@ struct xml_string* xml_node_attribute_content(struct xml_node* node, size_t attr
* @warning Last argument must be 0
*/
struct xml_node* xml_easy_child(struct xml_node* node, uint8_t const* child, ...);
-
+struct xml_node* xml_easy_vchild(struct xml_node* node, uint8_t const* child_name, va_list arguments);
/**
diff --git a/src/administration.cpp b/src/administration.cpp
index 690fcd9..a21c70f 100644
--- a/src/administration.cpp
+++ b/src/administration.cpp
@@ -1012,7 +1012,7 @@ a_err administration_contact_get_by_id(contact* buffer, char* id)
contact c = *(contact *)list_iterator_next(&g_administration.contacts);
if (strcmp(c.id, id) == 0) {
- list_iterator_stop(&g_administration.projects);
+ list_iterator_stop(&g_administration.contacts);
*buffer = c;
result = A_ERR_SUCCESS;
}
@@ -1579,8 +1579,10 @@ invoice administration_invoice_create_empty()
snprintf(result.id, sizeof(result.id), "I/%d", administration_create_id());
snprintf(result.sequential_number, sizeof(result.id), "INV%010d", administration_create_sequence_number());
result.issued_at = time(NULL);
- result.delivered_at = time(NULL);
- result.expires_at = time(NULL) + administration_get_default_invoice_expire_duration();
+ 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); // @leak
strops_copy(result.currency, administration_get_default_currency_for_country(g_administration.company_info.address.country_code), MAX_LEN_CURRENCY);
return result;
@@ -1767,12 +1769,20 @@ a_err administration_invoice_update(invoice* inv)
return A_ERR_NOT_FOUND;
}
-a_err administration_invoice_add(invoice* inv)
+a_err administration_invoice_import(invoice* inv)
{
a_err result = administration_invoice_is_valid(inv);
if (result != A_ERR_SUCCESS) return result;
- administration_invoice_set_refs(inv);
+ if (!inv->is_triangulation)
+ {
+ memcpy(&inv->addressee, &inv->customer, sizeof(contact));
+ }
+
+ inv->issued_at -= (inv->issued_at % 86400);
+ inv->delivered_at -= (inv->delivered_at % 86400);
+ inv->expires_at -= (inv->expires_at % 86400);
+ inv->is_outgoing = strcmp(inv->supplier.id, MY_COMPANY_ID) == 0;
invoice copy = administration_invoice_create_copy(inv); // Create copy to make copy of billing item list.
invoice* new_inv = (invoice*)malloc(sizeof(invoice));
@@ -1780,11 +1790,36 @@ a_err administration_invoice_add(invoice* inv)
memcpy(new_inv, &copy, sizeof(invoice));
- new_inv->payment_means.payment_method = PAYMENT_METHOD_STANDING_AGREEMENT;
- strops_copy(new_inv->payment_means.payee_bank_account, inv->customer.bank_account, sizeof(new_inv->payment_means.payee_bank_account));
- strops_copy(new_inv->payment_means.payee_account_name, inv->customer.name, sizeof(new_inv->payment_means.payee_account_name));
- strops_copy(new_inv->payment_means.service_provider_id, "", sizeof(new_inv->payment_means.service_provider_id));
- strops_copy(new_inv->payment_means.payer_bank_account, inv->supplier.bank_account, sizeof(new_inv->payment_means.payer_bank_account));
+ 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;
+
+ administration_invoice_set_refs(inv);
+
+ inv->issued_at -= (inv->issued_at % 86400);
+ inv->delivered_at -= (inv->delivered_at % 86400);
+ inv->expires_at -= (inv->expires_at % 86400);
+ inv->is_outgoing = strcmp(inv->supplier.id, MY_COMPANY_ID) == 0;
+
+ inv->payment_means.payment_method = PAYMENT_METHOD_STANDING_AGREEMENT;
+ strops_copy(inv->payment_means.payee_bank_account, inv->customer.bank_account, sizeof(inv->payment_means.payee_bank_account));
+ strops_copy(inv->payment_means.payee_account_name, inv->customer.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->supplier.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*)malloc(sizeof(invoice));
+ if (!new_inv) return A_ERR_GENERIC;
+
+ memcpy(new_inv, &copy, sizeof(invoice));
if (!list_append(&g_administration.invoices, new_inv)) {
return A_ERR_GENERIC;
@@ -1838,6 +1873,24 @@ 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 (strcmp(c.id, id) == 0) {
+ 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);
@@ -2065,6 +2118,25 @@ a_err administration_billing_item_is_valid(billing_item item)
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*)malloc(sizeof(billing_item));
+ if (!tb) return A_ERR_GENERIC;
+
+ memcpy(tb, &item, sizeof(billing_item));
+
+ administration_recalculate_billing_item_totals(tb);
+ administration_recalculate_invoice_totals(invoice);
+
+ if (!list_append(&invoice->billing_items, tb)) {
+ return A_ERR_GENERIC;
+ }
+
+ 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;
diff --git a/src/administration_reader.cpp b/src/administration_reader.cpp
index 8c6bf89..3ea6316 100644
--- a/src/administration_reader.cpp
+++ b/src/administration_reader.cpp
@@ -14,11 +14,13 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
+#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <zip.h>
#include <xml.h>
+#include <time.h>
#include "log.hpp"
#include "strops.hpp"
@@ -104,6 +106,10 @@ bool administration_reader_open_existing(char* file_path)
{
administration_reader_import_contact(buffer, (size_t)size);
}
+ else if (strops_prefix("I/", name))
+ {
+ administration_reader_import_invoice(buffer, (size_t)size);
+ }
free(buffer);
}
@@ -136,6 +142,64 @@ static s64 _get_xml_s64(xml_node* root, char* child_name)
static s32 _get_xml_s32(xml_node* root, char* child_name)
{
struct xml_node* node = xml_easy_child(root, (uint8_t *)child_name, 0);
+ if (!node) return 0;
+
+ char xml_content[512];
+ memset(xml_content, 0, 512);
+ struct xml_string* str = xml_node_content(node);
+ xml_string_copy(str, (uint8_t *)xml_content, xml_string_length(str));
+
+ char *endptr;
+ long val = strtol(xml_content, &endptr, 10);
+
+ s32 num = (int32_t) val;
+ return num;
+}
+
+static xml_node* _get_xml_node_x(xml_node* root, char* child_name, ...)
+{
+ va_list arguments;
+ va_start(arguments, child_name);
+
+ struct xml_node* node = xml_easy_vchild(root, (const uint8_t *)child_name, arguments);
+ return node;
+}
+
+static char* _get_xml_str_attribute(xml_node* root, char* buffer, size_t bufsize, char* attribute_name, char* child_name, ...)
+{
+ va_list arguments;
+ va_start(arguments, child_name);
+
+ struct xml_node* node = xml_easy_vchild(root, (const uint8_t *)child_name, arguments);
+ if (!node) return 0;
+
+ size_t num_attributes = xml_node_attributes(node);
+ for (int x = 0; x < num_attributes; x++)
+ {
+ struct xml_string* attr_name = xml_node_attribute_name(node, x);
+ size_t b_length = xml_string_length(attr_name);
+ uint8_t* b_buffer = (uint8_t*)alloca((b_length + 1) * sizeof(uint8_t));
+ xml_string_copy(attr_name, b_buffer, b_length);
+ b_buffer[b_length] = 0;
+
+ if (strcmp((char*)b_buffer, attribute_name) == 0) {
+ struct xml_string* attr_content = xml_node_attribute_content(node, x);
+
+ xml_string_copy(attr_content, (uint8_t *)buffer, bufsize);
+ buffer[bufsize-1] = 0;
+ return buffer;
+ }
+ }
+ return 0;
+}
+
+static s32 _get_xml_s32_x(xml_node* root, char* child_name, ...)
+{
+ va_list arguments;
+ va_start(arguments, child_name);
+
+ struct xml_node* node = xml_easy_vchild(root, (const uint8_t *)child_name, arguments);
+ if (!node) return 0;
char xml_content[512];
memset(xml_content, 0, 512);
@@ -152,6 +216,25 @@ static s32 _get_xml_s32(xml_node* root, char* child_name)
static float _get_xml_float(xml_node* root, char* child_name)
{
struct xml_node* node = xml_easy_child(root, (uint8_t *)child_name, 0);
+ if (!node) return 0;
+
+ char xml_content[512];
+ memset(xml_content, 0, 512);
+ struct xml_string* str = xml_node_content(node);
+ xml_string_copy(str, (uint8_t *)xml_content, xml_string_length(str));
+
+ char *endptr;
+ float val = strtof(xml_content, &endptr);
+ return val;
+}
+
+static float _get_xml_float_x(xml_node* root, char* child_name, ...)
+{
+ va_list arguments;
+ va_start(arguments, child_name);
+
+ struct xml_node* node = xml_easy_vchild(root, (const uint8_t *)child_name, arguments);
+ if (!node) return 0;
char xml_content[512];
memset(xml_content, 0, 512);
@@ -166,6 +249,7 @@ static float _get_xml_float(xml_node* root, char* child_name)
static char* _get_xml_str(xml_node* root, char* buffer, size_t bufsize, char* child_name)
{
struct xml_node* node = xml_easy_child(root, (uint8_t *)child_name, 0);
+ if (!node) return 0;
memset(buffer, 0, bufsize);
struct xml_string* str = xml_node_content(node);
@@ -174,6 +258,168 @@ static char* _get_xml_str(xml_node* root, char* buffer, size_t bufsize, char* ch
return buffer;
}
+static char* _get_xml_str_x(xml_node* root, char* buffer, size_t bufsize, char* child_name, ...)
+{
+ va_list arguments;
+ va_start(arguments, child_name);
+
+ struct xml_node* node = xml_easy_vchild(root, (const uint8_t *)child_name, arguments);
+ if (!node) return 0;
+
+ memset(buffer, 0, bufsize);
+ struct xml_string* str = xml_node_content(node);
+ xml_string_copy(str, (uint8_t *)buffer, xml_string_length(str));
+
+ return buffer;
+}
+
+static time_t _get_xml_date_x(xml_node* root, char* child_name, ...)
+{
+ va_list arguments;
+ va_start(arguments, child_name);
+
+ struct xml_node* node = xml_easy_vchild(root, (const uint8_t *)child_name, arguments);
+ if (!node) return 0;
+
+ char date_buffer[11];
+ struct xml_string* str = xml_node_content(node);
+ xml_string_copy(str, (uint8_t *)date_buffer, xml_string_length(str));
+
+ struct tm tm_info = {0};
+ int year, month, day;
+ if (sscanf(date_buffer, "%d-%d-%d", &year, &month, &day) != 3) {
+ return (time_t)-1; // parse failed
+ }
+
+ tm_info.tm_year = year - 1900; // struct tm expects years since 1900
+ tm_info.tm_mon = month - 1; // struct tm months are 0–11
+ tm_info.tm_mday = day;
+ tm_info.tm_hour = 0;
+ tm_info.tm_min = 0;
+ tm_info.tm_sec = 0;
+
+ return mktime(&tm_info) + 86400; // Hack
+}
+
+bool administration_reader_import_invoice(char* buffer, size_t buffer_size)
+{
+ STOPWATCH_START;
+
+ xml_document* document = xml_parse_document((uint8_t *)buffer, buffer_size);
+ if (!document) return false;
+
+ struct xml_node* root = xml_document_root(document);
+
+ invoice data = administration_invoice_create_empty();
+ _get_xml_str(root, data.id, MAX_LEN_ID, "cbc:ID");
+ _get_xml_str_x(root, data.sequential_number, MAX_LEN_ID, "cac:OrderReference", "cbc:ID", 0);
+ _get_xml_str(root, data.currency, MAX_LEN_CURRENCY, "cbc:DocumentCurrencyCode");
+ data.status = (invoice_status)_get_xml_s32_x(root, "cac:DespatchDocumentReference", "cbc:ID", 0);
+
+ // Dates
+ data.issued_at = _get_xml_date_x(root, "cbc:IssueDate", 0);
+ data.expires_at = _get_xml_date_x(root, "cbc:DueDate", 0);
+ data.delivered_at = _get_xml_date_x(root, "cac:Delivery", "cbc:ActualDeliveryDate", 0);
+
+ // References
+ _get_xml_str_x(root, data.document, MAX_LEN_PATH, "cac:AdditionalDocumentReference", "cbc:ID", 0);
+ _get_xml_str_x(root, data.project_id, MAX_LEN_ID, "cac:ProjectReference", "cbc:ID", 0);
+ _get_xml_str(root, data.cost_center_id, MAX_LEN_ID, "cac:AccountingCost");
+
+ // Payment means
+ data.payment_means.payment_method = (payment_method)_get_xml_s32_x(root, "cac:PaymentMeans", "cbc:PaymentMeansCode", 0);
+ _get_xml_str_x(root, data.payment_means.payee_bank_account, MAX_LEN_BANK, "cac:PaymentMeans", "cac:PayeeFinancialAccount", "cbc:ID", 0);
+ _get_xml_str_x(root, data.payment_means.payee_account_name, MAX_LEN_LONG_DESC, "cac:PaymentMeans", "cac:PayeeFinancialAccount", "cbc:Name", 0);
+ _get_xml_str_x(root, data.payment_means.service_provider_id, MAX_LEN_ID, "cac:PaymentMeans", "cac:PayeeFinancialAccount", "cac:FinancialInstitutionBranch", "cac:FinancialInstitution", "cbc:ID", 0);
+ _get_xml_str_x(root, data.payment_means.payer_bank_account, MAX_LEN_BANK, "cac:PaymentMeans", "cac:PayerFinancialAccount", "cbc:ID", 0);
+
+ // Totals
+ data.tax = _get_xml_float_x(root, "cac:TaxTotal", "cbc:TaxAmount", 0);
+ data.total = _get_xml_float_x(root, "cac:LegalMonetaryTotal", "cbc:TaxInclusiveAmount", 0);
+ data.net = _get_xml_float_x(root, "cac:LegalMonetaryTotal", "cbc:TaxExclusiveAmount", 0);
+ data.allowance = data.net - _get_xml_float_x(root, "cac:LegalMonetaryTotal", "cbc:LineExtensionAmount", 0);
+
+ // Supplier
+ _get_xml_str_x(root, data.supplier.id, MAX_LEN_ID, "cac:AccountingSupplierParty", "cac:Party", "cac:Contact", "cbc:Name", 0);
+ strops_copy(data.supplier_id, data.supplier.id, MAX_LEN_ID);
+ strops_copy(data.supplier.bank_account, data.payment_means.payee_bank_account, MAX_LEN_BANK);
+ _get_xml_str_x(root, data.supplier.name, MAX_LEN_LONG_DESC, "cac:AccountingSupplierParty", "cac:Party", "cac:PartyName", "cbc:Name", 0);
+ _get_xml_str_x(root, data.supplier.address.address1, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:StreetName", 0);
+ _get_xml_str_x(root, data.supplier.address.address2, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:AdditionalStreetName", 0);
+ _get_xml_str_x(root, data.supplier.address.city, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:CityName", 0);
+ _get_xml_str_x(root, data.supplier.address.postal, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:PostalZone", 0);
+ _get_xml_str_x(root, data.supplier.address.region, MAX_LEN_ADDRESS, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cbc:CountrySubentity", 0);
+ _get_xml_str_x(root, data.supplier.address.country_code, MAX_LEN_COUNTRY_CODE, "cac:AccountingSupplierParty", "cac:Party", "cac:PostalAddress", "cac:Country", "cbc:IdentificationCode", 0);
+ _get_xml_str_x(root, data.supplier.taxid, MAX_LEN_TAXID, "cac:AccountingSupplierParty", "cac:Party", "cac:PartyTaxScheme", "cbc:CompanyID", 0);
+ _get_xml_str_x(root, data.supplier.businessid, MAX_LEN_BUSINESSID, "cac:AccountingSupplierParty", "cac:Party", "cac:PartyIdentification", "cbc:ID", 0);
+ _get_xml_str_x(root, data.supplier.phone_number, MAX_LEN_PHONE, "cac:AccountingSupplierParty", "cac:Party", "cac:Contact", "cbc:Telephone", 0);
+ _get_xml_str_x(root, data.supplier.email, MAX_LEN_EMAIL, "cac:AccountingSupplierParty", "cac:Party", "cac:Contact", "cbc:ElectronicMail", 0);
+
+ // Customer
+ _get_xml_str_x(root, data.customer.id, MAX_LEN_ID, "cac:AccountingCustomerParty", "cac:Party", "cac:Contact", "cbc:Name", 0);
+ strops_copy(data.customer_id, data.customer.id, MAX_LEN_ID);
+ strops_copy(data.customer.bank_account, data.payment_means.payer_bank_account, MAX_LEN_BANK);
+ _get_xml_str_x(root, data.customer.name, MAX_LEN_LONG_DESC, "cac:AccountingCustomerParty", "cac:Party", "cac:PartyName", "cbc:Name", 0);
+ _get_xml_str_x(root, data.customer.address.address1, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:StreetName", 0);
+ _get_xml_str_x(root, data.customer.address.address2, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:AdditionalStreetName", 0);
+ _get_xml_str_x(root, data.customer.address.city, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:CityName", 0);
+ _get_xml_str_x(root, data.customer.address.postal, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:PostalZone", 0);
+ _get_xml_str_x(root, data.customer.address.region, MAX_LEN_ADDRESS, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cbc:CountrySubentity", 0);
+ _get_xml_str_x(root, data.customer.address.country_code, MAX_LEN_COUNTRY_CODE, "cac:AccountingCustomerParty", "cac:Party", "cac:PostalAddress", "cac:Country", "cbc:IdentificationCode", 0);
+ _get_xml_str_x(root, data.customer.taxid, MAX_LEN_TAXID, "cac:AccountingCustomerParty", "cac:Party", "cac:PartyTaxScheme", "cbc:CompanyID", 0);
+ _get_xml_str_x(root, data.customer.businessid, MAX_LEN_BUSINESSID, "cac:AccountingCustomerParty", "cac:Party", "cac:PartyIdentification", "cbc:ID", 0);
+ _get_xml_str_x(root, data.customer.phone_number, MAX_LEN_PHONE, "cac:AccountingCustomerParty", "cac:Party", "cac:Contact", "cbc:Telephone", 0);
+ _get_xml_str_x(root, data.customer.email, MAX_LEN_EMAIL, "cac:AccountingCustomerParty", "cac:Party", "cac:Contact", "cbc:ElectronicMail", 0);
+
+ // Addressee
+ _get_xml_str_x(root, data.addressee.name, MAX_LEN_LONG_DESC, "cac:Delivery", "cac:DeliveryParty", "cac:PartyName", "cbc:Name", 0);
+ _get_xml_str_x(root, data.addressee.address.address1, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:StreetName", 0);
+ _get_xml_str_x(root, data.addressee.address.address2, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:AdditionalStreetName", 0);
+ _get_xml_str_x(root, data.addressee.address.city, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:CityName", 0);
+ _get_xml_str_x(root, data.addressee.address.postal, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:PostalZone", 0);
+ _get_xml_str_x(root, data.addressee.address.region, MAX_LEN_ADDRESS, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cbc:CountrySubentity", 0);
+ _get_xml_str_x(root, data.addressee.address.country_code, MAX_LEN_COUNTRY_CODE, "cac:Delivery", "cac:DeliveryLocation", "cac:Address", "cac:Country", "cbc:IdentificationCode", 0);
+
+ size_t child_count = xml_node_children(root);
+ for (size_t x = 0; x < child_count; x++)
+ {
+ xml_node* child = xml_node_child(root, x);
+
+ char* child_name = (char*)xml_easy_name(child);
+ if (strcmp(child_name, "cac:InvoiceLine") == 0)
+ {
+
+ billing_item bi = {0};
+ _get_xml_str_x(child, bi.id, MAX_LEN_ID, "cbc:ID", 0);
+ _get_xml_str_x(child, bi.tax_rate_id, MAX_LEN_ID, "cac:Item", "cac:AdditionalItemProperty", "cbc:Value", 0);
+ bi.amount = _get_xml_float_x(child, "cbc:InvoicedQuantity", 0);
+ bi.net_per_item = _get_xml_float_x(child, "cac:Price", "cbc:PriceAmount", 0);
+ bi.net = _get_xml_float_x(child, "cbc:LineExtensionAmount", 0);
+ bi.discount = _get_xml_float_x(child, "cac:AllowanceCharge", "cbc:Amount", 0);
+ _get_xml_str_x(child, bi.description, MAX_LEN_LONG_DESC, "cac:Item", "cbc:Name", 0);
+
+ char percentage_buffer[5] = {0};
+ _get_xml_str_attribute(child, percentage_buffer, 5, "unitCode", "cbc:InvoicedQuantity", 0);
+ bi.amount_is_percentage = strcmp(percentage_buffer, "%") == 0;
+
+ _get_xml_str_attribute(child, bi.currency, 5, "currencyID", "cbc:LineExtensionAmount", 0);
+ bi.discount_is_percentage = _get_xml_node_x(child, "cac:AllowanceCharge", "cbc:MultiplierFactorNumeric", 0) != 0;
+ if (bi.discount_is_percentage) {
+ bi.discount = _get_xml_float_x(child, "cac:AllowanceCharge", "cbc:MultiplierFactorNumeric", 0);
+ }
+
+ administration_billing_item_import_to_invoice(&data, bi);
+ }
+
+ free(child_name);
+ }
+
+ bool result = administration_invoice_import(&data);
+ log_add("Loaded invoice '%s' in %.3fms.", data.sequential_number, STOPWATCH_TIME);
+
+ return result;
+}
+
bool administration_reader_import_contact(char* buffer, size_t buffer_size)
{
STOPWATCH_START;
diff --git a/src/administration_writer.cpp b/src/administration_writer.cpp
index e02d476..75f5daf 100644
--- a/src/administration_writer.cpp
+++ b/src/administration_writer.cpp
@@ -298,13 +298,8 @@ 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:
@@ -312,11 +307,13 @@ bool administration_writer_save_invoice_blocking(invoice inv)
// - 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, "{{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_CODE}}", inv.cost_center_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_int32(file_content, buf_length, "{{INVOICE_STATUS}}", (s32)inv.status);
// Supplier data
strops_replace(file_content, buf_length, "{{SUPPLIER_ENDPOINT_SCHEME}}", administration_writer_get_eas_scheme_for_contact(inv.supplier));
@@ -366,9 +363,11 @@ bool administration_writer_save_invoice_blocking(invoice inv)
// Payment means
strops_replace_int32(file_content, buf_length, "{{PAYMENT_TYPE}}", inv.payment_means.payment_method);
- strops_replace(file_content, buf_length, "{{SUPPLIER_IBAN}}", inv.payment_means.payee_bank_account);
- strops_replace(file_content, buf_length, "{{SUPPLIER_BIC}}", inv.payment_means.service_provider_id);
-
+ strops_replace(file_content, buf_length, "{{RECIPIENT_IBAN}}", inv.payment_means.payee_bank_account);
+ strops_replace(file_content, buf_length, "{{RECIPIENT_NAME}}", inv.payment_means.payee_account_name);
+ strops_replace(file_content, buf_length, "{{RECIPIENT_BIC}}", inv.payment_means.service_provider_id);
+ strops_replace(file_content, buf_length, "{{SENDER_IBAN}}", inv.payment_means.payer_bank_account);
+
// Tax breakdown
strops_replace_float(file_content, buf_length, "{{TOTAL_TAX_AMOUNT}}", inv.tax, 2);
{ // Create tax subtotal list.
@@ -431,9 +430,10 @@ bool administration_writer_save_invoice_blocking(invoice inv)
billing_item bi = billing_item_buffer[i];
tax_rate rate;
administration_tax_rate_get_by_id(&rate, bi.tax_rate_id);
-
+
strops_replace(billing_item_file_content, billing_item_buf_length, "{{CURRENCY}}", bi.currency);
strops_replace(billing_item_file_content, billing_item_buf_length, "{{LINE_ID}}", bi.id);
+ strops_replace(billing_item_file_content, billing_item_buf_length, "{{LINE_TAX_ID}}", bi.tax_rate_id);
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}}", rate.category_code);
strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{LINE_TAX_PERCENT}}", rate.rate, 2);
@@ -441,10 +441,9 @@ bool administration_writer_save_invoice_blocking(invoice inv)
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); // 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_rate_id);
if (bi.discount_is_percentage) {
- strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", (bi.allowance / (bi.net + bi.allowance)) * 100.0f, 2);
+ strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_TOTAL_PERCENTAGE}}", bi.discount, 2);
strops_replace_float(billing_item_file_content, billing_item_buf_length, "{{DISCOUNT_BASE_AMOUNT}}", bi.net + bi.allowance, 2); // Total net before discount.
}
else {
diff --git a/tests/administration_rw_tests.cpp b/tests/administration_rw_tests.cpp
index ba5507e..b09fd9e 100644
--- a/tests/administration_rw_tests.cpp
+++ b/tests/administration_rw_tests.cpp
@@ -1,16 +1,3 @@
-#define _CRT_SECURE_NO_WARNINGS
-
-#include <stdio.h>
-#include <stdlib.h>
-
-#include "greatest.h"
-#include "timer.h"
-
-#include "strops.hpp"
-#include "administration.hpp"
-#include "administration_reader.hpp"
-#include "administration_writer.hpp"
-
#ifdef _WIN32
#include <io.h> // for _unlink
#define unlink _unlink
@@ -18,8 +5,6 @@
#include <unistd.h> // for unlink
#endif
-char* test_file_path = "build\\test.openbook";
-
static void setup_cb(void *data) {
(void)data;
unlink(test_file_path);
@@ -174,6 +159,109 @@ TEST _administration_rw_info(void)
PASS();
}
+TEST _administration_rw_invoice(void)
+{
+ u32 count;
+ invoice inv;
+ invoice invr;
+
+ administration_writer_create();
+
+ administration_create_default(test_file_path);
+ {
+ contact pw = administration_contact_create_empty();
+ strops_copy(pw.name, "Piet Pinda", sizeof(pw.name));
+ strops_copy(pw.address.address1, "Address 1", sizeof(pw.address.address1));
+ strops_copy(pw.address.country_code, "NL", sizeof(pw.address.country_code));
+ strops_copy(pw.address.city, "Amsterdam", sizeof(pw.address.city));
+ strops_copy(pw.address.postal, "1234AA", sizeof(pw.address.postal));
+ strops_copy(pw.taxid, "T123", sizeof(pw.taxid));
+ strops_copy(pw.businessid, "B321", sizeof(pw.businessid));
+ strops_copy(pw.email, "test@test.com", sizeof(pw.email));
+ pw.type = contact_type::CONTACT_BUSINESS;
+
+ inv = administration_invoice_create_empty();
+ inv.supplier = pw;
+ inv.customer = pw;
+ inv.is_outgoing = 1;
+ inv.status = invoice_status::INVOICE_CONCEPT;
+ inv.issued_at = time(NULL);
+ inv.delivered_at = inv.issued_at;
+ inv.expires_at = inv.issued_at + 1000;
+
+ administration_billing_item_add_to_invoice(&inv, _create_bi3(&inv));
+ administration_billing_item_add_to_invoice(&inv, _create_bi4(&inv));
+ administration_billing_item_add_to_invoice(&inv, _create_bi5(&inv));
+ administration_billing_item_add_to_invoice(&inv, _create_bi6(&inv));
+
+ count = administration_invoice_count();
+ ASSERT_EQ(administration_invoice_add(&inv), A_ERR_SUCCESS);
+ ASSERT_EQ(count+1, administration_invoice_count());
+ }
+
+ administration_reader_open_existing(test_file_path);
+ {
+ ASSERT_EQ(count+1, administration_invoice_count());
+ ASSERT_EQ(A_ERR_SUCCESS, administration_invoice_get_by_id(&invr, inv.id));
+
+ ASSERT_STR_EQ(invr.id, inv.id);
+ ASSERT_STR_EQ(invr.sequential_number, inv.sequential_number);
+ ASSERT_STR_EQ(invr.customer_id, inv.customer_id);
+ ASSERT_STR_EQ(invr.supplier_id, inv.supplier_id);
+
+ ASSERT_EQ(invr.issued_at, inv.issued_at);
+ ASSERT_EQ(invr.expires_at, inv.expires_at);
+ ASSERT_EQ(invr.delivered_at, inv.delivered_at);
+ ASSERT_STR_EQ(invr.document, inv.document);
+ ASSERT_STR_EQ(invr.project_id, inv.project_id);
+ ASSERT_STR_EQ(invr.cost_center_id, inv.cost_center_id);
+
+ ASSERT_EQ(administration_billing_item_count(&inv), administration_billing_item_count(&invr));
+
+ int num_items = administration_billing_item_count(&inv);
+ for (int i = 0; i < num_items; i++)
+ {
+ billing_item* b1 = (billing_item*)list_get_at(&inv.billing_items, i);
+ billing_item* b2 = (billing_item*)list_get_at(&invr.billing_items, i);
+
+ ASSERT_STR_EQ(b1->id, b2->id);
+ ASSERT_EQ(b1->amount, b2->amount);
+ ASSERT_EQ(b1->amount_is_percentage, b2->amount_is_percentage);
+ ASSERT_STR_EQ(b1->description, b2->description);
+ ASSERT_EQ(b1->net_per_item, b2->net_per_item);
+ ASSERT_EQ(b1->discount, b2->discount);
+ ASSERT_EQ(b1->discount_is_percentage, b2->discount_is_percentage);
+ ASSERT_EQ(b1->allowance, b2->allowance);
+ ASSERT_EQ(b1->net, b2->net);
+ ASSERT_STR_EQ(b1->currency, b2->currency);
+ ASSERT_STR_EQ(b1->tax_rate_id, b2->tax_rate_id);
+ ASSERT_EQ(b1->tax, b2->tax);
+ ASSERT_EQ(b1->total, b2->total);
+ }
+
+ ASSERT_EQ(invr.orig_total, inv.orig_total);
+ ASSERT_EQ(invr.orig_tax, inv.orig_tax);
+ ASSERT_EQ(invr.orig_net, inv.orig_net);
+ ASSERT_EQ(invr.orig_allowance, inv.orig_allowance);
+
+ ASSERT_EQ(invr.total, inv.total);
+ ASSERT_EQ(invr.tax, inv.tax);
+ ASSERT_EQ(invr.net, inv.net);
+ ASSERT_EQ(invr.allowance, inv.allowance);
+
+ ASSERT_STR_EQ(invr.currency, inv.currency);
+ ASSERT_EQ(invr.is_triangulation, inv.is_triangulation);
+ ASSERT_EQ(invr.status, inv.status);
+ ASSERT_EQ(invr.is_outgoing, inv.is_outgoing);
+ ASSERT_MEM_EQ(&invr.payment_means, &inv.payment_means, sizeof(payment_information));
+ ASSERT_MEM_EQ(&invr.supplier, &inv.supplier, sizeof(contact));
+ ASSERT_MEM_EQ(&invr.customer, &inv.customer, sizeof(contact));
+ ASSERT_MEM_EQ(&invr.addressee, &inv.addressee, sizeof(contact));
+ }
+
+ PASS();
+}
+
SUITE(administration_rw) {
SET_SETUP(setup_cb, NULL);
SET_TEARDOWN(teardown_cb, NULL);
@@ -183,4 +271,5 @@ SUITE(administration_rw) {
RUN_TEST(_administration_rw_project);
RUN_TEST(_administration_rw_contact);
RUN_TEST(_administration_rw_info);
+ RUN_TEST(_administration_rw_invoice);
} \ No newline at end of file
diff --git a/tests/administration_validation_tests.cpp b/tests/administration_validation_tests.cpp
index f307d2d..f406efd 100644
--- a/tests/administration_validation_tests.cpp
+++ b/tests/administration_validation_tests.cpp
@@ -1,11 +1,3 @@
-#include <stdio.h>
-#include <stdlib.h>
-
-#include "greatest.h"
-#include "timer.h"
-
-#include "strops.hpp"
-#include "administration.hpp"
// Project
//////////////////
diff --git a/tests/main.cpp b/tests/main.cpp
index 234c92d..358ca2e 100644
--- a/tests/main.cpp
+++ b/tests/main.cpp
@@ -1,3 +1,5 @@
+#include "test_helper.cpp"
+
#include "administration_rw_tests.cpp"
#include "administration_validation_tests.cpp"
#include "peppol_write_tests.cpp"
diff --git a/tests/peppol_write_tests.cpp b/tests/peppol_write_tests.cpp
index c00b53b..fb74ed7 100644
--- a/tests/peppol_write_tests.cpp
+++ b/tests/peppol_write_tests.cpp
@@ -1,126 +1,3 @@
-#include <malloc.h>
-
-#include "zip.h"
-#include "xml.h"
-
-char* validation_file = "libs\\PEPPOL-EN16931-UBL.sch";
-char* result_file = "build\\invoice_report.xml";
-char* extract_dir = "build\\extracted";
-
-static contact _create_nl_business()
-{
- contact pw = administration_contact_create_empty();
- strops_copy(pw.name, "Piet Pinda", sizeof(pw.name));
- strops_copy(pw.address.address1, "Address 1", sizeof(pw.address.address1));
- strops_copy(pw.address.country_code, "NL", sizeof(pw.address.country_code));
- strops_copy(pw.address.city, "Amsterdam", sizeof(pw.address.city));
- strops_copy(pw.address.postal, "1234AA", sizeof(pw.address.postal));
- strops_copy(pw.taxid, "T123", sizeof(pw.taxid));
- strops_copy(pw.businessid, "B321", sizeof(pw.businessid));
- strops_copy(pw.email, "test@test.com", sizeof(pw.email));
- pw.type = contact_type::CONTACT_BUSINESS;
- return pw;
-}
-
-static contact _create_nl_consumer()
-{
- contact pw = _create_nl_business();
- pw.type = contact_type::CONTACT_CONSUMER;
- return pw;
-}
-
-static billing_item _create_bi1(invoice *inv)
-{
- billing_item item = administration_billing_item_create_empty();
- item.amount = 1;
- item.amount_is_percentage = 0;
- strops_copy(item.description, "Shampoo", MAX_LEN_LONG_DESC);
- item.net_per_item = 10.00f;
- item.discount = 2.0f;
- item.discount_is_percentage = 0;
-
- tax_rate buffer[20];
- char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
- u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
- tax_rate rand_rate = buffer[tax_rate_count-1];
- strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
-
- return item;
-}
-
-static billing_item _create_bi2(invoice *inv)
-{
- billing_item item = administration_billing_item_create_empty();
- item.amount = 2;
- item.amount_is_percentage = 0;
- strops_copy(item.description, "Soap", MAX_LEN_LONG_DESC);
- item.net_per_item = 5.00f;
- item.discount = 5.0f;
- item.discount_is_percentage = 1;
-
- tax_rate buffer[20];
- char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
- u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
- tax_rate rand_rate = buffer[tax_rate_count-1];
- strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
-
- return item;
-}
-
-static bool _test_peppol_file(char* id)
-{
- bool result = 1;
-
- zip_extract(test_file_path, extract_dir, 0, 0);
-
- char command[200];
- snprintf(command, 200, "java -jar libs\\schxslt-cli.jar -d %s\\%s.xml -s %s -o %s > NUL 2>&1",
- extract_dir, id, validation_file, result_file);
- system(command);
-
- FILE* file = fopen(result_file, "r");
- struct xml_document* document = xml_open_document(file);
-
- struct xml_node* root = xml_document_root(document);
-
- size_t num_children = xml_node_children(root);
- for (int i = 0; i < num_children; i++)
- {
- struct xml_node* node = xml_node_child(root, i);
-
- char* name = (char*)xml_easy_name(node);
-
- if (strcmp(name, "svrl:failed-assert") == 0) {
- struct xml_node* text_node = xml_easy_child(node, (uint8_t *)"svrl:text", 0);
- char* content = (char*)xml_easy_content(text_node);
-
- size_t num_attributes = xml_node_attributes(node);
- for (int x = 0; x < num_attributes; x++)
- {
- struct xml_string* attr_name = xml_node_attribute_name(node, x);
- size_t b_length = xml_string_length(attr_name);
- uint8_t* b_buffer = (uint8_t*)alloca((b_length + 1) * sizeof(uint8_t));
- xml_string_copy(attr_name, b_buffer, b_length);
- b_buffer[b_length] = 0;
-
- if (strcmp((char*)b_buffer, "location") == 0) {
- struct xml_string* attr_content = xml_node_attribute_content(node, x);
-
- size_t a_length = xml_string_length(attr_content);
- uint8_t* a_buffer = (uint8_t*)alloca((a_length + 1) * sizeof(uint8_t));
- xml_string_copy(attr_content, a_buffer, a_length);
- a_buffer[a_length] = 0;
-
- printf("FAILURE: %s @ %s\n", content, (char*)a_buffer);
- }
- }
-
- result = 0;
- }
- }
-
- return result;
-}
//////////////////
// Netherlands
diff --git a/tests/test_helper.cpp b/tests/test_helper.cpp
new file mode 100644
index 0000000..aedaccc
--- /dev/null
+++ b/tests/test_helper.cpp
@@ -0,0 +1,223 @@
+#define _CRT_SECURE_NO_WARNINGS
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <malloc.h>
+
+#include "zip.h"
+#include "xml.h"
+#include "greatest.h"
+#include "timer.h"
+
+#include "strops.hpp"
+#include "administration.hpp"
+#include "administration_reader.hpp"
+#include "administration_writer.hpp"
+
+char* test_file_path = "build\\test.openbook";
+char* validation_file = "libs\\PEPPOL-EN16931-UBL.sch";
+char* result_file = "build\\invoice_report.xml";
+char* extract_dir = "build\\extracted";
+
+static contact _create_nl_business()
+{
+ contact pw = administration_contact_create_empty();
+ strops_copy(pw.name, "Dutch Company BV", sizeof(pw.name));
+ strops_copy(pw.address.address1, "Address 1", sizeof(pw.address.address1));
+ strops_copy(pw.address.country_code, "NL", sizeof(pw.address.country_code));
+ strops_copy(pw.address.city, "Amsterdam", sizeof(pw.address.city));
+ strops_copy(pw.address.region, "Noord-Holland", sizeof(pw.address.region));
+ strops_copy(pw.address.postal, "1234AA", sizeof(pw.address.postal));
+ strops_copy(pw.taxid, "T123", sizeof(pw.taxid));
+ strops_copy(pw.businessid, "B321", sizeof(pw.businessid));
+ strops_copy(pw.email, "test@test.com", sizeof(pw.email));
+ strops_copy(pw.phone_number, "+311234567", sizeof(pw.phone_number));
+ strops_copy(pw.bank_account, "IBAN123456789", sizeof(pw.bank_account));
+ pw.type = contact_type::CONTACT_BUSINESS;
+ return pw;
+}
+
+static contact _create_nl_consumer()
+{
+ contact pw = administration_contact_create_empty();
+ strops_copy(pw.name, "Piet Pinda", sizeof(pw.name));
+ strops_copy(pw.address.address1, "Address 2", sizeof(pw.address.address1));
+ strops_copy(pw.address.country_code, "NL", sizeof(pw.address.country_code));
+ strops_copy(pw.address.city, "Maastricht", sizeof(pw.address.city));
+ strops_copy(pw.address.region, "Limburg", sizeof(pw.address.region));
+ strops_copy(pw.address.postal, "4321AA", sizeof(pw.address.postal));
+ strops_copy(pw.email, "test@test.com", sizeof(pw.email));
+ strops_copy(pw.phone_number, "+317654321", sizeof(pw.phone_number));
+ pw.type = contact_type::CONTACT_CONSUMER;
+ return pw;
+}
+
+
+static billing_item _create_bi1(invoice *inv)
+{
+ billing_item item = administration_billing_item_create_empty();
+ item.amount = 1;
+ item.amount_is_percentage = 0;
+ strops_copy(item.description, "Shampoo", MAX_LEN_LONG_DESC);
+ item.net_per_item = 10.00f;
+ item.discount = 2.0f;
+ item.discount_is_percentage = 0;
+
+ tax_rate buffer[20];
+ char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
+ u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
+ tax_rate rand_rate = buffer[tax_rate_count-1];
+ strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
+
+ return item;
+}
+
+static billing_item _create_bi2(invoice *inv)
+{
+ billing_item item = administration_billing_item_create_empty();
+ item.amount = 2;
+ item.amount_is_percentage = 0;
+ strops_copy(item.description, "Soap", MAX_LEN_LONG_DESC);
+ item.net_per_item = 5.00f;
+ item.discount = 5.0f;
+ item.discount_is_percentage = 1;
+
+ tax_rate buffer[20];
+ char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
+ u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
+ tax_rate rand_rate = buffer[tax_rate_count-1];
+ strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
+
+ return item;
+}
+
+static billing_item _create_bi3(invoice *inv)
+{
+ billing_item item = administration_billing_item_create_empty();
+ item.amount = 1;
+ item.amount_is_percentage = 0;
+ strops_copy(item.description, "Guacamole", MAX_LEN_LONG_DESC);
+ item.net_per_item = 10.00f;
+ item.discount = 2.0f;
+ item.discount_is_percentage = 0;
+
+ tax_rate buffer[20];
+ char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
+ u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
+ tax_rate rand_rate = buffer[tax_rate_count-1];
+ strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
+
+ return item;
+}
+
+static billing_item _create_bi4(invoice *inv)
+{
+ billing_item item = administration_billing_item_create_empty();
+ item.amount = 3;
+ item.amount_is_percentage = 0;
+ strops_copy(item.description, "Bananas", MAX_LEN_LONG_DESC);
+ item.net_per_item = 4.00f;
+ item.discount = 0.0f;
+ item.discount_is_percentage = 0;
+
+ tax_rate buffer[20];
+ char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
+ u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
+ tax_rate rand_rate = buffer[tax_rate_count-1];
+ strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
+
+ return item;
+}
+
+static billing_item _create_bi5(invoice *inv)
+{
+ billing_item item = administration_billing_item_create_empty();
+ item.amount = 5;
+ item.amount_is_percentage = 0;
+ strops_copy(item.description, "Apple", MAX_LEN_LONG_DESC);
+ item.net_per_item = 1.00f;
+ item.discount = 5.0f;
+ item.discount_is_percentage = 1;
+
+ tax_rate buffer[20];
+ char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
+ u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
+ tax_rate rand_rate = buffer[tax_rate_count-1];
+ strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
+
+ return item;
+}
+
+static billing_item _create_bi6(invoice *inv)
+{
+ billing_item item = administration_billing_item_create_empty();
+ item.amount = 10;
+ item.amount_is_percentage = 1;
+ strops_copy(item.description, "Tip", MAX_LEN_LONG_DESC);
+ item.net_per_item = 50.00f;
+ item.discount = 0.0f;
+ item.discount_is_percentage = 0;
+
+ tax_rate buffer[20];
+ char* _country_codes[2] = {inv->supplier.address.country_code, inv->customer.address.country_code};
+ u32 tax_rate_count = administration_tax_rate_get_by_country(buffer, 2, _country_codes);
+ tax_rate rand_rate = buffer[tax_rate_count-1];
+ strops_copy(item.tax_rate_id, rand_rate.id, MAX_LEN_ID);
+
+ return item;
+}
+
+static bool _test_peppol_file(char* id)
+{
+ bool result = 1;
+
+ zip_extract(test_file_path, extract_dir, 0, 0);
+
+ char command[200];
+ snprintf(command, 200, "java -jar libs\\schxslt-cli.jar -d %s\\%s.xml -s %s -o %s > NUL 2>&1",
+ extract_dir, id, validation_file, result_file);
+ system(command);
+
+ FILE* file = fopen(result_file, "r");
+ struct xml_document* document = xml_open_document(file);
+
+ struct xml_node* root = xml_document_root(document);
+
+ size_t num_children = xml_node_children(root);
+ for (int i = 0; i < num_children; i++)
+ {
+ struct xml_node* node = xml_node_child(root, i);
+
+ char* name = (char*)xml_easy_name(node);
+
+ if (strcmp(name, "svrl:failed-assert") == 0) {
+ struct xml_node* text_node = xml_easy_child(node, (uint8_t *)"svrl:text", 0);
+ char* content = (char*)xml_easy_content(text_node);
+
+ size_t num_attributes = xml_node_attributes(node);
+ for (int x = 0; x < num_attributes; x++)
+ {
+ struct xml_string* attr_name = xml_node_attribute_name(node, x);
+ size_t b_length = xml_string_length(attr_name);
+ uint8_t* b_buffer = (uint8_t*)alloca((b_length + 1) * sizeof(uint8_t));
+ xml_string_copy(attr_name, b_buffer, b_length);
+ b_buffer[b_length] = 0;
+
+ if (strcmp((char*)b_buffer, "location") == 0) {
+ struct xml_string* attr_content = xml_node_attribute_content(node, x);
+
+ size_t a_length = xml_string_length(attr_content);
+ uint8_t* a_buffer = (uint8_t*)alloca((a_length + 1) * sizeof(uint8_t));
+ xml_string_copy(attr_content, a_buffer, a_length);
+ a_buffer[a_length] = 0;
+
+ printf("FAILURE: %s @ %s\n", content, (char*)a_buffer);
+ }
+ }
+
+ result = 0;
+ }
+ }
+
+ return result;
+} \ No newline at end of file