diff --git a/modules/route/back/methods/route/cmr.js b/modules/route/back/methods/route/cmr.js
new file mode 100644
index 000000000..cd7ef57ce
--- /dev/null
+++ b/modules/route/back/methods/route/cmr.js
@@ -0,0 +1,36 @@
+module.exports = Self => {
+ Self.remoteMethodCtx('cmr', {
+ description: 'Returns the cmr',
+ accessType: 'READ',
+ accepts: [
+ {
+ arg: 'id',
+ type: 'number',
+ required: true,
+ description: 'The cmr id',
+ http: {source: 'path'}
+ }
+ ],
+ returns: [
+ {
+ arg: 'body',
+ type: 'file',
+ root: true
+ }, {
+ arg: 'Content-Type',
+ type: 'String',
+ http: {target: 'header'}
+ }, {
+ arg: 'Content-Disposition',
+ type: 'String',
+ http: {target: 'header'}
+ }
+ ],
+ http: {
+ path: '/:id/cmr',
+ verb: 'GET'
+ }
+ });
+
+ Self.cmr = (ctx, id) => Self.printReport(ctx, id, 'cmr');
+};
diff --git a/modules/route/back/models/route.js b/modules/route/back/models/route.js
index 883f4597e..96e7ed04f 100644
--- a/modules/route/back/models/route.js
+++ b/modules/route/back/models/route.js
@@ -14,6 +14,7 @@ module.exports = Self => {
require('../methods/route/driverRouteEmail')(Self);
require('../methods/route/sendSms')(Self);
require('../methods/route/downloadZip')(Self);
+ require('../methods/route/cmr')(Self);
Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000'
diff --git a/modules/ticket/back/methods/ticket/closure.js b/modules/ticket/back/methods/ticket/closure.js
index eee5e28e2..493a2c3a0 100644
--- a/modules/ticket/back/methods/ticket/closure.js
+++ b/modules/ticket/back/methods/ticket/closure.js
@@ -5,177 +5,208 @@ const config = require('vn-print/core/config');
const storage = require('vn-print/core/storage');
module.exports = async function(ctx, Self, tickets, reqArgs = {}) {
- const userId = ctx.req.accessToken.userId;
- if (tickets.length == 0) return;
+ const userId = ctx.req.accessToken.userId;
+ if (tickets.length == 0) return;
- const failedtickets = [];
- for (const ticket of tickets) {
- try {
- await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId});
+ const failedtickets = [];
+ for (const ticket of tickets) {
+ try {
+ await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId});
- const [invoiceOut] = await Self.rawSql(`
- SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
- FROM ticket t
- JOIN invoiceOut io ON io.ref = t.refFk
- JOIN company cny ON cny.id = io.companyFk
- WHERE t.id = ?
- `, [ticket.id]);
+ const [invoiceOut] = await Self.rawSql(`
+ SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
+ FROM ticket t
+ JOIN invoiceOut io ON io.ref = t.refFk
+ JOIN company cny ON cny.id = io.companyFk
+ WHERE t.id = ?
+ `, [ticket.id]);
- const mailOptions = {
- overrideAttachments: true,
- attachments: []
- };
+ const mailOptions = {
+ overrideAttachments: true,
+ attachments: []
+ };
- const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
+ const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
- if (invoiceOut) {
- const args = {
- reference: invoiceOut.ref,
- recipientId: ticket.clientFk,
- recipient: ticket.recipient,
- replyTo: ticket.salesPersonEmail
- };
+ if (invoiceOut) {
+ const args = {
+ reference: invoiceOut.ref,
+ recipientId: ticket.clientFk,
+ recipient: ticket.recipient,
+ replyTo: ticket.salesPersonEmail
+ };
- const invoiceReport = new Report('invoice', args);
- const stream = await invoiceReport.toPdfStream();
+ const invoiceReport = new Report('invoice', args);
+ const stream = await invoiceReport.toPdfStream();
- const issued = invoiceOut.issued;
- const year = issued.getFullYear().toString();
- const month = (issued.getMonth() + 1).toString();
- const day = issued.getDate().toString();
+ const issued = invoiceOut.issued;
+ const year = issued.getFullYear().toString();
+ const month = (issued.getMonth() + 1).toString();
+ const day = issued.getDate().toString();
- const fileName = `${year}${invoiceOut.ref}.pdf`;
+ const fileName = `${year}${invoiceOut.ref}.pdf`;
- // Store invoice
- await storage.write(stream, {
- type: 'invoice',
- path: `${year}/${month}/${day}`,
- fileName: fileName
- });
+ // Store invoice
+ await storage.write(stream, {
+ type: 'invoice',
+ path: `${year}/${month}/${day}`,
+ fileName: fileName
+ });
- await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId});
+ await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId});
- if (isToBeMailed) {
- const invoiceAttachment = {
- filename: fileName,
- content: stream
- };
+ if (isToBeMailed) {
+ const invoiceAttachment = {
+ filename: fileName,
+ content: stream
+ };
- if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
- const exportation = new Report('exportation', args);
- const stream = await exportation.toPdfStream();
- const fileName = `CITES-${invoiceOut.ref}.pdf`;
+ if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
+ const exportation = new Report('exportation', args);
+ const stream = await exportation.toPdfStream();
+ const fileName = `CITES-${invoiceOut.ref}.pdf`;
- mailOptions.attachments.push({
- filename: fileName,
- content: stream
- });
- }
+ mailOptions.attachments.push({
+ filename: fileName,
+ content: stream
+ });
+ }
- mailOptions.attachments.push(invoiceAttachment);
+ mailOptions.attachments.push(invoiceAttachment);
- const email = new Email('invoice', args);
- await email.send(mailOptions);
- }
- } else if (isToBeMailed) {
- const args = {
- id: ticket.id,
- recipientId: ticket.clientFk,
- recipient: ticket.recipient,
- replyTo: ticket.salesPersonEmail
- };
+ const email = new Email('invoice', args);
+ await email.send(mailOptions);
+ }
+ } else if (isToBeMailed) {
+ const args = {
+ id: ticket.id,
+ recipientId: ticket.clientFk,
+ recipient: ticket.recipient,
+ replyTo: ticket.salesPersonEmail
+ };
- const email = new Email('delivery-note-link', args);
- await email.send();
- }
+ const email = new Email('delivery-note-link', args);
+ await email.send();
+ }
- // Incoterms authorization
- const [{firstOrder}] = await Self.rawSql(`
- SELECT COUNT(*) as firstOrder
- FROM ticket t
- JOIN client c ON c.id = t.clientFk
- WHERE t.clientFk = ?
- AND NOT t.isDeleted
- AND c.isVies
- `, [ticket.clientFk]);
+ // Incoterms authorization
+ const [{firstOrder}] = await Self.rawSql(`
+ SELECT COUNT(*) as firstOrder
+ FROM ticket t
+ JOIN client c ON c.id = t.clientFk
+ WHERE t.clientFk = ?
+ AND NOT t.isDeleted
+ AND c.isVies
+ `, [ticket.clientFk]);
- if (firstOrder == 1) {
- const args = {
- id: ticket.clientFk,
- companyId: ticket.companyFk,
- recipientId: ticket.clientFk,
- recipient: ticket.recipient,
- replyTo: ticket.salesPersonEmail
- };
+ if (firstOrder == 1) {
+ const args = {
+ id: ticket.clientFk,
+ companyId: ticket.companyFk,
+ recipientId: ticket.clientFk,
+ recipient: ticket.recipient,
+ replyTo: ticket.salesPersonEmail
+ };
- const email = new Email('incoterms-authorization', args);
- await email.send();
+ const email = new Email('incoterms-authorization', args);
+ await email.send();
- const [sample] = await Self.rawSql(
- `SELECT id
- FROM sample
- WHERE code = 'incoterms-authorization'
- `);
+ const [sample] = await Self.rawSql(
+ `SELECT id
+ FROM sample
+ WHERE code = 'incoterms-authorization'
+ `);
- await Self.rawSql(`
- INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
- `, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
- }
- } catch (error) {
- // Domain not found
- if (error.responseCode == 450)
- return invalidEmail(ticket);
+ await Self.rawSql(`
+ INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
+ `, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
+ }
- // Save tickets on a list of failed ids
- failedtickets.push({
- id: ticket.id,
- stacktrace: error
- });
- }
- }
+ await Self.rawSql(`
+ INSERT INTO cmr (ticketFk, companyFk, addressToFk, addressFromFk, supplierFk, ead)
+ SELECT t.id,
+ com.id,
+ a.id,
+ c2.defaultAddressFk,
+ su.id,
+ t.landed
+ FROM ticket t
+ JOIN ticketState ts ON ts.ticketFk = t.id
+ JOIN state s ON s.id = ts.stateFk
+ JOIN alertLevel al ON al.id = s.alertLevel
+ JOIN client c ON c.id = t.clientFk
+ JOIN address a ON a.id = t.addressFk
+ JOIN province p ON p.id = a.provinceFk
+ JOIN country co ON co.id = p.countryFk
+ JOIN agencyMode am ON am.id = t.agencyModeFk
+ JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
+ JOIN warehouse w ON w.id = t.warehouseFk
+ JOIN company com ON com.id = t.companyFk
+ JOIN client c2 ON c2.id = com.clientFk
+ JOIN supplierAccount sa ON sa.id = com.supplierAccountFk
+ JOIN supplier su ON su.id = sa.supplierFk
+ WHERE shipped BETWEEN util.yesterday() AND util.dayEnd(util.yesterday())
+ AND al.code IN ('PACKED', 'DELIVERED')
+ AND co.code <> 'ES'
+ AND am.name <> 'ABONO'
+ AND w.code = 'ALG'
+ AND dm.code = 'DELIVERY'
+ `);
+ } catch (error) {
+ // Domain not found
+ if (error.responseCode == 450)
+ return invalidEmail(ticket);
- // Send email with failed tickets
- if (failedtickets.length > 0) {
- let body = 'This following tickets have failed:
';
+ // Save tickets on a list of failed ids
+ failedtickets.push({
+ id: ticket.id,
+ stacktrace: error
+ });
+ }
+ }
- for (const ticket of failedtickets) {
- body += `Ticket: ${ticket.id}
-
${ticket.stacktrace}
`;
- }
+ // Send email with failed tickets
+ if (failedtickets.length > 0) {
+ let body = 'This following tickets have failed:
';
- smtp.send({
- to: config.app.reportEmail,
- subject: '[API] Nightly ticket closure report',
- html: body
- });
- }
+ for (const ticket of failedtickets) {
+ body += `Ticket: ${ticket.id}
+
${ticket.stacktrace}
`;
+ }
- async function invalidEmail(ticket) {
- await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
- ticket.clientFk
- ], {userId});
+ smtp.send({
+ to: config.app.reportEmail,
+ subject: '[API] Nightly ticket closure report',
+ html: body
+ });
+ }
- const oldInstance = `{"email": "${ticket.recipient}"}`;
- const newInstance = `{"email": ""}`;
- await Self.rawSql(`
- INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
- VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
- ticket.clientFk,
- oldInstance,
- newInstance
- ], {userId});
+ async function invalidEmail(ticket) {
+ await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
+ ticket.clientFk
+ ], {userId});
- const body = `No se ha podido enviar el albarán ${ticket.id}
- al cliente ${ticket.clientFk} - ${ticket.clientName}
- porque la dirección de email "${ticket.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.`;
+ const oldInstance = `{"email": "${ticket.recipient}"}`;
+ const newInstance = `{"email": ""}`;
+ await Self.rawSql(`
+ INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
+ VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
+ ticket.clientFk,
+ oldInstance,
+ newInstance
+ ], {userId});
- smtp.send({
- to: ticket.salesPersonEmail,
- subject: 'No se ha podido enviar el albarán',
- html: body
- });
- }
+ const body = `No se ha podido enviar el albarán ${ticket.id}
+ al cliente ${ticket.clientFk} - ${ticket.clientName}
+ porque la dirección de email "${ticket.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: ticket.salesPersonEmail,
+ subject: 'No se ha podido enviar el albarán',
+ html: body
+ });
+ }
};
diff --git a/print/templates/reports/cmr/assets/css/import.js b/print/templates/reports/cmr/assets/css/import.js
new file mode 100644
index 000000000..37a98dfdd
--- /dev/null
+++ b/print/templates/reports/cmr/assets/css/import.js
@@ -0,0 +1,12 @@
+const Stylesheet = require(`vn-print/core/stylesheet`);
+
+const path = require('path');
+const vnPrintPath = path.resolve('print');
+
+module.exports = new Stylesheet([
+ `${vnPrintPath}/common/css/spacing.css`,
+ `${vnPrintPath}/common/css/misc.css`,
+ `${vnPrintPath}/common/css/layout.css`,
+ `${vnPrintPath}/common/css/report.css`,
+ `${__dirname}/style.css`])
+ .mergeStyles();
diff --git a/print/templates/reports/cmr/assets/css/style.css b/print/templates/reports/cmr/assets/css/style.css
new file mode 100644
index 000000000..201afc3b6
--- /dev/null
+++ b/print/templates/reports/cmr/assets/css/style.css
@@ -0,0 +1,101 @@
+html {
+ font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+ margin: 10px;
+ font-size: 22px;
+}
+.mainTable, .specialTable, .categoryTable {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: inherit;
+}
+.mainTable td {
+ width: 50%;
+ border: 1px solid black;
+ vertical-align: top;
+ padding: 15px;
+ font-size: inherit;
+}
+.signTable {
+ height: 12%;
+}
+.signTable td {
+ width: calc(100% / 3);
+ border: 1px solid black;
+ vertical-align: top;
+ font-size: inherit;
+ padding: 15px;
+ border-top: none;
+}
+#title {
+ font-weight: bold;
+ font-size: 85px;
+}
+hr {
+ border: 1px solid #cccccc;
+ height: 0px;
+ border-radius: 25px;
+}
+#cellHeader {
+ border: 0px;
+ text-align: center;
+ vertical-align: middle;
+}
+#label, #merchandiseLabels {
+ font-size: 13px;
+}
+#merchandiseLabels {
+ border: none;
+}
+.imgSection {
+ text-align: center;
+ height: 200px;
+ overflow: hidden;
+}
+img {
+ object-fit: contain;
+ width: 100%;
+ height: 100%;
+}
+#lineBreak {
+ white-space: pre-line;
+}
+.specialTable td {
+ border: 1px solid black;
+ vertical-align: top;
+ padding: 15px;
+ font-size: inherit;
+ border-top: none;
+ border-bottom: none;
+}
+.specialTable #itemCategoryList {
+ width: 70%;
+ padding-top: 10px;
+}
+.categoryTable {
+ padding-bottom: none;
+}
+.categoryTable td {
+ vertical-align: top;
+ font-size: inherit;
+ border: none;
+ padding: 5px;
+ overflow: hidden;
+}
+.categoryTable #merchandiseLabels {
+ border-bottom: 4px solid #cccccc;
+ padding: none;
+}
+#merchandiseDetail {
+ font-weight: bold;
+ padding-top: 10px;
+}
+#merchandiseData {
+ font-weight: bold;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+#merchandiseLabels td {
+ padding-bottom: 11px;
+ max-width: 300px;
+}
\ No newline at end of file
diff --git a/print/templates/reports/cmr/assets/images/signature.png b/print/templates/reports/cmr/assets/images/signature.png
new file mode 100644
index 000000000..0961df764
Binary files /dev/null and b/print/templates/reports/cmr/assets/images/signature.png differ
diff --git a/print/templates/reports/cmr/cmr.html b/print/templates/reports/cmr/cmr.html
new file mode 100644
index 000000000..8fd6f097c
--- /dev/null
+++ b/print/templates/reports/cmr/cmr.html
@@ -0,0 +1,212 @@
+
+
+
+
+ 1. Remitente / Expediteur / Sender
+ + {{data.senderName}} + {{data.senderStreet}} + {{data.senderPostCode}} {{data.senderCity}} {{(data.senderCountry) ? `(${data.senderCountry})` : null}} + |
+
+ CMR + {{data.cmrFk}} + |
+
+ 2. Consignatario / Destinataire / Consignee
+ + {{data.deliveryAddressFk}} + {{data.deliveryName}} + {{data.deliveryPhone || data.clientPhone}} + {{((data.deliveryPhone || data.clientPhone) && data.deliveryMobile) ? '/' : null}} + {{data.deliveryMobile}} + |
+
+ 16. Transportista / Transporteur / Carrier
+ + {{data.carrierName}} + {{data.carrierStreet}} + {{data.carrierPostalCode}} {{data.carrierCity}} {{(data.carrierCountry) ? `(${data.carrierCountry})` : null}} + |
+
+
+ 3. Lugar y fecha de entrega /
+ Lieu et date de livraison /
+ Place and date of delivery
+
+ + {{data.deliveryStreet}} + {{data.deliveryPostalCode}} {{data.deliveryCity}} {{(data.deliveryCountry) ? `(${data.deliveryCountry})` : null}} + {{(data.ead) ? formatDate(data.ead, '%d/%m/%Y') : null}} + + |
+
+ 17. Porteadores sucesivos / Transporteurs succesifs / Succesive Carriers
+ + |
+
+
+ 4. Lugar y fecha de carga /
+ Lieu et date del prise en charge de la merchandise /
+ Place and date of taking over the goods
+
+ + {{data.loadStreet}} + {{data.loadPostalCode}} {{data.loadCity}} {{(data.loadCountry) ? `(${data.loadCountry})` : null}} + {{formatDate(data.created, '%d/%m/%Y')}} + |
+
+
+ 18. Obervaciones del transportista /
+ Reserves et observations du transporteur /
+ Carrier's reservations and observations
+
+ + {{data.truckPlate}} + {{data.observations}} + |
+
+ 5. Documentos anexos / Documents annexes / Documents attached
+ + |
+
+
+ 7 & 8. Número de bultos y clase de embalage /
+ Number of packages and packaging class /
+ Nombre de colis et classe d'emballage
+
+ +
+ {{data.packagesList}}
+
+ |
+
+
+ {{data.merchandiseDetail}}
+
+ |
+
+
+ 13. Instrucciones del remitente /
+ Instrunstions de l'expèditeur / Sender
+ instruccions
+
+ + {{data.senderInstruccions}} + |
+
+
+ 19. Estipulaciones particulares /
+ Conventions particulieres /
+ Special agreements
+
+ + {{data.specialAgreements}} + |
+
+
+ 14. Forma de pago /
+ Prescriptions d'affranchissement /
+ Instruction as to payment for carriage
+
+ + {{data.paymentInstruccions}} + |
+
+ 20. A pagar por / Être payé pour / To be paid by
+ + |
+
+ 21. Formalizado en / Etabile a / Estabilshed in
+ + {{data.loadStreet}} + {{data.loadPostalCode}} {{data.loadCity}} {{(data.loadCountry) ? `(${data.loadCountry})` : null}} + |
+
+ 15. Reembolso / Remboursement / Cash on delivery
+ + |
+
+
+ 22. Firma y sello del remitente /
+ Signature et timbre de l'expèditeur /
+ Signature and stamp of the sender
+
+ +
+
+
+ |
+
+
+ 23. Firma y sello del transportista /
+ Signature et timbre du transporteur /
+ Signature and stamp of the carrier
+
+ +
+
+
+ |
+
+
+ 24. Firma y sello del consignatario /
+ Signature et timbre du destinataire /
+ Signature and stamp of the consignee
+
+ +
+
+
+ |
+