From 69b46045c504f611c005f989c0681ea9df231567 Mon Sep 17 00:00:00 2001 From: "Nina.van.hoof" Date: Tue, 5 Dec 2023 16:19:20 +0100 Subject: [PATCH] clean up after feedback --- backend/src/controllers/config.ts | 26 +++--- backend/src/controllers/invoices.ts | 76 +++++++++--------- backend/src/controllers/utils/index.ts | 105 ++++++++++++++----------- backend/src/models/invoices.ts | 53 ++++++++----- 4 files changed, 145 insertions(+), 115 deletions(-) diff --git a/backend/src/controllers/config.ts b/backend/src/controllers/config.ts index 14c226c5..ed2e94f1 100644 --- a/backend/src/controllers/config.ts +++ b/backend/src/controllers/config.ts @@ -1,15 +1,15 @@ -import { Request, Response } from 'express'; +import {Request, Response} from 'express'; import fs from 'fs'; -import { ObjectID } from 'mongodb'; +import {ObjectID} from 'mongodb'; import appConfig from '../config'; -import { ICompanyConfig } from '../models/config'; -import { getTemplatesPath } from './utils'; -import { CollectionNames, updateAudit } from '../models/common'; -import { ConfacRequest } from '../models/technical'; -import { saveAudit } from './utils/audit-logs'; +import {ICompanyConfig} from '../models/config'; +import {getTemplatesPath} from './utils'; +import {CollectionNames, updateAudit} from '../models/common'; +import {ConfacRequest} from '../models/technical'; +import {saveAudit} from './utils/audit-logs'; export const getCompanyConfig = async (req: Request, res: Response) => { - const companyConfig: ICompanyConfig | null = await req.db.collection(CollectionNames.CONFIG).findOne({ key: 'conf' }); + const companyConfig: ICompanyConfig | null = await req.db.collection(CollectionNames.CONFIG).findOne({key: 'conf'}); if (companyConfig) { return res.send(companyConfig); } @@ -32,16 +32,16 @@ export const getSecurityConfig = async (req: Request, res: Response) => { export const saveCompanyConfig = async (req: ConfacRequest, res: Response) => { - const { _id, ...config }: ICompanyConfig = req.body; + const {_id, ...config}: ICompanyConfig = req.body; if (_id) { config.audit = updateAudit(config.audit, req.user); - const { value: originalConfig } = await req.db.collection(CollectionNames.CONFIG) - .findOneAndUpdate({ _id: new ObjectID(_id) }, { $set: config }, { returnOriginal: true }); + const {value: originalConfig} = await req.db.collection(CollectionNames.CONFIG) + .findOneAndUpdate({_id: new ObjectID(_id)}, {$set: config}, {returnOriginal: true}); await saveAudit(req, 'config', originalConfig, config); - return res.send({ _id, ...config }); + return res.send({_id, ...config}); } const inserted = await req.db.collection(CollectionNames.CONFIG).insertOne(config); @@ -60,7 +60,7 @@ export const getTemplates = (req: Request, res: Response) => { /** Get logs_audit for an entity */ export const getAudit = async (req: Request, res: Response) => { const logs = await req.db.collection('logs_audit') - .find({ model: req.query.model, modelId: new ObjectID(req.query.modelId) }) + .find({model: req.query.model, modelId: new ObjectID(req.query.modelId)}) .toArray(); return res.send(logs); diff --git a/backend/src/controllers/invoices.ts b/backend/src/controllers/invoices.ts index c5ff78ab..187c1514 100644 --- a/backend/src/controllers/invoices.ts +++ b/backend/src/controllers/invoices.ts @@ -1,14 +1,13 @@ import moment from 'moment'; -import { Request, Response } from 'express'; -import { ObjectID, Db } from 'mongodb'; -import { IInvoice, INVOICE_EXCEL_HEADERS } from '../models/invoices'; -import { IAttachmentCollection } from '../models/attachments'; -import { createPdf, createXml } from './utils'; -import { CollectionNames, IAttachment, createAudit, updateAudit } from '../models/common'; -import { IProjectMonth } from '../models/projectsMonth'; -import { ConfacRequest, Jwt } from '../models/technical'; -import { saveAudit } from './utils/audit-logs'; -import { ICompanyConfig } from '../models/config'; +import {Request, Response} from 'express'; +import {ObjectID, Db} from 'mongodb'; +import {IInvoice, INVOICE_EXCEL_HEADERS} from '../models/invoices'; +import {IAttachmentCollection} from '../models/attachments'; +import {createPdf, createXml} from './utils'; +import {CollectionNames, IAttachment, createAudit, updateAudit} from '../models/common'; +import {IProjectMonth} from '../models/projectsMonth'; +import {ConfacRequest, Jwt} from '../models/technical'; +import {saveAudit} from './utils/audit-logs'; @@ -22,8 +21,7 @@ const createInvoice = async (invoice: IInvoice, db: Db, pdfBuffer: Buffer, user: let xmlBuffer; if (!invoice.isQuotation) { - const companyConfig: ICompanyConfig = await db.collection(CollectionNames.CONFIG).findOne({ key: 'conf' }); - xmlBuffer = Buffer.from(createXml(invoice, companyConfig)); + xmlBuffer = Buffer.from(createXml(invoice)); await db.collection>(CollectionNames.ATTACHMENTS).insertOne({ _id: new ObjectID(createdInvoice._id), pdf: pdfBuffer, @@ -44,21 +42,21 @@ const createInvoice = async (invoice: IInvoice, db: Db, pdfBuffer: Buffer, user: const moveProjectMonthAttachmentsToInvoice = async (invoice: IInvoice, projectMonthId: ObjectID, db: Db) => { const projectMonthAttachments: IAttachmentCollection | null = await db.collection(CollectionNames.ATTACHMENTS_PROJECT_MONTH) - .findOne({ _id: projectMonthId }, { projection: { _id: false } }); + .findOne({_id: projectMonthId}, {projection: {_id: false}}); if (projectMonthAttachments) { - await db.collection(CollectionNames.ATTACHMENTS).findOneAndUpdate({ _id: invoice._id }, { $set: { ...projectMonthAttachments } }); + await db.collection(CollectionNames.ATTACHMENTS).findOneAndUpdate({_id: invoice._id}, {$set: {...projectMonthAttachments}}); } - const projectMonth = await db.collection(CollectionNames.PROJECTS_MONTH).findOne({ _id: projectMonthId }); + const projectMonth = await db.collection(CollectionNames.PROJECTS_MONTH).findOne({_id: projectMonthId}); const updatedAttachmentDetails = projectMonth ? [...invoice.attachments, ...projectMonth?.attachments] : invoice.attachments; const inserted = await db.collection(CollectionNames.INVOICES) - .findOneAndUpdate({ _id: new ObjectID(invoice._id) }, { $set: { attachments: updatedAttachmentDetails } }, { returnOriginal: false }); + .findOneAndUpdate({_id: new ObjectID(invoice._id)}, {$set: {attachments: updatedAttachmentDetails}}, {returnOriginal: false}); const updatedInvoice = inserted.value; - await db.collection(CollectionNames.PROJECTS_MONTH).findOneAndUpdate({ _id: projectMonthId }, { $set: { attachments: [] } }); - await db.collection(CollectionNames.ATTACHMENTS_PROJECT_MONTH).findOneAndDelete({ _id: projectMonthId }); + await db.collection(CollectionNames.PROJECTS_MONTH).findOneAndUpdate({_id: projectMonthId}, {$set: {attachments: []}}); + await db.collection(CollectionNames.ATTACHMENTS_PROJECT_MONTH).findOneAndDelete({_id: projectMonthId}); return updatedInvoice; }; @@ -67,7 +65,7 @@ const moveProjectMonthAttachmentsToInvoice = async (invoice: IInvoice, projectMo export const getInvoicesController = async (req: Request, res: Response) => { const getFrom = moment().subtract(req.query.months, 'months').startOf('month').format('YYYY-MM-DD'); const invoices = await req.db.collection(CollectionNames.INVOICES) - .find({ date: { $gte: getFrom } }) + .find({ date: {$gte: getFrom} }) .toArray(); return res.send(invoices); }; @@ -78,8 +76,8 @@ export const createInvoiceController = async (req: ConfacRequest, res: Response) const invoice: IInvoice = req.body; if (!invoice.isQuotation) { - const [lastInvoice] = await req.db.collection(CollectionNames.INVOICES).find({ isQuotation: false }) - .sort({ number: -1 }) + const [lastInvoice] = await req.db.collection(CollectionNames.INVOICES).find({isQuotation: false}) + .sort({number: -1}) .limit(1) .toArray(); @@ -130,10 +128,10 @@ export const createInvoiceController = async (req: ConfacRequest, res: Response) /** Update an existing invoice */ export const updateInvoiceController = async (req: ConfacRequest, res: Response) => { - const { _id, ...invoice }: IInvoice = req.body; + const {_id, ...invoice}: IInvoice = req.body; invoice.audit = updateAudit(invoice.audit, req.user); - const updatedPdfBuffer = await createPdf({ _id, ...invoice }); + const updatedPdfBuffer = await createPdf({_id, ...invoice}); if (!Buffer.isBuffer(updatedPdfBuffer) && updatedPdfBuffer.error) { return res.status(500).send(updatedPdfBuffer.error); @@ -141,7 +139,7 @@ export const updateInvoiceController = async (req: ConfacRequest, res: Response) if (Buffer.isBuffer(updatedPdfBuffer)) { await req.db.collection(CollectionNames.ATTACHMENTS) - .findOneAndUpdate({ _id: new ObjectID(_id) }, { $set: { pdf: updatedPdfBuffer } }); + .findOneAndUpdate({_id: new ObjectID(_id)}, {$set: { pdf: updatedPdfBuffer }}); } if (!invoice.projectMonth) { @@ -149,8 +147,8 @@ export const updateInvoiceController = async (req: ConfacRequest, res: Response) invoice.projectMonth = undefined; } - const { value: originalInvoice } = await req.db.collection(CollectionNames.INVOICES) - .findOneAndUpdate({ _id: new ObjectID(_id) }, { $set: invoice }, { returnOriginal: true }); + const {value: originalInvoice} = await req.db.collection(CollectionNames.INVOICES) + .findOneAndUpdate({_id: new ObjectID(_id)}, {$set: invoice}, {returnOriginal: true}); // Fix diff if (!invoice.projectMonth) { @@ -167,12 +165,12 @@ export const updateInvoiceController = async (req: ConfacRequest, res: Response) // Right now it is always updating the projectMonth.verified but this only changes when the invoice.verified changes // This is now 'fixed' on the frontend. projectMonth = await req.db.collection(CollectionNames.PROJECTS_MONTH) - .findOneAndUpdate({ _id: new ObjectID(invoice.projectMonth.projectMonthId) }, { $set: { verified: invoice.verified } }); + .findOneAndUpdate({_id: new ObjectID(invoice.projectMonth.projectMonthId)}, {$set: { verified: invoice.verified }}); } const result: Array = [{ type: 'invoice', - model: { _id, ...invoice }, + model: {_id, ...invoice}, }]; if (projectMonth && projectMonth.ok && projectMonth.value) { result.push({ @@ -188,13 +186,13 @@ export const updateInvoiceController = async (req: ConfacRequest, res: Response) /** Hard invoice delete: There is no coming back from this one */ export const deleteInvoiceController = async (req: Request, res: Response) => { - const { id: invoiceId }: { id: string; } = req.body; + const {id: invoiceId}: {id: string;} = req.body; - const invoice = await req.db.collection(CollectionNames.INVOICES).findOne({ _id: new ObjectID(invoiceId) }); + const invoice = await req.db.collection(CollectionNames.INVOICES).findOne({_id: new ObjectID(invoiceId)}); if (invoice?.projectMonth) { const invoiceAttachments: IAttachmentCollection | null = await req.db.collection(CollectionNames.ATTACHMENTS) - .findOne({ _id: new ObjectID(invoiceId) as ObjectID }, { + .findOne({_id: new ObjectID(invoiceId) as ObjectID}, { projection: { _id: false, pdf: false, @@ -202,8 +200,8 @@ export const deleteInvoiceController = async (req: Request, res: Response) => { }); if (invoiceAttachments !== null && Object.keys(invoiceAttachments).length > 0) { - await req.db.collection(CollectionNames.ATTACHMENTS_PROJECT_MONTH).updateOne({ _id: new ObjectID(invoice.projectMonth.projectMonthId) }, { - $set: { ...invoiceAttachments } + await req.db.collection(CollectionNames.ATTACHMENTS_PROJECT_MONTH).updateOne({_id: new ObjectID(invoice.projectMonth.projectMonthId)}, { + $set: { ...invoiceAttachments } }, { upsert: true }); @@ -211,11 +209,11 @@ export const deleteInvoiceController = async (req: Request, res: Response) => { const projectMonthCollection = req.db.collection(CollectionNames.PROJECTS_MONTH); const attachments = invoice.attachments.filter(a => a.type !== 'pdf'); - await projectMonthCollection.findOneAndUpdate({ _id: new ObjectID(invoice.projectMonth.projectMonthId) }, { $set: { attachments } }); + await projectMonthCollection.findOneAndUpdate({_id: new ObjectID(invoice.projectMonth.projectMonthId)}, {$set: {attachments}}); } - await req.db.collection(CollectionNames.INVOICES).findOneAndDelete({ _id: new ObjectID(invoiceId) }); - await req.db.collection(CollectionNames.ATTACHMENTS).findOneAndDelete({ _id: new ObjectID(invoiceId) }); + await req.db.collection(CollectionNames.INVOICES).findOneAndDelete({_id: new ObjectID(invoiceId)}); + await req.db.collection(CollectionNames.ATTACHMENTS).findOneAndDelete({_id: new ObjectID(invoiceId)}); return res.send(invoiceId); }; @@ -241,7 +239,7 @@ export const previewPdfInvoiceController = async (req: Request, res: Response) = export const generateExcelForInvoicesController = async (req: Request, res: Response) => { const invoiceIds: ObjectID[] = req.body.map((invoiceId: string) => new ObjectID(invoiceId)); - const invoices = await req.db.collection(CollectionNames.INVOICES).find({ _id: { $in: invoiceIds } }) + const invoices = await req.db.collection(CollectionNames.INVOICES).find({_id: {$in: invoiceIds}}) .toArray(); const separator = ';'; @@ -269,9 +267,9 @@ export const generateExcelForInvoicesController = async (req: Request, res: Resp export const getInvoiceXmlController = async (req: Request, res: Response) => { - const { id } = req.params; + const {id} = req.params; const invoiceAttachments: IAttachmentCollection | null = await req.db.collection(CollectionNames.ATTACHMENTS) - .findOne({ _id: new ObjectID(id) as ObjectID }); + .findOne({_id: new ObjectID(id) as ObjectID}); if (invoiceAttachments && invoiceAttachments.xml) { return res.type('application/xml').send(atob(invoiceAttachments.xml.toString())); } else { diff --git a/backend/src/controllers/utils/index.ts b/backend/src/controllers/utils/index.ts index a45aaf3e..c620e2eb 100644 --- a/backend/src/controllers/utils/index.ts +++ b/backend/src/controllers/utils/index.ts @@ -3,16 +3,15 @@ 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 { ICompanyConfig } from '../../models/config'; +import {COUNTRY_CODES, ENDPOINT_SCHEMES, IInvoice, UNIT_CODES} 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 {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'; // See: https://github.com/marcbachmann/node-html-pdf/issues/531 const pdfOptions = { @@ -34,7 +33,7 @@ export const convertHtmlToBuffer = (html: string): Promise => new Promis }); }); -export const createHtml = (invoice: IInvoice): string | { error: string; } => { +export const createHtml = (invoice: IInvoice): string | {error: string;} => { /* eslint-disable no-param-reassign */ invoice = JSON.parse(JSON.stringify(invoice)); // if (Array.isArray(invoice.extraFields)) { @@ -82,8 +81,11 @@ export const getTemplatesPath = (): string => { return './templates/'; }; - -export const createXml = (savedInvoice: IInvoice, companyConfig: ICompanyConfig): 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 => { 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: { @@ -101,33 +103,31 @@ export const createXml = (savedInvoice: IInvoice, companyConfig: ICompanyConfig) providerNit: '' } }); - - if (savedInvoice && companyConfig) { - const currencyID = { currencyID: DEFAULT_CURRENCY }; - const taxSchemeIDVAT = new TaxScheme({ id: 'VAT' }); + 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); - let cityRef = savedInvoice.client.city.trim(); const customerPostalAddress = new PostalAddress({ - streetName: savedInvoice.client.address, - cityName: cityRef, - country: new Country({ identificationCode: customerCountryAndCode ? customerCountryAndCode.code : DEFAULT_COUNTRY_CODE }) + streetName: savedInvoice.client.address.trim(), + cityName: savedInvoice.client.city.trim(), + country: new Country({identificationCode: customerCountryAndCode ? customerCountryAndCode.code : DEFAULT_COUNTRY_CODE}) }); - cityRef = companyConfig.company.city.trim(); const supplierPostalAddress = new PostalAddress({ - streetName: companyConfig.company.address, - cityName: cityRef, - country: new Country({ identificationCode: DEFAULT_COUNTRY_CODE }) + streetName: savedInvoice.your.address.trim(), + cityName: savedInvoice.your.city.trim(), + country: new Country({identificationCode: DEFAULT_COUNTRY_CODE}) }); const supplierLegalEntity = new PartyLegalEntity({ - registrationName: companyConfig.company.name, - companyID: companyConfig.company.btw + 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(companyConfig.company.btw, { - schemeID: supplierEndpointScheme ? supplierEndpointScheme.schemeID : '9925' + const supplierEndpointID = new UdtIdentifier(savedInvoice.your.btw, { + schemeID: supplierEndpointScheme ? supplierEndpointScheme.schemeID : belgianVATEndpointScheme }); const supplierParty = new Party({ @@ -135,12 +135,12 @@ export const createXml = (savedInvoice: IInvoice, companyConfig: ICompanyConfig) partyLegalEntities: [supplierLegalEntity], postalAddress: supplierPostalAddress, contact: new Contact({ - name: companyConfig.company.website, - electronicMail: companyConfig.company.email, - telephone: companyConfig.company.telephone + name: savedInvoice.your.website, + electronicMail: savedInvoice.your.email, + telephone: savedInvoice.your.telephone }), partyTaxSchemes: [new PartyTaxScheme({ - companyID: companyConfig.company.btw, + companyID: savedInvoice.your.btw, taxScheme: taxSchemeIDVAT })] }); @@ -177,23 +177,29 @@ export const createXml = (savedInvoice: IInvoice, companyConfig: ICompanyConfig) }); - 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 }; + /** 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: 'AE', + id: reversedTaxChargeCode, percent: '0', - taxExemptionReasonCode: 'VATEX-EU-AE', + taxExemptionReasonCode: reversedTaxChargeReasonCode, taxScheme: taxSchemeIDVAT } classifiedTaxObject = { - id: 'AE', + id: reversedTaxChargeCode, percent: '0', taxScheme: taxSchemeIDVAT } } else { taxObject = { - id: 'S', + id: standardTaxChargeCode, percent: '21', taxScheme: taxSchemeIDVAT } @@ -210,17 +216,24 @@ export const createXml = (savedInvoice: IInvoice, companyConfig: ICompanyConfig) })] }); + /** more info on PaymentMeans codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL4461/ */ + const debitPaymentMeansCode = '31'; const paymentMeans = new PaymentMeans({ - paymentMeansCode: '31', + paymentMeansCode: debitPaymentMeansCode, payeeFinancialAccount: new PayeeFinancialAccount({ - id: companyConfig.company.iban, + id: savedInvoice.your.iban, financialInstitutioBranch: new FinancialInstitutionBranch({ - id: companyConfig.company.bic + id: savedInvoice.your.bic }) }) }); const orderRef = savedInvoice.orderNr && savedInvoice.orderNr.trim().length !== 0 ? savedInvoice.orderNr : savedInvoice._id; + + /** More info on invoice type codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNCL1001-inv/ + * Code 380 signals a commercial invoice. + */ + const commercialInvoiceTypeCode = '380'; invoiceXml.addProperty('xmlns', 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'); invoiceXml.addProperty('xmlns:cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); @@ -229,21 +242,25 @@ export const createXml = (savedInvoice: IInvoice, companyConfig: ICompanyConfig) invoiceXml.setProfileID('urn:fdc:peppol.eu:2017:poacc:billing:01:1.0'); invoiceXml.setIssueDate(moment(savedInvoice.date).format('YYYY-MM-DD')); invoiceXml.setDueDate(moment(savedInvoice.date).add(30, 'days').format('YYYY-MM-DD')); - invoiceXml.setInvoiceTypeCode('380'); + invoiceXml.setInvoiceTypeCode(commercialInvoiceTypeCode); invoiceXml.setDocumentCurrencyCode(DEFAULT_CURRENCY); invoiceXml.setAccountingSupplierParty(accountingSupplierParty); invoiceXml.setAccountingCustomerParty(accountingCustomerParty); invoiceXml.setLegalMonetaryTotal(legalMonetaryTotal); invoiceXml.setID(savedInvoice.number.toString()); - invoiceXml.setOrderReference(new OrderReference({ id: orderRef })); + invoiceXml.setOrderReference(new OrderReference({id: orderRef})); 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 : 'C64' }), + 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, diff --git a/backend/src/models/invoices.ts b/backend/src/models/invoices.ts index df515001..a8ba0bf5 100644 --- a/backend/src/models/invoices.ts +++ b/backend/src/models/invoices.ts @@ -1,6 +1,7 @@ -import {ObjectID} from 'mongodb'; -import {IClient} from './clients'; -import {IAttachment, IAudit} from './common'; +import { ObjectID } from 'mongodb'; +import { IClient } from './clients'; +import { IAttachment, IAudit } from './common'; +import { EditClientRateType } from './projects'; export interface IInvoiceMoney { totalWithoutTax: number; @@ -76,26 +77,40 @@ export const INVOICE_EXCEL_HEADERS = [ 'First line desc', 'Id', ]; +/** + * ISO 3166 country codes used to identify countries + * This is a requirement for the e-invoice xml based on the peppol protocol + * see https://docs.peppol.eu/poacc/billing/3.0/codelist/ISO3166/ + */ export const COUNTRY_CODES = [ - { code: 'BE', country: 'Belgiƫ' }, - { code: 'NL', country: 'Nederland' }, - { code: 'FR', country: 'Frankrijk' }, - { code: 'DE', country: 'Duitsland' }, - { code: 'GB', country: 'UK' } + {code: 'BE', country: 'Belgiƫ'}, + {code: 'NL', country: 'Nederland'}, + {code: 'FR', country: 'Frankrijk'}, + {code: 'DE', country: 'Duitsland'}, + {code: 'GB', country: 'UK'} ] +/** + * Endpoint scheme codes are a requirement for the e-invoice xml based on the peppol protocol and represent international commercial entity codes + * more info on endpoint scheme codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/eas/ */ export const ENDPOINT_SCHEMES = [ - { country: 'BE', schemeID: '9925' }, - { country: 'NL', schemeID: '9944' }, - { country: 'FR', schemeID: '9957' }, - { country: 'DE', schemeID: '9930' }, - { country: 'GB', schemeID: '9932' } + {country: 'BE', schemeID: '9925'}, + {country: 'NL', schemeID: '9944'}, + {country: 'FR', schemeID: '9957'}, + {country: 'DE', schemeID: '9930'}, + {country: 'GB', schemeID: '9932'} ]; -export const UNIT_CODES = [ - { unit: 'daily', code: 'DAY' }, - { unit: 'hourly', code: 'HUR' }, - { unit: 'km', code: 'KMT' }, - { unit: 'items', code: 'NAR' }, - { unit: 'other', code: 'C64' } +/** + * Unit codes are a requirement for the e-invoice xml based on the peppol protocol + * more info on unit codes: more info on unit codes: https://docs.peppol.eu/poacc/billing/3.0/codelist/UNECERec20/ + */ + +type UnitCodes = { unit: EditClientRateType, code: string }; +export const UNIT_CODES: UnitCodes[] = [ + {unit: 'daily', code: 'DAY'}, + {unit: 'hourly', code: 'HUR'}, + {unit: 'km', code: 'KMT'}, + {unit: 'items', code: 'NAR'}, + {unit: 'other', code: 'C62'} ];