diff --git a/db/changes/10411-january/00-ticket_getMovable.sql b/db/changes/10411-january/00-ticket_getMovable.sql new file mode 100644 index 000000000..5f5b0a93a --- /dev/null +++ b/db/changes/10411-january/00-ticket_getMovable.sql @@ -0,0 +1,43 @@ +DROP PROCEDURE IF EXISTS `vn`.`ticket_getMovable`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`ticket_getMovable`(vTicketFk INT, vDatedNew DATETIME, vWarehouseFk INT) +BEGIN +/** + * Cálcula el stock movible para los artículos de un ticket + * + * @param vTicketFk -> Ticket + * @param vDatedNew -> Nueva fecha + * @return Sales con Movible +*/ + DECLARE vDatedOld DATETIME; + + SELECT t.shipped INTO vDatedOld + FROM ticket t + WHERE t.id = vTicketFk; + + CALL itemStock(vWarehouseFk, DATE_SUB(vDatedNew, INTERVAL 1 DAY), NULL); + CALL item_getMinacum(vWarehouseFk, vDatedNew, DATEDIFF(vDatedOld, vDatedNew), NULL); + + SELECT s.id, + s.itemFk, + s.quantity, + s.concept, + s.price, + s.reserved, + s.discount, + i.image, + i.subName, + il.stock + IFNULL(im.amount, 0) AS movable + FROM ticket t + JOIN sale s ON s.ticketFk = t.id + JOIN item i ON i.id = s.itemFk + LEFT JOIN tmp.itemMinacum im ON im.itemFk = s.itemFk AND im.warehouseFk = vWarehouseFk + LEFT JOIN tmp.itemList il ON il.itemFk = s.itemFk + WHERE t.id = vTicketFk; + + DROP TEMPORARY TABLE IF EXISTS tmp.itemList; + DROP TEMPORARY TABLE IF EXISTS tmp.itemMinacum; +END$$ +DELIMITER ; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index ed03cba1e..b6629a50f 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -625,6 +625,7 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF (25 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, CURDATE()), (26 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, CURDATE()), (27 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, CURDATE()); + INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`) VALUES (1, 11, 1, 'ready'), @@ -899,7 +900,8 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric (29, 4, 17, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()), (30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()), (31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, CURDATE()), - (32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE()); + (32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE()), + (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, CURDATE()); INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`) VALUES diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 2900a285b..bd4078564 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -526,6 +526,7 @@ export default { acceptDialog: '.vn-dialog.shown button[response="accept"]', acceptChangeHourButton: '.vn-dialog.shown button[response="accept"]', descriptorDeliveryDate: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(4) > section > span', + descriptorDeliveryAgency: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(5) > section > span', acceptInvoiceOutButton: '.vn-confirm.shown button[response="accept"]', acceptDeleteStowawayButton: '.vn-dialog.shown button[response="accept"]' }, @@ -603,10 +604,12 @@ export default { ticketBasicData: { agency: 'vn-autocomplete[ng-model="$ctrl.agencyModeId"]', zone: 'vn-autocomplete[ng-model="$ctrl.zoneId"]', + shipped: 'vn-date-picker[ng-model="$ctrl.shipped"]', nextStepButton: 'vn-step-control .buttons > section:last-child vn-button', finalizeButton: 'vn-step-control .buttons > section:last-child button[type=submit]', stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two > vn-side-menu div:nth-child(4)', chargesReason: 'vn-ticket-basic-data-step-two div:nth-child(3) > vn-radio', + withoutNegatives: 'vn-check[ng-model="$ctrl.ticket.withoutNegatives"]', }, ticketComponents: { base: 'vn-ticket-components > vn-side-menu div:nth-child(1) > div:nth-child(2)' diff --git a/e2e/paths/05-ticket/06_basic_data_steps.spec.js b/e2e/paths/05-ticket/06_basic_data_steps.spec.js index a5f9a60cf..7a09edf06 100644 --- a/e2e/paths/05-ticket/06_basic_data_steps.spec.js +++ b/e2e/paths/05-ticket/06_basic_data_steps.spec.js @@ -83,4 +83,62 @@ describe('Ticket Edit basic data path', () => { await page.waitToClick(selectors.ticketBasicData.finalizeButton); await page.waitForState('ticket.card.summary'); }); + + it(`should not find ticket`, async() => { + await page.doSearch('29'); + const count = await page.countElement(selectors.ticketsIndex.searchResult); + + expect(count).toEqual(0); + }); + + it(`should split ticket without negatives`, async() => { + const newAgency = 'Silla247'; + const newDate = new Date(); + newDate.setDate(newDate.getDate() + 1); + + await page.accessToSearchResult('14'); + await page.accessToSection('ticket.card.basicData.stepOne'); + + await page.autocompleteSearch(selectors.ticketBasicData.agency, newAgency); + await page.pickDate(selectors.ticketBasicData.shipped, newDate); + + await page.waitToClick(selectors.ticketBasicData.nextStepButton); + + await page.waitToClick(selectors.ticketBasicData.withoutNegatives); + await page.waitToClick(selectors.ticketBasicData.finalizeButton); + + await page.waitForState('ticket.card.summary'); + + const newTicketAgency = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText'); + const newTicketDate = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText'); + + expect(newAgency).toEqual(newTicketAgency); + expect(newTicketDate).toContain(newDate.getDate()); + }); + + it(`should new ticket have sale of old ticket`, async() => { + await page.accessToSection('ticket.card.sale'); + await page.waitForState('ticket.card.sale'); + + const item = await page.waitToGetProperty(selectors.ticketSales.firstSaleId, 'innerText'); + + expect(item).toEqual('4'); + }); + + it(`should old ticket have old date and agency`, async() => { + const oldDate = new Date(); + const oldAgency = 'Super-Man delivery'; + + await page.accessToSearchResult('14'); + + const oldTicketAgency = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText'); + const oldTicketDate = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText'); + + expect(oldTicketAgency).toEqual(oldAgency); + expect(oldTicketDate).toContain(oldDate.getDate()); + }); }); diff --git a/modules/ticket/back/methods/ticket/componentUpdate.js b/modules/ticket/back/methods/ticket/componentUpdate.js index 53e5fedc8..de06212c7 100644 --- a/modules/ticket/back/methods/ticket/componentUpdate.js +++ b/modules/ticket/back/methods/ticket/componentUpdate.js @@ -77,6 +77,12 @@ module.exports = Self => { type: 'number', description: 'Action id', required: true + }, + { + arg: 'isWithoutNegatives', + type: 'boolean', + description: 'Is whithout negatives', + required: true }], returns: { type: ['object'], @@ -127,6 +133,18 @@ module.exports = Self => { } } + if (args.isWithoutNegatives) { + const query = `CALL ticket_getMovable(?,?,?)`; + const params = [args.id, args.shipped, args.warehouseFk]; + const [salesMovable] = await Self.rawSql(query, params, myOptions); + + const salesNewTicket = salesMovable.filter(sale => (sale.movable ?? 0) >= sale.quantity); + if (salesNewTicket.length) { + const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions); + args.id = newTicket.id; + } + } + const originalTicket = await models.Ticket.findOne({ where: {id: args.id}, fields: [ @@ -230,8 +248,9 @@ module.exports = Self => { await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); } + res.id = args.id; if (tx) await tx.commit(); - + return res; } catch (e) { if (tx) await tx.rollback(); diff --git a/modules/ticket/back/methods/ticket/priceDifference.js b/modules/ticket/back/methods/ticket/priceDifference.js index 1856cb08f..c91956ece 100644 --- a/modules/ticket/back/methods/ticket/priceDifference.js +++ b/modules/ticket/back/methods/ticket/priceDifference.js @@ -40,6 +40,12 @@ module.exports = Self => { type: 'number', description: 'The warehouse id', required: true + }, + { + arg: 'shipped', + type: 'date', + description: 'shipped', + required: true }], returns: { type: ['object'], @@ -104,19 +110,32 @@ module.exports = Self => { totalDifference: 0.00, }; - const query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`; - const params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId]; + // Get items movable + const ticketOrigin = await models.Ticket.findById(args.id, null, myOptions); + const differenceShipped = ticketOrigin.shipped.getTime() != args.shipped.getTime(); + const differenceWarehouse = ticketOrigin.warehouseFk != args.warehouseId; + + salesObj.haveDifferences = differenceShipped || differenceWarehouse; + + let query = `CALL ticket_getMovable(?,?,?)`; + let params = [args.id, args.shipped, args.warehouseId]; + const [salesMovable] = await Self.rawSql(query, params, myOptions); + + const itemMovable = new Map(); + for (sale of salesMovable) + itemMovable.set(sale.id, sale.movable ?? 0); + + // Sale price component, one per sale + query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`; + params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId]; const [difComponents] = await Self.rawSql(query, params, myOptions); const map = new Map(); - - // Sale price component, one per sale for (difComponent of difComponents) map.set(difComponent.saleFk, difComponent); for (sale of salesObj.items) { const difComponent = map.get(sale.id); - if (difComponent) { sale.component = difComponent; @@ -129,10 +148,11 @@ module.exports = Self => { salesObj.totalUnitPrice += sale.price; salesObj.totalUnitPrice = round(salesObj.totalUnitPrice); + sale.movable = itemMovable.get(sale.id); } if (tx) await tx.commit(); - + return salesObj; } catch (e) { if (tx) await tx.rollback(); diff --git a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js index 9fa69b595..2aa2a07c4 100644 --- a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js +++ b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js @@ -45,7 +45,8 @@ describe('ticket componentUpdate()', () => { shipped: today, landed: tomorrow, isDeleted: false, - option: 1 + option: 1, + isWithoutNegatives: false }; let ctx = { @@ -94,7 +95,8 @@ describe('ticket componentUpdate()', () => { shipped: today, landed: tomorrow, isDeleted: false, - option: 1 + option: 1, + isWithoutNegatives: false }; const ctx = { @@ -134,4 +136,60 @@ describe('ticket componentUpdate()', () => { throw e; } }); + + it('should change warehouse and without negatives', async() => { + const tx = await models.SaleComponent.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const saleToTransfer = 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: true + }; + + const ctx = { + args: args, + req: { + accessToken: {userId: 9}, + headers: {origin: 'http://localhost'}, + __: value => { + return value; + } + } + }; + await models.Ticket.componentUpdate(ctx, options); + + const [newTicketID] = await models.Ticket.rawSql('SELECT MAX(id) as id FROM ticket', null, options); + const oldTicket = await models.Ticket.findById(ticketID, null, options); + const newTicket = await models.Ticket.findById(newTicketID.id, null, options); + const newTicketSale = await models.Sale.findOne({where: {ticketFk: args.id}}, options); + + expect(oldTicket.shipped).toEqual(originDate); + expect(newTicket.shipped).toEqual(newDate); + expect(newTicketSale.id).toEqual(saleToTransfer); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); }); diff --git a/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js b/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js index fed899d77..e9aa5030a 100644 --- a/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js +++ b/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js @@ -15,6 +15,7 @@ describe('sale priceDifference()', () => { ctx.args = { id: 16, landed: tomorrow, + shipped: tomorrow, addressId: 126, agencyModeId: 7, zoneId: 3, @@ -45,6 +46,7 @@ describe('sale priceDifference()', () => { ctx.args = { id: 1, landed: new Date(), + shipped: new Date(), addressId: 121, zoneId: 3, warehouseId: 1 @@ -59,4 +61,38 @@ describe('sale priceDifference()', () => { expect(error).toEqual(new UserError(`The sales of this ticket can't be modified`)); }); + + it('should return ticket movable', async() => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + const ctx = {req: {accessToken: {userId: 1106}}}; + ctx.args = { + id: 11, + shipped: tomorrow, + landed: tomorrow, + addressId: 122, + agencyModeId: 7, + zoneId: 3, + warehouseId: 1 + }; + + const result = await models.Ticket.priceDifference(ctx, options); + const firstItem = result.items[0]; + const secondtItem = result.items[1]; + + expect(firstItem.movable).toEqual(440); + expect(secondtItem.movable).toEqual(1980); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); }); diff --git a/modules/ticket/front/basic-data/step-one/index.js b/modules/ticket/front/basic-data/step-one/index.js index 215f36a46..f532265e2 100644 --- a/modules/ticket/front/basic-data/step-one/index.js +++ b/modules/ticket/front/basic-data/step-one/index.js @@ -201,7 +201,8 @@ class Controller extends Component { addressId: this.ticket.addressFk, agencyModeId: this.ticket.agencyModeFk, zoneId: this.ticket.zoneFk, - warehouseId: this.ticket.warehouseFk + warehouseId: this.ticket.warehouseFk, + shipped: this.ticket.shipped }; return this.$http.post(query, params).then(res => { diff --git a/modules/ticket/front/basic-data/step-two/index.html b/modules/ticket/front/basic-data/step-two/index.html index 439f2b527..092c9e746 100644 --- a/modules/ticket/front/basic-data/step-two/index.html +++ b/modules/ticket/front/basic-data/step-two/index.html @@ -9,6 +9,7 @@ Item Description + Movable Quantity Price (PPU) New (PPU) @@ -31,6 +32,13 @@ tabindex="-1"> + + + {{::sale.movable}} + + {{::sale.quantity}} {{::sale.price | currency: 'EUR': 2}} {{::sale.component.newPrice | currency: 'EUR': 2}} @@ -66,6 +74,13 @@ +
+ + +
diff --git a/modules/ticket/front/basic-data/step-two/index.js b/modules/ticket/front/basic-data/step-two/index.js index 7ffdb8315..c12647aa5 100644 --- a/modules/ticket/front/basic-data/step-two/index.js +++ b/modules/ticket/front/basic-data/step-two/index.js @@ -20,6 +20,7 @@ class Controller extends Component { this.getTotalNewPrice(); this.getTotalDifferenceOfPrice(); this.loadDefaultTicketAction(); + this.ticketHaveNegatives(); } loadDefaultTicketAction() { @@ -63,6 +64,22 @@ class Controller extends Component { this.totalPriceDifference = totalPriceDifference; } + ticketHaveNegatives() { + let haveNegatives = false; + let haveNotNegatives = false; + const haveDifferences = this.ticket.sale.haveDifferences; + + this.ticket.sale.items.forEach(item => { + if (item.quantity > item.movable) + haveNegatives = true; + else + haveNotNegatives = true; + }); + + this.ticket.withoutNegatives = false; + this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences); + } + onSubmit() { if (!this.ticket.option) { return this.vnApp.showError( @@ -70,8 +87,8 @@ class Controller extends Component { ); } - let query = `tickets/${this.ticket.id}/componentUpdate`; - let params = { + const query = `tickets/${this.ticket.id}/componentUpdate`; + const params = { clientFk: this.ticket.clientFk, nickname: this.ticket.nickname, agencyModeFk: this.ticket.agencyModeFk, @@ -82,16 +99,20 @@ class Controller extends Component { shipped: this.ticket.shipped, landed: this.ticket.landed, isDeleted: this.ticket.isDeleted, - option: parseInt(this.ticket.option) + option: parseInt(this.ticket.option), + isWithoutNegatives: this.ticket.withoutNegatives }; - this.$http.post(query, params).then(res => { - this.vnApp.showMessage( - this.$t(`The ticket has been unrouted`) - ); - this.card.reload(); - this.$state.go('ticket.card.summary', {id: this.$params.id}); - }); + this.$http.post(query, params) + .then(res => { + this.ticketToMove = res.data.id; + this.vnApp.showMessage( + this.$t(`The ticket has been unrouted`) + ); + }) + .finally(() => { + this.$state.go('ticket.card.summary', {id: this.ticketToMove}); + }); } } diff --git a/modules/ticket/front/basic-data/step-two/index.spec.js b/modules/ticket/front/basic-data/step-two/index.spec.js index ea8268716..ac85a7818 100644 --- a/modules/ticket/front/basic-data/step-two/index.spec.js +++ b/modules/ticket/front/basic-data/step-two/index.spec.js @@ -64,5 +64,103 @@ describe('Ticket', () => { expect(controller.totalPriceDifference).toEqual(0.3); }); }); + + describe('ticketHaveNegatives()', () => { + it('should show if ticket have any negative, have differences, but not all sale are negative', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 1, + movable: 5 + } + ], + haveDifferences: true + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(true); + }); + + it('should not show if ticket not have any negative', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 2, + movable: 1 + } + ], + haveDifferences: true + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(false); + }); + + it('should not show if all sale are negative', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 2, + movable: 1 + } + ], + haveDifferences: true + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(false); + }); + + it('should not show if ticket not have differences', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 1, + movable: 2 + } + ], + haveDifferences: false + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(false); + }); + }); }); }); diff --git a/modules/ticket/front/basic-data/step-two/locale/es.yml b/modules/ticket/front/basic-data/step-two/locale/es.yml index a2a07991b..e1f1e0bfc 100644 --- a/modules/ticket/front/basic-data/step-two/locale/es.yml +++ b/modules/ticket/front/basic-data/step-two/locale/es.yml @@ -5,4 +5,7 @@ Charge difference to: Cargar diferencia a The ticket has been unrouted: El ticket ha sido desenrutado Price: Precio New price: Nuevo precio -Price difference: Diferencia de precio \ No newline at end of file +Price difference: Diferencia de precio +Create without negatives: Crear sin negativos +Clone this ticket with the changes and only sales availables: Clona este ticket con los cambios y solo las ventas disponibles. +Movable: Movible \ No newline at end of file