Merge pull request '#7277 - RefundInvoices' (!2904) from 7277-RefundInvoices into dev
gitea/salix/pipeline/head There was a failure building this commit
Details
gitea/salix/pipeline/head There was a failure building this commit
Details
Reviewed-on: #2904 Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
This commit is contained in:
commit
40a1b24403
|
@ -15,9 +15,7 @@ describe('ticket assignCollection()', () => {
|
||||||
args: {}
|
args: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({active: ctx.req});
|
||||||
active: ctx.req
|
|
||||||
});
|
|
||||||
|
|
||||||
options = {transaction: tx};
|
options = {transaction: tx};
|
||||||
tx = await models.Sale.beginTransaction({});
|
tx = await models.Sale.beginTransaction({});
|
||||||
|
@ -25,7 +23,7 @@ describe('ticket assignCollection()', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async() => {
|
afterEach(async() => {
|
||||||
await tx.rollback();
|
if (tx) await tx.rollback();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error when there is not picking tickets', async() => {
|
it('should throw an error when there is not picking tickets', async() => {
|
||||||
|
|
|
@ -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 (747,'CplusRectificationType','*','READ','ALLOW','ROLE','administrative',NULL);
|
||||||
INSERT INTO `ACL` VALUES (748,'SiiTypeInvoiceOut','*','READ','ALLOW','ROLE','salesPerson',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 (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 (751,'Application','executeProc','*','ALLOW','ROLE','employee',NULL);
|
||||||
INSERT INTO `ACL` VALUES (752,'Application','executeFunc','*','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);
|
INSERT INTO `ACL` VALUES (753,'NotificationSubscription','getList','READ','ALLOW','ROLE','employee',NULL);
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
|
||||||
|
VALUES ('InvoiceOut','refundAndInvoice','WRITE','ALLOW','ROLE','administrative');
|
|
@ -230,10 +230,12 @@
|
||||||
"This workCenter is already assigned to this agency": "This workCenter is already assigned to this agency",
|
"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",
|
"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",
|
"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",
|
"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 add holidays on this day": "Cannot add holidays on this day",
|
||||||
"Cannot send mail": "Cannot send mail",
|
"Cannot send mail": "Cannot send mail",
|
||||||
"CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`": "CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`",
|
"CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`": "CONSTRAINT `chkParkingCodeFormat` failed for `vn`.`parking`",
|
||||||
"This postcode already exists": "This postcode already exists"
|
"This postcode already exists": "This postcode already exists",
|
||||||
}
|
"Original invoice not found": "Original invoice not found"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -363,12 +363,13 @@
|
||||||
"You can not use the same password": "No puedes usar la misma contraseña",
|
"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",
|
"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",
|
"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",
|
"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",
|
"Payment method is required": "El método de pago es obligatorio",
|
||||||
"Cannot send mail": "Não é possível enviar o email",
|
"Cannot send mail": "Não é possível enviar o email",
|
||||||
"CONSTRAINT `supplierAccountTooShort` failed for `vn`.`supplier`": "La cuenta debe tener exactamente 10 dígitos",
|
"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 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",
|
"The entry not have stickers": "La entrada no tiene etiquetas",
|
||||||
"Too many records": "Demasiados registros"
|
"Too many records": "Demasiados registros",
|
||||||
}
|
"Original invoice not found": "Factura original no encontrada"
|
||||||
|
}
|
||||||
|
|
|
@ -358,7 +358,9 @@
|
||||||
"This workCenter is already assigned to this agency": "Ce centre de travail est déjà assigné à cette agence",
|
"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",
|
"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 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é",
|
"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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -358,6 +358,8 @@
|
||||||
"This workCenter is already assigned to this agency": "Este centro de trabalho já está atribuído a esta agência",
|
"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",
|
"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 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",
|
"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"
|
"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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,12 +47,16 @@ module.exports = Self => {
|
||||||
Self.invoiceClient = async(ctx, options) => {
|
Self.invoiceClient = async(ctx, options) => {
|
||||||
const args = ctx.args;
|
const args = ctx.args;
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
options = typeof options === 'object' ? {...options} : {};
|
|
||||||
options.userId = ctx.req.accessToken.userId;
|
|
||||||
|
|
||||||
let tx;
|
let tx;
|
||||||
if (!options.transaction)
|
const myOptions = {userId: ctx.req.accessToken.userId};
|
||||||
tx = options.transaction = await Self.beginTransaction({});
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
|
if (!myOptions.transaction) {
|
||||||
|
tx = await Self.beginTransaction({});
|
||||||
|
myOptions.transaction = tx;
|
||||||
|
}
|
||||||
|
|
||||||
const minShipped = Date.vnNew();
|
const minShipped = Date.vnNew();
|
||||||
minShipped.setFullYear(args.maxShipped.getFullYear() - 1);
|
minShipped.setFullYear(args.maxShipped.getFullYear() - 1);
|
||||||
|
|
|
@ -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};
|
||||||
|
};
|
||||||
|
};
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
|
@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -26,7 +26,8 @@ module.exports = Self => {
|
||||||
require('../methods/invoiceOut/getInvoiceDate')(Self);
|
require('../methods/invoiceOut/getInvoiceDate')(Self);
|
||||||
require('../methods/invoiceOut/negativeBases')(Self);
|
require('../methods/invoiceOut/negativeBases')(Self);
|
||||||
require('../methods/invoiceOut/negativeBasesCsv')(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) {
|
Self.filePath = async function(id, options) {
|
||||||
const fields = ['ref', 'issued'];
|
const fields = ['ref', 'issued'];
|
||||||
|
|
|
@ -138,7 +138,6 @@ class Controller extends Section {
|
||||||
transferInvoice() {
|
transferInvoice() {
|
||||||
const params = {
|
const params = {
|
||||||
id: this.invoiceOut.id,
|
id: this.invoiceOut.id,
|
||||||
refFk: this.invoiceOut.ref,
|
|
||||||
newClientFk: this.clientId,
|
newClientFk: this.clientId,
|
||||||
cplusRectificationTypeFk: this.cplusRectificationType,
|
cplusRectificationTypeFk: this.cplusRectificationType,
|
||||||
siiTypeInvoiceOutFk: this.siiTypeInvoiceOut,
|
siiTypeInvoiceOutFk: this.siiTypeInvoiceOut,
|
||||||
|
@ -155,7 +154,7 @@ class Controller extends Section {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => {
|
this.$http.post(`InvoiceOuts/transfer`, params).then(res => {
|
||||||
const invoiceId = res.data;
|
const invoiceId = res.data;
|
||||||
this.vnApp.showSuccess(this.$t('Transferred invoice'));
|
this.vnApp.showSuccess(this.$t('Transferred invoice'));
|
||||||
this.$state.go('invoiceOut.card.summary', {id: invoiceId});
|
this.$state.go('invoiceOut.card.summary', {id: invoiceId});
|
||||||
|
|
|
@ -35,8 +35,11 @@ module.exports = Self => {
|
||||||
|
|
||||||
Self.cloneAll = async(ctx, ticketsIds, withWarehouse, negative, options) => {
|
Self.cloneAll = async(ctx, ticketsIds, withWarehouse, negative, options) => {
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const myOptions = typeof options == 'object' ? {...options} : {};
|
|
||||||
let tx;
|
let tx;
|
||||||
|
const myOptions = {};
|
||||||
|
|
||||||
|
if (typeof options == 'object')
|
||||||
|
Object.assign(myOptions, options);
|
||||||
|
|
||||||
if (!myOptions.transaction) {
|
if (!myOptions.transaction) {
|
||||||
tx = await Self.beginTransaction({});
|
tx = await Self.beginTransaction({});
|
||||||
|
|
Loading…
Reference in New Issue