Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2609-travel_index_addEntry

This commit is contained in:
Bernat Exposito 2021-01-04 10:55:53 +01:00
commit ccfeea8e74
48 changed files with 700 additions and 65 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

@ -0,0 +1,18 @@
CREATE TABLE `vn`.`zoneLog` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`originFk` int(10) NOT NULL,
`userFk` int(10) unsigned DEFAULT NULL,
`action` set('insert','update','delete') COLLATE utf8_unicode_ci NOT NULL,
`creationDate` timestamp NULL DEFAULT current_timestamp(),
`description` text CHARACTER SET utf8 DEFAULT NULL,
`changedModel` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL,
`oldInstance` text COLLATE utf8_unicode_ci DEFAULT NULL,
`newInstance` text COLLATE utf8_unicode_ci DEFAULT NULL,
`changedModelId` int(11) DEFAULT NULL,
`changedModelValue` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `originFk` (`originFk`),
KEY `userFk` (`userFk`),
CONSTRAINT `zoneLog_ibfk_1` FOREIGN KEY (`originFk`) REFERENCES `vn`.`zone` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `zoneLog_ibfk_2` FOREIGN KEY (`userFk`) REFERENCES `account`.`user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

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',
@ -927,6 +939,14 @@ export default {
newEntryCompany: 'vn-entry-create vn-autocomplete[ng-model="$ctrl.entry.companyFk"]', newEntryCompany: 'vn-entry-create vn-autocomplete[ng-model="$ctrl.entry.companyFk"]',
saveNewEntry: 'vn-entry-create button[type="submit"]' saveNewEntry: 'vn-entry-create button[type="submit"]'
}, },
entryObservations: {
addNewObservation: 'vn-entry-observation vn-icon-button[icon="add_circle"]',
firstObservationType: 'vn-entry-observation vn-horizontal:nth-child(1) > vn-autocomplete[ng-model="observation.observationTypeFk"]',
secondObservationType: 'vn-entry-observation vn-horizontal:nth-child(2) > vn-autocomplete[ng-model="observation.observationTypeFk"]',
firstObservationDescription: 'vn-entry-observation vn-horizontal:nth-child(1) > vn-textfield[ng-model="observation.description"]',
secondObservationDescription: 'vn-entry-observation vn-horizontal:nth-child(2) > vn-textfield[ng-model="observation.description"]',
saveObservationsButton: 'vn-entry-observation vn-submit > button'
},
supplierSummary: { supplierSummary: {
header: 'vn-supplier-summary > vn-card > h5', header: 'vn-supplier-summary > vn-card > h5',
basicDataId: 'vn-supplier-summary vn-label-value[label="Id"]', basicDataId: 'vn-supplier-summary vn-label-value[label="Id"]',

View File

@ -200,7 +200,7 @@ describe('Client Edit fiscalData path', () => {
it('should confirm the sageTransaction have been edited', async() => { it('should confirm the sageTransaction have been edited', async() => {
const result = await page.waitToGetProperty(selectors.clientFiscalData.sageTransaction, 'value'); const result = await page.waitToGetProperty(selectors.clientFiscalData.sageTransaction, 'value');
expect(result).toEqual('Regularización de inversiones'); expect(result).toEqual('36: Regularización de inversiones');
}); });
it('should confirm the transferor have been edited', async() => { it('should confirm the transferor have been edited', async() => {

View File

@ -1,7 +1,7 @@
import selectors from '../../helpers/selectors.js'; import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer'; import getBrowser from '../../helpers/puppeteer';
describe('claim Summary path', () => { describe('Claim summary path', () => {
let browser; let browser;
let page; let page;
const claimId = '4'; const claimId = '4';

View File

@ -1,7 +1,7 @@
import selectors from '../../helpers/selectors.js'; import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer'; import getBrowser from '../../helpers/puppeteer';
describe('claim Descriptor path', () => { describe('Claim descriptor path', () => {
let browser; let browser;
let page; let page;
const claimId = '1'; const claimId = '1';

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

@ -0,0 +1,66 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Entry observations path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
// await page.loginAndModule('buyer', 'entry'); // access denied, awaiting role confirmation
await page.loginAndModule('developer', 'entry');
await page.accessToSearchResult('2');
await page.accessToSection('entry.card.observation');
});
afterAll(async() => {
await browser.close();
});
it(`should add two new observations of the same type then fail to save as they can't be repeated`, async() => {
await page.waitToClick(selectors.entryObservations.addNewObservation);
await page.waitToClick(selectors.entryObservations.addNewObservation);
await page.autocompleteSearch(selectors.entryObservations.firstObservationType, 'comercial');
await page.autocompleteSearch(selectors.entryObservations.secondObservationType, 'comercial');
await page.write(selectors.entryObservations.firstObservationDescription, 'first observation');
await page.write(selectors.entryObservations.secondObservationDescription, 'second observation');
await page.waitToClick(selectors.entryObservations.saveObservationsButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain(`The observation type can't be repeated`);
});
it('should set the 2nd observation of a different one and successfully save both', async() => {
await page.autocompleteSearch(selectors.entryObservations.secondObservationType, 'delivery');
await page.waitToClick(selectors.entryObservations.saveObservationsButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should reload the section and make sure the first observation type was saved correctly', async() => {
await page.reloadSection('entry.card.observation');
const result = await page.waitToGetProperty(selectors.entryObservations.firstObservationType, 'value');
expect(result).toEqual('comercial');
});
it('should make sure the first observation description was saved correctly', async() => {
const result = await page.waitToGetProperty(selectors.entryObservations.firstObservationDescription, 'value');
expect(result).toEqual('first observation');
});
it('should make sure the second observation type was saved correctly', async() => {
const result = await page.waitToGetProperty(selectors.entryObservations.secondObservationType, 'value');
expect(result).toEqual('delivery');
});
it('should make sure the second observation description was saved correctly', async() => {
const result = await page.waitToGetProperty(selectors.entryObservations.secondObservationDescription, 'value');
expect(result).toEqual('second observation');
});
});

View File

@ -23,10 +23,13 @@ describe('Supplier fiscal data path', () => {
await page.clearInput(selectors.supplierFiscalData.country); await page.clearInput(selectors.supplierFiscalData.country);
await page.clearInput(selectors.supplierFiscalData.postCode); await page.clearInput(selectors.supplierFiscalData.postCode);
await page.write(selectors.supplierFiscalData.city, 'Valencia'); await page.write(selectors.supplierFiscalData.city, 'Valencia');
await page.waitForTimeout(1000); // must repeat this action twice or fails. also #2699 may be a cool solution to this.
await page.clearInput(selectors.supplierFiscalData.city);
await page.write(selectors.supplierFiscalData.city, 'Valencia');
await page.clearInput(selectors.supplierFiscalData.socialName); await page.clearInput(selectors.supplierFiscalData.socialName);
await page.write(selectors.supplierFiscalData.socialName, 'Farmer King SL'); await page.write(selectors.supplierFiscalData.socialName, 'Farmer King SL');
await page.clearInput(selectors.supplierFiscalData.taxNumber); await page.clearInput(selectors.supplierFiscalData.taxNumber);
await page.write(selectors.supplierFiscalData.taxNumber, 'invalid tax number'); await page.write(selectors.supplierFiscalData.taxNumber, 'Wrong tax number');
await page.clearInput(selectors.supplierFiscalData.account); await page.clearInput(selectors.supplierFiscalData.account);
await page.write(selectors.supplierFiscalData.account, 'edited account number'); await page.write(selectors.supplierFiscalData.account, 'edited account number');
await page.autocompleteSearch(selectors.supplierFiscalData.sageWihholding, 'retencion estimacion objetiva'); await page.autocompleteSearch(selectors.supplierFiscalData.sageWihholding, 'retencion estimacion objetiva');

View File

@ -145,9 +145,15 @@ module.exports = function(Self) {
rewriteDbError(replaceErrFunc) { rewriteDbError(replaceErrFunc) {
function replaceErr(err, replaceErrFunc) { function replaceErr(err, replaceErrFunc) {
if (Array.isArray(err)) { if (Array.isArray(err)) {
const errors = err.filter(error => {
return error != undefined && error != null;
});
let errs = []; let errs = [];
for (let e of err) for (let e of errors) {
if (!(e instanceof UserError))
errs.push(replaceErrFunc(e)); errs.push(replaceErrFunc(e));
else errs.push(e);
}
return errs; return errs;
} }
return replaceErrFunc(err); return replaceErrFunc(err);

View File

@ -84,5 +84,7 @@
"companyFk": "Company", "companyFk": "Company",
"You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data", "You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data",
"The social name cannot be empty": "The social name cannot be empty", "The social name cannot be empty": "The social name cannot be empty",
"The nif cannot be empty": "The nif cannot be empty" "The nif cannot be empty": "The nif cannot be empty",
"A travel with this data already exists": "A travel with this data already exists",
"The observation type can't be repeated": "The observation type can't be repeated"
} }

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

@ -1,8 +1,9 @@
<mg-ajax path="Claims/updateClaim/{{patch.params.id}}" options="vnPatch"></mg-ajax>
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"
url="Claims/updateClaim"
data="$ctrl.claim" data="$ctrl.claim"
form="form"> form="form"
save="patch">
</vn-watcher> </vn-watcher>
<vn-crud-model <vn-crud-model
auto-load="true" auto-load="true"

View File

@ -1,7 +1,6 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
// Issue #2471 describe('client sendSms()', () => {
xdescribe('client sendSms()', () => {
let createdLog; let createdLog;
afterAll(async done => { afterAll(async done => {

View File

@ -1,8 +1,7 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
const soap = require('soap'); const soap = require('soap');
// Issue #2471 describe('sms send()', () => {
xdescribe('sms send()', () => {
it('should return the expected message and status code', async() => { it('should return the expected message and status code', async() => {
const code = 200; const code = 200;
const smsConfig = await app.models.SmsConfig.findOne(); const smsConfig = await app.models.SmsConfig.findOne();

View File

@ -42,9 +42,10 @@ export default class Controller extends Section {
// Town auto complete // Town auto complete
set town(selection) { set town(selection) {
const oldValue = this._town;
this._town = selection; this._town = selection;
if (!selection) return; if (!selection || !oldValue) return;
const province = selection.province; const province = selection.province;
const postcodes = selection.postcodes; const postcodes = selection.postcodes;
@ -62,9 +63,10 @@ export default class Controller extends Section {
// Postcode auto complete // Postcode auto complete
set postcode(selection) { set postcode(selection) {
const oldValue = this._postcode;
this._postcode = selection; this._postcode = selection;
if (!selection) return; if (!selection || !oldValue) return;
const town = selection.town; const town = selection.town;
const province = town.province; const province = town.province;

View File

@ -24,12 +24,6 @@
data="sageTaxTypes" data="sageTaxTypes"
order="vat"> order="vat">
</vn-crud-model> </vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTransactionTypes"
data="sageTransactionTypes"
order="transaction">
</vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md"> <form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal> <vn-horizontal>
@ -69,12 +63,15 @@
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete vn-one <vn-autocomplete vn-one
ng-model="$ctrl.client.sageTransactionTypeFk" ng-model="$ctrl.client.sageTransactionTypeFk"
data="sageTransactionTypes" url="SageTransactionTypes"
show-field="transaction" show-field="transaction"
value-field="id" value-field="id"
label="Sage transaction type" label="Sage transaction type"
search-function="{or: [{id: $search}, {transaction: {like: '%'+ $search +'%'}}]}"
vn-acl="salesAssistant" vn-acl="salesAssistant"
order="transaction"
rule> rule>
<tpl-item>{{id}}: {{transaction}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete vn-one <vn-autocomplete vn-one
ng-model="$ctrl.client.transferorFk" ng-model="$ctrl.client.transferorFk"

View File

@ -106,9 +106,10 @@ export default class Controller extends Section {
// Province auto complete // Province auto complete
set province(selection) { set province(selection) {
const oldValue = this._province;
this._province = selection; this._province = selection;
if (!selection) return; if (!selection || !oldValue) return;
const country = selection.country; const country = selection.country;
@ -122,9 +123,10 @@ export default class Controller extends Section {
// Town auto complete // Town auto complete
set town(selection) { set town(selection) {
const oldValue = this._town;
this._town = selection; this._town = selection;
if (!selection) return; if (!selection || !oldValue) return;
const province = selection.province; const province = selection.province;
const country = province.country; const country = province.country;

View File

@ -0,0 +1,29 @@
const app = require('vn-loopback/server/server');
describe('EntryObservation', () => {
const entryId = 1;
it('should throw a user error if the observation type is duplicated.', async() => {
const observations = [
{
entryFk: entryId,
observationTypeFk: 1,
description: 'repeated description'
},
{
entryFk: entryId,
observationTypeFk: 1,
description: 'repeated description'
},
];
let error;
try {
await app.models.EntryObservation.create(observations);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
});

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

@ -24,12 +24,6 @@
data="sageTaxTypes" data="sageTaxTypes"
order="vat"> order="vat">
</vn-crud-model> </vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTransactionTypes"
data="sageTransactionTypes"
order="transaction">
</vn-crud-model>
<vn-crud-model <vn-crud-model
auto-load="true" auto-load="true"
url="SageWithholdings" url="SageWithholdings"
@ -83,11 +77,14 @@
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete vn-one <vn-autocomplete vn-one
ng-model="$ctrl.supplier.sageTransactionTypeFk" ng-model="$ctrl.supplier.sageTransactionTypeFk"
data="sageTransactionTypes" url="SageTransactionTypes"
show-field="transaction" show-field="transaction"
value-field="id" value-field="id"
label="Sage transaction type" label="Sage transaction type"
search-function="{or: [{id: $search}, {transaction: {like: '%'+ $search +'%'}}]}"
order="transaction"
rule> rule>
<tpl-item>{{id}}: {{transaction}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>

View File

@ -8,9 +8,10 @@ export default class Controller extends Section {
// Province auto complete // Province auto complete
set province(selection) { set province(selection) {
const oldValue = this._province;
this._province = selection; this._province = selection;
if (!selection) return; if (!selection || !oldValue) return;
const country = selection.country; const country = selection.country;
@ -24,9 +25,10 @@ export default class Controller extends Section {
// Town auto complete // Town auto complete
set town(selection) { set town(selection) {
const oldValue = this._town;
this._town = selection; this._town = selection;
if (!selection) return; if (!selection || !oldValue) return;
const province = selection.province; const province = selection.province;
const country = province.country; const country = province.country;

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

@ -1,7 +1,6 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
// Issue #2471 describe('ticket sendSms()', () => {
xdescribe('ticket sendSms()', () => {
let logId; let logId;
afterAll(async done => { afterAll(async done => {

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,6 +7,7 @@
<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>
@ -16,6 +17,13 @@
translate> translate>
Add entry Add entry
</a> </a>
<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>
@ -26,3 +34,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,2 +1,4 @@
Clone travel: Clonar envío Clone travel: Clonar envío
Add entry: Añadir entrada Add entry: Añadir entrada
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>

View File

@ -31,5 +31,8 @@
}, },
"ZoneEstimatedDelivery": { "ZoneEstimatedDelivery": {
"dataSource": "vn" "dataSource": "vn"
},
"ZoneLog": {
"dataSource": "vn"
} }
} }

View File

@ -1,6 +1,10 @@
{ {
"name": "ZoneEvent", "name": "ZoneEvent",
"base": "VnModel", "base": "Loggable",
"log": {
"model":"ZoneLog",
"relation": "zone"
},
"options": { "options": {
"mysql": { "mysql": {
"table": "zoneEvent" "table": "zoneEvent"

View File

@ -1,6 +1,11 @@
{ {
"name": "ZoneIncluded", "name": "ZoneIncluded",
"base": "VnModel", "base": "Loggable",
"log": {
"model": "ZoneLog",
"relation": "zone",
"showField": "isIncluded"
},
"options": { "options": {
"mysql": { "mysql": {
"table": "zoneIncluded" "table": "zoneIncluded"

View File

@ -0,0 +1,58 @@
{
"name": "ZoneLog",
"base": "VnModel",
"options": {
"mysql": {
"table": "zoneLog"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"forceId": false
},
"originFk": {
"type": "Number",
"required": true
},
"userFk": {
"type": "Number"
},
"action": {
"type": "String",
"required": true
},
"changedModel": {
"type": "String"
},
"oldInstance": {
"type": "Object"
},
"newInstance": {
"type": "Object"
},
"creationDate": {
"type": "Date"
},
"changedModelId": {
"type": "String"
},
"changedModelValue": {
"type": "String"
},
"description": {
"type": "String"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -1,6 +1,10 @@
{ {
"name": "ZoneWarehouse", "name": "ZoneWarehouse",
"base": "VnModel", "base": "Loggable",
"log": {
"model":"ZoneLog",
"relation": "zone"
},
"options": { "options": {
"mysql": { "mysql": {
"table": "zoneWarehouse" "table": "zoneWarehouse"

View File

@ -1,6 +1,10 @@
{ {
"name": "Zone", "name": "Zone",
"base": "VnModel", "base": "Loggable",
"log": {
"model":"ZoneLog",
"showField": "name"
},
"options": { "options": {
"mysql": { "mysql": {
"table": "zone" "table": "zone"
@ -39,11 +43,6 @@
} }
}, },
"relations": { "relations": {
"geolocations": {
"type": "hasMany",
"model": "ZoneGeo",
"foreignKey": "zoneFk"
},
"agencyMode": { "agencyMode": {
"type": "belongsTo", "type": "belongsTo",
"model": "AgencyMode", "model": "AgencyMode",

View File

@ -16,3 +16,4 @@ import './calendar';
import './location'; import './location';
import './calendar'; import './calendar';
import './upcoming-deliveries'; import './upcoming-deliveries';
import './log';

View File

@ -0,0 +1 @@
<vn-log url="ZoneLogs" origin-id="$ctrl.$params.id"></vn-log>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import Section from 'salix/components/section';
ngModule.vnComponent('vnZoneLog', {
template: require('./index.html'),
controller: Section,
});

View File

@ -14,6 +14,7 @@
{"state": "zone.card.basicData", "icon": "settings"}, {"state": "zone.card.basicData", "icon": "settings"},
{"state": "zone.card.location", "icon": "my_location"}, {"state": "zone.card.location", "icon": "my_location"},
{"state": "zone.card.warehouses", "icon": "home"}, {"state": "zone.card.warehouses", "icon": "home"},
{"state": "zone.card.log", "icon": "history"},
{"state": "zone.card.events", "icon": "today"} {"state": "zone.card.events", "icon": "today"}
] ]
}, },
@ -84,6 +85,11 @@
"params": { "params": {
"zone": "$ctrl.zone" "zone": "$ctrl.zone"
} }
}, {
"url" : "/log",
"state": "zone.card.log",
"component": "vn-zone-log",
"description": "Log"
} }
] ]
} }