refs #5874 feat: refactorizado proceso facturación #1658
|
@ -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}}"
|
||||||
}
|
}
|
|
@ -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}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
|||||||
|
</vn-label-value>
|
||||||
</vn-one>
|
</vn-one>
|
||||||
</vn-horizontal>
|
</vn-horizontal>
|
||||||
<vn-horizontal>
|
<vn-horizontal>
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
jgallego
commented
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
jgallego
commented
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
vicent
commented
Ho fa el proc invoiceOut_new, que se crida. Ho fa el proc invoiceOut_new, que se crida.
|
|||||||
|
};
|
||||||
|
|
|
@ -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};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
jgallego
commented
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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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')));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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')));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
sense decimals, aprofita y modifiques tb el crédit son int