diff --git a/db/changes/10260-holidays/00-travel_cloneWithEntries.sql b/db/changes/10260-holidays/00-travel_cloneWithEntries.sql new file mode 100644 index 000000000..1302a78ca --- /dev/null +++ b/db/changes/10260-holidays/00-travel_cloneWithEntries.sql @@ -0,0 +1,135 @@ +-- DROP PROCEDURE `vn`.`clonTravelComplete`; + +DELIMITER $$ +USE `vn`$$ +CREATE + DEFINER = root@`%` PROCEDURE `vn`.`travel_cloneWithEntries`(IN vTravelFk INT, IN vDateStart DATE, IN vDateEnd DATE, + IN vRef VARCHAR(255), OUT vNewTravelFk INT) +BEGIN + DECLARE vEntryNew INT; + DECLARE vDone BOOLEAN DEFAULT FALSE; + DECLARE vAuxEntryFk INT; + DECLARE vRsEntry CURSOR FOR + SELECT e.id + FROM entry e + JOIN travel t + ON t.id = e.travelFk + WHERE e.travelFk = vTravelFk; + + DECLARE vRsBuy CURSOR FOR + SELECT b.* + FROM buy b + JOIN entry e + ON b.entryFk = e.id + WHERE e.travelFk = vNewTravelFk and b.entryFk=vNewTravelFk + ORDER BY e.id; + + DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE; + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + + START TRANSACTION; + + INSERT INTO travel (shipped,landed, warehouseInFk, warehouseOutFk, agencyFk, ref, isDelivered, isReceived, m3, kg) + SELECT vDateStart, vDateEnd,warehouseInFk, warehouseOutFk, agencyFk, vRef, isDelivered, isReceived, m3, kg + FROM travel + WHERE id = vTravelFk; + + SET vNewTravelFk = LAST_INSERT_ID(); + SET vDone = FALSE; + OPEN vRsEntry ; + FETCH vRsEntry INTO vAuxEntryFk; + + WHILE NOT vDone DO + INSERT INTO entry (supplierFk, + ref, + isInventory, + isConfirmed, + isOrdered, + isRaid, + commission, + created, + evaNotes, + travelFk, + currencyFk, + companyFk, + gestDocFk, + invoiceInFk) + SELECT supplierFk, + ref, + isInventory, + isConfirmed, + isOrdered, + isRaid, + commission, + created, + evaNotes, + vNewTravelFk, + currencyFk, + companyFk, + gestDocFk, + invoiceInFk + FROM entry + WHERE id = vAuxEntryFk; + + SET vEntryNew = LAST_INSERT_ID(); + + + INSERT INTO buy (entryFk, + itemFk, + quantity, + buyingValue, + packageFk, + stickers, + freightValue, + packageValue, + comissionValue, + packing, + `grouping`, + groupingMode, + location, + price1, + price2, + price3, + minPrice, + producer, + printedStickers, + isChecked, + weight) + SELECT vEntryNew, + itemFk, + quantity, + buyingValue, + packageFk, + stickers, + freightValue, + packageValue, + comissionValue, + packing, + `grouping`, + groupingMode, + location, + price1, + price2, + price3, + minPrice, + producer, + printedStickers, + isChecked, + weight + FROM buy + WHERE entryFk = vAuxEntryFk; + + + FETCH vRsEntry INTO vAuxEntryFk; + END WHILE; + CLOSE vRsEntry; + COMMIT; +END;$$ +DELIMITER ; + + diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 0ed3607ad..066d432c8 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -831,7 +831,8 @@ export default { firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)' }, travelExtraCommunity: { - firstTravelReference: 'vn-travel-extra-community > vn-data-viewer div > vn-tbody > vn-tr > vn-td-editable', + anySearchResult: 'vn-travel-extra-community > vn-data-viewer div > vn-tbody > vn-tr', + firstTravelReference: 'vn-travel-extra-community vn-card:nth-child(1) vn-td-editable', removeContinentFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(3) > vn-icon > i' }, travelBasicData: { @@ -863,7 +864,18 @@ export default { travelDescriptor: { filterByAgencyButton: 'vn-descriptor-content .quicklinks > div:nth-child(1) > vn-quick-link > a[vn-tooltip="All travels with current agency"]', dotMenu: 'vn-travel-descriptor vn-icon-button[icon="more_vert"]', - dotMenuClone: '#clone' + dotMenuClone: '#clone', + dotMenuCloneWithEntries: '#cloneWithEntries', + acceptClonation: 'tpl-buttons > button[response="accept"]' + }, + travelCreate: { + reference: 'vn-travel-create vn-textfield[ng-model="$ctrl.travel.ref"]', + agency: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.agencyModeFk"]', + shipped: 'vn-travel-create vn-date-picker[ng-model="$ctrl.travel.shipped"]', + landed: 'vn-travel-create vn-date-picker[ng-model="$ctrl.travel.landed"]', + warehouseOut: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.warehouseOutFk"]', + warehouseIn: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.warehouseInFk"]', + saveButton: 'vn-travel-create vn-submit[label="Save"]' }, zoneIndex: { searchResult: 'vn-zone-index a.vn-tr', diff --git a/e2e/paths/08-route/03_create_and_clone.spec.js b/e2e/paths/08-route/03_create_and_clone.spec.js index be758f788..c0132362f 100644 --- a/e2e/paths/08-route/03_create_and_clone.spec.js +++ b/e2e/paths/08-route/03_create_and_clone.spec.js @@ -74,6 +74,7 @@ describe('Route create path', () => { }); it(`should clone the first route`, async() => { + await page.waitForTimeout(1000); // needs time for the index to show all items await page.waitToClick(selectors.routeIndex.firstRouteCheckbox); await page.waitToClick(selectors.routeIndex.cloneButton); await page.waitToClick(selectors.routeIndex.submitClonationButton); diff --git a/e2e/paths/10-travel/03_descriptor.spec.js b/e2e/paths/10-travel/03_descriptor.spec.js index 3f79bea06..cdca379ad 100644 --- a/e2e/paths/10-travel/03_descriptor.spec.js +++ b/e2e/paths/10-travel/03_descriptor.spec.js @@ -42,4 +42,48 @@ describe('Travel descriptor path', () => { expect(state).toBe('travel.create'); }); + + it('should edit the data to clone and then get redirected to the cloned travel basic data', async() => { + await page.clearInput(selectors.travelCreate.reference); + await page.write(selectors.travelCreate.reference, 'reference'); + await page.autocompleteSearch(selectors.travelCreate.agency, 'entanglement'); + await page.pickDate(selectors.travelCreate.shipped); + await page.pickDate(selectors.travelCreate.landed); + await page.autocompleteSearch(selectors.travelCreate.warehouseOut, 'warehouse one'); + await page.autocompleteSearch(selectors.travelCreate.warehouseIn, 'warehouse two'); + await page.waitToClick(selectors.travelCreate.saveButton); + await page.waitForState('travel.card.basicData'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should atempt to clone the travel and its entries using the descriptor menu but receive an error', async() => { + await page.waitToClick(selectors.travelDescriptor.dotMenu); + await page.waitToClick(selectors.travelDescriptor.dotMenuCloneWithEntries); + await page.waitToClick(selectors.travelDescriptor.acceptClonation); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('A travel with this data already exists'); + }); + + it('should update the landed date to a future date to enable cloneWithEntries', async() => { + const nextMonth = new Date(); + nextMonth.setMonth(nextMonth.getMonth() + 1); + await page.pickDate(selectors.travelBasicData.deliveryDate, nextMonth); + await page.waitToClick(selectors.travelBasicData.save); + await page.waitForState('travel.card.basicData'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should navigate to the summary and then clone the travel and its entries using the descriptor menu to get redirected to the cloned travel basic data', async() => { + await page.waitToClick('vn-icon[icon="preview"]'); // summary icon + await page.waitForState('travel.card.summary'); + await page.waitToClick(selectors.travelDescriptor.dotMenu); + await page.waitToClick(selectors.travelDescriptor.dotMenuCloneWithEntries); + await page.waitToClick(selectors.travelDescriptor.acceptClonation); + await page.waitForState('travel.card.basicData'); + }); }); diff --git a/e2e/paths/10-travel/04_extra_community.spec.js b/e2e/paths/10-travel/04_extra_community.spec.js index bc81c086c..7a37b89e4 100644 --- a/e2e/paths/10-travel/04_extra_community.spec.js +++ b/e2e/paths/10-travel/04_extra_community.spec.js @@ -18,6 +18,7 @@ describe('Travel extra community path', () => { it('should edit the travel reference', async() => { await page.waitToClick(selectors.travelExtraCommunity.removeContinentFilter); + await page.waitForSpinnerLoad(); await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelReference, 'edited reference'); }); diff --git a/loopback/util/log.js b/loopback/util/log.js index baba3e827..d81fc39a0 100644 --- a/loopback/util/log.js +++ b/loopback/util/log.js @@ -33,6 +33,9 @@ exports.translateValues = async(instance, changes) => { }).format(date); } + if (changes instanceof instance) + changes = changes.__data; + const properties = Object.assign({}, changes); for (let property in properties) { const relation = getRelation(instance, property); @@ -41,13 +44,14 @@ exports.translateValues = async(instance, changes) => { if (relation) { let fieldsToShow = ['alias', 'name', 'code', 'description']; - const log = instance.definition.settings.log; + const modelName = relation.model; + const model = models[modelName]; + const log = model.definition.settings.log; if (log && log.showField) - fieldsToShow = log.showField; + fieldsToShow = [log.showField]; - const model = relation.model; - const row = await models[model].findById(value, { + const row = await model.findById(value, { fields: fieldsToShow }); const newValue = getValue(row); diff --git a/modules/supplier/back/methods/supplier/updateFiscalData.js b/modules/supplier/back/methods/supplier/updateFiscalData.js index be031a18a..daa602af0 100644 --- a/modules/supplier/back/methods/supplier/updateFiscalData.js +++ b/modules/supplier/back/methods/supplier/updateFiscalData.js @@ -23,23 +23,27 @@ module.exports = Self => { }, { arg: 'account', - type: 'string' + type: 'any' }, { arg: 'sageTaxTypeFk', - type: 'number' + type: 'any' }, { arg: 'sageWithholdingFk', - type: 'number' + type: 'any' }, { arg: 'sageTransactionTypeFk', - type: 'number' + type: 'any' }, { arg: 'postCode', - type: 'string' + type: 'any' + }, + { + arg: 'street', + type: 'any' }, { arg: 'city', @@ -47,11 +51,11 @@ module.exports = Self => { }, { arg: 'provinceFk', - type: 'number' + type: 'any' }, { arg: 'countryFk', - type: 'number' + type: 'any' }], returns: { arg: 'res', diff --git a/modules/ticket/back/methods/ticket/new.js b/modules/ticket/back/methods/ticket/new.js index 2763f1bd0..8bafe5403 100644 --- a/modules/ticket/back/methods/ticket/new.js +++ b/modules/ticket/back/methods/ticket/new.js @@ -130,7 +130,7 @@ module.exports = Self => { let logRecord = { originFk: cleanInstance.id, userFk: myUserId, - action: 'create', + action: 'insert', changedModel: 'Ticket', changedModelId: cleanInstance.id, oldInstance: {}, diff --git a/modules/travel/back/methods/travel/cloneWithEntries.js b/modules/travel/back/methods/travel/cloneWithEntries.js new file mode 100644 index 000000000..ef385f1e0 --- /dev/null +++ b/modules/travel/back/methods/travel/cloneWithEntries.js @@ -0,0 +1,93 @@ +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; +const UserError = require('vn-loopback/util/user-error'); +const loggable = require('vn-loopback/util/log'); + +module.exports = Self => { + Self.remoteMethodCtx('cloneWithEntries', { + description: 'Clone travel', + accessType: 'WRITE', + accepts: [{ + arg: 'id', + type: 'number', + required: true, + description: 'The original travel id', + http: {source: 'path'} + }], + returns: { + type: 'Object', + description: 'The new cloned travel id', + root: true, + }, + http: { + path: `/:id/cloneWithEntries`, + verb: 'post' + } + }); + + Self.cloneWithEntries = async(ctx, id) => { + const userId = ctx.req.accessToken.userId; + const conn = Self.dataSource.connector; + const models = Self.app.models; + const travel = await Self.findById(id, { + fields: [ + 'id', + 'shipped', + 'landed', + 'warehouseInFk', + 'warehouseOutFk', + 'agencyFk', + 'ref' + ] + }); + const started = new Date(); + const ended = new Date(); + + if (!travel) + throw new UserError('Travel not found'); + + let stmts = []; + let stmt; + + try { + stmt = new ParameterizedSQL( + `CALL travel_cloneWithEntries(?, ?, ?, ?, @vTravelFk)`, [ + id, started, ended, travel.ref]); + + stmts.push(stmt); + const index = stmts.push('SELECT @vTravelFk AS id') - 1; + + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await conn.executeStmt(sql); + const [lastInsert] = result[index]; + const newTravel = await Self.findById(lastInsert.id, { + fields: [ + 'id', + 'shipped', + 'landed', + 'warehouseInFk', + 'warehouseOutFk', + 'agencyFk', + 'ref' + ] + }); + + const oldProperties = await loggable.translateValues(Self, travel); + const newProperties = await loggable.translateValues(Self, newTravel); + await models.TravelLog.create({ + originFk: newTravel.id, + userFk: userId, + action: 'insert', + changedModel: 'Travel', + changedModelId: newTravel.id, + oldInstance: oldProperties, + newInstance: newProperties + }); + + return newTravel.id; + } catch (error) { + if (error.code === 'ER_DUP_ENTRY') + throw new UserError('A travel with this data already exists'); + throw error; + } + }; +}; diff --git a/modules/travel/back/methods/travel/specs/cloneWithEntries.spec.js b/modules/travel/back/methods/travel/specs/cloneWithEntries.spec.js new file mode 100644 index 000000000..ab7d3aa1d --- /dev/null +++ b/modules/travel/back/methods/travel/specs/cloneWithEntries.spec.js @@ -0,0 +1,79 @@ +const app = require('vn-loopback/server/server'); + +// #2687 - Cannot make a data rollback because of the triggers +xdescribe('Travel cloneWithEntries()', () => { + const models = app.models; + const travelId = 5; + const currentUserId = 102; + const ctx = {req: {accessToken: {userId: currentUserId}}}; + let travelBefore; + let newTravelId; + + afterAll(async done => { + try { + const entries = await models.Entry.find({ + where: { + travelFk: newTravelId + } + }); + const entriesId = entries.map(entry => entry.id); + + // Destroy all entries buys + await models.Buy.destroyAll({ + where: { + entryFk: {inq: entriesId} + } + }); + + // Destroy travel entries + await models.Entry.destroyAll({ + where: { + travelFk: newTravelId + } + }); + + // Destroy new travel + await models.Travel.destroyById(newTravelId); + + // Restore original travel shipped & landed + const travel = await models.Travel.findById(travelId); + await travel.updateAttributes({ + shipped: travelBefore.shipped, + landed: travelBefore.landed + }); + } catch (error) { + console.error(error); + } + + done(); + }); + + it(`should clone the travel and the containing entries`, async() => { + const warehouseThree = 3; + const agencyModeOne = 1; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + travelBefore = await models.Travel.findById(travelId); + await travelBefore.updateAttributes({ + shipped: yesterday, + landed: yesterday + }); + + newTravelId = await models.Travel.cloneWithEntries(ctx, travelId); + const travelEntries = await models.Entry.find({ + where: { + travelFk: newTravelId + } + }); + + const newTravel = await models.Travel.findById(travelId); + + expect(newTravelId).not.toEqual(travelId); + expect(newTravel.ref).toEqual('fifth travel'); + expect(newTravel.warehouseInFk).toEqual(warehouseThree); + expect(newTravel.warehouseOutFk).toEqual(warehouseThree); + expect(newTravel.agencyFk).toEqual(agencyModeOne); + expect(travelEntries.length).toBeGreaterThan(0); + }); +}); diff --git a/modules/travel/back/models/travel.js b/modules/travel/back/models/travel.js index b8a1a24b3..46d33b305 100644 --- a/modules/travel/back/models/travel.js +++ b/modules/travel/back/models/travel.js @@ -8,6 +8,7 @@ module.exports = Self => { require('../methods/travel/deleteThermograph')(Self); require('../methods/travel/updateThermograph')(Self); require('../methods/travel/extraCommunityFilter')(Self); + require('../methods/travel/cloneWithEntries')(Self); Self.rewriteDbError(function(err) { if (err.code === 'ER_DUP_ENTRY') diff --git a/modules/travel/front/create/index.html b/modules/travel/front/create/index.html index 1400ef6d7..0931c322e 100644 --- a/modules/travel/front/create/index.html +++ b/modules/travel/front/create/index.html @@ -43,7 +43,6 @@ this.$state.go('travel.card.summary', {id: res.data.id}) + res => this.$state.go('travel.card.basicData', {id: res.data.id}) ); } } diff --git a/modules/travel/front/create/index.spec.js b/modules/travel/front/create/index.spec.js index 4bde7747e..99f52b322 100644 --- a/modules/travel/front/create/index.spec.js +++ b/modules/travel/front/create/index.spec.js @@ -22,7 +22,7 @@ describe('Travel Component vnTravelCreate', () => { controller.onSubmit(); - expect(controller.$state.go).toHaveBeenCalledWith('travel.card.summary', {id: 1234}); + expect(controller.$state.go).toHaveBeenCalledWith('travel.card.basicData', {id: 1234}); }); }); @@ -39,4 +39,3 @@ describe('Travel Component vnTravelCreate', () => { }); }); }); - diff --git a/modules/travel/front/descriptor-menu/index.html b/modules/travel/front/descriptor-menu/index.html index 1eb558008..171aa89ec 100644 --- a/modules/travel/front/descriptor-menu/index.html +++ b/modules/travel/front/descriptor-menu/index.html @@ -7,9 +7,17 @@ Clone travel + + Clone travel and his entries + @@ -20,3 +28,11 @@ question="Do you want to clone this travel?" message="All it's properties will be copied"> + + + + diff --git a/modules/travel/front/descriptor-menu/index.js b/modules/travel/front/descriptor-menu/index.js index 975cd9134..d34c5495b 100644 --- a/modules/travel/front/descriptor-menu/index.js +++ b/modules/travel/front/descriptor-menu/index.js @@ -48,6 +48,10 @@ class Controller extends Section { .then(res => this.travel = res.data); } + get isBuyer() { + return this.aclService.hasAny(['buyer']); + } + onCloneAccept() { const params = JSON.stringify({ ref: this.travel.ref, @@ -59,6 +63,11 @@ class Controller extends Section { }); this.$state.go('travel.create', {q: params}); } + + onCloneWithEntriesAccept() { + this.$http.post(`Travels/${this.travelId}/cloneWithEntries`) + .then(res => this.$state.go('travel.card.basicData', {id: res.data})); + } } Controller.$inject = ['$element', '$scope']; diff --git a/modules/travel/front/descriptor-menu/index.spec.js b/modules/travel/front/descriptor-menu/index.spec.js index d66f3a435..3d94a0963 100644 --- a/modules/travel/front/descriptor-menu/index.spec.js +++ b/modules/travel/front/descriptor-menu/index.spec.js @@ -2,11 +2,14 @@ import './index.js'; describe('Travel Component vnTravelDescriptorMenu', () => { let controller; + let $httpBackend; beforeEach(ngModule('travel')); - beforeEach(inject(($componentController, $state,) => { + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; const $element = angular.element(''); controller = $componentController('vnTravelDescriptorMenu', {$element}); + controller._travelId = 5; })); describe('onCloneAccept()', () => { @@ -36,4 +39,18 @@ describe('Travel Component vnTravelDescriptorMenu', () => { expect(controller.$state.go).toHaveBeenCalledWith('travel.create', {'q': params}); }); }); + + describe('onCloneWithEntriesAccept()', () => { + it('should make an HTTP query and then call to the $state.go method with the returned id', () => { + jest.spyOn(controller.$state, 'go').mockReturnValue('ok'); + + $httpBackend.expect('POST', `Travels/${controller.travelId}/cloneWithEntries`).respond(200, 9); + controller.onCloneWithEntriesAccept(); + $httpBackend.flush(); + + expect(controller.$state.go).toHaveBeenCalledWith('travel.card.basicData', { + id: jasmine.any(Number) + }); + }); + }); }); diff --git a/modules/travel/front/descriptor-menu/locale/es.yml b/modules/travel/front/descriptor-menu/locale/es.yml index 117611660..ca61c4e01 100644 --- a/modules/travel/front/descriptor-menu/locale/es.yml +++ b/modules/travel/front/descriptor-menu/locale/es.yml @@ -1 +1,3 @@ -Clone travel: Clonar envío \ No newline at end of file +Clone travel: Clonar envío +Clone travel and his entries: Clonar travel y sus entradas +Do you want to clone this travel and all containing entries?: ¿Quieres clonar este travel y todas las entradas que contiene? \ No newline at end of file diff --git a/modules/travel/front/locale/es.yml b/modules/travel/front/locale/es.yml index 06fc80601..7231d37cd 100644 --- a/modules/travel/front/locale/es.yml +++ b/modules/travel/front/locale/es.yml @@ -13,7 +13,7 @@ Received: Recibido Travel id: Id envío Search travels by id: Buscar envíos por identificador New travel: Nuevo envío -travel: envio +travel: envío # Sections Travels: Envíos diff --git a/modules/travel/front/summary/index.html b/modules/travel/front/summary/index.html index 8815c09e2..de6f6e979 100644 --- a/modules/travel/front/summary/index.html +++ b/modules/travel/front/summary/index.html @@ -7,6 +7,7 @@ {{$ctrl.travelData.id}} - {{$ctrl.travelData.ref}} +