diff --git a/back/methods/collection/spec/assignCollection.spec.js b/back/methods/collection/spec/assignCollection.spec.js index e8f3882a3..7cdcd6cb6 100644 --- a/back/methods/collection/spec/assignCollection.spec.js +++ b/back/methods/collection/spec/assignCollection.spec.js @@ -15,9 +15,7 @@ describe('ticket assignCollection()', () => { args: {} }; - spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ - active: ctx.req - }); + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({active: ctx.req}); options = {transaction: tx}; tx = await models.Sale.beginTransaction({}); @@ -25,7 +23,7 @@ describe('ticket assignCollection()', () => { }); afterEach(async() => { - await tx.rollback(); + if (tx) await tx.rollback(); }); it('should throw an error when there is not picking tickets', async() => { diff --git a/db/dump/.dump/data.sql b/db/dump/.dump/data.sql index a1c15a30f..58b892604 100644 --- a/db/dump/.dump/data.sql +++ b/db/dump/.dump/data.sql @@ -1959,7 +1959,7 @@ INSERT INTO `ACL` VALUES (746,'Claim','getSummary','READ','ALLOW','ROLE','claimV INSERT INTO `ACL` VALUES (747,'CplusRectificationType','*','READ','ALLOW','ROLE','administrative',NULL); INSERT INTO `ACL` VALUES (748,'SiiTypeInvoiceOut','*','READ','ALLOW','ROLE','salesPerson',NULL); INSERT INTO `ACL` VALUES (749,'InvoiceCorrectionType','*','READ','ALLOW','ROLE','salesPerson',NULL); -INSERT INTO `ACL` VALUES (750,'InvoiceOut','transferInvoice','WRITE','ALLOW','ROLE','administrative',NULL); +INSERT INTO `ACL` VALUES (750,'InvoiceOut','transfer','WRITE','ALLOW','ROLE','administrative',NULL); INSERT INTO `ACL` VALUES (751,'Application','executeProc','*','ALLOW','ROLE','employee',NULL); INSERT INTO `ACL` VALUES (752,'Application','executeFunc','*','ALLOW','ROLE','employee',NULL); INSERT INTO `ACL` VALUES (753,'NotificationSubscription','getList','READ','ALLOW','ROLE','employee',NULL); diff --git a/db/versions/11163-maroonEucalyptus/00-firstScript.sql b/db/versions/11163-maroonEucalyptus/00-firstScript.sql deleted file mode 100644 index 88e5fc022..000000000 --- a/db/versions/11163-maroonEucalyptus/00-firstScript.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE USER 'vn'@'localhost'; -GRANT ALL PRIVILEGES ON *.* TO 'vn'@'localhost' WITH GRANT OPTION;; diff --git a/db/versions/11206-turquoiseCyca/00-firstScript.sql b/db/versions/11206-turquoiseCyca/00-firstScript.sql new file mode 100644 index 000000000..ab6b35a0f --- /dev/null +++ b/db/versions/11206-turquoiseCyca/00-firstScript.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO salix.ACL (model,property,principalId) + VALUES ('Entry','getBuysCsv','supplier'); diff --git a/db/versions/11207-turquoiseMastic/00-firstScript.sql b/db/versions/11207-turquoiseMastic/00-firstScript.sql new file mode 100644 index 000000000..d1fd184aa --- /dev/null +++ b/db/versions/11207-turquoiseMastic/00-firstScript.sql @@ -0,0 +1,2 @@ +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) + VALUES ('InvoiceOut','refundAndInvoice','WRITE','ALLOW','ROLE','administrative'); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index e447c5d9d..d9d9c8511 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -230,10 +230,12 @@ "This workCenter is already assigned to this agency": "This workCenter is already assigned to this agency", "You can only have one PDA": "You can only have one PDA", "Incoterms and Customs agent are required for a non UEE member": "Incoterms and Customs agent are required for a non UEE member", - "It has been invoiced but the PDF could not be generated": "It has been invoiced but the PDF could not be generated", + "The invoices have been created but the PDFs could not be generated": "The invoices have been created but the PDFs could not be generated", "It has been invoiced but the PDF of refund not be generated": "It has been invoiced but the PDF of refund not be generated", "Cannot add holidays on this day": "Cannot add holidays on this day", "Cannot send mail": "Cannot send mail", "CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`": "CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`", - "This postcode already exists": "This postcode already exists" -} \ No newline at end of file + "This postcode already exists": "This postcode already exists", + "Original invoice not found": "Original invoice not found" + +} diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 8768e0ba3..8c11b3f1c 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -363,12 +363,13 @@ "You can not use the same password": "No puedes usar la misma contraseña", "This PDA is already assigned to another user": "Este PDA ya está asignado a otro usuario", "You can only have one PDA": "Solo puedes tener un PDA", - "It has been invoiced but the PDF could not be generated": "Se ha facturado pero no se ha podido generar el PDF", + "The invoices have been created but the PDFs could not be generated": "Se ha facturado pero no se ha podido generar el PDF", "It has been invoiced but the PDF of refund not be generated": "Se ha facturado pero no se ha podido generar el PDF del abono", "Payment method is required": "El método de pago es obligatorio", "Cannot send mail": "Não é possível enviar o email", "CONSTRAINT `supplierAccountTooShort` failed for `vn`.`supplier`": "La cuenta debe tener exactamente 10 dígitos", "The sale not exists in the item shelving": "La venta no existe en la estantería del artículo", "The entry not have stickers": "La entrada no tiene etiquetas", - "Too many records": "Demasiados registros" -} \ No newline at end of file + "Too many records": "Demasiados registros", + "Original invoice not found": "Factura original no encontrada" +} diff --git a/loopback/locale/fr.json b/loopback/locale/fr.json index 107e669f3..601fe35f5 100644 --- a/loopback/locale/fr.json +++ b/loopback/locale/fr.json @@ -358,7 +358,9 @@ "This workCenter is already assigned to this agency": "Ce centre de travail est déjà assigné à cette agence", "Select ticket or client": "Choisissez un ticket ou un client", "It was not able to create the invoice": "Il n'a pas été possible de créer la facture", - "It has been invoiced but the PDF could not be generated": "La facture a été émise mais le PDF n'a pas pu être généré", + "The invoices have been created but the PDFs could not be generated": "La facture a été émise mais le PDF n'a pas pu être généré", "It has been invoiced but the PDF of refund not be generated": "Il a été facturé mais le PDF de remboursement n'a pas été généré", - "Cannot send mail": "Impossible d'envoyer le mail" + "Cannot send mail": "Impossible d'envoyer le mail", + "Original invoice not found": "Facture originale introuvable" + } diff --git a/loopback/locale/pt.json b/loopback/locale/pt.json index 95c1fff0a..6425db9ed 100644 --- a/loopback/locale/pt.json +++ b/loopback/locale/pt.json @@ -358,6 +358,8 @@ "This workCenter is already assigned to this agency": "Este centro de trabalho já está atribuído a esta agência", "Select ticket or client": "Selecione um ticket ou cliente", "It was not able to create the invoice": "Não foi possível criar a fatura", - "It has been invoiced but the PDF could not be generated": "Foi faturado, mas o PDF não pôde ser gerado", - "It has been invoiced but the PDF of refund not be generated": "Foi faturado mas não foi gerado o PDF do reembolso" + "The invoices have been created but the PDFs could not be generated": "Foi faturado, mas o PDF não pôde ser gerado", + "It has been invoiced but the PDF of refund not be generated": "Foi faturado mas não foi gerado o PDF do reembolso", + "Original invoice not found": "Fatura original não encontrada" + } diff --git a/loopback/util/flatten.js b/loopback/util/flatten.js new file mode 100644 index 000000000..35c368d3b --- /dev/null +++ b/loopback/util/flatten.js @@ -0,0 +1,43 @@ + +/** + * Flattens an array of objects by converting each object into a flat structure. + * + * @param {Array} dataArray Array of objects to be flattened + * @return {Array} Array of flattened objects + */ +function flatten(dataArray) { + return dataArray.map(item => flattenObj(item.__data)); +} + +/** + * Recursively flattens an object, converting nested properties into a single level object + * with keys representing the original nested structure. + * + * @param {Object} data The object to be flattened + * @param {String} [prefix=''] Optional prefix for nested keys + * @return {Object} Flattened object + */ +function flattenObj(data, prefix = '') { + let result = {}; + try { + for (let key in data) { + if (!data[key]) continue; + + const newKey = prefix ? `${prefix}_${key}` : key; + const value = data[key]; + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) + Object.assign(result, flattenObj(value.__data, newKey)); + else + result[newKey] = value; + } + } catch (error) { + console.error(error); + } + + return result; +} +module.exports = { + flatten, + flattenObj, +}; diff --git a/modules/entry/back/methods/entry/getBuysCsv.js b/modules/entry/back/methods/entry/getBuysCsv.js new file mode 100644 index 000000000..a46f09c66 --- /dev/null +++ b/modules/entry/back/methods/entry/getBuysCsv.js @@ -0,0 +1,42 @@ +const {toCSV} = require('vn-loopback/util/csv'); +const {flatten} = require('vn-loopback/util/flatten'); + +module.exports = Self => { + Self.remoteMethodCtx('getBuysCsv', { + description: 'Returns buys for one entry in CSV file format', + accessType: 'READ', + accepts: [{ + arg: 'id', + type: 'number', + required: true, + description: 'The entry id', + http: {source: 'path'} + } + ], + returns: [ + { + arg: 'body', + type: 'file', + root: true + }, { + arg: 'Content-Type', + type: 'String', + http: {target: 'header'} + }, { + arg: 'Content-Disposition', + type: 'String', + http: {target: 'header'} + } + ], + http: { + path: `/:id/getBuysCsv`, + verb: 'GET' + } + }); + + Self.getBuysCsv = async(ctx, id, options) => { + const data = await Self.getBuys(ctx, id, null, options); + const dataFlatted = flatten(data); + return [toCSV(dataFlatted), 'text/csv', `inline; filename="buys-${id}.csv"`]; + }; +}; diff --git a/modules/entry/back/models/entry.js b/modules/entry/back/models/entry.js index b11d64415..8ca79f531 100644 --- a/modules/entry/back/models/entry.js +++ b/modules/entry/back/models/entry.js @@ -3,6 +3,7 @@ module.exports = Self => { require('../methods/entry/filter')(Self); require('../methods/entry/getEntry')(Self); require('../methods/entry/getBuys')(Self); + require('../methods/entry/getBuysCsv')(Self); require('../methods/entry/importBuys')(Self); require('../methods/entry/importBuysPreview')(Self); require('../methods/entry/lastItemBuys')(Self); diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js index 2c44cef34..bf7e7d3cb 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js @@ -47,12 +47,16 @@ module.exports = Self => { Self.invoiceClient = async(ctx, options) => { const args = ctx.args; const models = Self.app.models; - options = typeof options === 'object' ? {...options} : {}; - options.userId = ctx.req.accessToken.userId; - let tx; - if (!options.transaction) - tx = options.transaction = await Self.beginTransaction({}); + const myOptions = {userId: ctx.req.accessToken.userId}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } const minShipped = Date.vnNew(); minShipped.setFullYear(args.maxShipped.getFullYear() - 1); diff --git a/modules/invoiceOut/back/methods/invoiceOut/refundAndInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/refundAndInvoice.js new file mode 100644 index 000000000..7c7788459 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/refundAndInvoice.js @@ -0,0 +1,89 @@ +module.exports = Self => { + Self.remoteMethodCtx('refundAndInvoice', { + description: 'Refund an invoice and create a new one', + accessType: 'WRITE', + accepts: [ + { + arg: 'id', + type: 'number', + required: true, + description: 'Issued invoice id' + }, + { + arg: 'cplusRectificationTypeFk', + type: 'number', + required: true + }, + { + arg: 'siiTypeInvoiceOutFk', + type: 'number', + required: true + }, + { + arg: 'invoiceCorrectionTypeFk', + type: 'number', + required: true + }, + ], + returns: { + type: 'object', + root: true + }, + http: { + path: '/refundAndInvoice', + verb: 'post' + } + }); + + Self.refundAndInvoice = async( + ctx, + id, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + options + ) => { + const models = Self.app.models; + const myOptions = {userId: ctx.req.accessToken.userId}; + let refundId; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + let tx; + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const originalInvoice = await models.InvoiceOut.findById(id, myOptions); + + const refundedTickets = await Self.refund(ctx, originalInvoice.ref, false, myOptions); + + const invoiceCorrection = { + correctedFk: id, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk + }; + const ticketIds = refundedTickets.map(ticket => ticket.id); + refundId = await models.Ticket.invoiceTickets(ctx, ticketIds, invoiceCorrection, myOptions); + + tx && await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + + if (tx) { + try { + await models.InvoiceOut.makePdfList(ctx, refundId); + } catch (e) { + throw new UserError('The invoices have been created but the PDFs could not be generated'); + } + } + + return {refundId}; + }; +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/makePdfAndNotify.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/makePdfAndNotify.spec.js new file mode 100644 index 000000000..002face07 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/makePdfAndNotify.spec.js @@ -0,0 +1,105 @@ +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); +const UserError = require('vn-loopback/util/user-error'); + +describe('InvoiceOut makePdfAndNotify()', () => { + const userId = 5; + const ctx = { + req: { + accessToken: {userId}, + __: (key, obj) => `Translated: ${key}, ${JSON.stringify(obj)}`, + getLocale: () => 'es' + }, + args: {} + }; + const activeCtx = {accessToken: {userId}}; + const id = 4; + const printerFk = 1; + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({active: activeCtx}); + }); + + it('should generate PDF and send email when client is to be mailed', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + await models.InvoiceOut.makePdfAndNotify(ctx, id, printerFk, options); + + const invoice = await models.InvoiceOut.findById(id, { + fields: ['ref', 'clientFk'], + include: { + relation: 'client', + scope: { + fields: ['id', 'email', 'isToBeMailed', 'salesPersonFk'] + } + } + }, options); + + expect(invoice).toBeDefined(); + expect(invoice.client().isToBeMailed).toBe(true); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should generate PDF and print when client is not to be mailed', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + await models.InvoiceOut.makePdfAndNotify(ctx, id, null, options); + + const invoice = await models.InvoiceOut.findById(id, { + fields: ['ref', 'clientFk'], + include: { + relation: 'client', + scope: { + fields: ['id', 'email', 'isToBeMailed', 'salesPersonFk'] + } + } + }, options); + + expect(invoice).toBeDefined(); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw UserError when PDF generation fails', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + await models.InvoiceOut.makePdfAndNotify(ctx, null, null, options); + await tx.rollback(); + } catch (e) { + expect(e instanceof UserError).toBe(true); + expect(e.message).toContain('Error while generating PDF'); + await tx.rollback(); + } + }); + + it('should send message to salesperson when email fails', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + spyOn(models.InvoiceOut, 'invoiceEmail').and.rejectWith(new Error('Test Error')); + await models.InvoiceOut.makePdfAndNotify(ctx, id, null, options); + await tx.rollback(); + } catch (e) { + expect(e instanceof UserError).toBe(true); + expect(e.message).toContain('Error when sending mail to client'); + + await tx.rollback(); + } + }); +}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/refundAndInvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/refundAndInvoice.spec.js new file mode 100644 index 000000000..c54ae5f6c --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/refundAndInvoice.spec.js @@ -0,0 +1,46 @@ +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('InvoiceOut refundAndInvoice()', () => { + const userId = 5; + const ctx = {req: {accessToken: {userId}}}; + const activeCtx = {accessToken: {userId}}; + const id = 4; + const cplusRectificationTypeFk = 1; + const siiTypeInvoiceOutFk = 1; + const invoiceCorrectionTypeFk = 1; + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({active: activeCtx}); + }); + + it('should refund an invoice and create a new invoice', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + const result = await models.InvoiceOut.refundAndInvoice( + ctx, + id, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + options + ); + + expect(result).toBeDefined(); + expect(result.refundId).toBeDefined(); + + const invoicesAfter = await models.InvoiceOut.find({where: {id: result.refundId}}, options); + const ticketsAfter = await models.Ticket.find({where: {refFk: 'R10100001'}}, options); + + expect(invoicesAfter.length).toBeGreaterThan(0); + expect(ticketsAfter.length).toBeGreaterThan(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/transfer.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/transfer.spec.js new file mode 100644 index 000000000..8ec91743e --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/transfer.spec.js @@ -0,0 +1,135 @@ +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); +const UserError = require('vn-loopback/util/user-error'); + +describe('InvoiceOut transfer()', () => { + const userId = 5; + let options; + let tx; + let ctx; + const activeCtx = {accessToken: {userId}}; + const id = 4; + const newClientFk = 1101; + const cplusRectificationTypeFk = 1; + const siiTypeInvoiceOutFk = 1; + const invoiceCorrectionTypeFk = 1; + + beforeEach(async() => { + ctx = { + req: { + accessToken: {userId: 1106}, + headers: {origin: 'http://localhost'}, + __: value => value, + getLocale: () => 'es' + }, + args: {} + }; + + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({active: activeCtx}); + options = {transaction: tx}; + tx = await models.Sale.beginTransaction({}); + options.transaction = tx; + }); + + afterEach(async() => { + await tx.rollback(); + }); + + it('should transfer an invoice to a new client and return the new invoice ID', async() => { + const makeInvoice = true; + const makePdfListMock = spyOn(models.InvoiceOut, 'makePdfList').and.returnValue(); + + const [result] = await models.InvoiceOut.transfer( + ctx, + id, + newClientFk, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + makeInvoice, + options + ); + + const newInvoice = await models.InvoiceOut.findById(result, null, options); + + expect(newInvoice.clientFk).toBe(newClientFk); + + const transferredTickets = await models.Ticket.find({ + where: { + refFk: newInvoice.ref, + clientFk: newClientFk + } + }, options); + + expect(transferredTickets.length).toBeGreaterThan(0); + expect(makePdfListMock).toHaveBeenCalledWith(ctx, [result]); + }); + + it('should throw an error if original invoice is not found', async() => { + const makeInvoice = true; + try { + await models.InvoiceOut.transfer( + ctx, + 'idNotExists', + newClientFk, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + makeInvoice, + options + ); + fail('Expected an error to be thrown'); + } catch (e) { + expect(e).toBeInstanceOf(UserError); + expect(e.message).toBe('Original invoice not found'); + } + }); + + it('should throw an error if the new client is the same as the original client', async() => { + const makeInvoice = true; + const originalInvoice = await models.InvoiceOut.findById(id, options); + + try { + await models.InvoiceOut.transfer( + ctx, + id, + originalInvoice.clientFk, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + makeInvoice, + options + ); + fail('Expected an error to be thrown'); + } catch (e) { + expect(e).toBeInstanceOf(UserError); + expect(e.message).toBe('Select a different client'); + } + }); + + it('should not create a new invoice if makeInvoice is false', async() => { + const originalTickets = await models.Ticket.find({ + where: {clientFk: newClientFk, refFk: null}, + options + }); + + const result = await models.InvoiceOut.transfer( + ctx, + id, + newClientFk, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + false, + options + ); + + expect(result).toBeUndefined(); + + const transferredTickets = await models.Ticket.find({ + where: {clientFk: newClientFk, refFk: null} + }, options); + + expect(transferredTickets.length).toBeGreaterThan(originalTickets.length); + }); +}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js deleted file mode 100644 index 22787e730..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js +++ /dev/null @@ -1,116 +0,0 @@ -const models = require('vn-loopback/server/server').models; -const LoopBackContext = require('loopback-context'); - -describe('InvoiceOut transferInvoice()', () => { - const activeCtx = { - accessToken: {userId: 5}, - http: { - req: { - headers: {origin: 'http://localhost'} - } - } - }; - const ctx = {req: activeCtx}; - - beforeEach(() => { - spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ - active: activeCtx - }); - }); - - it('should return the id of the created issued invoice', async() => { - const tx = await models.InvoiceOut.beginTransaction({}); - const options = {transaction: tx}; - const id = 4; - const newClient = 1; - spyOn(models.InvoiceOut, 'makePdfList'); - - try { - const {clientFk: oldClient} = await models.InvoiceOut.findById(id, {fields: ['clientFk']}); - const invoicesBefore = await models.InvoiceOut.find({}, options); - const result = await models.InvoiceOut.transferInvoice( - ctx, - id, - 'T4444444', - newClient, - 1, - 1, - 1, - true, - options); - const invoicesAfter = await models.InvoiceOut.find({}, options); - const rectificativeInvoice = invoicesAfter[invoicesAfter.length - 2]; - const newInvoice = invoicesAfter[invoicesAfter.length - 1]; - - expect(result).toBeDefined(); - expect(invoicesAfter.length - invoicesBefore.length).toEqual(2); - expect(rectificativeInvoice.clientFk).toEqual(oldClient); - expect(newInvoice.clientFk).toEqual(newClient); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); - - it('should throw an error when it is the same client', async() => { - const tx = await models.InvoiceOut.beginTransaction({}); - const options = {transaction: tx}; - spyOn(models.InvoiceOut, 'makePdfList'); - - try { - await models.InvoiceOut.transferInvoice(ctx, '1', 'T1111111', 1101, 1, 1, 1, true, options); - await tx.rollback(); - } catch (e) { - expect(e.message).toBe(`Select a different client`); - await tx.rollback(); - } - }); - - it('should throw an error when it is refund', async() => { - const tx = await models.InvoiceOut.beginTransaction({}); - const options = {transaction: tx}; - spyOn(models.InvoiceOut, 'makePdfList'); - try { - await models.InvoiceOut.transferInvoice(ctx, '1', 'T1111111', 1102, 1, 1, 1, true, options); - await tx.rollback(); - } catch (e) { - expect(e.message).toContain(`This ticket is already a refund`); - await tx.rollback(); - } - }); - - it('should throw an error when pdf failed', async() => { - const tx = await models.InvoiceOut.beginTransaction({}); - const options = {transaction: tx}; - spyOn(models.InvoiceOut, 'makePdfList').and.returnValue(() => { - throw new Error('test'); - }); - - try { - await models.InvoiceOut.transferInvoice(ctx, '1', 'T1111111', 1102, 1, 1, 1, true, options); - await tx.rollback(); - } catch (e) { - expect(e.message).toContain(`It has been invoiced but the PDF could not be generated`); - await tx.rollback(); - } - }); - - it('should not generate an invoice', async() => { - const tx = await models.InvoiceOut.beginTransaction({}); - const options = {transaction: tx}; - spyOn(models.InvoiceOut, 'makePdfList'); - - let response; - try { - response = await models.InvoiceOut.transferInvoice(ctx, '1', 'T1111111', 1102, 1, 1, 1, false, options); - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - - expect(response).not.toBeDefined(); - }); -}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/transfer.js b/modules/invoiceOut/back/methods/invoiceOut/transfer.js new file mode 100644 index 000000000..954adf780 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/transfer.js @@ -0,0 +1,122 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('transfer', { + description: 'Transfer an issued invoice to another client', + accessType: 'WRITE', + accepts: [ + { + arg: 'id', + type: 'number', + required: true, + description: 'Issued invoice id' + }, + { + arg: 'newClientFk', + type: 'number', + required: true + }, + { + arg: 'cplusRectificationTypeFk', + type: 'number', + required: true + }, + { + arg: 'siiTypeInvoiceOutFk', + type: 'number', + required: true + }, + { + arg: 'invoiceCorrectionTypeFk', + type: 'number', + required: true + }, + { + arg: 'makeInvoice', + type: 'boolean', + required: true + }, + ], + returns: {type: 'object', root: true}, + http: {path: '/transfer', verb: 'post'} + }); + + Self.transfer = async( + ctx, + id, + newClientFk, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + makeInvoice, + options + ) => { + const models = Self.app.models; + let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + const originalInvoice = await models.InvoiceOut.findById(id); + if (!originalInvoice) + throw new UserError('Original invoice not found'); + + if (originalInvoice.clientFk === newClientFk) + throw new UserError('Select a different client'); + + let transferredInvoiceId; + try { + await Self.refundAndInvoice( + ctx, + id, + cplusRectificationTypeFk, + siiTypeInvoiceOutFk, + invoiceCorrectionTypeFk, + myOptions + ); + + const tickets = await models.Ticket.find({where: {refFk: originalInvoice.ref}}, myOptions); + const ticketIds = tickets.map(ticket => ticket.id); + const transferredTickets = await models.Ticket.cloneAll(ctx, ticketIds, false, false, myOptions); + const client = await models.Client.findById(newClientFk, + {fields: ['id', 'defaultAddressFk']}, myOptions); + const address = await models.Address.findById(client.defaultAddressFk, + {fields: ['id', 'nickname']}, myOptions); + + const transferredTicketIds = transferredTickets.map(ticket => ticket.id); + await models.Ticket.updateAll( + {id: {inq: transferredTicketIds}}, + { + clientFk: newClientFk, + addressFk: client.defaultAddressFk, + nickname: address.nickname + }, + myOptions + ); + + if (makeInvoice) + transferredInvoiceId = await models.Ticket.invoiceTickets(ctx, transferredTicketIds, null, myOptions); + + if (tx) await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + + if (transferredInvoiceId) { + try { + await models.InvoiceOut.makePdfList(ctx, transferredInvoiceId); + } catch (e) { + throw new UserError('The invoices have been created but the PDFs could not be generatedd'); + } + } + + return transferredInvoiceId; + }; +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js deleted file mode 100644 index c31f381d9..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js +++ /dev/null @@ -1,131 +0,0 @@ -const UserError = require('vn-loopback/util/user-error'); - -module.exports = Self => { - Self.remoteMethodCtx('transferInvoice', { - description: 'Transfer an issued invoice to another client', - accessType: 'WRITE', - accepts: [ - { - arg: 'id', - type: 'number', - required: true, - description: 'Issued invoice id' - }, - { - arg: 'refFk', - type: 'string', - required: true - }, - { - arg: 'newClientFk', - type: 'number', - required: true - }, - { - arg: 'cplusRectificationTypeFk', - type: 'number', - required: true - }, - { - arg: 'siiTypeInvoiceOutFk', - type: 'number', - required: true - }, - { - arg: 'invoiceCorrectionTypeFk', - type: 'number', - required: true - }, - { - arg: 'makeInvoice', - type: 'boolean', - required: true - }, - ], - returns: { - type: 'object', - root: true - }, - http: { - path: '/transferInvoice', - verb: 'post' - } - }); - - Self.transferInvoice = async( - ctx, - id, - refFk, - newClientFk, - cplusRectificationTypeFk, - siiTypeInvoiceOutFk, - invoiceCorrectionTypeFk, - makeInvoice, - options - ) => { - const models = Self.app.models; - const myOptions = {userId: ctx.req.accessToken.userId}; - let invoiceId; - let refundId; - - let tx; - if (typeof options == 'object') - Object.assign(myOptions, options); - - const {clientFk} = await models.InvoiceOut.findById(id); - - if (clientFk == newClientFk) - throw new UserError(`Select a different client`); - - if (!myOptions.transaction) { - tx = await Self.beginTransaction({}); - myOptions.transaction = tx; - } - try { - const tickets = await models.Ticket.find({where: {refFk}}, myOptions); - const ticketsIds = tickets.map(ticket => ticket.id); - const refundTickets = await models.Ticket.cloneAll(ctx, ticketsIds, false, true, myOptions); - - const clonedTickets = await models.Ticket.cloneAll(ctx, ticketsIds, false, false, myOptions); - - const clonedTicketIds = []; - - for (const clonedTicket of clonedTickets) { - await clonedTicket.updateAttribute('clientFk', newClientFk, myOptions); - clonedTicketIds.push(clonedTicket.id); - } - - const invoiceCorrection = { - correctedFk: id, - cplusRectificationTypeFk, - siiTypeInvoiceOutFk, - invoiceCorrectionTypeFk - }; - const refundTicketIds = refundTickets.map(ticket => ticket.id); - - refundId = await models.Ticket.invoiceTickets(ctx, refundTicketIds, invoiceCorrection, myOptions); - - if (makeInvoice) - invoiceId = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, null, myOptions); - - tx && await tx.commit(); - } catch (e) { - if (tx) await tx.rollback(); - throw e; - } - - if (tx && makeInvoice) { - try { - await models.InvoiceOut.makePdfList(ctx, invoiceId); - } catch (e) { - throw new UserError('It has been invoiced but the PDF could not be generated'); - } - try { - await models.InvoiceOut.makePdfList(ctx, refundId); - } catch (e) { - throw new UserError('It has been invoiced but the PDF of refund not be generated'); - } - } - return invoiceId; - }; -}; diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index b0e05b626..47dbcbea4 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -26,7 +26,8 @@ module.exports = Self => { require('../methods/invoiceOut/getInvoiceDate')(Self); require('../methods/invoiceOut/negativeBases')(Self); require('../methods/invoiceOut/negativeBasesCsv')(Self); - require('../methods/invoiceOut/transferInvoice')(Self); + require('../methods/invoiceOut/transfer')(Self); + require('../methods/invoiceOut/refundAndInvoice')(Self); Self.filePath = async function(id, options) { const fields = ['ref', 'issued']; diff --git a/modules/invoiceOut/front/descriptor-menu/index.js b/modules/invoiceOut/front/descriptor-menu/index.js index 07a0f1768..288de879e 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.js +++ b/modules/invoiceOut/front/descriptor-menu/index.js @@ -138,7 +138,6 @@ class Controller extends Section { transferInvoice() { const params = { id: this.invoiceOut.id, - refFk: this.invoiceOut.ref, newClientFk: this.clientId, cplusRectificationTypeFk: this.cplusRectificationType, siiTypeInvoiceOutFk: this.siiTypeInvoiceOut, @@ -155,7 +154,7 @@ class Controller extends Section { return; } - this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => { + this.$http.post(`InvoiceOuts/transfer`, params).then(res => { const invoiceId = res.data; this.vnApp.showSuccess(this.$t('Transferred invoice')); this.$state.go('invoiceOut.card.summary', {id: invoiceId}); diff --git a/modules/ticket/back/methods/ticket/cloneAll.js b/modules/ticket/back/methods/ticket/cloneAll.js index cf99a7edc..29d45615f 100644 --- a/modules/ticket/back/methods/ticket/cloneAll.js +++ b/modules/ticket/back/methods/ticket/cloneAll.js @@ -35,8 +35,11 @@ module.exports = Self => { Self.cloneAll = async(ctx, ticketsIds, withWarehouse, negative, options) => { const models = Self.app.models; - const myOptions = typeof options == 'object' ? {...options} : {}; let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); if (!myOptions.transaction) { tx = await Self.beginTransaction({});