diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 331d115a3..a7d22a811 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -101,6 +101,16 @@ INSERT INTO `vn`.`sector`(`id`, `description`, `warehouseFk`, `isPreviousPrepare (1, 'First sector', 1, 1, 'FIRST', 999, 999), (2, 'Second sector', 2, 0, 'SECOND', 100, 150); +INSERT INTO `vn`.`parking` (`id`, `column`, `row`, `sectorFk`, `code`, `pickingOrder`) + VALUES + ('1', '700', '01', '1', '700-01', '70001'), + ('2', '700', '02', '2', '700-02', '70002'); + +INSERT INTO `vn`.`shelving` (`code`, `parkingFk`, `isPrinted`, `priority`, `parked`, `userFk`) + VALUES + ('GVC', '1', '0', '1', '0', '106'), + ('HEJ', '2', '0', '1', '0', '106'); + INSERT INTO `vn`.`warehouseAlias`(`id`, `name`) VALUES (1, 'Main Warehouse'); @@ -928,6 +938,18 @@ INSERT INTO `vn`.`saleComponent`(`saleFk`, `componentFk`, `value`) (32, 36, -92.324), (32, 39, 0.994); +INSERT INTO `vn`.`itemShelving` (`id`, `itemFk`, `shelvingFk`, `shelve`, `deep`, `quantity`, `visible`, `available`, `grouping`, `packing`, `level`, `userFk`) + VALUES + ('1', '2', 'GVC', 'A', '0', '1', '1', '1', '1', '1', '1', '106'), + ('2', '4', 'HEJ', 'A', '0', '2', '1', '1', '1', '1', '1', '106'); + +INSERT INTO `vn`.`itemShelvingSale` (`itemShelvingFk`, `saleFk`, `quantity`, `created`, `userFk`) + VALUES + ('1', '1', '1', '', '106'), + ('2', '2', '5', '', '106'), + ('1', '7', '1', '', '106'), + ('2', '8', '5', '', '106'); + INSERT INTO `vncontrol`.`accion`(`accion_id`, `accion`) VALUES (3, 'ACTION ONE'), diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 5469c09df..94d11887c 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -367,6 +367,13 @@ export default { advancedSearchInvoiceOut: 'vn-ticket-search-panel vn-textfield[ng-model="filter.refFk"]', newTicketButton: 'vn-ticket-index a', searchResult: 'vn-ticket-index vn-card > vn-table > div > vn-tbody > a.vn-tr', + secondTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(2) > vn-td:nth-child(1) > vn-check', + thirdTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(3) > vn-td:nth-child(1) > vn-check', + sixthTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(6) > vn-td:nth-child(1) > vn-check', + payoutButton: 'vn-ticket-index vn-button[icon="icon-recovery"]', + payoutCompany: '.vn-dialog vn-autocomplete[ng-model="$ctrl.receipt.companyFk"]', + payoutBank: '.vn-dialog vn-autocomplete[ng-model="$ctrl.receipt.bankFk"]', + submitPayout: '.vn-dialog vn-button[label="Save"]', searchWeeklyResult: 'vn-ticket-weekly-index vn-table vn-tbody > vn-tr', searchResultDate: 'vn-ticket-summary [label=Landed] span', topbarSearch: 'vn-searchbar', diff --git a/e2e/paths/05-ticket/18_index_payout.spec.js b/e2e/paths/05-ticket/18_index_payout.spec.js new file mode 100644 index 000000000..749428c44 --- /dev/null +++ b/e2e/paths/05-ticket/18_index_payout.spec.js @@ -0,0 +1,60 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket index payout path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should navigate to the ticket index', async() => { + await page.loginAndModule('administrative', 'ticket'); + let url = await page.expectURL('#!/ticket/index'); + + expect(url).toBe(true); + }); + + it('should check three tickets 2 of a clinet and 1 of another', async() => { + await page.keyboard.press('Enter'); + await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox); + await page.waitToClick(selectors.ticketsIndex.sixthTicketCheckbox); + await page.waitToClick(selectors.ticketsIndex.payoutButton); + const result = await page.waitForLastSnackbar(); + + expect(result).toEqual('You cannot make a payment on account from multiple clients'); + }); + + it('should uncheck the sixth ticket result and check the third which is from the same client then open the payout form', async() => { + await page.waitToClick(selectors.ticketsIndex.sixthTicketCheckbox); + await page.waitToClick(selectors.ticketsIndex.thirdTicketCheckbox); + await page.waitToClick(selectors.ticketsIndex.payoutButton); + + await page.waitForSelector(selectors.ticketsIndex.payoutCompany); + }); + + it('should fill the company and bank to perform a payout', async() => { + await page.autocompleteSearch(selectors.ticketsIndex.payoutBank, 'cash'); + await page.waitToClick(selectors.ticketsIndex.submitPayout); + const result = await page.waitForLastSnackbar(); + + expect(result).toEqual('Data saved!'); + }); + + it('should navigate to the client balance section and check a new balance line was entered', async() => { + await page.waitToClick(selectors.globalItems.homeButton); + await page.selectModule('client'); + await page.accessToSearchResult('101'); + await page.accessToSection('client.card.balance.index'); + await page.waitForSelector('vn-client-balance-index vn-tbody > vn-tr'); + let result = await page.countElement('vn-client-balance-index vn-tbody > vn-tr'); + + expect(result).toEqual(4); + }); +}); diff --git a/e2e/paths/07-order/02_basic_data.spec.js b/e2e/paths/07-order/02_basic_data.spec.js index d7bd01208..2c3292b61 100644 --- a/e2e/paths/07-order/02_basic_data.spec.js +++ b/e2e/paths/07-order/02_basic_data.spec.js @@ -81,7 +81,7 @@ describe('Order edit basic data path', () => { await page.waitToClick(selectors.orderBasicData.saveButton); const result = await page.waitForLastSnackbar(); - expect(result).toEqual('Data saved!'); + expect(result).toContain('Data saved!'); }); it('should now confirm the client have been edited', async() => { diff --git a/e2e/paths/07-order/04_catalog.spec.js b/e2e/paths/07-order/04_catalog.spec.js index 0db313088..34fdbbec0 100644 --- a/e2e/paths/07-order/04_catalog.spec.js +++ b/e2e/paths/07-order/04_catalog.spec.js @@ -69,8 +69,7 @@ describe('Order catalog', () => { }); it('should search for an item by id', async() => { - await page.write(selectors.orderCatalog.itemId, '2'); - await page.keyboard.press('Enter'); + await page.accessToSearchResult('2'); await page.waitForNumberOfElements('section.product', 1); const result = await page.countElement('section.product'); diff --git a/modules/item/back/model-config.json b/modules/item/back/model-config.json index d8ec5914a..c085e075a 100644 --- a/modules/item/back/model-config.json +++ b/modules/item/back/model-config.json @@ -44,6 +44,9 @@ "ItemTypeTag": { "dataSource": "vn" }, + "ItemShelving": { + "dataSource": "vn" + }, "ItemShelvingSale": { "dataSource": "vn" }, diff --git a/modules/item/back/models/item-shelving-sale.json b/modules/item/back/models/item-shelving-sale.json index 547c882a0..04f505ddd 100644 --- a/modules/item/back/models/item-shelving-sale.json +++ b/modules/item/back/models/item-shelving-sale.json @@ -25,6 +25,11 @@ "model": "Sale", "foreignKey": "saleFk" }, + "itemShelving": { + "type": "belongsTo", + "model": "ItemShelving", + "foreignKey": "itemShelvingFk" + }, "user": { "type": "belongsTo", "model": "Account", diff --git a/modules/item/back/models/item-shelving.json b/modules/item/back/models/item-shelving.json new file mode 100644 index 000000000..0fcc00f7e --- /dev/null +++ b/modules/item/back/models/item-shelving.json @@ -0,0 +1,40 @@ +{ + "name": "ItemShelving", + "base": "VnModel", + "options": { + "mysql": { + "table": "itemShelving" + } + }, + "properties": { + "id": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "shelve": { + "type": "string" + }, + "deep": { + "type": "number" + }, + "quantity": { + "type": "number" + }, + "created": { + "type": "Date" + } + }, + "relations": { + "item": { + "type": "belongsTo", + "model": "Item", + "foreignKey": "itemFk" + }, + "user": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "userFk" + } + } +} diff --git a/modules/order/front/catalog/index.html b/modules/order/front/catalog/index.html index 4c8786e13..f9c2c40bd 100644 --- a/modules/order/front/catalog/index.html +++ b/modules/order/front/catalog/index.html @@ -11,6 +11,13 @@ limit="50" data="$ctrl.items"> + + + + @@ -77,14 +84,6 @@ - - - - - - Id: {{$ctrl.itemId}} + + Name + : {{$ctrl.itemName}} + { - if (this.$params.itemId) - this.itemId = parseInt(this.$params.itemId); - if (this.$params.categoryId) this.categoryId = parseInt(this.$params.categoryId); @@ -111,17 +108,6 @@ class Controller extends Section { this.applyFilters(); } - get itemId() { - return this._itemId; - } - - set itemId(value) { - this._itemId = value; - - this.updateStateParams(); - this.applyFilters(); - } - get tags() { return this._tags; } @@ -195,18 +181,6 @@ class Controller extends Section { this.itemTypes = res.data); } - /** - * Search by item id filter - * @param {object} event - */ - onSearchById(event) { - const value = this.$.itemId.value; - if (event.key === 'Enter' && value) { - this.itemId = value; - this.$.itemId.value = null; - } - } - /** * Search by tag value * @param {object} event @@ -230,9 +204,19 @@ class Controller extends Section { this.applyFilters(); } - applyFilters() { + removeItemId() { + this.itemId = null; + this.applyFilters(); + } + + removeItemName() { + this.itemName = null; + this.applyFilters(); + } + + applyFilters(filter = {}) { let newParams = {}; - let newFilter = {}; + let newFilter = Object.assign({}, filter); const model = this.$.model; if (this.categoryId) @@ -241,16 +225,13 @@ class Controller extends Section { if (this.typeId) newFilter.typeFk = this.typeId; - if (this.itemId) - newFilter = {'i.id': this.itemId}; - newParams = { - orderFk: this.order.id, + orderFk: this.$params.id, orderBy: this.getOrderBy(), tags: this.tags, }; - model.applyFilter({where: newFilter}, newParams); + return model.applyFilter({where: newFilter}, newParams); } openPanel(event) { @@ -282,10 +263,6 @@ class Controller extends Section { if (this.typeId) params.typeId = this.typeId; - params.itemId = undefined; - if (this.itemId) - params.itemId = this.itemId; - params.tags = undefined; if (this.tags.length) { const tags = []; @@ -344,6 +321,27 @@ class Controller extends Section { newFilterList = newFilterList.concat(tags); this.orderFields = newFilterList; } + + onSearch(params) { + if (!params) return; + + this.itemId = null; + this.itemName = null; + + if (params.search) { + if (/^\d+$/.test(params.search)) { + this.itemId = params.search; + return this.applyFilters({ + 'i.id': params.search + }); + } else { + this.itemName = params.search; + return this.applyFilters({ + 'i.name': {like: `%${params.search}%`} + }); + } + } else return this.applyFilters(); + } } ngModule.component('vnOrderCatalog', { diff --git a/modules/order/front/catalog/index.spec.js b/modules/order/front/catalog/index.spec.js index f01712110..f7635665a 100644 --- a/modules/order/front/catalog/index.spec.js +++ b/modules/order/front/catalog/index.spec.js @@ -17,12 +17,15 @@ describe('Order', () => { $scope.search = {}; $scope.itemId = {}; $state = _$state_; - $state.params.categoryId = 1; - $state.params.typeId = 2; $state.current.name = 'my.current.state'; const $element = angular.element(''); controller = $componentController('vnOrderCatalog', {$element, $scope}); controller._order = {id: 4}; + controller.$params = { + categoryId: 1, + typeId: 2, + id: 4 + }; })); describe('order() setter', () => { @@ -112,18 +115,6 @@ describe('Order', () => { }); }); - describe('itemId() setter', () => { - it(`should set itemId property and then call updateStateParams() and applyFilters() methods`, () => { - jest.spyOn(controller, 'updateStateParams'); - jest.spyOn(controller, 'applyFilters'); - - controller.itemId = 1; - - expect(controller.updateStateParams).toHaveBeenCalledWith(); - expect(controller.applyFilters).toHaveBeenCalledWith(); - }); - }); - describe('tags() setter', () => { it(`should set tags property and then call updateStateParams() and applyFilters() methods`, () => { jest.spyOn(controller, 'updateStateParams'); @@ -158,23 +149,27 @@ describe('Order', () => { }); }); - describe('onSearchById()', () => { - it(`should not filter by id if the event key code doesn't equals to 'Enter'`, () => { + describe('onSearch()', () => { + it(`should apply a filter by item id an then call the applyFilters method`, () => { jest.spyOn(controller, 'applyFilters'); - controller.$.itemId.value = 1; - controller.onSearchById({key: 'Tab'}); + const itemId = 1; + controller.onSearch({search: itemId}); - expect(controller.applyFilters).not.toHaveBeenCalledWith(); + expect(controller.applyFilters).toHaveBeenCalledWith({ + 'i.id': itemId + }); }); - it(`should filter by id if the event key code equals to 'Enter' an then call applyFilters()`, () => { + it(`should apply a filter by item name an then call the applyFilters method`, () => { jest.spyOn(controller, 'applyFilters'); - controller.$.itemId.value = 1; - controller.onSearchById({key: 'Enter'}); + const itemName = 'Bow'; + controller.onSearch({search: itemName}); - expect(controller.applyFilters).toHaveBeenCalledWith(); + expect(controller.applyFilters).toHaveBeenCalledWith({ + 'i.name': {like: `%${itemName}%`} + }); }); }); @@ -224,7 +219,6 @@ describe('Order', () => { controller._categoryId = 2; controller._typeId = 4; - controller._itemId = 1; controller._tags = [ {tagFk: 11, value: 'Precission', tagSelection: {name: 'Category'}} ]; @@ -232,7 +226,7 @@ describe('Order', () => { value: 'Precission', tagFk: 11, tagSelection: {name: 'Category'}} ]); - let result = {categoryId: 2, typeId: 4, itemId: 1, tags: tags}; + let result = {categoryId: 2, typeId: 4, tags: tags}; controller.updateStateParams(); expect(controller.$state.go).toHaveBeenCalledWith('my.current.state', result); diff --git a/modules/order/front/catalog/locale/es.yml b/modules/order/front/catalog/locale/es.yml new file mode 100644 index 000000000..27d16fe2d --- /dev/null +++ b/modules/order/front/catalog/locale/es.yml @@ -0,0 +1,2 @@ +Name: Nombre +Search by item id or name: Buscar por id de artículo o nombre \ No newline at end of file diff --git a/modules/order/front/routes.json b/modules/order/front/routes.json index b607aef9d..eec628b89 100644 --- a/modules/order/front/routes.json +++ b/modules/order/front/routes.json @@ -41,7 +41,7 @@ "order": "$ctrl.order" } }, { - "url": "/catalog?categoryId&typeId&itemId&tags", + "url": "/catalog?q&categoryId&typeId&tags", "state": "order.card.catalog", "component": "vn-order-catalog", "description": "Catalog", diff --git a/modules/ticket/back/methods/ticket/setDeleted.js b/modules/ticket/back/methods/ticket/setDeleted.js index 6daad7c39..0f7e0b57f 100644 --- a/modules/ticket/back/methods/ticket/setDeleted.js +++ b/modules/ticket/back/methods/ticket/setDeleted.js @@ -23,6 +23,7 @@ module.exports = Self => { Self.setDeleted = async(ctx, id) => { const models = Self.app.models; + const userId = ctx.req.accessToken.userId; const isEditable = await Self.isEditable(ctx, id); const $t = ctx.req.__; // $translate @@ -30,16 +31,30 @@ module.exports = Self => { throw new UserError(`The sales of this ticket can't be modified`); // Check if has sales with shelving + const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant'); const sales = await models.Sale.find({ - include: {relation: 'itemShelving'}, + include: {relation: 'itemShelvingSale'}, where: {ticketFk: id} }); const hasItemShelvingSales = sales.some(sale => { - return sale.itemShelving(); + return sale.itemShelvingSale(); }); - if (hasItemShelvingSales) + + if (hasItemShelvingSales && !isSalesAssistant) throw new UserError(`You cannot delete a ticket that part of it is being prepared`); + if (hasItemShelvingSales && isSalesAssistant) { + const promises = []; + for (let sale of sales) { + if (sale.itemShelvingSale()) { + const itemShelvingSale = sale.itemShelvingSale(); + const destroyedShelving = models.ItemShelvingSale.destroyById(itemShelvingSale.id); + promises.push(destroyedShelving); + } + } + await Promise.all(promises); + } + // Check for existing claim const claimOfATicket = await models.Claim.findOne({where: {ticketFk: id}}); if (claimOfATicket) diff --git a/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js b/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js index 890fc6c45..2713bd700 100644 --- a/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js +++ b/modules/ticket/back/methods/ticket/specs/setDeleted.spec.js @@ -1,29 +1,33 @@ const app = require('vn-loopback/server/server'); +const models = app.models; describe('ticket deleted()', () => { let ticket; - let ctx; + let sale; beforeAll(async done => { - let originalTicket = await app.models.Ticket.findOne({where: {id: 16}}); + let originalTicket = await models.Ticket.findOne({where: {id: 16}}); originalTicket.id = null; - ticket = await app.models.Ticket.create(originalTicket); + ticket = await models.Ticket.create(originalTicket); + sale = await models.Sale.create({ + ticketFk: ticket.id, + itemFk: 4, + concept: 'Melee weapon', + quantity: 10 + }); - ctx = { - req: { - accessToken: {userId: 106}, - headers: { - origin: 'http://localhost:5000' - }, - __: () => {} - } - }; + await models.ItemShelvingSale.create({ + itemShelvingFk: 1, + saleFk: sale.id, + quantity: 10, + userFk: 106 + }); done(); }); afterAll(async done => { - await app.models.Ticket.destroyById(ticket.id); + await models.Ticket.destroyById(ticket.id); done(); }); @@ -32,16 +36,62 @@ describe('ticket deleted()', () => { expect(ticket.isDeleted).toEqual(false); }); - it('should set a ticket to deleted', async() => { + it('should make sure the ticket sale has an item shelving', async() => { + const sales = await models.Sale.find({ + include: {relation: 'itemShelvingSale'}, + where: {ticketFk: ticket.id} + }); + const hasItemShelvingSales = sales.some(sale => { + return sale.itemShelvingSale(); + }); + + expect(hasItemShelvingSales).toEqual(true); + }); + + it('should set a ticket to deleted and remove all item shelvings', async() => { + const salesAssistantId = 21; + const ctx = { + req: { + accessToken: {userId: salesAssistantId}, + headers: { + origin: 'http://localhost:5000' + }, + __: () => {} + } + }; await app.models.Ticket.setDeleted(ctx, ticket.id); - let deletedTicket = await app.models.Ticket.findOne({where: {id: ticket.id}, fields: ['isDeleted']}); + let deletedTicket = await app.models.Ticket.findOne({ + where: {id: ticket.id}, + fields: ['isDeleted'] + }); expect(deletedTicket.isDeleted).toEqual(true); }); + it('should not have any item shelving', async() => { + const sales = await models.Sale.find({ + include: {relation: 'itemShelvingSale'}, + where: {ticketFk: ticket.id} + }); + const hasItemShelvingSales = sales.some(sale => { + return sale.itemShelvingSale(); + }); + + expect(hasItemShelvingSales).toEqual(false); + }); + it('should throw an error if the given ticket has a claim', async() => { - let ticketId = 16; + const ticketId = 16; + const ctx = { + req: { + accessToken: {userId: 106}, + headers: { + origin: 'http://localhost:5000' + }, + __: () => {} + } + }; let error; try { diff --git a/modules/ticket/back/models/sale.json b/modules/ticket/back/models/sale.json index 1f2ea4bbf..767a3e59e 100644 --- a/modules/ticket/back/models/sale.json +++ b/modules/ticket/back/models/sale.json @@ -73,7 +73,7 @@ "model": "SaleTracking", "foreignKey": "saleFk" }, - "itemShelving": { + "itemShelvingSale": { "type": "hasOne", "model": "ItemShelvingSale", "foreignKey": "saleFk" diff --git a/modules/ticket/front/index/index.js b/modules/ticket/front/index/index.js index 7d6163086..4803f40ff 100644 --- a/modules/ticket/front/index/index.js +++ b/modules/ticket/front/index/index.js @@ -29,7 +29,7 @@ export default class Controller extends Section { } get checked() { - const tickets = this.$.tickets || []; + const tickets = this.$.model.data || []; const checkedLines = []; for (let ticket of tickets) { if (ticket.checked) diff --git a/modules/ticket/front/index/index.spec.js b/modules/ticket/front/index/index.spec.js index 1cb57926b..10bd88d3d 100644 --- a/modules/ticket/front/index/index.spec.js +++ b/modules/ticket/front/index/index.spec.js @@ -88,7 +88,7 @@ describe('Component vnTicketIndex', () => { controller.$.balanceCreateDialog = {show: () => {}}; jest.spyOn(controller.$.balanceCreateDialog, 'show').mockReturnThis(); - controller.$.tickets = tickets; + controller.$.model = {data: tickets}; controller.$.balanceCreateDialog.amountPaid = 0; controller.openBalanceDialog(); @@ -102,7 +102,7 @@ describe('Component vnTicketIndex', () => { describe('checked()', () => { it('should return an array of checked tickets', () => { - controller.$.tickets = tickets; + controller.$.model = {data: tickets}; const result = controller.checked; const firstRow = result[0]; const secondRow = result[1]; @@ -115,7 +115,7 @@ describe('Component vnTicketIndex', () => { describe('totalChecked()', () => { it('should return the total number of checked tickets', () => { - controller.$.tickets = tickets; + controller.$.model = {data: tickets}; const result = controller.checked; expect(result.length).toEqual(2);