diff --git a/README.md b/README.md
index e76826c8..fb95fd66 100644
--- a/README.md
+++ b/README.md
@@ -96,4 +96,4 @@ React.FC
## E-invoice xml
-The e-invoices generated by confac should comply with the Peppol BIS Billing 3.0 standard defined by the EU. To doublecheck if the created xml does follow this protocol, you can use this [tool](https://ecosio.com/en/peppol-and-xml-document-validator/). Choose 'OpenPeppol UBL Invoice (2023.05) (aka BIS Billing 3.0.15)' as the ruleset. This is the most current ruleset at the time of writing. Release notes and the general xml structure can be consulted [here]( https://docs.peppol.eu/poacc/billing/3.0)
\ No newline at end of file
+The e-invoices generated by confac should comply with the Peppol BIS Billing 3.0 standard defined by the EU. To doublecheck if the created xml does follow this protocol, you can use this [tool](https://ecosio.com/en/peppol-and-xml-document-validator/). Choose 'OpenPeppol UBL Invoice (2023.11) (aka BIS Billing 3.0.16)' as the ruleset. This is the most current ruleset at the time of writing. Release notes and the general xml structure can be consulted [here]( https://docs.peppol.eu/poacc/billing/3.0)
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 7d93381d..a1da2fab 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -20,6 +20,7 @@
"express-async-errors": "^3.1.1",
"express-jwt": "^5.3.3",
"express-unless": "^0.5.0",
+ "fast-xml-parser": "^4.4.0",
"google-auth-library": "^6.0.0",
"html-pdf": "^2.1.0",
"jsonwebtoken": "^8.5.1",
@@ -34,7 +35,8 @@
"regenerator-runtime": "^0.13.3",
"slugify": "^1.1.0",
"tmp": "^0.2.1",
- "ubl-builder": "github:pipesanta/ubl-builder"
+ "ubl-builder": "github:pipesanta/ubl-builder",
+ "uuid": "^10.0.0"
},
"devDependencies": {
"@babel/cli": "^7.7.7",
@@ -64,6 +66,7 @@
"@types/request": "^2.48.4",
"@types/supertest": "^2.0.12",
"@types/tmp": "^0.1.0",
+ "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^2.17.0",
"@typescript-eslint/parser": "^2.17.0",
"babel-eslint": "^10.0.3",
@@ -391,6 +394,23 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@aws-sdk/client-sts/node_modules/fast-xml-parser": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz",
+ "integrity": "sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "strnum": "^1.0.5"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ },
+ "funding": {
+ "type": "paypal",
+ "url": "https://paypal.me/naturalintelligence"
+ }
+ },
"node_modules/@aws-sdk/config-resolver": {
"version": "3.303.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.303.0.tgz",
@@ -4279,6 +4299,12 @@
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw=="
},
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "dev": true
+ },
"node_modules/@types/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -8047,20 +8073,24 @@
"integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="
},
"node_modules/fast-xml-parser": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz",
- "integrity": "sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==",
- "dev": true,
- "optional": true,
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz",
+ "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/naturalintelligence"
+ }
+ ],
"dependencies": {
"strnum": "^1.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
- },
- "funding": {
- "type": "paypal",
- "url": "https://paypal.me/naturalintelligence"
}
},
"node_modules/fb-watchman": {
@@ -13712,6 +13742,19 @@
"node": ">=10"
}
},
+ "node_modules/mongodb-memory-server-core/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/mongodb-memory-server-core/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -17016,9 +17059,7 @@
"node_modules/strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
- "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
- "dev": true,
- "optional": true
+ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
},
"node_modules/superagent": {
"version": "8.0.9",
@@ -17818,7 +17859,6 @@
"node_modules/ubl-builder": {
"version": "1.4.3",
"resolved": "git+ssh://git@github.com/pipesanta/ubl-builder.git#3367464b5369a4cbc1adf8c4e65c0d2f3431ee27",
- "license": "ISC",
"dependencies": {
"@xmldom/xmldom": "^0.7.0",
"weeknumber": "^1.1.2",
@@ -18192,10 +18232,13 @@
}
},
"node_modules/uuid": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
- "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
- "dev": true,
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -19167,6 +19210,18 @@
"@aws-sdk/util-utf8": "3.303.0",
"fast-xml-parser": "4.1.2",
"tslib": "^2.5.0"
+ },
+ "dependencies": {
+ "fast-xml-parser": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz",
+ "integrity": "sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "strnum": "^1.0.5"
+ }
+ }
}
},
"@aws-sdk/config-resolver": {
@@ -22147,6 +22202,12 @@
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw=="
},
+ "@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "dev": true
+ },
"@types/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -25051,11 +25112,9 @@
"integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="
},
"fast-xml-parser": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.1.2.tgz",
- "integrity": "sha512-CDYeykkle1LiA/uqQyNwYpFbyF6Axec6YapmpUP+/RHWIoR1zKjocdvNaTsxCxZzQ6v9MLXaSYm9Qq0thv0DHg==",
- "dev": true,
- "optional": true,
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz",
+ "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==",
"requires": {
"strnum": "^1.0.5"
}
@@ -29356,6 +29415,12 @@
"lru-cache": "^6.0.0"
}
},
+ "uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "dev": true
+ },
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -31975,9 +32040,7 @@
"strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
- "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
- "dev": true,
- "optional": true
+ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
},
"superagent": {
"version": "8.0.9",
@@ -32873,10 +32936,9 @@
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
},
"uuid": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
- "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
- "dev": true
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
},
"v8-compile-cache": {
"version": "2.3.0",
diff --git a/backend/package.json b/backend/package.json
index 8df273db..584753a8 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -38,6 +38,7 @@
"express-async-errors": "^3.1.1",
"express-jwt": "^5.3.3",
"express-unless": "^0.5.0",
+ "fast-xml-parser": "^4.4.0",
"google-auth-library": "^6.0.0",
"html-pdf": "^2.1.0",
"jsonwebtoken": "^8.5.1",
@@ -52,7 +53,8 @@
"regenerator-runtime": "^0.13.3",
"slugify": "^1.1.0",
"tmp": "^0.2.1",
- "ubl-builder": "github:pipesanta/ubl-builder"
+ "ubl-builder": "github:pipesanta/ubl-builder",
+ "uuid": "^10.0.0"
},
"devDependencies": {
"@babel/cli": "^7.7.7",
@@ -82,6 +84,7 @@
"@types/request": "^2.48.4",
"@types/supertest": "^2.0.12",
"@types/tmp": "^0.1.0",
+ "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^2.17.0",
"@typescript-eslint/parser": "^2.17.0",
"babel-eslint": "^10.0.3",
diff --git a/backend/src/controllers/invoices.ts b/backend/src/controllers/invoices.ts
index 557ad6f4..e723f9f6 100644
--- a/backend/src/controllers/invoices.ts
+++ b/backend/src/controllers/invoices.ts
@@ -20,7 +20,7 @@ const createInvoice = async (invoice: IInvoice, db: Db, pdfBuffer: Buffer, user:
if (!invoice.isQuotation) {
- const xmlBuffer = Buffer.from(createXml(createdInvoice));
+ const xmlBuffer = Buffer.from(createXml(createdInvoice, pdfBuffer));
await db.collection>(CollectionNames.ATTACHMENTS).insertOne({
_id: new ObjectID(createdInvoice._id),
pdf: pdfBuffer,
@@ -141,10 +141,10 @@ export const updateInvoiceController = async (req: ConfacRequest, res: Response)
}
if (!invoice.isQuotation) {
- const updateXmlBuffer = Buffer.from(createXml({_id, ...invoice}));
+ const updateXmlBuffer = Buffer.from(createXml({_id, ...invoice}, updatedPdfBuffer as Buffer));
await req.db.collection(CollectionNames.ATTACHMENTS)
.findOneAndUpdate({_id: new ObjectID(_id)}, {$set: {xml: updateXmlBuffer}});
- }
+ }
if (!invoice.projectMonth) {
// Makes sure projectMonth is overwritten in the db if already present there
diff --git a/backend/src/controllers/utils/index.ts b/backend/src/controllers/utils/index.ts
index d6337c08..0e6217a4 100644
--- a/backend/src/controllers/utils/index.ts
+++ b/backend/src/controllers/utils/index.ts
@@ -3,15 +3,23 @@ import pug from 'pug';
import appConfig from '../../config';
import locals from '../../pug-helpers';
-import {COUNTRY_CODES, ENDPOINT_SCHEMES, IInvoice, UNIT_CODES} from '../../models/invoices';
+import {IInvoice} from '../../models/invoices';
import moment from 'moment';
import {Invoice} from 'ubl-builder';
-import {TaxScheme, PostalAddress, Country, PartyLegalEntity, Party, Contact, PartyTaxScheme, AccountingSupplierParty, AccountingCustomerParty, LegalMonetaryTotal, TaxTotal, TaxSubtotal, TaxCategory, PaymentMeans, OrderReference, InvoiceLine, Item, ClassifiedTaxCategory, Price} from 'ubl-builder/lib/ubl21/CommonAggregateComponents';
-import {FinancialInstitutionBranch} from 'ubl-builder/lib/ubl21/CommonAggregateComponents/FinancialInstitutionBranch';
-import {PayeeFinancialAccount} from 'ubl-builder/lib/ubl21/CommonAggregateComponents/PayeeFinancialAccount';
-import {UdtIdentifier, UdtAmount, UdtPercent, UdtQuantity} from 'ubl-builder/lib/ubl21/types/UnqualifiedDataTypes';
-import {SellersItemIdentification} from 'ubl-builder/lib/ubl21/CommonAggregateComponents/SellersItemIdentification';
-import {DEFAULT_COUNTRY_CODE, DEFAULT_CURRENCY} from '../config';
+import {TaxScheme } from 'ubl-builder/lib/ubl21/CommonAggregateComponents';
+import {DEFAULT_CURRENCY} from '../config';
+import {
+ createAccountingCustomerParty,
+ createAccountingSupplierParty,
+ createAdditionalDocumentReference,
+ createInvoiceLine,
+ createLegalMonetaryTotal,
+ createOrderReference,
+ createPaymentMeans,
+ createTaxObjects,
+ createTaxTotal,
+ postProccess} from './peppol';
+import { XMLBuilder, XMLParser } from 'fast-xml-parser';
// See: https://github.com/marcbachmann/node-html-pdf/issues/531
const pdfOptions = {
@@ -85,7 +93,7 @@ export const getTemplatesPath = (): string => {
*This method creates an invoice xml following UBL 2.1 standard, required for Peppol protocol.
Check https://docs.peppol.eu/poacc/billing/3.0/ for full documentation.
*/
-export const createXml = (savedInvoice: IInvoice): string => {
+export const createXml = (savedInvoice: IInvoice, pdf?: Buffer): string => {
const invoiceXml = new Invoice(savedInvoice.number.toString(), {
//This empty object is created to keep TypeScript from complaining, it has no influence on the generated xml
issuer: {
@@ -106,133 +114,22 @@ export const createXml = (savedInvoice: IInvoice): string => {
if (savedInvoice) {
const currencyID = {currencyID: DEFAULT_CURRENCY};
const taxSchemeIDVAT = new TaxScheme({id: 'VAT'});
- const customerCountryAndCode = COUNTRY_CODES.find(codes => codes.country === savedInvoice.client.country || codes.code === savedInvoice.client.country);
- const customerPostalAddress = new PostalAddress({
- streetName: savedInvoice.client.address.trim(),
- cityName: savedInvoice.client.city.trim(),
- country: new Country({identificationCode: customerCountryAndCode ? customerCountryAndCode.code : DEFAULT_COUNTRY_CODE}),
- postalZone: savedInvoice.client.postalCode.trim(),
- });
-
- const supplierPostalAddress = new PostalAddress({
- streetName: savedInvoice.your.address.trim(),
- cityName: savedInvoice.your.city.trim(),
- country: new Country({identificationCode: DEFAULT_COUNTRY_CODE}),
- postalZone: savedInvoice.your.postalCode.trim(),
- });
-
- const supplierLegalEntity = new PartyLegalEntity({
- registrationName: savedInvoice.your.name,
- companyID: savedInvoice.your.btw
- });
-
- const belgianVATEndpointScheme = '9925';
- const supplierEndpointScheme = ENDPOINT_SCHEMES.find(scheme => scheme.country === DEFAULT_COUNTRY_CODE);
- const supplierEndpointID = new UdtIdentifier(savedInvoice.your.btw, {
- schemeID: supplierEndpointScheme ? supplierEndpointScheme.schemeID : belgianVATEndpointScheme
- });
-
- const supplierParty = new Party({
- EndpointID: supplierEndpointID,
- partyLegalEntities: [supplierLegalEntity],
- postalAddress: supplierPostalAddress,
- contact: new Contact({
- name: savedInvoice.your.website,
- electronicMail: savedInvoice.your.email,
- telephone: savedInvoice.your.telephone
- }),
- partyTaxSchemes: [new PartyTaxScheme({
- companyID: savedInvoice.your.btw,
- taxScheme: taxSchemeIDVAT
- })]
- });
- const accountingSupplierParty = new AccountingSupplierParty({
- party: supplierParty
- });
-
- const customerLegalEntity = new PartyLegalEntity({
- registrationName: savedInvoice.client.name,
- companyID: savedInvoice.client.btw
- });
- const customerEndpointScheme = ENDPOINT_SCHEMES.find(scheme => scheme.country === savedInvoice.client.country);
- const customerEndpointID = new UdtIdentifier(savedInvoice.client.btw, {
- schemeID: customerEndpointScheme ? customerEndpointScheme.schemeID : ''
- });
- const customerParty = new Party({
- EndpointID: customerEndpointID,
- partyLegalEntities: [customerLegalEntity],
- postalAddress: customerPostalAddress,
- partyTaxSchemes: [new PartyTaxScheme({
- companyID: savedInvoice.client.btw,
- taxScheme: taxSchemeIDVAT
- })]
- });
- const accountingCustomerParty = new AccountingCustomerParty({
- party: customerParty
- });
+ const accountingSupplierParty = createAccountingSupplierParty(savedInvoice, taxSchemeIDVAT);
+ const accountingCustomerParty = createAccountingCustomerParty(savedInvoice, taxSchemeIDVAT);
+ const legalMonetaryTotal = createLegalMonetaryTotal(savedInvoice, currencyID);
+ const {taxCategory, classifiedTaxCategory} = createTaxObjects(savedInvoice, taxSchemeIDVAT);
+ const taxTotal = createTaxTotal(savedInvoice, taxCategory, currencyID);
+ const paymentMeans = createPaymentMeans(savedInvoice);
+ const orderReference = createOrderReference(savedInvoice);
- const legalMonetaryTotal = new LegalMonetaryTotal({
- lineExtensionAmount: new UdtAmount(savedInvoice.money.totalWithoutTax.toFixed(2), currencyID),
- taxInclusiveAmount: new UdtAmount(savedInvoice.money.total.toFixed(2), currencyID),
- taxExclusiveAmount: new UdtAmount(savedInvoice.money.totalWithoutTax.toFixed(2), currencyID),
- payableAmount: new UdtAmount(savedInvoice.money.total.toFixed(2), currencyID)
- });
+ const additionalDocumentReference = createAdditionalDocumentReference(pdf);
- /** More info on the tax codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/
- * and on the reason codes: https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-TaxTotal/cac-TaxSubtotal/cac-TaxCategory/cbc-TaxExemptionReasonCode/
- */
- let taxObject: {id: string | UdtIdentifier; percent: string | UdtPercent; taxScheme: TaxScheme | undefined; taxExemptionReasonCode?: string | undefined};
- let classifiedTaxObject: {id: string | UdtIdentifier; percent: string | UdtPercent; taxScheme: TaxScheme | undefined};
- const reversedTaxChargeCode = 'AE';
- const standardTaxChargeCode = 'S';
- const reversedTaxChargeReasonCode = 'VATEX-EU-AE';
- if (savedInvoice.client.country !== DEFAULT_COUNTRY_CODE) {
- taxObject = {
- id: reversedTaxChargeCode,
- percent: '0',
- taxExemptionReasonCode: reversedTaxChargeReasonCode,
- taxScheme: taxSchemeIDVAT
- }
- classifiedTaxObject = {
- id: reversedTaxChargeCode,
- percent: '0',
- taxScheme: taxSchemeIDVAT
- }
- } else {
- taxObject = {
- id: standardTaxChargeCode,
- percent: '21',
- taxScheme: taxSchemeIDVAT
- }
- classifiedTaxObject = taxObject;
+ if(additionalDocumentReference){
+ invoiceXml.addAdditionalDocumentReference(additionalDocumentReference);
}
-
- const taxTotal = new TaxTotal({
- taxAmount: new UdtAmount(savedInvoice.money.totalTax.toFixed(2), currencyID),
- taxSubtotals: [new TaxSubtotal({
- taxableAmount: new UdtAmount(savedInvoice.money.totalWithoutTax.toFixed(2), currencyID),
- taxAmount: new UdtAmount(savedInvoice.money.totalTax.toFixed(2), currencyID),
- taxCategory: new TaxCategory(taxObject)
- })]
- });
-
- /** more info on PaymentMeans codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL4461/ */
- const debitPaymentMeansCode = '31';
- const paymentMeans = new PaymentMeans({
- paymentMeansCode: debitPaymentMeansCode,
- payeeFinancialAccount: new PayeeFinancialAccount({
- id: savedInvoice.your.iban,
- financialInstitutioBranch: new FinancialInstitutionBranch({
- id: savedInvoice.your.bic
- })
- })
- });
-
- const orderRef = savedInvoice.orderNr && savedInvoice.orderNr.trim().length !== 0 ? savedInvoice.orderNr : savedInvoice._id.toString();
-
/** More info on invoice type codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL1001-inv/
* Code 380 signals a commercial invoice.
*/
@@ -251,36 +148,19 @@ export const createXml = (savedInvoice: IInvoice): string => {
invoiceXml.setAccountingCustomerParty(accountingCustomerParty);
invoiceXml.setLegalMonetaryTotal(legalMonetaryTotal);
invoiceXml.setID(savedInvoice._id.toString());
- invoiceXml.setOrderReference(new OrderReference({id: orderRef}));
+ invoiceXml.setOrderReference(orderReference);
invoiceXml.addTaxTotal(taxTotal);
invoiceXml.addPaymentMeans(paymentMeans);
- /** more info on unit codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNECERec20/
- * code C62 is a general code meaning 'one' or 'unit'
- */
- const defaultUnitCode = 'C62';
- savedInvoice.lines.forEach((line, index) => {
- const unitCode = UNIT_CODES.find(unitCode => unitCode.unit === line.type);
- const invoiceLine = new InvoiceLine({
- id: (index + 1).toString(),
- invoicedQuantity: new UdtQuantity(line.amount.toString(), {unitCode: unitCode ? unitCode.code : defaultUnitCode}),
- lineExtensionAmount: new UdtAmount((line.price * line.amount).toFixed(2), currencyID),
- item: new Item({
- name: line.desc,
- //sellersItemID is not necessary, but added here to keep TypeScript happy
- sellersItemIdentification: new SellersItemIdentification({
- id: savedInvoice.number + '-' + (index + 1)
- }),
- classifiedTaxCategory: new ClassifiedTaxCategory(classifiedTaxObject)
- }),
- price: new Price({
- priceAmount: new UdtAmount(line.price.toFixed(2), currencyID),
- baseQuantity: '1'
- })
- });
- invoiceXml.addInvoiceLine(invoiceLine);
+
+ savedInvoice.lines.forEach((line, index:number) => {
+ const createdInvoiceLine = createInvoiceLine(savedInvoice, line, index, currencyID, classifiedTaxCategory)
+ invoiceXml.addInvoiceLine(createdInvoiceLine);
});
+
}
- return invoiceXml.getXml();
+ const xml = postProccess(invoiceXml, pdf, savedInvoice);
+ return xml;
}
+
diff --git a/backend/src/controllers/utils/peppol.ts b/backend/src/controllers/utils/peppol.ts
new file mode 100644
index 00000000..92e06019
--- /dev/null
+++ b/backend/src/controllers/utils/peppol.ts
@@ -0,0 +1,340 @@
+import { AccountingCustomerParty, AccountingSupplierParty, AdditionalDocumentReference, AdditionalDocumentReferenceParams, ClassifiedTaxCategory, Contact, Country, InvoiceLine, Item, LegalMonetaryTotal, OrderReference, Party, PartyLegalEntity, PartyName, PartyTaxScheme, PaymentMeans, PostalAddress, Price, TaxCategory, TaxScheme, TaxSubtotal, TaxTotal } from "ubl-builder/lib/ubl21/CommonAggregateComponents";
+import { COUNTRY_CODES, ENDPOINT_SCHEMES, IInvoice, UNIT_CODES, InvoiceLine as IInvoiceLine } from "../../models/invoices";
+import { UdtAmount, UdtIdentifier, UdtName, UdtQuantity } from "ubl-builder/lib/ubl21/types/UnqualifiedDataTypes";
+import { DEFAULT_COUNTRY_CODE } from "../config";
+import { PayeeFinancialAccount } from "ubl-builder/lib/ubl21/CommonAggregateComponents/PayeeFinancialAccount";
+import { FinancialInstitutionBranch } from "ubl-builder/lib/ubl21/CommonAggregateComponents/FinancialInstitutionBranch";
+import { SellersItemIdentification } from "ubl-builder/lib/ubl21/CommonAggregateComponents/SellersItemIdentification";
+import { v4 as uuidv4 } from 'uuid'
+import { Invoice } from "ubl-builder";
+import { XMLBuilder, XMLParser } from "fast-xml-parser";
+
+const companyNumberScheme = '0208';
+
+export const createAccountingSupplierParty = (invoice: IInvoice, taxScheme: TaxScheme): AccountingSupplierParty => {
+ /**
+ * https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-AccountingSupplierParty/
+ */
+ const supplierPostalAddress = new PostalAddress({
+ streetName: invoice.your.address.trim(),
+ cityName: invoice.your.city.trim(),
+ country: new Country({identificationCode: DEFAULT_COUNTRY_CODE}),
+ postalZone: invoice.your.postalCode.trim(),
+ });
+
+ const companyNumber = createCompanyNumber(invoice.your.btw, 'BE');
+
+ const supplierEndpointID = new UdtIdentifier(companyNumber, {
+ schemeID: companyNumberScheme
+ });
+
+ const cleanedVat = cleanVat(invoice.your.btw);
+
+ const supplierLegalEntity = new PartyLegalEntity({
+ registrationName: invoice.your.name,
+ companyID: companyNumber
+ });
+
+ const supplierParty = new Party({
+ partyNames: [
+ new PartyName({
+ name: new UdtName(invoice.your.name)
+ })
+ ],
+ EndpointID: supplierEndpointID,
+ partyLegalEntities: [supplierLegalEntity],
+ postalAddress: supplierPostalAddress,
+ contact: new Contact({
+ name: invoice.your.name,
+ electronicMail: invoice.your.email,
+ telephone: invoice.your.telephone?.replace('+', '00') ?? ''
+ }),
+ partyTaxSchemes: [new PartyTaxScheme({
+ companyID: cleanedVat,
+ taxScheme: taxScheme
+ })]
+ });
+
+ const accountingSupplierParty = new AccountingSupplierParty({
+ party: supplierParty
+ });
+
+ return accountingSupplierParty;
+}
+
+export const createAccountingCustomerParty = (invoice: IInvoice, taxScheme: TaxScheme): AccountingCustomerParty => {
+ /**
+ * https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-AccountingCustomerParty/
+ */
+
+ const customerCountryAndCode = COUNTRY_CODES.find(codes => codes.country === invoice.client.country || codes.code === invoice.client.country);
+
+ const customerPostalAddress = new PostalAddress({
+ streetName: invoice.client.address?.trim() ?? '',
+ cityName: invoice.client.city?.trim() ?? '',
+ country: new Country({identificationCode: customerCountryAndCode ? customerCountryAndCode.code : DEFAULT_COUNTRY_CODE}),
+ postalZone: invoice.client.postalCode?.trim() ?? '',
+ });
+
+ const companyNumber = createCompanyNumber(invoice.client.btw, customerCountryAndCode?.code ?? DEFAULT_COUNTRY_CODE);
+
+ const customerLegalEntity = new PartyLegalEntity({
+ registrationName: invoice.client.name,
+ companyID: companyNumber
+ });
+
+ const cleanedVat = cleanVat(invoice.your.btw);
+
+ const customerEndpointScheme = ENDPOINT_SCHEMES.find(scheme => scheme.country === customerCountryAndCode?.country ? customerCountryAndCode.code : DEFAULT_COUNTRY_CODE);
+
+ const customerEndpointID = new UdtIdentifier(cleanedVat, {
+ schemeID: customerEndpointScheme ? customerEndpointScheme.schemeID : ''
+ });
+
+ const customerParty = new Party({
+ partyNames: [
+ new PartyName({
+ name: new UdtName(invoice.client.name)
+ })
+ ],
+ EndpointID: customerEndpointID,
+ partyLegalEntities: [customerLegalEntity],
+ postalAddress: customerPostalAddress,
+ partyTaxSchemes: [new PartyTaxScheme({
+ companyID: cleanedVat,
+ taxScheme: taxScheme
+ })]
+ });
+
+ const accountingCustomerParty = new AccountingCustomerParty({
+ party: customerParty
+ });
+
+ return accountingCustomerParty;
+}
+
+export const createLegalMonetaryTotal = (invoice: IInvoice, currency: {currencyID: string}): LegalMonetaryTotal => {
+ /**
+ * https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-LegalMonetaryTotal/
+ */
+ const legalMonetaryTotal = new LegalMonetaryTotal({
+ lineExtensionAmount: new UdtAmount(invoice.money.totalWithoutTax.toFixed(2), currency),
+ taxInclusiveAmount: new UdtAmount(invoice.money.total.toFixed(2), currency),
+ taxExclusiveAmount: new UdtAmount(invoice.money.totalWithoutTax.toFixed(2), currency),
+ payableAmount: new UdtAmount(invoice.money.total.toFixed(2), currency)
+ });
+
+ return legalMonetaryTotal;
+}
+
+export const createTaxTotal = (invoice: IInvoice, taxCategory: TaxCategory, currency: {currencyID: string}): TaxTotal => {
+ /**
+ * https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-TaxTotal/
+ */
+ const taxTotal = new TaxTotal({
+ taxAmount: new UdtAmount(invoice.money.totalTax.toFixed(2), currency),
+ taxSubtotals: [new TaxSubtotal({
+ taxableAmount: new UdtAmount(invoice.money.totalWithoutTax.toFixed(2), currency),
+ taxAmount: new UdtAmount(invoice.money.totalTax.toFixed(2), currency),
+ taxCategory: taxCategory
+ })]
+ });
+
+ return taxTotal;
+}
+
+export const createTaxObjects = (invoice: IInvoice, taxScheme: TaxScheme) : { taxCategory: TaxCategory, classifiedTaxCategory: ClassifiedTaxCategory } => {
+ /**
+ * More info on the tax codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL5305/
+ * More info on vatex codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/vatex/
+ * and on the reason codes: https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-TaxTotal/cac-TaxSubtotal/cac-TaxCategory/cbc-TaxExemptionReasonCode/
+ */
+ const customerCountryAndCode = COUNTRY_CODES.find(codes => codes.country === invoice.client.country || codes.code === invoice.client.country);
+
+ if(customerCountryAndCode?.code === 'UK'){
+ const outsideEuropeTaxChargeCode = 'G';
+ const outsideEuropeTaxChargeReasonCode = 'VATEX-EU-G';
+ return {
+ taxCategory : new TaxCategory({
+ id: outsideEuropeTaxChargeCode,
+ percent: '0',
+ taxExemptionReasonCode: outsideEuropeTaxChargeReasonCode,
+ taxScheme: taxScheme
+ }),
+ classifiedTaxCategory: new ClassifiedTaxCategory({
+ id: outsideEuropeTaxChargeCode,
+ percent: '0',
+ taxScheme: taxScheme
+ })
+ }
+ }
+ else if (customerCountryAndCode?.code !== DEFAULT_COUNTRY_CODE) {
+ const reversedTaxChargeCode = 'AE';
+ const reversedTaxChargeReasonCode = 'VATEX-EU-AE';
+ return {
+ taxCategory : new TaxCategory({
+ id: reversedTaxChargeCode,
+ percent: '0',
+ taxExemptionReasonCode: reversedTaxChargeReasonCode,
+ taxScheme: taxScheme
+ }),
+ classifiedTaxCategory: new ClassifiedTaxCategory({
+ id: reversedTaxChargeCode,
+ percent: '0',
+ taxScheme: taxScheme
+ })
+ }
+ }
+
+ const vat: number = 21
+ const standardTaxChargeCode = 'S';
+ return {
+ taxCategory: new TaxCategory({
+ id: standardTaxChargeCode,
+ percent: vat.toFixed(2),
+ taxScheme: taxScheme
+ }),
+ classifiedTaxCategory: new ClassifiedTaxCategory({
+ id: standardTaxChargeCode,
+ percent: vat.toFixed(2),
+ taxScheme: taxScheme
+ })
+ }
+}
+
+export const createPaymentMeans = (invoice: IInvoice): PaymentMeans => {
+ /**
+ * https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-PaymentMeans/
+ * more info on PaymentMeans codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL4461/
+ */
+ const debitPaymentMeansCode = '30';
+ const iban = invoice.your.iban.split(' ').join('')
+ const paymentMeans = new PaymentMeans({
+ paymentMeansCode: debitPaymentMeansCode,
+ payeeFinancialAccount: new PayeeFinancialAccount({
+ id: iban,
+ financialInstitutioBranch: new FinancialInstitutionBranch({
+ id: invoice.your.bic
+ })
+ })
+ });
+
+ return paymentMeans;
+}
+
+export const createOrderReference = (invoice: IInvoice): OrderReference => {
+ /**
+ * https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-OrderReference/
+ */
+ const orderRef = invoice.orderNr && invoice.orderNr.trim().length !== 0 ? invoice.orderNr : invoice._id.toString();
+ const orderReference = new OrderReference({
+ id: orderRef
+ })
+ return orderReference;
+}
+
+export const createInvoiceLine = (invoice: IInvoice, line: IInvoiceLine, index: number, currency: {currencyID: string}, classifiedTaxCategory: ClassifiedTaxCategory) : InvoiceLine => {
+ /**
+ * https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/cac-InvoiceLine/
+ * more info on unit codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNECERec20/
+ * code C62 is a general code meaning 'one' or 'unit'
+ */
+ const defaultUnitCode = 'C62';
+ const unitCode = UNIT_CODES.find(unitCode => unitCode.unit === line.type);
+ const invoiceLine = new InvoiceLine({
+ id: (index + 1).toString(),
+ invoicedQuantity: new UdtQuantity(line.amount.toString(), {unitCode: unitCode ? unitCode.code : defaultUnitCode}),
+ lineExtensionAmount: new UdtAmount((line.price * line.amount).toFixed(2), currency),
+ item: new Item({
+ name: line.desc,
+ //sellersItemID is not necessary, but added here to keep TypeScript happy
+ sellersItemIdentification: new SellersItemIdentification({
+ id: invoice.number + '-' + (index + 1)
+ }),
+ classifiedTaxCategory: classifiedTaxCategory,
+
+ }),
+ price: new Price({
+ priceAmount: new UdtAmount(line.price.toFixed(2), currency),
+ baseQuantity: '1'
+ })
+ });
+
+ return invoiceLine
+}
+
+export const createAdditionalDocumentReference = (pdf: Buffer | undefined):AdditionalDocumentReference | undefined => {
+ if(pdf){
+ const additionalDocumentReference = new AdditionalDocumentReference({
+ id: uuidv4(),
+ attachment: ''
+ } as AdditionalDocumentReferenceParams)
+
+ return additionalDocumentReference;
+ }
+}
+
+export const postProccess = (invoice: Invoice, pdf: Buffer | undefined, savedInvoice: IInvoice):string => {
+ const xml = invoice.getXml();
+ //stuff we cant do with ubl builder
+ const parser = new XMLParser({
+ ignoreAttributes: false,
+ attributeNamePrefix: '@_'
+ });
+
+ let jObj = parser.parse(xml);
+
+ //somehow ublbuilder removes leading 0 so we readd it here
+ const companyNumber = createCompanyNumber(savedInvoice.your.btw, 'BE');
+
+ if(jObj.Invoice['cac:AccountingSupplierParty']['cac:Party']['cbc:EndpointID']){
+ jObj.Invoice['cac:AccountingSupplierParty']['cac:Party']['cbc:EndpointID'] = {
+ '#text': companyNumber,
+ '@_schemeID': companyNumberScheme
+ }
+ }
+
+ if(pdf){
+ jObj.Invoice['cac:AdditionalDocumentReference']['cac:Attachment'] = {
+ 'cbc:EmbeddedDocumentBinaryObject':{
+ '#text': pdf.toString('base64'),
+ '@_filename': 'invoice.pdf',
+ '@_mimeCode': 'application/pdf'
+ }
+ };
+ }
+
+ const builder = new XMLBuilder({
+ ignoreAttributes: false,
+ attributeNamePrefix: '@_'
+ });
+
+ const xmlContent = builder.build(jObj);
+
+ return xmlContent;
+}
+
+const cleanVat = (vat: string): string => {
+ const cleanedVat = vat
+ .split('.').join('')
+ .split(' ').join('');
+
+ return cleanedVat;
+}
+
+const createCompanyNumber = (vat: string, countryCode: string): string => {
+ const companyNumber = cleanVat(vat)
+ .replace(countryCode, '');
+
+ switch(countryCode){
+ case 'NL':
+ return companyNumber.padStart(12, '0')
+ case 'UK':
+ case 'DE':
+ case 'FR':
+ return companyNumber.padStart(9, '0')
+ case 'BE':
+ default:
+ return companyNumber.padStart(10, '0')
+ }
+}