Merge branch 'dev' into 4059-revocar-permisos-client-account
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2023-07-04 11:47:39 +02:00
commit e4e7a9444a
202 changed files with 4067 additions and 4156 deletions

View File

@ -17,7 +17,7 @@ rules:
camelcase: 0
default-case: 0
no-eq-null: 0
no-console: ["error"]
no-console: ["warn"]
no-warning-comments: 0
no-empty: [error, allowEmptyCatch: true]
complexity: 0

View File

@ -8,13 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2326.01] - 2023-06-29
### Added
- (Entradas -> Correo) Al cambiar el tipo de cambio enviará un correo a las personas designadas
- (General -> Históricos) Botón para ver el estado del registro en cada punto
- (General -> Históricos) Al filtar por registro se muestra todo el histórial desde que fue creado
- (Tickets -> Índice) Permite enviar varios albaranes a Docuware
### Changed
- (General -> Históricos) Los registros se muestran agrupados por usuario y entidad
- (Facturas -> Facturación global) Optimizada, generación de PDFs y notificaciones en paralelo
### Fixed
-
- (General -> Históricos) Duplicidades eliminadas
- (Facturas -> Facturación global) Solucionados fallos que paran el proceso
## [2324.01] - 2023-06-08
## [2324.01] - 2023-06-15
### Added
- (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén

View File

@ -8,7 +8,7 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications.
* Node.js = 14.x LTS
* Node.js >= 16.x LTS
* Docker
* Git
@ -71,7 +71,7 @@ $ npm run test:e2e
Open Visual Studio Code, press Ctrl+P and paste the following commands.
In Visual Studio Code we use the ESLint extension.
In Visual Studio Code we use the ESLint extension.
```
ext install dbaeumer.vscode-eslint
```

View File

@ -0,0 +1,38 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('renewToken', {
description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it',
accessType: 'WRITE',
accepts: [],
returns: {
type: 'Object',
root: true
},
http: {
path: `/renewToken`,
verb: 'POST'
}
});
Self.renewToken = async function(ctx) {
const models = Self.app.models;
const token = ctx.req.accessToken;
const now = new Date();
const differenceMilliseconds = now - token.created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
const fields = ['renewPeriod', 'courtesyTime'];
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields});
if (differenceSeconds < accessTokenConfig.renewPeriod - accessTokenConfig.courtesyTime)
throw new UserError(`The renew period has not been exceeded`, 'periodNotExceeded');
await Self.logout(token.id);
const user = await Self.findById(token.userId);
const accessToken = await user.createAccessToken();
return {id: accessToken.id, ttl: accessToken.ttl};
};
};

View File

@ -62,10 +62,9 @@ module.exports = Self => {
scopes: ['change-password'],
userId: vnUser.id
});
throw new UserError('Pass expired', 'passExpired', {
id: vnUser.id,
token: changePasswordToken.id
});
const err = new UserError('Pass expired', 'passExpired');
err.details = {token: changePasswordToken};
throw err;
}
try {
@ -77,6 +76,6 @@ module.exports = Self => {
let loginInfo = Object.assign({password}, userInfo);
token = await Self.login(loginInfo, 'user');
return {token: token.id};
return {token: token.id, ttl: token.ttl};
};
};

View File

@ -9,7 +9,7 @@ describe('VnUser signIn()', () => {
expect(login.token).toBeDefined();
await models.VnUser.signOut(ctx);
await models.VnUser.logout(ctx.req.accessToken.id);
});
it('should return the token if the user doesnt exist but the client does', async() => {
@ -19,7 +19,7 @@ describe('VnUser signIn()', () => {
expect(login.token).toBeDefined();
await models.VnUser.signOut(ctx);
await models.VnUser.logout(ctx.req.accessToken.id);
});
});

View File

@ -1,42 +0,0 @@
const {models} = require('vn-loopback/server/server');
describe('VnUser signOut()', () => {
it('should logout and remove token after valid login', async() => {
let loginResponse = await models.VnUser.signOut('buyer', 'nightmare');
let accessToken = await models.AccessToken.findById(loginResponse.token);
let ctx = {req: {accessToken: accessToken}};
let logoutResponse = await models.VnUser.signOut(ctx);
let tokenAfterLogout = await models.AccessToken.findById(loginResponse.token);
expect(logoutResponse).toBeTrue();
expect(tokenAfterLogout).toBeNull();
});
it('should throw a 401 error when token is invalid', async() => {
let error;
let ctx = {req: {accessToken: {id: 'invalidToken'}}};
try {
response = await models.VnUser.signOut(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(401);
});
it('should throw an error when no token is passed', async() => {
let error;
let ctx = {req: {accessToken: null}};
try {
response = await models.VnUser.signOut(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
});

View File

@ -2,6 +2,14 @@
"AccountingType": {
"dataSource": "vn"
},
"AccessTokenConfig": {
"dataSource": "vn",
"options": {
"mysql": {
"table": "salix.accessTokenConfig"
}
}
},
"Bank": {
"dataSource": "vn"
},

View File

@ -0,0 +1,34 @@
{
"name": "AccessTokenConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "accessTokenConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"renewPeriod": {
"type": "number",
"required": true
},
"courtesyTime": {
"type": "number",
"required": true
},
"renewInterval": {
"type": "number",
"required": true
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -10,6 +10,9 @@ module.exports = function(Self) {
require('../methods/vn-user/recover-password')(Self);
require('../methods/vn-user/validate-token')(Self);
require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/renew-token')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
// Validations

View File

@ -118,5 +118,24 @@
"principalId": "$authenticated",
"permission": "ALLOW"
}
]
],
"scopes": {
"preview": {
"fields": [
"id",
"name",
"username",
"roleFk",
"nickname",
"lang",
"active",
"created",
"updated",
"image",
"hasGrant",
"realm",
"email"
]
}
}
}

View File

@ -4,4 +4,4 @@ apps:
instances: 1
max_restarts: 3
restart_delay: 15000
node_args: --tls-min-v1.0
node_args: --tls-min-v1.0 --openssl-legacy-provider

View File

@ -1,6 +1,5 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', '*', '*', 'ALLOW', 'ROLE', 'employee'),
('VnUser','acl','READ','ALLOW','ROLE','account'),
('VnUser','getCurrentUserData','READ','ALLOW','ROLE','account'),
('VnUser','changePassword', 'WRITE', 'ALLOW', 'ROLE', 'account'),

View File

@ -0,0 +1,22 @@
CREATE TABLE `vn`.`travelConfig` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`warehouseInFk` smallint(6) unsigned NOT NULL DEFAULT 8 COMMENT 'Warehouse de origen',
`warehouseOutFk` smallint(6) unsigned NOT NULL DEFAULT 60 COMMENT 'Warehouse destino',
`agencyFk` int(11) NOT NULL DEFAULT 1378 COMMENT 'Agencia por defecto',
`companyFk` int(10) unsigned NOT NULL DEFAULT 442 COMMENT 'Compañía por defecto',
PRIMARY KEY (`id`),
KEY `travelConfig_FK` (`warehouseInFk`),
KEY `travelConfig_FK_1` (`warehouseOutFk`),
KEY `travelConfig_FK_2` (`agencyFk`),
KEY `travelConfig_FK_3` (`companyFk`),
CONSTRAINT `travelConfig_FK` FOREIGN KEY (`warehouseInFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_1` FOREIGN KEY (`warehouseOutFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_2` FOREIGN KEY (`agencyFk`) REFERENCES `agencyMode` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_3` FOREIGN KEY (`companyFk`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Entry', 'addFromPackaging', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Entry', 'addFromBuy', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Supplier', 'getItemsPackaging', 'READ', 'ALLOW', 'ROLE', 'production');

View File

@ -0,0 +1,8 @@
DELETE
FROM `salix`.`ACL`
WHERE model='Account' AND property='*' AND accessType='*';
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('Account', '*', 'WRITE', 'ALLOW', 'ROLE', 'sysadmin'),
('Account', '*', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,5 @@
DELETE FROM `salix`.`ACL` WHERE model = 'MailAliasAccount';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('MailAliasAccount', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('MailAliasAccount', '*', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,5 @@
DELETE FROM `salix`.`ACL` WHERE model = 'MailForward';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('MailForward', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('MailForward', '*', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,5 @@
DELETE FROM `salix`.`ACL` WHERE model = 'Role';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Role', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Role', '*', 'WRITE', 'ALLOW', 'ROLE', 'it');

View File

@ -0,0 +1,10 @@
DELETE
FROM `salix`.`ACL`
WHERE model = 'VnUser' AND property = '*' AND principalId = 'employee';
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', '*', '*', 'ALLOW', 'ROLE', 'itManagement'),
('VnUser', '__get__preview', 'READ', 'ALLOW', 'ROLE', 'employee'),
('VnUser', 'preview', '*', 'ALLOW', 'ROLE', 'employee'),
('VnUser', 'create', '*', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'renewToken', 'WRITE', 'ALLOW', 'ROLE', 'employee')

View File

@ -0,0 +1,40 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`entry_updateComission`(vCurrency INT)
BEGIN
/**
* Actualiza la comision de las entradas de hoy a futuro y las recalcula
*
* @param vCurrency id del tipo de moneda(SAR,EUR,USD,GBP,JPY)
*/
DECLARE vCurrencyName VARCHAR(25);
DECLARE vComission INT;
CREATE OR REPLACE TEMPORARY TABLE tmp.recalcEntryCommision
SELECT e.id
FROM vn.entry e
JOIN vn.travel t ON t.id = e.travelFk
JOIN vn.warehouse w ON w.id = t.warehouseInFk
WHERE t.shipped >= util.VN_CURDATE()
AND e.currencyFk = vCurrency;
SET vComission = currency_getCommission(vCurrency);
UPDATE vn.entry e
JOIN tmp.recalcEntryCommision tmp ON tmp.id = e.id
SET e.commission = vComission;
SELECT `name` INTO vCurrencyName
FROM currency
WHERE id = vCurrency;
CALL entry_recalc();
SELECT util.notification_send(
'entry-update-comission',
JSON_OBJECT('currencyName', vCurrencyName, 'referenceCurrent', vComission),
account.myUser_getId()
);
DROP TEMPORARY TABLE tmp.recalcEntryCommision;
END$$
DELIMITER ;

View File

@ -0,0 +1,71 @@
CREATE TABLE `vn`.`packingSiteAdvanced` (
`ticketFk` int(11),
`workerFk` int(10) unsigned,
PRIMARY KEY (`ticketFk`),
KEY `packingSiteAdvanced_FK_1` (`workerFk`),
CONSTRAINT `packingSiteAdvanced_FK` FOREIGN KEY (`ticketFk`) REFERENCES `ticket` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `packingSiteAdvanced_FK_1` FOREIGN KEY (`workerFk`) REFERENCES `worker` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('PackingSiteAdvanced', '*', '*', 'ALLOW', 'ROLE', 'production');
DROP PROCEDURE IF EXISTS `vn`.`packingSite_startCollection`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`packingSite_startCollection`(vSelf INT, vTicketFk INT)
proc: BEGIN
/**
* @param vSelf packingSite id
* @param vTicketFk A ticket id from the collection to start
*/
DECLARE vExists BOOL;
DECLARE vIsAdvanced BOOL;
DECLARE vNewCollectionFk INT;
DECLARE vOldCollectionFk INT;
DECLARE vIsPackingByOther BOOL;
SELECT id, collectionFk
INTO vExists, vOldCollectionFk
FROM packingSite
WHERE id = vSelf;
IF NOT vExists THEN
CALL util.throw('packingSiteNotExists');
END IF;
SELECT COUNT(*) > 0
INTO vIsAdvanced
FROM packingSiteAdvanced
WHERE ticketFk = vTicketFk;
IF vIsAdvanced THEN
LEAVE proc;
END IF;
SELECT collectionFk INTO vNewCollectionFk
FROM ticketCollection WHERE ticketFk = vTicketFk;
IF vOldCollectionFk IS NOT NULL
AND vOldCollectionFk <> vNewCollectionFk THEN
SELECT COUNT(*) > 0
INTO vIsPackingByOther
FROM packingSite
WHERE id <> vSelf
AND collectionFk = vOldCollectionFk;
IF NOT vIsPackingByOther AND NOT collection_isPacked(vOldCollectionFk) THEN
CALL util.throw('cannotChangeCollection');
END IF;
END IF;
UPDATE packingSite SET collectionFk = vNewCollectionFk
WHERE id = vSelf;
END$$
DELIMITER ;

View File

@ -0,0 +1,11 @@
CREATE TABLE `salix`.`accessTokenConfig` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`renewPeriod` int(10) unsigned DEFAULT NULL,
`courtesyTime` int(10) unsigned DEFAULT NULL,
`renewInterval` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT IGNORE INTO `salix`.`accessTokenConfig` (`id`, `renewPeriod`, `courtesyTime`, `renewInterval`)
VALUES
(1, 21600, 5, 300);

View File

@ -0,0 +1,13 @@
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES
('InvoiceOut','makePdfAndNotify','WRITE','ALLOW','ROLE','invoicing'),
('InvoiceOutConfig','*','READ','ALLOW','ROLE','invoicing');
CREATE OR REPLACE TABLE `vn`.`invoiceOutConfig` (
id INT UNSIGNED auto_increment NOT NULL,
parallelism int UNSIGNED DEFAULT 1 NOT NULL,
PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb3
COLLATE=utf8mb3_unicode_ci;

View File

@ -0,0 +1,13 @@
UPDATE `salix`.`ACL`
SET principalId='financialBoss'
WHERE
model = 'Client'
AND property = 'editCredit';
UPDATE `salix`.`ACL`
SET property='zeroCreditEditor'
WHERE
model = 'Client'
AND property = 'isNotEditableCredit';

File diff suppressed because one or more lines are too long

View File

@ -144,17 +144,17 @@ INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`)
(3, 'GBP', 'Libra', 1),
(4, 'JPY', 'Yen Japones', 1);
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`, `politicalCountryFk`)
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
VALUES
(1, 'España', 1, 'ES', 1, 24, 4, 0, 1, 1),
(2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1, 2),
(3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1, 3),
(4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1, 4),
(5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1, 5),
(8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1, 8),
(13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2, 13),
(19,'Francia', 1, 'FR', 1, 27, 4, 0, 1, 19),
(30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2, 30);
(1, 'España', 1, 'ES', 1, 24, 4, 0, 1),
(2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1),
(3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1),
(4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1),
(5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1),
(8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1),
(13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2),
(19,'Francia', 1, 'FR', 1, 27, 4, 0, 1),
(30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2);
INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
VALUES
@ -550,10 +550,13 @@ INSERT INTO `vn`.`supplierAddress`(`id`, `supplierFk`, `nickname`, `street`, `pr
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`, `commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`, `workerFk`, `supplierActivityFk`, `isPayMethodChecked`, `healthRegister`)
VALUES
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(1381, 'Ornamentales', 'Ornamentales', 7185000440, 1, '03815934E', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
(69, 'Packaging', 'Packaging nick', 4100000069, 1, '94935005K', 0, util.VN_CURDATE(), 1, 'supplier address 5', 'ASGARD', 3, 46600, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(567, 'Holland', 'Holland nick', 4000020567, 1, '14364089Z', 0, util.VN_CURDATE(), 1, 'supplier address 6', 'ASGARD', 3, 46600, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
(791, 'Bros SL', 'Bros nick', 5115000791, 1, '37718083S', 0, util.VN_CURDATE(), 1, 'supplier address 7', 'ASGARD', 3, 46600, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(1381, 'Ornamentales', 'Ornamentales', 7185001381, 1, '07972486L', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES
@ -603,6 +606,9 @@ UPDATE `vn`.`invoiceOut` SET ref = 'T3333333' WHERE id = 3;
UPDATE `vn`.`invoiceOut` SET ref = 'T4444444' WHERE id = 4;
UPDATE `vn`.`invoiceOut` SET ref = 'A1111111' WHERE id = 5;
INSERT INTO vn.invoiceOutConfig
SET parallelism = 8;
INSERT INTO `vn`.`invoiceOutTax` (`invoiceOutFk`, `taxableBase`, `vat`, `pgcFk`)
VALUES
(1, 895.76, 89.58, 4722000010),
@ -696,12 +702,12 @@ INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agen
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`)
VALUES
(1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, 'T1111111', 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, 'T1111111', 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, 'T2222222', 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)),
(4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, 'T3333333', 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH)),
(5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, 'T4444444', 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH)),
(6 , 1, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Mountain Drive Gotham', 1, 'A1111111', 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)),
(4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH)),
(5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH)),
(6 , 1, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Mountain Drive Gotham', 1, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(7 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'Mountain Drive Gotham', 1, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(8 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'Bat cave', 121, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(9 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
@ -2569,6 +2575,26 @@ INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
CALL `vn`.`ticket_doRecalc`();
UPDATE `vn`.`ticket`
SET refFk = 'T1111111'
WHERE id IN (1,2);
UPDATE `vn`.`ticket`
SET refFk = 'T2222222'
WHERE id = 3;
UPDATE `vn`.`ticket`
SET refFk = 'T3333333'
WHERE id = 4;
UPDATE `vn`.`ticket`
SET refFk = 'T4444444'
WHERE id = 5;
UPDATE `vn`.`ticket`
SET refFk = 'A1111111'
WHERE id = 6;
INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
VALUES
(1, 1, 1),
@ -2576,7 +2602,7 @@ INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
(3, 6, 5),
(4, 7, 1);
INSERT INTO `vn`.`expeditionTruck` (`id`, `ETD`, `description`)
INSERT INTO `vn`.`expeditionTruck` (`id`, `eta`, `description`)
VALUES
(1, CONCAT(YEAR(DATE_ADD(util.VN_CURDATE(), INTERVAL +3 YEAR))), 'Best truck in fleet');
@ -2736,7 +2762,8 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
(1, 'print-email', 'notification fixture one'),
(2, 'invoice-electronic', 'A electronic invoice has been generated'),
(3, 'not-main-printer-configured', 'A printer distinct than main has been configured'),
(4, 'supplier-pay-method-update', 'A supplier pay method has been updated');
(4, 'supplier-pay-method-update', 'A supplier pay method has been updated'),
(5, 'modified-entry', 'An entry has been modified');
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
VALUES
@ -2838,7 +2865,8 @@ INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk
INSERT INTO `vn`.`ticketRefund`(`refundTicketFk`, `originalTicketFk`)
VALUES
(1, 12);
(1, 12),
(8, 10);
INSERT INTO `vn`.`deviceProductionModels` (`code`)
VALUES
@ -2894,6 +2922,10 @@ INSERT INTO `vn`.`wagonTypeTray` (`id`, `typeFk`, `height`, `colorFk`)
(2, 1, 50, 2),
(3, 1, 0, 3);
INSERT INTO `salix`.`accessTokenConfig` (`id`, `renewPeriod`, `renewInterval`)
VALUES
(1, 21600, 300);
INSERT INTO `vn`.`travelConfig` (`id`, `warehouseInFk`, `warehouseOutFk`, `agencyFk`, `companyFk`)
VALUES
(1, 1, 1, 1, 442);

File diff suppressed because it is too large Load Diff

View File

@ -311,9 +311,9 @@ export default {
},
clientDefaulter: {
anyClient: 'vn-client-defaulter tbody > tr',
firstClientName: 'vn-client-defaulter tbody > tr:nth-child(1) > td:nth-child(2) > span',
firstSalesPersonName: 'vn-client-defaulter tbody > tr:nth-child(1) > td:nth-child(3) > span',
firstObservation: 'vn-client-defaulter tbody > tr:nth-child(1) > td:nth-child(8) > vn-textarea[ng-model="defaulter.observation"]',
firstClientName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(2) > span',
firstSalesPersonName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(4) > span',
firstObservation: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(9) > vn-textarea[ng-model="defaulter.observation"]',
allDefaulterCheckbox: 'vn-client-defaulter thead vn-multi-check',
addObservationButton: 'vn-client-defaulter vn-button[icon="icon-notes"]',
observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]',
@ -334,15 +334,15 @@ export default {
},
itemsIndex: {
createItemButton: `vn-float-button`,
firstSearchResult: 'vn-item-index tbody tr:nth-child(1)',
firstSearchResult: 'vn-item-index tbody tr:nth-child(2)',
searchResult: 'vn-item-index tbody tr:not(.empty-rows)',
firstResultPreviewButton: 'vn-item-index tbody > :nth-child(1) .buttons > [icon="preview"]',
firstResultPreviewButton: 'vn-item-index tbody > :nth-child(2) .buttons > [icon="preview"]',
searchResultCloneButton: 'vn-item-index .buttons > [icon="icon-clone"]',
acceptClonationAlertButton: '.vn-confirm.shown [response="accept"]',
closeItemSummaryPreview: '.vn-popup.shown',
shownColumns: 'vn-item-index vn-button[id="shownColumns"]',
shownColumnsList: '.vn-popover.shown .content',
firstItemImage: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(1) > img',
firstItemImage: 'vn-item-index tbody > tr:nth-child(2) > td:nth-child(1) > img',
firstItemImageTd: 'vn-item-index smart-table tr:nth-child(1) td:nth-child(1)',
firstItemId: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(2)',
idCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Identifier"]',
@ -479,9 +479,6 @@ export default {
fourthBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(4) > vn-td.balance > span',
firstBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(1) > vn-td.balance'
},
itemLog: {
anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr',
},
ticketSummary: {
header: 'vn-ticket-summary > vn-card > h5',
state: 'vn-ticket-summary vn-label-value[label="State"] > section > span',
@ -523,11 +520,11 @@ export default {
searchResultDate: 'vn-ticket-summary [label=Landed] span',
topbarSearch: 'vn-searchbar',
moreMenu: 'vn-ticket-index vn-icon-button[icon=more_vert]',
fourthWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(4)',
fiveWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(5)',
fourthWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(5)',
fiveWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(6)',
weeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table table tbody tr',
firstWeeklyTicketDeleteIcon: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(1) vn-icon-button[icon="delete"]',
firstWeeklyTicketAgency: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(1) [ng-model="weekly.agencyModeFk"]',
firstWeeklyTicketDeleteIcon: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(2) vn-icon-button[icon="delete"]',
firstWeeklyTicketAgency: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(2) [ng-model="weekly.agencyModeFk"]',
acceptDeleteTurn: '.vn-confirm.shown button[response="accept"]'
},
createTicketView: {
@ -572,8 +569,8 @@ export default {
submitNotesButton: 'button[type=submit]'
},
ticketExpedition: {
firstSaleCheckbox: 'vn-ticket-expedition tr:nth-child(1) vn-check[ng-model="expedition.checked"]',
thirdSaleCheckbox: 'vn-ticket-expedition tr:nth-child(3) vn-check[ng-model="expedition.checked"]',
firstSaleCheckbox: 'vn-ticket-expedition tr:nth-child(2) vn-check[ng-model="expedition.checked"]',
thirdSaleCheckbox: 'vn-ticket-expedition tr:nth-child(4) vn-check[ng-model="expedition.checked"]',
deleteExpeditionButton: 'vn-ticket-expedition slot-actions > vn-button[icon="delete"]',
moveExpeditionButton: 'vn-ticket-expedition slot-actions > vn-button[icon="keyboard_arrow_down"]',
moreMenuWithoutRoute: 'vn-item[name="withoutRoute"]',
@ -667,15 +664,6 @@ export default {
thirdRemoveRequestButton: 'vn-ticket-request-index vn-tr:nth-child(3) vn-icon[icon="delete"]',
thirdRequestQuantity: 'vn-ticket-request-index vn-table vn-tr:nth-child(3) > vn-td:nth-child(6) vn-input-number',
saveButton: 'vn-ticket-request-create button[type=submit]',
},
ticketLog: {
firstTD: 'vn-ticket-log vn-table vn-td:nth-child(1)',
logButton: 'vn-left-menu a[ui-sref="ticket.card.log"]',
user: 'vn-ticket-log vn-tbody vn-tr vn-td:nth-child(2)',
action: 'vn-ticket-log vn-tbody vn-tr vn-td:nth-child(4)',
changes: 'vn-ticket-log vn-data-viewer vn-tbody vn-tr table tr:nth-child(2) td.after',
id: 'vn-ticket-log vn-tr:nth-child(1) table tr:nth-child(1) td.before'
},
ticketService: {
addServiceButton: 'vn-ticket-service vn-icon-button[vn-tooltip="Add service"] > button',
@ -712,7 +700,7 @@ export default {
problems: 'vn-check[label="With problems"]',
tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Future tickets"]',
firstCheck: 'tbody > tr:nth-child(1) > td > vn-check',
firstCheck: 'tbody > tr:nth-child(2) > td > vn-check',
multiCheck: 'vn-multi-check',
tableId: 'vn-textfield[name="id"]',
tableFutureId: 'vn-textfield[name="futureId"]',
@ -736,7 +724,7 @@ export default {
tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Advance tickets"]',
acceptButton: '.vn-confirm.shown button[response="accept"]',
firstCheck: 'tbody > tr:nth-child(1) > td > vn-check',
firstCheck: 'tbody > tr:nth-child(2) > td > vn-check',
tableId: 'vn-textfield[name="id"]',
tableFutureId: 'vn-textfield[name="futureId"]',
tableLiters: 'vn-textfield[name="liters"]',
@ -810,7 +798,7 @@ export default {
claimAction: {
importClaimButton: 'vn-claim-action vn-button[label="Import claim"]',
anyLine: 'vn-claim-action vn-tbody > vn-tr',
firstDeleteLine: 'vn-claim-action tr:nth-child(1) vn-icon-button[icon="delete"]',
firstDeleteLine: 'vn-claim-action tr:nth-child(2) vn-icon-button[icon="delete"]',
isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]'
},
ordersIndex: {
@ -1179,8 +1167,6 @@ export default {
allBuyCheckbox: 'vn-entry-buy-index thead vn-check',
firstBuyCheckbox: 'vn-entry-buy-index tbody:nth-child(2) vn-check',
deleteBuysButton: 'vn-entry-buy-index vn-button[icon="delete"]',
addBuyButton: 'vn-entry-buy-index vn-icon[icon="add"]',
secondBuyPackingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price3"]',
secondBuyGroupingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price2"]',
secondBuyPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.buyingValue"]',
secondBuyGrouping: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.grouping"]',
@ -1216,7 +1202,7 @@ export default {
addTagButton: 'vn-icon-button[vn-tooltip="Add tag"]',
itemTagInput: 'vn-autocomplete[ng-model="itemTag.tagFk"]',
itemTagValueInput: 'vn-autocomplete[ng-model="itemTag.value"]',
firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(1)',
firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(2)',
allBuysCheckBox: 'vn-entry-latest-buys thead vn-check',
secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.checked"]',
editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]',

View File

@ -19,15 +19,14 @@ describe('SmartTable SearchBar integration', () => {
await page.waitToClick(selectors.itemsIndex.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.itemsIndex.advancedSearchItemType, 'Anthurium');
await page.waitToClick(selectors.itemsIndex.advancedSearchButton);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 4);
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 4);
await page.waitToClick(selectors.itemsIndex.advancedSmartTableButton);
await page.write(selectors.itemsIndex.advancedSmartTableGrouping, '1');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
@ -36,7 +35,7 @@ describe('SmartTable SearchBar integration', () => {
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 1);
});
it('should filter in section without smart-table and search in searchBar go to zone section', async() => {

View File

@ -5,8 +5,8 @@ const $ = {
userName: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.name"]',
email: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.email"]',
saveButton: 'vn-client-web-access button[type=submit]',
nameValue: 'vn-client-log .change:nth-child(1) .basic-json:nth-child(2) vn-json-value',
activeValue: 'vn-client-log .change:nth-child(2) .basic-json:nth-child(1) vn-json-value'
nameValue: 'vn-client-log .changes-log:nth-child(2) .basic-json:nth-child(2) vn-json-value',
activeValue: 'vn-client-log .changes-log:nth-child(3) .basic-json:nth-child(1) vn-json-value'
};
describe('Client web access path', () => {
@ -39,9 +39,9 @@ describe('Client web access path', () => {
const userName = await page.getValue($.userName);
const email = await page.getValue($.email);
await page.accessToSection('client.card.log');
const logName = await page.innerText($.nameValue);
const logActive = await page.innerText($.activeValue);
// await page.accessToSection('client.card.log');
// const logName = await page.innerText($.nameValue);
// const logActive = await page.innerText($.activeValue);
expect(enableMessage.type).toBe('success');
expect(modifyMessage.type).toBe('success');
@ -50,7 +50,7 @@ describe('Client web access path', () => {
expect(userName).toEqual('Legion');
expect(email).toEqual('legion@marvel.com');
expect(logName).toEqual('Legion');
expect(logActive).toEqual('✗');
// expect(logName).toEqual('Legion');
// expect(logActive).toEqual('✗');
});
});

View File

@ -19,7 +19,7 @@ describe('Client defaulter path', () => {
it('should count the amount of clients in the turns section', async() => {
const result = await page.countElement(selectors.clientDefaulter.anyClient);
expect(result).toEqual(5);
expect(result).toEqual(6);
});
it('should check contain expected client', async() => {

View File

@ -53,7 +53,7 @@ describe('Worker create path', () => {
expect(message.text).toContain('Data saved!');
// 'rollback'
await page.loginAndModule('sysadmin', 'account');
await page.loginAndModule('itManagement', 'account');
await page.accessToSearchResult(newWorker);
await page.waitToClick(selectors.accountDescriptor.menuButton);

View File

@ -18,11 +18,11 @@ describe('Item summary path', () => {
await page.doSearch('Ranged weapon');
const resultsCount = await page.countElement(selectors.itemsIndex.searchResult);
await page.waitForTextInElement(selectors.itemsIndex.searchResult, 'Ranged weapon');
await page.waitForTextInElement(selectors.itemsIndex.firstSearchResult, 'Ranged weapon');
await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton);
const isVisible = await page.isVisible(selectors.itemSummary.basicData);
expect(resultsCount).toBe(3);
expect(resultsCount).toBe(4);
expect(isVisible).toBeTruthy();
});
@ -66,7 +66,7 @@ describe('Item summary path', () => {
await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton);
await page.waitForSelector(selectors.itemSummary.basicData, {visible: true});
expect(resultsCount).toBe(2);
expect(resultsCount).toBe(3);
});
it(`should now check the item summary preview shows fields from basic data`, async() => {

View File

@ -19,7 +19,6 @@ describe('Item edit tax path', () => {
it(`should add the item tax to all countries`, async() => {
await page.autocompleteSearch(selectors.itemTax.firstClass, 'General VAT');
await page.autocompleteSearch(selectors.itemTax.secondClass, 'General VAT');
await page.autocompleteSearch(selectors.itemTax.thirdClass, 'General VAT');
await page.waitToClick(selectors.itemTax.submitTaxButton);
const message = await page.waitForSnackbar();
@ -40,13 +39,6 @@ describe('Item edit tax path', () => {
expect(secondVatType).toEqual('General VAT');
});
it(`should confirm the third item tax class was edited`, async() => {
const thirdVatType = await page
.waitToGetProperty(selectors.itemTax.thirdClass, 'value');
expect(thirdVatType).toEqual('General VAT');
});
it(`should edit the first class without saving the form`, async() => {
await page.autocompleteSearch(selectors.itemTax.firstClass, 'Reduced VAT');
const firstVatType = await page.waitToGetProperty(selectors.itemTax.firstClass, 'value');

View File

@ -18,7 +18,7 @@ describe('Item log path', () => {
await page.doSearch('Knowledge artifact');
const nResults = await page.countElement(selectors.itemsIndex.searchResult);
expect(nResults).toEqual(0);
expect(nResults).toEqual(1);
});
it('should access to the create item view by clicking the create floating button', async() => {

View File

@ -27,6 +27,6 @@ describe('Ticket expeditions and log path', () => {
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(3);
expect(result).toEqual(4);
});
});

View File

@ -19,7 +19,7 @@ describe('Ticket descriptor path', () => {
it('should count the amount of tickets in the turns section', async() => {
const result = await page.countElement(selectors.ticketsIndex.weeklyTicket);
expect(result).toEqual(6);
expect(result).toEqual(7);
});
it('should go back to the ticket index then search and access a ticket summary', async() => {
@ -89,7 +89,7 @@ describe('Ticket descriptor path', () => {
await page.doSearch('11');
const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult);
expect(nResults).toEqual(1);
expect(nResults).toEqual(2);
});
it('should delete the weekly ticket 11', async() => {
@ -104,7 +104,7 @@ describe('Ticket descriptor path', () => {
await page.doSearch();
const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult);
expect(nResults).toEqual(6);
expect(nResults).toEqual(7);
});
it('should update the agency then remove it afterwards', async() => {

View File

@ -29,7 +29,7 @@ describe('Ticket expeditions', () => {
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
expect(result).toEqual(2);
});
it(`should move one expedition to new ticket with route`, async() => {
@ -45,6 +45,6 @@ describe('Ticket expeditions', () => {
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
expect(result).toEqual(2);
});
});

View File

@ -87,7 +87,7 @@ describe('Ticket Future path', () => {
await page.clearInput(selectors.ticketFuture.futureState);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 5);
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);

View File

@ -9,7 +9,7 @@ describe('Travel descriptor path', () => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('buyer', 'travel');
await page.write(selectors.travelIndex.generalSearchFilter, '1');
await page.write(selectors.travelIndex.generalSearchFilter, '3');
await page.keyboard.press('Enter');
await page.waitForState('travel.card.summary');
});
@ -23,7 +23,7 @@ describe('Travel descriptor path', () => {
await page.waitForState('travel.index');
const result = await page.countElement(selectors.travelIndex.anySearchResult);
expect(result).toBeGreaterThanOrEqual(7);
expect(result).toBeGreaterThanOrEqual(1);
});
it('should navigate to the first search result', async() => {

View File

@ -66,97 +66,4 @@ describe('Entry import, create and edit buys path', () => {
await page.waitToClick(selectors.globalItems.acceptButton);
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 1);
});
it('should add a new buy', async() => {
await page.waitToClick(selectors.entryBuys.addBuyButton);
await page.write(selectors.entryBuys.secondBuyPackingPrice, '999');
await page.write(selectors.entryBuys.secondBuyGroupingPrice, '999');
await page.write(selectors.entryBuys.secondBuyPrice, '999');
await page.write(selectors.entryBuys.secondBuyGrouping, '999');
await page.write(selectors.entryBuys.secondBuyPacking, '999');
await page.write(selectors.entryBuys.secondBuyWeight, '999');
await page.write(selectors.entryBuys.secondBuyStickers, '999');
await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '1');
await page.write(selectors.entryBuys.secondBuyQuantity, '999');
await page.autocompleteSearch(selectors.entryBuys.secondBuyItem, '1');
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 2);
});
it('should edit the newest buy and check data', async() => {
await page.clearInput(selectors.entryBuys.secondBuyPackingPrice);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPackingPrice, '100');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyGroupingPrice);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyGroupingPrice, '200');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyPrice);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPrice, '300');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyGrouping);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyGrouping, '400');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyPacking);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPacking, '500');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyWeight);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyWeight, '600');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyStickers);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyStickers, '700');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '94');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyQuantity);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyQuantity, '800');
await page.keyboard.press('Enter');
await page.reloadSection('entry.card.buy.index');
const secondBuyPackingPrice = await page.getValue(selectors.entryBuys.secondBuyPackingPrice);
const secondBuyGroupingPrice = await page.getValue(selectors.entryBuys.secondBuyGroupingPrice);
const secondBuyPrice = await page.getValue(selectors.entryBuys.secondBuyPrice);
const secondBuyGrouping = await page.getValue(selectors.entryBuys.secondBuyGrouping);
const secondBuyPacking = await page.getValue(selectors.entryBuys.secondBuyPacking);
const secondBuyWeight = await page.getValue(selectors.entryBuys.secondBuyWeight);
const secondBuyStickers = await page.getValue(selectors.entryBuys.secondBuyStickers);
const secondBuyPackage = await page.getValue(selectors.entryBuys.secondBuyPackage);
const secondBuyQuantity = await page.getValue(selectors.entryBuys.secondBuyQuantity);
expect(secondBuyPackingPrice).toEqual('100');
expect(secondBuyGroupingPrice).toEqual('200');
expect(secondBuyPrice).toEqual('300');
expect(secondBuyGrouping).toEqual('400');
expect(secondBuyPacking).toEqual('500');
expect(secondBuyWeight).toEqual('600');
expect(secondBuyStickers).toEqual('700');
expect(secondBuyPackage).toEqual('94');
expect(secondBuyQuantity).toEqual('800');
});
});

View File

@ -8,7 +8,7 @@ describe('Account create and basic data path', () => {
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('developer', 'account');
await page.loginAndModule('itManagement', 'account');
});
afterAll(async() => {

View File

@ -81,6 +81,6 @@ describe('Account Role create and basic data path', () => {
await page.accessToSection('account.role.card.inherited');
const rolesCount = await page.countElement(selectors.accountRoleInheritance.anyResult);
expect(rolesCount).toEqual(6);
expect(rolesCount).toEqual(7);
});
});

View File

@ -4,8 +4,8 @@ vn-avatar {
display: block;
border-radius: 50%;
overflow: hidden;
height: 36px;
width: 36px;
height: 38px;
width: 38px;
font-size: 22px;
background-color: $color-main;
position: relative;

View File

@ -40,6 +40,8 @@ export default class SmartTable extends Component {
this._options = options;
if (!options) return;
options.defaultSearch = true;
if (options.defaultSearch)
this.displaySearch();

View File

@ -59,12 +59,13 @@ export default class Auth {
password: password || undefined
};
const now = new Date();
return this.$http.post('VnUsers/signIn', params)
.then(json => this.onLoginOk(json, remember));
.then(json => this.onLoginOk(json, now, remember));
}
onLoginOk(json, remember) {
this.vnToken.set(json.data.token, remember);
onLoginOk(json, now, remember) {
this.vnToken.set(json.data.token, now, json.data.ttl, remember);
return this.loadAcls().then(() => {
let continueHash = this.$state.params.continue;

View File

@ -11,3 +11,4 @@ import './report';
import './email';
import './file';
import './date';

View File

@ -1,11 +1,16 @@
import ngModule from '../module';
import HttpError from 'core/lib/http-error';
interceptor.$inject = ['$q', 'vnApp', 'vnToken', '$translate'];
function interceptor($q, vnApp, vnToken, $translate) {
interceptor.$inject = ['$q', 'vnApp', '$translate'];
function interceptor($q, vnApp, $translate) {
let apiPath = 'api/';
let token = sessionStorage.getItem('vnToken')
?? localStorage.getItem('vnToken');
return {
setToken(newToken) {
token = newToken;
},
setApiPath(path) {
apiPath = path;
},
@ -14,8 +19,8 @@ function interceptor($q, vnApp, vnToken, $translate) {
if (config.url.charAt(0) !== '/' && apiPath)
config.url = `${apiPath}${config.url}`;
if (vnToken.token)
config.headers.Authorization = vnToken.token;
if (token)
config.headers.Authorization = token;
if ($translate.use())
config.headers['Accept-Language'] = $translate.use();
if (config.filter) {

View File

@ -6,29 +6,118 @@ import ngModule from '../module';
* @property {String} token The current login token or %null
*/
export default class Token {
constructor() {
constructor(vnInterceptor, $http, $rootScope) {
Object.assign(this, {
vnInterceptor,
$http,
$rootScope
});
try {
this.token = sessionStorage.getItem('vnToken');
if (!this.token)
this.token = localStorage.getItem('vnToken');
this.getStorage(sessionStorage);
this.remember = true;
if (!this.token) {
this.getStorage(localStorage);
this.remember = false;
}
} catch (e) {}
}
set(value, remember) {
set(token, created, ttl, remember) {
this.unset();
Object.assign(this, {
token,
created,
ttl,
remember
});
this.vnInterceptor.setToken(token);
try {
if (remember)
localStorage.setItem('vnToken', value);
this.setStorage(localStorage, token, created, ttl);
else
sessionStorage.setItem('vnToken', value);
} catch (e) {}
this.token = value;
this.setStorage(sessionStorage, token, created, ttl);
} catch (err) {
console.error(err);
}
}
unset() {
localStorage.removeItem('vnToken');
sessionStorage.removeItem('vnToken');
this.token = null;
this.created = null;
this.ttl = null;
this.remember = null;
this.vnInterceptor.setToken(null);
this.removeStorage(localStorage);
this.removeStorage(sessionStorage);
}
getStorage(storage) {
this.token = storage.getItem('vnToken');
if (!this.token) return;
const created = storage.getItem('vnTokenCreated');
this.created = created && new Date(created);
this.renewPeriod = storage.getItem('vnTokenRenewPeriod');
}
setStorage(storage, token, created, ttl) {
storage.setItem('vnToken', token);
storage.setItem('vnTokenCreated', created.toJSON());
storage.setItem('vnTokenTtl', ttl);
}
removeStorage(storage) {
storage.removeItem('vnToken');
storage.removeItem('vnTokenCreated');
storage.removeItem('vnTokenTtl');
}
fetchConfig() {
const filter = {fields: ['renewInterval', 'renewPeriod']};
this.$http.get('AccessTokenConfigs/findOne', {filter}).then(res => {
const data = res.data;
if (!data) return;
this.renewPeriod = data.renewPeriod;
this.stopRenewer();
this.inservalId = setInterval(() => this.checkValidity(), data.renewInterval * 1000);
this.checkValidity();
});
}
checkValidity() {
if (this.checking || !this.created) return;
this.checking = true;
const renewPeriod = Math.min(this.ttl, this.renewPeriod) * 1000;
const maxDate = this.created.getTime() + renewPeriod;
const now = new Date();
if (now.getTime() <= maxDate) {
this.checking = false;
return;
}
this.$http.post('VnUsers/renewToken')
.then(res => {
const token = res.data;
this.set(token.id, now, token.ttl, this.remember);
})
.catch(res => {
if (res.data?.error?.code !== 'periodNotExceeded')
throw res;
})
.finally(() => {
this.checking = false;
});
}
stopRenewer() {
clearInterval(this.inservalId);
}
}
Token.$inject = ['vnInterceptor', '$http', '$rootScope'];
ngModule.service('vnToken', Token);

View File

@ -15,7 +15,7 @@ export default class Controller {
}
$onInit() {
if (!this.$state.params || !this.$state.params.id || !this.$state.params.token)
if (!this.$state.params.id)
this.$state.go('login');
this.$http.get('UserPasswords/findOne')
@ -25,7 +25,7 @@ export default class Controller {
}
submit() {
const id = this.$state.params.id;
const userId = this.$state.params.userId;
const newPassword = this.newPassword;
const oldPassword = this.oldPassword;
@ -35,12 +35,12 @@ export default class Controller {
throw new UserError(`Passwords don't match`);
const headers = {
Authorization: this.$state.params.token
Authorization: this.$state.params.id
};
this.$http.post('VnUsers/change-password',
{
id,
id: userId,
oldPassword,
newPassword
},

View File

@ -42,7 +42,7 @@
<button class="buttonAccount">
<img
id="user"
ng-src="{{$ctrl.getImageUrl()}}"
ng-src="{{::$ctrl.getImageUrl()}}"
ng-click="userPopover.show($event)"
translate-attr="{title: 'Account'}"
on-error-src/>
@ -93,4 +93,4 @@
</vn-list>
</vn-portal>
<ui-view class="main-view"></ui-view>
<vn-scroll-up></vn-scroll-up>
<vn-scroll-up></vn-scroll-up>

View File

@ -30,6 +30,10 @@ export class Layout extends Component {
refresh() {
window.location.reload();
}
$onDestroy() {
this.vnToken.stopRenewer();
}
}
Layout.$inject = ['$element', '$scope', 'vnModules'];

View File

@ -2,8 +2,6 @@
vn-id="model"
url="{{$ctrl.url}}"
filter="$ctrl.filter"
link="{originFk: $ctrl.originId}"
where="{changedModel: $ctrl.changedModel, changedModelId: $ctrl.changedModelId}"
data="$ctrl.logs"
order="creationDate DESC, id DESC"
limit="20">
@ -17,90 +15,110 @@
<vn-data-viewer
model="model"
class="vn-w-sm vn-px-sm vn-pb-xl">
<div class="change vn-mb-sm" ng-repeat="log in $ctrl.logs">
<div class="left">
<vn-avatar class="vn-mt-xs"
ng-class="::{system: !log.user}"
val="{{::log.user ? log.user.nickname : $ctrl.$t('System')}}"
ng-click="$ctrl.showWorkerDescriptor($event, log)">
<img
ng-if="::log.user.image"
ng-src="/api/Images/user/160x160/{{::log.userFk}}/download?access_token={{::$ctrl.vnToken.token}}">
</img>
</vn-avatar>
<div class="arrow bg-panel"></div>
<div class="origin-log" ng-repeat="originLog in $ctrl.logTree">
<div class="origin-info vn-mb-md" ng-if="::$ctrl.logTree.length > 1">
<h6 class="origin-id">
{{::$ctrl.modelI18n}} #{{::originLog.originFk}}
</h6>
<div class="line"></div>
</div>
<vn-card class="detail">
<div class="header vn-pa-sm">
<div class="action-model">
<span class="model-name"
ng-if="::$ctrl.showModelName && log.changedModel"
ng-style="::{backgroundColor: $ctrl.hashToColor(log.changedModel)}"
title="{{::log.changedModel}}">
{{::log.changedModelI18n}}
</span>
</div>
<div
class="action-date text-secondary text-caption vn-ml-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
{{::$ctrl.relativeDate(log.creationDate)}}
<vn-icon
class="action vn-ml-xs"
ng-class="::$ctrl.actionsClass[log.action]"
icon="{{::$ctrl.actionsIcon[log.action]}}"
translate-attr="::{title: $ctrl.actionsText[log.action]}">
</vn-icon>
<div class="user-log vn-mb-sm" ng-repeat="userLog in ::originLog.logs">
<div class="timeline">
<div class="user-avatar">
<vn-avatar
ng-class="::{system: !userLog.user}"
val="{{::userLog.user ? userLog.user.nickname : $ctrl.$t('System')}}"
ng-click="$ctrl.showWorkerDescriptor($event, userLog)">
<img
ng-if="::userLog.user.image"
ng-src="/api/Images/user/160x160/{{::userLog.userFk}}/download?access_token={{::$ctrl.vnToken.token}}">
</img>
</vn-avatar>
</div>
<div class="arrow bg-panel" ng-if="::$ctrl.byRecord"></div>
<div class="line"></div>
</div>
<div class="model vn-pb-sm vn-px-sm"
ng-if="::$ctrl.showModelName">
<span class="model-id" ng-if="::log.changedModelId">#{{::log.changedModelId}}</span>
<vn-icon
icon="filter_alt"
translate-attr="{title: 'Show all record changes'}"
ng-click="$ctrl.filterByEntity(log)">
</vn-icon>
<span class="model-value" title="{{::log.changedModelValue}}">{{::log.changedModelValue}}</span>
</div>
<div class="changes vn-pa-sm"
ng-class="{expanded: log.expand}"
ng-if="::log.props.length || log.description">
<vn-icon
icon="expand_more"
translate-attr="{title: 'Details'}"
ng-click="log.expand = !log.expand">
</vn-icon>
<span ng-if="::log.props.length"
class="attributes">
<span ng-if="!log.expand" ng-repeat="prop in ::log.props"
class="basic-json">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
<div class="user-changes">
<div class="model-log" ng-repeat="modelLog in ::userLog.logs">
<div class="model-info vn-my-sm" ng-if="::!$ctrl.byRecord">
<vn-icon
icon="filter_alt"
translate-attr="{title: 'Show all record changes'}"
ng-click="$ctrl.filterByRecord(modelLog)">
</vn-icon>
<span class="model-name"
ng-if="::$ctrl.showModelName && modelLog.model"
ng-style="::{backgroundColor: $ctrl.hashToColor(modelLog.model)}"
title="{{::modelLog.model}}">
{{::modelLog.modelI18n}}
</span>
<vn-json-value value="::prop.val.val"></vn-json-value><span ng-if="::!$last">,</span>
</span>
<div ng-if="log.expand" class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-log-value val="::prop.val"></vn-log-value>
<span ng-if="::log.action == 'update'">
<vn-log-value val="::prop.old"></vn-log-value>
</span>
</div>
<span class="model-id" ng-if="::modelLog.id">#{{::modelLog.id}}</span>
<span class="model-value" title="{{::modelLog.showValue}}">{{::modelLog.showValue}}</span>
</div>
</span>
<span ng-if="::!log.props.length" class="description">
{{::log.description}}
</span>
</vn-card>
<vn-card class="changes-log vn-mb-xs" ng-repeat="log in ::modelLog.logs">
<div class="change-info vn-pa-sm">
<div
class="date text-secondary text-caption vn-mr-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
{{::$ctrl.relativeDate(log.creationDate)}}
</div>
<div>
<vn-icon
class="pit vn-ml-xs"
icon="preview"
translate-attr="::{title: 'View record at this point in time'}"
ng-show="::log.action != 'insert'"
ng-click="$ctrl.viewPitInstance($event, log.id, modelLog)">
</vn-icon>
<vn-icon
class="action vn-ml-xs"
ng-class="::$ctrl.actionsClass[log.action]"
icon="{{::$ctrl.actionsIcon[log.action]}}"
translate-attr="::{title: $ctrl.actionsText[log.action]}">
</vn-icon>
</div>
</div>
<div class="change-detail vn-pa-sm"
ng-class="{expanded: log.expand}"
ng-if="::log.props.length || log.description">
<vn-icon
icon="expand_more"
translate-attr="{title: 'Details'}"
ng-click="log.expand = !log.expand">
</vn-icon>
<span ng-if="::log.props.length"
class="attributes">
<span ng-if="!log.expand" ng-repeat="prop in ::log.props"
class="basic-json">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::prop.val.val"></vn-json-value><span ng-if="::!$last">,</span>
</span>
<div ng-if="log.expand" class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-log-value val="::prop.val"></vn-log-value>
<span ng-if="::log.action == 'update'">
<vn-log-value val="::prop.old"></vn-log-value>
</span>
</div>
</div>
</span>
<span ng-if="::!log.props.length" class="description">
{{::log.description}}
</span>
</vn-card>
</div>
</div>
</div>
</div>
</div>
</vn-data-viewer>
<vn-float-button
ng-if="model.userFilter"
ng-if="$ctrl.hasFilter"
icon="filter_alt_off"
translate-attr="{title: 'Quit filter'}"
ng-click="$ctrl.resetFilter()"
@ -212,5 +230,33 @@
</vn-date-picker>
</form>
</vn-side-menu>
<vn-worker-descriptor-popover vn-id="workerDescriptor">
<vn-popover vn-id="instance-popover">
<tpl-body class="vn-log-instance">
<vn-spinner
ng-if="$ctrl.instance.canceler"
class="loading vn-pa-sm"
enable="true">
</vn-spinner>
<div
ng-if="!$ctrl.instance.canceler" class="instance">
<h6 class="header vn-pa-sm">
{{$ctrl.instance.modelLog.modelI18n}} #{{$ctrl.instance.modelLog.id}}
</h6>
<div class="change-detail vn-pa-sm">
<div ng-if="$ctrl.instance.props"
ng-repeat="prop in $ctrl.instance.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-log-value val="::prop.val"></vn-log-value>
</div>
<div ng-if="!$ctrl.instance.props" translate>
No data
</div>
</div>
</div>
</tpl-body>
</vn-popover>
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>

View File

@ -3,7 +3,10 @@ import Section from '../section';
import {hashToColor} from 'core/lib/string';
import './style.scss';
const validDate = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/;
const validDate = new RegExp(
/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])/.source
+ /T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/.source
);
export default class Controller extends Section {
constructor($element, $) {
@ -28,6 +31,20 @@ export default class Controller extends Section {
select: 'visibility'
};
this.filter = {
fields: [
'id',
'originFk',
'userFk',
'action',
'changedModel',
'oldInstance',
'newInstance',
'creationDate',
'changedModel',
'changedModelId',
'changedModelValue',
'description'
],
include: [{
relation: 'user',
scope: {
@ -48,6 +65,11 @@ export default class Controller extends Section {
this.today.setHours(0, 0, 0, 0);
}
$onInit() {
const match = this.url?.match(/(.*)Logs$/);
this.modelI18n = match && this.translateModel(match[1]);
}
$postLink() {
this.resetFilter();
this.$.$watch(
@ -63,47 +85,75 @@ export default class Controller extends Section {
set logs(value) {
this._logs = value;
this.logTree = [];
if (!value) return;
const empty = {};
const validations = window.validations;
const castJsonValue = this.castJsonValue;
for (const log of value) {
let originLog;
let userLog;
let modelLog;
let nLogs;
for (let i = 0; i < value.length; i++) {
const log = value[i];
const prevLog = i > 0 ? value[i - 1] : null;
const locale = validations[log.changedModel]?.locale || empty;
// Origin
const originChanged = !prevLog
|| log.originFk != prevLog.originFk;
if (originChanged) {
this.logTree.push(originLog = {
originFk: log.originFk,
logs: []
});
}
// User
const userChanged = originChanged
|| log.userFk != prevLog.userFk;
if (userChanged) {
originLog.logs.push(userLog = {
user: log.user,
userFk: log.userFk,
logs: []
});
}
// Model
const modelChanged = userChanged
|| log.changedModel != prevLog.changedModel
|| log.changedModelId != prevLog.changedModelId
|| nLogs >= 6;
if (modelChanged) {
userLog.logs.push(modelLog = {
model: log.changedModel,
modelI18n: firstUpper(locale.name) || log.changedModel,
id: log.changedModelId,
showValue: log.changedModelValue,
logs: []
});
nLogs = 0;
}
nLogs++;
modelLog.logs.push(log);
// Changes
const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || empty;
const vals = (notDelete ? log.newInstance : log.oldInstance) || empty;
const locale = validations[log.changedModel]?.locale || empty;
log.changedModelI18n = firstUpper(locale.name) || log.changedModel;
let props = Object.keys(olds).concat(Object.keys(vals));
props = [...new Set(props)];
let propNames = Object.keys(olds).concat(Object.keys(vals));
propNames = [...new Set(propNames)];
log.props = [];
for (const prop of props) {
if (prop.endsWith('$')) continue;
log.props.push({
name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop,
old: getVal(olds, prop),
val: getVal(vals, prop)
});
}
log.props.sort(
(a, b) => a.nameI18n.localeCompare(b.nameI18n));
}
function getVal(vals, prop) {
let val, id;
const showProp = `${prop}$`;
if (vals[showProp] != null) {
val = vals[showProp];
id = vals[prop];
} else
val = vals[prop];
return {val: castJsonValue(val), id};
log.props = this.parseProps(propNames, locale, vals, olds);
}
}
@ -114,17 +164,76 @@ export default class Controller extends Section {
set models(value) {
this._models = value;
if (!value) return;
for (const model of value) {
const name = model.changedModel;
model.changedModelI18n =
firstUpper(window.validations[name]?.locale?.name) || name;
}
for (const model of value)
model.changedModelI18n = this.translateModel(model.changedModel);
}
get showModelName() {
return !(this.changedModel && this.changedModelId);
}
parseProps(propNames, locale, vals, olds) {
const castJsonValue = this.castJsonValue;
const props = [];
for (const prop of propNames) {
if (prop.endsWith('$')) continue;
props.push({
name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop,
val: getVal(vals, prop),
old: olds && getVal(olds, prop)
});
}
props.sort(
(a, b) => a.nameI18n.localeCompare(b.nameI18n));
function getVal(vals, prop) {
let val; let id;
const showProp = `${prop}$`;
if (vals[showProp] != null) {
val = vals[showProp];
id = vals[prop];
} else
val = vals[prop];
return {val: castJsonValue(val), id};
}
return props;
}
viewPitInstance(event, id, modelLog) {
if (this.instance?.canceler)
this.instance.canceler.resolve();
const canceler = this.$q.defer();
this.instance = {
modelLog,
canceler
};
const options = {timeout: canceler.promise};
this.$http.get(`${this.url}/${id}/pitInstance`, options)
.then(res => {
const instance = res.data;
const propNames = Object.keys(instance);
const locale = window.validations[modelLog.model]?.locale || {};
this.instance.props = this.parseProps(propNames, locale, instance);
})
.finally(() => {
this.instance.canceler = null;
this.$.$applyAsync(() => this.$.instancePopover.relocate());
});
this.$.instancePopover.show(event);
}
translateModel(name) {
return firstUpper(window.validations[name]?.locale?.name) || name;
}
castJsonValue(value) {
return typeof value === 'string' && validDate.test(value)
? new Date(value)
@ -160,12 +269,11 @@ export default class Controller extends Section {
applyFilter() {
const filter = this.$.filter;
function getParam(prop, value) {
const getParam = (prop, value) => {
if (value == null || value == '') return null;
switch (prop) {
case 'search':
const or = [];
if (/^\s*[0-9]+\s*$/.test(value))
if (/^\s*[0-9]+\s*$/.test(value) || this.byRecord)
return {changedModelId: value.trim()};
else
return {changedModelValue: {like: `%${value}%`}};
@ -177,72 +285,86 @@ export default class Controller extends Section {
]};
case 'who':
switch (value) {
case 'all':
return null;
case 'user':
return {userFk: {neq: null}};
case 'system':
return {userFk: null};
case 'all':
default:
return null;
}
case 'actions':
case 'actions': {
const inq = [];
for (const action in value) {
if (value[action])
inq.push(action);
}
return inq.length ? {action: {inq}} : null;
}
case 'from':
if (filter.to) {
if (filter.to)
return {creationDate: {gte: value}};
} else {
else {
const to = new Date(value);
to.setHours(23, 59, 59, 999);
return {creationDate: {between: [value, to]}};
}
case 'to':
case 'to': {
const to = new Date(value);
to.setHours(23, 59, 59, 999);
return {creationDate: {lte: to}};
}
case 'userFk':
return filter.who != 'system'
? {[prop]: value} : null;
default:
return {[prop]: value};
}
}
};
this.hasFilter = false;
const and = [];
if (!filter.search || !filter.changedModel)
this.byRecord = false;
if (!this.byRecord)
and.push({originFk: this.originId});
for (const prop in filter) {
const param = getParam(prop, filter[prop]);
if (param) and.push(param);
if (param) {
and.push(param);
this.hasFilter = true;
}
}
const lbFilter = and.length ? {where: {and}} : null;
return this.$.model.applyFilter(lbFilter);
}
filterByEntity(log) {
filterByRecord(modelLog) {
this.byRecord = true;
this.$.filter = {
who: 'all',
search: log.changedModelId,
changedModel: log.changedModel
search: modelLog.id,
changedModel: modelLog.model
};
}
searchUser(search) {
if (/^[0-9]+$/.test(search)) {
if (/^[0-9]+$/.test(search))
return {id: search};
} else {
else {
return {or: [
{name: search},
{nickname: {like: `%${search}%`}}
]}
]};
}
}
showWorkerDescriptor(event, log) {
if (log.user?.worker)
this.$.workerDescriptor.show(event.target, log.userFk);
showWorkerDescriptor(event, userLog) {
if (userLog.user?.worker)
this.$.workerDescriptor.show(event.target, userLog.userFk);
}
}

View File

@ -24,4 +24,5 @@ Changes: Cambios
today: hoy
yesterday: ayer
Show all record changes: Mostrar todos los cambios realizados en el registro
View record at this point in time: Ver el registro en este punto
Quit filter: Quitar filtro

View File

@ -1,167 +1,246 @@
@import "util";
vn-log {
.change {
.origin-log {
&:first-child > .origin-info {
margin-top: 0;
}
& > .origin-info {
display: flex;
align-items: center;
margin-top: 28px;
gap: 6px;
& > .origin-id {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: $color-font-secondary;
margin: 0;
}
& > .line {
flex-grow: 1;
background-color: $color-font-secondary;
height: 2px;
}
}
}
.user-log {
display: flex;
& > .left {
& > .timeline {
position: relative;
padding-right: 10px;
width: 38px;
min-width: 38px;
flex-grow: auto;
& > vn-avatar {
cursor: pointer;
&.system {
background-color: $color-main !important;
}
}
& > .arrow {
height: 8px;
width: 8px;
position: absolute;
transform: rotateY(0deg) rotate(45deg);
top: 18px;
top: 15px;
right: -4px;
z-index: 1;
}
& > .user-avatar {
background-color: $color-bg;
padding: $spacing-sm 0;
margin-top: -$spacing-sm;
position: sticky;
top: 64px;
& > vn-avatar {
cursor: pointer;
display: block;
&.system {
background-color: $color-main !important;
}
}
}
& > .line {
position: absolute;
background-color: $color-main;
width: 2px;
left: 17px;
left: 18px;
z-index: -1;
top: 44px;
bottom: -8px;
top: 0;
bottom: -$spacing-sm;
}
}
&:last-child > .left > .line {
&:last-child > .timeline > .line {
display: none;
}
.detail {
position: relative;
& > .user-changes {
flex-grow: 1;
width: 100%;
border-radius: 2px;
overflow: hidden;
}
}
.model-log {
& > .model-info {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-height: 22px;
& > .header {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
& > .model-name {
display: inline-block;
padding: 2px 5px;
color: $color-font-dark;
border-radius: 8px;
vertical-align: middle;
}
& > .model-value {
font-style: italic;
}
& > .model-id {
color: $color-font-secondary;
font-size: .9rem;
}
& > vn-icon[icon="filter_alt"] {
@extend %clickable-light;
vertical-align: middle;
font-size: 18px;
color: $color-font-secondary;
float: right;
display: none;
& > .action-model {
display: inline-flex;
overflow: hidden;
& > .model-name {
display: inline-block;
padding: 2px 5px;
color: $color-font-dark;
border-radius: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
& > .action-date {
white-space: nowrap;
& > .action {
display: inline-flex;
align-items: center;
justify-content: center;
color: $color-font-bg;
vertical-align: middle;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 18px;
&.notice {
background-color: $color-notice-medium
}
&.success {
background-color: $color-success-medium;
}
&.warning {
background-color: $color-main-medium;
}
&.alert {
background-color: lighten($color-alert, 5%);
}
}
@include mobile {
display: initial;
}
}
& > .model {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-height: 18px;
}
&:hover > .model-info > vn-icon[icon="filter_alt"] {
display: initial;
}
}
.changes-log {
position: relative;
max-width: 100%;
width: 100%;
border-radius: 2px;
overflow: hidden;
& > vn-icon {
&:last-child {
margin-bottom: 0;
}
& > .change-info {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
& > .date {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& > div {
white-space: nowrap;
& > vn-icon.pit {
@extend %clickable-light;
vertical-align: middle;
padding: 2px;
margin: -2px;
font-size: 18px;
font-size: 20px;
color: $color-font-secondary;
float: right;
display: none;
@include mobile {
display: initial;
display: inline-block;
}
}
& > .model-value {
font-style: italic;
}
& > .model-id {
color: $color-font-secondary;
font-size: .9rem;
}
}
&:hover > .model > vn-icon {
display: initial;
}
}
}
.changes {
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
color: $color-font-light;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 34px;
box-sizing: border-box;
& > .action {
display: inline-flex;
align-items: center;
justify-content: center;
color: $color-font-bg;
vertical-align: middle;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 18px;
& > vn-icon {
@extend %clickable;
float: right;
position: relative;
transition-property: transform, background-color;
transition-duration: 150ms;
margin: -5px;
margin-left: 4px;
padding: 1px;
border-radius: 50%;
&.notice {
background-color: $color-notice-medium
}
&.success {
background-color: $color-success-medium;
}
&.warning {
background-color: $color-main-medium;
}
&.alert {
background-color: lighten($color-alert, 5%);
}
}
}
&:hover vn-icon.pit {
display: inline-block;
}
}
&.expanded {
text-overflow: initial;
white-space: initial;
& > .change-detail {
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
color: $color-font-light;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 34px;
box-sizing: border-box;
& > vn-icon {
transform: rotate(180deg);
@extend %clickable;
float: right;
position: relative;
transition-property: transform, background-color;
transition-duration: 150ms;
margin: -5px;
margin-left: 4px;
padding: 1px;
border-radius: 50%;
}
&.expanded {
text-overflow: initial;
white-space: initial;
& > vn-icon {
transform: rotate(180deg);
}
}
& > .no-changes {
font-style: italic;
}
}
& > .no-changes {
font-style: italic;
}
.id-value {
font-size: .9rem;
color: $color-font-secondary;
}
}
.vn-log-instance {
display: block;
& > .loading {
display: flex;
justify-content: center;
}
& > .instance {
min-width: 180px;
max-width: 400px;
& > .header {
background-color: $color-main;
color: $color-font-dark;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0;
}
& > .change-detail {
color: $color-font-light;
}
}
}
vn-log-value > .id-value {
font-size: .9rem;
color: $color-font-secondary;
}

View File

@ -27,10 +27,9 @@ export default class Controller {
this.loading = false;
this.password = '';
this.focusUser();
if (req?.data?.error?.code == 'passExpired') {
const [args] = req.data.error.translateArgs;
this.$state.go('change-password', args);
}
const err = req.data?.error;
if (err?.code == 'passExpired')
this.$state.go('change-password', err.details.token);
throw req;
});

View File

@ -11,7 +11,8 @@ function config($stateProvider, $urlRouterProvider) {
abstract: true,
template: '<vn-layout></vn-layout>',
resolve: {
config: ['vnConfig', vnConfig => vnConfig.initialize()]
config: ['vnConfig', vnConfig => vnConfig.initialize()],
token: ['vnToken', vnToken => vnToken.fetchConfig()]
}
})
.state('outLayout', {
@ -38,7 +39,7 @@ function config($stateProvider, $urlRouterProvider) {
})
.state('change-password', {
parent: 'outLayout',
url: '/change-password?id&token',
url: '/change-password?id&userId',
description: 'Change password',
template: '<vn-change-password></vn-change-password>'
})

View File

@ -0,0 +1,91 @@
const NotFoundError = require('vn-loopback/util/not-found-error');
module.exports = Self => {
Self.remoteMethod('pitInstance', {
description: 'Gets the status of instance at specific point in time',
accepts: [
{
arg: 'id',
type: 'integer',
description: 'The log id',
required: true
}
],
returns: {
type: [Self],
root: true
},
http: {
path: `/:id/pitInstance`,
verb: 'GET'
}
});
Self.pitInstance = async function(id) {
const log = await Self.findById(id, {
fields: [
'changedModel',
'changedModelId',
'creationDate'
]
});
if (!log)
throw new NotFoundError();
const where = {
changedModel: log.changedModel,
changedModelId: log.changedModelId
};
// Fetch creation and all update logs for record up to requested log
const createdWhere = {
action: 'insert',
creationDate: {lte: log.creationDate}
};
const createdLog = await Self.findOne({
fields: ['id', 'creationDate', 'newInstance'],
where: Object.assign(createdWhere, where),
order: 'creationDate DESC, id DESC'
});
const instance = {};
let logsWhere = {
action: 'update'
};
if (createdLog) {
Object.assign(instance, createdLog.newInstance);
Object.assign(logsWhere, {
creationDate: {between: [
createdLog.creationDate,
log.creationDate
]},
id: {between: [
Math.min(id, createdLog.id),
Math.max(id, createdLog.id)
]}
});
} else {
Object.assign(logsWhere, {
creationDate: {lte: log.creationDate},
id: {lte: id}
});
}
const logs = await Self.find({
fields: ['newInstance'],
where: Object.assign(logsWhere, where),
order: 'creationDate, id'
});
if (!logs.length && !createdLog)
throw new NotFoundError('No logs found for record');
// Merge all logs in order into one instance
for (const log of logs)
Object.assign(instance, log.newInstance);
return instance;
};
};

View File

@ -5,6 +5,7 @@ module.exports = function(Self) {
Self.super_.setup.call(this);
require('../methods/log/editors')(this);
require('../methods/log/models')(this);
require('../methods/log/pitInstance')(this);
}
});
};

View File

@ -200,22 +200,20 @@ module.exports = function(Self) {
const connector = this.dataSource.connector;
let conn;
let res;
const opts = Object.assign({}, options);
try {
if (userId) {
conn = await new Promise((resolve, reject) => {
connector.client.getConnection(function(err, conn) {
if (err)
reject(err);
else
resolve(conn);
if (!options.transaction) {
options = Object.assign({}, options);
conn = await new Promise((resolve, reject) => {
connector.client.getConnection(function(err, conn) {
if (err)
reject(err);
else
resolve(conn);
});
});
});
const opts = Object.assign({}, options);
if (!opts.transaction) {
opts.transaction = {
options.transaction = {
connection: conn,
connector
};
@ -223,15 +221,14 @@ module.exports = function(Self) {
await connector.executeP(
'CALL account.myUser_loginWithName((SELECT name FROM account.user WHERE id = ?))',
[userId], opts
[userId], options
);
}
res = await connector.executeP(query, params, opts);
res = await connector.executeP(query, params, options);
if (userId) {
await connector.executeP('CALL account.myUser_logout()', null, opts);
}
if (userId)
await connector.executeP('CALL account.myUser_logout()', null, options);
} finally {
if (conn) conn.release();
}

View File

@ -115,7 +115,7 @@
"This client is not invoiceable": "This client is not invoiceable",
"INACTIVE_PROVIDER": "Inactive provider",
"reference duplicated": "reference duplicated",
"The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option",
"The PDF document does not exist": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option",
"This item is not available": "This item is not available",
"Deny buy request": "Purchase request for ticket id [{{ticketId}}]({{{url}}}) has been rejected. Reason: {{observation}}",
"The type of business must be filled in basic data": "The type of business must be filled in basic data",
@ -175,5 +175,7 @@
"Pass expired": "The password has expired, change it from Salix",
"Can't transfer claimed sales": "Can't transfer claimed sales",
"Invalid quantity": "Invalid quantity",
"Failed to upload delivery note": "Error to upload delivery note {{id}}"
"Failed to upload delivery note": "Error to upload delivery note {{id}}",
"Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address",
"The renew period has not been exceeded": "The renew period has not been exceeded"
}

View File

@ -177,7 +177,6 @@
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria",
"Action not allowed on the test environment": "Esta acción no está permitida en el entorno de pruebas",
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",
"Sorts whole route": "Reordena ruta entera",
"New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día *{{shipped}}*, con una cantidad de *{{quantity}}* y un precio de *{{price}} €*",
"New ticket request has been created": "Se ha creado una nueva petición de compra '{{description}}' para el día *{{shipped}}*, con una cantidad de *{{quantity}}*",
"Swift / BIC cannot be empty": "Swift / BIC no puede estar vacío",
@ -211,7 +210,7 @@
"You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito",
"You can't change the credit set to zero from a financialBoss": "No puedes cambiar el cŕedito establecido a cero por un jefe de finanzas",
"Amounts do not match": "Las cantidades no coinciden",
"The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'",
"The PDF document does not exist": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'",
"The type of business must be filled in basic data": "El tipo de negocio debe estar rellenado en datos básicos",
"You can't create a claim from a ticket delivered more than seven days ago": "No puedes crear una reclamación de un ticket entregado hace más de siete días",
"The worker has hours recorded that day": "El trabajador tiene horas fichadas ese día",
@ -265,7 +264,7 @@
"It is not possible to modify cloned sales": "No es posible modificar líneas de pedido clonadas",
"A supplier with the same name already exists. Change the country.": "Un proveedor con el mismo nombre ya existe. Cambie el país.",
"There is no assigned email for this client": "No hay correo asignado para este cliente",
"Exists an invoice with a previous date": "Existe una factura con fecha anterior",
"Exists an invoice with a future date": "Existe una factura con fecha posterior",
"Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite",
"Warehouse inventory not set": "El almacén inventario no está establecido",
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
@ -293,5 +292,11 @@
"Pass expired": "La contraseña ha caducado, cambiela desde Salix",
"Invalid NIF for VIES": "Invalid NIF for VIES",
"Ticket does not exist": "Este ticket no existe",
"Ticket is already signed": "Este ticket ya ha sido firmado"
"Ticket is already signed": "Este ticket ya ha sido firmado",
"You can only add negative amounts in refund tickets": "Solo se puede añadir cantidades negativas en tickets abono",
"Fecha fuera de rango": "Fecha fuera de rango",
"Error while generating PDF": "Error al generar PDF",
"Error when sending mail to client": "Error al enviar el correo al cliente",
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado"
}

View File

@ -0,0 +1,3 @@
name: account
columns:
id: id

View File

@ -0,0 +1,3 @@
name: cuenta
columns:
id: id

View File

@ -0,0 +1,5 @@
name: mail alias
columns:
id: id
mailAlias: alias
account: account

View File

@ -0,0 +1,5 @@
name: alias de correo
columns:
id: id
mailAlias: alias
account: cuenta

View File

@ -5,8 +5,7 @@ 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(`Accounts/syncAll`)
.then(() => this.vnApp.showSuccess(this.$t('Users synchronized!')));
this.$http.patch(`Accounts/syncAll`);
}
onUserSync() {

View File

@ -17,7 +17,9 @@
<vn-icon-button
icon="delete"
translate-attr="{title: 'Unsubscribe'}"
ng-click="removeConfirm.show(row)">
ng-click="removeConfirm.show(row)"
vn-acl="itManagement"
vn-acl-action="remove">
</vn-icon-button>
</vn-item-section>
</vn-item>
@ -30,9 +32,11 @@
translate-attr="{title: 'Add'}"
vn-bind="+"
ng-click="$ctrl.onAddClick()"
fixed-bottom-right>
fixed-bottom-right
vn-acl="itManagement"
vn-acl-action="remove">
</vn-float-button>
<vn-dialog
<vn-dialog
vn-id="dialog"
on-accept="$ctrl.onAddSave()">
<tpl-body>
@ -49,7 +53,7 @@
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>
<vn-confirm
<vn-confirm
vn-id="removeConfirm"
message="User will be removed from alias"
question="Are you sure you want to continue?"

View File

@ -5,6 +5,7 @@ import './style.scss';
class Controller extends ModuleCard {
reload() {
const filter = {
where: {id: this.$params.id},
include: {
relation: 'role',
scope: {
@ -14,8 +15,11 @@ class Controller extends ModuleCard {
};
return Promise.all([
this.$http.get(`VnUsers/${this.$params.id}`, {filter})
.then(res => this.user = res.data),
this.$http.get(`VnUsers/preview`, {filter})
.then(res => {
const [user] = res.data;
this.user = user;
}),
this.$http.get(`Accounts/${this.$params.id}/exists`)
.then(res => this.hasAccount = res.data.exists)
]);

View File

@ -15,12 +15,12 @@ describe('component vnUserCard', () => {
it('should reload the controller data', () => {
controller.$params.id = 1;
$httpBackend.expectGET('VnUsers/1').respond('foo');
$httpBackend.expectGET('VnUsers/preview').respond('foo');
$httpBackend.expectGET('Accounts/1/exists').respond({exists: true});
controller.reload();
$httpBackend.flush();
expect(controller.user).toBe('foo');
expect(controller.user).toBe('f');
expect(controller.hasAccount).toBeTruthy();
});
});

View File

@ -12,18 +12,18 @@
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-textfield
label="Name"
label="Name"
ng-model="$ctrl.user.name"
rule="VnUser"
vn-focus>
</vn-textfield>
<vn-textfield
label="Nickname"
label="Nickname"
ng-model="$ctrl.user.nickname"
rule="VnUser">
</vn-textfield>
<vn-textfield
label="Email"
label="Email"
ng-model="$ctrl.user.email"
rule="VnUser">
</vn-textfield>
@ -39,7 +39,7 @@
type="password">
</vn-textfield>
<vn-check
label="Active"
label="Active"
ng-model="$ctrl.user.active">
</vn-check>
</vn-vertical>

View File

@ -2,6 +2,11 @@ import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.user = {active: true};
}
onSubmit() {
return this.$.watcher.submit().then(res => {
this.$state.go('account.card.basicData', {id: res.data.id});

View File

@ -6,7 +6,7 @@
<vn-item
ng-click="deleteUser.show()"
name="deleteUser"
vn-acl="it"
vn-acl="itManagement"
vn-acl-action="remove"
translate>
Delete
@ -15,7 +15,7 @@
ng-if="::$root.user.id == $ctrl.id"
ng-click="$ctrl.onChangePassClick(true)"
name="changePassword"
vn-acl="hr"
vn-acl="sysadmin"
vn-acl-action="remove"
translate>
Change password
@ -23,7 +23,7 @@
<vn-item
ng-click="$ctrl.onChangePassClick(false)"
name="setPassword"
vn-acl="hr"
vn-acl="sysadmin"
vn-acl-action="remove"
translate>
Set password
@ -32,7 +32,7 @@
ng-if="!$ctrl.hasAccount"
ng-click="enableAccount.show()"
name="enableAccount"
vn-acl="it"
vn-acl="sysadmin"
vn-acl-action="remove"
translate>
Enable account
@ -41,7 +41,7 @@
ng-if="$ctrl.hasAccount"
ng-click="disableAccount.show()"
name="disableAccount"
vn-acl="it"
vn-acl="sysadmin"
vn-acl-action="remove"
translate>
Disable account
@ -50,7 +50,7 @@
ng-if="!$ctrl.user.active"
ng-click="activateUser.show()"
name="activateUser"
vn-acl="hr"
vn-acl="itManagement"
vn-acl-action="remove"
translate>
Activate user
@ -59,7 +59,7 @@
ng-if="$ctrl.user.active"
ng-click="deactivateUser.show()"
name="deactivateUser"
vn-acl="hr"
vn-acl="itManagement"
vn-acl-action="remove"
translate>
Deactivate user

View File

@ -14,11 +14,11 @@
<vn-item-section>
<h6>{{::user.nickname}}</h6>
<vn-label-value
label="Id"
label="Id"
value="{{::user.id}}">
</vn-label-value>
<vn-label-value
label="User"
label="User"
value="{{::user.name}}">
</vn-label-value>
</vn-item-section>
@ -36,12 +36,12 @@
<vn-popup vn-id="summary">
<vn-user-summary user="$ctrl.selectedUser"></vn-user-summary>
</vn-popup>
<a
<a
fixed-bottom-right
ui-sref="account.create"
vn-tooltip="New user"
vn-bind="+"
vn-acl="it"
vn-acl="itManagement"
vn-acl-action="remove">
<vn-float-button icon="add"></vn-float-button>
</a>
</a>

View File

@ -14,12 +14,12 @@
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-check
label="Enable mail forwarding"
label="Enable mail forwarding"
ng-model="watcher.hasData">
</vn-check>
<vn-textfield
ng-if="watcher.hasData"
label="Forward email"
label="Forward email"
ng-model="data.forwardTo"
info="All emails will be forwarded to the specified address."
rule="MailForward"

View File

@ -4,3 +4,4 @@ Enable mail forwarding: Habilitar redirección de correo
All emails will be forwarded to the specified address.: >
Todos los correos serán reenviados a la dirección especificada, no se
mantendrá copia de los mismos en el buzón del usuario.
You don't have enough privileges: No tienes suficientes permisos

View File

@ -1,6 +1,6 @@
<vn-crud-model
vn-id="model"
url="VnUsers"
url="VnUsers/preview"
filter="::$ctrl.filter"
limit="20">
</vn-crud-model>

View File

@ -1,9 +1,7 @@
<mg-ajax path="VnUsers/{{post.params.id}}/privileges" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
url="VnUsers"
data="$ctrl.user"
id-value="$ctrl.$params.id"
form="form"
save="post">
</vn-watcher>
@ -11,15 +9,16 @@
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg" vn-focus>
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-check
label="Has grant"
ng-model="$ctrl.user.hasGrant">
</vn-check>
</vn-vertical>
<vn-vertical
class="vn-mt-md">
</vn-card>
<vn-card class="vn-pa-lg vn-mt-md">
<vn-vertical>
<vn-autocomplete
label="Role"
ng-model="$ctrl.user.roleFk"

View File

@ -1,9 +1,21 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {}
export default class Controller extends Section {
get user() {
return this._user;
}
set user(value) {
this._user = value;
if (!value) return;
}
}
ngModule.component('vnUserPrivileges', {
template: require('./index.html'),
controller: Controller
controller: Controller,
bindings: {
user: '<'
}
});

View File

@ -49,15 +49,13 @@
"url": "/index?q",
"state": "account.index",
"component": "vn-user-index",
"description": "Users",
"acl": ["marketing", "hr"]
"description": "Users"
},
{
"url": "/create",
"state": "account.create",
"component": "vn-user-create",
"description": "New user",
"acl": ["it"]
"description": "New user"
},
{
"url": "/:id",
@ -80,7 +78,7 @@
"state": "account.card.basicData",
"component": "vn-user-basic-data",
"description": "Basic data",
"acl": ["hr"]
"acl": ["itManagement"]
},
{
"url" : "/log",
@ -98,8 +96,7 @@
"url": "/roles",
"state": "account.card.roles",
"component": "vn-user-roles",
"description": "Inherited roles",
"acl": ["it"]
"description": "Inherited roles"
},
{
"url": "/mail-forwarding",
@ -111,15 +108,16 @@
"url": "/aliases",
"state": "account.card.aliases",
"component": "vn-user-aliases",
"description": "Mail aliases",
"acl": ["marketing", "hr"]
"description": "Mail aliases"
},
{
"url": "/privileges",
"state": "account.card.privileges",
"component": "vn-user-privileges",
"description": "Privileges",
"acl": ["hr"]
"params": {
"user": "$ctrl.user"
}
},
{
"url": "/role?q",
@ -180,8 +178,7 @@
"url": "/alias?q",
"state": "account.alias",
"component": "vn-alias",
"description": "Mail aliases",
"acl": ["marketing"]
"description": "Mail aliases"
},
{
"url": "/create",

View File

@ -8,6 +8,7 @@ class Controller extends Summary {
if (!value) return;
const filter = {
where: {id: value.id},
include: {
relation: 'role',
scope: {
@ -15,8 +16,11 @@ class Controller extends Summary {
}
}
};
this.$http.get(`VnUsers/${value.id}`, {filter})
.then(res => this.$.summary = res.data);
this.$http.get(`VnUsers/preview`, {filter})
.then(res => {
const [summary] = res.data;
this.$.summary = summary;
});
}
get isHr() {
return this.aclService.hasAny(['hr']);

View File

@ -77,14 +77,6 @@ module.exports = Self => {
if (salesPersonId)
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
await models.ClaimLog.create({
originFk: args.id,
userFk: userId,
action: 'insert',
description: 'Claim-pickup-order sent',
changedModel: 'Mail'
});
const email = new Email('claim-pickup-order', params);
return email.send();

View File

@ -52,4 +52,5 @@ columns:
hasInvoiceSimplified: simplified invoice
typeFk: type
lastSalesPersonFk: last salesperson
rating: rating
recommendedCredit: recommended credit

View File

@ -52,4 +52,5 @@ columns:
hasInvoiceSimplified: factura simple
typeFk: tipo
lastSalesPersonFk: último comercial
rating: clasificación
recommendedCredit: crédito recomendado

View File

@ -19,9 +19,6 @@ module.exports = Self => {
});
Self.confirmTransaction = async(ctx, id, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let tx;
const myOptions = {userId: ctx.req.accessToken.userId};
@ -34,29 +31,8 @@ module.exports = Self => {
}
try {
const oldTpvTransaction = await models.TpvTransaction.findById(id, null, myOptions);
const confirm = await Self.rawSql('CALL hedera.tpvTransaction_confirmById(?)', [id], myOptions);
const tpvTransaction = await models.TpvTransaction.findById(id, null, myOptions);
const oldInstance = {status: oldTpvTransaction.status};
const newInstance = {status: tpvTransaction.status};
const logRecord = {
originFk: tpvTransaction.clientFk,
userFk: userId,
action: 'update',
changedModel: 'TpvTransaction',
changedModelId: id,
oldInstance: oldInstance,
newInstance: newInstance
};
await models.ClientLog.create(logRecord, myOptions);
if (tx) await tx.commit();
return confirm;
} catch (e) {
if (tx) await tx.rollback();

View File

@ -30,34 +30,9 @@ module.exports = Self => {
}
});
Self.sendSms = async(ctx, id, destination, message, options) => {
Self.sendSms = async(ctx, id, destination, message) => {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const userId = ctx.req.accessToken.userId;
const sms = await models.Sms.send(ctx, destination, message);
const logRecord = {
originFk: id,
userFk: userId,
action: 'insert',
changedModel: 'sms',
newInstance: {
destinationFk: id,
destination: destination,
message: message,
statusCode: sms.statusCode,
status: sms.status
}
};
const clientLog = await models.ClientLog.create(logRecord, myOptions);
sms.logId = clientLog.id;
return sms;
};
};

View File

@ -13,10 +13,7 @@ describe('client sendSms()', () => {
const sms = await models.Client.sendSms(ctx, id, destination, message, options);
const createdLog = await models.ClientLog.findById(sms.logId, null, options);
const json = JSON.parse(JSON.stringify(createdLog.newInstance));
expect(json.message).toEqual(message);
expect(sms).toBeDefined();
await tx.rollback();
} catch (e) {

View File

@ -144,20 +144,7 @@ module.exports = Self => {
if (taxDataChecked && !hasSageData)
throw new UserError(`You need to fill sage information before you check verified data`);
if (args.despiteOfClient) {
const logRecord = {
originFk: clientId,
userFk: userId,
action: 'update',
changedModel: 'Client',
changedModelId: clientId,
description: $t(`Client checked as validated despite of duplication`, {
clientId: args.despiteOfClient
})
};
await models.ClientLog.create(logRecord, myOptions);
}
// Remove unwanted properties
delete args.ctx;
delete args.id;

View File

@ -60,6 +60,7 @@ module.exports = Self => {
DISTINCT c.id clientFk,
c.name clientName,
c.salesPersonFk,
c.businessTypeFk,
u.name salesPersonName,
d.amount,
co.created,

View File

@ -2,7 +2,7 @@ const UserError = require('vn-loopback/util/user-error');
const base64url = require('base64url');
module.exports = Self => {
Self.remoteMethodCtx('confirm', {
Self.remoteMethod('confirm', {
description: 'Confirms electronic payment transaction',
accessType: 'WRITE',
accepts: [
@ -30,7 +30,7 @@ module.exports = Self => {
}
});
Self.confirm = async(ctx, signatureVersion, merchantParameters, signature) => {
Self.confirm = async(signatureVersion, merchantParameters, signature) => {
const $ = Self.app.models;
let transaction;
@ -83,7 +83,7 @@ module.exports = Self => {
params['Ds_Currency'],
params['Ds_Response'],
params['Ds_ErrorCode']
], {userId: ctx.req.accessToken.userId});
]);
return true;
} catch (err) {

View File

@ -14,7 +14,7 @@ module.exports = Self => {
Self.validatesPresenceOf('street', {
message: 'Street cannot be empty'
});
Self.validatesPresenceOf('city', {
message: 'City cannot be empty'
});
@ -282,7 +282,7 @@ module.exports = Self => {
await Self.changeCredit(ctx, finalState, changes);
// Credit management changes
if (orgData?.rating != changes.rating || orgData?.recommendedCredit != changes.recommendedCredit)
if (changes?.rating || changes?.recommendedCredit)
await Self.changeCreditManagement(ctx, finalState, changes);
const oldInstance = {};
@ -401,22 +401,32 @@ module.exports = Self => {
Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
const models = Self.app.models;
const userId = ctx.options.accessToken.userId;
const accessToken = {req: {accessToken: ctx.options.accessToken} };
const accessToken = {req: {accessToken: ctx.options.accessToken}};
const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE');
if (!canEditCredit) {
const lastCredit = await models.ClientCredit.findOne({
field: ['workerFk', 'amount'],
where: {
clientFk: finalState.id
},
order: 'id DESC'
}, ctx.options);
const lastAmount = lastCredit && lastCredit.amount;
const lastCreditIsNotEditable = !await models.ACL.checkAccessAcl(accessToken, 'Client', 'isNotEditableCredit', 'WRITE');
if (lastCredit && lastCredit.amount == 0) {
const zeroCreditEditor =
await models.ACL.checkAccessAcl(accessToken, 'Client', 'zeroCreditEditor', 'WRITE');
const lastCreditIsNotEditable =
await models.ACL.checkAccessAcl(
{req: {accessToken: {userId: lastCredit.workerFk}}},
'Client',
'zeroCreditEditor',
'WRITE'
);
if (lastAmount == 0 && lastCreditIsNotEditable)
throw new UserError(`You can't change the credit set to zero from a financialBoss`);
if (lastCreditIsNotEditable && !zeroCreditEditor)
throw new UserError(`You can't change the credit set to zero from a financialBoss`);
}
const creditLimits = await models.ClientCreditLimit.find({
fields: ['roleFk'],
@ -479,21 +489,6 @@ module.exports = Self => {
hasChanges = oldData.name != changes.name || oldData.active != changes.active;
if (!hasChanges) return;
const isClient = await Self.app.models.Client.count({id: oldData.id});
if (isClient) {
const loopBackContext = LoopBackContext.getCurrentContext();
const userId = loopBackContext.active.accessToken.userId;
const logRecord = {
originFk: oldData.id,
userFk: userId,
action: 'update',
changedModel: 'VnUser',
oldInstance: {name: oldData.name, active: oldData.active},
newInstance: {name: changes.name, active: changes.active}
};
await Self.app.models.ClientLog.create(logRecord);
}
}
});
});

View File

@ -60,22 +60,22 @@ describe('Client Model', () => {
try {
const options = {transaction: tx};
const context = {options};
const ctx = {options};
// Set credit to zero by a financialBoss
const financialBoss = await models.VnUser.findOne({
where: {name: 'financialBoss'}
}, options);
context.options.accessToken = {userId: financialBoss.id};
ctx.options.accessToken = {userId: financialBoss.id};
await models.Client.changeCredit(context, instance, {credit: 0});
await models.Client.changeCredit(ctx, instance, {credit: 0});
const salesAssistant = await models.VnUser.findOne({
where: {name: 'salesAssistant'}
}, options);
context.options.accessToken = {userId: salesAssistant.id};
ctx.options.accessToken = {userId: salesAssistant.id};
await models.Client.changeCredit(context, instance, {credit: 300});
await models.Client.changeCredit(ctx, instance, {credit: 300});
await tx.rollback();
} catch (e) {
@ -93,14 +93,14 @@ describe('Client Model', () => {
try {
const options = {transaction: tx};
const context = {options};
const ctx = {options};
const salesAssistant = await models.VnUser.findOne({
where: {name: 'salesAssistant'}
}, options);
context.options.accessToken = {userId: salesAssistant.id};
ctx.options.accessToken = {userId: salesAssistant.id};
await models.Client.changeCredit(context, instance, {credit: 99999});
await models.Client.changeCredit(ctx, instance, {credit: 99999});
await tx.rollback();
} catch (e) {

View File

@ -4,7 +4,7 @@
filter="::$ctrl.filter"
limit="20"
order="amount DESC"
data="defaulters"
data="$ctrl.defaulters"
on-data-change="$ctrl.reCheck()"
auto-load="true">
</vn-crud-model>
@ -34,7 +34,7 @@
</div>
<div class="vn-pa-md">
<vn-button
ng-show="$ctrl.checked.length > 0"
disabled="$ctrl.checked.length == 0"
ng-click="notesDialog.show()"
name="notesDialog"
vn-tooltip="Add observation"
@ -54,6 +54,9 @@
<th field="clientFk">
<span translate>Client</span>
</th>
<th>
<span translate>Es trabajador</span>
</th>
<th field="salesPersonFk">
<span translate>Comercial</span>
</th>
@ -94,7 +97,7 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="defaulter in defaulters">
<tr ng-repeat="defaulter in $ctrl.defaulters">
<td shrink>
<vn-check
ng-model="defaulter.checked"
@ -110,6 +113,12 @@
{{::defaulter.clientName}}
</span>
</td>
<td>
<vn-check
ng-model="defaulter.isWorker"
disabled="true">
</vn-check>
</td>
<td>
<span
title="{{::defaulter.salesPersonName}}"

View File

@ -6,6 +6,7 @@ export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.defaulter = {};
this.defaulters = [];
this.checkedDefaulers = [];
this.smartTableOptions = {
@ -69,6 +70,18 @@ export default class Controller extends Section {
this.getBalanceDueTotal();
}
set defaulters(value) {
if (!value || !value.length) return;
for (let defaulter of value)
defaulter.isWorker = defaulter.businessTypeFk === 'worker';
this._defaulters = value;
}
get defaulters() {
return this._defaulters;
}
get checked() {
const clients = this.$.model.data || [];
const checkedLines = [];

View File

@ -1,7 +1,5 @@
<vn-watcher
vn-id="watcher"
url="VnUsers"
id-field="id"
data="$ctrl.account"
form="form">
</vn-watcher>
@ -51,9 +49,9 @@
label="Save">
</vn-submit>
<vn-button
ng-if="$ctrl.canChangePassword"
label="Change password"
vn-dialog="change-pass">
ng-if="$ctrl.canChangePassword"
label="Change password"
vn-dialog="change-pass">
</vn-button>
<vn-button
class="cancel"

View File

@ -8,6 +8,22 @@ export default class Controller extends Section {
this.canEnableCheckBox = true;
}
set client(value) {
this._client = value;
if (!value) return;
const filter = {where: {id: value.id}};
this.$http.get(`VnUsers/preview`, {filter})
.then(res => {
const [user] = res.data;
this.account = user;
});
}
get client() {
return this._client;
}
$onChanges() {
if (this.client) {
this.account = this.client.account;

View File

@ -5,12 +5,14 @@ describe('Component VnClientWebAccess', () => {
let $scope;
let vnApp;
let controller;
let $httpParamSerializer;
beforeEach(ngModule('client'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _vnApp_) => {
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_, _vnApp_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
vnApp = _vnApp_;
jest.spyOn(vnApp, 'showError');
const $element = angular.element('<vn-client-web-access></vn-client-web-access>');
@ -32,7 +34,10 @@ describe('Component VnClientWebAccess', () => {
describe('isCustomer()', () => {
it('should return true if the password can be modified', () => {
controller.client = {id: '1234'};
const filter = {where: {id: controller.client.id}};
const serializedParams = $httpParamSerializer({filter});
$httpBackend.expectGET(`VnUsers/preview?${serializedParams}`).respond('foo');
$httpBackend.expectGET(`Clients/${controller.client.id}/hasCustomerRole`).respond(true);
controller.isCustomer();
$httpBackend.flush();
@ -42,7 +47,10 @@ describe('Component VnClientWebAccess', () => {
it(`should return a false if the password can't be modified`, () => {
controller.client = {id: '1234'};
const filter = {where: {id: controller.client.id}};
const serializedParams = $httpParamSerializer({filter});
$httpBackend.expectGET(`VnUsers/preview?${serializedParams}`).respond('foo');
$httpBackend.expectGET(`Clients/${controller.client.id}/hasCustomerRole`).respond(false);
controller.isCustomer();
$httpBackend.flush();
@ -54,9 +62,12 @@ describe('Component VnClientWebAccess', () => {
describe('checkConditions()', () => {
it('should perform a query to check if the client is valid', () => {
controller.client = {id: '1234'};
const filter = {where: {id: controller.client.id}};
const serializedParams = $httpParamSerializer({filter});
expect(controller.canEnableCheckBox).toBeTruthy();
$httpBackend.expectGET(`VnUsers/preview?${serializedParams}`).respond('foo');
$httpBackend.expectGET(`Clients/${controller.client.id}/isValidClient`).respond(false);
controller.checkConditions();
$httpBackend.flush();
@ -82,7 +93,10 @@ describe('Component VnClientWebAccess', () => {
controller.newPassword = 'm24x8';
controller.repeatPassword = 'm24x8';
controller.canChangePassword = true;
const filter = {where: {id: controller.client.id}};
const serializedParams = $httpParamSerializer({filter});
$httpBackend.expectGET(`VnUsers/preview?${serializedParams}`).respond('foo');
const query = `Clients/${controller.client.id}/setPassword`;
$httpBackend.expectPATCH(query, {newPassword: controller.newPassword}).respond('done');
controller.onPassChange();

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