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));
+ }
};