diff --git a/back/models/emailUser.json b/back/models/email-user.json similarity index 72% rename from back/models/emailUser.json rename to back/models/email-user.json index d612d5d297..e983635ce5 100644 --- a/back/models/emailUser.json +++ b/back/models/email-user.json @@ -23,5 +23,13 @@ "model": "Account", "foreignKey": "userFk" } - } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] } diff --git a/db/changes/10370-pickles/00-ACL.sql b/db/changes/10370-pickles/00-ACL.sql new file mode 100644 index 0000000000..c5e10dec51 --- /dev/null +++ b/db/changes/10370-pickles/00-ACL.sql @@ -0,0 +1 @@ +UPDATE salix.ACL t SET t.principalId = 'employee' WHERE t.id = 269; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 2b65efbc10..1635335c45 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -489,11 +489,11 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`) VALUES - (1, 'T', 1014.24, CURDATE(), 1101, CURDATE(), 442, CURDATE(), CURDATE(), 1, 1), - (2, 'T', 121.36, CURDATE(), 1102, CURDATE(), 442, CURDATE(), CURDATE(), 1, 1), - (3, 'T', 8.88, CURDATE(), 1103, CURDATE(), 442, CURDATE(), CURDATE(), 1, 1), - (4, 'T', 8.88, CURDATE(), 1103, CURDATE(), 442, CURDATE(), CURDATE(), 1, 1), - (5, 'A', 8.88, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1103, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 442, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 1); + (1, 'T', 1014.24, CURDATE(), 1101, CURDATE(), 442, CURDATE(), CURDATE(), 1, 0), + (2, 'T', 121.36, CURDATE(), 1102, CURDATE(), 442, CURDATE(), CURDATE(), 1, 0), + (3, 'T', 8.88, CURDATE(), 1103, CURDATE(), 442, CURDATE(), CURDATE(), 1, 0), + (4, 'T', 8.88, CURDATE(), 1103, CURDATE(), 442, CURDATE(), CURDATE(), 1, 0), + (5, 'A', 8.88, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1103, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 442, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 0); UPDATE `vn`.`invoiceOut` SET ref = 'T1111111' WHERE id = 1; UPDATE `vn`.`invoiceOut` SET ref = 'T2222222' WHERE id = 2; diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 70f3984a6d..bc97eae195 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -115,5 +115,6 @@ "A ticket with a negative base can't be invoiced": "A ticket with a negative base can't be invoiced", "This client is not invoiceable": "This client is not invoiceable", "INACTIVE_PROVIDER": "Inactive provider", - "reference duplicated": "reference duplicated" + "reference duplicated": "reference duplicated", + "The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 7a670d2edd..cdf15c6744 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -209,5 +209,6 @@ "Wasn't able to invoice the following clients": "No se han podido facturar los siguientes clientes", "Can't verify data unless the client has a business type": "No se puede verificar datos de un cliente que no tiene tipo de negocio", "You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito", - "You can't change the credit set to zero from a manager": "No puedes cambiar el cŕedito establecido a cero por un gerente" + "You can't change the credit set to zero from a manager": "No puedes cambiar el cŕedito establecido a cero por un gerente", + "The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'" } \ No newline at end of file diff --git a/modules/client/back/methods/client/hasCustomerRole.js b/modules/client/back/methods/client/hasCustomerRole.js index d668399362..e790d6e3ae 100644 --- a/modules/client/back/methods/client/hasCustomerRole.js +++ b/modules/client/back/methods/client/hasCustomerRole.js @@ -19,7 +19,7 @@ module.exports = Self => { } }); - Self.hasCustomerRole = (id, options) => { + Self.hasCustomerRole = async(id, options) => { const myOptions = {}; if (typeof options == 'object') @@ -31,7 +31,9 @@ module.exports = Self => { JOIN salix.Role r ON r.id = A.roleFK WHERE r.name = 'customer' AND A.id IN (?)`; + const [result] = await Self.rawSql(query, [id], myOptions); + const {isCustomer} = result; - return Self.rawSql(query, [id], myOptions); + return isCustomer; }; }; diff --git a/modules/client/back/methods/client/specs/hasCustomerRole.spec.js b/modules/client/back/methods/client/specs/hasCustomerRole.spec.js index 08b13a88eb..0fe83b7388 100644 --- a/modules/client/back/methods/client/specs/hasCustomerRole.spec.js +++ b/modules/client/back/methods/client/specs/hasCustomerRole.spec.js @@ -9,9 +9,9 @@ describe('Client hasCustomerRole', () => { const id = 1101; - const [result] = await models.Client.hasCustomerRole(id, options); + const result = await models.Client.hasCustomerRole(id, options); - expect(result).toEqual(jasmine.objectContaining({isCustomer: 1})); + expect(result).toBeTruthy(); await tx.rollback(); } catch (e) { @@ -27,9 +27,9 @@ describe('Client hasCustomerRole', () => { const options = {transaction: tx}; const id = 8; - const [result] = await models.Client.hasCustomerRole(id, options); + const result = await models.Client.hasCustomerRole(id, options); - expect(result).toEqual(jasmine.objectContaining({isCustomer: 0})); + expect(result).toBeFalsy(); await tx.rollback(); } catch (e) { @@ -46,9 +46,9 @@ describe('Client hasCustomerRole', () => { const id = 999; - const [result] = await models.Client.hasCustomerRole(id, options); + const result = await models.Client.hasCustomerRole(id, options); - expect(result).toEqual(jasmine.objectContaining({isCustomer: 0})); + expect(result).toBeFalsy(); await tx.rollback(); } catch (e) { @@ -65,9 +65,9 @@ describe('Client hasCustomerRole', () => { const id = 'WRONG!'; - const [result] = await models.Client.hasCustomerRole(id, options); + const result = await models.Client.hasCustomerRole(id, options); - expect(result).toEqual(jasmine.objectContaining({isCustomer: 0})); + expect(result).toBeFalsy(); await tx.rollback(); } catch (e) { diff --git a/modules/client/front/web-access/index.js b/modules/client/front/web-access/index.js index 03c1292d06..a37e72a9dd 100644 --- a/modules/client/front/web-access/index.js +++ b/modules/client/front/web-access/index.js @@ -19,7 +19,7 @@ export default class Controller extends Section { isCustomer() { if (this.client.id) { this.$http.get(`Clients/${this.client.id}/hasCustomerRole`).then(res => { - this.canChangePassword = res.data && res.data.isCustomer; + this.canChangePassword = res.data && res.data; }); } } diff --git a/modules/client/front/web-access/index.spec.js b/modules/client/front/web-access/index.spec.js index 73c4f10436..00fa12781a 100644 --- a/modules/client/front/web-access/index.spec.js +++ b/modules/client/front/web-access/index.spec.js @@ -33,7 +33,7 @@ describe('Component VnClientWebAccess', () => { it('should return true if the password can be modified', () => { controller.client = {id: '1234'}; - $httpBackend.expectGET(`Clients/${controller.client.id}/hasCustomerRole`).respond({isCustomer: true}); + $httpBackend.expectGET(`Clients/${controller.client.id}/hasCustomerRole`).respond(true); controller.isCustomer(); $httpBackend.flush(); @@ -43,7 +43,7 @@ describe('Component VnClientWebAccess', () => { it(`should return a false if the password can't be modified`, () => { controller.client = {id: '1234'}; - $httpBackend.expectGET(`Clients/${controller.client.id}/hasCustomerRole`).respond({isCustomer: false}); + $httpBackend.expectGET(`Clients/${controller.client.id}/hasCustomerRole`).respond(false); controller.isCustomer(); $httpBackend.flush(); diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js index a787e48a1a..ac79f0d5d0 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -29,7 +29,7 @@ module.exports = Self => { const models = Self.app.models; const headers = ctx.req.headers; const origin = headers.origin; - const authorization = headers.authorization; + const auth = ctx.req.accessToken; if (process.env.NODE_ENV == 'test') throw new UserError(`Action not allowed on the test environment`); @@ -48,25 +48,30 @@ module.exports = Self => { let fileSrc; try { const invoiceOut = await Self.findById(id, null, myOptions); + const hasInvoicing = await models.Account.hasRole(auth.userId, 'invoicing', myOptions); + + if (invoiceOut.hasPdf && !hasInvoicing) + throw new UserError(`You don't have enough privileges`); + await invoiceOut.updateAttributes({ hasPdf: true }, myOptions); const response = got.stream(`${origin}/api/report/invoice`, { query: { - authorization: authorization, + authorization: auth.id, invoiceId: id } }); - const created = invoiceOut.created; - const year = created.getFullYear().toString(); - const month = created.getMonth().toString(); - const day = created.getDate().toString(); + const issued = invoiceOut.issued; + const year = issued.getFullYear().toString(); + const month = (issued.getMonth() + 1).toString(); + const day = issued.getDate().toString(); const container = await models.InvoiceContainer.container(year); const rootPath = container.client.root; - const fileName = `${invoiceOut.ref}.pdf`; + const fileName = `${year}${invoiceOut.ref}.pdf`; const src = path.join(rootPath, year, month, day); fileSrc = path.join(src, fileName); @@ -75,16 +80,11 @@ module.exports = Self => { if (tx) await tx.commit(); const writeStream = fs.createWriteStream(fileSrc); - writeStream.on('open', () => { - response.pipe(writeStream); - }); + writeStream.on('open', () => response.pipe(writeStream)); + writeStream.on('finish', () => writeStream.end()); return new Promise(resolve => { - writeStream.on('finish', () => { - writeStream.end(); - - resolve(invoiceOut); - }); + writeStream.on('close', () => resolve(invoiceOut)); }); } catch (e) { if (tx) await tx.rollback(); diff --git a/modules/invoiceOut/back/methods/invoiceOut/download.js b/modules/invoiceOut/back/methods/invoiceOut/download.js index 9836479823..f1138dd51d 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/download.js +++ b/modules/invoiceOut/back/methods/invoiceOut/download.js @@ -1,8 +1,9 @@ const fs = require('fs-extra'); const path = require('path'); +const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { - Self.remoteMethod('download', { + Self.remoteMethodCtx('download', { description: 'Download an invoice PDF', accessType: 'READ', accepts: [ @@ -34,34 +35,41 @@ module.exports = Self => { } }); - Self.download = async function(id, options) { + Self.download = async function(ctx, id, options) { const models = Self.app.models; const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); - const invoiceOut = await models.InvoiceOut.findById(id, null, myOptions); + try { + const invoiceOut = await models.InvoiceOut.findById(id, null, myOptions); - const created = invoiceOut.created; - const year = created.getFullYear().toString(); - const month = created.getMonth().toString(); - const day = created.getDate().toString(); + const issued = invoiceOut.issued; + const year = issued.getFullYear().toString(); + const month = (issued.getMonth() + 1).toString(); + const day = issued.getDate().toString(); - const container = await models.InvoiceContainer.container(year); - const rootPath = container.client.root; - const src = path.join(rootPath, year, month, day); - const fileName = `${invoiceOut.ref}.pdf`; - const fileSrc = path.join(src, fileName); + const container = await models.InvoiceContainer.container(year); + const rootPath = container.client.root; + const src = path.join(rootPath, year, month, day); + const fileName = `${year}${invoiceOut.ref}.pdf`; + const fileSrc = path.join(src, fileName); - const file = { - path: fileSrc, - contentType: 'application/pdf', - name: `${id}.pdf` - }; + const file = { + path: fileSrc, + contentType: 'application/pdf', + name: fileName + }; - await fs.access(file.path); - let stream = fs.createReadStream(file.path); - return [stream, file.contentType, `filename="${file.name}"`]; + await fs.access(file.path); + let stream = fs.createReadStream(file.path); + return [stream, file.contentType, `filename="${file.name}"`]; + } catch (error) { + if (error.code === 'ENOENT') + throw new UserError('The PDF document does not exists'); + + throw error; + } }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js index c053adde16..3a46311ad4 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js @@ -2,16 +2,27 @@ const models = require('vn-loopback/server/server').models; const fs = require('fs-extra'); describe('InvoiceOut download()', () => { - it('should return the downloaded fine name', async() => { + const userId = 9; + const invoiceId = 1; + const ctx = { + req: { + + accessToken: {userId: userId}, + headers: {origin: 'http://localhost:5000'}, + } + }; + + it('should return the downloaded file name', async() => { spyOn(models.InvoiceContainer, 'container').and.returnValue({ client: {root: '/path'} }); spyOn(fs, 'createReadStream').and.returnValue(new Promise(resolve => resolve('streamObject'))); spyOn(fs, 'access').and.returnValue(true); + spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true))); - const result = await models.InvoiceOut.download(1); + const result = await models.InvoiceOut.download(ctx, invoiceId); expect(result[1]).toEqual('application/pdf'); - expect(result[2]).toEqual('filename="1.pdf"'); + expect(result[2]).toEqual('filename="2021T1111111.pdf"'); }); }); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js index 2a530048e3..ededc5679b 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js @@ -61,9 +61,12 @@ describe('InvoiceOut filter()', () => { } }; + const invoiceOut = await models.InvoiceOut.findById(1, null, options); + await invoiceOut.updateAttribute('hasPdf', true, options); + const result = await models.InvoiceOut.filter(ctx, {}, options); - expect(result.length).toEqual(5); + expect(result.length).toEqual(1); await tx.rollback(); } catch (e) { diff --git a/modules/invoiceOut/front/card/index.html b/modules/invoiceOut/front/card/index.html index c70ac06e1c..a6f56f7755 100644 --- a/modules/invoiceOut/front/card/index.html +++ b/modules/invoiceOut/front/card/index.html @@ -1,5 +1,8 @@ - + + diff --git a/modules/invoiceOut/front/card/index.js b/modules/invoiceOut/front/card/index.js index 093fcdf669..85d03537b3 100644 --- a/modules/invoiceOut/front/card/index.js +++ b/modules/invoiceOut/front/card/index.js @@ -10,7 +10,8 @@ class Controller extends ModuleCard { 'issued', 'amount', 'clientFk', - 'companyFk' + 'companyFk', + 'hasPdf' ], include: [ { diff --git a/modules/invoiceOut/front/descriptor/index.html b/modules/invoiceOut/front/descriptor/index.html index 1da487d15a..4d9a4ffd88 100644 --- a/modules/invoiceOut/front/descriptor/index.html +++ b/modules/invoiceOut/front/descriptor/index.html @@ -33,11 +33,10 @@ - Regenerate invoice PDF + {{!$ctrl.invoiceOut.hasPdf ? 'Generate PDF invoice': 'Regenerate PDF invoice'}} @@ -101,8 +100,8 @@ + question="Are you sure you want to generate/regenerate the PDF invoice?" + message="Generate PDF invoice document"> diff --git a/modules/invoiceOut/front/descriptor/index.js b/modules/invoiceOut/front/descriptor/index.js index 5a535c9b52..0600ffa5b9 100644 --- a/modules/invoiceOut/front/descriptor/index.js +++ b/modules/invoiceOut/front/descriptor/index.js @@ -10,6 +10,10 @@ class Controller extends Descriptor { this.entity = value; } + get hasInvoicing() { + return this.aclService.hasAny(['invoicing']); + } + deleteInvoiceOut() { return this.$http.post(`InvoiceOuts/${this.id}/delete`) .then(() => this.$state.go('invoiceOut.index')) @@ -25,6 +29,7 @@ class Controller extends Descriptor { createInvoicePdf() { const invoiceId = this.invoiceOut.id; return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`) + .then(() => this.reload()) .then(() => { const snackbarMessage = this.$t( `The invoice PDF document has been regenerated`); @@ -60,6 +65,17 @@ class Controller extends Descriptor { .then(res => this.entity = res.data); } + reload() { + return this.loadData().then(() => { + if (this.cardReload) + this.cardReload(); + }); + } + + cardReload() { + // Prevents error when not defined + } + sendInvoice($data) { return this.vnEmail.send('invoice', { recipientId: this.invoiceOut.client.id, @@ -73,6 +89,7 @@ ngModule.vnComponent('vnInvoiceOutDescriptor', { template: require('./index.html'), controller: Controller, bindings: { - invoiceOut: '<' + invoiceOut: '<', + cardReload: '&' } }); diff --git a/modules/invoiceOut/front/descriptor/index.spec.js b/modules/invoiceOut/front/descriptor/index.spec.js index c16900a0ad..0a5494b9ab 100644 --- a/modules/invoiceOut/front/descriptor/index.spec.js +++ b/modules/invoiceOut/front/descriptor/index.spec.js @@ -18,6 +18,7 @@ describe('vnInvoiceOutDescriptor', () => { controller.invoiceOut = invoiceOut; + $httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond(); $httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond(); controller.createInvoicePdf(); $httpBackend.flush(); diff --git a/modules/invoiceOut/front/descriptor/locale/es.yml b/modules/invoiceOut/front/descriptor/locale/es.yml index ec9cd33100..3430fe4e70 100644 --- a/modules/invoiceOut/front/descriptor/locale/es.yml +++ b/modules/invoiceOut/front/descriptor/locale/es.yml @@ -12,5 +12,6 @@ Are you sure you want to clone this invoice?: Estas seguro de clonar esta factur Book invoice: Asentar factura InvoiceOut booked: Factura asentada Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? -Regenerate invoice PDF: Regenerar PDF factura +Generate PDF invoice: Generar PDF factura +Regenerate PDF invoice: Regenerar PDF factura The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado \ No newline at end of file diff --git a/modules/ticket/front/card/index.html b/modules/ticket/front/card/index.html index a7c70c484b..110ffeed40 100644 --- a/modules/ticket/front/card/index.html +++ b/modules/ticket/front/card/index.html @@ -1,5 +1,8 @@ - + + diff --git a/modules/ticket/front/descriptor-menu/index.html b/modules/ticket/front/descriptor-menu/index.html index 4f65fe0085..555246f9f2 100644 --- a/modules/ticket/front/descriptor-menu/index.html +++ b/modules/ticket/front/descriptor-menu/index.html @@ -80,12 +80,10 @@ - Regenerate invoice PDF + {{!$ctrl.ticket.invoiceOut.hasPdf ? 'Generate PDF invoice': 'Regenerate PDF invoice'}} + question="Are you sure you want to generate/regenerate the PDF invoice?" + message="Generate PDF invoice document"> diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js index 5da9544e2b..01709cf339 100644 --- a/modules/ticket/front/descriptor-menu/index.js +++ b/modules/ticket/front/descriptor-menu/index.js @@ -226,6 +226,7 @@ class Controller extends Section { createInvoicePdf() { const invoiceId = this.ticket.invoiceOut.id; return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`) + .then(() => this.reload()) .then(() => { const snackbarMessage = this.$t( `The invoice PDF document has been regenerated`); diff --git a/modules/ticket/front/descriptor-menu/index.spec.js b/modules/ticket/front/descriptor-menu/index.spec.js index b102b1f445..eabb772355 100644 --- a/modules/ticket/front/descriptor-menu/index.spec.js +++ b/modules/ticket/front/descriptor-menu/index.spec.js @@ -153,6 +153,7 @@ describe('Ticket Component vnTicketDescriptorMenu', () => { it('should make a query and show a success snackbar', () => { jest.spyOn(controller.vnApp, 'showSuccess'); + $httpBackend.whenGET(`Tickets/16`).respond(); $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond(); controller.createInvoicePdf(); $httpBackend.flush(); diff --git a/modules/ticket/front/descriptor/index.js b/modules/ticket/front/descriptor/index.js index 28d5eb953e..172e2767e6 100644 --- a/modules/ticket/front/descriptor/index.js +++ b/modules/ticket/front/descriptor/index.js @@ -18,6 +18,10 @@ class Controller extends Descriptor { super.entity = value; } + get hasInvoicing() { + return this.aclService.hasAny(['invoicing']); + } + loadData() { const filter = { include: [ diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml index c2b181c97e..c72b2e9233 100644 --- a/modules/ticket/front/descriptor/locale/es.yml +++ b/modules/ticket/front/descriptor/locale/es.yml @@ -21,8 +21,8 @@ Regenerate invoice PDF: Regenerar PDF factura The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado You are going to invoice this ticket: Vas a facturar este ticket Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket? -You are going to regenerate the invoice PDF document: Vas a regenerar el documento PDF de la factura -Are you sure you want to regenerate the invoice PDF document?: ¿Seguro que quieres regenerar el documento PDF de la factura? +Generate PDF invoice document: Generar PDF de la factura +Are you sure you want to generate/regenerate the PDF invoice?: ¿Seguro que quieres generar/regenerar el PDF de la factura? Shipped hour updated: Hora de envio modificada Deleted ticket: Ticket eliminado Recalculate components: Recalcular componentes