diff --git a/db/changes/234601/00-transferInvoice.sql b/db/changes/234601/00-transferInvoice.sql new file mode 100644 index 000000000..7a9890ae4 --- /dev/null +++ b/db/changes/234601/00-transferInvoice.sql @@ -0,0 +1,6 @@ +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) + VALUES + ('CplusRectificationType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('CplusInvoiceType477', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('InvoiceCorrectionType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('InvoiceOut', 'transferInvoice', 'WRITE', 'ALLOW', 'ROLE', 'administrative'); diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index a062168c9..faf58fd78 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -604,7 +604,7 @@ 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, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), + (1, 'T', 1026.24, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (2, 'T', 121.36, util.VN_CURDATE(), 1102, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (3, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (4, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), @@ -2977,3 +2977,9 @@ INSERT INTO vn.XDiario (id, ASIEN, FECHA, SUBCTA, CONTRA, CONCEPTO, EURODEBE, EU INSERT INTO `vn`.`mistakeType` (`id`, `description`) VALUES (1, 'Incorrect quantity'); + +INSERT INTO `vn`.`invoiceCorrectionType` (`id`, `description`) + VALUES + (1, 'Error in VAT calculation'), + (2, 'Error in sales details'), + (3, 'Error in customer data'); \ No newline at end of file diff --git a/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js b/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js index 23983a9c8..5e82306cc 100644 --- a/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js +++ b/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js @@ -212,7 +212,7 @@ describe('Ticket Edit sale path', () => { it('should log in as salesAssistant and navigate to ticket sales', async() => { await page.loginAndModule('salesAssistant', 'ticket'); - await page.accessToSearchResult('17'); + await page.accessToSearchResult('15'); await page.accessToSection('ticket.card.sale'); }); @@ -316,7 +316,7 @@ describe('Ticket Edit sale path', () => { }); it('should confirm the transfered quantity is the correct one', async() => { - const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText'); + const result = await page.waitToGetProperty(selectors.ticketSales.firstSaleQuantityCell, 'innerText'); expect(result).toContain('20'); }); @@ -370,7 +370,7 @@ describe('Ticket Edit sale path', () => { await page.waitToClick(selectors.ticketSales.moveToNewTicketButton); const message = await page.waitForSnackbar(); - expect(message.text).toContain(`You can't create a ticket for a inactive client`); + expect(message.text).toContain(`You can't create a ticket for an inactive client`); await page.closePopup(); }); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 8dfed66f6..26650175d 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -23,7 +23,7 @@ "Agency cannot be blank": "Agency cannot be blank", "The IBAN does not have the correct format": "The IBAN does not have the correct format", "You can't make changes on the basic data of an confirmed order or with rows": "You can't make changes on the basic data of an confirmed order or with rows", - "You can't create a ticket for a inactive client": "You can't create a ticket for a inactive client", + "You can't create a ticket for an inactive client": "You can't create a ticket for an inactive client", "Worker cannot be blank": "Worker cannot be blank", "You must delete the claim id %d first": "You must delete the claim id %d first", "You don't have enough privileges": "You don't have enough privileges", @@ -188,7 +188,14 @@ "The ticket doesn't exist.": "The ticket doesn't exist.", "The sales do not exists": "The sales do not exists", "Ticket without Route": "Ticket without route", + "Select a different client": "Select a different client", + "Fill all the fields": "Fill all the fields", + "Error while generating PDF": "Error while generating PDF", + "Can't invoice to future": "Can't invoice to future", + "This ticket is already invoiced": "This ticket is already invoiced", + "Negative basis of tickets: 23": "Negative basis of tickets: 23", "Booking completed": "Booking complete", "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation", "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets" } + diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 4b297144f..3cc9a9627 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -5,10 +5,10 @@ "The default consignee can not be unchecked": "No se puede desmarcar el consignatario predeterminado", "Unable to default a disabled consignee": "No se puede poner predeterminado un consignatario desactivado", "Can't be blank": "No puede estar en blanco", - "Invalid TIN": "NIF/CIF invalido", + "Invalid TIN": "NIF/CIF inválido", "TIN must be unique": "El NIF/CIF debe ser único", "A client with that Web User name already exists": "Ya existe un cliente con ese Usuario Web", - "Is invalid": "Is invalid", + "Is invalid": "Es inválido", "Quantity cannot be zero": "La cantidad no puede ser cero", "Enter an integer different to zero": "Introduce un entero distinto de cero", "Package cannot be blank": "El embalaje no puede estar en blanco", @@ -55,17 +55,17 @@ "You must delete the claim id %d first": "Antes debes borrar la reclamación %d", "You don't have enough privileges": "No tienes suficientes permisos", "Cannot check Equalization Tax in this NIF/CIF": "No se puede marcar RE en este NIF/CIF", - "You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos basicos de una orden con artículos", - "INVALID_USER_NAME": "El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no esta permitido el uso de la letra ñ", + "You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos básicos de una orden con artículos", + "INVALID_USER_NAME": "El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no está permitido el uso de la letra ñ", "You can't create a ticket for a frozen client": "No puedes crear un ticket para un cliente congelado", - "You can't create a ticket for a inactive client": "No puedes crear un ticket para un cliente inactivo", + "You can't create a ticket for an inactive client": "No puedes crear un ticket para un cliente inactivo", "Tag value cannot be blank": "El valor del tag no puede quedar en blanco", "ORDER_EMPTY": "Cesta vacía", "You don't have enough privileges to do that": "No tienes permisos para cambiar esto", "NO SE PUEDE DESACTIVAR EL CONSIGNAT": "NO SE PUEDE DESACTIVAR EL CONSIGNAT", "Error. El NIF/CIF está repetido": "Error. El NIF/CIF está repetido", "Street cannot be empty": "Dirección no puede estar en blanco", - "City cannot be empty": "Cuidad no puede estar en blanco", + "City cannot be empty": "Ciudad no puede estar en blanco", "Code cannot be blank": "Código no puede estar en blanco", "You cannot remove this department": "No puedes eliminar este departamento", "The extension must be unique": "La extensión debe ser unica", @@ -102,8 +102,8 @@ "You can't delete a confirmed order": "No puedes borrar un pedido confirmado", "The social name has an invalid format": "El nombre fiscal tiene un formato incorrecto", "Invalid quantity": "Cantidad invalida", - "This postal code is not valid": "This postal code is not valid", - "is invalid": "is invalid", + "This postal code is not valid": "Este código postal no es válido", + "is invalid": "es inválido", "The postcode doesn't exist. Please enter a correct one": "El código postal no existe. Por favor, introduce uno correcto", "The department name can't be repeated": "El nombre del departamento no puede repetirse", "This phone already exists": "Este teléfono ya existe", @@ -112,8 +112,8 @@ "You cannot delete a ticket that part of it is being prepared": "No puedes eliminar un ticket en el que una parte que está siendo preparada", "You must delete all the buy requests first": "Debes eliminar todas las peticiones de compra primero", "You should specify a date": "Debes especificar una fecha", - "You should specify at least a start or end date": "Debes especificar al menos una fecha de inicio o de fín", - "Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fín", + "You should specify at least a start or end date": "Debes especificar al menos una fecha de inicio o de fin", + "Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fin", "You should mark at least one week day": "Debes marcar al menos un día de la semana", "Swift / BIC can't be empty": "Swift / BIC no puede estar vacío", "Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios", @@ -144,15 +144,15 @@ "Unable to clone this travel": "No ha sido posible clonar este travel", "This thermograph id already exists": "La id del termógrafo ya existe", "Choose a date range or days forward": "Selecciona un rango de fechas o días en adelante", - "ORDER_ALREADY_CONFIRMED": "ORDER_ALREADY_CONFIRMED", + "ORDER_ALREADY_CONFIRMED": "ORDEN YA CONFIRMADA", "Invalid password": "Invalid password", "Password does not meet requirements": "La contraseña no cumple los requisitos", - "Role already assigned": "Role already assigned", - "Invalid role name": "Invalid role name", - "Role name must be written in camelCase": "Role name must be written in camelCase", - "Email already exists": "Email already exists", - "User already exists": "User already exists", - "Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral", + "Role already assigned": "Rol ya asignado", + "Invalid role name": "Nombre de rol no válido", + "Role name must be written in camelCase": "El nombre del rol debe escribirse en camelCase", + "Email already exists": "El correo ya existe", + "User already exists": "El/La usuario/a ya existe", + "Absence change notification on the labour calendar": "Notificación de cambio de ausencia en el calendario laboral", "Record of hours week": "Registro de horas semana {{week}} año {{year}} ", "Created absence": "El empleado {{author}} ha añadido una ausencia de tipo '{{absenceType}}' a {{employee}} para el día {{dated}}.", "Deleted absence": "El empleado {{author}} ha eliminado una ausencia de tipo '{{absenceType}}' a {{employee}} del día {{dated}}.", @@ -317,6 +317,9 @@ "The ticket doesn't exist.": "No existe el ticket.", "Social name should be uppercase": "La razón social debe ir en mayúscula", "Street should be uppercase": "La dirección fiscal debe ir en mayúscula", + "Ticket without Route": "Ticket sin ruta", + "Select a different client": "Seleccione un cliente distinto", + "Fill all the fields": "Rellene todos los campos", "The response is not a PDF": "La respuesta no es un PDF", "Ticket without Route": "Ticket sin ruta", "Booking completed": "Reserva completada", diff --git a/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js index 1de15b666..4bba2498f 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js +++ b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js @@ -23,7 +23,7 @@ module.exports = Self => { } }); - Self.makePdfAndNotify = async function(ctx, id, printerFk) { + Self.makePdfAndNotify = async function(ctx, id, printerFk, options) { const models = Self.app.models; options = typeof options == 'object' diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js index 6ecfe6015..22891f161 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js @@ -3,7 +3,7 @@ const LoopBackContext = require('loopback-context'); describe('InvoiceOut refund()', () => { const userId = 5; - const ctx = {req: {accessToken: userId}}; + const ctx = {req: {accessToken: userId}, args: {}}; const withWarehouse = true; const activeCtx = { accessToken: {userId: userId}, diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js new file mode 100644 index 000000000..04f6df299 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js @@ -0,0 +1,68 @@ + +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('InvoiceOut tranferInvoice()', () => { + 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 args = { + id: '1', + ref: 'T4444444', + newClientFk: 1, + cplusRectificationId: 1, + cplusInvoiceType477Id: 1, + invoiceCorrectionTypeId: 1 + }; + ctx.args = args; + try { + const result = await models.InvoiceOut.transferInvoice( + ctx, + options); + + expect(result).toBeDefined(); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw an UserError when it is the same client', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + const args = { + id: '1', + ref: 'T1111111', + newClientFk: 1101, + cplusRectificationId: 1, + cplusInvoiceType477Id: 1, + invoiceCorrectionTypeId: 1 + }; + ctx.args = args; + try { + await models.InvoiceOut.transferInvoice( + ctx, + options); + } catch (e) { + expect(e.message).toBe(`Select a different client`); + await tx.rollback(); + } + }); +}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js new file mode 100644 index 000000000..8a0609b8d --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js @@ -0,0 +1,111 @@ +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: 'ref', + type: 'string', + required: true + }, + { + arg: 'newClientFk', + type: 'number', + required: true + }, + { + arg: 'cplusRectificationId', + type: 'number', + required: true + }, + { + arg: 'cplusInvoiceType477Id', + type: 'number', + required: true + }, + { + arg: 'invoiceCorrectionTypeId', + type: 'number', + required: true + }, + ], + returns: { + type: 'boolean', + root: true + }, + http: { + path: '/transferInvoice', + verb: 'post' + } + }); + + Self.transferInvoice = async(ctx, options) => { + const models = Self.app.models; + const myOptions = {userId: ctx.req.accessToken.userId}; + const args = ctx.args; + let tx; + if (typeof options == 'object') + Object.assign(myOptions, options); + + const {clientFk} = await models.InvoiceOut.findById(args.id); + + if (clientFk == args.newClientFk) + throw new UserError(`Select a different client`); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + try { + const filterRef = {where: {refFk: args.ref}}; + const tickets = await models.Ticket.find(filterRef, myOptions); + const ticketsIds = tickets.map(ticket => ticket.id); + await models.Ticket.refund(ctx, ticketsIds, null, myOptions); + + const filterTicket = {where: {ticketFk: {inq: ticketsIds}}}; + + const services = await models.TicketService.find(filterTicket, myOptions); + const servicesIds = services.map(service => service.id); + + const sales = await models.Sale.find(filterTicket, myOptions); + const salesIds = sales.map(sale => sale.id); + + const clonedTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, false, false, myOptions); + const clonedTicketIds = []; + + for (const clonedTicket of clonedTickets) { + await clonedTicket.updateAttribute('clientFk', args.newClientFk, myOptions); + clonedTicketIds.push(clonedTicket.id); + } + + const invoiceIds = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, myOptions); + const [invoiceId] = invoiceIds; + + await models.InvoiceCorrection.create({ + correctingFk: invoiceId, + correctedFk: args.id, + cplusRectificationTypeFk: args.cplusRectificationId, + cplusInvoiceType477Fk: args.cplusInvoiceType477Id, + invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId + }, myOptions); + + if (tx) { + await tx.commit(); + await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null); + } + + return invoiceId; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json index 9e8b119ab..23246893b 100644 --- a/modules/invoiceOut/back/model-config.json +++ b/modules/invoiceOut/back/model-config.json @@ -31,5 +31,17 @@ }, "ZipConfig": { "dataSource": "vn" + }, + "CplusRectificationType": { + "dataSource": "vn" + }, + "InvoiceCorrectionType": { + "dataSource": "vn" + }, + "InvoiceCorrection": { + "dataSource": "vn" + }, + "CplusInvoiceType477": { + "dataSource": "vn" } } diff --git a/modules/invoiceOut/back/models/cplus-invoice-type-477.json b/modules/invoiceOut/back/models/cplus-invoice-type-477.json new file mode 100644 index 000000000..840a9a7e4 --- /dev/null +++ b/modules/invoiceOut/back/models/cplus-invoice-type-477.json @@ -0,0 +1,19 @@ +{ + "name": "CplusInvoiceType477", + "base": "VnModel", + "options": { + "mysql": { + "table": "cplusInvoiceType477" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "description": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/cplus-rectification-type.json b/modules/invoiceOut/back/models/cplus-rectification-type.json new file mode 100644 index 000000000..e7bfb957f --- /dev/null +++ b/modules/invoiceOut/back/models/cplus-rectification-type.json @@ -0,0 +1,19 @@ +{ + "name": "CplusRectificationType", + "base": "VnModel", + "options": { + "mysql": { + "table": "cplusRectificationType" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "description": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoice-correction-type.json b/modules/invoiceOut/back/models/invoice-correction-type.json new file mode 100644 index 000000000..ad3f034ea --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-correction-type.json @@ -0,0 +1,19 @@ +{ + "name": "InvoiceCorrectionType", + "base": "VnModel", + "options": { + "mysql": { + "table": "invoiceCorrectionType" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "description": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoice-correction.json b/modules/invoiceOut/back/models/invoice-correction.json new file mode 100644 index 000000000..48bd172a6 --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-correction.json @@ -0,0 +1,28 @@ +{ + "name": "InvoiceCorrection", + "base": "VnModel", + "options": { + "mysql": { + "table": "invoiceCorrection" + } + }, + "properties": { + "correctingFk": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "correctedFk": { + "type": "number" + }, + "cplusRectificationTypeFk": { + "type": "number" + }, + "cplusInvoiceType477Fk": { + "type": "number" + }, + "invoiceCorrectionTypeFk": { + "type": "number" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index d3aaf3b3d..ca77c856f 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -23,6 +23,7 @@ module.exports = Self => { require('../methods/invoiceOut/getInvoiceDate')(Self); require('../methods/invoiceOut/negativeBases')(Self); require('../methods/invoiceOut/negativeBasesCsv')(Self); + require('../methods/invoiceOut/transferInvoice')(Self); Self.filePath = async function(id, options) { const fields = ['ref', 'issued']; diff --git a/modules/invoiceOut/front/descriptor-menu/index.html b/modules/invoiceOut/front/descriptor-menu/index.html index 106f8e3cc..7d465f4ea 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.html +++ b/modules/invoiceOut/front/descriptor-menu/index.html @@ -1,3 +1,19 @@ + + + + + + + + Transfer invoice to... + Confirm + + + +
+ + + + #{{id}} - {{::name}} + + + + + {{::description}} + + + + + + + + + +
+
+ + + +
\ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor-menu/index.js b/modules/invoiceOut/front/descriptor-menu/index.js index 38c3c9434..7d2644158 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.js +++ b/modules/invoiceOut/front/descriptor-menu/index.js @@ -125,6 +125,22 @@ class Controller extends Section { this.$state.go('ticket.card.sale', {id: refundTicket.id}); }); } + + transferInvoice() { + const params = { + id: this.invoiceOut.id, + ref: this.invoiceOut.ref, + newClientFk: this.invoiceOut.client.id, + cplusRectificationId: this.cplusRectificationType, + cplusInvoiceType477Id: this.cplusInvoiceType477, + invoiceCorrectionTypeId: this.invoiceCorrectionType + }; + this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => { + const invoiceId = res.data; + this.vnApp.showSuccess(this.$t('Invoice trasfered!')); + this.$state.go('invoiceOut.card.summary', {id: invoiceId}); + }); + } } Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail']; diff --git a/modules/invoiceOut/front/descriptor-menu/locale/en.yml b/modules/invoiceOut/front/descriptor-menu/locale/en.yml index d299155d7..8fad5f25e 100644 --- a/modules/invoiceOut/front/descriptor-menu/locale/en.yml +++ b/modules/invoiceOut/front/descriptor-menu/locale/en.yml @@ -1 +1,3 @@ The following refund tickets have been created: "The following refund tickets have been created: {{ticketIds}}" +Transfer invoice to...: Transfer invoice to... +Cplus Type: Cplus Type \ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor-menu/locale/es.yml b/modules/invoiceOut/front/descriptor-menu/locale/es.yml index 393efd58c..0f74b5fec 100644 --- a/modules/invoiceOut/front/descriptor-menu/locale/es.yml +++ b/modules/invoiceOut/front/descriptor-menu/locale/es.yml @@ -21,3 +21,5 @@ The invoice PDF document has been regenerated: El documento PDF de la factura ha 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}}" Refund...: Abono... +Transfer invoice to...: Transferir factura a... +Cplus Type: Cplus Tipo diff --git a/modules/invoiceOut/front/descriptor-menu/style.scss b/modules/invoiceOut/front/descriptor-menu/style.scss index b68301961..9e4cf4297 100644 --- a/modules/invoiceOut/front/descriptor-menu/style.scss +++ b/modules/invoiceOut/front/descriptor-menu/style.scss @@ -21,4 +21,10 @@ vn-invoice-out-descriptor-menu { font-size: 1.75rem; } } -} \ No newline at end of file + +} +@media screen and (min-width: 1000px) { + .transferInvoice { + min-width: $width-md; + } +} diff --git a/modules/ticket/back/methods/sale/clone.js b/modules/ticket/back/methods/sale/clone.js new file mode 100644 index 000000000..a5ccb6de4 --- /dev/null +++ b/modules/ticket/back/methods/sale/clone.js @@ -0,0 +1,122 @@ +module.exports = Self => { + Self.clone = async(ctx, salesIds, servicesIds, withWarehouse, group, negative, options) => { + const models = Self.app.models; + const myOptions = {}; + let tx; + const newTickets = []; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const salesFilter = { + where: {id: {inq: salesIds}}, + include: { + relation: 'components', + scope: { + fields: ['saleFk', 'componentFk', 'value'] + } + } + }; + const sales = await models.Sale.find(salesFilter, myOptions); + let ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))]; + + const mappedTickets = new Map(); + + if (group) ticketsIds = [ticketsIds[0]]; + + for (let ticketId of ticketsIds) { + const newTicket = await createTicket( + ctx, + ticketId, + withWarehouse, + negative, + myOptions + ); + newTickets.push(newTicket); + mappedTickets.set(ticketId, newTicket.id); + } + + for (const sale of sales) { + const newTicketId = mappedTickets.get(sale.ticketFk); + + const createdSale = await models.Sale.create({ + ticketFk: newTicketId, + itemFk: sale.itemFk, + quantity: negative ? - sale.quantity : sale.quantity, + concept: sale.concept, + price: sale.price, + discount: sale.discount, + }, myOptions); + + const components = sale.components(); + for (const component of components) + component.saleFk = createdSale.id; + + await models.SaleComponent.create(components, myOptions); + } + + if (servicesIds && servicesIds.length) { + const servicesFilter = { + where: {id: {inq: servicesIds}} + }; + const services = await models.TicketService.find(servicesFilter, myOptions); + + for (const service of services) { + const newTicketId = mappedTickets.get(service.ticketFk); + + await models.TicketService.create({ + description: service.description, + quantity: negative ? - service.quantity : service.quantity, + price: service.price, + taxClassFk: service.taxClassFk, + ticketFk: newTicketId, + ticketServiceTypeFk: service.ticketServiceTypeFk, + }, myOptions); + } + } + + if (tx) await tx.commit(); + + return newTickets; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + + async function createTicket( + ctx, + ticketId, + withWarehouse, + negative, + myOptions + ) { + const models = Self.app.models; + const now = Date.vnNew(); + + const ticket = await models.Ticket.findById(ticketId, null, myOptions); + ctx.args.clientId = ticket.clientFk; + ctx.args.shipped = now; + ctx.args.landed = now; + ctx.args.warehouseId = withWarehouse ? ticket.warehouseFk : null; + ctx.args.companyId = ticket.companyFk; + ctx.args.addressId = ticket.addressFk; + + const newTicket = await models.Ticket.new(ctx, myOptions); + + if (negative) { + await models.TicketRefund.create({ + originalTicketFk: ticketId, + refundTicketFk: newTicket.id + }, myOptions); + } + + return newTicket; + } + }; +}; diff --git a/modules/ticket/back/methods/sale/refund.js b/modules/ticket/back/methods/sale/refund.js index 03302550e..17b70f12b 100644 --- a/modules/ticket/back/methods/sale/refund.js +++ b/modules/ticket/back/methods/sale/refund.js @@ -5,7 +5,8 @@ module.exports = Self => { accepts: [ { arg: 'salesIds', - type: ['number'] + type: ['number'], + required: true }, { arg: 'servicesIds', @@ -40,122 +41,23 @@ module.exports = Self => { myOptions.transaction = tx; } - let refundTicket = null; try { - const refundAgencyMode = await models.AgencyMode.findOne({ - include: { - relation: 'zones', - scope: { - limit: 1, - field: ['id', 'name'] - } - }, - where: {code: 'refund'} - }, myOptions); - - const refoundZoneId = refundAgencyMode.zones()[0].id; - - if (salesIds.length) { - 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 now = Date.vnNew(); - const [firstTicketId] = ticketsIds; - - // eslint-disable-next-line max-len - refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions); - - for (const sale of sales) { - const createdSale = await models.Sale.create({ - ticketFk: refundTicket.id, - itemFk: sale.itemFk, - quantity: - sale.quantity, - concept: sale.concept, - price: sale.price, - discount: sale.discount, - }, myOptions); - - const components = sale.components(); - for (const component of components) - component.saleFk = createdSale.id; - - await models.SaleComponent.create(components, myOptions); - } - } - if (!refundTicket) { - const servicesFilter = { - where: {id: {inq: servicesIds}} - }; - const services = await models.TicketService.find(servicesFilter, myOptions); - const firstTicketId = services[0].ticketFk; - - const now = Date.vnNew(); - - // eslint-disable-next-line max-len - refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, 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) { - await models.TicketService.create({ - description: service.description, - quantity: - service.quantity, - price: service.price, - taxClassFk: service.taxClassFk, - ticketFk: refundTicket.id, - ticketServiceTypeFk: service.ticketServiceTypeFk, - }, myOptions); - } - } - - const query = `CALL vn.ticket_recalc(?, NULL)`; - await Self.rawSql(query, [refundTicket.id], myOptions); + const refundsTicket = await models.Sale.clone( + ctx, + salesIds, + servicesIds, + withWarehouse, + false, + true, + myOptions + ); if (tx) await tx.commit(); - return refundTicket; + return refundsTicket[0]; } catch (e) { if (tx) await tx.rollback(); throw e; } }; - - async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions) { - const models = Self.app.models; - - const filter = {include: {relation: 'address'}}; - const ticket = await models.Ticket.findById(ticketId, filter, myOptions); - - const refundTicket = await models.Ticket.create({ - clientFk: ticket.clientFk, - shipped: now, - addressFk: ticket.address().id, - agencyModeFk: refundAgencyMode.id, - nickname: ticket.address().nickname, - warehouseFk: withWarehouse ? ticket.warehouseFk : null, - companyFk: ticket.companyFk, - landed: now, - zoneFk: refoundZoneId - }, myOptions); - - await models.TicketRefund.create({ - refundTicketFk: refundTicket.id, - originalTicketFk: ticket.id, - }, myOptions); - - return refundTicket; - } }; diff --git a/modules/ticket/back/methods/sale/specs/refund.spec.js b/modules/ticket/back/methods/sale/specs/refund.spec.js index b81f7f84d..08eb1fabd 100644 --- a/modules/ticket/back/methods/sale/specs/refund.spec.js +++ b/modules/ticket/back/methods/sale/specs/refund.spec.js @@ -3,7 +3,7 @@ const LoopBackContext = require('loopback-context'); describe('Sale refund()', () => { const userId = 5; - const ctx = {req: {accessToken: userId}}; + const ctx = {req: {accessToken: userId}, args: {}}; const activeCtx = { accessToken: {userId}, }; @@ -40,6 +40,7 @@ describe('Sale refund()', () => { try { const options = {transaction: tx}; + const ticketsBefore = await models.Ticket.find({}, options); const ticket = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options); @@ -61,12 +62,13 @@ describe('Sale refund()', () => { } ] }, options); - + const ticketsAfter = await models.Ticket.find({}, options); const salesLength = refundedTicket.ticketSales().length; const componentsLength = refundedTicket.ticketSales()[0].components().length; expect(refundedTicket).toBeDefined(); - expect(salesLength).toEqual(2); + expect(salesLength).toEqual(1); + expect(ticketsBefore.length).toEqual(ticketsAfter.length - 2); expect(componentsLength).toEqual(4); await tx.rollback(); diff --git a/modules/ticket/back/methods/ticket/invoiceTickets.js b/modules/ticket/back/methods/ticket/invoiceTickets.js index ca1bf15fb..fa3ee93af 100644 --- a/modules/ticket/back/methods/ticket/invoiceTickets.js +++ b/modules/ticket/back/methods/ticket/invoiceTickets.js @@ -77,9 +77,10 @@ module.exports = function(Self) { if (tx) await tx.rollback(); throw e; } - - for (const invoiceId of invoicesIds) - await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null); + if (tx) { + for (const invoiceId of invoicesIds) + await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null); + } return invoicesIds; }; diff --git a/modules/ticket/back/methods/ticket/new.js b/modules/ticket/back/methods/ticket/new.js index 0f5c323ed..288d38d77 100644 --- a/modules/ticket/back/methods/ticket/new.js +++ b/modules/ticket/back/methods/ticket/new.js @@ -96,7 +96,7 @@ module.exports = Self => { if (address.client().type().code === 'normal' && (!agencyMode || agencyMode.code != 'refund')) { const canCreateTicket = await models.Client.canCreateTicket(args.clientId, myOptions); if (!canCreateTicket) - throw new UserError(`You can't create a ticket for a inactive client`); + throw new UserError(`You can't create a ticket for an inactive client`); } if (!args.shipped && args.landed) { diff --git a/modules/ticket/back/methods/ticket/refund.js b/modules/ticket/back/methods/ticket/refund.js index c99b6aa83..758384ae2 100644 --- a/modules/ticket/back/methods/ticket/refund.js +++ b/modules/ticket/back/methods/ticket/refund.js @@ -39,7 +39,6 @@ module.exports = Self => { try { const filter = {where: {ticketFk: {inq: ticketsIds}}}; - const sales = await models.Sale.find(filter, myOptions); const salesIds = sales.map(sale => sale.id); diff --git a/modules/ticket/back/methods/ticket/specs/new.spec.js b/modules/ticket/back/methods/ticket/specs/new.spec.js index 0a2f93bc4..9aa073a7b 100644 --- a/modules/ticket/back/methods/ticket/specs/new.spec.js +++ b/modules/ticket/back/methods/ticket/specs/new.spec.js @@ -30,7 +30,7 @@ describe('ticket new()', () => { await tx.rollback(); } - expect(error).toEqual(new UserError(`You can't create a ticket for a inactive client`)); + expect(error).toEqual(new UserError(`You can't create a ticket for an inactive client`)); }); it('should throw an error if the address doesnt exist', async() => { diff --git a/modules/ticket/back/methods/ticket/transferSales.js b/modules/ticket/back/methods/ticket/transferSales.js index a2e92d524..54306510c 100644 --- a/modules/ticket/back/methods/ticket/transferSales.js +++ b/modules/ticket/back/methods/ticket/transferSales.js @@ -66,7 +66,7 @@ module.exports = Self => { const ticket = await models.Ticket.findById(id); const canCreateTicket = await models.Client.canCreateTicket(ticket.clientFk); if (!canCreateTicket) - throw new UserError(`You can't create a ticket for a inactive client`); + throw new UserError(`You can't create a ticket for an inactive client`); ticketId = await cloneTicket(originalTicket, myOptions); } diff --git a/modules/ticket/back/models/sale.js b/modules/ticket/back/models/sale.js index 99cfeeeea..3589eac4b 100644 --- a/modules/ticket/back/models/sale.js +++ b/modules/ticket/back/models/sale.js @@ -12,6 +12,7 @@ module.exports = Self => { require('../methods/sale/refund')(Self); require('../methods/sale/canEdit')(Self); require('../methods/sale/usesMana')(Self); + require('../methods/sale/clone')(Self); Self.validatesPresenceOf('concept', { message: `Concept cannot be blank` diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js index 1c9965d3b..b26472b08 100755 --- a/print/templates/reports/invoice/invoice.js +++ b/print/templates/reports/invoice/invoice.js @@ -7,6 +7,7 @@ module.exports = { mixins: [vnReport], async serverPrefetch() { this.invoice = await this.findOneFromDef('invoice', [this.reference]); + this.checkMainEntity(this.invoice); this.client = await this.findOneFromDef('client', [this.reference]); this.taxes = await this.rawSqlFromDef(`taxes`, [this.reference]);