diff --git a/db/changes/10480-june/00-ACL.sql b/db/changes/10480-june/00-ACL.sql index 2e863be34..b13e56e21 100644 --- a/db/changes/10480-june/00-ACL.sql +++ b/db/changes/10480-june/00-ACL.sql @@ -1,6 +1,21 @@ INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId) VALUES + ('InvoiceOut','refund','WRITE','ALLOW','ROLE','invoicing'), + ('InvoiceOut','refund','WRITE','ALLOW','ROLE','salesAssistant'), + ('InvoiceOut','refund','WRITE','ALLOW','ROLE','claimManager'), + ('Ticket','refund','WRITE','ALLOW','ROLE','invoicing'), + ('Ticket','refund','WRITE','ALLOW','ROLE','salesAssistant'), + ('Ticket','refund','WRITE','ALLOW','ROLE','claimManager'), + ('Sale','refund','WRITE','ALLOW','ROLE','salesAssistant'), + ('Sale','refund','WRITE','ALLOW','ROLE','claimManager'), + ('TicketRefund','*','WRITE','ALLOW','ROLE','invoicing'), ('ClaimObservation','*','WRITE','ALLOW','ROLE','salesPerson'), ('ClaimObservation','*','READ','ALLOW','ROLE','salesPerson'), ('Client','setPassword','WRITE','ALLOW','ROLE','salesPerson'), ('Client','updateUser','WRITE','ALLOW','ROLE','salesPerson'); + +DELETE FROM `salix`.`ACL` WHERE id=313; + +UPDATE `salix`.`ACL` + SET principalId='invoicing' + WHERE id=297; \ No newline at end of file diff --git a/db/changes/10480-june/00-ticketRefund_beforeUpsert.sql b/db/changes/10480-june/00-ticketRefund_beforeUpsert.sql new file mode 100644 index 000000000..e6506c5d7 --- /dev/null +++ b/db/changes/10480-june/00-ticketRefund_beforeUpsert.sql @@ -0,0 +1,21 @@ +DROP PROCEDURE IF EXISTS `vn`.`ticketRefund_beforeUpsert`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticketRefund_beforeUpsert`(vRefundTicketFk INT, vOriginalTicketFk INT) +BEGIN + DECLARE vAlreadyExists BOOLEAN DEFAULT FALSE; + + IF vRefundTicketFk = vOriginalTicketFk THEN + CALL util.throw('Original ticket and refund ticket has same id'); + END IF; + + SELECT COUNT(*) INTO vAlreadyExists + FROM ticketRefund + WHERE originalTicketFk = vOriginalTicketFk; + + IF vAlreadyExists > 0 THEN + CALL util.throw('This ticket is already a refund'); + END IF; +END$$ +DELIMITER ; diff --git a/e2e/paths/05-ticket/13_services.spec.js b/e2e/paths/05-ticket/13_services.spec.js index 03e57b588..50df23582 100644 --- a/e2e/paths/05-ticket/13_services.spec.js +++ b/e2e/paths/05-ticket/13_services.spec.js @@ -23,9 +23,9 @@ describe('Ticket services path', () => { await page.waitForClassPresent(selectors.ticketService.firstAddServiceTypeButton, 'disabled'); await page.waitToClick(selectors.ticketService.addServiceButton); await page.waitForSelector(selectors.ticketService.firstAddServiceTypeButton); - const result = await page.isDisabled(selectors.ticketService.firstAddServiceTypeButton); + const disabled = await page.isDisabled(selectors.ticketService.firstAddServiceTypeButton); - expect(result).toBe(true); + expect(disabled).toBe(true); }); it('should receive an error if you attempt to save a service without access rights', async() => { diff --git a/modules/invoiceIn/front/tax/index.spec.js b/modules/invoiceIn/front/tax/index.spec.js index c62ada9ca..52114afe5 100644 --- a/modules/invoiceIn/front/tax/index.spec.js +++ b/modules/invoiceIn/front/tax/index.spec.js @@ -1,7 +1,6 @@ import './index.js'; import watcher from 'core/mocks/watcher'; import crudModel from 'core/mocks/crud-model'; -const UserError = require('vn-loopback/util/user-error'); describe('InvoiceIn', () => { describe('Component tax', () => { diff --git a/modules/invoiceOut/back/methods/invoiceOut/download.js b/modules/invoiceOut/back/methods/invoiceOut/download.js index f1138dd51..19dea5b1a 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/download.js +++ b/modules/invoiceOut/back/methods/invoiceOut/download.js @@ -9,7 +9,7 @@ module.exports = Self => { accepts: [ { arg: 'id', - type: 'String', + type: 'string', description: 'The invoice id', http: {source: 'path'} } @@ -21,16 +21,16 @@ module.exports = Self => { root: true }, { arg: 'Content-Type', - type: 'String', + type: 'string', http: {target: 'header'} }, { arg: 'Content-Disposition', - type: 'String', + type: 'string', http: {target: 'header'} } ], http: { - path: `/:id/download`, + path: '/:id/download', verb: 'GET' } }); diff --git a/modules/invoiceOut/back/methods/invoiceOut/refund.js b/modules/invoiceOut/back/methods/invoiceOut/refund.js new file mode 100644 index 000000000..7ad6b03ec --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/refund.js @@ -0,0 +1,49 @@ +module.exports = Self => { + Self.remoteMethod('refund', { + description: 'Create refund tickets with sales and services if provided', + accessType: 'WRITE', + accepts: [{ + arg: 'ref', + type: 'string', + description: 'The invoice reference' + }], + returns: { + type: ['number'], + root: true + }, + http: { + path: '/refund', + verb: 'post' + } + }); + + Self.refund = async(ref, options) => { + const models = Self.app.models; + const myOptions = {}; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const filter = {where: {refFk: ref}}; + const tickets = await models.Ticket.find(filter, myOptions); + + const ticketsIds = tickets.map(ticket => ticket.id); + + const refundedTickets = await models.Ticket.refund(ticketsIds, myOptions); + + if (tx) await tx.commit(); + + return refundedTickets; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js new file mode 100644 index 000000000..628318d42 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js @@ -0,0 +1,28 @@ +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('InvoiceOut refund()', () => { + const userId = 5; + const activeCtx = { + accessToken: {userId: userId}, + }; + + it('should return the ids for the created refund tickets', async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + const result = await models.InvoiceOut.refund('T1111111', options); + + expect(result.length).toEqual(2); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index 3b2822ada..c8c97702f 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -8,4 +8,5 @@ module.exports = Self => { require('../methods/invoiceOut/createPdf')(Self); require('../methods/invoiceOut/createManualInvoice')(Self); require('../methods/invoiceOut/globalInvoicing')(Self); + require('../methods/invoiceOut/refund')(Self); }; diff --git a/modules/invoiceOut/front/descriptor-menu/index.html b/modules/invoiceOut/front/descriptor-menu/index.html index ef4c9a62e..1c0919288 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.html +++ b/modules/invoiceOut/front/descriptor-menu/index.html @@ -80,6 +80,8 @@ ng-click="refundConfirmation.show()" name="refundInvoice" vn-tooltip="Create a single ticket with all the content of the current invoice" + vn-acl="invoicing, claimManager, salesAssistant" + vn-acl-action="remove" translate> Refund diff --git a/modules/invoiceOut/front/descriptor-menu/index.js b/modules/invoiceOut/front/descriptor-menu/index.js index b884e50cb..8d8a052cf 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.js +++ b/modules/invoiceOut/front/descriptor-menu/index.js @@ -117,32 +117,12 @@ class Controller extends Section { }); } - async refundInvoiceOut() { - let filter = { - where: {refFk: this.invoiceOut.ref} - }; - const tickets = await this.$http.get('Tickets', {filter}); - this.tickets = tickets.data; - this.ticketsIds = []; - for (let ticket of this.tickets) - this.ticketsIds.push(ticket.id); - - filter = { - where: {ticketFk: {inq: this.ticketsIds}} - }; - const sales = await this.$http.get('Sales', {filter}); - this.sales = sales.data; - - const ticketServices = await this.$http.get('TicketServices', {filter}); - this.services = ticketServices.data; - - const params = { - sales: this.sales, - services: this.services - }; - const query = `Sales/refund`; - return this.$http.post(query, params).then(res => { - this.$state.go('ticket.card.sale', {id: res.data}); + refundInvoiceOut() { + const query = 'InvoiceOuts/refund'; + const params = {ref: this.invoiceOut.ref}; + this.$http.post(query, params).then(res => { + const ticketIds = res.data.map(ticket => ticket.id).join(', '); + this.vnApp.showSuccess(this.$t('The following refund tickets have been created', {ticketIds})); }); } } diff --git a/modules/invoiceOut/front/descriptor-menu/index.spec.js b/modules/invoiceOut/front/descriptor-menu/index.spec.js index c84c97a57..e91196381 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.spec.js +++ b/modules/invoiceOut/front/descriptor-menu/index.spec.js @@ -15,14 +15,17 @@ describe('vnInvoiceOutDescriptorMenu', () => { $httpBackend = _$httpBackend_; $httpParamSerializer = _$httpParamSerializer_; controller = $componentController('vnInvoiceOutDescriptorMenu', {$element: null}); + controller.invoiceOut = { + id: 1, + ref: 'T1111111', + client: {id: 1101} + }; })); describe('createPdfInvoice()', () => { it('should make a query to the createPdf() endpoint and show a success snackbar', () => { jest.spyOn(controller.vnApp, 'showSuccess'); - controller.invoiceOut = invoiceOut; - $httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond(); $httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond(); controller.createPdfInvoice(); @@ -36,8 +39,6 @@ describe('vnInvoiceOutDescriptorMenu', () => { it('should make a query to the csv invoice download endpoint and show a message snackbar', () => { jest.spyOn(window, 'open').mockReturnThis(); - controller.invoiceOut = invoiceOut; - const expectedParams = { invoiceId: invoiceOut.id, recipientId: invoiceOut.client.id @@ -52,7 +53,6 @@ describe('vnInvoiceOutDescriptorMenu', () => { describe('deleteInvoiceOut()', () => { it(`should make a query and call showSuccess()`, () => { - controller.invoiceOut = invoiceOut; controller.$state.reload = jest.fn(); jest.spyOn(controller.vnApp, 'showSuccess'); @@ -65,7 +65,6 @@ describe('vnInvoiceOutDescriptorMenu', () => { }); it(`should make a query and call showSuccess() after state.go if the state wasn't in invoiceOut module`, () => { - controller.invoiceOut = invoiceOut; jest.spyOn(controller.$state, 'go').mockReturnValue('ok'); jest.spyOn(controller.vnApp, 'showSuccess'); controller.$state.current.name = 'invoiceOut.card.something'; @@ -83,8 +82,6 @@ describe('vnInvoiceOutDescriptorMenu', () => { it('should make a query to the email invoice endpoint and show a message snackbar', () => { jest.spyOn(controller.vnApp, 'showMessage'); - controller.invoiceOut = invoiceOut; - const $data = {email: 'brucebanner@gothamcity.com'}; const expectedParams = { invoiceId: invoiceOut.id, @@ -105,8 +102,6 @@ describe('vnInvoiceOutDescriptorMenu', () => { it('should make a query to the csv invoice send endpoint and show a message snackbar', () => { jest.spyOn(controller.vnApp, 'showMessage'); - controller.invoiceOut = invoiceOut; - const $data = {email: 'brucebanner@gothamcity.com'}; const expectedParams = { invoiceId: invoiceOut.id, @@ -123,33 +118,16 @@ describe('vnInvoiceOutDescriptorMenu', () => { }); }); - // #4084 review with Juan - xdescribe('refundInvoiceOut()', () => { - it('should make a query and go to ticket.card.sale', () => { - controller.$state.go = jest.fn(); + describe('refundInvoiceOut()', () => { + it('should make a query and show a success message', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + const params = {ref: controller.invoiceOut.ref}; - const invoiceOut = { - id: 1, - ref: 'T1111111' - }; - controller.invoiceOut = invoiceOut; - const tickets = [{id: 1}]; - const sales = [{id: 1}]; - const services = [{id: 2}]; - - $httpBackend.expectGET(`Tickets`).respond(tickets); - $httpBackend.expectGET(`Sales`).respond(sales); - $httpBackend.expectGET(`TicketServices`).respond(services); - - const expectedParams = { - sales: sales, - services: services - }; - $httpBackend.expectPOST(`Sales/refund`, expectedParams).respond(); + $httpBackend.expectPOST(`InvoiceOuts/refund`, params).respond([{id: 1}, {id: 2}]); controller.refundInvoiceOut(); $httpBackend.flush(); - expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: undefined}); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); }); }); }); diff --git a/modules/invoiceOut/front/descriptor-menu/locale/en.yml b/modules/invoiceOut/front/descriptor-menu/locale/en.yml new file mode 100644 index 000000000..d299155d7 --- /dev/null +++ b/modules/invoiceOut/front/descriptor-menu/locale/en.yml @@ -0,0 +1 @@ +The following refund tickets have been created: "The following refund tickets have been created: {{ticketIds}}" diff --git a/modules/invoiceOut/front/descriptor-menu/locale/es.yml b/modules/invoiceOut/front/descriptor-menu/locale/es.yml index 8949f1f91..488c1a3f8 100644 --- a/modules/invoiceOut/front/descriptor-menu/locale/es.yml +++ b/modules/invoiceOut/front/descriptor-menu/locale/es.yml @@ -17,3 +17,4 @@ Create a single ticket with all the content of the current invoice: Crear un tic Regenerate PDF invoice: Regenerar PDF factura The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado The email can't be empty: El correo no puede estar vacío +The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}" diff --git a/modules/ticket/back/methods/sale/refund.js b/modules/ticket/back/methods/sale/refund.js index 703fb4ba7..66c0550e0 100644 --- a/modules/ticket/back/methods/sale/refund.js +++ b/modules/ticket/back/methods/sale/refund.js @@ -1,23 +1,20 @@ -const UserError = require('vn-loopback/util/user-error'); - module.exports = Self => { - Self.remoteMethodCtx('refund', { - description: 'Create ticket refund with lines and services changing the sign to the quantites', + Self.remoteMethod('refund', { + description: 'Create refund tickets with sales and services if provided', accessType: 'WRITE', - accepts: [{ - arg: 'sales', - description: 'The sales', - type: ['object'], - required: false - }, - { - arg: 'services', - type: ['object'], - required: false, - description: 'The services' - }], + accepts: [ + { + arg: 'salesIds', + type: ['number'], + required: true + }, + { + arg: 'servicesIds', + type: ['number'] + }, + ], returns: { - type: 'number', + type: ['number'], root: true }, http: { @@ -26,7 +23,8 @@ module.exports = Self => { } }); - Self.refund = async(ctx, sales, services, options) => { + Self.refund = async(salesIds, servicesIds, options) => { + const models = Self.app.models; const myOptions = {}; let tx; @@ -39,56 +37,103 @@ module.exports = Self => { } try { - const userId = ctx.req.accessToken.userId; + const refundAgencyMode = await models.AgencyMode.findOne({ + include: { + relation: 'zones', + scope: { + limit: 1, + field: ['id', 'name'] + } + }, + where: {code: 'refund'} + }, myOptions); - const isClaimManager = await Self.app.models.Account.hasRole(userId, 'claimManager'); - const isSalesAssistant = await Self.app.models.Account.hasRole(userId, 'salesAssistant'); - const hasValidRole = isClaimManager || isSalesAssistant; + const refoundZoneId = refundAgencyMode.zones()[0].id; - if (!hasValidRole) - throw new UserError(`You don't have privileges to create refund`); + const salesFilter = { + where: {id: {inq: salesIds}}, + include: { + relation: 'components', + scope: { + fields: ['saleFk', 'componentFk', 'value'] + } + } + }; + const sales = await models.Sale.find(salesFilter, myOptions); + const ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))]; - const salesIds = []; - if (sales) { - for (let sale of sales) - salesIds.push(sale.id); - } else - salesIds.push(null); + const refundTickets = []; - const servicesIds = []; - if (services && services.length) { - for (let service of services) - servicesIds.push(service.id); - } else - servicesIds.push(null); + const now = new Date(); + const mappedTickets = new Map(); - const query = ` - DROP TEMPORARY TABLE IF EXISTS tmp.sale; - DROP TEMPORARY TABLE IF EXISTS tmp.ticketService; + for (let ticketId of ticketsIds) { + const filter = {include: {relation: 'address'}}; + const ticket = await models.Ticket.findById(ticketId, filter, myOptions); - CREATE TEMPORARY TABLE tmp.sale - SELECT s.id, s.itemFk, s.quantity, s.concept, s.price, s.discount, s.ticketFk - FROM sale s - WHERE s.id IN (?); + const refundTicket = await models.Ticket.create({ + clientFk: ticket.clientFk, + shipped: now, + addressFk: ticket.address().id, + agencyModeFk: refundAgencyMode.id, + nickname: ticket.address().nickname, + warehouseFk: ticket.warehouseFk, + companyFk: ticket.companyFk, + landed: now, + zoneFk: refoundZoneId + }, myOptions); - CREATE TEMPORARY TABLE tmp.ticketService - SELECT ts.description, ts.quantity, ts.price, ts.taxClassFk, ts.ticketServiceTypeFk, ts.ticketFk - FROM ticketService ts - WHERE ts.id IN (?); + refundTickets.push(refundTicket); - CALL vn.ticket_doRefund(@newTicket); + mappedTickets.set(ticketId, refundTicket.id); - DROP TEMPORARY TABLE tmp.sale; - DROP TEMPORARY TABLE tmp.ticketService;`; + await models.TicketRefund.create({ + refundTicketFk: refundTicket.id, + originalTicketFk: ticket.id, + }, myOptions); + } - await Self.rawSql(query, [salesIds, servicesIds], myOptions); + for (const sale of sales) { + const refundTicketId = mappedTickets.get(sale.ticketFk); + const createdSale = await models.Sale.create({ + ticketFk: refundTicketId, + itemFk: sale.itemFk, + quantity: sale.quantity, + concept: sale.concept, + price: sale.price, + discount: sale.discount, + }, myOptions); - const [newTicket] = await Self.rawSql('SELECT @newTicket id', null, myOptions); - const newTicketId = newTicket.id; + const components = sale.components(); + for (const component of components) + component.saleFk = createdSale.id; + + await models.SaleComponent.create(components, myOptions); + } + + if (servicesIds && servicesIds.length > 0) { + const servicesFilter = { + where: {id: {inq: servicesIds}} + }; + const services = await models.TicketService.find(servicesFilter, myOptions); + + for (const service of services) { + const refundTicketId = mappedTickets.get(service.ticketFk); + + await models.TicketService.create({ + description: service.description, + quantity: service.quantity, + price: service.price, + taxClassFk: service.taxClassFk, + ticketFk: refundTicketId, + ticketServiceTypeFk: service.ticketServiceTypeFk, + }, myOptions); + } + } if (tx) await tx.commit(); - return newTicketId; + return refundTickets; } catch (e) { if (tx) await tx.rollback(); throw e; diff --git a/modules/ticket/back/methods/sale/specs/refund.spec.js b/modules/ticket/back/methods/sale/specs/refund.spec.js index 5cb353055..74077cf29 100644 --- a/modules/ticket/back/methods/sale/specs/refund.spec.js +++ b/modules/ticket/back/methods/sale/specs/refund.spec.js @@ -1,23 +1,30 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('sale refund()', () => { - const sales = [ - {id: 7, ticketFk: 11}, - {id: 8, ticketFk: 11} - ]; - const services = [{id: 1}]; + const userId = 5; + const activeCtx = { + accessToken: {userId: userId}, + }; - it('should create ticket with the selected lines changing the sign to the quantites', async() => { + const servicesIds = [3]; + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + + it('should create ticket with the selected lines', async() => { const tx = await models.Sale.beginTransaction({}); - const ctx = {req: {accessToken: {userId: 9}}}; + const salesIds = [7, 8]; try { const options = {transaction: tx}; - const response = await models.Sale.refund(ctx, sales, services, options); - const [newTicketId] = await models.Sale.rawSql('SELECT MAX(t.id) id FROM vn.ticket t;', null, options); + const response = await models.Sale.refund(salesIds, servicesIds, options); - expect(response).toEqual(newTicketId.id); + expect(response.length).toBeGreaterThanOrEqual(1); await tx.rollback(); } catch (e) { @@ -26,24 +33,53 @@ describe('sale refund()', () => { } }); - it(`should throw an error if the user doesn't have privileges to create a refund`, async() => { + it('should create a ticket for each unique ticketFk in the sales', async() => { const tx = await models.Sale.beginTransaction({}); - const ctx = {req: {accessToken: {userId: 1}}}; - - let error; + const salesIds = [6, 7]; try { const options = {transaction: tx}; - await models.Sale.refund(ctx, sales, services, options); + const tickets = await models.Sale.refund(salesIds, servicesIds, options); + + const ticketsIds = tickets.map(ticket => ticket.id); + + const refundedTickets = await models.Ticket.find({ + where: { + id: { + inq: ticketsIds + } + }, + include: [ + { + relation: 'ticketSales', + scope: { + include: { + relation: 'components' + } + } + }, + { + relation: 'ticketServices', + } + ] + }, options); + + const firstRefoundedTicket = refundedTickets[0]; + const secondRefoundedTicket = refundedTickets[1]; + const salesLength = firstRefoundedTicket.ticketSales().length; + const componentsLength = firstRefoundedTicket.ticketSales()[0].components().length; + const servicesLength = secondRefoundedTicket.ticketServices().length; + + expect(refundedTickets.length).toEqual(2); + expect(salesLength).toEqual(1); + expect(componentsLength).toEqual(4); + expect(servicesLength).toBeGreaterThanOrEqual(1); await tx.rollback(); } catch (e) { await tx.rollback(); - error = e; + throw e; } - - expect(error).toBeDefined(); - expect(error.message).toEqual(`You don't have privileges to create refund`); }); }); diff --git a/modules/ticket/back/methods/ticket/refund.js b/modules/ticket/back/methods/ticket/refund.js new file mode 100644 index 000000000..620c8b0c7 --- /dev/null +++ b/modules/ticket/back/methods/ticket/refund.js @@ -0,0 +1,54 @@ +module.exports = Self => { + Self.remoteMethod('refund', { + description: 'Create refund tickets with all their sales and services', + accessType: 'WRITE', + accepts: [ + { + arg: 'ticketsIds', + type: ['number'], + required: true + }, + ], + returns: { + type: ['number'], + root: true + }, + http: { + path: `/refund`, + verb: 'post' + } + }); + + Self.refund = async(ticketsIds, options) => { + const models = Self.app.models; + const myOptions = {}; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const filter = {where: {ticketFk: {inq: ticketsIds}}}; + + const sales = await models.Sale.find(filter, myOptions); + const salesIds = sales.map(sale => sale.id); + + const services = await models.TicketService.find(filter, myOptions); + const servicesIds = services.map(service => service.id); + + const refundedTickets = await models.Sale.refund(salesIds, servicesIds, myOptions); + + if (tx) await tx.commit(); + + return refundedTickets; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/ticket/back/model-config.json b/modules/ticket/back/model-config.json index 0908503a5..8a6ac0c00 100644 --- a/modules/ticket/back/model-config.json +++ b/modules/ticket/back/model-config.json @@ -56,6 +56,9 @@ "TicketPackaging": { "dataSource": "vn" }, + "TicketRefund": { + "dataSource": "vn" + }, "TicketRequest": { "dataSource": "vn" }, diff --git a/modules/ticket/back/models/ticket-refund.json b/modules/ticket/back/models/ticket-refund.json new file mode 100644 index 000000000..8fd0e2306 --- /dev/null +++ b/modules/ticket/back/models/ticket-refund.json @@ -0,0 +1,40 @@ +{ + "name": "TicketRefund", + "base": "Loggable", + "options": { + "mysql": { + "table": "ticketRefund" + } + }, + "log": { + "model": "TicketLog", + "relation": "originalTicket" + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "refundTicketFk": { + "type": "number", + "required": true + }, + "originalTicketFk": { + "type": "number", + "required": true + } + }, + "relations": { + "refundTicket": { + "type": "belongsTo", + "model": "Ticket", + "foreignKey": "refundTicketFk" + }, + "originalTicket": { + "type": "belongsTo", + "model": "Ticket", + "foreignKey": "originalTicketFk" + } + } +} diff --git a/modules/ticket/back/models/ticket-service.js b/modules/ticket/back/models/ticket-service.js index aa94c42e3..209727ee4 100644 --- a/modules/ticket/back/models/ticket-service.js +++ b/modules/ticket/back/models/ticket-service.js @@ -3,17 +3,18 @@ const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { Self.observe('before save', async ctx => { const models = Self.app.models; - let changes = ctx.currentInstance || ctx.instance; - if (changes) { - let ticketId = changes.ticketFk; - let isLocked = await models.Ticket.isLocked(ticketId); + const changes = ctx.currentInstance || ctx.instance; + + if (changes && !ctx.isNewInstance) { + const ticketId = changes.ticketFk; + const isLocked = await models.Ticket.isLocked(ticketId); if (isLocked) throw new UserError(`The current ticket can't be modified`); + } - if (changes.ticketServiceTypeFk) { - const ticketServiceType = await models.TicketServiceType.findById(changes.ticketServiceTypeFk); - changes.description = ticketServiceType.name; - } + if (changes && changes.ticketServiceTypeFk) { + const ticketServiceType = await models.TicketServiceType.findById(changes.ticketServiceTypeFk); + changes.description = ticketServiceType.name; } }); diff --git a/modules/ticket/back/models/ticket.js b/modules/ticket/back/models/ticket.js index 300893968..47d105824 100644 --- a/modules/ticket/back/models/ticket.js +++ b/modules/ticket/back/models/ticket.js @@ -27,6 +27,7 @@ module.exports = Self => { require('../methods/ticket/isLocked')(Self); require('../methods/ticket/freightCost')(Self); require('../methods/ticket/getComponentsSum')(Self); + require('../methods/ticket/refund')(Self); Self.observe('before save', async function(ctx) { const loopBackContext = LoopBackContext.getCurrentContext(); diff --git a/modules/ticket/back/models/ticket.json b/modules/ticket/back/models/ticket.json index 80604a713..09b01d213 100644 --- a/modules/ticket/back/models/ticket.json +++ b/modules/ticket/back/models/ticket.json @@ -105,6 +105,16 @@ "model": "TicketPackaging", "foreignKey": "ticketFk" }, + "ticketSales": { + "type": "hasMany", + "model": "Sale", + "foreignKey": "ticketFk" + }, + "ticketServices": { + "type": "hasMany", + "model": "TicketService", + "foreignKey": "ticketFk" + }, "tracking": { "type": "hasMany", "model": "TicketTracking", diff --git a/modules/ticket/front/descriptor-menu/index.html b/modules/ticket/front/descriptor-menu/index.html index cfd817c45..ea84743bc 100644 --- a/modules/ticket/front/descriptor-menu/index.html +++ b/modules/ticket/front/descriptor-menu/index.html @@ -127,6 +127,8 @@ Refund all diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js index 120091089..379942cdd 100644 --- a/modules/ticket/front/descriptor-menu/index.js +++ b/modules/ticket/front/descriptor-menu/index.js @@ -253,22 +253,14 @@ class Controller extends Section { } async refund() { - const filter = { - where: {ticketFk: this.id} - }; - const sales = await this.$http.get('Sales', {filter}); - this.sales = sales.data; - - const ticketServices = await this.$http.get('TicketServices', {filter}); - this.services = ticketServices.data; - - const params = { - sales: this.sales, - services: this.services - }; - const query = `Sales/refund`; + const params = {ticketsIds: [this.id]}; + const query = 'Tickets/refund'; return this.$http.post(query, params).then(res => { - this.$state.go('ticket.card.sale', {id: res.data}); + const [refundTicket] = res.data; + this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { + ticketId: refundTicket.id + })); + this.$state.go('ticket.card.sale', {id: refundTicket.id}); }); } } diff --git a/modules/ticket/front/descriptor-menu/index.spec.js b/modules/ticket/front/descriptor-menu/index.spec.js index 65da73ca0..2d08a7846 100644 --- a/modules/ticket/front/descriptor-menu/index.spec.js +++ b/modules/ticket/front/descriptor-menu/index.spec.js @@ -245,27 +245,20 @@ describe('Ticket Component vnTicketDescriptorMenu', () => { }); }); - // #4084 review with Juan - xdescribe('refund()', () => { + describe('refund()', () => { it('should make a query and go to ticket.card.sale', () => { controller.$state.go = jest.fn(); controller._id = ticket.id; - const sales = [{id: 1}]; - const services = [{id: 2}]; - $httpBackend.expectGET(`Sales`).respond(sales); - $httpBackend.expectGET(`TicketServices`).respond(services); - - const expectedParams = { - sales: sales, - services: services + const params = { + ticketsIds: [16] }; - $httpBackend.expectPOST(`Sales/refund`, expectedParams).respond(); + $httpBackend.expectPOST('Tickets/refund', params).respond([{id: 99}]); controller.refund(); $httpBackend.flush(); - expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: undefined}); + expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: 99}); }); }); diff --git a/modules/ticket/front/descriptor-menu/locale/en.yml b/modules/ticket/front/descriptor-menu/locale/en.yml new file mode 100644 index 000000000..03f93db8d --- /dev/null +++ b/modules/ticket/front/descriptor-menu/locale/en.yml @@ -0,0 +1 @@ +The following refund ticket have been created: "The following refund ticket have been created: {{ticketId}}" \ No newline at end of file diff --git a/modules/ticket/front/descriptor-menu/locale/es.yml b/modules/ticket/front/descriptor-menu/locale/es.yml index 060d03154..b65159a3c 100644 --- a/modules/ticket/front/descriptor-menu/locale/es.yml +++ b/modules/ticket/front/descriptor-menu/locale/es.yml @@ -8,4 +8,5 @@ Send CSV: Enviar CSV Send CSV Delivery Note: Enviar albarán en CSV Send PDF Delivery Note: Enviar albarán en PDF Show Proforma: Ver proforma -Refund all: Abonar todo \ No newline at end of file +Refund all: Abonar todo +The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}" \ No newline at end of file diff --git a/modules/ticket/front/sale/index.html b/modules/ticket/front/sale/index.html index f4e5840f3..42eb10cb0 100644 --- a/modules/ticket/front/sale/index.html +++ b/modules/ticket/front/sale/index.html @@ -514,7 +514,7 @@ Refund diff --git a/modules/ticket/front/sale/index.js b/modules/ticket/front/sale/index.js index 987333e28..2724bb97d 100644 --- a/modules/ticket/front/sale/index.js +++ b/modules/ticket/front/sale/index.js @@ -483,11 +483,18 @@ class Controller extends Section { const sales = this.selectedValidSales(); if (!sales) return; - const params = {sales: sales}; - const query = `Sales/refund`; - this.resetChanges(); + const salesIds = sales.map(sale => sale.id); + + const params = {salesIds: salesIds}; + const query = 'Sales/refund'; this.$http.post(query, params).then(res => { - this.$state.go('ticket.card.sale', {id: res.data}); + const [refundTicket] = res.data; + this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { + ticketId: refundTicket.id + })); + this.$state.go('ticket.card.sale', {id: refundTicket.id}); + + this.resetChanges(); }); } diff --git a/modules/ticket/front/sale/index.spec.js b/modules/ticket/front/sale/index.spec.js index a8ac2f3de..28d874932 100644 --- a/modules/ticket/front/sale/index.spec.js +++ b/modules/ticket/front/sale/index.spec.js @@ -704,18 +704,19 @@ describe('Ticket', () => { }); describe('createRefund()', () => { - it('should make an HTTP POST query and then call to the $state go() method', () => { + it('should make a query and then navigate to the created ticket sales section', () => { jest.spyOn(controller, 'selectedValidSales').mockReturnValue(controller.sales); - jest.spyOn(controller, 'resetChanges'); jest.spyOn(controller.$state, 'go'); - - const expectedId = 9999; - $httpBackend.expect('POST', `Sales/refund`).respond(200, expectedId); + const params = { + salesIds: [1, 4], + }; + const refundTicket = {id: 99}; + const createdTickets = [refundTicket]; + $httpBackend.expect('POST', 'Sales/refund', params).respond(200, createdTickets); controller.createRefund(); $httpBackend.flush(); - expect(controller.resetChanges).toHaveBeenCalledWith(); - expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: expectedId}); + expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', refundTicket); }); }); diff --git a/modules/zone/back/models/agency-mode.json b/modules/zone/back/models/agency-mode.json index 027cec190..99ed43b97 100644 --- a/modules/zone/back/models/agency-mode.json +++ b/modules/zone/back/models/agency-mode.json @@ -55,6 +55,11 @@ "type": "belongsTo", "model": "DeliveryMethod", "foreignKey": "deliveryMethodFk" + }, + "zones": { + "type": "hasMany", + "model": "Zone", + "foreignKey": "agencyModeFk" } }, "acls": [