diff --git a/CHANGELOG.md b/CHANGELOG.md index 30afaa69b..08a62f044 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/db/changes/234801/00-ticket_canAdvance_update.sql b/db/changes/234801/00-ticket_canAdvance_update.sql new file mode 100644 index 000000000..afe0a4dc6 --- /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 cec0545a0..b5dbd42ac 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 9f9961f57..949136459 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -198,4 +198,4 @@ "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", "Try again": "Try again" -} \ No newline at end of file +} diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 5b536bde2..a4de6f997 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", diff --git a/modules/ticket/back/methods/ticket/componentUpdate.js b/modules/ticket/back/methods/ticket/componentUpdate.js index f7c36f108..3acd68cfb 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 ef6422be2..40be4161c 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 a6cf8face..ca4d994d0 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 389bcdf14..fb539311f 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 da8614ca5..883993c1c 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 3111d9afd..e42e65b6e 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 8fa9de438..82ae901cd 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 74e2df074..8c8a3a079 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)