diff options
| -rw-r--r-- | docs/CHANGES.rst | 3 | ||||
| -rw-r--r-- | docs/README.rst | 5 | ||||
| -rw-r--r-- | include/administration.hpp | 4 | ||||
| -rw-r--r-- | include/file_templates.hpp | 119 | ||||
| -rw-r--r-- | include/strops.hpp | 5 | ||||
| -rw-r--r-- | src/administration.cpp | 117 | ||||
| -rw-r--r-- | src/administration_writer.cpp | 152 | ||||
| -rw-r--r-- | src/locales/en.cpp | 3 | ||||
| -rw-r--r-- | src/strops.cpp | 25 | ||||
| -rw-r--r-- | src/ui/ui_contacts.cpp | 15 |
10 files changed, 397 insertions, 51 deletions
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index c909fba..2397853 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -1,5 +1,6 @@ .. _changes: +TODO: - Send invoice by email - create invoice from PDF file - create invoice from image of receipt @@ -14,6 +15,8 @@ - Make sure invoices/expenses cannot be added when company info is empty/invalid. - net negative billing items should not also have discounts. - net negative billing items should set tax to 0. +- validate data within administration on save to make sure it is valid for transmissions. (e.g. rules of https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-AccountingSupplierParty/cac-Party/cbc-EndpointID/) +- View local business number / vat number naming on contact form. e.g. when country is austria: "Österreichische Umsatzsteuer-Identifikationsnummer" v0.1 (master) diff --git a/docs/README.rst b/docs/README.rst index ceeb6f0..a432e92 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -21,6 +21,7 @@ What OpenBooks **can** do: - 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. +- OpenBooks stores all incomming and outgoing invoices in Peppol BIS Billing 3.0 format for archiving. What OpenBooks **can't** do: @@ -31,9 +32,9 @@ What OpenBooks **can't** do: 2. Supported countries ---------------------- -This section lists all country specific implementations to be compliant. If the country you operate from is not listed below, some reporting requirements might not be handled by OpenBooks. +This section lists all supported countries. If the country you operate from is not listed below, some reporting requirements might not be handled by OpenBooks. -- **The Netherlands**: Quarterly tax reports, Archiving (Peppol BIS 3.0). +- **The Netherlands**: Quarterly tax reports, Peppol BIS 3.0 Billing. 3. Dependencies --------------- diff --git a/include/administration.hpp b/include/administration.hpp index e9ca22f..774faf8 100644 --- a/include/administration.hpp +++ b/include/administration.hpp @@ -41,6 +41,9 @@ typedef struct { char address1[MAX_LEN_ADDRESS]; char address2[MAX_LEN_ADDRESS]; + char city[MAX_LEN_ADDRESS]; + char postal[MAX_LEN_ADDRESS]; + char region[MAX_LEN_ADDRESS]; char country_code[MAX_LEN_COUNTRY_CODE]; // 2 letter country code } address; @@ -248,6 +251,7 @@ bool administration_invoice_is_valid(invoice* invoice); 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); // Billing item functions. // ======================= diff --git a/include/file_templates.hpp b/include/file_templates.hpp new file mode 100644 index 0000000..f267055 --- /dev/null +++ b/include/file_templates.hpp @@ -0,0 +1,119 @@ +const char *peppol_invoice_template = +/*"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"*/ +"<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\"\n" +" xmlns:cac=\"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2\"\n" +" xmlns:cbc=\"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2\">\n" +"\n" +" <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>\n" +" <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>\n" +"\n" +" <cbc:ID>{{INVOICE_ID}}</cbc:ID>\n" +" <cbc:IssueDate>{{ISSUE_DATE}}</cbc:IssueDate>\n" +" <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>\n" +" <cbc:DocumentCurrencyCode>{{CURRENCY}}</cbc:DocumentCurrencyCode>\n" +"\n" +" <cac:AccountingSupplierParty>\n" +" <cac:Party>\n" +" <cbc:EndpointID schemeID=\"{{SUPPLIER_ENDPOINT_SCHEME}}\">{{SUPPLIER_ENDPOINT_ID}}</cbc:EndpointID>\n" +" <cac:PartyName>\n" +" <cbc:Name>{{SUPPLIER_NAME}}</cbc:Name>\n" +" </cac:PartyName>\n" +" <cac:PostalAddress>\n" +" <cbc:StreetName>{{SUPPLIER_STREET}}</cbc:StreetName>\n" +" <cbc:AdditionalStreetName>{{SUPPLIER_STREET2}}</cbc:AdditionalStreetName>\n" +" <cbc:CityName>{{SUPPLIER_CITY}}</cbc:CityName>\n" +" <cbc:PostalZone>{{SUPPLIER_POSTAL}}</cbc:PostalZone>\n" +" <cbc:CountrySubentity>{{SUPPLIER_REGION}}</cbc:CountrySubentity>\n" +" <cac:Country>\n" +" <cbc:IdentificationCode>{{SUPPLIER_COUNTRY}}</cbc:IdentificationCode>\n" +" </cac:Country>\n" +" </cac:PostalAddress>\n" +" <cac:PartyTaxScheme>\n" +" <cbc:CompanyID>{{SUPPLIER_VAT_ID}}</cbc:CompanyID>\n" +" <cac:TaxScheme>\n" +" <cbc:ID>VAT</cbc:ID>\n" +" </cac:TaxScheme>\n" +" </cac:PartyTaxScheme>\n" +" </cac:Party>\n" +" </cac:AccountingSupplierParty>\n" +"\n" +" <cac:AccountingCustomerParty>\n" +" <cac:Party>\n" +" <cbc:EndpointID schemeID=\"{{CUSTOMER_ENDPOINT_SCHEME}}\">{{CUSTOMER_ENDPOINT_ID}}</cbc:EndpointID>\n" +" <cac:PartyName>\n" +" <cbc:Name>{{CUSTOMER_NAME}}</cbc:Name>\n" +" </cac:PartyName>\n" +" <cac:PostalAddress>\n" +" <cbc:StreetName>{{CUSTOMER_STREET}}</cbc:StreetName>\n" +" <cbc:AdditionalStreetName>{{CUSTOMER_STREET2}}</cbc:AdditionalStreetName>\n" +" <cbc:CityName>{{CUSTOMER_CITY}}</cbc:CityName>\n" +" <cbc:PostalZone>{{CUSTOMER_POSTAL}}</cbc:PostalZone>\n" +" <cbc:CountrySubentity>{{CUSTOMER_REGION}}</cbc:CountrySubentity>\n" +" <cac:Country>\n" +" <cbc:IdentificationCode>{{CUSTOMER_COUNTRY}}</cbc:IdentificationCode>\n" +" </cac:Country>\n" +" </cac:PostalAddress>\n" +" <cac:PartyTaxScheme>\n" +" <cbc:CompanyID>{{CUSTOMER_VAT_ID}}</cbc:CompanyID>\n" +" <cac:TaxScheme>\n" +" <cbc:ID>VAT</cbc:ID>\n" +" </cac:TaxScheme>\n" +" </cac:PartyTaxScheme>\n" +" </cac:Party>\n" +" </cac:AccountingCustomerParty>\n" +"\n" +" <cac:PaymentMeans>\n" +" <cbc:PaymentMeansCode>31</cbc:PaymentMeansCode>\n" +" <cac:PayeeFinancialAccount>\n" +" <cbc:ID>{{SUPPLIER_IBAN}}</cbc:ID>\n" +" <cac:FinancialInstitutionBranch>\n" +" <cac:FinancialInstitution>\n" +" <cbc:ID>{{SUPPLIER_BIC}}</cbc:ID>\n" +" </cac:FinancialInstitution>\n" +" </cac:FinancialInstitutionBranch>\n" +" </cac:PayeeFinancialAccount>\n" +" </cac:PaymentMeans>\n" +"\n" +" <cac:TaxTotal>\n" +" <cbc:TaxAmount currencyID=\"{{CURRENCY}}\">{{TOTAL_TAX_AMOUNT}}</cbc:TaxAmount>\n" +" <cac:TaxSubtotal>\n" +" <cbc:TaxableAmount currencyID=\"{{CURRENCY}}\">{{TAXABLE_AMOUNT}}</cbc:TaxableAmount>\n" +" <cbc:TaxAmount currencyID=\"{{CURRENCY}}\">{{TAX_AMOUNT}}</cbc:TaxAmount>\n" +" <cac:TaxCategory>\n" +" <cbc:ID>{{TAX_CATEGORY}}</cbc:ID>\n" +" <cbc:Percent>{{TAX_PERCENT}}</cbc:Percent>\n" +" <cac:TaxScheme>\n" +" <cbc:ID>VAT</cbc:ID>\n" +" </cac:TaxScheme>\n" +" </cac:TaxCategory>\n" +" </cac:TaxSubtotal>\n" +" </cac:TaxTotal>\n" +"\n" +" <cac:LegalMonetaryTotal>\n" +" <cbc:LineExtensionAmount currencyID=\"{{CURRENCY}}\">{{LINE_EXTENSION_AMOUNT}}</cbc:LineExtensionAmount>\n" +" <cbc:TaxExclusiveAmount currencyID=\"{{CURRENCY}}\">{{TAX_EXCLUSIVE_AMOUNT}}</cbc:TaxExclusiveAmount>\n" +" <cbc:TaxInclusiveAmount currencyID=\"{{CURRENCY}}\">{{TAX_INCLUSIVE_AMOUNT}}</cbc:TaxInclusiveAmount>\n" +" <cbc:PayableAmount currencyID=\"{{CURRENCY}}\">{{PAYABLE_AMOUNT}}</cbc:PayableAmount>\n" +" </cac:LegalMonetaryTotal>\n" +"\n" +" <cac:InvoiceLine>\n" +" <cbc:ID>{{LINE_ID}}</cbc:ID>\n" +" <cbc:InvoicedQuantity unitCode=\"{{UNIT_CODE}}\">{{QUANTITY}}</cbc:InvoicedQuantity>\n" +" <cbc:LineExtensionAmount currencyID=\"{{CURRENCY}}\">{{LINE_AMOUNT}}</cbc:LineExtensionAmount>\n" +" <cac:Item>\n" +" <cbc:Description>{{ITEM_DESCRIPTION}}</cbc:Description>\n" +" <cbc:Name>{{ITEM_NAME}}</cbc:Name>\n" +" <cac:ClassifiedTaxCategory>\n" +" <cbc:ID>{{LINE_TAX_CATEGORY}}</cbc:ID>\n" +" <cbc:Percent>{{LINE_TAX_PERCENT}}</cbc:Percent>\n" +" <cac:TaxScheme>\n" +" <cbc:ID>VAT</cbc:ID>\n" +" </cac:TaxScheme>\n" +" </cac:ClassifiedTaxCategory>\n" +" </cac:Item>\n" +" <cac:Price>\n" +" <cbc:PriceAmount currencyID=\"{{CURRENCY}}\">{{UNIT_PRICE}}</cbc:PriceAmount>\n" +" </cac:Price>\n" +" </cac:InvoiceLine>\n" +"\n" +"</Invoice>\n"; diff --git a/include/strops.hpp b/include/strops.hpp index 08dbd5a..4570a15 100644 --- a/include/strops.hpp +++ b/include/strops.hpp @@ -1,4 +1,5 @@ #pragma once -size_t strops_copy(char *dst, const char *src, size_t size); -char* strops_stristr(char* a, char* b);
\ No newline at end of file +size_t strops_copy(char *dst, const char *src, size_t size); +char* strops_stristr(char* a, char* b); +void strops_replace(char *buf, size_t buf_size, const char *search, const char *replace);
\ No newline at end of file diff --git a/src/administration.cpp b/src/administration.cpp index 21b70f3..d75aff7 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -274,23 +274,25 @@ static void administration_create_debug_data() { srand((unsigned) time(NULL)); - #define ADD_CONSUMER(_name, _addr1, _addr2, _cc)\ + #define ADD_CONSUMER(_name, _addr1, _cc, _city, _postal)\ {contact _c = administration_contact_create_empty();\ strops_copy(_c.name, _name, sizeof(_c.name));\ strops_copy(_c.address.address1, _addr1, sizeof(_c.address.address1));\ - strops_copy(_c.address.address2, _addr2, sizeof(_c.address.address2));\ strops_copy(_c.address.country_code, _cc, sizeof(_c.address.country_code));\ + strops_copy(_c.address.city, _city, sizeof(_c.address.city));\ + strops_copy(_c.address.postal, _postal, sizeof(_c.address.postal));\ _c.type = contact_type::CONTACT_CONSUMER;\ administration_contact_add(_c);}; - #define ADD_BUSINESS(_name, _addr1, _addr2, _cc, _tc, _bc)\ + #define ADD_BUSINESS(_name, _addr1, _cc, _tc, _bc, _city, _postal)\ {contact _c = administration_contact_create_empty();\ strops_copy(_c.name, _name, sizeof(_c.name));\ strops_copy(_c.address.address1, _addr1, sizeof(_c.address.address1));\ - strops_copy(_c.address.address2, _addr2, sizeof(_c.address.address2));\ strops_copy(_c.address.country_code, _cc, sizeof(_c.address.country_code));\ strops_copy(_c.taxid, _tc, sizeof(_c.taxid));\ strops_copy(_c.businessid, _bc, sizeof(_c.businessid));\ + strops_copy(_c.address.city, _city, sizeof(_c.address.city));\ + strops_copy(_c.address.postal, _postal, sizeof(_c.address.postal));\ _c.type = contact_type::CONTACT_BUSINESS;\ administration_contact_add(_c);}; @@ -301,37 +303,37 @@ static void administration_create_debug_data() strops_copy(g_administration.path, "C:\\Users\\aldri\\Downloads\\test.openbooks", sizeof(g_administration.path)); - ADD_CONSUMER("Emma Müller", "Hauptstraße 12", "10115 Berlin", "DE"); - ADD_CONSUMER("Luca Rossi", "Via Roma 45", "00184 Roma", "IT"); - ADD_CONSUMER("Sofia Garcia", "Calle Mayor 7", "28013 Madrid", "ES"); - ADD_CONSUMER("Jean Dupont", "10 Rue de la Paix", "75002 Paris", "FR"); - ADD_CONSUMER("Anna Nowak", "ul. Kwiatowa 3", "00-001 Warszawa", "PL"); - ADD_CONSUMER("Mikkel Jensen", "Østergade 8", "8000 Aarhus", "DK"); - ADD_CONSUMER("Maria Svensson", "Kungsgatan 15", "111 22 Stockholm", "SE"); - ADD_CONSUMER("Péter Kovács", "Fő utca 25", "1051 Budapest", "HU"); - ADD_CONSUMER("Lucas Silva", "Rua Augusta 100", "1250-001 Lisboa", "PT"); - ADD_CONSUMER("Isabelle Lefevre", "5 Place Stanislas", "54000 Nancy", "FR"); - - ADD_BUSINESS("Schmidt & Co GmbH", "Friedrichstraße 45", "10117 Berlin", "DE", "DE123456789", "HRB123456"); - ADD_BUSINESS("Bianchi Srl", "Corso Venezia 12", "20121 Milano", "IT", "IT987654321", "MI1234567"); - ADD_BUSINESS("Fernández y Asociados", "Gran Vía 20", "28013 Madrid", "ES", "ES456789123", "CIFB123456"); - ADD_BUSINESS("Martin & Partners", "12 Avenue Victor Hugo", "75016 Paris", "FR", "FR321654987", "SIRET123456"); - ADD_BUSINESS("Zielińska Consulting", "ul. Marszałkowska 10", "00-590 Warszawa", "PL", "PL789123456", "REGON123456"); - ADD_BUSINESS("Sørensen ApS", "Strøget 3", "1460 København", "DK", "DK654321789", "CVR12345678"); - ADD_BUSINESS("Johansson AB", "Drottninggatan 22", "111 51 Stockholm", "SE", "SE987654321", "OrgNr1234567"); - ADD_BUSINESS("Nagy Kft.", "Andrássy út 60", "1062 Budapest", "HU", "HU123987654", "Cégjegyzékszám123"); - ADD_BUSINESS("Santos Lda.", "Avenida da Liberdade 50", "1250-142 Lisboa", "PT", "PT321789654", "NIPC123456789"); - ADD_BUSINESS("Dupuis SARL", "8 Rue Saint-Denis", "75001 Paris", "FR", "FR456123789", "SIREN123456"); - ADD_BUSINESS("Müller & Söhne GmbH", "Leipziger Platz 8", "10117 Berlin", "DE", "DE654987321", "HRB987654"); - ADD_BUSINESS("Romano Srl", "Via Garibaldi 14", "16124 Genova", "IT", "IT321654987", "GE1239876"); - ADD_BUSINESS("López Asociados", "Plaza del Pilar 6", "50003 Zaragoza", "ES", "ES789321654", "CIFC654321"); - ADD_BUSINESS("Laurent & Fils", "15 Boulevard Haussmann", "75009 Paris", "FR", "FR987321654", "SIRET654987"); - ADD_BUSINESS("Kowalczyk Sp. z o.o.", "ul. Piotrkowska 55", "90-001 Łódź", "PL", "PL123456789", "REGON654321"); - ADD_BUSINESS("Nielsen ApS", "Nørregade 12", "1165 København", "DK", "DK789456123", "CVR87654321"); - ADD_BUSINESS("Lindberg AB", "Vasagatan 18", "111 20 Stockholm", "SE", "SE456789123", "OrgNr7654321"); - ADD_BUSINESS("Szabó Kft.", "Kossuth Lajos tér 1", "1055 Budapest", "HU", "HU987123654", "Cégjegyzékszám654321"); - ADD_BUSINESS("Costa Lda.", "Rua do Ouro 24", "1100-063 Lisboa", "PT", "PT654123987", "NIPC987654321"); - ADD_BUSINESS("Moreau SARL", "3 Place de la République", "75011 Paris", "FR", "FR321456987", "SIREN789123"); + ADD_CONSUMER("Emma Müller", "Hauptstraße 12", "DE", "Hamburg", "20095"); + ADD_CONSUMER("Luca Rossi", "Via Roma 45", "IT", "Torino", "10121"); + ADD_CONSUMER("Sofia Garcia", "Calle Mayor 7", "ES", "Valencia", "46001"); + ADD_CONSUMER("Jean Dupont", "10 Rue de la Paix", "FR", "Lyon", "69001"); + ADD_CONSUMER("Anna Nowak", "ul. Kwiatowa 3", "PL", "Kraków", "30-001"); + ADD_CONSUMER("Mikkel Jensen", "Østergade 8", "DK", "Odense", "5000"); + ADD_CONSUMER("Maria Svensson", "Kungsgatan 15", "SE", "Gothenburg", "411 01"); + ADD_CONSUMER("Péter Kovács", "Fő utca 25", "HU", "Debrecen", "4025"); + ADD_CONSUMER("Lucas Silva", "Rua Augusta 100", "PT", "Porto", "4000-123"); + ADD_CONSUMER("Isabelle Lefevre", "5 Place Stanislas", "FR", "Marseille", "13001"); + + ADD_BUSINESS("Schmidt & Co GmbH", "Friedrichstraße 45", "DE", "DE123456789", "HRB123456", "Stuttgart", "70173"); + ADD_BUSINESS("Bianchi Srl", "Corso Venezia 12", "IT", "IT987654321", "MI1234567", "Napoli", "80100"); + ADD_BUSINESS("Fernández y Asociados", "Gran Vía 20", "ES", "ES456789123", "CIFB123456", "Sevilla", "41001"); + ADD_BUSINESS("Martin & Partners", "12 Avenue Victor Hugo", "FR", "FR321654987", "SIRET123456", "Nice", "06000"); + ADD_BUSINESS("Zielińska Consulting", "ul. Marszałkowska 10", "PL", "PL789123456", "REGON123456", "Gdańsk", "80-001"); + ADD_BUSINESS("Sørensen ApS", "Strøget 3", "DK", "DK654321789", "CVR12345678", "Aalborg", "9000"); + ADD_BUSINESS("Johansson AB", "Drottninggatan 22", "SE", "SE987654321", "OrgNr1234567", "Malmö", "211 01"); + ADD_BUSINESS("Nagy Kft.", "Andrássy út 60", "HU", "HU123987654", "Cégjegyzékszám123", "Szeged", "6720"); + ADD_BUSINESS("Santos Lda.", "Avenida da Liberdade 50", "PT", "PT321789654", "NIPC123456789", "Coimbra", "3000-132"); + ADD_BUSINESS("Dupuis SARL", "8 Rue Saint-Denis", "FR", "FR456123789", "SIREN123456", "Toulouse", "31000"); + ADD_BUSINESS("Müller & Söhne GmbH", "Leipziger Platz 8", "DE", "DE654987321", "HRB987654", "Dresden", "01067"); + ADD_BUSINESS("Romano Srl", "Via Garibaldi 14", "IT", "IT321654987", "GE1239876", "Firenze", "50123"); + ADD_BUSINESS("López Asociados", "Plaza del Pilar 6", "ES", "ES789321654", "CIFC654321", "Bilbao", "48001"); + ADD_BUSINESS("Laurent & Fils", "15 Boulevard Haussmann", "FR", "FR987321654", "SIRET654987", "Bordeaux", "33000"); + ADD_BUSINESS("Kowalczyk Sp. z o.o.", "ul. Piotrkowska 55", "PL", "PL123456789", "REGON654321", "Poznań", "60-101"); + ADD_BUSINESS("Nielsen ApS", "Nørregade 12", "DK", "DK789456123", "CVR87654321", "Esbjerg", "6700"); + ADD_BUSINESS("Lindberg AB", "Vasagatan 18", "SE", "SE456789123", "OrgNr7654321", "Uppsala", "753 10"); + ADD_BUSINESS("Szabó Kft.", "Kossuth Lajos tér 1", "HU", "HU987123654", "Cégjegyzékszám654321", "Pécs", "7621"); + ADD_BUSINESS("Costa Lda.", "Rua do Ouro 24", "PT", "PT654123987", "NIPC987654321", "Braga", "4700-001"); + ADD_BUSINESS("Moreau SARL", "3 Place de la République", "FR", "FR321456987", "SIREN789123", "Lille", "59000"); ADD_PROJECT("eCommerce"); ADD_PROJECT("Retail store #1"); @@ -342,8 +344,10 @@ static void administration_create_debug_data() snprintf(g_administration.company_info.id, sizeof(g_administration.company_info.id), "C/%d", administration_create_id()); strops_copy(g_administration.company_info.name, "Aldrik Ramaekers", sizeof(g_administration.company_info.name)); strops_copy(g_administration.company_info.address.address1, "Keerderstraat 81", sizeof(g_administration.company_info.address.address1)); - strops_copy(g_administration.company_info.address.address2, "6226XW Maastricht", sizeof(g_administration.company_info.address.address2)); - strops_copy(g_administration.company_info.address.country_code, "NL", sizeof(g_administration.company_info.address.country_code)); + strops_copy(g_administration.company_info.address.address2, "", sizeof(g_administration.company_info.address.address2)); + strops_copy(g_administration.company_info.address.city, "Maastricht", sizeof(g_administration.company_info.address.city)); + strops_copy(g_administration.company_info.address.postal, "6226XW", sizeof(g_administration.company_info.address.postal)); + 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)); g_administration.next_id++; @@ -366,8 +370,8 @@ static void administration_create_debug_data() } // Create about 2 years of random data. - for (int i = 0; i < 410; i++) ADD_INVOICE(1); - for (int i = 0; i < 700; i++) ADD_INVOICE(0); + for (int i = 0; i < 1; i++) { ADD_INVOICE(1); } + for (int i = 0; i < 1; i++) { ADD_INVOICE(0); } } static s32 administration_create_sequence_number() @@ -587,17 +591,20 @@ int administration_contact_get_autocompletions(contact* buffer, int buf_size, ch bool administration_contact_is_valid(contact data) { - if (data.type == contact_type::CONTACT_CONSUMER) - { - return strlen(data.name) > 0 && strlen(data.address.address1) > 0 && strlen(data.address.address2) > 0 && strlen(data.address.country_code) > 0; - } - else if (data.type == contact_type::CONTACT_BUSINESS) + if (strlen(data.name) == 0) return 0; + if (strlen(data.address.city) == 0) return 0; + if (strlen(data.address.postal) == 0) return 0; + if (strlen(data.address.address1) == 0) return 0; + //if (strlen(data.address.address2) == 0) return 0; + if (strlen(data.address.country_code) == 0) return 0; + + if (data.type == contact_type::CONTACT_BUSINESS) { - return strlen(data.name) > 0 && strlen(data.address.address1) > 0 && strlen(data.address.address2) > 0 && strlen(data.address.country_code) > 0 - && strlen(data.taxid) > 0 && strlen(data.businessid); + if (strlen(data.taxid) == 0) return 0; + if (strlen(data.businessid) == 0) return 0; } - return false; + return true; } contact administration_contact_create_empty() @@ -1184,6 +1191,22 @@ u32 administation_invoice_get_outgoing_count() return g_administration.invoice_count; } +u32 administration_invoice_get_all(invoice* buffer) +{ + assert(buffer); + + u32 write_cursor = 0; + + list_iterator_start(&g_administration.invoices); + while (list_iterator_hasnext(&g_administration.invoices)) { + invoice c = *(invoice *)list_iterator_next(&g_administration.invoices); + buffer[write_cursor++] = c; + } + list_iterator_stop(&g_administration.invoices); + + return write_cursor; +} + static u32 administration_invoice_get_partial_list(u32 page_index, u32 page_size, invoice* buffer, bool want_outgoing) { assert(buffer); diff --git a/src/administration_writer.cpp b/src/administration_writer.cpp index 00543af..40fcd25 100644 --- a/src/administration_writer.cpp +++ b/src/administration_writer.cpp @@ -6,7 +6,9 @@ #include <threads.h> #include "ui.hpp" +#include "strops.hpp" #include "administration_writer.hpp" +#include "file_templates.hpp" mtx_t _save_file_mutex; @@ -80,6 +82,155 @@ static bool administration_writer_write_to_zip(char* entry_to_replace, char* ori } ///////////////////////////// +//// Invoices +///////////////////////////// +static char* administration_writer_get_eas_id_for_contact(contact contact) +{ + // https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/ + char* country_code = contact.address.country_code; + + // Countries using tax identification numbers. + if (strcmp(country_code, "AT") == 0) return contact.taxid; // Austria + if (strcmp(country_code, "BE") == 0) return contact.taxid; // Belgium + if (strcmp(country_code, "BG") == 0) return contact.taxid; // Bulgaria + if (strcmp(country_code, "CY") == 0) return contact.taxid; // Cyprus + if (strcmp(country_code, "CZ") == 0) return contact.taxid; // Czech Republic + if (strcmp(country_code, "DE") == 0) return contact.taxid; // Germany + if (strcmp(country_code, "EE") == 0) return contact.taxid; // Estonia + if (strcmp(country_code, "FR") == 0) return contact.taxid; // France + if (strcmp(country_code, "GR") == 0) return contact.taxid; // Greece + if (strcmp(country_code, "HR") == 0) return contact.taxid; // Croatia + if (strcmp(country_code, "HU") == 0) return contact.taxid; // Hungary + if (strcmp(country_code, "IE") == 0) return contact.taxid; // Ireland + if (strcmp(country_code, "LU") == 0) return contact.taxid; // Luxembourg + if (strcmp(country_code, "LV") == 0) return contact.taxid; // Latvia + if (strcmp(country_code, "MT") == 0) return contact.taxid; // Malta + if (strcmp(country_code, "NL") == 0) return contact.taxid; // Netherlands + if (strcmp(country_code, "PL") == 0) return contact.taxid; // Poland + if (strcmp(country_code, "PT") == 0) return contact.taxid; // Portugal + if (strcmp(country_code, "RO") == 0) return contact.taxid; // Romania + if (strcmp(country_code, "SI") == 0) return contact.taxid; // Slovenia + if (strcmp(country_code, "SK") == 0) return contact.taxid; // Slovakia + if (strcmp(country_code, "ES") == 0) return contact.taxid; // Spain + + // Countries using business identification numbers. + if (strcmp(country_code, "SE") == 0) return contact.businessid; // Sweden + if (strcmp(country_code, "LT") == 0) return contact.businessid; // Lithuania + if (strcmp(country_code, "IT") == 0) return contact.businessid; // Italy + if (strcmp(country_code, "FI") == 0) return contact.businessid; // Finland + if (strcmp(country_code, "DK") == 0) return contact.businessid; // Denmark + return NULL; // Unknown country code +} + +static char* administration_writer_get_eas_scheme_for_address(address addr) +{ + // https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/ + char* country_code = addr.country_code; + if (strcmp(country_code, "AT") == 0) return "9914"; // Austria + if (strcmp(country_code, "BE") == 0) return "9925"; // Belgium + if (strcmp(country_code, "BG") == 0) return "9926"; // Bulgaria + if (strcmp(country_code, "CY") == 0) return "9928"; // Cyprus + if (strcmp(country_code, "CZ") == 0) return "9929"; // Czech Republic + if (strcmp(country_code, "DE") == 0) return "9930"; // Germany + if (strcmp(country_code, "DK") == 0) return "0096"; // Denmark + if (strcmp(country_code, "EE") == 0) return "9931"; // Estonia + if (strcmp(country_code, "FR") == 0) return "9957"; // France + if (strcmp(country_code, "GR") == 0) return "9933"; // Greece + if (strcmp(country_code, "HR") == 0) return "9934"; // Croatia + if (strcmp(country_code, "HU") == 0) return "9910"; // Hungary + if (strcmp(country_code, "IE") == 0) return "9935"; // Ireland + if (strcmp(country_code, "FI") == 0) return "0212"; // Finland + if (strcmp(country_code, "IT") == 0) return "0208"; // Italy + if (strcmp(country_code, "LT") == 0) return "0200"; // Lithuania + if (strcmp(country_code, "LU") == 0) return "9938"; // Luxembourg + if (strcmp(country_code, "LV") == 0) return "9939"; // Latvia + if (strcmp(country_code, "MT") == 0) return "9943"; // Malta + if (strcmp(country_code, "NL") == 0) return "9944"; // Netherlands + if (strcmp(country_code, "PL") == 0) return "9945"; // Poland + if (strcmp(country_code, "PT") == 0) return "9946"; // Portugal + if (strcmp(country_code, "RO") == 0) return "9947"; // Romania + if (strcmp(country_code, "SE") == 0) return "0007"; // Sweden + if (strcmp(country_code, "SI") == 0) return "9949"; // Slovenia + if (strcmp(country_code, "SK") == 0) return "9950"; // Slovakia + if (strcmp(country_code, "ES") == 0) return "9920"; // Spain + return NULL; // Unknown country code +} + +bool administration_writer_save_invoice_blocking(invoice inv) +{ + #define APPEND(_txt, ...) file_content += sprintf(file_content, _txt, __VA_ARGS__); + + bool result = 1; + int buf_length = 15000; // 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)); + + struct tm *tm_info = 0; + char date_buffer[11]; // "YYYY-MM-DD" + null terminator + + strops_replace(file_content, buf_length, "{{INVOICE_ID}}", inv.sequential_number); + strops_replace(file_content, buf_length, "{{CURRENCY}}", inv.currency); + + // 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_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); + strops_replace(file_content, buf_length, "{{SUPPLIER_CITY}}", inv.supplier.address.city); + strops_replace(file_content, buf_length, "{{SUPPLIER_POSTAL}}", inv.supplier.address.postal); + 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); + + // 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_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); + strops_replace(file_content, buf_length, "{{CUSTOMER_CITY}}", inv.customer.address.city); + strops_replace(file_content, buf_length, "{{CUSTOMER_POSTAL}}", inv.customer.address.postal); + 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); + + // Dates + tm_info = localtime(&inv.issued_at); + strftime(date_buffer, sizeof(date_buffer), "%Y-%m-%d", tm_info); + strops_replace(file_content, buf_length, "{{ISSUE_DATE}}", date_buffer); + + //// Write to Disk. + char final_path[50]; + snprintf(final_path, 50, "%s.xml", inv.id); + + int final_length = (int)strlen(file_content); + if (!xml_parse_document((uint8_t*)file_content, final_length)) result = 0; + else if (!administration_writer_write_to_zip(final_path, file_content, final_length)) result = 0; + + free(file_content); + return result; +} + +static bool administration_writer_save_all_invoices_blocking() +{ + bool result = 1; + u32 num_invoices = administration_invoice_count(); + u32 buffer_size = sizeof(invoice) * num_invoices; + invoice* invoice_buffer = (invoice*)malloc(buffer_size); + num_invoices = administration_invoice_get_all(invoice_buffer); + + for (u32 i = 0; i < num_invoices; i++) { + invoice c = invoice_buffer[i]; + if (!administration_writer_save_invoice_blocking(c)) result = 0; + } + + free(invoice_buffer); + return result; +} + +///////////////////////////// //// Projects ///////////////////////////// bool administration_writer_save_project_blocking(project project) @@ -326,6 +477,7 @@ static int administration_writer_write_all_t(void *arg) { mtx_lock(&_save_file_mutex); + if (!administration_writer_save_all_invoices_blocking()) result = 0; if (!administration_writer_save_all_projects_blocking()) result = 0; if (!administration_writer_save_all_administration_info_blocking()) result = 0; if (!administration_writer_save_all_tax_brackets_blocking()) result = 0; diff --git a/src/locales/en.cpp b/src/locales/en.cpp index c965edd..767a07d 100644 --- a/src/locales/en.cpp +++ b/src/locales/en.cpp @@ -99,6 +99,9 @@ locale_entry en_locales[] = { {"contact.table.identifier", "Identifier"}, {"contact.table.name", "Name"}, {"contact.table.address", "Address"}, + {"contact.table.city", "City"}, + {"contact.table.postal", "Postal"}, + {"contact.table.region", "Region"}, // Project strings. {"project.form.identifier", "Identifier"}, diff --git a/src/strops.cpp b/src/strops.cpp index 116eec4..3389eb1 100644 --- a/src/strops.cpp +++ b/src/strops.cpp @@ -32,4 +32,29 @@ char* strops_stristr(char* haystack, char* needle) } } while (*haystack++); return 0; +} + +void strops_replace(char *buf, size_t buf_size, const char *search, const char *replace) +{ + size_t search_len = strlen(search); + size_t replace_len = strlen(replace); + + char *r = buf; // read pointer + char *w = buf; // write pointer + + while (*r && (w - buf) < (signed int)(buf_size - 1)) { + if (strncmp(r, search, search_len) == 0) { + // Ensure space + size_t remaining = buf_size - (w - buf) - 1; + size_t copy_len = (replace_len < remaining) ? replace_len : remaining; + + memcpy(w, replace, copy_len); + w += copy_len; + r += search_len; + } else { + *w++ = *r++; + } + } + + *w = '\0'; // terminate }
\ No newline at end of file diff --git a/src/ui/ui_contacts.cpp b/src/ui/ui_contacts.cpp index d680ae8..ca73ac3 100644 --- a/src/ui/ui_contacts.cpp +++ b/src/ui/ui_contacts.cpp @@ -30,6 +30,21 @@ void ui_draw_address_form(address* buffer) ImGui::InputTextWithHint(id, localize("contact.form.address2"), buffer->address2, IM_ARRAYSIZE(buffer->address2)); ImGui::SameLine();ui_helper_draw_required_tag(); + ImGui::SetNextItemWidth(widthAvailable*0.5f); + snprintf(id, sizeof(id), "%s##%p", localize("contact.form.city"), buffer); + ImGui::InputTextWithHint(id, localize("contact.form.city"), buffer->city, IM_ARRAYSIZE(buffer->city)); + ImGui::SameLine();ui_helper_draw_required_tag(); + + ImGui::SetNextItemWidth(widthAvailable*0.5f); + snprintf(id, sizeof(id), "%s##%p", localize("contact.form.postal"), buffer); + ImGui::InputTextWithHint(id, localize("contact.form.postal"), buffer->postal, IM_ARRAYSIZE(buffer->postal)); + ImGui::SameLine();ui_helper_draw_required_tag(); + + ImGui::SetNextItemWidth(widthAvailable*0.5f); + snprintf(id, sizeof(id), "%s##%p", localize("contact.form.region"), buffer); + ImGui::InputTextWithHint(id, localize("contact.form.region"), buffer->region, IM_ARRAYSIZE(buffer->region)); + ImGui::SameLine();ui_helper_draw_required_tag(); + // 5. Country dropdown. ImGui::SetNextItemWidth(widthAvailable*0.5f); const char* countries[] = { localize("country.AT"),localize("country.BE"),localize("country.BG"),localize("country.HR"),localize("country.CY"),localize("country.CZ"),localize("country.DK"),localize("country.EE"),localize("country.FI"),localize("country.FR"),localize("country.DE"),localize("country.GR"),localize("country.HU"),localize("country.IE"),localize("country.IT"),localize("country.LV"),localize("country.LT"),localize("country.LU"),localize("country.MT"),localize("country.NL"),localize("country.PL"),localize("country.PT"),localize("country.RO"),localize("country.SK"),localize("country.SI"),localize("country.ES"),localize("country.SE") }; |
