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
18 changed files with 389 additions and 225 deletions

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES('Ticket', 'invoiceTickets', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -177,5 +177,6 @@
"Invalid quantity": "Invalid quantity",
"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",
"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 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",
"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

@ -14,7 +14,7 @@ module.exports = Self => {
Self.validatesPresenceOf('street', {
message: 'Street cannot be empty'
});
Self.validatesPresenceOf('city', {
message: 'City cannot be empty'
});
@ -89,7 +89,7 @@ module.exports = Self => {
};
const country = await Self.app.models.Country.findOne(filter);
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))
err();
@ -401,7 +401,7 @@ module.exports = Self => {
Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
const models = Self.app.models;
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');
if (!canEditCredit) {

View File

@ -68,7 +68,7 @@
</span>
</vn-td>
<vn-td number>{{::clientInforma.rating}}</vn-td>
<vn-td>{{::clientInforma.recommendedCredit | currency: 'EUR': 2}}</vn-td>
<vn-td number>{{::clientInforma.recommendedCredit | currency: 'EUR'}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>

View File

@ -64,7 +64,7 @@
<vn-label-value label="Channel"
value="{{$ctrl.summary.contactChannel.name}}">
</vn-label-value>
<vn-label-value label="Business type"
<vn-label-value label="Business type"
value="{{$ctrl.summary.businessType.description}}">
</vn-label-value>
</vn-one>
@ -270,7 +270,7 @@
info="Invoices minus payments plus orders not yet invoiced">
</vn-label-value>
<vn-label-value label="Credit"
value="{{$ctrl.summary.credit | currency: 'EUR':2 }} "
value="{{$ctrl.summary.credit | currency: 'EUR'}} "
ng-class="{alert: $ctrl.summary.credit > $ctrl.summary.creditInsurance ||
($ctrl.summary.credit && $ctrl.summary.creditInsurance == null)}"
info="Verdnatura's maximum risk">
@ -296,6 +296,9 @@
value="{{$ctrl.summary.rating}}"
info="Value from 1 to 20. The higher the better value">
</vn-label-value>
<vn-label-value label="Recommended credit"
value="{{$ctrl.summary.recommendedCredit | currency: 'EUR'}}">
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-horizontal>
<vn-horizontal>

View File

@ -1,5 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('invoiceClient', {
description: 'Make a invoice of a client',
@ -56,7 +54,6 @@ module.exports = Self => {
const minShipped = Date.vnNew();
minShipped.setFullYear(args.maxShipped.getFullYear() - 1);
let invoiceId;
try {
const client = await models.Client.findById(args.clientId, {
fields: ['id', 'hasToInvoiceByAddress']
@ -77,56 +74,21 @@ module.exports = Self => {
], options);
}
// Check negative bases
let query =
`SELECT COUNT(*) isSpanishCompany
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,
const invoiceType = 'G';
const invoiceId = await models.Ticket.makeInvoice(
ctx,
invoiceType,
args.companyFk,
'G'
], 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;
args.invoiceDate,
options
);
if (tx) await tx.commit();
return invoiceId;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
return invoiceId;
};
};

View File

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

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethodCtx('canBeInvoiced', {
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 $t = ctx.req.__; // $translate
if (typeof options == 'object')
Object.assign(myOptions, options);
@ -31,29 +34,43 @@ module.exports = function(Self) {
where: {
id: {inq: ticketsIds}
},
fields: ['id', 'refFk', 'shipped', 'totalWithVat']
fields: ['id', 'refFk', 'shipped', 'totalWithVat', 'companyFk']
}, myOptions);
const [firstTicket] = tickets;
const companyFk = firstTicket.companyFk;
const query = `
SELECT vn.hasSomeNegativeBase(t.id) AS hasSomeNegativeBase
FROM ticket t
WHERE id IN(?)`;
const ticketBases = await Self.rawSql(query, [ticketsIds], myOptions);
const hasSomeNegativeBase = ticketBases.some(
ticketBases => ticketBases.hasSomeNegativeBase
);
const query =
`SELECT COUNT(*) isSpanishCompany
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
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 invalidTickets = tickets.some(ticket => {
tickets.some(ticket => {
const shipped = new Date(ticket.shipped);
const shippingInFuture = shipped.getTime() > today.getTime();
const isInvoiced = ticket.refFk;
const priceZero = ticket.totalWithVat == 0;
if (shippingInFuture)
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,104 @@
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(`
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice
vicent marked this conversation as resolved Outdated

CREATE OR REPLACE

CREATE OR REPLACE
(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',
accepts: [
{
arg: 'ticketsIds',
description: 'The tickets id',
type: ['number'],
arg: 'invoiceType',
description: 'The invoice type',
type: 'string',
required: true
},
{
arg: 'companyFk',
description: 'The company id',
type: 'string',
required: true
},
{
arg: 'invoiceDate',
description: 'The invoice date',
type: 'date',
required: true
}
],
returns: {
arg: 'data',
type: 'boolean',
type: ['object'],
root: true
},
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 date = Date.vnNew();
date.setHours(0, 0, 0, 0);
invoiceDate.setHours(0, 0, 0, 0);
const myOptions = {userId: ctx.req.accessToken.userId};
let tx;
@ -40,81 +50,50 @@ module.exports = function(Self) {
}
let serial;
let invoiceId;
let invoiceOut;
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({
where: {
id: {inq: ticketsIds}
},
fields: ['id', 'clientFk', 'companyFk']
fields: ['id', 'clientFk']
}, myOptions);
await models.Ticket.canBeInvoiced(ctx, ticketsIds, 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 clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, myOptions);
if (!clientCanBeInvoiced)
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 [result] = await Self.rawSql(query, [
clientId,
companyId,
'R'
companyFk,
invoiceType,
], myOptions);
serial = result.serial;
await Self.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(?) AND refFk IS NULL
`, [ticketsIds], myOptions);
await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, date], myOptions);
await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], 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();
return resultInvoice.id;
} catch (e) {
if (tx) await tx.rollback();
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,81 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket canBeInvoiced()', () => {
const userId = 19;
const ticketId = 11;
const activeCtx = {
accessToken: {userId: userId}
const ctx = {req: {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() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ticket = await models.Ticket.findById(ticketId, null, options);
await ticket.updateAttribute('refFk', 'T1111111', options);
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options);
await models.Ticket.rawSql(`
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice
vicent marked this conversation as resolved Outdated

create or replace

create or replace
(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);
await tx.rollback();
} catch (e) {
error = e;
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() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ticket = await models.Ticket.findById(ticketId, null, options);
await ticket.updateAttribute('totalWithVat', 0, options);
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options);
await models.Ticket.rawSql(`
CREATE OR REPLACE 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);
await tx.rollback();
} catch (e) {
error = e;
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() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
@ -66,15 +86,26 @@ describe('ticket canBeInvoiced()', () => {
await ticket.updateAttribute('shipped', shipped, options);
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options);
await models.Ticket.rawSql(`
CREATE OR REPLACE 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);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
throw e;
}
expect(error.message).toEqual(`Can't invoice to future`);
});
it('should return truthy for an invoiceable ticket', async() => {
@ -83,7 +114,16 @@ describe('ticket canBeInvoiced()', () => {
try {
const options = {transaction: tx};
const canBeInvoiced = await models.Ticket.canBeInvoiced([ticketId], options);
await models.Ticket.rawSql(`
CREATE OR REPLACE 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);

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()', () => {
const userId = 19;
const ticketId = 11;
const clientId = 1102;
const invoiceType = 'R';
const companyFk = 442;
const invoiceDate = Date.vnNew();
const activeCtx = {
getLocale: () => {
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() => {
const invoiceOutModel = models.InvoiceOut;
spyOn(invoiceOutModel, 'createPdf');
@ -101,10 +31,20 @@ describe('ticket makeInvoice()', () => {
try {
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();
expect(invoice.serial).toEqual('T');
const invoiceId = await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options);
expect(invoiceId).toBeDefined();
await tx.rollback();
} catch (e) {

View File

@ -39,4 +39,5 @@ module.exports = function(Self) {
require('../methods/ticket/collectionLabel')(Self);
require('../methods/ticket/expeditionPalletLabel')(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.vnApp.showSuccess(this.$t('Ticket invoiced')));
}

View File

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

View File

@ -163,7 +163,7 @@ export default class Controller extends Section {
makeInvoice() {
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.vnApp.showSuccess(this.$t('Ticket invoiced')));
}