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..a8d44cd05 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' @@ -28,5 +29,5 @@ module.exports = Self => { const routeMaxKm = 1000; if (routeTotalKm > routeMaxKm || this.kmStart > this.kmEnd) err(); - } + }; }; diff --git a/modules/ticket/back/methods/ticket/closure.js b/modules/ticket/back/methods/ticket/closure.js index eee5e28e2..9f9aec9bd 100644 --- a/modules/ticket/back/methods/ticket/closure.js +++ b/modules/ticket/back/methods/ticket/closure.js @@ -5,177 +5,177 @@ 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}); + }; + } catch (error) { + // Domain not found + if (error.responseCode == 450) + return invalidEmail(ticket); - // Save tickets on a list of failed ids - failedtickets.push({ - id: ticket.id, - stacktrace: error - }); - } - } + // Save tickets on a list of failed ids + failedtickets.push({ + id: ticket.id, + stacktrace: error + }); + } + } - // Send email with failed tickets - if (failedtickets.length > 0) { - let body = 'This following tickets have failed:

'; + // Send email with failed tickets + if (failedtickets.length > 0) { + let body = 'This following tickets have failed:

'; - for (const ticket of failedtickets) { - body += `Ticket: ${ticket.id} -
${ticket.stacktrace}

`; - } + for (const ticket of failedtickets) { + body += `Ticket: ${ticket.id} +
${ticket.stacktrace}

`; + } - smtp.send({ - to: config.app.reportEmail, - subject: '[API] Nightly ticket closure report', - html: body - }); - } + smtp.send({ + to: config.app.reportEmail, + subject: '[API] Nightly ticket closure report', + html: body + }); + } - async function invalidEmail(ticket) { - await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [ - ticket.clientFk - ], {userId}); + async function invalidEmail(ticket) { + await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [ + ticket.clientFk + ], {userId}); - 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}); + 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}); - 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 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 - }); - } + 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/cmr.html b/print/templates/reports/cmr/cmr.html new file mode 100644 index 000000000..c6a9e79d6 --- /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}} +
+
+ + + + + + + + + + + + + + + +
6. Marcas y números / Brands and numbers / Marques et numéros9. Naturaleza de la merc. / Nature of goods / Nature des marchandises10. nº Estadístico / Statistical no. / n° statistique11. Peso bruto / Gross weight / Poids brut (kg)12. Volumen / Volume (m3)
{{merchandise.ticketFk}}{{merchandise.name}}N/A{{merchandise.weight}}{{merchandise.volume}}
+
+ {{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 + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/print/templates/reports/cmr/cmr.js b/print/templates/reports/cmr/cmr.js new file mode 100644 index 000000000..c939e5152 --- /dev/null +++ b/print/templates/reports/cmr/cmr.js @@ -0,0 +1,45 @@ +const config = require(`vn-print/core/config`); +const vnReport = require('../../../core/mixins/vn-report.js'); +const md5 = require('md5'); +const fs = require('fs-extra'); + +const prefixBase64 = 'data:image/png;base64,'; + +module.exports = { + name: 'cmr', + mixins: [vnReport], + async serverPrefetch() { + this.data = await this.findOneFromDef('data', [this.id]); + if (this.data.ticketFk) { + this.merchandises = await this.rawSqlFromDef('merchandise', [this.data.ticketFk]); + this.signature = await this.findOneFromDef('signature', [this.data.ticketFk]); + } else + this.merchandises = null; + + this.senderStamp = (this.data.senderStamp) + ? `${prefixBase64} ${this.data.senderStamp.toString('base64')}` + : null; + this.deliveryStamp = (this.data.deliveryStamp) + ? `${prefixBase64} ${this.data.deliveryStamp.toString('base64')}` + : null; + }, + props: { + id: { + type: Number, + required: true, + description: 'The cmr id' + }, + }, + computed: { + signPath() { + if (!this.signature) return; + + const signatureName = this.signature.signature + const hash = md5(signatureName.toString()).substring(0, 3); + const file = `${config.storage.root}/${hash}/${signatureName}.png`; + if (!fs.existsSync(file)) return null; + + return `${prefixBase64} ${Buffer.from(fs.readFileSync(file), 'utf8').toString('base64')}`; + }, + } +}; \ No newline at end of file diff --git a/print/templates/reports/cmr/locale/es.yml b/print/templates/reports/cmr/locale/es.yml new file mode 100644 index 000000000..79f481b35 --- /dev/null +++ b/print/templates/reports/cmr/locale/es.yml @@ -0,0 +1 @@ +reportName: cmr \ No newline at end of file diff --git a/print/templates/reports/cmr/options.json b/print/templates/reports/cmr/options.json new file mode 100644 index 000000000..9151ca63b --- /dev/null +++ b/print/templates/reports/cmr/options.json @@ -0,0 +1,3 @@ +{ + "format": "A4" +} \ No newline at end of file diff --git a/print/templates/reports/cmr/sql/data.sql b/print/templates/reports/cmr/sql/data.sql new file mode 100644 index 000000000..9708c4483 --- /dev/null +++ b/print/templates/reports/cmr/sql/data.sql @@ -0,0 +1,52 @@ +SELECT c.id cmrFk, + t.id ticketFk, + c.truckPlate, + c.observations, + c.senderInstruccions, + c.paymentInstruccions, + c.specialAgreements, + c.created, + c.packagesList, + c.merchandiseDetail, + c.ead, + s.name carrierName, + s.street carrierStreet, + s.postCode carrierPostCode, + s.city carrierCity, + cou.country carrierCountry, + s2.name senderName, + s2.street senderStreet, + s2.postCode senderPostCode, + s2.city senderCity, + cou2.country senderCountry, + a.street deliveryStreet, + a.id deliveryAddressFk, + a.postalCode deliveryPostalCode, + a.city deliveryCity, + a.nickname deliveryName, + a.phone deliveryPhone, + a.mobile deliveryMobile, + cou3.country deliveryCountry, + cl.phone clientPhone, + a2.street loadStreet, + a2.postalCode loadPostalCode, + a2.city loadCity, + cou4.country loadCountry, + co.stamp senderStamp, + s.stamp deliveryStamp + FROM cmr c + LEFT JOIN supplier s ON s.id = c.supplierFk + LEFT JOIN country cou ON cou.id = s.countryFk + LEFT JOIN company co ON co.id = c.companyFk + LEFT JOIN supplierAccount sa ON sa.id = co.supplierAccountFk + LEFT JOIN supplier s2 ON s2.id = sa.supplierFk + LEFT JOIN country cou2 ON cou2.id = s2.countryFk + LEFT JOIN `address` a ON a.id = c.addressToFk + LEFT JOIN province p ON p.id = a.provinceFk + LEFT JOIN country cou3 ON cou3.id = p.countryFk + LEFT JOIN client cl ON cl.id = a.clientFk + LEFT JOIN `address` a2 ON a2.id = c.addressFromFk + LEFT JOIN province p2 ON p2.id = a2.provinceFk + LEFT JOIN country cou4 ON cou4.id = p2.countryFk + LEFT JOIN ticket t ON t.cmrFk = c.id + WHERE c.id = ? \ No newline at end of file diff --git a/print/templates/reports/cmr/sql/merchandise.sql b/print/templates/reports/cmr/sql/merchandise.sql new file mode 100644 index 000000000..cab597caa --- /dev/null +++ b/print/templates/reports/cmr/sql/merchandise.sql @@ -0,0 +1,11 @@ +SELECT s.ticketFk, + ic.name, + CAST(SUM(sv.weight) AS DECIMAL(10,2)) `weight`, + CAST(SUM(sv.volume) AS DECIMAL(10,3)) volume + FROM sale s + JOIN saleVolume sv ON sv.saleFk = s.id + JOIN item i ON i.id = s.itemFk + JOIN itemType it ON it.id = i.typeFk + JOIN itemCategory ic ON ic.id = it.categoryFk + WHERE sv.ticketFk = ? + GROUP BY ic.id \ No newline at end of file diff --git a/print/templates/reports/cmr/sql/signature.sql b/print/templates/reports/cmr/sql/signature.sql new file mode 100644 index 000000000..7ec7380a5 --- /dev/null +++ b/print/templates/reports/cmr/sql/signature.sql @@ -0,0 +1,5 @@ +SELECT dc.id `signature` + FROM ticket t + JOIN ticketDms dt ON dt.ticketFk = t.id + LEFT JOIN dms dc ON dc.id = dt.dmsFk + WHERE t.id = ? \ No newline at end of file