diff --git a/CHANGELOG.md b/CHANGELOG.md index 30afaa69b0..08a62f0449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2348.01] - 2023-11-30 ### Added +- (Ticket -> Adelantar) Permite mover lineas sin generar negativos +- (Ticket -> Adelantar) Permite modificar la fecha de los tickets + ### Changed ### Fixed +- (Ticket -> RocketChat) Arreglada detección de cambios + ## [2346.01] - 2023-11-16 diff --git a/README.md b/README.md index 59f5dbcf78..b420bc44f6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Salix is also the scientific name of a beautifull tree! :) Required applications. -* Node.js >= 16.x LTS +* Node.js * Docker * Git @@ -17,20 +17,7 @@ You will need to install globally the following items. $ sudo npm install -g jest gulp-cli ``` -For the usage of jest --watch on macOs. -``` -$ brew install watchman -``` -* [watchman](https://facebook.github.io/watchman/) - -## Linux Only Prerequisites - -Your user must be on the docker group to use it so you will need to run this command: -``` -$ sudo usermod -a -G docker yourusername -``` - -## Getting Started // Installing +## Installing dependencies and launching Pull from repository. @@ -76,29 +63,6 @@ In Visual Studio Code we use the ESLint extension. ext install dbaeumer.vscode-eslint ``` -Gitlens for visualization of code authorship -``` -ext install eamodio.gitlens -``` - -Spanish language pack -``` -ext install ms-ceintl.vscode-language-pack-es -``` - -### Recommended extensions - -Material icon Theme -``` -ext install pkief.material-icon-theme -``` - -Material UI Themes -``` -ext install equinusocio.vsc-material-theme -``` - - ## Built With * [angularjs](https://angularjs.org/) diff --git a/back/methods/vn-user/sign-in.js b/back/methods/vn-user/sign-in.js index b9e0d2f705..25f708b8e5 100644 --- a/back/methods/vn-user/sign-in.js +++ b/back/methods/vn-user/sign-in.js @@ -49,8 +49,13 @@ module.exports = Self => { if (vnUser.twoFactor) throw new ForbiddenError(null, 'REQUIRES_2FA'); } - - return Self.validateLogin(user, password); + const validateLogin = await Self.validateLogin(user, password); + await Self.app.models.SignInLog.create({ + id: validateLogin.token, + userFk: vnUser.id, + ip: ctx.req.ip + }); + return validateLogin; }; Self.passExpired = async vnUser => { diff --git a/back/models/vn-user.js b/back/models/vn-user.js index de5bf7b63e..5845c2192c 100644 --- a/back/models/vn-user.js +++ b/back/models/vn-user.js @@ -2,6 +2,7 @@ const vnModel = require('vn-loopback/common/models/vn-model'); const {Email} = require('vn-print'); const ForbiddenError = require('vn-loopback/util/forbiddenError'); const LoopBackContext = require('loopback-context'); +const UserError = require('vn-loopback/util/user-error'); module.exports = function(Self) { vnModel(Self); @@ -92,7 +93,11 @@ module.exports = function(Self) { }; Self.on('resetPasswordRequest', async function(info) { - const url = await Self.app.models.Url.getUrl(); + const loopBackContext = LoopBackContext.getCurrentContext(); + const httpCtx = {req: loopBackContext.active}; + const httpRequest = httpCtx.req.http.req; + const headers = httpRequest.headers; + const origin = headers.origin; const defaultHash = '/reset-password?access_token=$token$'; const recoverHashes = { @@ -108,7 +113,7 @@ module.exports = function(Self) { const params = { recipient: info.email, lang: user.lang, - url: url.slice(0, -1) + recoverHash + url: origin + '/#!' + recoverHash }; const options = Object.assign({}, info.options); @@ -121,10 +126,16 @@ module.exports = function(Self) { }); Self.validateLogin = async function(user, password) { - let loginInfo = Object.assign({password}, Self.userUses(user)); - token = await Self.login(loginInfo, 'user'); + const loginInfo = Object.assign({password}, Self.userUses(user)); + const token = await Self.login(loginInfo, 'user'); const userToken = await token.user.get(); + + if (userToken.username.toLowerCase() !== user.toLowerCase()) { + console.error('ERROR!!! - Signin with other user', userToken, user); + throw new UserError('Try again'); + } + try { await Self.app.models.Account.sync(userToken.name, password); } catch (err) { @@ -220,8 +231,11 @@ module.exports = function(Self) { class Mailer { async send(verifyOptions, cb) { + const url = new URL(verifyOptions.verifyHref); + if (process.env.NODE_ENV) url.port = ''; + const params = { - url: verifyOptions.verifyHref, + url: url.href, recipient: verifyOptions.to }; diff --git a/db/changes/234603/00-createSignInLogTable.sql b/db/changes/234603/00-createSignInLogTable.sql new file mode 100644 index 0000000000..977de46463 --- /dev/null +++ b/db/changes/234603/00-createSignInLogTable.sql @@ -0,0 +1,19 @@ + + +-- +-- Table structure for table `signInLog` +-- + +DROP TABLE IF EXISTS `account`.`signInLog`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `account`.`signInLog` ( + `id` varchar(10) NOT NULL , + `userFk` int(10) unsigned DEFAULT NULL, + `creationDate` timestamp NULL DEFAULT current_timestamp(), + `ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL, + PRIMARY KEY (`id`), + KEY `userFk` (`userFk`), + CONSTRAINT `signInLog_ibfk_1` FOREIGN KEY (`userFk`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +); + diff --git a/db/changes/234801/00-ticket_canAdvance_update.sql b/db/changes/234801/00-ticket_canAdvance_update.sql new file mode 100644 index 0000000000..afe0a4dc6d --- /dev/null +++ b/db/changes/234801/00-ticket_canAdvance_update.sql @@ -0,0 +1,152 @@ +DELIMITER $$ +$$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canAdvance`(vDateFuture DATE, vDateToAdvance DATE, vWarehouseFk INT) +BEGIN +/** + * Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar. + * + * @param vDateFuture Fecha de los tickets que se quieren adelantar. + * @param vDateToAdvance Fecha a cuando se quiere adelantar. + * @param vWarehouseFk Almacén + */ + DECLARE vDateInventory DATE; + + SELECT inventoried INTO vDateInventory FROM config; + + CREATE OR REPLACE TEMPORARY TABLE tmp.stock + (itemFk INT PRIMARY KEY, + amount INT) + ENGINE = MEMORY; + + INSERT INTO tmp.stock(itemFk, amount) + SELECT itemFk, SUM(quantity) amount FROM + ( + SELECT itemFk, quantity + FROM itemTicketOut + WHERE shipped >= vDateInventory + AND shipped < vDateFuture + AND warehouseFk = vWarehouseFk + UNION ALL + SELECT itemFk, quantity + FROM itemEntryIn + WHERE landed >= vDateInventory + AND landed <= vDateToAdvance + AND isVirtualStock = FALSE + AND warehouseInFk = vWarehouseFk + UNION ALL + SELECT itemFk, quantity + FROM itemEntryOut + WHERE shipped >= vDateInventory + AND shipped < vDateFuture + AND warehouseOutFk = vWarehouseFk + ) t + GROUP BY itemFk HAVING amount != 0; + + CREATE OR REPLACE TEMPORARY TABLE tmp.filter + (INDEX (id)) + SELECT + origin.ticketFk futureId, + dest.ticketFk id, + dest.state, + origin.futureState, + origin.futureIpt, + dest.ipt, + origin.workerFk, + origin.futureLiters, + origin.futureLines, + dest.shipped, + origin.shipped futureShipped, + dest.totalWithVat, + origin.totalWithVat futureTotalWithVat, + dest.agency, + dest.agencyModeFk, + origin.futureAgency, + origin.agencyModeFk futureAgencyModeFk, + dest.lines, + dest.liters, + origin.futureLines - origin.hasStock AS notMovableLines, + (origin.futureLines = origin.hasStock) AS isFullMovable, + dest.zoneFk, + origin.futureZoneFk, + origin.futureZoneName, + origin.classColor futureClassColor, + dest.classColor, + origin.clientFk futureClientFk, + origin.addressFk futureAddressFk, + origin.warehouseFk futureWarehouseFk, + origin.companyFk futureCompanyFk, + IFNULL(dest.nickname, origin.nickname) nickname, + dest.landed + FROM ( + SELECT + s.ticketFk, + c.salesPersonFk workerFk, + t.shipped, + t.totalWithVat, + st.name futureState, + am.name futureAgency, + count(s.id) futureLines, + GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) futureIpt, + CAST(SUM(litros) AS DECIMAL(10,0)) futureLiters, + SUM((s.quantity <= IFNULL(st.amount,0))) hasStock, + z.id futureZoneFk, + z.name futureZoneName, + st.classColor, + t.clientFk, + t.nickname, + t.addressFk, + t.warehouseFk, + t.companyFk, + t.agencyModeFk + FROM ticket t + JOIN client c ON c.id = t.clientFk + JOIN sale s ON s.ticketFk = t.id + JOIN saleVolume sv ON sv.saleFk = s.id + JOIN item i ON i.id = s.itemFk + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN state st ON st.id = ts.stateFk + JOIN agencyMode am ON t.agencyModeFk = am.id + JOIN zone z ON t.zoneFk = z.id + LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk + LEFT JOIN tmp.stock st ON st.itemFk = i.id + WHERE t.shipped BETWEEN vDateFuture AND util.dayend(vDateFuture) + AND t.warehouseFk = vWarehouseFk + GROUP BY t.id + ) origin + LEFT JOIN ( + SELECT + t.id ticketFk, + st.name state, + GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) ipt, + t.shipped, + t.totalWithVat, + am.name agency, + CAST(SUM(litros) AS DECIMAL(10,0)) liters, + CAST(COUNT(*) AS DECIMAL(10,0)) `lines`, + st.classColor, + t.clientFk, + t.nickname, + t.addressFk, + t.zoneFk, + t.warehouseFk, + t.companyFk, + t.landed, + t.agencyModeFk + FROM ticket t + JOIN sale s ON s.ticketFk = t.id + JOIN saleVolume sv ON sv.saleFk = s.id + JOIN item i ON i.id = s.itemFk + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN state st ON st.id = ts.stateFk + JOIN agencyMode am ON t.agencyModeFk = am.id + LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk + WHERE t.shipped BETWEEN vDateToAdvance AND util.dayend(vDateToAdvance) + AND t.warehouseFk = vWarehouseFk + AND st.order <= 5 + GROUP BY t.id + ) dest ON dest.addressFk = origin.addressFk + WHERE origin.hasStock; + + DROP TEMPORARY TABLE tmp.stock; +END$$ +DELIMITER ; diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index cec0545a01..b5dbd42ac1 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -722,7 +722,7 @@ export default { isFullMovable: 'vn-check[ng-model="filter.isFullMovable"]', warehouseFk: 'vn-autocomplete[label="Warehouse"]', tableButtonSearch: 'vn-button[vn-tooltip="Search"]', - moveButton: 'vn-button[vn-tooltip="Advance tickets"]', + moveButton: 'vn-button[vn-tooltip="Advance tickets with negatives"]', acceptButton: '.vn-confirm.shown button[response="accept"]', firstCheck: 'tbody > tr:nth-child(2) > td > vn-check', tableId: 'vn-textfield[name="id"]', diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 26650175d6..9491364591 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -196,6 +196,6 @@ "Negative basis of tickets: 23": "Negative basis of tickets: 23", "Booking completed": "Booking complete", "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation", - "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets" + "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets", + "Try again": "Try again" } - diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 7655a46c84..a4de6f9972 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -224,7 +224,7 @@ "date in the future": "Fecha en el futuro", "reference duplicated": "Referencia duplicada", "This ticket is already a refund": "Este ticket ya es un abono", - "isWithoutNegatives": "isWithoutNegatives", + "isWithoutNegatives": "Sin negativos", "routeFk": "routeFk", "Can't change the password of another worker": "No se puede cambiar la contraseña de otro trabajador", "No hay un contrato en vigor": "No hay un contrato en vigor", @@ -323,8 +323,9 @@ "The response is not a PDF": "La respuesta no es un PDF", "Booking completed": "Reserva completada", "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación", - "The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina", - "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina", "Incoterms data for consignee is missing": "Faltan los datos de los Incoterms para el consignatario", - "The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada" + "The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada", + "User disabled": "Usuario desactivado", + "The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mínima", + "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima" } diff --git a/modules/account/back/model-config.json b/modules/account/back/model-config.json index a4eb9fa571..b4bd6dbafd 100644 --- a/modules/account/back/model-config.json +++ b/modules/account/back/model-config.json @@ -35,6 +35,9 @@ "SambaConfig": { "dataSource": "vn" }, + "SignInLog": { + "dataSource": "vn" + }, "Sip": { "dataSource": "vn" }, diff --git a/modules/account/back/models/sign_in-log.json b/modules/account/back/models/sign_in-log.json new file mode 100644 index 0000000000..44575b0137 --- /dev/null +++ b/modules/account/back/models/sign_in-log.json @@ -0,0 +1,34 @@ +{ + "name": "SignInLog", + "base": "VnModel", + "options": { + "mysql": { + "table": "account.signInLog" + } + }, + "properties": { + "id": { + "id": true, + "type": "string" + }, + "creationDate": { + "type": "date" + }, + "userFk": { + "type": "number" + }, + "ip": { + "type": "string" + } + }, + "relations": { + "user": { + "type": "belongsTo", + "model": "VnUser", + "foreignKey": "userFk" + } + }, + "scope": { + "order": ["creationDate DESC", "id DESC"] + } +} diff --git a/modules/item/back/methods/item/setVisibleDiscard.js b/modules/item/back/methods/item/setVisibleDiscard.js index 08cc507c32..16cef6baf7 100644 --- a/modules/item/back/methods/item/setVisibleDiscard.js +++ b/modules/item/back/methods/item/setVisibleDiscard.js @@ -20,8 +20,7 @@ module.exports = Self => { }, { arg: 'addressFk', - type: 'Number', - required: true, + type: 'Any', description: 'The address id' }], http: { diff --git a/modules/item/back/models/item-shelving.json b/modules/item/back/models/item-shelving.json index 61d05539e1..bb1a141c42 100644 --- a/modules/item/back/models/item-shelving.json +++ b/modules/item/back/models/item-shelving.json @@ -1,6 +1,6 @@ { "name": "ItemShelving", - "base": "VnModel", + "base": "Loggable", "options": { "mysql": { "table": "itemShelving" diff --git a/modules/ticket/back/methods/sale/updateQuantity.js b/modules/ticket/back/methods/sale/updateQuantity.js index 1a497da248..6108262838 100644 --- a/modules/ticket/back/methods/sale/updateQuantity.js +++ b/modules/ticket/back/methods/sale/updateQuantity.js @@ -64,7 +64,10 @@ module.exports = Self => { const sale = await models.Sale.findById(id, filter, myOptions); const oldQuantity = sale.quantity; - const result = await sale.updateAttributes({quantity: newQuantity}, myOptions); + const result = await sale.updateAttributes({ + quantity: newQuantity, + originalQuantity: newQuantity + }, myOptions); const salesPerson = sale.ticket().client().salesPersonUser(); if (salesPerson) { diff --git a/modules/ticket/back/methods/ticket/closeAll.js b/modules/ticket/back/methods/ticket/closeAll.js index 660c168937..46c45aa927 100644 --- a/modules/ticket/back/methods/ticket/closeAll.js +++ b/modules/ticket/back/methods/ticket/closeAll.js @@ -32,31 +32,30 @@ module.exports = Self => { throw new UserError('You cannot close tickets for today'); const tickets = await Self.rawSql(` - SELECT - t.id, - t.clientFk, - t.companyFk, - c.name clientName, - c.email recipient, - c.salesPersonFk, - c.isToBeMailed, - c.hasToInvoice, - co.hasDailyInvoice, - eu.email salesPersonEmail - FROM ticket t - JOIN agencyMode am ON am.id = t.agencyModeFk - JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission - JOIN ticketState ts ON ts.ticketFk = t.id - JOIN alertLevel al ON al.id = ts.alertLevel - JOIN client c ON c.id = t.clientFk - JOIN province p ON p.id = c.provinceFk - JOIN country co ON co.id = p.countryFk - LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk - WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code != 'delivered')) - AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY) - AND util.dayEnd(?) - AND t.refFk IS NULL - GROUP BY t.id + SELECT t.id, + t.clientFk, + t.companyFk, + c.name clientName, + c.email recipient, + c.salesPersonFk, + c.isToBeMailed, + c.hasToInvoice, + co.hasDailyInvoice, + eu.email salesPersonEmail, + t.addressFk + FROM ticket t + JOIN agencyMode am ON am.id = t.agencyModeFk + JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN alertLevel al ON al.id = ts.alertLevel + JOIN client c ON c.id = t.clientFk + JOIN province p ON p.id = c.provinceFk + JOIN country co ON co.id = p.countryFk + LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk + WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code != 'delivered')) + AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY) AND util.dayEnd(?) + AND t.refFk IS NULL + GROUP BY t.id `, [toDate, toDate]); await closure(ctx, Self, tickets); diff --git a/modules/ticket/back/methods/ticket/closure.js b/modules/ticket/back/methods/ticket/closure.js index 7a2825a4d6..1d04679d3a 100644 --- a/modules/ticket/back/methods/ticket/closure.js +++ b/modules/ticket/back/methods/ticket/closure.js @@ -14,12 +14,12 @@ module.exports = async function(ctx, Self, tickets, reqArgs = {}) { 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]); + 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, @@ -91,13 +91,13 @@ module.exports = async function(ctx, Self, tickets, reqArgs = {}) { // 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]); + 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 = { @@ -105,7 +105,8 @@ module.exports = async function(ctx, Self, tickets, reqArgs = {}) { companyId: ticket.companyFk, recipientId: ticket.clientFk, recipient: ticket.recipient, - replyTo: ticket.salesPersonEmail + replyTo: ticket.salesPersonEmail, + addressId: ticket.addressFk }; const email = new Email('incoterms-authorization', args); @@ -113,13 +114,13 @@ module.exports = async function(ctx, Self, tickets, reqArgs = {}) { const [sample] = await Self.rawSql( `SELECT id - FROM sample - WHERE code = 'incoterms-authorization' - `); + FROM sample + WHERE code = 'incoterms-authorization' + `); await Self.rawSql(` - INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?) - `, [ticket.clientFk, sample.id, ticket.companyFk], {userId}); + INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?) + `, [ticket.clientFk, sample.id, ticket.companyFk], {userId}); } } catch (error) { // Domain not found @@ -140,7 +141,7 @@ module.exports = async function(ctx, Self, tickets, reqArgs = {}) { for (const ticket of failedtickets) { body += `Ticket: ${ticket.id} -
${ticket.stacktrace}

`; +
${ticket.stacktrace}

`; } smtp.send({ @@ -158,19 +159,19 @@ module.exports = async function(ctx, Self, tickets, reqArgs = {}) { const oldInstance = `{"email": "${ticket.recipient}"}`; const newInstance = `{"email": ""}`; await Self.rawSql(` - INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance) - VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [ + 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.`; + 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, diff --git a/modules/ticket/back/methods/ticket/componentUpdate.js b/modules/ticket/back/methods/ticket/componentUpdate.js index f7c36f1088..3acd68cfb1 100644 --- a/modules/ticket/back/methods/ticket/componentUpdate.js +++ b/modules/ticket/back/methods/ticket/componentUpdate.js @@ -75,8 +75,7 @@ module.exports = Self => { { arg: 'option', type: 'number', - description: 'Action id', - required: true + description: 'Action id' }, { arg: 'isWithoutNegatives', @@ -88,7 +87,18 @@ module.exports = Self => { arg: 'withWarningAccept', type: 'boolean', description: 'Has pressed in confirm message', - }], + }, + { + arg: 'newTicket', + type: 'number', + description: 'Ticket id you want to move the lines to, if applicable', + }, + { + arg: 'keepPrice', + type: 'boolean', + description: 'If prices should be maintained', + } + ], returns: { type: ['object'], root: true @@ -141,12 +151,18 @@ module.exports = Self => { const salesNewTicketLength = salesNewTicket.length; if (salesNewTicketLength && sales.length != salesNewTicketLength) { - const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions); + const newTicket = await models.Ticket.transferSales( + ctx, + args.id, + args.newTicket, + salesNewTicket, + myOptions + ); args.id = newTicket.id; } } - const originalTicket = await models.Ticket.findOne({ + const ticketToChange = await models.Ticket.findOne({ where: {id: args.id}, fields: [ 'id', @@ -179,33 +195,38 @@ module.exports = Self => { }, ] }, myOptions); + const originalTicket = JSON.parse(JSON.stringify(ticketToChange)); + const ticketChanges = { + clientFk: args.clientFk, + nickname: args.nickname, + agencyModeFk: args.agencyModeFk, + addressFk: args.addressFk, + zoneFk: args.zoneFk, + warehouseFk: args.warehouseFk, + companyFk: args.companyFk, + shipped: args.shipped, + landed: args.landed, + isDeleted: args.isDeleted + }; - args.routeFk = null; + let response; + if (args.keepPrice) { + ticketChanges.routeFk = null; + response = await ticketToChange.updateAttributes(ticketChanges, myOptions); + } else { + const hasToBeUnrouted = true; + response = await Self.rawSql( + 'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + [args.id].concat(Object.values(ticketChanges), [hasToBeUnrouted, args.option]), + myOptions + ); + } if (args.isWithoutNegatives === false) delete args.isWithoutNegatives; const updatedTicket = Object.assign({}, args); delete updatedTicket.ctx; delete updatedTicket.option; - // Force to unroute ticket - const hasToBeUnrouted = true; - const query = 'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - const res = await Self.rawSql(query, [ - args.id, - args.clientFk, - args.nickname, - args.agencyModeFk, - args.addressFk, - args.zoneFk, - args.warehouseFk, - args.companyFk, - args.shipped, - args.landed, - args.isDeleted, - hasToBeUnrouted, - args.option - ], myOptions); - - if (originalTicket.addressFk != updatedTicket.addressFk && args.id) { + if (ticketToChange.addressFk != updatedTicket.addressFk && args.id) { await models.TicketObservation.destroyAll({ ticketFk: args.id }, myOptions); @@ -232,10 +253,15 @@ module.exports = Self => { const hasChanges = Object.keys(changes.old).length > 0 || Object.keys(changes.new).length > 0; if (hasChanges) { + delete changes.old.client; + delete changes.old.address; + delete changes.new.client; + delete changes.new.address; + const oldProperties = await loggable.translateValues(Self, changes.old); const newProperties = await loggable.translateValues(Self, changes.new); - const salesPersonId = originalTicket.client().salesPersonFk; + const salesPersonId = ticketToChange.client().salesPersonFk; if (salesPersonId) { const url = await Self.app.models.Url.getUrl(); @@ -256,10 +282,10 @@ module.exports = Self => { } } - res.id = args.id; + response.id = args.id; if (tx) await tx.commit(); - return res; + return response; } catch (e) { if (tx) await tx.rollback(); throw e; diff --git a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js index ef6422be2b..40be4161c1 100644 --- a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js +++ b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js @@ -209,4 +209,112 @@ describe('ticket componentUpdate()', () => { throw e; } }); + + describe('componentUpdate() keepPrice', () => { + it('should change shipped and keep price', async() => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const saleId = 27; + const originDate = today; + const newDate = tomorrow; + const ticketID = 14; + newDate.setHours(0, 0, 0, 0, 0); + originDate.setHours(0, 0, 0, 0, 0); + + const args = { + id: ticketID, + clientFk: 1104, + agencyModeFk: 2, + addressFk: 4, + zoneFk: 9, + warehouseFk: 1, + companyFk: 442, + shipped: newDate, + landed: tomorrow, + isDeleted: false, + option: 1, + isWithoutNegatives: false, + keepPrice: true + }; + + const ctx = { + args: args, + req: { + accessToken: {userId: 9}, + headers: {origin: 'http://localhost'}, + __: value => { + return value; + } + } + }; + + const beforeSale = await models.Sale.findById(saleId, null, options); + await models.Ticket.componentUpdate(ctx, options); + const afterSale = await models.Sale.findById(saleId, null, options); + + expect(beforeSale.price).toEqual(afterSale.price); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should change shipped and not keep price', async() => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const saleId = 27; + const originDate = today; + const newDate = tomorrow; + const ticketID = 14; + newDate.setHours(0, 0, 0, 0, 0); + originDate.setHours(0, 0, 0, 0, 0); + + const args = { + id: ticketID, + clientFk: 1104, + agencyModeFk: 2, + addressFk: 4, + zoneFk: 9, + warehouseFk: 1, + companyFk: 442, + shipped: newDate, + landed: tomorrow, + isDeleted: false, + option: 1, + isWithoutNegatives: false, + keepPrice: false + }; + + const ctx = { + args: args, + req: { + accessToken: {userId: 9}, + headers: {origin: 'http://localhost'}, + __: value => { + return value; + } + } + }; + + const beforeSale = await models.Sale.findById(saleId, null, options); + await models.Ticket.componentUpdate(ctx, options); + const afterSale = await models.Sale.findById(saleId, null, options); + + expect(beforeSale.price).not.toEqual(afterSale.price); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + }); }); diff --git a/modules/ticket/front/advance/index.html b/modules/ticket/front/advance/index.html index a6cf8facea..ca4d994d06 100644 --- a/modules/ticket/front/advance/index.html +++ b/modules/ticket/front/advance/index.html @@ -21,10 +21,17 @@ expr-builder="$ctrl.exprBuilder(param, value)" > - + vn-tooltip="Advance tickets with negatives"> + + @@ -32,8 +39,14 @@ - Destination - Origin + + Destination + {{model.userParams.dateToAdvance| date: 'dd/MM/yyyy'}} + + + Origin + {{model.userParams.dateFuture | date: 'dd/MM/yyyy'}} + @@ -48,9 +61,6 @@ ID - - Date - IPT @@ -69,9 +79,6 @@ ID - - Date - IPT @@ -105,7 +112,7 @@ @@ -114,12 +121,7 @@ - {{::ticket.id | dashIfEmpty}} - - - - - {{::ticket.shipped | date: 'dd/MM/yyyy'}} + {{::ticket.id}} {{::ticket.ipt | dashIfEmpty}} @@ -135,7 +137,7 @@ - {{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}} + {{::ticket.totalWithVat | currency: 'EUR': 2}} @@ -145,11 +147,6 @@ {{::ticket.futureId | dashIfEmpty}} - - - {{::ticket.futureShipped | date: 'dd/MM/yyyy'}} - - {{::ticket.futureIpt | dashIfEmpty}} + + + + + + + + + + + Error list + + + + + + + + + + diff --git a/modules/ticket/front/advance/index.js b/modules/ticket/front/advance/index.js index 389bcdf14a..fb539311f7 100644 --- a/modules/ticket/front/advance/index.js +++ b/modules/ticket/front/advance/index.js @@ -75,20 +75,6 @@ export default class Controller extends Section { }); } - compareDate(date) { - let today = Date.vnNew(); - 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'; - } - get checked() { const tickets = this.$.model.data || []; const checkedLines = []; @@ -134,9 +120,16 @@ export default class Controller extends Section { '\n' + this.$t(`Destination agency`, {agency: agency}); } - moveTicketsAdvance() { + async moveTicketsAdvance() { let ticketsToMove = []; - this.checked.forEach(ticket => { + for (const ticket of this.checked) { + if (!ticket.id) { + try { + const {query, params} = await this.requestComponentUpdate(ticket, false); + this.$http.post(query, params); + } catch (e) {} + continue; + } ticketsToMove.push({ originId: ticket.futureId, destinationId: ticket.id, @@ -144,15 +137,105 @@ export default class Controller extends Section { destinationShipped: ticket.shipped, workerFk: ticket.workerFk }); - }); + } + const params = {tickets: ticketsToMove}; return this.$http.post('Tickets/merge', params) .then(() => { - this.$.model.refresh(); - this.vnApp.showSuccess(this.$t('Success')); + this.refresh(); + if (ticketsToMove.length) + this.vnApp.showSuccess(this.$t('Success', {tickets: ticketsToMove.length})); }); } + async getLanded(params) { + const query = `Agencies/getLanded`; + return this.$http.get(query, {params}).then(res => { + if (res.data) + return res.data; + + return this.vnApp.showError( + this.$t(`No delivery zone available for this landing date`) + ); + }); + } + + async splitTickets() { + this.progress = []; + this.splitErrors = []; + this.$.splitTickets.hide(); + this.$.splitProgress.enable = true; + this.$.splitProgress.cancel = false; + this.$.splitProgress.show(); + + for (const ticket of this.checked) { + if (this.$.splitProgress.cancel) break; + try { + const {query, params} = await this.requestComponentUpdate(ticket, true); + this.$http.post(query, params) + .catch(e => { + this.splitErrors.push({id: ticket.futureId, reason: e.message}); + }) + .finally(() => this.progressAdd(ticket.futureId)); + } catch (e) { + this.splitErrors.push({id: ticket.futureId, reason: e.message}); + this.progressAdd(ticket.futureId); + } + } + } + + progressAdd(ticketId) { + this.progress.push(ticketId); + if (this.progress.length == this.checked.length) { + this.$.splitProgress.enable = false; + this.refresh(); + if ((this.progress.length - this.splitErrors.length) > 0) { + this.vnApp.showSuccess(this.$t('Success', { + tickets: this.progress.length - this.splitErrors.length + })); + } + } + } + + async requestComponentUpdate(ticket, isWithoutNegatives) { + const query = `tickets/${ticket.futureId}/componentUpdate`; + if (!ticket.landed) { + const newLanded = await this.getLanded({ + shipped: this.$.model.userParams.dateToAdvance, + addressFk: ticket.addressFk, + agencyModeFk: ticket.agencyModeFk, + warehouseFk: ticket.warehouseFk + }); + if (!newLanded) + throw new Error(this.$t(`No delivery zone available for this landing date`)); + + ticket.landed = newLanded.landed; + ticket.zoneFk = newLanded.zoneFk; + } + const params = { + clientFk: ticket.clientFk, + nickname: ticket.nickname, + agencyModeFk: ticket.agencyModeFk ?? ticket.futureAgencyModeFk, + addressFk: ticket.addressFk, + zoneFk: ticket.zoneFk ?? ticket.futureZoneFk, + warehouseFk: ticket.warehouseFk, + companyFk: ticket.companyFk, + shipped: this.$.model.userParams.dateToAdvance, + landed: ticket.landed, + isDeleted: false, + isWithoutNegatives, + newTicket: ticket.id ?? undefined, + keepPrice: true + }; + + return {query, params}; + } + + refresh() { + this.$.model.refresh(); + this.$checkAll = null; + } + exprBuilder(param, value) { switch (param) { case 'id': diff --git a/modules/ticket/front/advance/index.spec.js b/modules/ticket/front/advance/index.spec.js index da8614ca5d..883993c1cb 100644 --- a/modules/ticket/front/advance/index.spec.js +++ b/modules/ticket/front/advance/index.spec.js @@ -24,31 +24,6 @@ describe('Component vnTicketAdvance', () => { }]; })); - describe('compareDate()', () => { - it('should return warning when the date is the present', () => { - let today = Date.vnNew(); - let result = controller.compareDate(today); - - expect(result).toEqual('warning'); - }); - - it('should return sucess when the date is in the future', () => { - let futureDate = Date.vnNew(); - futureDate = futureDate.setDate(futureDate.getDate() + 10); - let result = controller.compareDate(futureDate); - - expect(result).toEqual('success'); - }); - - it('should return undefined when the date is in the past', () => { - let pastDate = Date.vnNew(); - pastDate = pastDate.setDate(pastDate.getDate() - 10); - let result = controller.compareDate(pastDate); - - expect(result).toEqual(undefined); - }); - }); - describe('checked()', () => { it('should return an array of checked tickets', () => { const result = controller.checked; diff --git a/modules/ticket/front/advance/locale/es.yml b/modules/ticket/front/advance/locale/es.yml index 3111d9afd3..e42e65b6e6 100644 --- a/modules/ticket/front/advance/locale/es.yml +++ b/modules/ticket/front/advance/locale/es.yml @@ -1,7 +1,9 @@ Advance tickets: Adelantar tickets +Advance tickets with negatives: Adelantar tickets con negativos +Advance tickets without negatives: Adelantar tickets sin negativos Search advance tickets by date: Busca tickets para adelantar por fecha Advance confirmation: ¿Desea adelantar {{checked}} tickets? -Success: Tickets movidos correctamente +Success: "{{tickets}} Tickets movidos correctamente" Lines: Líneas Liters: Litros Item Packing Type: Encajado @@ -9,3 +11,7 @@ Origin agency: "Agencia origen: {{agency}}" Destination agency: "Agencia destino: {{agency}}" Less than 50€: Menor a 50€ Not Movable: No movibles +Error list: Lista de errores +Progress: Progreso +Total progress: Progreso total +Advance tickets (without negatives): Adelantar tickets (sin negativos) diff --git a/modules/ticket/front/advance/style.scss b/modules/ticket/front/advance/style.scss index 8fa9de4381..82ae901cd7 100644 --- a/modules/ticket/front/advance/style.scss +++ b/modules/ticket/front/advance/style.scss @@ -5,3 +5,12 @@ vn-ticket-advance{ color: #f7931e } } +.split-progress { + width: 40em; +} + +@media screen and (max-width: 600px) { + .split-progress { + width: 100%; + } +} diff --git a/modules/ticket/front/basic-data/step-two/index.js b/modules/ticket/front/basic-data/step-two/index.js index 74e2df074c..8c8a3a079e 100644 --- a/modules/ticket/front/basic-data/step-two/index.js +++ b/modules/ticket/front/basic-data/step-two/index.js @@ -114,7 +114,8 @@ class Controller extends Component { isDeleted: this.ticket.isDeleted, option: parseInt(this.ticket.option), isWithoutNegatives: this.ticket.withoutNegatives, - withWarningAccept: this.ticket.withWarningAccept + withWarningAccept: this.ticket.withWarningAccept, + keepPrice: false }; this.$http.post(query, params) diff --git a/modules/worker/front/descriptor/index.html b/modules/worker/front/descriptor/index.html index aa6b803003..8290e2a155 100644 --- a/modules/worker/front/descriptor/index.html +++ b/modules/worker/front/descriptor/index.html @@ -38,7 +38,7 @@ + > @@ -61,8 +61,6 @@
@@ -75,13 +73,13 @@ - -