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 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/modules/ticket/front/future-search-panel/index.js b/modules/ticket/front/future-search-panel/index.js new file mode 100644 index 000000000..bbc2148e7 --- /dev/null +++ b/modules/ticket/front/future-search-panel/index.js @@ -0,0 +1,30 @@ +import ngModule from '../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +class Controller extends SearchPanel { + constructor($, $element) { + super($, $element); + this.filter = this.$.filter; + } + + get from() { + return this._from; + } + + set from(value) { + this._from = value; + } + + get to() { + return this._to; + } + + set to(value) { + this._to = value; + } +} + +ngModule.vnComponent('vnFutureTicketSearchPanel', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/ticket/front/future-search-panel/locale/en.yml b/modules/ticket/front/future-search-panel/locale/en.yml new file mode 100644 index 000000000..0440752c5 --- /dev/null +++ b/modules/ticket/front/future-search-panel/locale/en.yml @@ -0,0 +1 @@ +Future tickets: Tickets a futuro \ No newline at end of file diff --git a/modules/ticket/front/future-search-panel/locale/es.yml b/modules/ticket/front/future-search-panel/locale/es.yml new file mode 100644 index 000000000..e459c3cd9 --- /dev/null +++ b/modules/ticket/front/future-search-panel/locale/es.yml @@ -0,0 +1,13 @@ +Future tickets: Tickets a futuro +Origin date: Fecha origen +Destination date: Fecha destino +Origin ETD: ETD origen +Destination ETD: ETD destino +Max Lines Origin: Líneas máx. origen +Max Liters Origin: Litros máx. origen +Origin ITP: ITP origen +Destination ITP: ITP destino +With problems: Con problemas +Warehouse: Almacén +Origin Agrupated State: Estado agrupado origen +Destination Agrupated State: Estado agrupado destino diff --git a/modules/ticket/front/future/index.html b/modules/ticket/front/future/index.html new file mode 100644 index 000000000..9dbb5a4ca --- /dev/null +++ b/modules/ticket/front/future/index.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + 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}}
+
+
+
+ + + + \ No newline at end of file diff --git a/modules/ticket/front/future/index.js b/modules/ticket/front/future/index.js new file mode 100644 index 000000000..6dda5812d --- /dev/null +++ b/modules/ticket/front/future/index.js @@ -0,0 +1,107 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + this.$checkAll = false; + + const originDated = new Date(); + const futureDated = new Date(); + const warehouseFk = 1; + const litersMax = 9999; + const linesMax = 9999; + + this.defaultFilter = { + originDated, + futureDated, + warehouseFk, + litersMax, + linesMax + }; + + this.smartTableOptions = { + activeButtons: { + search: true + }, + columns: [{ + field:'problems', + searchable: false + }, + { + field:'ETD', + searchable: false + }, + { + field:'tfETD', + searchable: false + }] + }; + } + + get checked() { + const tickets = this.$.model.data || []; + const checkedLines = []; + for (let ticket of tickets) { + if (ticket.checked) + checkedLines.push(ticket); + } + + return checkedLines; + } + + moveTicketsFuture() + { + let params = { + tickets: this.checked + }; + this.$http.post('Tickets/moveTicketsFuture', params); + this.reload(); + } + + compareDate(date) { + let today = new Date(); + today.setHours(0, 0, 0, 0); + let timeTicket = new Date(date); + timeTicket.setHours(0, 0, 0, 0); + + let comparation = today - timeTicket; + + if (comparation == 0) + return 'warning'; + if (comparation < 0) + return 'success'; + } + + stateColor(state) { + if (state === 'OK') + return 'success'; + else if (state === 'FREE') + return 'notice'; + } + + exprBuilder(param, value) { + switch (param) { + case 'shipped': + return {'shipped': {like: `%${value}%`}}; + } + } + + dateRange(value) { + const minHour = new Date(value); + minHour.setHours(0, 0, 0, 0); + const maxHour = new Date(value); + maxHour.setHours(23, 59, 59, 59); + + return [minHour, maxHour]; + } + + reload() { + this.$.model.refresh(); + } +} + +ngModule.vnComponent('vnTicketFuture', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/ticket/front/future/locale/es.yml b/modules/ticket/front/future/locale/es.yml new file mode 100644 index 000000000..946611e66 --- /dev/null +++ b/modules/ticket/front/future/locale/es.yml @@ -0,0 +1,15 @@ +Future tickets: Tickets a futuro +Search tickets: Buscar tickets +Search future tickets by date: Buscar tickets por fecha +Problems: Problemas +Origin ID: ID origen +Closing: Cierre +Origin State: Estado origen +Destination State: Estado destino +Liters: Litros +Available Lines: Líneas disponibles +Destination ID: ID destino +Destination ETD: ETD Destino +Origin ETD: ETD Origen +Move tickets: Mover tickets +Move confirmation: ¿Desea mover {0} tickets hacia el futuro? diff --git a/modules/ticket/front/future/style.scss b/modules/ticket/front/future/style.scss new file mode 100644 index 000000000..e69de29bb diff --git a/modules/ticket/front/index.js b/modules/ticket/front/index.js index 0558d251d..6106a22eb 100644 --- a/modules/ticket/front/index.js +++ b/modules/ticket/front/index.js @@ -34,3 +34,5 @@ import './dms/create'; import './dms/edit'; import './sms'; import './boxing'; +import './future'; +import './future-search-panel'; diff --git a/modules/ticket/front/routes.json b/modules/ticket/front/routes.json index 4be8e2183..62f43a098 100644 --- a/modules/ticket/front/routes.json +++ b/modules/ticket/front/routes.json @@ -7,7 +7,8 @@ "menus": { "main": [ {"state": "ticket.index", "icon": "icon-ticket"}, - {"state": "ticket.weekly.index", "icon": "schedule"} + {"state": "ticket.weekly.index", "icon": "schedule"}, + {"state": "ticket.future", "icon": "double_arrow"} ], "card": [ {"state": "ticket.card.basicData.stepOne", "icon": "settings"}, @@ -283,6 +284,12 @@ "params": { "ticket": "$ctrl.ticket" } + }, + { + "url": "/future", + "state": "ticket.future", + "component": "vn-ticket-future", + "description": "Future tickets" } ] }