refs #5874 feat: refactorizado proceso facturación #1658

Merged
vicent merged 3 commits from hotfix_makeInvoice-refactor into master 2023-07-11 09:49:37 +00:00
16 changed files with 390 additions and 223 deletions
Showing only changes of commit 93e70e699a - Show all commits

View File

@ -177,5 +177,6 @@
"Invalid quantity": "Invalid quantity", "Invalid quantity": "Invalid quantity",
"Failed to upload delivery note": "Error to upload delivery note {{id}}", "Failed to upload delivery note": "Error to upload delivery note {{id}}",
"Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address", "Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address",
"The renew period has not been exceeded": "The renew period has not been exceeded" "The renew period has not been exceeded": "The renew period has not been exceeded",
"Negative basis of tickets": "Negative basis of tickets: {{ticketsIds}}"
} }

View File

@ -297,5 +297,6 @@
"Error while generating PDF": "Error al generar PDF", "Error while generating PDF": "Error al generar PDF",
"Error when sending mail to client": "Error al enviar el correo al cliente", "Error when sending mail to client": "Error al enviar el correo al cliente",
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico", "Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado" "The renew period has not been exceeded": "El periodo de renovación no ha sido superado",
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}"
} }

View File

@ -89,7 +89,7 @@ module.exports = Self => {
}; };
const country = await Self.app.models.Country.findOne(filter); const country = await Self.app.models.Country.findOne(filter);
const code = country ? country.code.toLowerCase() : null; const code = country ? country.code.toLowerCase() : null;
const countryCode = this.fi.toLowerCase().substring(0, 2); const countryCode = this.fi?.toLowerCase().substring(0, 2);
if (!this.fi || !validateTin(this.fi, code) || (this.isVies && countryCode == code)) if (!this.fi || !validateTin(this.fi, code) || (this.isVies && countryCode == code))
err(); err();
@ -401,7 +401,7 @@ module.exports = Self => {
Self.changeCredit = async function changeCredit(ctx, finalState, changes) { Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.options.accessToken.userId; const userId = ctx.options.accessToken.userId;
const accessToken = {req: {accessToken: ctx.options.accessToken} }; const accessToken = {req: {accessToken: ctx.options.accessToken}};
const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE'); const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE');
if (!canEditCredit) { if (!canEditCredit) {

View File

@ -296,6 +296,9 @@
value="{{$ctrl.summary.rating}}" value="{{$ctrl.summary.rating}}"
info="Value from 1 to 20. The higher the better value"> info="Value from 1 to 20. The higher the better value">
</vn-label-value> </vn-label-value>
<vn-label-value label="Recommended credit"
value="{{$ctrl.summary.recommendedCredit | currency: 'EUR': 2}}">
vicent marked this conversation as resolved Outdated

sense decimals, aprofita y modifiques tb el crédit son int

sense decimals, aprofita y modifiques tb el crédit son int
</vn-label-value>
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>

View File

@ -1,5 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('invoiceClient', { Self.remoteMethodCtx('invoiceClient', {
description: 'Make a invoice of a client', description: 'Make a invoice of a client',
@ -56,7 +54,6 @@ module.exports = Self => {
const minShipped = Date.vnNew(); const minShipped = Date.vnNew();
minShipped.setFullYear(args.maxShipped.getFullYear() - 1); minShipped.setFullYear(args.maxShipped.getFullYear() - 1);
let invoiceId;
try { try {
const client = await models.Client.findById(args.clientId, { const client = await models.Client.findById(args.clientId, {
fields: ['id', 'hasToInvoiceByAddress'] fields: ['id', 'hasToInvoiceByAddress']
@ -77,56 +74,21 @@ module.exports = Self => {
], options); ], options);
} }
// Check negative bases const invoiceType = 'G';
const invoiceId = await models.Ticket.makeInvoice(
let query = ctx,
`SELECT COUNT(*) isSpanishCompany invoiceType,
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
const [supplierCompany] = await Self.rawSql(query, [
args.companyFk
], options);
const isSpanishCompany = supplierCompany?.isSpanishCompany;
query = 'SELECT hasAnyNegativeBase() AS base';
const [result] = await Self.rawSql(query, null, options);
const hasAnyNegativeBase = result?.base;
if (hasAnyNegativeBase && isSpanishCompany)
throw new UserError('Negative basis');
// Invoicing
query = `SELECT invoiceSerial(?, ?, ?) AS serial`;
const [invoiceSerial] = await Self.rawSql(query, [
client.id,
args.companyFk, args.companyFk,
'G' args.invoiceDate,
], options); options
const serialLetter = invoiceSerial.serial; );
query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`;
await Self.rawSql(query, [
serialLetter,
args.invoiceDate
], options);
const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, options);
if (!newInvoice)
throw new UserError('No tickets to invoice', 'notInvoiced');
await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], options);
invoiceId = newInvoice.id;
if (tx) await tx.commit(); if (tx) await tx.commit();
return invoiceId;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
throw e; throw e;
} }
return invoiceId;
}; };
}; };

View File

@ -14,8 +14,7 @@ module.exports = Self => {
}, { }, {
arg: 'printerFk', arg: 'printerFk',
type: 'number', type: 'number',
description: 'The printer to print', description: 'The printer to print'
required: true
} }
], ],
http: { http: {
@ -51,7 +50,7 @@ module.exports = Self => {
const ref = invoiceOut.ref; const ref = invoiceOut.ref;
const client = invoiceOut.client(); const client = invoiceOut.client();
if (client.isToBeMailed) { if (client.isToBeMailed || !printerFk) {
try { try {
ctx.args = { ctx.args = {
reference: ref, reference: ref,

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) { module.exports = function(Self) {
Self.remoteMethodCtx('canBeInvoiced', { Self.remoteMethodCtx('canBeInvoiced', {
description: 'Whether the ticket can or not be invoiced', description: 'Whether the ticket can or not be invoiced',
@ -21,8 +23,9 @@ module.exports = function(Self) {
} }
}); });
Self.canBeInvoiced = async(ticketsIds, options) => { Self.canBeInvoiced = async(ctx, ticketsIds, options) => {
const myOptions = {}; const myOptions = {};
const $t = ctx.req.__; // $translate
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
@ -31,29 +34,43 @@ module.exports = function(Self) {
where: { where: {
id: {inq: ticketsIds} id: {inq: ticketsIds}
}, },
fields: ['id', 'refFk', 'shipped', 'totalWithVat'] fields: ['id', 'refFk', 'shipped', 'totalWithVat', 'companyFk']
}, myOptions); }, myOptions);
const [firstTicket] = tickets;
const companyFk = firstTicket.companyFk;
const query = ` const query =
SELECT vn.hasSomeNegativeBase(t.id) AS hasSomeNegativeBase `SELECT COUNT(*) isSpanishCompany
FROM ticket t FROM supplier s
WHERE id IN(?)`; JOIN country c ON c.id = s.countryFk
const ticketBases = await Self.rawSql(query, [ticketsIds], myOptions); AND c.code = 'ES'
const hasSomeNegativeBase = ticketBases.some( WHERE s.id = ?`;
ticketBases => ticketBases.hasSomeNegativeBase const [supplierCompany] = await Self.rawSql(query, [companyFk], options);
);
const isSpanishCompany = supplierCompany?.isSpanishCompany;
const [result] = await Self.rawSql('SELECT hasAnyNegativeBase() AS base', null, options);
const hasAnyNegativeBase = result?.base && isSpanishCompany;
if (hasAnyNegativeBase)
throw new UserError($t('Negative basis of tickets', {ticketsIds: ticketsIds}));
const today = Date.vnNew(); const today = Date.vnNew();
const invalidTickets = tickets.some(ticket => { tickets.some(ticket => {
const shipped = new Date(ticket.shipped); const shipped = new Date(ticket.shipped);
const shippingInFuture = shipped.getTime() > today.getTime(); const shippingInFuture = shipped.getTime() > today.getTime();
const isInvoiced = ticket.refFk; if (shippingInFuture)
const priceZero = ticket.totalWithVat == 0; throw new UserError(`Can't invoice to future`);
return isInvoiced || priceZero || shippingInFuture; const isInvoiced = ticket.refFk;
if (isInvoiced)
throw new UserError(`This ticket is already invoiced`);
const priceZero = ticket.totalWithVat == 0;
if (priceZero)
throw new UserError(`A ticket with an amount of zero can't be invoiced`);
}); });
return !(invalidTickets || hasSomeNegativeBase); return true;
}; };
}; };

View File

@ -0,0 +1,105 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethodCtx('invoiceTickets', {
description: 'Make out an invoice from one or more tickets',
accessType: 'WRITE',
accepts: [
{
arg: 'ticketsIds',
description: 'The tickets id',
type: ['number'],
required: true
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/invoiceTickets`,
verb: 'POST'
}
});
Self.invoiceTickets = async(ctx, ticketsIds, options) => {
const models = Self.app.models;
const date = Date.vnNew();
date.setHours(0, 0, 0, 0);
const myOptions = {userId: ctx.req.accessToken.userId};
let tx;
if (typeof options === 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
let invoicesIds = [];
try {
const tickets = await models.Ticket.find({
where: {
id: {inq: ticketsIds}
},
fields: ['id', 'clientFk', 'companyFk']
}, myOptions);
const [firstTicket] = tickets;
const clientId = firstTicket.clientFk;
const companyId = firstTicket.companyFk;
const isSameClient = tickets.every(ticket => ticket.clientFk === clientId);
if (!isSameClient)
throw new UserError(`You can't invoice tickets from multiple clients`);
const client = await models.Client.findById(clientId, {
fields: ['id', 'hasToInvoiceByAddress']
}, myOptions);
if (client.hasToInvoiceByAddress) {
const query = `
SELECT DISTINCT addressFk
FROM ticket t
WHERE id IN (?)`;
const result = await Self.rawSql(query, [ticketsIds], myOptions);
const addressIds = result.map(address => address.addressFk);
for (const address of addressIds)
await createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, myOptions);
} else
await createInvoice(ctx, companyId, ticketsIds, null, invoicesIds, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
for (const invoiceId of invoicesIds)
await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null);
return invoicesIds;
};
async function createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, myOptions) {
const models = Self.app.models;
await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
vicent marked this conversation as resolved Outdated

CREATE OR REPLACE

CREATE OR REPLACE
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
${address ? `AND addressFk = ${address}` : ''}
`, [ticketsIds], myOptions);
const invoiceId = await models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), myOptions);
invoicesIds.push(invoiceId);
}
vicent marked this conversation as resolved
Review

tmp.ticketToInvoice crec que ací has de fer drop de la taula temporal

tmp.ticketToInvoice crec que ací has de fer drop de la taula temporal
Review

Ho fa el proc invoiceOut_new, que se crida.

Ho fa el proc invoiceOut_new, que se crida.
};

View File

@ -6,15 +6,26 @@ module.exports = function(Self) {
accessType: 'WRITE', accessType: 'WRITE',
accepts: [ accepts: [
{ {
arg: 'ticketsIds', arg: 'invoiceType',
description: 'The tickets id', description: 'The invoice type',
type: ['number'], type: 'string',
required: true
},
{
arg: 'companyFk',
description: 'The company id',
type: 'string',
required: true
},
{
arg: 'invoiceDate',
description: 'The invoice date',
type: 'date',
required: true required: true
} }
], ],
returns: { returns: {
arg: 'data', type: ['object'],
type: 'boolean',
root: true root: true
}, },
http: { http: {
@ -23,10 +34,9 @@ module.exports = function(Self) {
} }
}); });
Self.makeInvoice = async(ctx, ticketsIds, options) => { Self.makeInvoice = async(ctx, invoiceType, companyFk, invoiceDate, options) => {
const models = Self.app.models; const models = Self.app.models;
const date = Date.vnNew(); invoiceDate.setHours(0, 0, 0, 0);
date.setHours(0, 0, 0, 0);
const myOptions = {userId: ctx.req.accessToken.userId}; const myOptions = {userId: ctx.req.accessToken.userId};
let tx; let tx;
@ -40,81 +50,50 @@ module.exports = function(Self) {
} }
let serial; let serial;
let invoiceId;
let invoiceOut;
try { try {
const ticketToInvoice = await Self.rawSql(`
SELECT id
FROM tmp.ticketToInvoice`, null, myOptions);
const ticketsIds = ticketToInvoice.map(ticket => ticket.id);
const tickets = await models.Ticket.find({ const tickets = await models.Ticket.find({
where: { where: {
id: {inq: ticketsIds} id: {inq: ticketsIds}
}, },
fields: ['id', 'clientFk', 'companyFk'] fields: ['id', 'clientFk']
}, myOptions); }, myOptions);
await models.Ticket.canBeInvoiced(ctx, ticketsIds, myOptions);
const [firstTicket] = tickets; const [firstTicket] = tickets;
const clientId = firstTicket.clientFk; const clientId = firstTicket.clientFk;
const companyId = firstTicket.companyFk;
const isSameClient = tickets.every(ticket => ticket.clientFk == clientId);
if (!isSameClient)
throw new UserError(`You can't invoice tickets from multiple clients`);
const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, myOptions); const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, myOptions);
if (!clientCanBeInvoiced) if (!clientCanBeInvoiced)
throw new UserError(`This client can't be invoiced`); throw new UserError(`This client can't be invoiced`);
const ticketCanBeInvoiced = await models.Ticket.canBeInvoiced(ticketsIds, myOptions);
if (!ticketCanBeInvoiced)
throw new UserError(`Some of the selected tickets are not billable`);
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`; const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
const [result] = await Self.rawSql(query, [ const [result] = await Self.rawSql(query, [
clientId, clientId,
companyId, companyFk,
'R' invoiceType,
], myOptions); ], myOptions);
serial = result.serial; serial = result.serial;
await Self.rawSql(` await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], myOptions);
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id FROM vn.ticket
WHERE id IN(?) AND refFk IS NULL
`, [ticketsIds], myOptions);
await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, date], myOptions);
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions); const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions);
if (!resultInvoice)
throw new UserError('No tickets to invoice', 'notInvoiced');
invoiceId = resultInvoice.id; if (serial != 'R' && resultInvoice.id)
await Self.rawSql('CALL invoiceOutBooking(?)', [resultInvoice.id], myOptions);
if (serial != 'R' && invoiceId)
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], myOptions);
invoiceOut = await models.InvoiceOut.findById(invoiceId, {
include: {
relation: 'client'
}
}, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();
return resultInvoice.id;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
throw e; throw e;
} }
if (serial != 'R' && invoiceId)
await models.InvoiceOut.createPdf(ctx, invoiceId);
if (invoiceId) {
ctx.args = {
reference: invoiceOut.ref,
recipientId: invoiceOut.clientFk,
recipient: invoiceOut.client().email
};
await models.InvoiceOut.invoiceEmail(ctx, invoiceOut.ref);
}
return {invoiceFk: invoiceId, serial: serial};
}; };
}; };

View File

@ -1,61 +1,83 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket canBeInvoiced()', () => { describe('ticket canBeInvoiced()', () => {
const userId = 19; const userId = 19;
const ticketId = 11; const ticketId = 11;
const activeCtx = { const ctx = {req: {accessToken: {userId: userId}}};
accessToken: {userId: userId} ctx.req.__ = value => {
return value;
}; };
beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should return falsy for an already invoiced ticket', async() => { it('should return falsy for an already invoiced ticket', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ticket = await models.Ticket.findById(ticketId, null, options); const ticket = await models.Ticket.findById(ticketId, null, options);
await ticket.updateAttribute('refFk', 'T1111111', options); await ticket.updateAttribute('refFk', 'T1111111', options);
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options); await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
vicent marked this conversation as resolved Outdated

create or replace

create or replace
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(false); expect(canBeInvoiced).toEqual(false);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e;
await tx.rollback(); await tx.rollback();
throw e;
} }
expect(error.message).toEqual(`This ticket is already invoiced`);
}); });
it('should return falsy for a ticket with a price of zero', async() => { it('should return falsy for a ticket with a price of zero', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ticket = await models.Ticket.findById(ticketId, null, options); const ticket = await models.Ticket.findById(ticketId, null, options);
await ticket.updateAttribute('totalWithVat', 0, options); await ticket.updateAttribute('totalWithVat', 0, options);
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options); await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(false); expect(canBeInvoiced).toEqual(false);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e;
await tx.rollback(); await tx.rollback();
throw e;
} }
expect(error.message).toEqual(`A ticket with an amount of zero can't be invoiced`);
}); });
it('should return falsy for a ticket shipping in future', async() => { it('should return falsy for a ticket shipping in future', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
@ -66,15 +88,27 @@ describe('ticket canBeInvoiced()', () => {
await ticket.updateAttribute('shipped', shipped, options); await ticket.updateAttribute('shipped', shipped, options);
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options); await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(false); expect(canBeInvoiced).toEqual(false);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e;
await tx.rollback(); await tx.rollback();
throw e;
} }
expect(error.message).toEqual(`Can't invoice to future`);
}); });
it('should return truthy for an invoiceable ticket', async() => { it('should return truthy for an invoiceable ticket', async() => {
@ -83,7 +117,17 @@ describe('ticket canBeInvoiced()', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options); await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(true); expect(canBeInvoiced).toEqual(true);

View File

@ -0,0 +1,115 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket invoiceTickets()', () => {
const userId = 19;
const clientId = 1102;
const activeCtx = {
getLocale: () => {
return 'en';
},
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
};
const ctx = {req: activeCtx};
beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should throw an error when invoicing tickets from multiple clients', async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'makePdfAndNotify');
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ticketsIds = [11, 16];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`You can't invoice tickets from multiple clients`);
});
it(`should throw an error when invoicing a client without tax data checked`, async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'makePdfAndNotify');
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('isTaxDataChecked', false, options);
const ticketsIds = [11];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`This client can't be invoiced`);
});
it('should invoice a ticket, then try again to fail', async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'makePdfAndNotify');
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ticketsIds = [11];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`This ticket is already invoiced`);
});
it('should success to invoice a ticket', async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'makePdfAndNotify');
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const ticketsIds = [11];
const invoicesIds = await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
expect(invoicesIds.length).toBeGreaterThan(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -3,8 +3,9 @@ const LoopBackContext = require('loopback-context');
describe('ticket makeInvoice()', () => { describe('ticket makeInvoice()', () => {
const userId = 19; const userId = 19;
const ticketId = 11; const invoiceType = 'R';
const clientId = 1102; const companyFk = 442;
const invoiceDate = Date.vnNew();
const activeCtx = { const activeCtx = {
getLocale: () => { getLocale: () => {
return 'en'; return 'en';
@ -20,77 +21,6 @@ describe('ticket makeInvoice()', () => {
}); });
}); });
it('should throw an error when invoicing tickets from multiple clients', async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'createPdf');
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const otherClientTicketId = 16;
await models.Ticket.makeInvoice(ctx, [ticketId, otherClientTicketId], options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`You can't invoice tickets from multiple clients`);
});
it(`should throw an error when invoicing a client without tax data checked`, async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'createPdf');
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('isTaxDataChecked', false, options);
await models.Ticket.makeInvoice(ctx, [ticketId], options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`This client can't be invoiced`);
});
it('should invoice a ticket, then try again to fail', async() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'createPdf');
spyOn(invoiceOutModel, 'invoiceEmail');
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Ticket.makeInvoice(ctx, [ticketId], options);
await models.Ticket.makeInvoice(ctx, [ticketId], options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`Some of the selected tickets are not billable`);
});
it('should success to invoice a ticket', async() => { it('should success to invoice a ticket', async() => {
const invoiceOutModel = models.InvoiceOut; const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'createPdf'); spyOn(invoiceOutModel, 'createPdf');
@ -101,10 +31,20 @@ describe('ticket makeInvoice()', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const invoice = await models.Ticket.makeInvoice(ctx, [ticketId], options); const ticketsIds = [11, 16];
await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketsIds], options);
expect(invoice.invoiceFk).toBeDefined(); const invoiceId = await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options);
expect(invoice.serial).toEqual('T');
expect(invoiceId).toBeDefined();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -39,4 +39,5 @@ module.exports = function(Self) {
require('../methods/ticket/collectionLabel')(Self); require('../methods/ticket/collectionLabel')(Self);
require('../methods/ticket/expeditionPalletLabel')(Self); require('../methods/ticket/expeditionPalletLabel')(Self);
require('../methods/ticket/saveSign')(Self); require('../methods/ticket/saveSign')(Self);
require('../methods/ticket/invoiceTickets')(Self);
}; };

View File

@ -275,7 +275,7 @@ class Controller extends Section {
}); });
} }
return this.$http.post(`Tickets/makeInvoice`, params) return this.$http.post(`Tickets/invoiceTickets`, params)
.then(() => this.reload()) .then(() => this.reload())
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced'))); .then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
} }

View File

@ -191,7 +191,7 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
const expectedParams = {ticketsIds: [ticket.id]}; const expectedParams = {ticketsIds: [ticket.id]};
$httpBackend.expectPOST(`Tickets/makeInvoice`, expectedParams).respond(); $httpBackend.expectPOST(`Tickets/invoiceTickets`, expectedParams).respond();
controller.makeInvoice(); controller.makeInvoice();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -163,7 +163,7 @@ export default class Controller extends Section {
makeInvoice() { makeInvoice() {
const ticketsIds = this.checked.map(ticket => ticket.id); const ticketsIds = this.checked.map(ticket => ticket.id);
return this.$http.post(`Tickets/makeInvoice`, {ticketsIds}) return this.$http.post(`Tickets/invoiceTickets`, {ticketsIds})
.then(() => this.$.model.refresh()) .then(() => this.$.model.refresh())
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced'))); .then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
} }