Merge pull request '2661 - Added clone options' (#494) from 2661-travel_cloneWithEntries into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #494
Reviewed-by: Carlos Jimenez Ruiz <carlosjr@verdnatura.es>
This commit is contained in:
Carlos Jimenez Ruiz 2020-12-31 10:00:42 +00:00
commit 1a3cc54dd2
20 changed files with 438 additions and 21 deletions

View File

@ -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 ;

View File

@ -831,7 +831,8 @@ export default {
firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)' firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)'
}, },
travelExtraCommunity: { 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' removeContinentFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(3) > vn-icon > i'
}, },
travelBasicData: { travelBasicData: {
@ -863,7 +864,18 @@ export default {
travelDescriptor: { travelDescriptor: {
filterByAgencyButton: 'vn-descriptor-content .quicklinks > div:nth-child(1) > vn-quick-link > a[vn-tooltip="All travels with current agency"]', 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"]', 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: { zoneIndex: {
searchResult: 'vn-zone-index a.vn-tr', searchResult: 'vn-zone-index a.vn-tr',

View File

@ -74,6 +74,7 @@ describe('Route create path', () => {
}); });
it(`should clone the first route`, async() => { 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.firstRouteCheckbox);
await page.waitToClick(selectors.routeIndex.cloneButton); await page.waitToClick(selectors.routeIndex.cloneButton);
await page.waitToClick(selectors.routeIndex.submitClonationButton); await page.waitToClick(selectors.routeIndex.submitClonationButton);

View File

@ -42,4 +42,48 @@ describe('Travel descriptor path', () => {
expect(state).toBe('travel.create'); 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');
});
}); });

View File

@ -18,6 +18,7 @@ describe('Travel extra community path', () => {
it('should edit the travel reference', async() => { it('should edit the travel reference', async() => {
await page.waitToClick(selectors.travelExtraCommunity.removeContinentFilter); await page.waitToClick(selectors.travelExtraCommunity.removeContinentFilter);
await page.waitForSpinnerLoad();
await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelReference, 'edited reference'); await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelReference, 'edited reference');
}); });

View File

@ -33,6 +33,9 @@ exports.translateValues = async(instance, changes) => {
}).format(date); }).format(date);
} }
if (changes instanceof instance)
changes = changes.__data;
const properties = Object.assign({}, changes); const properties = Object.assign({}, changes);
for (let property in properties) { for (let property in properties) {
const relation = getRelation(instance, property); const relation = getRelation(instance, property);
@ -41,13 +44,14 @@ exports.translateValues = async(instance, changes) => {
if (relation) { if (relation) {
let fieldsToShow = ['alias', 'name', 'code', 'description']; 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) if (log && log.showField)
fieldsToShow = log.showField; fieldsToShow = [log.showField];
const model = relation.model; const row = await model.findById(value, {
const row = await models[model].findById(value, {
fields: fieldsToShow fields: fieldsToShow
}); });
const newValue = getValue(row); const newValue = getValue(row);

View File

@ -23,23 +23,27 @@ module.exports = Self => {
}, },
{ {
arg: 'account', arg: 'account',
type: 'string' type: 'any'
}, },
{ {
arg: 'sageTaxTypeFk', arg: 'sageTaxTypeFk',
type: 'number' type: 'any'
}, },
{ {
arg: 'sageWithholdingFk', arg: 'sageWithholdingFk',
type: 'number' type: 'any'
}, },
{ {
arg: 'sageTransactionTypeFk', arg: 'sageTransactionTypeFk',
type: 'number' type: 'any'
}, },
{ {
arg: 'postCode', arg: 'postCode',
type: 'string' type: 'any'
},
{
arg: 'street',
type: 'any'
}, },
{ {
arg: 'city', arg: 'city',
@ -47,11 +51,11 @@ module.exports = Self => {
}, },
{ {
arg: 'provinceFk', arg: 'provinceFk',
type: 'number' type: 'any'
}, },
{ {
arg: 'countryFk', arg: 'countryFk',
type: 'number' type: 'any'
}], }],
returns: { returns: {
arg: 'res', arg: 'res',

View File

@ -130,7 +130,7 @@ module.exports = Self => {
let logRecord = { let logRecord = {
originFk: cleanInstance.id, originFk: cleanInstance.id,
userFk: myUserId, userFk: myUserId,
action: 'create', action: 'insert',
changedModel: 'Ticket', changedModel: 'Ticket',
changedModelId: cleanInstance.id, changedModelId: cleanInstance.id,
oldInstance: {}, oldInstance: {},

View File

@ -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;
}
};
};

View File

@ -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);
});
});

View File

@ -8,6 +8,7 @@ module.exports = Self => {
require('../methods/travel/deleteThermograph')(Self); require('../methods/travel/deleteThermograph')(Self);
require('../methods/travel/updateThermograph')(Self); require('../methods/travel/updateThermograph')(Self);
require('../methods/travel/extraCommunityFilter')(Self); require('../methods/travel/extraCommunityFilter')(Self);
require('../methods/travel/cloneWithEntries')(Self);
Self.rewriteDbError(function(err) { Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY') if (err.code === 'ER_DUP_ENTRY')

View File

@ -43,7 +43,6 @@
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>
<vn-submit <vn-submit
disabled="!watcher.dataChanged()"
label="Save"> label="Save">
</vn-submit> </vn-submit>
<vn-button <vn-button

View File

@ -9,7 +9,7 @@ class Controller extends Section {
onSubmit() { onSubmit() {
return this.$.watcher.submit().then( return this.$.watcher.submit().then(
res => this.$state.go('travel.card.summary', {id: res.data.id}) res => this.$state.go('travel.card.basicData', {id: res.data.id})
); );
} }
} }

View File

@ -22,7 +22,7 @@ describe('Travel Component vnTravelCreate', () => {
controller.onSubmit(); 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', () => {
}); });
}); });
}); });

View File

@ -7,9 +7,17 @@
<vn-item <vn-item
id="clone" id="clone"
ng-click="clone.show()" ng-click="clone.show()"
ng-show="::$ctrl.isBuyer"
translate> translate>
Clone travel Clone travel
</vn-item> </vn-item>
<vn-item
id="cloneWithEntries"
ng-click="cloneWithEntries.show()"
ng-show="::$ctrl.isBuyer"
translate>
Clone travel and his entries
</vn-item>
</vn-list> </vn-list>
</vn-menu> </vn-menu>
@ -20,3 +28,11 @@
question="Do you want to clone this travel?" question="Do you want to clone this travel?"
message="All it's properties will be copied"> message="All it's properties will be copied">
</vn-confirm> </vn-confirm>
<!-- Clone travel popup -->
<vn-confirm
vn-id="cloneWithEntries"
on-accept="$ctrl.onCloneWithEntriesAccept()"
question="Do you want to clone this travel and all containing entries?"
message="All it's properties will be copied">
</vn-confirm>

View File

@ -48,6 +48,10 @@ class Controller extends Section {
.then(res => this.travel = res.data); .then(res => this.travel = res.data);
} }
get isBuyer() {
return this.aclService.hasAny(['buyer']);
}
onCloneAccept() { onCloneAccept() {
const params = JSON.stringify({ const params = JSON.stringify({
ref: this.travel.ref, ref: this.travel.ref,
@ -59,6 +63,11 @@ class Controller extends Section {
}); });
this.$state.go('travel.create', {q: params}); 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']; Controller.$inject = ['$element', '$scope'];

View File

@ -2,11 +2,14 @@ import './index.js';
describe('Travel Component vnTravelDescriptorMenu', () => { describe('Travel Component vnTravelDescriptorMenu', () => {
let controller; let controller;
let $httpBackend;
beforeEach(ngModule('travel')); beforeEach(ngModule('travel'));
beforeEach(inject(($componentController, $state,) => { beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-travel-descriptor-menu></vn-travel-descriptor-menu>'); const $element = angular.element('<vn-travel-descriptor-menu></vn-travel-descriptor-menu>');
controller = $componentController('vnTravelDescriptorMenu', {$element}); controller = $componentController('vnTravelDescriptorMenu', {$element});
controller._travelId = 5;
})); }));
describe('onCloneAccept()', () => { describe('onCloneAccept()', () => {
@ -36,4 +39,18 @@ describe('Travel Component vnTravelDescriptorMenu', () => {
expect(controller.$state.go).toHaveBeenCalledWith('travel.create', {'q': params}); 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)
});
});
});
}); });

View File

@ -1 +1,3 @@
Clone travel: Clonar envío 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?

View File

@ -13,7 +13,7 @@ Received: Recibido
Travel id: Id envío Travel id: Id envío
Search travels by id: Buscar envíos por identificador Search travels by id: Buscar envíos por identificador
New travel: Nuevo envío New travel: Nuevo envío
travel: envio travel: envío
# Sections # Sections
Travels: Envíos Travels: Envíos

View File

@ -7,6 +7,7 @@
<vn-icon-button icon="launch"></vn-icon-button> <vn-icon-button icon="launch"></vn-icon-button>
</a> </a>
<span>{{$ctrl.travelData.id}} - {{$ctrl.travelData.ref}}</span> <span>{{$ctrl.travelData.id}} - {{$ctrl.travelData.ref}}</span>
<vn-travel-descriptor-menu travel-id="$ctrl.travel.id"/>
</h5> </h5>
<vn-horizontal> <vn-horizontal>
<vn-one> <vn-one>