diff --git a/db/changes/10501-november/00-ticket_canMerge.sql b/db/changes/10501-november/00-ticket_canMerge.sql new file mode 100644 index 000000000..6db3637ac --- /dev/null +++ b/db/changes/10501-november/00-ticket_canMerge.sql @@ -0,0 +1,9 @@ +DROP PROCEDURE IF EXISTS vn.ticket_canMerge; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canMerge`(vDated DATE, vScopeDays INT, vLitersMax INT, vLinesMax INT, vWarehouseFk INT) +BEGIN + CALL vn.ticket_canbePostponed(vDated,TIMESTAMPADD(DAY, vScopeDays, vDated),vLitersMax,vLinesMax,vWarehouseFk); +END $$ +DELIMITER ; diff --git a/db/changes/10501-november/00-ticket_canbePostposed.sql b/db/changes/10501-november/00-ticket_canbePostposed.sql new file mode 100644 index 000000000..6ad9df154 --- /dev/null +++ b/db/changes/10501-november/00-ticket_canbePostposed.sql @@ -0,0 +1,75 @@ +DROP PROCEDURE IF EXISTS vn.ticket_canbePostponed; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canbePostponed`(vOriginDated DATE, vFutureDated DATE, vLitersMax INT, vLinesMax INT, vWarehouseFk INT) +BEGIN +/** + * Devuelve un listado de tickets susceptibles de fusionarse con otros tickets en el futuro + * + * @param vOriginDated Fecha en cuestión + * @param vFutureDated Fecha en el futuro a sondear + * @param vLitersMax Volumen máximo de los tickets a catapultar + * @param vLinesMax Número máximo de lineas de los tickets a catapultar + * @param vWarehouseFk Identificador de vn.warehouse + */ + DROP TEMPORARY TABLE IF EXISTS tmp.filter; + CREATE TEMPORARY TABLE tmp.filter + (INDEX (id)) + SELECT sv.ticketFk id, + GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt, + CAST(sum(litros) AS DECIMAL(10,0)) liters, + CAST(count(*) AS DECIMAL(10,0)) `lines`, + st.name state, + sub2.id ticketFuture, + t.landed originETD, + sub2.landed destETD, + sub2.iptd tfIpt, + sub2.state tfState, + t.clientFk, + t.warehouseFk, + ts.alertLevel, + t.shipped, + sub2.shipped tfShipped, + t.workerFk + FROM vn.saleVolume sv + JOIN vn.sale s ON s.id = sv.saleFk + JOIN vn.item i ON i.id = s.itemFk + JOIN vn.ticket t ON t.id = sv.ticketFk + JOIN vn.address a ON a.id = t.addressFk + JOIN vn.province p ON p.id = a.provinceFk + JOIN vn.country c ON c.id = p.countryFk + JOIN vn.ticketState ts ON ts.ticketFk = t.id + JOIN vn.state st ON st.id = ts.stateFk + JOIN vn.alertLevel al ON al.id = ts.alertLevel + LEFT JOIN vn.ticketParking tp ON tp.ticketFk = t.id + LEFT JOIN ( + SELECT * + FROM ( + SELECT + t.addressFk , + t.id, + t.landed, + t.shipped, + st.name state, + GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) iptd + FROM vn.ticket t + JOIN vn.ticketState ts ON ts.ticketFk = t.id + JOIN vn.state st ON st.id = ts.stateFk + JOIN vn.sale s ON s.ticketFk = t.id + JOIN vn.item i ON i.id = s.itemFk + WHERE t.shipped BETWEEN vFutureDated + AND util.dayend(vFutureDated) + AND t.warehouseFk = vWarehouseFk + GROUP BY t.id + ) sub + GROUP BY sub.addressFk + ) sub2 ON sub2.addressFk = t.addressFk AND t.id != sub2.id + WHERE t.shipped BETWEEN vOriginDated AND util.dayend(vOriginDated) + AND t.warehouseFk = vWarehouseFk + AND al.code = 'FREE' + AND tp.ticketFk IS NULL + GROUP BY sv.ticketFk + HAVING liters <= IFNULL(vLitersMax, 9999) AND `lines` <= IFNULL(vLinesMax, 9999) AND ticketFuture; +END$$ +DELIMITER ; diff --git a/loopback/locale/es.json b/loopback/locale/es.json index a41315dd1..a12cf1c09 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -237,5 +237,6 @@ "Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador", "Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente", "You don't have grant privilege": "No tienes privilegios para dar privilegios", - "You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario" + "You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario", + "MOVE_TICKET_CONFIRMATION": "Ticket {{id}} ({{originDated}}) fusionado con {{tfId}} ({{futureDated}}) [{{fullPath}}]" } diff --git a/modules/ticket/back/methods/ticket-future/getTicketsFuture.js b/modules/ticket/back/methods/ticket-future/getTicketsFuture.js new file mode 100644 index 000000000..5b83e50be --- /dev/null +++ b/modules/ticket/back/methods/ticket-future/getTicketsFuture.js @@ -0,0 +1,179 @@ +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; +const buildFilter = require('vn-loopback/util/filter').buildFilter; +const mergeFilters = require('vn-loopback/util/filter').mergeFilters; + +module.exports = Self => { + Self.remoteMethodCtx('getTicketsFuture', { + description: 'Find all instances of the model matched by filter from the data source.', + accessType: 'READ', + accepts: [ + { + arg: 'originDated', + type: 'date', + description: 'The date in question', + required: true + }, + { + arg: 'futureDated', + type: 'date', + description: 'The date to probe', + required: true + }, + { + arg: 'litersMax', + type: 'number', + description: 'Maximum volume of tickets to catapult', + required: true + }, + { + arg: 'linesMax', + type: 'number', + description: 'Maximum number of lines of tickets to catapult', + required: true + }, + { + arg: 'warehouseFk', + type: 'number', + description: 'Warehouse identifier', + required: true + }, + { + arg: 'filter', + type: 'object', + description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string` + } + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/getTicketsFuture`, + verb: 'GET' + } + }); + + Self.getTicketsFuture = async (ctx, options) => { + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + const conn = Self.dataSource.connector; + const args = ctx.args; + const stmts = []; + let stmt; + + stmt = new ParameterizedSQL( + `CALL vn.ticket_canbePostponed(?,?,?,?,?)`, + [args.originDated, args.futureDated, args.litersMax, args.linesMax, args.warehouseFk]); + + stmts.push(stmt); + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.sale_getProblems'); + + stmt = new ParameterizedSQL(` + CREATE TEMPORARY TABLE tmp.sale_getProblems + (INDEX (ticketFk)) + ENGINE = MEMORY + SELECT f.id ticketFk, f.clientFk, f.warehouseFk, f.shipped + FROM tmp.filter f + LEFT JOIN alertLevel al ON al.id = f.alertLevel + WHERE (al.code = 'FREE' OR f.alertLevel IS NULL)`); + stmts.push(stmt); + + stmts.push('CALL ticket_getProblems(FALSE)'); + + stmt = new ParameterizedSQL(` + SELECT f.*, tp.* + FROM tmp.filter f + LEFT JOIN tmp.ticket_problems tp ON tp.ticketFk = f.id`); + + if (args.problems != undefined && (!args.from && !args.to)) + throw new UserError('Choose a date range or days forward'); + + const myWhere = buildFilter(ctx.args, (param, value) => { + switch (param) { + // case 'search': + // return /^\d+$/.test(value) + // ? {'t.id': {inq: value}} + // : {'t.nickname': {like: `%${value}%`}}; + case 'shipped': + return {'shipped': {like: `%${value}%`}}; + // case 'refFk': + // return {'t.refFk': value}; + + // case 'provinceFk': + // return {'t.provinceFk': value}; + // case 'stateFk': + // return {'t.stateFk': value}; + // case 'alertLevel': + // return {'t.alertLevel': value}; + // case 'pending': + // if (value) { + // return {and: [ + // {'t.alertLevel': 0}, + // {'t.alertLevelCode': {nin: [ + // 'OK', + // 'BOARDING', + // 'PRINTED', + // 'PRINTED_AUTO', + // 'PICKER_DESIGNED' + // ]}} + // ]}; + // } else { + // return {and: [ + // {'t.alertLevel': {gt: 0}} + // ]}; + // } + // case 'agencyModeFk': + // case 'warehouseFk': + // param = `t.${param}`; + // return {[param]: value}; + } + }); + let finalFilter = {}; + finalFilter = mergeFilters(finalFilter, {where: myWhere}); + if (finalFilter.where) + stmt.merge(conn.makeWhere(finalFilter.where)); + + let condition; + let hasProblem; + let range; + let hasWhere; + switch (args.problems) { + case true: + condition = `or`; + hasProblem = true; + range = {neq: null}; + hasWhere = true; + break; + + case false: + condition = `and`; + hasProblem = null; + range = null; + hasWhere = true; + break; + } + + const problems = {[condition]: [ + {'tp.isFreezed': hasProblem}, + {'tp.risk': hasProblem}, + {'tp.hasTicketRequest': hasProblem}, + {'tp.itemShortage': range} + ]}; + + if (hasWhere) + stmt.merge(conn.makeWhere(problems)); + + const ticketsIndex = stmts.push(stmt) - 1; + + stmts.push( + `DROP TEMPORARY TABLE + tmp.filter, + tmp.ticket_problems`); + + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await conn.executeStmt(sql, myOptions); + return result[ticketsIndex]; + }; +}; \ No newline at end of file diff --git a/modules/ticket/back/methods/ticket-future/moveTicketsFuture.js b/modules/ticket/back/methods/ticket-future/moveTicketsFuture.js new file mode 100644 index 000000000..940a83c33 --- /dev/null +++ b/modules/ticket/back/methods/ticket-future/moveTicketsFuture.js @@ -0,0 +1,68 @@ +const LoopBackContext = require('loopback-context'); + +module.exports = Self => { + Self.remoteMethodCtx('moveTicketsFuture', { + description: 'Move specified tickets to the future', + accessType: 'WRITE', + accepts: [ + { + arg: 'tickets', + type: ['object'], + description: 'The array of tickets', + required: false + } + ], + returns: { + type: 'string', + root: true + }, + http: { + path: `/moveTicketsFuture`, + verb: 'POST' + } + }); + + Self.moveTicketsFuture = async (ctx, tickets, options) => { + const loopBackContext = LoopBackContext.getCurrentContext(); + const httpCtx = {req: loopBackContext.active}; + const models = Self.app.models; + const myOptions = {}; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + for (let ticket of tickets) { + console.log(ticket); + try { + if(!ticket.id || !ticket.ticketFuture) continue; + await models.Sale.updateAll({ticketFk: ticket.id}, {ticketFk: ticket.ticketFuture}, myOptions); + await models.Ticket.setDeleted(ctx, ticket.id, myOptions); + if (tx) + { + const httpRequest = httpCtx.req.http.req; + const $t = httpRequest.__; + const origin = httpRequest.headers.origin; + const fullPath = `${origin}/#!/ticket/${ticket.ticketFuture}/summary`; + const message = $t('MOVE_TICKET_CONFIRMATION', { + originDated: new Date(ticket.originETD).toLocaleDateString('es-ES'), + futureDated: new Date(ticket.destETD).toLocaleDateString('es-ES'), + id: ticket.id, + tfId: ticket.ticketFuture, + fullPath + }); + await tx.commit(); + await models.Chat.sendCheckingPresence(httpCtx, ticket.workerFk, message); + } + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; + }; +}; \ No newline at end of file diff --git a/modules/ticket/back/model-config.json b/modules/ticket/back/model-config.json index 21e800b36..82d8e3b1a 100644 --- a/modules/ticket/back/model-config.json +++ b/modules/ticket/back/model-config.json @@ -91,5 +91,8 @@ }, "TicketConfig": { "dataSource": "vn" + }, + "TicketFuture": { + "dataSource": "vn" } } diff --git a/modules/ticket/back/models/ticket-future.json b/modules/ticket/back/models/ticket-future.json new file mode 100644 index 000000000..5d0b24dbc --- /dev/null +++ b/modules/ticket/back/models/ticket-future.json @@ -0,0 +1,12 @@ +{ + "name": "TicketFuture", + "base": "PersistedModel", + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] + } \ No newline at end of file diff --git a/modules/ticket/back/models/ticket.js b/modules/ticket/back/models/ticket.js index c05130552..649a83160 100644 --- a/modules/ticket/back/models/ticket.js +++ b/modules/ticket/back/models/ticket.js @@ -4,6 +4,8 @@ const LoopBackContext = require('loopback-context'); module.exports = Self => { // Methods require('./ticket-methods')(Self); + require('../methods/ticket-future/getTicketsFuture')(Self); + require('../methods/ticket-future/moveTicketsFuture')(Self); Self.observe('before save', async function(ctx) { const loopBackContext = LoopBackContext.getCurrentContext(); diff --git a/modules/ticket/front/future-search-panel/index.html b/modules/ticket/front/future-search-panel/index.html new file mode 100644 index 000000000..4f5486d62 --- /dev/null +++ b/modules/ticket/front/future-search-panel/index.html @@ -0,0 +1,86 @@ +
+ |
+ + Problems + | ++ Origin ID + | ++ Origin ETD + | ++ Origin State + | ++ IPT + | ++ Liters + | ++ Available Lines + | ++ Destination ID + | ++ Destination ETD + | ++ Destination State + | ++ IPT + | +
---|---|---|---|---|---|---|---|---|---|---|---|
+ |
+
+ |
+ + {{::ticket.id}} + | ++ {{::ticket.originETD | date: 'dd/MM/yyyy'}} + | ++ {{::ticket.state}} + | +{{::ticket.ipt}} | +{{::ticket.liters}} | +{{::ticket.lines}} | ++ {{::ticket.ticketFuture}} + | ++ {{::ticket.destETD | date: 'dd/MM/yyyy'}} + | ++ {{::ticket.tfState}} + | +{{::ticket.tfIpt}} | +