diff --git a/db/changes/232801/00-aclTicket.sql b/db/changes/232801/00-aclTicket.sql new file mode 100644 index 0000000000..0e00751e68 --- /dev/null +++ b/db/changes/232801/00-aclTicket.sql @@ -0,0 +1,2 @@ +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) +VALUES('Ticket', 'invoiceTickets', 'WRITE', 'ALLOW', 'ROLE', 'employee'); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 106df379eb..97d995c8d7 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -303,5 +303,8 @@ "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", - "Valid priorities": "Prioridades válidas: %d" -} + "Valid priorities": "Prioridades válidas: %d", + "There are no tickets to bill": "There are no tickets to bill", + "Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}", + "Base negativa para los tickets: 10": "Base negativa para los tickets: 10" +} \ No newline at end of file diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js index ddd008942e..f74fdc8080 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js @@ -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,14 @@ 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, - 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; + const invoiceId = await models.Ticket.makeInvoice(ctx, 'G', args.companyFk, options); if (tx) await tx.commit(); + + return invoiceId; } catch (e) { if (tx) await tx.rollback(); throw e; } - - return invoiceId; }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js index a999437c06..a48664b302 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js +++ b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js @@ -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, diff --git a/modules/ticket/back/methods/ticket/canBeInvoiced.js b/modules/ticket/back/methods/ticket/canBeInvoiced.js index 6b8f9e71a4..b297ad8c41 100644 --- a/modules/ticket/back/methods/ticket/canBeInvoiced.js +++ b/modules/ticket/back/methods/ticket/canBeInvoiced.js @@ -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,17 +34,25 @@ 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(); @@ -54,6 +65,6 @@ module.exports = function(Self) { return isInvoiced || priceZero || shippingInFuture; }); - return !(invalidTickets || hasSomeNegativeBase); + return !(invalidTickets || hasAnyNegativeBase); }; }; diff --git a/modules/ticket/back/methods/ticket/invoiceTickets.js b/modules/ticket/back/methods/ticket/invoiceTickets.js new file mode 100644 index 0000000000..262ee6c5af --- /dev/null +++ b/modules/ticket/back/methods/ticket/invoiceTickets.js @@ -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; + 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, myOptions); + invoicesIds.push(invoiceId); + } +}; + diff --git a/modules/ticket/back/methods/ticket/makeInvoice.js b/modules/ticket/back/methods/ticket/makeInvoice.js index ee82b874fc..955b84c2bb 100644 --- a/modules/ticket/back/methods/ticket/makeInvoice.js +++ b/modules/ticket/back/methods/ticket/makeInvoice.js @@ -6,15 +6,20 @@ module.exports = function(Self) { accessType: 'WRITE', accepts: [ { - arg: 'ticketsIds', - description: 'The tickets id', - type: ['number'], + arg: 'type', + description: 'The invoice type', + type: 'string', + required: true + }, + { + arg: 'companyFk', + description: 'The company id', + type: 'string', required: true } ], returns: { - arg: 'data', - type: 'boolean', + type: ['object'], root: true }, http: { @@ -23,7 +28,7 @@ module.exports = function(Self) { } }); - Self.makeInvoice = async(ctx, ticketsIds, options) => { + Self.makeInvoice = async(ctx, type, companyFk, options) => { const models = Self.app.models; const date = Date.vnNew(); date.setHours(0, 0, 0, 0); @@ -41,80 +46,52 @@ 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); + const ticketCanBeInvoiced = await models.Ticket.canBeInvoiced(ctx, ticketsIds, myOptions); + if (!ticketCanBeInvoiced || !ticketToInvoice.length) + throw new UserError(`Some of the selected tickets are not billable`); + 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, + type, ], 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); 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' && invoiceId) + if (serial != 'R' && resultInvoice.id) 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}; }; }; diff --git a/modules/ticket/back/models/ticket-methods.js b/modules/ticket/back/models/ticket-methods.js index e5a8e8d942..f0c85ecc45 100644 --- a/modules/ticket/back/models/ticket-methods.js +++ b/modules/ticket/back/models/ticket-methods.js @@ -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); }; diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js index 17a34ad597..dd518671b7 100644 --- a/modules/ticket/front/descriptor-menu/index.js +++ b/modules/ticket/front/descriptor-menu/index.js @@ -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'))); } diff --git a/modules/ticket/front/index/index.js b/modules/ticket/front/index/index.js index 55229eabbb..b3afc838cc 100644 --- a/modules/ticket/front/index/index.js +++ b/modules/ticket/front/index/index.js @@ -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'))); }