diff --git a/db/routines/vn/procedures/ticket_close.sql b/db/routines/vn/procedures/ticket_close.sql index e2dcef9a5a..2b6a33fbaa 100644 --- a/db/routines/vn/procedures/ticket_close.sql +++ b/db/routines/vn/procedures/ticket_close.sql @@ -18,6 +18,7 @@ BEGIN DECLARE vWithPackage BOOL; DECLARE vHasToInvoice BOOL; DECLARE vSerial VARCHAR(2); + DECLARE vStateCode VARCHAR(45); DECLARE cur CURSOR FOR SELECT ticketFk FROM tmp.ticket_close; @@ -80,27 +81,14 @@ BEGIN AND getSpecialPrice(e.freightItemFk, vClientFk) > 0 GROUP BY e.freightItemFk); - IF(vHasDailyInvoice) AND vHasToInvoice THEN - SELECT invoiceSerial(vClientFk, vCompanyFk, 'quick') INTO vSerial; - IF vSerial IS NULL THEN - CALL util.throw('Cannot booking without a serial'); - END IF; - CALL ticket_setState(vCurTicketFk, 'DELIVERED'); - - IF vIsTaxDataChecked THEN - CALL invoiceOut_newFromClient( - vClientFk, - vSerial, - vShipped, - vCompanyFk, - NULL, - NULL, - vNewInvoiceId); - END IF; + IF vHasDailyInvoice AND vHasToInvoice THEN + SET vStateCode = 'DELIVERED'; ELSE - CALL ticket_setState(vCurTicketFk, (SELECT vn.getAlert3State(vCurTicketFk))); + SELECT vn.getAlert3State(vCurTicketFk) INTO vStateCode; END IF; + CALL ticket_setState(vCurTicketFk, vStateCode); + END LOOP; CLOSE cur; diff --git a/modules/ticket/back/methods/ticket/closeAll.js b/modules/ticket/back/methods/ticket/closeAll.js index 71122808cc..307cc011dd 100644 --- a/modules/ticket/back/methods/ticket/closeAll.js +++ b/modules/ticket/back/methods/ticket/closeAll.js @@ -1,4 +1,5 @@ -const closure = require('./closure'); +const smtp = require('vn-print/core/smtp'); +const config = require('vn-print/core/config'); module.exports = Self => { Self.remoteMethodCtx('closeAll', { @@ -25,122 +26,45 @@ module.exports = Self => { Self.closeAll = async(ctx, options) => { const userId = ctx.req.accessToken.userId; const myOptions = {userId}; - + let tx; if (typeof options == 'object') Object.assign(myOptions, options); - let tx; - // IMPORTANT: Due to its high cost in production, wrapping this process in a transaction may cause timeouts. - + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } const toDate = Date.vnNew(); toDate.setHours(0, 0, 0, 0); toDate.setDate(toDate.getDate() - 1); - const tickets = await Self.rawSql(` - SELECT t.id, - t.clientFk, - t.companyFk, - c.id clientFk, - c.name clientName, - c.email recipient, - c.salesPersonFk, - c.isToBeMailed, - c.hasToInvoice, - c.hasDailyInvoice, - eu.email salesPersonEmail, - t.addressFk - FROM ticket t - JOIN agencyMode am ON am.id = t.agencyModeFk - JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission - JOIN ticketState ts ON ts.ticketFk = t.id - JOIN alertLevel al ON al.id = ts.alertLevel - JOIN client c ON c.id = t.clientFk - JOIN province p ON p.id = c.provinceFk - JOIN country co ON co.id = p.countryFk - LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk - JOIN ticketConfig tc ON TRUE - WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered')) - AND t.shipped BETWEEN ? - INTERVAL tc.closureDaysAgo DAY AND util.dayEnd(?) - AND t.refFk IS NULL - GROUP BY t.id + await Self.rawSql(` + DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close; + CREATE TEMPORARY TABLE tmp.ticket_close + ENGINE = MEMORY + SELECT + DISTINCT t.id ticketFk, + t.clientFk, + c.name clientName, + c.email recipient, + eu.email salesPersonEmail, + t.addressFk, + c.hasToInvoiceByAddress, + t.totalWithVat, + t.companyFk + FROM ticket t + JOIN agencyMode am ON am.id = t.agencyModeFk + JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN alertLevel al ON al.id = ts.alertLevel + JOIN client c ON c.id = t.clientFk + LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk + JOIN ticketConfig tc ON TRUE + WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered')) + AND t.shipped BETWEEN ? - INTERVAL tc.closureDaysAgo DAY AND util.dayEnd(?) + AND t.refFk IS NULL; + CALL ticket_close(); `, [toDate, toDate], myOptions); - const ticketIds = tickets.map(ticket => ticket.id); - await Self.rawSql(` - INSERT INTO util.debug (variable, value) - VALUES ('nightInvoicing', ?) - `, [ticketIds.join(',')], myOptions); - - await Self.rawSql(` - WITH ticketNotInvoiceable AS( - SELECT JSON_OBJECT( - 'tickets', - JSON_ARRAYAGG( - JSON_OBJECT( - 'ticketId', ticketFk, - 'reason', reason, - 'clientId', clientFk - ) - ) - )errors - FROM ( - SELECT ticketFk, - CONCAT_WS(', ', - IF(hasErrorToInvoice, 'Facturar', NULL), - IF(hasErrorTaxDataChecked, 'Datos comprobados', NULL), - IF(hasErrorDeleted, 'Eliminado', NULL), - IF(hasErrorItemTaxCountry, 'Impuesto no informado', NULL), - IF(hasErrorAddress, 'Sin dirección', NULL), - IF(hasErrorInfoTaxAreaWorld, 'Datos exportaciones', NULL)) reason, - clientFk - FROM ( - SELECT t.id ticketFk, - SUM(NOT c.hasToInvoice) hasErrorToInvoice, - SUM(NOT c.isTaxDataChecked) hasErrorTaxDataChecked, - SUM(t.isDeleted) hasErrorDeleted, - SUM(itc.id IS NULL) hasErrorItemTaxCountry, - SUM(a.id IS NULL) hasErrorAddress, - SUM(ios.code IS NOT NULL - AND(ad.customsAgentFk IS NULL - OR ad.incotermsFk IS NULL)) hasErrorInfoTaxAreaWorld, - t.clientFk clientFk - FROM ticket t - LEFT JOIN address ad ON ad.id = t.addressFk - JOIN sale s ON s.ticketFk = t.id - JOIN item i ON i.id = s.itemFk - JOIN supplier su ON su.id = t.companyFk - JOIN agencyMode am ON am.id = t.agencyModeFk - JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission - JOIN ticketState ts ON ts.ticketFk = t.id - JOIN alertLevel al ON al.id = ts.alertLevel - JOIN client c ON c.id = t.clientFk - JOIN province p ON p.id = c.provinceFk - JOIN ticketConfig tc ON TRUE - LEFT JOIN autonomy a ON a.id = p.autonomyFk - JOIN country co ON co.id = p.countryFk - LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk - LEFT JOIN itemTaxCountry itc ON itc.itemFk = i.id - AND itc.countryFk = su.countryFk - LEFT JOIN vn.invoiceOutSerial ios ON ios.taxAreaFk = 'WORLD' - AND ios.code = invoiceSerial(t.clientFk, t.companyFk, 'multiple') - WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered')) - AND t.shipped BETWEEN ? - INTERVAL tc.closureDaysAgo DAY AND util.dayEnd(?) - AND t.refFk IS NULL - AND c.hasDailyInvoice - GROUP BY ticketFk - HAVING hasErrorToInvoice - OR hasErrorTaxDataChecked - OR hasErrorDeleted - OR hasErrorItemTaxCountry - OR hasErrorAddress - OR hasErrorInfoTaxAreaWorld - )sub - )sub2 - ) SELECT IF(errors = '{"tickets": null}', - 'No errors', - util.notification_send('invoice-ticket-closure', errors, NULL)) - FROM ticketNotInvoiceable`, [toDate, toDate], myOptions); - - await closure(ctx, Self, tickets, myOptions); await Self.rawSql(` UPDATE ticket t @@ -157,11 +81,94 @@ module.exports = Self => { AND tob.id IS NULL AND t.routeFk`, [toDate, toDate], myOptions); + const [clients] = await Self.rawSql(` + SELECT clientFk clientId, + clientName, + recipient, + salesPersonEmail, + addressFk addressId, + companyFk, + SUM(totalWithVat) total, + 'quick' serialType + FROM tmp.ticket_close + GROUP BY IF (hasToInvoiceByAddress, addressFk, clientFk), companyFk + HAVING total > 0; + DROP TEMPORARY TABLE tmp.ticket_close; + `, [], myOptions); + if (tx) await tx.commit(); + const failedClients = []; + // Only for testing + const nestedTransaction = options?.transaction ? myOptions : {}; + for (const client of clients) { + ctx.args = { + ...client, + invoiceDate: Date.vnNew(), + maxShipped: Date.vnNew() + }; + try { + const id = await Self.app.models.InvoiceOut.invoiceClient(ctx, nestedTransaction); + if (id) + await Self.app.models.InvoiceOut.makePdfAndNotify(ctx, id, null, nestedTransaction); + } catch (error) { + await Self.rawSql(` + INSERT INTO util.debug (variable, value) + VALUES ('invoicingTicketError', ?) + `, [client.clientId + ' - ' + error]); + + if (error.responseCode == 450) { + await invalidEmail(client); + continue; + } + + failedClients.push({ + id: client.clientId, + address: client.addressId, + error + }); + } + } + + if (failedClients.length > 0) { + let body = 'This following tickets have failed:

'; + + for (const client of failedClients) { + body += `Client: ${client.id} + Address: ${client.address} +
${client.error}

`; + } + + smtp.send({ + to: config.app.reportEmail, + subject: '[API] Nightly ticket closure report', + html: body, + }).catch(err => console.error(err)); + } + return { message: 'Success' }; }; + + async function invalidEmail(client) { + await Self.rawSql( + `UPDATE client SET email = NULL WHERE id = ?`, + [client.clientId], + myOptions + ); + + const body = `No se ha podido facturar al cliente ${client.clientId} - ${client.clientName} + porque la dirección de email "${client.recipient}" no es correcta + o no está disponible.

+ Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente. + Actualiza la dirección de email con una correcta.`; + + smtp.send({ + to: client.salesPersonEmail, + subject: 'No se ha podido enviar el albarán', + html: body, + }).catch(err => console.error(err)); + } };