refs #5874 feat: refactorizado proceso facturación #1658
|
@ -0,0 +1,2 @@
|
|||
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
|
||||
VALUES('Ticket', 'invoiceTickets', 'WRITE', 'ALLOW', 'ROLE', 'employee');
|
|
@ -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}}"
|
||||
}
|
||||
|
|
|
@ -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}}"
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
</vn-label-value>
|
||||
</vn-one>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
jgallego
commented
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
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',
|
||||
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};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
jgallego
commented
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);
|
||||
|
||||
|
|
|
@ -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()', () => {
|
||||
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) {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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')));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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')));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
sense decimals, aprofita y modifiques tb el crédit son int