Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2538-supplier_contact

This commit is contained in:
Bernat Exposito 2020-11-02 11:14:42 +01:00
commit d07e95a7e9
103 changed files with 38871 additions and 4146 deletions

View File

@ -9,7 +9,7 @@
"properties": {
"id": {
"type": "number",
"required": true
"id": true
},
"name": {
"type": "string",
@ -56,7 +56,8 @@
"roles": {
"type": "hasMany",
"model": "RoleRole",
"foreignKey": "role"
"foreignKey": "role",
"primaryKey": "roleFk"
},
"emailUser": {
"type": "hasOne",

View File

@ -0,0 +1,9 @@
ALTER TABLE `vn`.`observationType`
ADD COLUMN `code` VARCHAR(45) NOT NULL AFTER `description`;
UPDATE `vn`.`observationType` SET `code` = 'itemPicker' WHERE (`id` = '1');
UPDATE `vn`.`observationType` SET `code` = 'packager' WHERE (`id` = '2');
UPDATE `vn`.`observationType` SET `code` = 'salesPerson' WHERE (`id` = '4');
UPDATE `vn`.`observationType` SET `code` = 'administrative' WHERE (`id` = '5');
UPDATE `vn`.`observationType` SET `code` = 'weight' WHERE (`id` = '6');
UPDATE `vn`.`observationType` SET `code` = 'delivery' WHERE (`id` = '3');

View File

@ -0,0 +1,7 @@
ALTER TABLE `account`.`roleRole`
ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
ADD PRIMARY KEY (`id`);
UPDATE `account`.`role` SET id = 100 WHERE `name` = 'root';
CALL account.role_sync;

View File

@ -0,0 +1,25 @@
DROP TRIGGER IF EXISTS `vn`.`ticket_afterUpdate`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` TRIGGER `ticket_afterUpdate`
AFTER UPDATE ON `ticket`
FOR EACH ROW
BEGIN
IF !(NEW.id <=> OLD.id)
OR !(NEW.warehouseFk <=> OLD.warehouseFk)
OR !(NEW.shipped <=> OLD.shipped) THEN
CALL stock.log_add('ticket', NEW.id, OLD.id);
END IF;
IF NEW.clientFk = 2067 AND !(NEW.clientFk <=> OLD.clientFk) THEN
-- Fallo que se insertan no se sabe como tickets en este cliente
INSERT INTO vn.mail SET
`sender` = 'jgallego@verdnatura.es',
`replyTo` = 'jgallego@verdnatura.es',
`subject` = 'Modificado ticket al cliente 2067',
`body` = CONCAT(account.myUserGetName(), ' ha modificado el ticket ',
NEW.id);
END IF;
END$$
DELIMITER ;

View File

@ -325,7 +325,7 @@ INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `pr
(119, 'address 19', 'Somewhere in Alberic', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(120, 'address 20', 'Somewhere in Montortal', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(121, 'address 21', 'the bat cave', 'Silla', 46460, 1, 1111111111, 222222222, 1, 101, 2, NULL, NULL, 0, 0),
(122, 'address 22', 'NY roofs', 'Silla', 46460, 1, 1111111111, 222222222, 1, 102, 2, NULL, NULL, 0, 0),
(122, 'NY roofs', 'address 22', 'Silla', 46460, 1, 1111111111, 222222222, 1, 102, 2, NULL, NULL, 0, 0),
(123, 'address 23', 'The phone box', 'Silla', 46460, 1, 1111111111, 222222222, 1, 103, 2, NULL, NULL, 0, 0),
(124, 'address 24', 'Stark tower Silla', 'Silla', 46460, 1, 1111111111, 222222222, 1, 104, 2, NULL, NULL, 0, 0),
(125, 'address 25', 'The plastic cell', 'Silla', 46460, 1, 1111111111, 222222222, 1, 105, 2, NULL, NULL, 0, 0),
@ -400,18 +400,20 @@ INSERT INTO `vn`.`clientObservation`(`id`, `clientFk`, `workerFk`, `text`, `crea
(9, 109, 18, 'HULK SMASH! ...', CURDATE()),
(10, 110, 18, 'They say everyone is born a hero. But if you let it, life will push you over the line until you are the villain.', CURDATE());
INSERT INTO `vn`.`observationType`(`id`,`description`)
INSERT INTO `vn`.`observationType`(`id`,`description`, `code`)
VALUES
(1,'observation one'),
(2,'observation two'),
(3,'observation three'),
(4,'comercial');
(1, 'observation one', 'observation one'),
(2, 'observation two', 'observation two'),
(3, 'observation three', 'observation three'),
(4, 'comercial', 'salesPerson'),
(5, 'delivery', 'delivery');
INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`)
VALUES
(1, 121, 1, 'under the floor'),
(2, 121, 2, 'wears leather and goes out at night'),
(3, 121, 3, 'care with the dog');
(3, 121, 3, 'care with the dog'),
(5, 122, 5, 'Delivery after 10am');
INSERT INTO `vn`.`creditClassification`(`id`, `client`, `dateStart`, `dateEnd`)
VALUES
@ -605,7 +607,8 @@ INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `des
(8, 23, 2, 'wears leather and goes out at night'),
(9, 23, 3, 'care with the dog'),
(10, 23, 4, 'Reclama ticket: 8'),
(11, 24, 4, 'Reclama ticket: 7');
(11, 24, 4, 'Reclama ticket: 7'),
(12, 11, 5, 'Delivery after 10am');
-- FIX for state hours on local, inter_afterInsert
UPDATE vncontrol.inter SET odbc_date = DATE_ADD(CURDATE(), INTERVAL -10 SECOND);

View File

@ -316,10 +316,23 @@ let actions = {
},
waitForTextInElement: async function(selector, text) {
await this.waitForSelector(selector);
return await this.waitForFunction((selector, text) => {
return document.querySelector(selector).innerText.toLowerCase().includes(text.toLowerCase());
}, {}, selector, text);
const expectedText = text.toLowerCase();
return new Promise((resolve, reject) => {
let attempts = 0;
const interval = setInterval(async() => {
const currentText = await this.evaluate(selector => {
return document.querySelector(selector).innerText.toLowerCase();
}, selector);
if (currentText === expectedText || attempts === 40) {
clearInterval(interval);
resolve(currentText);
}
attempts += 1;
}, 100);
}).then(result => {
return expect(result).toContain(expectedText);
});
},
selectorFormater: function(selector) {

View File

@ -123,19 +123,19 @@ export default {
streetAddress: 'vn-textfield[ng-model="$ctrl.address.street"]',
postcode: 'vn-datalist[ng-model="$ctrl.address.postalCode"]',
city: 'vn-datalist[ng-model="$ctrl.address.city"]',
province: 'vn-autocomplete[ng-model="$ctrl.address.provinceId"]',
agency: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeId"]',
province: 'vn-autocomplete[ng-model="$ctrl.address.provinceFk"]',
agency: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeFk"]',
phone: 'vn-textfield[ng-model="$ctrl.address.phone"]',
mobileInput: 'vn-textfield[ng-model="$ctrl.address.mobile"]',
defaultAddress: 'vn-client-address-index div:nth-child(1) div[name="street"]',
incoterms: 'vn-autocomplete[ng-model="$ctrl.address.incotermsId"]',
addNewCustomsAgent: 'vn-client-address-create vn-autocomplete[ng-model="$ctrl.address.customsAgentId"] vn-icon-button[icon="add_circle"]',
incoterms: 'vn-autocomplete[ng-model="$ctrl.address.incotermsFk"]',
addNewCustomsAgent: 'vn-client-address-create vn-autocomplete[ng-model="$ctrl.address.customsAgentFk"] vn-icon-button[icon="add_circle"]',
newCustomsAgentFiscalID: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.nif"]',
newCustomsAgentFiscalName: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.fiscalName"]',
newCustomsAgentStreet: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.street"]',
newCustomsAgentPhone: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.phone"]',
saveNewCustomsAgentButton: 'button[response="accept"]',
customsAgent: 'vn-autocomplete[ng-model="$ctrl.address.customsAgentId"]',
customsAgent: 'vn-autocomplete[ng-model="$ctrl.address.customsAgentFk"]',
secondMakeDefaultStar: 'vn-client-address-index vn-card div:nth-child(2) vn-icon-button[icon="star_border"]',
firstEditAddress: 'vn-client-address-index div:nth-child(1) > a',
secondEditAddress: 'vn-client-address-index div:nth-child(2) > a',

View File

@ -19,7 +19,7 @@ describe('Login path', async() => {
const message = await page.waitForSnackbar();
const state = await page.getState();
expect(message.type).toBe('error');
expect(message.text).toBe('Invalid login, remember that distinction is made between uppercase and lowercase');
expect(state).toBe('login');
});
@ -28,7 +28,7 @@ describe('Login path', async() => {
const message = await page.waitForSnackbar();
const state = await page.getState();
expect(message.type).toBe('error');
expect(message.text).toBe('Invalid login, remember that distinction is made between uppercase and lowercase');
expect(state).toBe('login');
});
@ -37,7 +37,7 @@ describe('Login path', async() => {
const message = await page.waitForSnackbar();
const state = await page.getState();
expect(message.type).toBe('error');
expect(message.text).toBe('Please enter your username');
expect(state).toBe('login');
});
});

View File

@ -119,7 +119,7 @@ describe('Client create path', () => {
await page.waitToClick(selectors.createClientView.createButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should click on the Clients button of the top bar menu', async() => {

View File

@ -37,7 +37,7 @@ describe('Client Edit basicData path', () => {
await page.waitToClick(selectors.clientBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the name have been edited', async() => {
@ -101,7 +101,7 @@ describe('Client Edit basicData path', () => {
await page.waitToClick(selectors.clientBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should now confirm the name have been edited', async() => {

View File

@ -93,7 +93,7 @@ describe('Client Edit fiscalData path', () => {
await page.waitToClick(selectors.globalItems.acceptButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should propagate the Equalization tax', async() => {
@ -119,7 +119,7 @@ describe('Client Edit fiscalData path', () => {
await page.waitToClick(selectors.clientFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
// confirm all addresses have now EQtax checked step 1
@ -155,7 +155,7 @@ describe('Client Edit fiscalData path', () => {
await page.waitToClick(selectors.clientFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should propagate the Equalization tax changes', async() => {
@ -289,7 +289,7 @@ describe('Client Edit fiscalData path', () => {
await page.waitToClick(selectors.clientAddresses.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
// confirm invoice by address checkbox gets checked if the EQtax differs between addresses step 3

View File

@ -91,7 +91,7 @@ describe('Client Add address path', () => {
await page.waitToClick(selectors.clientAddresses.secondMakeDefaultStar);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the default address is the expected one`, async() => {

View File

@ -48,6 +48,6 @@ describe('Client add address notes path', () => {
await page.waitToClick(selectors.clientAddresses.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});

View File

@ -23,7 +23,7 @@ describe('Client Edit web access path', () => {
await page.waitToClick(selectors.clientWebAccess.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm web access is now unchecked', async() => {

View File

@ -31,7 +31,7 @@ describe('Client Add notes path', () => {
await page.waitToClick(selectors.clientNotes.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the note was created', async() => {

View File

@ -27,7 +27,7 @@ describe('Client Add credit path', () => {
await page.waitToClick(selectors.clientCredit.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the credit was updated', async() => {

View File

@ -36,7 +36,7 @@ describe('Client Add greuge path', () => {
await page.waitToClick(selectors.clientGreuge.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the greuge was added to the list', async() => {

View File

@ -32,7 +32,7 @@ describe('Client lock verified data path', () => {
await page.waitToClick(selectors.clientFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the social name have been edited', async() => {
@ -62,7 +62,7 @@ describe('Client lock verified data path', () => {
await page.waitToClick(selectors.globalItems.acceptButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm Verified data checkbox is checked', async() => {
@ -79,7 +79,7 @@ describe('Client lock verified data path', () => {
await page.waitToClick(selectors.clientFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should again confirm the social name have been edited', async() => {
@ -133,7 +133,7 @@ describe('Client lock verified data path', () => {
await page.waitToClick(selectors.clientFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should now confirm the social name have been edited once and for all', async() => {

View File

@ -22,7 +22,7 @@ describe('Client log path', () => {
await page.waitToClick(selectors.clientBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should navigate to the log section', async() => {

View File

@ -20,7 +20,7 @@ describe('Client balance path', () => {
await page.autocompleteSearch(selectors.globalItems.userLocalCompany, 'CCs');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should access to the balance section to check the data shown matches the local settings', async() => {
@ -35,7 +35,7 @@ describe('Client balance path', () => {
await page.clearInput(selectors.globalItems.userConfigThirdAutocomplete);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should click the new payment button', async() => {
@ -51,7 +51,7 @@ describe('Client balance path', () => {
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should edit the 1st line reference', async() => {
@ -60,7 +60,7 @@ describe('Client balance path', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check balance is now 0, the reference was saved and the company is now VNL becouse the user local settings were removed', async() => {
@ -85,7 +85,7 @@ describe('Client balance path', () => {
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check balance is now -100', async() => {
@ -101,7 +101,7 @@ describe('Client balance path', () => {
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check balance is now 50', async() => {

View File

@ -22,7 +22,7 @@ describe('Client DMS', () => {
await page.respondToDialog('accept');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should click on the first document line worker name making the descriptor visible`, async() => {

View File

@ -24,7 +24,7 @@ describe('Client contacts', () => {
await page.waitToClick(selectors.clientContacts.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should delete de contact', async() => {
@ -33,6 +33,6 @@ describe('Client contacts', () => {
await page.waitToClick(selectors.clientContacts.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});

View File

@ -26,7 +26,7 @@ describe('Worker basic data path', () => {
await page.waitToClick(selectors.workerBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should reload the section then check the name was edited', async() => {

View File

@ -93,12 +93,8 @@ describe('Worker time control path', () => {
expect(result).toEqual(scanTime);
});
it(`should check Hank Pym worked 8 hours`, async() => {
it(`should check Hank Pym worked 7 hours`, async() => {
await page.waitForTextInElement(selectors.workerTimeControl.mondayWorkedHours, '07:00 h.');
const result = await page
.waitToGetProperty(selectors.workerTimeControl.mondayWorkedHours, 'innerText');
expect(result).toEqual('07:00 h.');
});
});
@ -149,9 +145,6 @@ describe('Worker time control path', () => {
it(`should check Hank Pym worked 8 happy hours`, async() => {
await page.waitForTextInElement(selectors.workerTimeControl.tuesdayWorkedHours, '08:00 h.');
const result = await page.waitToGetProperty(selectors.workerTimeControl.tuesdayWorkedHours, 'innerText');
expect(result).toEqual('08:00 h.');
});
});
@ -202,9 +195,6 @@ describe('Worker time control path', () => {
it(`should check Hank Pym worked 8 cheerfull hours`, async() => {
await page.waitForTextInElement(selectors.workerTimeControl.wednesdayWorkedHours, '08:00 h.');
const result = await page.waitToGetProperty(selectors.workerTimeControl.wednesdayWorkedHours, 'innerText');
expect(result).toEqual('08:00 h.');
});
});
@ -252,9 +242,6 @@ describe('Worker time control path', () => {
it(`should check Hank Pym worked 8 joyfull hours`, async() => {
await page.waitForTextInElement(selectors.workerTimeControl.thursdayWorkedHours, '08:00 h.');
const result = await page.waitToGetProperty(selectors.workerTimeControl.thursdayWorkedHours, 'innerText');
expect(result).toEqual('08:00 h.');
});
});
@ -301,9 +288,6 @@ describe('Worker time control path', () => {
it(`should check Hank Pym worked 8 hours with a smile on his face`, async() => {
await page.waitForTextInElement(selectors.workerTimeControl.fridayWorkedHours, '08:00 h.');
const result = await page.waitToGetProperty(selectors.workerTimeControl.fridayWorkedHours, 'innerText');
expect(result).toEqual('08:00 h.');
});
});
});
@ -346,9 +330,6 @@ describe('Worker time control path', () => {
it(`should check Hank Pym worked 8 hours with all his will`, async() => {
await page.waitForTextInElement(selectors.workerTimeControl.saturdayWorkedHours, '08:00 h.');
const result = await page.waitToGetProperty(selectors.workerTimeControl.saturdayWorkedHours, 'innerText');
expect(result).toEqual('08:00 h.');
});
});
@ -375,19 +356,12 @@ describe('Worker time control path', () => {
it(`should check Hank Pym worked 8 glad hours`, async() => {
await page.waitForTextInElement(selectors.workerTimeControl.sundayWorkedHours, '08:00 h.');
const result = await page.waitToGetProperty(selectors.workerTimeControl.sundayWorkedHours, 'innerText');
expect(result).toEqual('08:00 h.');
});
it(`should check Hank Pym doesn't have hours set on the next months first week`, async() => {
await page.waitToClick(selectors.workerTimeControl.nextMonthButton);
await page.waitToClick(selectors.workerTimeControl.secondWeekDay);
await page.waitForTextInElement(selectors.workerTimeControl.weekWorkedHours, '00:00 h.');
const wholeWeekHours = await page
.waitToGetProperty(selectors.workerTimeControl.weekWorkedHours, 'innerText');
expect(wholeWeekHours).toEqual('00:00 h.');
});
it(`should check he didn't scan in this week yet`, async() => {
@ -410,11 +384,8 @@ describe('Worker time control path', () => {
await page.accessToSection('worker.card.timeControl');
});
it('should check his hours are alright', async() => {
it('should check his weekly hours are alright', async() => {
await page.waitForTextInElement(selectors.workerTimeControl.weekWorkedHours, '55:00 h.');
const wholeWeekHours = await page.waitToGetProperty(selectors.workerTimeControl.weekWorkedHours, 'innerText');
expect(wholeWeekHours).toEqual('55:00 h.');
});
});
});

View File

@ -24,7 +24,7 @@ describe('Worker calendar path', () => {
expect(result).toContain(' 5 ');
});
it('should set two days as holidays on the calendar', async() => {
it('should set two days as holidays on the calendar and check the total holidays increased by 1.5', async() => {
await page.waitToClick(selectors.workerCalendar.holidays);
await page.waitFor(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.januaryThirtyFirst);
@ -50,9 +50,8 @@ describe('Worker calendar path', () => {
await page.waitToClick(selectors.workerCalendar.halfFurlough);
await page.waitFor(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.mayEighth);
});
await page.waitFor(reasonableTimeBetweenClicks);
it('should check the total holidays increased by 1.5', async() => {
const result = await page.waitToGetProperty(selectors.workerCalendar.totalHolidaysUsed, 'innerText');
expect(result).toContain(' 6.5 ');

View File

@ -38,7 +38,7 @@ describe('Item Edit basic data path', () => {
await page.waitToClick(selectors.itemBasicData.submitBasicDataButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should create a new intrastat`, async() => {
@ -57,7 +57,7 @@ describe('Item Edit basic data path', () => {
await page.waitToClick(selectors.itemBasicData.submitBasicDataButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the item name was edited`, async() => {

View File

@ -23,7 +23,7 @@ describe('Item edit tax path', () => {
await page.waitToClick(selectors.itemTax.submitTaxButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the first item tax class was edited`, async() => {

View File

@ -26,7 +26,7 @@ describe('Item create tags path', () => {
await page.waitToClick(selectors.itemTags.submitItemTagsButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the fourth row data is the expected one`, async() => {

View File

@ -25,7 +25,7 @@ describe('Item create niche path', () => {
await page.waitToClick(selectors.itemNiches.submitNichesButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the first niche is the expected one`, async() => {

View File

@ -23,7 +23,7 @@ describe('Item Create botanical path', () => {
await page.waitToClick(selectors.itemBotanical.submitBotanicalButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the botanical for the item was created`, async() => {
@ -58,7 +58,7 @@ describe('Item Create botanical path', () => {
await page.waitToClick(selectors.itemBotanical.submitBotanicalButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the botanical for the item was edited`, async() => {

View File

@ -23,7 +23,7 @@ describe('Item Create barcodes path', () => {
await page.waitToClick(selectors.itemBarcodes.submitBarcodesButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the barcode 5 is created and it is now the third barcode as the first was deleted`, async() => {

View File

@ -45,7 +45,7 @@ describe('Item Create/Clone path', () => {
await page.waitToClick(selectors.itemCreateView.createButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm Infinity Gauntlet item was created', async() => {

View File

@ -20,7 +20,7 @@ describe('Item regularize path', () => {
await page.autocompleteSearch(selectors.globalItems.userLocalWarehouse, 'Warehouse Four');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check the local settings were saved', async() => {
@ -51,7 +51,7 @@ describe('Item regularize path', () => {
await page.waitToClick(selectors.itemDescriptor.regularizeSaveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should click on the Tickets button of the top bar menu', async() => {
@ -70,7 +70,7 @@ describe('Item regularize path', () => {
await page.clearInput(selectors.globalItems.userConfigFirstAutocomplete);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should search for the ticket with alias missing', async() => {
@ -114,7 +114,7 @@ describe('Item regularize path', () => {
await page.waitToClick(selectors.itemDescriptor.regularizeSaveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should again click on the Tickets button of the top bar menu', async() => {

View File

@ -36,7 +36,7 @@ describe('Item index path', () => {
await page.waitToClick(selectors.itemsIndex.saveFieldsButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should navigate forth and back to see the images column is still visible', async() => {
@ -70,7 +70,7 @@ describe('Item index path', () => {
await page.waitToClick(selectors.itemsIndex.saveFieldsButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should now navigate forth and back to see the ids column is now visible', async() => {

View File

@ -34,7 +34,7 @@ describe('Item log path', () => {
await page.waitToClick(selectors.itemCreateView.createButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should return to the items index by clicking the return to items button', async() => {

View File

@ -29,7 +29,7 @@ describe('Item descriptor path', () => {
await page.waitToClick(selectors.itemBasicData.submitBasicDataButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should reload the section and check the inactive icon is bright', async() => {
@ -45,6 +45,6 @@ describe('Item descriptor path', () => {
await page.waitToClick(selectors.itemBasicData.submitBasicDataButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});

View File

@ -77,7 +77,7 @@ describe('Ticket List sale path', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should update the description of the new sale', async() => {
@ -86,7 +86,7 @@ describe('Ticket List sale path', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should add a third empty item to the sale list', async() => {
@ -104,7 +104,7 @@ describe('Ticket List sale path', () => {
await page.waitToClick(selectors.globalItems.acceptButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should verify there's only 1 single line remaining`, async() => {

View File

@ -41,7 +41,7 @@ describe('Ticket Edit sale path', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should check it's state is libre now`, async() => {
@ -55,7 +55,7 @@ describe('Ticket Edit sale path', () => {
await page.waitToClick(selectors.ticketSales.setOk);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should check it's state is OK now`, async() => {
@ -135,7 +135,7 @@ describe('Ticket Edit sale path', () => {
await page.type(selectors.ticketSales.firstSaleQuantity, '9\u000d');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should update the price', async() => {
@ -144,7 +144,7 @@ describe('Ticket Edit sale path', () => {
await page.type(selectors.ticketSales.firstSalePriceInput, '5\u000d');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the price have been updated', async() => {
@ -165,7 +165,7 @@ describe('Ticket Edit sale path', () => {
await page.type(selectors.ticketSales.firstSaleDiscountInput, '50\u000d');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the discount have been updated', async() => {
@ -224,7 +224,7 @@ describe('Ticket Edit sale path', () => {
await page.waitForSpinnerLoad();
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the third sale was deleted`, async() => {

View File

@ -24,7 +24,7 @@ describe('Ticket Create notes path', () => {
await page.waitToClick(selectors.ticketNotes.submitNotesButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the note is the expected one', async() => {
@ -45,6 +45,6 @@ describe('Ticket Create notes path', () => {
await page.waitToClick(selectors.ticketNotes.submitNotesButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});

View File

@ -51,7 +51,7 @@ describe('Ticket Create packages path', () => {
await page.waitToClick(selectors.ticketPackages.savePackagesButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the first select is the expected one`, async() => {

View File

@ -36,7 +36,7 @@ describe('Ticket Create new tracking state path', () => {
await page.waitToClick(selectors.createStateView.saveStateButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});
@ -74,7 +74,7 @@ describe('Ticket Create new tracking state path', () => {
await page.waitToClick(selectors.createStateView.saveStateButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});
});

View File

@ -33,7 +33,7 @@ describe('Ticket descriptor path', () => {
await page.waitToClick(selectors.ticketDescriptor.thursdayButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should again click on the Tickets button of the top bar menu', async() => {
@ -68,7 +68,7 @@ describe('Ticket descriptor path', () => {
await page.waitToClick(selectors.ticketDescriptor.saturdayButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should click on the Tickets button of the top bar menu once again', async() => {
@ -97,7 +97,7 @@ describe('Ticket descriptor path', () => {
await page.waitToClick(selectors.ticketsIndex.acceptDeleteTurn);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the sixth weekly ticket was deleted', async() => {
@ -111,11 +111,11 @@ describe('Ticket descriptor path', () => {
await page.autocompleteSearch(selectors.ticketsIndex.firstWeeklyTicketAgency, 'Silla247');
let message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
await page.clearInput(selectors.ticketsIndex.firstWeeklyTicketAgency);
message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});

View File

@ -27,7 +27,7 @@ describe('Ticket purchase request path', () => {
await page.waitToClick(selectors.ticketRequests.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should have been redirected to the request index', async() => {
@ -39,24 +39,24 @@ describe('Ticket purchase request path', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the new request was added', async() => {
it('should check the new request was added', async() => {
await page.reloadSection('ticket.card.request.index');
const result = await page.waitToGetProperty(selectors.ticketRequests.thirdRequestQuantity, 'value');
expect(result).toEqual('99');
});
it(`should confirm first request can't be edited as its state is different to new`, async() => {
it(`should check the first request can't be edited as its state is different to new`, async() => {
await page.waitForClassPresent(selectors.ticketRequests.firstRequestQuantity, 'disabled');
const result = await page.isDisabled(selectors.ticketRequests.firstRequestQuantity);
expect(result).toBe(true);
});
it(`should confirm second request can't be edited as its state is different to new`, async() => {
it(`should check the second request can't be edited as its state is different to new`, async() => {
await page.waitForClassPresent(selectors.ticketRequests.secondRequestQuantity, 'disabled');
const result = await page.isDisabled(selectors.ticketRequests.secondRequestQuantity);
@ -67,10 +67,10 @@ describe('Ticket purchase request path', () => {
await page.waitToClick(selectors.ticketRequests.thirdRemoveRequestButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the request was deleted', async() => {
it('should check the request was deleted', async() => {
await page.reloadSection('ticket.card.request.index');
await page.wait(selectors.ticketRequests.addRequestButton);
await page.waitForSelector(selectors.ticketRequests.thirdDescription, {hidden: true});

View File

@ -100,7 +100,7 @@ describe('Ticket descriptor path', () => {
await page.waitToClick(selectors.ticketDescriptor.addStowawayDialogFirstTicket);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
xit(`should check the state of the stowaway ticket is embarked`, async() => {
@ -122,7 +122,7 @@ describe('Ticket descriptor path', () => {
await page.waitToClick(selectors.ticketDescriptor.acceptDeleteStowawayButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the ship buton doesn't exisist any more`, async() => {

View File

@ -83,7 +83,7 @@ describe('Ticket services path', () => {
await page.waitToClick(selectors.ticketService.saveServiceButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the service description was created correctly', async() => {
@ -114,7 +114,7 @@ describe('Ticket services path', () => {
await page.waitToClick(selectors.ticketService.saveServiceButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the service was removed`, async() => {

View File

@ -31,7 +31,7 @@ describe('Ticket create path', () => {
await page.waitToClick(selectors.createTicketView.createButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check the url is now the summary of the ticket', async() => {
@ -55,7 +55,7 @@ describe('Ticket create path', () => {
await page.waitToClick(selectors.createTicketView.createButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check the url is now the summary of the created ticket', async() => {
@ -68,7 +68,7 @@ describe('Ticket create path', () => {
await page.waitToClick(selectors.ticketDescriptor.addStowawayDialogFirstTicket);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should delete the current ticket', async() => {
@ -77,7 +77,7 @@ describe('Ticket create path', () => {
await page.waitToClick(selectors.ticketDescriptor.acceptDialog);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Ticket deleted. You can undo this action within the first hour');
});
it('should search for the stowaway ticket of the previously deleted ticket', async() => {

View File

@ -80,7 +80,7 @@ describe('Ticket Summary path', () => {
await page.waitToClick(selectors.ticketSummary.setOk);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the ticket state was updated', async() => {

View File

@ -29,7 +29,7 @@ describe('Ticket log path', () => {
await page.waitToClick(selectors.ticketNotes.submitNotesButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should navigate to the log section', async() => {

View File

@ -42,7 +42,7 @@ describe('Ticket index payout path', () => {
await page.waitToClick(selectors.ticketsIndex.submitPayout);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should navigate to the client balance section and check a new balance line was entered', async() => {

View File

@ -27,7 +27,7 @@ describe('Claim edit basic data path', () => {
await page.waitToClick(selectors.claimBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should have been redirected to the next section of claims as the role is claimManager`, async() => {
@ -40,7 +40,7 @@ describe('Claim edit basic data path', () => {
await page.waitToClick(selectors.claimBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the claim state was edited', async() => {
@ -71,6 +71,6 @@ describe('Claim edit basic data path', () => {
await page.waitToClick(selectors.claimBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
});

View File

@ -28,7 +28,7 @@ describe('Claim development', () => {
await page.waitToClick(selectors.claimDevelopment.saveDevelopmentButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should redirect to the next section of claims as the role is claimManager`, async() => {
@ -45,7 +45,7 @@ describe('Claim development', () => {
await page.waitToClick(selectors.claimDevelopment.saveDevelopmentButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the first development is the expected one', async() => {

View File

@ -23,7 +23,7 @@ xdescribe('Claim detail', () => {
await page.waitToClick(selectors.claimDetail.firstClaimableSaleFromTicket);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the claim contains now two items', async() => {
@ -38,7 +38,7 @@ xdescribe('Claim detail', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the first item quantity, and the claimed total were correctly edited', async() => {
@ -67,7 +67,7 @@ xdescribe('Claim detail', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check the mana is the expected one', async() => {
@ -81,7 +81,7 @@ xdescribe('Claim detail', () => {
await page.waitToClick(selectors.claimDetail.secondItemDeleteButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the claim contains now one item', async() => {
@ -95,7 +95,7 @@ xdescribe('Claim detail', () => {
await page.waitToClick(selectors.claimDetail.firstClaimableSaleFromTicket);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should have been redirected to the next section in claims`, async() => {

View File

@ -21,7 +21,7 @@ describe('Claim action path', () => {
await page.waitToClick(selectors.claimAction.importClaimButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should import the second importable ticket', async() => {
@ -33,7 +33,7 @@ describe('Claim action path', () => {
await page.waitToClick(selectors.claimAction.secondImportableTicket);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should edit the second line destination field', async() => {
@ -41,14 +41,14 @@ describe('Claim action path', () => {
await page.autocompleteSearch(selectors.claimAction.secondLineDestination, 'Bueno');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should delete the first line', async() => {
await page.waitToClick(selectors.claimAction.firstDeleteLine);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should refresh the view to check the remaining line is the expected one', async() => {
@ -62,14 +62,14 @@ describe('Claim action path', () => {
await page.waitToClick(selectors.claimAction.firstDeleteLine);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should check the "is paid with mana" checkbox', async() => {
await page.waitToClick(selectors.claimAction.isPaidWithManaCheckbox);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the "is paid with mana" is checked', async() => {

View File

@ -81,7 +81,7 @@ describe('Order edit basic data path', () => {
await page.waitToClick(selectors.orderBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should now confirm the client have been edited', async() => {

View File

@ -28,7 +28,7 @@ describe('Order lines', () => {
await page.waitToClick(selectors.orderLine.confirmButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the order subtotal has changed', async() => {

View File

@ -33,7 +33,7 @@ describe('Route basic Data path', () => {
await page.waitToClick(selectors.routeBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the worker was edited', async() => {

View File

@ -51,7 +51,7 @@ describe('Route create path', () => {
await page.waitToClick(selectors.createRouteView.submitButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should confirm the redirection to the created route summary`, async() => {

View File

@ -23,7 +23,7 @@ xdescribe('Route basic Data path', () => {
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should confirm the buscamanButton is disabled', async() => {

View File

@ -51,7 +51,7 @@ describe('Travel thermograph path', () => {
const message = await page.waitForSnackbar();
const state = await page.getState();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
expect(state).toBe('travel.card.thermograph.index');
});

View File

@ -52,7 +52,7 @@ describe('Travel basic data path', () => {
await page.waitToClick(selectors.travelBasicDada.save);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should reload the section and check the reference was saved', async() => {

View File

@ -40,7 +40,7 @@ describe('Zone basic data path', () => {
await page.waitToClick(selectors.zoneBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it('should now reload the section', async() => {

5148
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -154,7 +154,8 @@ module.exports = function(Self) {
const showFieldNames = [
'name',
'description',
'code'
'code',
'nickname'
];
for (field of showFieldNames) {
const propField = properties && properties[field];

View File

@ -41,7 +41,7 @@ module.exports = Self => {
attributes: ['dn'],
filter: 'objectClass=posixGroup'
};
res = await client.search(ldapConfig.groupDn, opts);
let res = await client.search(ldapConfig.groupDn, opts);
let reqs = [];
await new Promise((resolve, reject) => {
@ -62,31 +62,28 @@ module.exports = Self => {
let roles = await $.Role.find({
fields: ['id', 'name']
});
let roleRoles = await $.RoleRole.find({
fields: ['role', 'inheritsFrom']
});
let roleMap = toMap(roleRoles, e => {
return {key: e.inheritsFrom, val: e.role};
});
let accounts = await $.UserAccount.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
include: {
relation: 'roles',
scope: {
fields: ['inheritsFrom']
}
}
fields: ['name', 'roleFk'],
where: {active: true}
}
}
});
let map = new Map();
for (let account of accounts) {
let user = account.user();
for (let inherit of user.roles()) {
let roleId = inherit.inheritsFrom;
if (!map.has(roleId)) map.set(roleId, []);
map.get(roleId).push(user.name);
}
}
let accountMap = toMap(accounts, e => {
let user = e.user();
if (!user) return;
return {key: user.roleFk, val: user.name};
});
reqs = [];
for (let role of roles) {
@ -96,8 +93,14 @@ module.exports = Self => {
gidNumber: accountConfig.idBase + role.id
};
let memberUid = map.get(role.id);
if (memberUid) newEntry.memberUid = memberUid;
let memberUid = [];
for (subrole of roleMap.get(role.id) || [])
memberUid = memberUid.concat(accountMap.get(subrole) || []);
if (memberUid.length) {
memberUid.sort((a, b) => a.localeCompare(b));
newEntry.memberUid = memberUid;
}
let dn = `cn=${role.name},${ldapConfig.groupDn}`;
reqs.push(client.add(dn, newEntry));
@ -107,8 +110,19 @@ module.exports = Self => {
err = e;
}
// FIXME: Cannot disconnect, hangs on undind() call
// await client.unbind();
await client.unbind();
if (err) throw err;
};
};
function toMap(array, fn) {
let map = new Map();
for (let item of array) {
let keyVal = fn(item);
if (!keyVal) continue;
let key = keyVal.key;
if (!map.has(key)) map.set(key, []);
map.get(key).push(keyVal.val);
}
return map;
}

View File

@ -0,0 +1,37 @@
const SyncEngine = require('../../util/sync-engine');
module.exports = Self => {
Self.remoteMethod('syncAll', {
description: 'Synchronizes user database with LDAP and Samba',
http: {
path: `/syncAll`,
verb: 'PATCH'
}
});
Self.syncAll = async function() {
let $ = Self.app.models;
let se = new SyncEngine();
await se.init($);
let usersToSync = await se.getUsers();
usersToSync = Array.from(usersToSync.values())
.sort((a, b) => a.localeCompare(b));
for (let user of usersToSync) {
try {
console.log(`Synchronizing user '${user}'`);
await se.sync(user);
console.log(` -> '${user}' sinchronized`);
} catch (err) {
console.error(` -> '${user}' synchronization error:`, err.message);
}
}
await se.deinit();
await $.RoleInherit.sync();
};
};

View File

@ -1,7 +1,5 @@
const ldap = require('../../util/ldapjs-extra');
const nthash = require('smbhash').nthash;
const ssh = require('node-ssh');
const crypto = require('crypto');
const SyncEngine = require('../../util/sync-engine');
module.exports = Self => {
Self.remoteMethod('sync', {
@ -19,7 +17,7 @@ module.exports = Self => {
}
],
http: {
path: `/sync`,
path: `/:userName/sync`,
verb: 'PATCH'
}
});
@ -35,233 +33,19 @@ module.exports = Self => {
if (user && isSync) return;
let accountConfig;
let mailConfig;
let extraParams;
let hasAccount = false;
if (user) {
accountConfig = await $.AccountConfig.findOne({
fields: ['homedir', 'shell', 'idBase']
});
mailConfig = await $.MailConfig.findOne({
fields: ['domain']
});
user = await $.Account.findById(user.id, {
fields: [
'id',
'nickname',
'email',
'lang',
'roleFk',
'sync',
'active',
'created',
'updated'
],
where: {name: userName},
include: {
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
}
}
}
}
});
extraParams = {
corporateMail: `${userName}@${mailConfig.domain}`,
uidNumber: accountConfig.idBase + user.id
};
hasAccount = user.active
&& await $.UserAccount.exists(user.id);
}
if (user) {
let bcryptPassword = $.User.hashPassword(password);
await $.Account.upsertWithWhere({id: user.id},
{bcryptPassword}
);
await $.user.upsert({
id: user.id,
username: userName,
password: bcryptPassword,
email: user.email,
created: user.created,
updated: user.updated
});
}
// SIP
if (hasAccount) {
await Self.rawSql('CALL pbx.sip_setPassword(?, ?)',
[user.id, password]
);
}
// LDAP
let ldapConfig = await $.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn']
});
if (ldapConfig) {
let ldapClient = ldap.createClient({
url: `ldap://${ldapConfig.host}:389`
});
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await ldapClient.bind(ldapConfig.rdn, ldapPassword);
let err;
try {
// Deletes user
let se = new SyncEngine();
await se.init($);
try {
let dn = `uid=${userName},${ldapConfig.baseDn}`;
await ldapClient.del(dn);
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;
}
// Removes user from groups
let opts = {
scope: 'sub',
attributes: ['dn'],
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
};
res = await ldapClient.search(ldapConfig.groupDn, opts);
let oldGroups = [];
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldGroups.push(e.object));
res.on('end', resolve);
});
let reqs = [];
for (oldGroup of oldGroups) {
let change = new ldap.Change({
operation: 'delete',
modification: {memberUid: userName}
});
reqs.push(ldapClient.modify(oldGroup.dn, change));
}
await Promise.all(reqs);
if (hasAccount) {
// Recreates user
let nameArgs = user.nickname.split(' ');
let sshaPassword = crypto
.createHash('sha1')
.update(password)
.digest('base64');
let dn = `uid=${userName},${ldapConfig.baseDn}`;
let newEntry = {
uid: userName,
objectClass: [
'inetOrgPerson',
'posixAccount',
'sambaSamAccount'
],
cn: user.nickname || userName,
displayName: user.nickname,
givenName: nameArgs[0],
sn: nameArgs[1] || 'Empty',
mail: extraParams.corporateMail,
userPassword: `{SSHA}${sshaPassword}`,
preferredLanguage: user.lang,
homeDirectory: `${accountConfig.homedir}/${userName}`,
loginShell: accountConfig.shell,
uidNumber: extraParams.uidNumber,
gidNumber: accountConfig.idBase + user.roleFk,
sambaSID: '-',
sambaNTPassword: nthash(password)
};
await ldapClient.add(dn, newEntry);
// Adds user to groups
let reqs = [];
for (let role of user.roles()) {
let change = new ldap.Change({
operation: 'add',
modification: {memberUid: userName}
});
let roleName = role.inherits().name;
let dn = `cn=${roleName},${ldapConfig.groupDn}`;
reqs.push(ldapClient.modify(dn, change));
}
await Promise.all(reqs);
}
await se.sync(userName, password, true);
await $.UserSync.destroyById(userName);
} catch (e) {
err = e;
}
// FIXME: Cannot disconnect, hangs on undind() call
// await ldapClient.unbind();
await se.deinit();
if (err) throw err;
}
// Samba
let sambaConfig = await $.SambaConfig.findOne({
fields: ['host', 'sshUser', 'sshPass']
});
if (sambaConfig) {
let sshPassword = Buffer
.from(sambaConfig.sshPass, 'base64')
.toString('ascii');
let sshClient = new ssh.NodeSSH();
await sshClient.connect({
host: sambaConfig.host,
username: sambaConfig.sshUser,
password: sshPassword
});
let commands;
if (hasAccount) {
commands = [
`samba-tool user create "${userName}" `
+ `--uid-number=${extraParams.uidNumber} `
+ `--mail-address="${extraParams.corporateMail}" `
+ `--random-password`,
`samba-tool user setexpiry "${userName}" `
+ `--noexpiry`,
`samba-tool user setpassword "${userName}" `
+ `--newpassword="${password}"`,
`mkhomedir_helper "${userName}" 0027`
];
} else {
commands = [
`samba-tool user delete "${userName}"`
];
}
for (let command of commands)
await sshClient.execCommand(command);
await sshClient.dispose();
}
// Mark as synchronized
await $.UserSync.destroyById(userName);
};
};

View File

@ -5,6 +5,9 @@
"LdapConfig": {
"dataSource": "vn"
},
"Mail": {
"dataSource": "vn"
},
"MailAlias": {
"dataSource": "vn"
},
@ -34,8 +37,5 @@
},
"UserSync": {
"dataSource": "vn"
},
"Mail": {
"dataSource": "vn"
}
}

View File

@ -7,7 +7,7 @@
}
},
"properties": {
"role": {
"id": {
"id": true
}
},

View File

@ -2,4 +2,5 @@
module.exports = Self => {
require('../methods/user-account/sync')(Self);
require('../methods/user-account/sync-by-id')(Self);
require('../methods/user-account/sync-all')(Self);
};

View File

@ -0,0 +1,47 @@
/**
* Base class for user synchronizators.
*
* @property {Array<Model>} $
* @property {Object} accountConfig
* @property {Object} mailConfig
*/
class SyncConnector {
/**
* Initalizes the connector.
*/
async init() {
return true;
}
/**
* Get users to synchronize.
*
* @param {Set} usersToSync Set where users are added
*/
async getUsers(usersToSync) {}
/**
* Synchronizes a user.
*
* @param {Object} info User information
* @param {String} userName The user name
* @param {String} password Thepassword
*/
async sync(info, userName, password) {}
/**
* Synchronizes user groups.
*
* @param {User} user Instace of user
* @param {String} userName The user name
*/
async syncGroups(user, userName) {}
/**
* Deinitalizes the connector.
*/
async deinit() {}
}
SyncConnector.connectors = [];
module.exports = SyncConnector;

View File

@ -0,0 +1,57 @@
const SyncConnector = require('./sync-connector');
class SyncDb extends SyncConnector {
async sync(info, userName, password) {
let {$} = this;
let {user} = info;
if (user && user.active) {
let bcryptPassword = password
? $.User.hashPassword(password)
: user.bcryptPassword;
await $.Account.upsertWithWhere({id: user.id},
{bcryptPassword}
);
let dbUser = {
id: user.id,
username: userName,
email: user.email,
created: user.created,
updated: user.updated
};
if (bcryptPassword)
dbUser.password = bcryptPassword;
if (await $.user.exists(user.id))
await $.user.replaceById(user.id, dbUser);
else
await $.user.create(dbUser);
} else
await $.user.destroyAll({username: userName});
}
async getUsers(usersToSync) {
let accounts = await this.$.UserAccount.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
where: {active: true}
}
}
});
for (let account of accounts) {
let user = account.user();
if (!user) continue;
usersToSync.add(user.name);
}
}
}
SyncConnector.connectors.push(SyncDb);
module.exports = SyncDb;

View File

@ -0,0 +1,127 @@
const SyncConnector = require('./sync-connector');
require('./sync-db');
require('./sync-sip');
require('./sync-ldap');
require('./sync-samba');
module.exports = class SyncEngine {
async init($) {
let accountConfig = await $.AccountConfig.findOne({
fields: ['homedir', 'shell', 'idBase']
});
let mailConfig = await $.MailConfig.findOne({
fields: ['domain']
});
let connectors = [];
for (let ConnectorClass of SyncConnector.connectors) {
let connector = new ConnectorClass();
Object.assign(connector, {
se: this,
$
});
if (!await connector.init()) continue;
connectors.push(connector);
}
Object.assign(this, {
connectors,
$,
accountConfig,
mailConfig
});
}
async deinit() {
for (let connector of this.connectors)
await connector.deinit();
}
async sync(userName, password, syncGroups) {
let {
$,
accountConfig,
mailConfig
} = this;
if (!userName) return;
userName = userName.toLowerCase();
// Skip conflicting users
if (['administrator', 'root'].indexOf(userName) >= 0)
return;
let user = await $.Account.findOne({
where: {name: userName},
fields: [
'id',
'nickname',
'email',
'lang',
'roleFk',
'sync',
'active',
'created',
'bcryptPassword',
'updated'
],
include: {
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
}
}
}
}
});
let extraParams;
let hasAccount = false;
if (user) {
hasAccount = user.active
&& await $.UserAccount.exists(user.id);
extraParams = {
corporateMail: `${userName}@${mailConfig.domain}`,
uidNumber: accountConfig.idBase + user.id
};
}
let info = {
user,
extraParams,
hasAccount,
accountConfig,
mailConfig
};
let errs = [];
for (let connector of this.connectors) {
try {
await connector.sync(info, userName, password);
if (syncGroups)
await connector.syncGroups(info, userName);
} catch (err) {
errs.push(err);
}
}
if (errs.length) throw errs[0];
}
async getUsers() {
let usersToSync = new Set();
for (let connector of this.connectors)
await connector.getUsers(usersToSync);
return usersToSync;
}
};

View File

@ -0,0 +1,204 @@
const SyncConnector = require('./sync-connector');
const nthash = require('smbhash').nthash;
const ldap = require('./ldapjs-extra');
const crypto = require('crypto');
class SyncLdap extends SyncConnector {
async init() {
let ldapConfig = await this.$.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn']
});
if (!ldapConfig) return false;
let client = ldap.createClient({
url: `ldap://${ldapConfig.host}:389`
});
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await client.bind(ldapConfig.rdn, ldapPassword);
Object.assign(this, {
ldapConfig,
client
});
return true;
}
async deinit() {
if (this.client)
await this.client.unbind();
}
async sync(info, userName, password) {
let {
ldapConfig,
client,
} = this;
let {
user,
hasAccount,
extraParams,
accountConfig
} = info;
let res = await client.search(ldapConfig.baseDn, {
scope: 'sub',
attributes: ['userPassword', 'sambaNTPassword'],
filter: `&(uid=${userName})`
});
let oldUser;
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldUser = e.object);
res.on('end', resolve);
});
try {
let dn = `uid=${userName},${ldapConfig.baseDn}`;
await client.del(dn);
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;
}
if (!hasAccount) {
if (oldUser)
console.log(` -> '${userName}' removed from LDAP`);
return;
}
let nickname = user.nickname || userName;
let nameArgs = nickname.trim().split(' ');
let sn = nameArgs.length > 1
? nameArgs.splice(1).join(' ')
: '-';
let dn = `uid=${userName},${ldapConfig.baseDn}`;
let newEntry = {
uid: userName,
objectClass: [
'inetOrgPerson',
'posixAccount',
'sambaSamAccount'
],
cn: nickname,
displayName: nickname,
givenName: nameArgs[0],
sn,
mail: extraParams.corporateMail,
preferredLanguage: user.lang,
homeDirectory: `${accountConfig.homedir}/${userName}`,
loginShell: accountConfig.shell,
uidNumber: extraParams.uidNumber,
gidNumber: accountConfig.idBase + user.roleFk,
sambaSID: '-'
};
if (password) {
let salt = crypto
.randomBytes(8)
.toString('base64');
let hash = crypto.createHash('sha1');
hash.update(password);
hash.update(salt, 'binary');
let digest = hash.digest('binary');
let ssha = Buffer
.from(digest + salt, 'binary')
.toString('base64');
Object.assign(newEntry, {
userPassword: `{SSHA}${ssha}`,
sambaNTPassword: nthash(password)
});
} else if (oldUser) {
Object.assign(newEntry, {
userPassword: oldUser.userPassword,
sambaNTPassword: oldUser.sambaNTPassword
});
}
for (let prop in newEntry) {
if (newEntry[prop] == null)
delete newEntry[prop];
}
await client.add(dn, newEntry);
}
async syncGroups(info, userName) {
let {
ldapConfig,
client
} = this;
let {
user,
hasAccount
} = info;
let res = await client.search(ldapConfig.groupDn, {
scope: 'sub',
attributes: ['dn'],
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
});
let oldGroups = [];
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldGroups.push(e.object));
res.on('end', resolve);
});
let reqs = [];
for (let oldGroup of oldGroups) {
let change = new ldap.Change({
operation: 'delete',
modification: {memberUid: userName}
});
reqs.push(client.modify(oldGroup.dn, change));
}
await Promise.all(reqs);
if (!hasAccount) return;
reqs = [];
for (let role of user.roles()) {
let change = new ldap.Change({
operation: 'add',
modification: {memberUid: userName}
});
let roleName = role.inherits().name;
let dn = `cn=${roleName},${ldapConfig.groupDn}`;
reqs.push(client.modify(dn, change));
}
await Promise.all(reqs);
}
async getUsers(usersToSync) {
let {
ldapConfig,
client
} = this;
let res = await client.search(ldapConfig.baseDn, {
scope: 'sub',
attributes: ['uid'],
filter: `uid=*`
});
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => usersToSync.add(e.object.uid));
res.on('end', resolve);
});
}
}
SyncConnector.connectors.push(SyncLdap);
module.exports = SyncLdap;

View File

@ -0,0 +1,93 @@
const SyncConnector = require('./sync-connector');
const ssh = require('node-ssh');
class SyncSamba extends SyncConnector {
async init() {
let sambaConfig = await this.$.SambaConfig.findOne({
fields: ['host', 'sshUser', 'sshPass']
});
if (!sambaConfig) return false;
let sshPassword = Buffer
.from(sambaConfig.sshPass, 'base64')
.toString('ascii');
let client = new ssh.NodeSSH();
await client.connect({
host: sambaConfig.host,
username: sambaConfig.sshUser,
password: sshPassword
});
Object.assign(this, {
sambaConfig,
client
});
return true;
}
async deinit() {
if (this.client)
await this.client.dispose();
}
async sync(info, userName, password) {
let {
client
} = this;
let {
hasAccount,
extraParams
} = info;
if (hasAccount) {
try {
await client.exec('samba-tool user create', [
userName,
'--uid-number', `${extraParams.uidNumber}`,
'--mail-address', extraParams.corporateMail,
'--random-password'
]);
} catch (e) {}
await client.exec('samba-tool user setexpiry', [
userName,
'--noexpiry'
]);
if (password) {
await client.exec('samba-tool user setpassword', [
userName,
'--newpassword', password
]);
await client.exec('samba-tool user enable', [
userName
]);
}
await client.exec('mkhomedir_helper', [
userName,
'0027'
]);
} else {
try {
await client.exec('samba-tool user disable', [
userName
]);
console.log(` -> '${userName}' disabled on Samba`);
} catch (e) {}
}
}
async getUsers(usersToSync) {
let {client} = this;
let res = await client.execCommand('samba-tool user list');
let users = res.stdout.split('\n');
for (let user of users) usersToSync.add(user.trim());
}
}
SyncConnector.connectors.push(SyncSamba);
module.exports = SyncSamba;

View File

@ -0,0 +1,15 @@
const SyncConnector = require('./sync-connector');
class SyncSip extends SyncConnector {
async sync(info, userName, password) {
if (!info.hasAccount || !password) return;
await this.$.Account.rawSql('CALL pbx.sip_setPassword(?, ?)',
[info.user.id, password]
);
}
}
SyncConnector.connectors.push(SyncSip);
module.exports = SyncSip;

View File

@ -15,3 +15,6 @@ import './basic-data';
import './mail-forwarding';
import './aliases';
import './roles';
import './ldap';
import './samba';
import './posix';

View File

@ -0,0 +1,93 @@
<vn-watcher
vn-id="watcher"
url="LdapConfigs"
data="$ctrl.config"
id-value="1"
form="form">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-textfield
label="Host"
ng-model="$ctrl.config.host"
rule="LdapConfig"
vn-focus>
</vn-textfield>
<vn-textfield
label="RDN"
ng-model="$ctrl.config.rdn"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.config.password"
info="Password should be base64 encoded"
type="password"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="Base DN"
ng-model="$ctrl.config.baseDn"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="Filter"
ng-model="$ctrl.config.filter"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="Group DN"
ng-model="$ctrl.config.groupDn"
rule="LdapConfig">
</vn-textfield>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-button
label="Undo changes"
ng-if="watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
<vn-button
label="Synchronize now"
ng-if="watcher.hasData"
ng-click="$ctrl.onSynchronizeAll()">
</vn-button>
<vn-button
label="Synchronize user"
ng-if="watcher.hasData"
ng-click="syncUser.show()">
</vn-button>
</vn-button-bar>
<vn-submit
icon="save"
vn-tooltip="Save"
class="round"
fixed-bottom-right>
</vn-submit>
</form>
<vn-dialog
vn-id="syncUser"
on-accept="$ctrl.onUserSync()"
on-close="$ctrl.onPassClose()">
<tpl-body>
<vn-textfield
label="Username"
ng-model="$ctrl.syncUser">
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.syncPassword"
type="password"
info="If password is not specified, just user attributes are synchronized">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Synchronize</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,30 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
export default class Controller extends Section {
onSynchronizeAll() {
this.vnApp.showSuccess(this.$t('Synchronizing in the background'));
this.$http.patch(`UserAccounts/syncAll`)
.then(() => this.vnApp.showSuccess(this.$t('LDAP users synchronized')));
}
onUserSync() {
if (!this.syncUser)
throw new UserError('Please enter the username');
let params = {password: this.syncPassword};
return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params)
.then(() => this.vnApp.showSuccess(this.$t('User synchronized')));
}
onSyncClose() {
this.syncUser = '';
this.syncPassword = '';
}
}
ngModule.component('vnAccountLdap', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,16 @@
Host: Host
RDN: RDN
Base DN: DN base
Password should be base64 encoded: La contraseña debe estar codificada en base64
Filter: Filtro
Group DN: DN grupos
Synchronize now: Sincronizar ahora
Synchronize user: Sincronizar usuario
If password is not specified, just user attributes are synchronized: >-
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
Synchronizing in the background: Sincronizando en segundo plano
LDAP users synchronized: Usuarios LDAP sincronizados
Username: Nombre de usuario
Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario
User synchronized: Usuario sincronizado

View File

@ -0,0 +1,69 @@
<vn-watcher
vn-id="watcher"
url="AccountConfigs"
data="$ctrl.config"
id-value="1"
form="form">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-textfield
label="Homedir"
ng-model="$ctrl.config.homedir"
rule="AccountConfig"
vn-focus>
</vn-textfield>
<vn-textfield
label="Shell"
ng-model="$ctrl.config.shell"
rule="AccountConfig">
</vn-textfield>
<vn-input-number
label="Id base"
ng-model="$ctrl.config.idBase"
rule="AccountConfig">
</vn-input-number>
<vn-horizontal>
<vn-input-number
label="Min"
ng-model="$ctrl.config.min"
rule="AccountConfig">
</vn-input-number>
<vn-input-number
label="Max"
ng-model="$ctrl.config.max"
rule="AccountConfig">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number
label="Warn"
ng-model="$ctrl.config.warn"
rule="AccountConfig">
</vn-input-number>
<vn-input-number
label="Inact"
ng-model="$ctrl.config.inact"
rule="AccountConfig">
</vn-input-number>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-button
label="Undo changes"
ng-if="watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
<vn-submit
icon="save"
vn-tooltip="Save"
class="round"
fixed-bottom-right>
</vn-submit>
</form>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {}
ngModule.component('vnAccountPosix', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,16 @@
Host: Host
RDN: RDN
Base DN: DN base
Password should be base64 encoded: La contraseña debe estar codificada en base64
Filter: Filtro
Group DN: DN grupos
Synchronize now: Sincronizar ahora
Synchronize user: Sincronizar usuario
If password is not specified, just user attributes are synchronized: >-
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
Synchronizing in the background: Sincronizando en segundo plano
LDAP users synchronized: Usuarios LDAP sincronizados
Username: Nombre de usuario
Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario
User synchronized: Usuario sincronizado

View File

@ -9,6 +9,9 @@
{"state": "account.index", "icon": "face"},
{"state": "account.role", "icon": "group"},
{"state": "account.alias", "icon": "email"},
{"state": "account.posix", "icon": "accessibility"},
{"state": "account.ldap", "icon": "account_tree"},
{"state": "account.samba", "icon": "desktop_windows"},
{"state": "account.acl", "icon": "check"},
{"state": "account.connections", "icon": "share"}
],
@ -174,6 +177,24 @@
"state": "account.alias.card.users",
"component": "vn-alias-users",
"description": "Users"
}, {
"url": "/posix",
"state": "account.posix",
"component": "vn-account-posix",
"description": "Posix",
"acl": ["developer"]
}, {
"url": "/ldap",
"state": "account.ldap",
"component": "vn-account-ldap",
"description": "LDAP",
"acl": ["developer"]
}, {
"url": "/samba",
"state": "account.samba",
"component": "vn-account-samba",
"description": "Samba",
"acl": ["developer"]
}, {
"url": "/acl?q",
"state": "account.acl",

View File

@ -0,0 +1,47 @@
<vn-watcher
vn-id="watcher"
url="SambaConfigs"
data="$ctrl.config"
id-value="1"
form="form">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-textfield
label="SSH host"
ng-model="$ctrl.config.host"
rule="SambaConfig"
vn-focus>
</vn-textfield>
<vn-textfield
label="User"
ng-model="$ctrl.config.sshUser"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.config.sshPass"
info="Password should be base64 encoded"
type="password"
rule="SambaConfig">
</vn-textfield>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-button
label="Undo changes"
ng-if="watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
<vn-submit
icon="save"
vn-tooltip="Save"
class="round"
fixed-bottom-right>
</vn-submit>
</form>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {}
ngModule.component('vnAccountSamba', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,2 @@
SSH host: Host SSH
Password should be base64 encoded: La contraseña debe estar codificada en base64

View File

@ -4,7 +4,7 @@ module.exports = function(Self) {
Self.remoteMethodCtx('createAddress', {
description: 'Creates client address updating default address',
accepts: [{
arg: 'id',
arg: 'clientFk',
type: 'number',
description: 'The client id',
http: {source: 'path'}
@ -37,19 +37,19 @@ module.exports = function(Self) {
type: 'string'
},
{
arg: 'provinceId',
arg: 'provinceFk',
type: 'number'
},
{
arg: 'agencyModeId',
arg: 'agencyModeFk',
type: 'number'
},
{
arg: 'incotermsId',
arg: 'incotermsFk',
type: 'string'
},
{
arg: 'customsAgentId',
arg: 'customsAgentFk',
type: 'number'
},
{
@ -66,44 +66,33 @@ module.exports = function(Self) {
},
http: {
verb: 'post',
path: '/:id/createAddress'
path: '/:clientFk/createAddress'
}
});
Self.createAddress = async(ctx, clientId) => {
Self.createAddress = async(ctx, clientFk) => {
const models = Self.app.models;
const args = ctx.args;
const tx = await models.Address.beginTransaction({});
try {
const options = {transaction: tx};
const province = await models.Province.findById(args.provinceId, {
const province = await models.Province.findById(args.provinceFk, {
include: {
relation: 'country'
}
}, options);
const isUeeMember = province.country().isUeeMember;
if (!isUeeMember && !args.incotermsId)
if (!isUeeMember && !args.incotermsFk)
throw new UserError(`Incoterms is required for a non UEE member`);
if (!isUeeMember && !args.customsAgentId)
if (!isUeeMember && !args.customsAgentFk)
throw new UserError(`Customs agent is required for a non UEE member`);
const newAddress = await models.Address.create({
clientFk: clientId,
nickname: args.nickname,
incotermsFk: args.incotermsId,
customsAgentFk: args.customsAgentId,
city: args.city,
street: args.street,
phone: args.phone,
postalCode: args.postalCode,
provinceFk: args.provinceId,
agencyModeFk: args.agencyModeId,
isActive: args.isActive
}, options);
const client = await Self.findById(clientId, null, options);
delete args.ctx; // Remove unwanted properties
const newAddress = await models.Address.create(args, options);
const client = await Self.findById(clientFk, null, options);
if (args.isDefaultAddress) {
await client.updateAttributes({

View File

@ -1,25 +1,25 @@
const app = require('vn-loopback/server/server');
describe('Address createAddress', () => {
const clientId = 101;
const provinceId = 5;
const incotermsId = 'FAS';
const clientFk = 101;
const provinceFk = 5;
const incotermsFk = 'FAS';
const customAgentOneId = 1;
it('should throw a non uee member error if no incoterms is defined', async() => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceId: provinceId,
provinceFk: provinceFk,
nickname: expectedResult,
street: 'Wall Street',
city: 'New York',
customsAgentId: customAgentOneId
customsAgentFk: customAgentOneId
}
};
try {
await app.models.Client.createAddress(ctx, clientId);
await app.models.Client.createAddress(ctx, clientFk);
} catch (e) {
err = e;
}
@ -32,16 +32,16 @@ describe('Address createAddress', () => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceId: provinceId,
provinceFk: provinceFk,
nickname: expectedResult,
street: 'Wall Street',
city: 'New York',
incotermsId: incotermsId
incotermsFk: incotermsFk
}
};
try {
await app.models.Client.createAddress(ctx, clientId);
await app.models.Client.createAddress(ctx, clientFk);
} catch (e) {
err = e;
}
@ -51,7 +51,7 @@ describe('Address createAddress', () => {
});
it('should verify that client defaultAddressFk is untainted', async() => {
const client = await app.models.Client.findById(clientId);
const client = await app.models.Client.findById(clientFk);
expect(client.defaultAddressFk).toEqual(1);
});
@ -59,18 +59,23 @@ describe('Address createAddress', () => {
it('should create a new address and set as a client default address', async() => {
const ctx = {
args: {
provinceId: 1,
clientFk: 101,
provinceFk: 1,
nickname: 'My address',
street: 'Wall Street',
city: 'New York',
incotermsId: incotermsId,
customsAgentId: customAgentOneId,
phone: 678678678,
mobile: 678678678,
postalCode: 46680,
agencyModeFk: 1,
incotermsFk: incotermsFk,
customsAgentFk: customAgentOneId,
isDefaultAddress: true
}
};
const address = await app.models.Client.createAddress(ctx, clientId);
const client = await app.models.Client.findById(clientId);
const address = await app.models.Client.createAddress(ctx, clientFk);
const client = await app.models.Client.findById(clientFk);
expect(client.defaultAddressFk).toEqual(address.id);
@ -78,4 +83,31 @@ describe('Address createAddress', () => {
await client.updateAttributes({defaultAddressFk: 1});
await address.destroy();
});
it('should create a new address and set all properties', async() => {
const ctx = {
args: {
clientFk: 101,
provinceFk: 1,
nickname: 'My address',
street: 'Wall Street',
city: 'New York',
phone: '678678678',
mobile: '678678678',
postalCode: '46680',
agencyModeFk: 1,
incotermsFk: incotermsFk,
customsAgentFk: customAgentOneId,
isDefaultAddress: true
}
};
address = await app.models.Client.createAddress(ctx, clientFk);
expect(address).toEqual(jasmine.objectContaining(ctx.args));
// restores
const client = await app.models.Client.findById(clientFk);
await client.updateAttributes({defaultAddressFk: 1});
await address.destroy();
});
});

View File

@ -15,6 +15,10 @@
"description": {
"type": "String",
"required": true
},
"code": {
"type": "String",
"required": true
}
},
"acls": [

View File

@ -88,7 +88,7 @@
</vn-datalist>
<vn-autocomplete vn-id="province" vn-one
label="Province"
ng-model="$ctrl.address.provinceId"
ng-model="$ctrl.address.provinceFk"
data="provincesLocation"
fields="['id', 'name', 'countryFk']"
show-field="name"
@ -100,7 +100,7 @@
<vn-horizontal>
<vn-autocomplete
vn-one
ng-model="$ctrl.address.agencyModeId"
ng-model="$ctrl.address.agencyModeFk"
url="AgencyModes/isActive"
show-field="name"
value-field="id"
@ -121,14 +121,14 @@
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.address.incotermsId"
ng-model="$ctrl.address.incotermsFk"
data="incoterms"
show-field="name"
value-field="code"
label="Incoterms">
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.address.customsAgentId"
ng-model="$ctrl.address.customsAgentFk"
data="customsAgents"
show-field="fiscalName"
value-field="id"

View File

@ -29,7 +29,7 @@ export default class Controller extends Section {
onCustomAgentAccept() {
return this.$http.post(`CustomsAgents`, this.newCustomsAgent)
.then(res => this.address.customsAgentId = res.data.id);
.then(res => this.address.customsAgentFk = res.data.id);
}
get town() {
@ -45,8 +45,8 @@ export default class Controller extends Section {
const province = selection.province;
const postcodes = selection.postcodes;
if (!this.address.provinceI)
this.address.provinceId = province.id;
if (!this.address.provinceFk)
this.address.provinceFk = province.id;
if (postcodes.length === 1)
this.address.postalCode = postcodes[0].code;
@ -68,8 +68,8 @@ export default class Controller extends Section {
if (!this.address.city)
this.address.city = town.name;
if (!this.address.provinceId)
this.address.provinceId = province.id;
if (!this.address.provinceFk)
this.address.provinceFk = province.id;
}
onResponse(response) {

View File

@ -54,7 +54,7 @@ describe('Client', () => {
});
describe('town() setter', () => {
it(`should set provinceId property`, () => {
it(`should set provinceFk property`, () => {
controller.town = {
provinceFk: 1,
code: 46001,
@ -69,10 +69,10 @@ describe('Client', () => {
postcodes: []
};
expect(controller.address.provinceId).toEqual(1);
expect(controller.address.provinceFk).toEqual(1);
});
it(`should set provinceId property and fill the postalCode if there's just one`, () => {
it(`should set provinceFk property and fill the postalCode if there's just one`, () => {
controller.town = {
provinceFk: 1,
code: 46001,
@ -87,7 +87,7 @@ describe('Client', () => {
postcodes: [{code: '46001'}]
};
expect(controller.address.provinceId).toEqual(1);
expect(controller.address.provinceFk).toEqual(1);
expect(controller.address.postalCode).toEqual('46001');
});
});
@ -112,7 +112,7 @@ describe('Client', () => {
};
expect(controller.address.city).toEqual('New York');
expect(controller.address.provinceId).toEqual(1);
expect(controller.address.provinceFk).toEqual(1);
});
});
@ -123,7 +123,7 @@ describe('Client', () => {
controller.onCustomAgentAccept();
$httpBackend.flush();
expect(controller.address.customsAgentId).toEqual(1);
expect(controller.address.customsAgentFk).toEqual(1);
});
});
});

View File

@ -190,8 +190,6 @@ module.exports = Self => {
item.prices.push(price);
else
item.prices = [price];
item.available = price.grouping;
}
});
});

View File

@ -7,7 +7,7 @@ describe('Supplier getSummary()', () => {
expect(supplier.id).toEqual(1);
expect(supplier.name).toEqual('Plants SL');
expect(supplier.nif).toEqual('06089160W');
expect(supplier.account).toEqual(4000000001);
expect(supplier.account).toEqual(4100000001);
expect(supplier.payDay).toEqual(15);
});

View File

@ -83,7 +83,7 @@ module.exports = Self => {
});
await models.Chat.sendCheckingPresence(ctx, requesterId, message);
// loguejar
// log
let logRecord = {
originFk: sale.ticketFk,
userFk: userId,

View File

@ -1,7 +1,6 @@
const app = require('vn-loopback/server/server');
// #2512 confirm.spec pollutes other tests
xdescribe('ticket-request confirm()', () => {
describe('ticket-request confirm()', () => {
let ctx = {
req: {
accessToken: {userId: 9},
@ -83,37 +82,4 @@ xdescribe('ticket-request confirm()', () => {
// restores
await request.updateAttributes({saleFk: null});
});
it(`should create a new sale for the the request if there's no sale id`, async() => {
const requestId = 4;
const itemId = 1;
const quantity = 10;
const originalRequest = await app.models.TicketRequest.findById(requestId);
ctx.args = {
itemFk: itemId,
id: requestId,
quantity: quantity
};
const request = await app.models.TicketRequest.findById(requestId);
await request.updateAttributes({saleFk: null});
await app.models.TicketRequest.confirm(ctx);
const updatedRequest = await app.models.TicketRequest.findById(requestId);
const createdSaleId = updatedRequest.saleFk;
expect(updatedRequest.saleFk).toEqual(createdSaleId);
expect(updatedRequest.isOk).toEqual(true);
expect(updatedRequest.itemFk).toEqual(itemId);
// restores
await originalRequest.updateAttributes(originalRequest);
await app.models.Sale.destroyById(createdSaleId);
await app.models.Item.rawSql(`
TRUNCATE TABLE cache.last_buy
`);
});
});

View File

@ -1,82 +1,84 @@
const app = require('vn-loopback/server/server');
describe('ticket-request filter()', () => {
it('should now return all ticket requests', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {}};
const userId = 9;
let ctx = {req: {accessToken: {userId: userId}}};
let result = await app.models.TicketRequest.filter(ctx);
it('should now return all ticket requests', async() => {
ctx.args = {};
const result = await app.models.TicketRequest.filter(ctx);
expect(result.length).toEqual(3);
});
it('should return the ticket request matching a generic search value which is the ticket ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {search: 11}};
ctx.args = {search: 11};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching a generic search value which is the client address alias', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {search: 'NY roofs'}};
ctx.args = {search: 'NY roofs'};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching the ticket ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {ticketFk: 11}};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
ctx.args = {ticketFk: 11};
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching the atender ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {attenderFk: 35}};
ctx.args = {attenderFk: 35};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(3);
});
it('should return the ticket request matching the isOk triple-state', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {isOk: null}};
ctx.args = {isOk: null};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(3);
});
it('should return the ticket request matching the client ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {clientFk: 102}};
ctx.args = {clientFk: 102};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching the warehouse ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {warehouse: 1}};
ctx.args = {warehouse: 1};
let result = await app.models.TicketRequest.filter(ctx, {order: 'id'});
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx, {order: 'id'});
const requestId = result[0].id;
expect(requestId).toEqual(3);
});
it('should return the ticket request matching the salesPerson ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {salesPersonFk: 18}};
ctx.args = {salesPersonFk: 18};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(3);
});

View File

@ -89,15 +89,21 @@ module.exports = Self => {
if (!zoneShipped || zoneShipped.zoneFk != zoneFk)
throw new UserError(`You don't have privileges to change the zone`);
}
const originalTicket = await models.Ticket.findById(id, {
include: {
const observationTypeDelivery = await models.ObservationType.findOne({
where: {code: 'delivery'}
});
const originalTicket = await models.Ticket.findOne({
where: {id: id},
fields: ['id', 'clientFk', 'agencyModeFk', 'addressFk', 'zoneFk',
'warehouseFk', 'companyFk', 'shipped', 'landed', 'isDeleted'],
include: [
{
relation: 'client',
scope: {
fields: 'salesPersonFk'
}
},
fields: ['id', 'clientFk', 'agencyModeFk', 'addressFk', 'zoneFk',
'warehouseFk', 'companyFk', 'shipped', 'landed', 'isDeleted']
}]
});
const updatedTicket = Object.assign({}, ctx.args);
delete updatedTicket.ctx;
@ -121,6 +127,39 @@ module.exports = Self => {
option
]);
if (originalTicket.addressFk != updatedTicket.addressFk) {
const ticketObservation = await models.TicketObservation.findOne({
where: {
ticketFk: id,
observationTypeFk: observationTypeDelivery.id}
});
if (ticketObservation)
await ticketObservation.destroy();
const address = await models.Address.findOne({
where: {id: addressFk},
include: {
relation: 'observations',
scope: {
where: {observationTypeFk: observationTypeDelivery.id},
include: {
relation: 'observationType'
}
}
}
});
const [observation] = address.observations();
if (observation) {
await models.TicketObservation.create({
ticketFk: id,
observationTypeFk: observation.observationTypeFk,
description: observation.description
});
}
}
const changes = loggable.getChanges(originalTicket, updatedTicket);
const oldProperties = await loggable.translateValues(Self, changes.old);
const newProperties = await loggable.translateValues(Self, changes.new);

View File

@ -5,6 +5,7 @@ describe('ticket componentUpdate()', () => {
const ticketID = 11;
const today = new Date();
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
let deliveryComponentId;
@ -97,4 +98,62 @@ describe('ticket componentUpdate()', () => {
expect(firstvalueBeforeChange).toEqual(firstvalueAfterChange);
expect(secondvalueBeforeChange).toEqual(secondvalueAfterChange);
});
it('should change the addressFk and check that delivery observations have been changed and then undo the changes', async() => {
const clientID = 102;
const addressID = 122;
const newAddressID = 2;
const agencyModeID = 8;
const warehouseID = 1;
const zoneID = 5;
const shipped = today;
const companyID = 442;
const isDeleted = false;
const landed = tomorrow;
const option = 1;
const ctx = {
args: {clientFk: clientID,
agencyModeFk: agencyModeID},
req: {
accessToken: {userId: userID},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
const observationTypeDelivery = await app.models.ObservationType.findOne({
where: {code: 'delivery'}
});
const originalTicketObservation = await app.models.TicketObservation.findOne({
where: {
ticketFk: ticketID,
observationTypeFk: observationTypeDelivery.id}
});
expect(originalTicketObservation).toBeDefined();
await app.models.Ticket.componentUpdate(ctx, ticketID, clientID, agencyModeID, newAddressID,
zoneID, warehouseID, companyID, shipped, landed, isDeleted, option);
const removedTicketObservation = await app.models.TicketObservation.findOne({
where: {
ticketFk: ticketID,
observationTypeFk: observationTypeDelivery.id}
});
expect(removedTicketObservation).toBeNull();
// restores
await app.models.Ticket.componentUpdate(ctx, ticketID, clientID, agencyModeID, addressID,
zoneID, warehouseID, companyID, shipped, landed, isDeleted, option);
const restoredTicketObservation = await app.models.TicketObservation.findOne({
where: {
ticketFk: ticketID,
observationTypeFk: observationTypeDelivery.id}
});
expect(restoredTicketObservation.description).toEqual(originalTicketObservation.description);
});
});

View File

@ -203,8 +203,6 @@ class Controller extends Section {
}
set weekTotalHours(totalHours) {
if (!totalHours) return this._weekTotalHours = this.formatHours(0);
this._weekTotalHours = this.formatHours(totalHours);
}
@ -212,9 +210,7 @@ class Controller extends Section {
return this._weekTotalHours;
}
formatHours(timestamp) {
timestamp = timestamp || 0;
formatHours(timestamp = 0) {
let hour = Math.floor(timestamp / 3600);
let min = Math.floor(timestamp / 60 - 60 * hour);

Some files were not shown because too many files have changed in this diff Show More