diff --git a/.eslintrc.yml b/.eslintrc.yml index 247319137..163ff22a7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -31,3 +31,4 @@ rules: curly: [error, multi-or-nest] indent: [error, 4] arrow-parens: [error, as-needed] + jasmine/no-focused-tests: 0 diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 41246bd13..d163c809d 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -330,7 +330,7 @@ export default { moreMenuDeleteStowawayButton: 'vn-ticket-descriptor vn-drop-down > vn-popover ul > li:nth-child(5)', moreMenuAddToTurn: `vn-ticket-descriptor vn-drop-down > vn-popover ul > li:nth-child(2)`, moreMenuDeleteTicket: `vn-ticket-descriptor vn-drop-down > vn-popover ul > li:nth-child(3)`, - moreMenuMakeInvoice: 'vn-ticket-descriptor vn-drop-down > vn-popover ul > li:nth-child(5)', + moreMenuMakeInvoice: 'vn-ticket-descriptor vn-drop-down > vn-popover ul > li[name="Make invoice"]', addStowawayDialogSecondTicket: 'vn-ticket-descriptor > vn-add-stowaway > vn-dialog vn-table vn-tr:nth-child(2)', shipSelectButton: 'vn-ticket-descriptor > div > div.body > div.quicklinks > vn-button-menu[icon="icon-stowaway"]', shipButton: 'vn-ticket-descriptor > div > div.body > div.quicklinks vn-icon[icon="icon-stowaway"]', @@ -339,7 +339,7 @@ export default { saturdayButton: `vn-ticket-descriptor > vn-dialog > div > form > div.body > tpl-body > div > vn-tool-bar > vn-button:nth-child(6)`, closeStowawayDialog: 'vn-ticket-descriptor > vn-add-stowaway > vn-dialog > div > button[class="close"]', acceptDeleteButton: 'vn-ticket-descriptor button[response="ACCEPT"]', - acceptInvoiceOutButton: 'vn-ticket-descriptor vn-confirm[vn-id="invoiceMakeConfirmation"] button[response="ACCEPT"]', + acceptInvoiceOutButton: 'vn-ticket-descriptor vn-confirm[vn-id="makeInvoiceConfirmation"] button[response="ACCEPT"]', acceptDeleteStowawayButton: 'vn-ticket-descriptor > vn-remove-stowaway button[response="ACCEPT"]' }, ticketNotes: { diff --git a/front/core/components/drop-down/drop-down.js b/front/core/components/drop-down/drop-down.js index 123df1994..af9fc0bfa 100755 --- a/front/core/components/drop-down/drop-down.js +++ b/front/core/components/drop-down/drop-down.js @@ -285,6 +285,8 @@ export default class DropDown extends Component { } let li = this.document.createElement('li'); + + li.setAttribute('name', option[this.showField]); fragment.appendChild(li); if (this.multiple) { diff --git a/modules/invoiceOut/back/methods/invoiceOut/regenerate.js b/modules/invoiceOut/back/methods/invoiceOut/regenerate.js new file mode 100644 index 000000000..cf95d5af9 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/regenerate.js @@ -0,0 +1,48 @@ +module.exports = Self => { + Self.remoteMethodCtx('regenerate', { + description: 'Sends an invoice to a regeneration queue', + accepts: [{ + arg: 'id', + type: 'number', + required: true, + description: 'The invoiceOut id', + http: {source: 'path'} + }], + returns: { + type: 'object', + root: true + }, + http: { + path: `/:id/regenerate`, + verb: 'POST' + } + }); + + Self.regenerate = async(ctx, id) => { + const userId = ctx.req.accessToken.userId; + const models = Self.app.models; + const invoiceReportFk = 30; // FIXME - Should be deprecated + const worker = await models.Worker.findOne({where: {userFk: userId}}); + const transaction = await Self.beginTransaction({}); + + try { + // Remove all invoice references from tickets + const invoiceOut = await models.InvoiceOut.findById(id, {transaction}); + await invoiceOut.updateAttributes({ + hasPdf: false + }); + + // Send to print queue + await Self.rawSql(` + INSERT INTO vn.printServerQueue (reportFk, param1, workerFk) + VALUES (?, ?, ?)`, [invoiceReportFk, id, worker.id], {transaction}); + + await transaction.commit(); + + return invoiceOut; + } catch (e) { + await transaction.rollback(); + throw e; + } + }; +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js new file mode 100644 index 000000000..39e5f5171 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js @@ -0,0 +1,39 @@ +const app = require('vn-loopback/server/server'); + +describe('invoiceOut regenerate()', () => { + const invoiceReportFk = 30; // FIXME - Should be deprecated + const invoiceOutId = 1; + + afterAll(async done => { + const invoiceOut = await app.models.InvoiceOut.findById(invoiceOutId); + await invoiceOut.updateAttributes({hasPdf: true}); + await app.models.InvoiceOut.rawSql(` + DELETE FROM vn.printServerQueue + WHERE reportFk = ?`, [invoiceReportFk]); + + done(); + }); + + it('should check that the invoice has a PDF and is not in print generation queue', async() => { + const invoiceOut = await app.models.InvoiceOut.findById(invoiceOutId); + const [queue] = await app.models.InvoiceOut.rawSql(` + SELECT COUNT(*) AS total + FROM vn.printServerQueue + WHERE reportFk = ?`, [invoiceReportFk]); + + expect(invoiceOut.hasPdf).toBeTruthy(); + expect(queue.total).toEqual(0); + }); + + it(`should mark the invoice as doesn't have PDF and add it to a print queue`, async() => { + const ctx = {req: {accessToken: {userId: 5}}}; + const invoiceOut = await app.models.InvoiceOut.regenerate(ctx, invoiceOutId); + const [queue] = await app.models.InvoiceOut.rawSql(` + SELECT COUNT(*) AS total + FROM vn.printServerQueue + WHERE reportFk = ?`, [invoiceReportFk]); + + expect(invoiceOut.hasPdf).toBeFalsy(); + expect(queue.total).toEqual(1); + }); +}); diff --git a/modules/invoiceOut/back/models/invoiceOut.js b/modules/invoiceOut/back/models/invoiceOut.js index 2d7c691fc..726f26540 100644 --- a/modules/invoiceOut/back/models/invoiceOut.js +++ b/modules/invoiceOut/back/models/invoiceOut.js @@ -2,4 +2,5 @@ module.exports = Self => { require('../methods/invoiceOut/filter')(Self); require('../methods/invoiceOut/summary')(Self); require('../methods/invoiceOut/download')(Self); + require('../methods/invoiceOut/regenerate')(Self); }; diff --git a/modules/invoiceOut/back/models/invoiceOut.json b/modules/invoiceOut/back/models/invoiceOut.json index 7990949e9..0d8881de2 100644 --- a/modules/invoiceOut/back/models/invoiceOut.json +++ b/modules/invoiceOut/back/models/invoiceOut.json @@ -35,7 +35,7 @@ "type": "date" }, "hasPdf": { - "type": "Number", + "type": "Boolean", "mysql": { "columnName": "pdf" } diff --git a/modules/ticket/front/card/index.js b/modules/ticket/front/card/index.js index 9c1e45e78..2386b1bb6 100644 --- a/modules/ticket/front/card/index.js +++ b/modules/ticket/front/card/index.js @@ -7,6 +7,7 @@ class Controller { this.filter = { include: [ {relation: 'warehouse', scope: {fields: ['name']}}, + {relation: 'invoiceOut', scope: {fields: ['id']}}, {relation: 'address'}, {relation: 'ship'}, {relation: 'agencyMode', scope: {fields: ['name']}}, diff --git a/modules/ticket/front/descriptor/index.html b/modules/ticket/front/descriptor/index.html index 326a72695..6e77365c1 100644 --- a/modules/ticket/front/descriptor/index.html +++ b/modules/ticket/front/descriptor/index.html @@ -180,13 +180,22 @@ + + + + + \ No newline at end of file diff --git a/modules/ticket/front/descriptor/index.js b/modules/ticket/front/descriptor/index.js index 5d1c6253d..9578b7128 100644 --- a/modules/ticket/front/descriptor/index.js +++ b/modules/ticket/front/descriptor/index.js @@ -9,15 +9,34 @@ class Controller { this.$translate = $translate; this.aclService = aclService; this.moreOptions = [ - {callback: this.showAddTurnDialog, name: 'Add turn'}, - {callback: this.showAddStowaway, name: 'Add stowaway', show: () => this.isTicketModule()}, - {callback: this.showRemoveStowaway, name: 'Remove stowaway', show: () => this.shouldShowRemoveStowaway()}, - {callback: this.showInvoiceOutMakeDialog, name: 'Make invoice', acl: 'invoicing'}, - {callback: this.showDeliveryNote, name: 'Show Delivery Note'}, - {callback: this.showDeleteTicketDialog, name: 'Delete ticket'}, - {callback: this.showChangeShipped, name: 'Change shipped hour'}, - {callback: this.showSMSDialog, name: 'Send SMS'}, - {callback: this.openRptRoute, name: 'Show pallet report'} + {name: 'Add turn', callback: this.showAddTurnDialog}, + {name: 'Show Delivery Note', callback: this.showDeliveryNote}, + {name: 'Delete ticket', callback: this.showDeleteTicketDialog}, + {name: 'Change shipped hour', callback: this.showChangeShipped}, + {name: 'Send SMS', callback: this.showSMSDialog}, + {name: 'Show pallet report', callback: this.openRptRoute}, + { + name: 'Add stowaway', + callback: this.showAddStowaway, + show: () => this.isTicketModule() + }, + { + name: 'Remove stowaway', + callback: this.showRemoveStowaway, + show: () => this.shouldShowRemoveStowaway() + }, + { + name: 'Make invoice', + acl: 'invoicing', + callback: this.showMakeInvoiceDialog, + show: () => !this.hasInvoice() + }, + { + name: 'Regenerate invoice', + acl: 'invoicing', + callback: this.showRegenerateInvoiceDialog, + show: () => this.hasInvoice() + }, ]; } @@ -189,8 +208,8 @@ class Controller { /** * Shows an invoice confirmation */ - showInvoiceOutMakeDialog() { - this.$scope.invoiceMakeConfirmation.show(); + showMakeInvoiceDialog() { + this.$scope.makeInvoiceConfirmation.show(); } /** @@ -199,7 +218,7 @@ class Controller { * * @param {String} response - Response result */ - makeInvoiceOut(response) { + makeInvoice(response) { if (response === 'ACCEPT') { const query = `/ticket/api/Tickets/${this.ticket.id}/makeInvoice`; this.$http.post(query).then(() => { @@ -208,6 +227,40 @@ class Controller { }); } } + + /** + * Shows an invoice confirmation + */ + showRegenerateInvoiceDialog() { + this.$scope.regenerateInvoiceConfirmation.show(); + } + + /** + * Sends an invoice to a regeneration queue + * for the current ticket + * + * @param {String} response - Response result + */ + regenerateInvoice(response) { + if (response === 'ACCEPT') { + const invoiceId = this.ticket.invoiceOut.id; + const query = `/invoiceOut/api/InvoiceOuts/${invoiceId}/regenerate`; + this.$http.post(query).then(() => { + const snackbarMessage = this.$translate.instant( + `Invoice sent for a regeneration, will be available in a few minutes`); + this.vnApp.showSuccess(snackbarMessage); + }); + } + } + + /** + * Returns if the current ticket + * is already invoiced + * @return {Boolean} - True if invoiced + */ + hasInvoice() { + return this.ticket.refFk !== null; + } } Controller.$inject = ['$state', '$scope', '$http', 'vnApp', '$translate', 'aclService']; diff --git a/modules/ticket/front/descriptor/index.spec.js b/modules/ticket/front/descriptor/index.spec.js index 164ad8009..c799b16ec 100644 --- a/modules/ticket/front/descriptor/index.spec.js +++ b/modules/ticket/front/descriptor/index.spec.js @@ -9,7 +9,7 @@ describe('Ticket Component vnTicketDescriptor', () => { beforeEach(angular.mock.inject(($componentController, _$httpBackend_) => { $httpBackend = _$httpBackend_; controller = $componentController('vnTicketDescriptor'); - controller.ticket = {id: 2}; + controller.ticket = {id: 2, invoiceOut: {id: 1}}; })); describe('showAddTurnDialog()', () => { @@ -78,19 +78,32 @@ describe('Ticket Component vnTicketDescriptor', () => { }); }); - describe('invoiceMakeOut(response)', () => { + describe('makeInvoice(response)', () => { it('should make a query and call $state.reload() method if the response is ACCEPT', () => { spyOn(controller.$state, 'reload'); spyOn(controller.vnApp, 'showSuccess'); $httpBackend.when('POST', `/ticket/api/Tickets/2/makeInvoice`).respond(); $httpBackend.expect('POST', `/ticket/api/Tickets/2/makeInvoice`).respond(); - controller.makeInvoiceOut('ACCEPT'); + controller.makeInvoice('ACCEPT'); $httpBackend.flush(); expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Ticket invoiced'); expect(controller.$state.reload).toHaveBeenCalledWith(); }); }); + + describe('regenerateInvoice(response)', () => { + it('should make a query and show a success snackbar if the response is ACCEPT', () => { + spyOn(controller.vnApp, 'showSuccess'); + + $httpBackend.when('POST', `/invoiceOut/api/InvoiceOuts/1/regenerate`).respond(); + $httpBackend.expect('POST', `/invoiceOut/api/InvoiceOuts/1/regenerate`).respond(); + controller.regenerateInvoice('ACCEPT'); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Invoice sent for a regeneration, will be available in a few minutes'); + }); + }); }); diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml index a661c239f..1b6baed2a 100644 --- a/modules/ticket/front/descriptor/locale/es.yml +++ b/modules/ticket/front/descriptor/locale/es.yml @@ -15,6 +15,10 @@ SMSPayment: >- Verdnatura le comunica: Su pedido está pendiente de pago. Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias. Ticket invoiced: Ticket facturado -Make invoice: Facturar +Make invoice: Crear factura +Regenerate invoice: Regenerar factura 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? \ No newline at end of file +Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket? +You are going to regenerate the invoice: Vas a regenerar la factura +Are you sure you want to regenerate the invoice?: ¿Seguro que quieres regenerar la factura? +Invoice sent for a regeneration, will be available in a few minutes: La factura ha sido enviada para ser regenerada, estará disponible en unos minutos \ No newline at end of file