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]);