test → dev #1701

Merged
guillermo merged 10 commits from test into dev 2023-08-04 09:39:34 +00:00
13 changed files with 646 additions and 144 deletions
Showing only changes of commit 16353dfddc - Show all commits

View File

@ -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');
};

View File

@ -14,6 +14,7 @@ module.exports = Self => {
require('../methods/route/driverRouteEmail')(Self); require('../methods/route/driverRouteEmail')(Self);
require('../methods/route/sendSms')(Self); require('../methods/route/sendSms')(Self);
require('../methods/route/downloadZip')(Self); require('../methods/route/downloadZip')(Self);
require('../methods/route/cmr')(Self);
Self.validate('kmStart', validateDistance, { Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000' message: 'Distance must be lesser than 1000'

View File

@ -5,177 +5,208 @@ const config = require('vn-print/core/config');
const storage = require('vn-print/core/storage'); const storage = require('vn-print/core/storage');
module.exports = async function(ctx, Self, tickets, reqArgs = {}) { module.exports = async function(ctx, Self, tickets, reqArgs = {}) {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
if (tickets.length == 0) return; if (tickets.length == 0) return;
const failedtickets = []; const failedtickets = [];
for (const ticket of tickets) { for (const ticket of tickets) {
try { try {
await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId}); await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId});
const [invoiceOut] = await Self.rawSql(` const [invoiceOut] = await Self.rawSql(`
SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
FROM ticket t FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk JOIN invoiceOut io ON io.ref = t.refFk
JOIN company cny ON cny.id = io.companyFk JOIN company cny ON cny.id = io.companyFk
WHERE t.id = ? WHERE t.id = ?
`, [ticket.id]); `, [ticket.id]);
const mailOptions = { const mailOptions = {
overrideAttachments: true, overrideAttachments: true,
attachments: [] attachments: []
}; };
const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed; const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
if (invoiceOut) { if (invoiceOut) {
const args = { const args = {
reference: invoiceOut.ref, reference: invoiceOut.ref,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const invoiceReport = new Report('invoice', args); const invoiceReport = new Report('invoice', args);
const stream = await invoiceReport.toPdfStream(); const stream = await invoiceReport.toPdfStream();
const issued = invoiceOut.issued; const issued = invoiceOut.issued;
const year = issued.getFullYear().toString(); const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString(); const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString(); const day = issued.getDate().toString();
const fileName = `${year}${invoiceOut.ref}.pdf`; const fileName = `${year}${invoiceOut.ref}.pdf`;
// Store invoice // Store invoice
await storage.write(stream, { await storage.write(stream, {
type: 'invoice', type: 'invoice',
path: `${year}/${month}/${day}`, path: `${year}/${month}/${day}`,
fileName: fileName 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) { if (isToBeMailed) {
const invoiceAttachment = { const invoiceAttachment = {
filename: fileName, filename: fileName,
content: stream content: stream
}; };
if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') { if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
const exportation = new Report('exportation', args); const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream(); const stream = await exportation.toPdfStream();
const fileName = `CITES-${invoiceOut.ref}.pdf`; const fileName = `CITES-${invoiceOut.ref}.pdf`;
mailOptions.attachments.push({ mailOptions.attachments.push({
filename: fileName, filename: fileName,
content: stream content: stream
}); });
} }
mailOptions.attachments.push(invoiceAttachment); mailOptions.attachments.push(invoiceAttachment);
const email = new Email('invoice', args); const email = new Email('invoice', args);
await email.send(mailOptions); await email.send(mailOptions);
} }
} else if (isToBeMailed) { } else if (isToBeMailed) {
const args = { const args = {
id: ticket.id, id: ticket.id,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const email = new Email('delivery-note-link', args); const email = new Email('delivery-note-link', args);
await email.send(); await email.send();
} }
// Incoterms authorization // Incoterms authorization
const [{firstOrder}] = await Self.rawSql(` const [{firstOrder}] = await Self.rawSql(`
SELECT COUNT(*) as firstOrder SELECT COUNT(*) as firstOrder
FROM ticket t FROM ticket t
JOIN client c ON c.id = t.clientFk JOIN client c ON c.id = t.clientFk
WHERE t.clientFk = ? WHERE t.clientFk = ?
AND NOT t.isDeleted AND NOT t.isDeleted
AND c.isVies AND c.isVies
`, [ticket.clientFk]); `, [ticket.clientFk]);
if (firstOrder == 1) { if (firstOrder == 1) {
const args = { const args = {
id: ticket.clientFk, id: ticket.clientFk,
companyId: ticket.companyFk, companyId: ticket.companyFk,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const email = new Email('incoterms-authorization', args); const email = new Email('incoterms-authorization', args);
await email.send(); await email.send();
const [sample] = await Self.rawSql( const [sample] = await Self.rawSql(
`SELECT id `SELECT id
FROM sample FROM sample
WHERE code = 'incoterms-authorization' WHERE code = 'incoterms-authorization'
`); `);
await Self.rawSql(` await Self.rawSql(`
INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?) INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
`, [ticket.clientFk, sample.id, ticket.companyFk], {userId}); `, [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 await Self.rawSql(`
failedtickets.push({ INSERT INTO cmr (ticketFk, companyFk, addressToFk, addressFromFk, supplierFk, ead)
id: ticket.id, SELECT t.id,
stacktrace: error 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 // Save tickets on a list of failed ids
if (failedtickets.length > 0) { failedtickets.push({
let body = 'This following tickets have failed:<br/><br/>'; id: ticket.id,
stacktrace: error
});
}
}
for (const ticket of failedtickets) { // Send email with failed tickets
body += `Ticket: <strong>${ticket.id}</strong> if (failedtickets.length > 0) {
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`; let body = 'This following tickets have failed:<br/><br/>';
}
smtp.send({ for (const ticket of failedtickets) {
to: config.app.reportEmail, body += `Ticket: <strong>${ticket.id}</strong>
subject: '[API] Nightly ticket closure report', <br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
html: body }
});
}
async function invalidEmail(ticket) { smtp.send({
await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [ to: config.app.reportEmail,
ticket.clientFk subject: '[API] Nightly ticket closure report',
], {userId}); html: body
});
}
const oldInstance = `{"email": "${ticket.recipient}"}`; async function invalidEmail(ticket) {
const newInstance = `{"email": ""}`; await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
await Self.rawSql(` ticket.clientFk
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance) ], {userId});
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk,
oldInstance,
newInstance
], {userId});
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong> const oldInstance = `{"email": "${ticket.recipient}"}`;
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong> const newInstance = `{"email": ""}`;
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta await Self.rawSql(`
o no está disponible.<br/><br/> INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente. VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
Actualiza la dirección de email con una correcta.`; ticket.clientFk,
oldInstance,
newInstance
], {userId});
smtp.send({ const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
to: ticket.salesPersonEmail, al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
subject: 'No se ha podido enviar el albarán', porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta
html: body o no está disponible.<br/><br/>
}); 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
});
}
}; };

View File

@ -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();

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,212 @@
<!DOCTYPE html>
<html>
<body>
<table class="mainTable">
<tr>
<td>
<span id="label">1. Remitente / Expediteur / Sender</span>
<hr>
<b>{{data.senderName}}</b><br>
{{data.senderStreet}}<br>
{{data.senderPostCode}} {{data.senderCity}} {{(data.senderCountry) ? `(${data.senderCountry})` : null}}
</td>
<td id="cellHeader">
<span id="title">CMR</span><br>
{{data.cmrFk}}
</td>
</tr>
<tr>
<td>
<span id="label">2. Consignatario / Destinataire / Consignee</span>
<hr>
<b>{{data.deliveryAddressFk}}<br>
{{data.deliveryName}}<br>
{{data.deliveryPhone || data.clientPhone}}
{{((data.deliveryPhone || data.clientPhone) && data.deliveryMobile) ? '/' : null}}
{{data.deliveryMobile}}</b>
</td>
<td>
<span id="label">16. Transportista / Transporteur / Carrier</span>
<hr>
<b>{{data.carrierName}}</b><br>
{{data.carrierStreet}}<br>
{{data.carrierPostalCode}} {{data.carrierCity}} {{(data.carrierCountry) ? `(${data.carrierCountry})` : null}}
</td>
</tr>
<tr>
<td>
<span id="label">
3. Lugar y fecha de entrega /
Lieu et date de livraison /
Place and date of delivery
</span>
<hr>
<b>{{data.deliveryStreet}}<br>
{{data.deliveryPostalCode}} {{data.deliveryCity}} {{(data.deliveryCountry) ? `(${data.deliveryCountry})` : null}}<br>
{{(data.ead) ? formatDate(data.ead, '%d/%m/%Y') : null}}</b>
</td>
<td>
<span id="label">17. Porteadores sucesivos / Transporteurs succesifs / Succesive Carriers</span>
<hr>
</td>
</tr>
<tr>
<td>
<span id="label">
4. Lugar y fecha de carga /
Lieu et date del prise en charge de la merchandise /
Place and date of taking over the goods
</span>
<hr>
<b>{{data.loadStreet}}<br>
{{data.loadPostalCode}} {{data.loadCity}} {{(data.loadCountry) ? `(${data.loadCountry})` : null}}<br>
{{formatDate(data.created, '%d/%m/%Y')}}</b>
</td>
<td rowspan="2">
<span id="label">
18. Obervaciones del transportista /
Reserves et observations du transporteur /
Carrier's reservations and observations
</span>
<hr>
<b>{{data.truckPlate}}</b><br>
{{data.observations}}
</td>
</tr>
<tr>
<td>
<span id="label">5. Documentos anexos / Documents annexes / Documents attached</span>
<hr>
</td>
</tr>
</table>
<table class="specialTable">
<tr>
<td>
<span id="label">
7 & 8. Número de bultos y clase de embalage /
Number of packages and packaging class /
Nombre de colis et classe d'emballage
</span>
<hr>
<div id="lineBreak">
<b>{{data.packagesList}}</b>
</div>
</td>
<td id="itemCategoryList">
<table class="categoryTable">
<tr id="merchandiseLabels">
<td>6. Marcas y números / Brands and numbers / Marques et numéros</td>
<td>9. Naturaleza de la merc. / Nature of goods / Nature des marchandises</td>
<td>10. nº Estadístico / Statistical no. / n° statistique</td>
<td>11. Peso bruto / Gross weight / Poids brut (kg)</td>
<td>12. Volumen / Volume (m3)</td>
</tr>
<tr v-for="merchandise in merchandises" id="merchandiseData">
<td>{{merchandise.ticketFk}}</td>
<td>{{merchandise.name}}</td>
<td>N/A</td>
<td>{{merchandise.weight}}</td>
<td>{{merchandise.volume}}</td>
</tr>
</table>
<div v-if="!merchandises" id="merchandiseDetail">
{{data.merchandiseDetail}}
</div>
</td>
</tr>
</table>
<table class="mainTable">
<tr>
<td>
<span id="label">
13. Instrucciones del remitente /
Instrunstions de l'expèditeur / Sender
instruccions
</span>
<hr>
<b>{{data.senderInstruccions}}</b>
</td>
<td>
<span id="label">
19. Estipulaciones particulares /
Conventions particulieres /
Special agreements
</span>
<hr>
<b>{{data.specialAgreements}}</b>
</td>
</tr>
<tr>
<td>
<span id="label">
14. Forma de pago /
Prescriptions d'affranchissement /
Instruction as to payment for carriage
</span>
<hr>
<b>{{data.paymentInstruccions}}</b>
</td>
<td>
<span id="label">20. A pagar por / Être payé pour / To be paid by</span>
<hr>
</td>
</tr>
<tr>
<td>
<span id="label">21. Formalizado en / Etabile a / Estabilshed in</span>
<hr>
<b>{{data.loadStreet}}</b><br>
{{data.loadPostalCode}} {{data.loadCity}} {{(data.loadCountry) ? `(${data.loadCountry})` : null}} <br>
</td>
<td>
<span id="label">15. Reembolso / Remboursement / Cash on delivery</span>
<hr>
</td>
</tr>
</table>
<table class="signTable">
<tr>
<td>
<span id="label">
22. Firma y sello del remitente /
Signature et timbre de l'expèditeur /
Signature and stamp of the sender
</span>
<hr>
<div class="imgSection">
<img v-bind:src="getReportSrc('signature.png')"/>
</div>
</td>
<td>
<span id="label">
23. Firma y sello del transportista /
Signature et timbre du transporteur /
Signature and stamp of the carrier
</span>
<hr>
<div class="imgSection">
<img v-bind:src="dmsPath"/>
</div>
</td>
<td>
<span id="label">
24. Firma y sello del consignatario /
Signature et timbre du destinataire /
Signature and stamp of the consignee
</span>
<hr>
<div class="imgSection">
<img v-bind:src="dmsPath(true)"/>
</div>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,38 @@
const config = require(`vn-print/core/config`);
const vnReport = require('../../../core/mixins/vn-report.js');
const md5 = require('md5');
const fs = require('fs-extra');
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.signatures = await this.findOneFromDef('signatures', [this.data.ticketFk]);
}
},
props: {
id: {
type: Number,
required: true,
description: 'The cmr id'
},
},
methods: {
dmsPath(isClient) {
if (!this.signatures) return;
const signatureName = (isClient)
? this.signatures.clientSignature
: this.signatures.deliverySignature;
const hash = md5(signatureName.toString()).substring(0, 3);
const file = `${config.storage.root}/${hash}/${signatureName}.png`;
if (!fs.existsSync(file)) return null;
return `data:image/png;base64, ${Buffer.from(fs.readFileSync(file), 'utf8').toString('base64')}`;
}
}
};

View File

@ -0,0 +1 @@
reportName: cmr

View File

@ -0,0 +1,3 @@
{
"format": "A4"
}

View File

@ -0,0 +1,49 @@
SELECT c.id cmrFk,
c.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
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
WHERE c.id = ?

View File

@ -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

View File

@ -0,0 +1,7 @@
SELECT dc.id clientSignature, dd.id deliverySignature
FROM ticket t
JOIN ticketDms dt ON dt.ticketFk = t.id
LEFT JOIN dms dc ON dc.id = dt.dmsFk
JOIN `route` r ON r.id = t.routeFk
LEFT JOIN dms dd ON dd.id = r.deliverySignFk
WHERE t.id = ?