Merge branch 'dev' into 5066-vehicleSorted
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Carlos Satorres 2023-07-14 11:49:26 +00:00
commit b2b335326e
247 changed files with 7301 additions and 4613 deletions

View File

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2328.01] - 2023-07-13
## [2330.01] - 2023-07-27
### Added
@ -14,15 +14,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
## [2328.01] - 2023-07-13
### Added
- (Clientes -> Morosos) Añadida columna "es trabajador"
- (Trabajadores -> Departamentos) Nueva sección
- (Trabajadores -> Departamentos) Añadido listado de Trabajadores por departamento
- (Trabajadores -> Departamentos) Añadido características de departamento e información
### Changed
### Fixed
- (Trabajadores -> Departamentos) Arreglado búscador
## [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-15

View File

@ -0,0 +1,68 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('addAlias', {
description: 'Add an alias if the user has the grant',
accessType: 'WRITE',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
},
{
arg: 'id',
type: 'number',
required: true,
description: 'The user id',
http: {source: 'path'}
},
{
arg: 'mailAlias',
type: 'number',
description: 'The new alias for user',
required: true
}
],
http: {
path: `/:id/addAlias`,
verb: 'POST'
}
});
Self.addAlias = async function(ctx, id, mailAlias, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const user = await Self.findById(userId, {fields: ['hasGrant']}, myOptions);
if (!user.hasGrant)
throw new UserError(`You don't have grant privilege`);
const account = await models.Account.findById(userId, {
fields: ['id'],
include: {
relation: 'aliases',
scope: {
fields: ['mailAlias']
}
}
}, myOptions);
const aliases = account.aliases().map(alias => alias.mailAlias);
const hasAlias = aliases.includes(mailAlias);
if (!hasAlias)
throw new UserError(`You cannot assign an alias that you are not assigned to`);
return models.MailAliasAccount.create({
mailAlias: mailAlias,
account: id
}, myOptions);
};
};

View File

@ -0,0 +1,55 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('removeAlias', {
description: 'Remove alias if the user has the grant',
accessType: 'WRITE',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
},
{
arg: 'id',
type: 'number',
required: true,
description: 'The user id',
http: {source: 'path'}
},
{
arg: 'mailAlias',
type: 'number',
description: 'The alias to delete',
required: true
}
],
http: {
path: `/:id/removeAlias`,
verb: 'POST'
}
});
Self.removeAlias = async function(ctx, id, mailAlias, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const canRemoveAlias = await models.ACL.checkAccessAcl(ctx, 'VnUser', 'canRemoveAlias', 'WRITE');
if (userId != id && !canRemoveAlias) throw new UserError(`You don't have grant privilege`);
const mailAliasAccount = await models.MailAliasAccount.findOne({
where: {
mailAlias: mailAlias,
account: id
}
}, myOptions);
await mailAliasAccount.destroy(myOptions);
};
};

View File

@ -17,23 +17,22 @@ module.exports = Self => {
Self.renewToken = async function(ctx) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const created = ctx.req.accessToken.created;
const tokenId = ctx.req.accessToken.id;
const token = ctx.req.accessToken;
const now = new Date();
const differenceMilliseconds = now - created;
const differenceMilliseconds = now - token.created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields: ['renewPeriod']});
const fields = ['renewPeriod', 'courtesyTime'];
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields});
if (differenceSeconds <= accessTokenConfig.renewPeriod)
throw new UserError(`The renew period has not been exceeded`);
if (differenceSeconds < accessTokenConfig.renewPeriod - accessTokenConfig.courtesyTime)
throw new UserError(`The renew period has not been exceeded`, 'periodNotExceeded');
await Self.logout(tokenId);
const user = await Self.findById(userId);
await Self.logout(token.id);
const user = await Self.findById(token.userId);
const accessToken = await user.createAccessToken();
return {token: accessToken.id, created: accessToken.created};
return {id: accessToken.id, ttl: accessToken.ttl};
};
};

View File

@ -0,0 +1,102 @@
const ForbiddenError = require('vn-loopback/util/forbiddenError');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('signIn', {
description: 'Login a user with username/email and password',
accepts: [
{
arg: 'user',
type: 'String',
description: 'The user name or email',
required: true
}, {
arg: 'password',
type: 'String',
description: 'The password'
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/sign-in`,
verb: 'POST'
}
});
Self.signIn = async function(ctx, user, password, options) {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const where = Self.userUses(user);
const vnUser = await Self.findOne({
fields: ['id', 'name', 'password', 'active', 'email', 'passExpired', 'twoFactor'],
where
}, myOptions);
const validCredentials = vnUser
&& await vnUser.hasPassword(password);
if (validCredentials) {
if (!vnUser.active)
throw new UserError('User disabled');
await Self.sendTwoFactor(ctx, vnUser, myOptions);
await Self.passExpired(vnUser, myOptions);
if (vnUser.twoFactor)
throw new ForbiddenError(null, 'REQUIRES_2FA');
}
return Self.validateLogin(user, password);
};
Self.passExpired = async(vnUser, myOptions) => {
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
const $ = Self.app.models;
const changePasswordToken = await $.AccessToken.create({
scopes: ['changePassword'],
userId: vnUser.id
}, myOptions);
const err = new UserError('Pass expired', 'passExpired');
changePasswordToken.twoFactor = vnUser.twoFactor ? true : false;
err.details = {token: changePasswordToken};
throw err;
}
};
Self.sendTwoFactor = async(ctx, vnUser, myOptions) => {
if (vnUser.twoFactor === 'email') {
const $ = Self.app.models;
const code = String(Math.floor(Math.random() * 999999));
const maxTTL = ((60 * 1000) * 5); // 5 min
await $.AuthCode.upsertWithWhere({userFk: vnUser.id}, {
userFk: vnUser.id,
code: code,
expires: Date.vnNow() + maxTTL
}, myOptions);
const headers = ctx.req.headers;
const platform = headers['sec-ch-ua-platform']?.replace(/['"=]+/g, '');
const browser = headers['sec-ch-ua']?.replace(/['"=]+/g, '');
const params = {
args: {
recipientId: vnUser.id,
recipient: vnUser.email,
code: code,
ip: ctx.req?.connection?.remoteAddress,
device: platform && browser ? platform + ', ' + browser : headers['user-agent'],
},
req: {getLocale: ctx.req.getLocale},
};
await Self.sendTemplate(params, 'auth-code', true);
}
};
};

View File

@ -1,81 +0,0 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('signIn', {
description: 'Login a user with username/email and password',
accepts: [
{
arg: 'user',
type: 'String',
description: 'The user name or email',
http: {source: 'form'},
required: true
}, {
arg: 'password',
type: 'String',
description: 'The password'
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/signIn`,
verb: 'POST'
}
});
Self.signIn = async function(user, password) {
const models = Self.app.models;
const usesEmail = user.indexOf('@') !== -1;
let token;
const userInfo = usesEmail
? {email: user}
: {username: user};
const instance = await Self.findOne({
fields: ['username', 'password'],
where: userInfo
});
const where = usesEmail
? {email: user}
: {name: user};
const vnUser = await Self.findOne({
fields: ['id', 'active', 'passExpired'],
where
});
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const validCredentials = instance
&& await instance.hasPassword(password);
if (validCredentials) {
if (!vnUser.active)
throw new UserError('User disabled');
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
const changePasswordToken = await models.AccessToken.create({
scopes: ['change-password'],
userId: vnUser.id
});
const err = new UserError('Pass expired', 'passExpired');
err.details = {token: changePasswordToken};
throw err;
}
try {
await models.Account.sync(instance.username, password);
} catch (err) {
console.warn(err);
}
}
let loginInfo = Object.assign({password}, userInfo);
token = await Self.login(loginInfo, 'user');
return {token: token.id, created: token.created};
};
};

View File

@ -0,0 +1,101 @@
const {models} = require('vn-loopback/server/server');
describe('VnUser Sign-in()', () => {
const employeeId = 1;
const unauthCtx = {
req: {
headers: {},
connection: {
remoteAddress: '127.0.0.1'
},
getLocale: () => 'en'
},
args: {}
};
const {VnUser, AccessToken} = models;
describe('when credentials are correct', () => {
it('should return the token', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
let accessToken = await AccessToken.findById(login.token);
let ctx = {req: {accessToken: accessToken}};
expect(login.token).toBeDefined();
await VnUser.logout(ctx.req.accessToken.id);
});
it('should return the token if the user doesnt exist but the client does', async() => {
let login = await VnUser.signIn(unauthCtx, 'PetterParker', 'nightmare');
let accessToken = await AccessToken.findById(login.token);
let ctx = {req: {accessToken: accessToken}};
expect(login.token).toBeDefined();
await VnUser.logout(ctx.req.accessToken.id);
});
});
describe('when credentials are incorrect', () => {
it('should throw a 401 error', async() => {
let error;
try {
await VnUser.signIn(unauthCtx, 'IDontExist', 'TotallyWrongPassword');
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(401);
expect(error.code).toBe('LOGIN_FAILED');
});
});
describe('when two-factor auth is required', () => {
it('should throw a 403 error', async() => {
const employee = await VnUser.findById(employeeId);
const tx = await VnUser.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await employee.updateAttribute('twoFactor', 'email', options);
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(403);
expect(error.code).toBe('REQUIRES_2FA');
});
});
describe('when passExpired', () => {
it('should throw a passExpired error', async() => {
const tx = await VnUser.beginTransaction({});
const employee = await VnUser.findById(employeeId);
const yesterday = Date.vnNew();
yesterday.setDate(yesterday.getDate() - 1);
let error;
try {
const options = {transaction: tx};
await employee.updateAttribute('passExpired', yesterday, options);
await VnUser.signIn(unauthCtx, 'employee', 'nightmare', options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(400);
expect(error.message).toBe('Pass expired');
});
});
});

View File

@ -1,41 +0,0 @@
const {models} = require('vn-loopback/server/server');
describe('VnUser signIn()', () => {
describe('when credentials are correct', () => {
it('should return the token', async() => {
let login = await models.VnUser.signIn('salesAssistant', 'nightmare');
let accessToken = await models.AccessToken.findById(login.token);
let ctx = {req: {accessToken: accessToken}};
expect(login.token).toBeDefined();
await models.VnUser.logout(ctx.req.accessToken.id);
});
it('should return the token if the user doesnt exist but the client does', async() => {
let login = await models.VnUser.signIn('PetterParker', 'nightmare');
let accessToken = await models.AccessToken.findById(login.token);
let ctx = {req: {accessToken: accessToken}};
expect(login.token).toBeDefined();
await models.VnUser.logout(ctx.req.accessToken.id);
});
});
describe('when credentials are incorrect', () => {
it('should throw a 401 error', async() => {
let error;
try {
await models.VnUser.signIn('IDontExist', 'TotallyWrongPassword');
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(401);
expect(error.code).toBe('LOGIN_FAILED');
});
});
});

View File

@ -0,0 +1,52 @@
const {models} = require('vn-loopback/server/server');
describe('VnUser validate-auth()', () => {
describe('validateAuth', () => {
it('should signin if data is correct', async() => {
await models.AuthCode.create({
userFk: 9,
code: '555555',
expires: Date.vnNow() + (60 * 1000)
});
const token = await models.VnUser.validateAuth('developer', 'nightmare', '555555');
expect(token.token).toBeDefined();
});
});
describe('validateCode', () => {
it('should throw an error for a non existent code', async() => {
let error;
try {
await models.VnUser.validateCode('developer', '123456');
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(400);
expect(error.message).toEqual('Invalid or expired verification code');
});
it('should throw an error when a code doesn`t match the login username', async() => {
let error;
let authCode;
try {
authCode = await models.AuthCode.create({
userFk: 1,
code: '555555',
expires: Date.vnNow() + (60 * 1000)
});
await models.VnUser.validateCode('developer', '555555');
} catch (e) {
authCode && await authCode.destroy();
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(400);
expect(error.message).toEqual('Authentication failed');
});
});
});

View File

@ -0,0 +1,66 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('validateAuth', {
description: 'Login a user with username/email and password',
accepts: [
{
arg: 'user',
type: 'String',
description: 'The user name or email',
required: true
},
{
arg: 'password',
type: 'String',
description: 'The password'
},
{
arg: 'code',
type: 'String',
description: 'The auth code'
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/validate-auth`,
verb: 'POST'
}
});
Self.validateAuth = async(username, password, code, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const token = Self.validateLogin(username, password);
await Self.validateCode(username, code, myOptions);
return token;
};
Self.validateCode = async(username, code, myOptions) => {
const {AuthCode} = Self.app.models;
const authCode = await AuthCode.findOne({
where: {
code: code
}
}, myOptions);
const expired = authCode && Date.vnNow() > authCode.expires;
if (!authCode || expired)
throw new UserError('Invalid or expired verification code');
const user = await Self.findById(authCode.userFk, {
fields: ['name', 'twoFactor']
}, myOptions);
if (user.name !== username)
throw new UserError('Authentication failed');
await authCode.destroy(myOptions);
};
};

View File

@ -1,7 +1,4 @@
{
"AccountingType": {
"dataSource": "vn"
},
"AccessTokenConfig": {
"dataSource": "vn",
"options": {
@ -10,6 +7,12 @@
}
}
},
"AccountingType": {
"dataSource": "vn"
},
"AuthCode": {
"dataSource": "vn"
},
"Bank": {
"dataSource": "vn"
},

View File

@ -16,6 +16,10 @@
"type": "number",
"required": true
},
"courtesyTime": {
"type": "number",
"required": true
},
"renewInterval": {
"type": "number",
"required": true

View File

@ -0,0 +1,31 @@
{
"name": "AuthCode",
"base": "VnModel",
"options": {
"mysql": {
"table": "salix.authCode"
}
},
"properties": {
"userFk": {
"type": "number",
"required": true,
"id": true
},
"code": {
"type": "string",
"required": true
},
"expires": {
"type": "number",
"required": true
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
}
}

View File

@ -5,12 +5,15 @@ const {Email} = require('vn-print');
module.exports = function(Self) {
vnModel(Self);
require('../methods/vn-user/signIn')(Self);
require('../methods/vn-user/sign-in')(Self);
require('../methods/vn-user/acl')(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/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self);
require('../methods/vn-user/addAlias')(Self);
require('../methods/vn-user/removeAlias')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
@ -111,6 +114,18 @@ module.exports = function(Self) {
return email.send();
});
Self.validateLogin = async function(user, password) {
let loginInfo = Object.assign({password}, Self.userUses(user));
token = await Self.login(loginInfo, 'user');
return {token: token.id, ttl: token.ttl};
};
Self.userUses = function(user) {
return user.indexOf('@') !== -1
? {email: user}
: {username: user};
};
const _setPassword = Self.prototype.setPassword;
Self.prototype.setPassword = async function(newPassword, options, cb) {
if (cb === undefined && typeof options === 'function') {
@ -143,8 +158,9 @@ module.exports = function(Self) {
}
};
Self.sharedClass._methods.find(method => method.name == 'changePassword')
.accessScopes = ['change-password'];
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls =
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
.filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {

View File

@ -59,6 +59,9 @@
},
"passExpired": {
"type": "date"
},
"twoFactor": {
"type": "string"
}
},
"relations": {
@ -111,6 +114,13 @@
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
"property": "validateAuth",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "privileges",
"accessType": "*",

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,11 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'addAlias', 'WRITE', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'removeAlias', 'WRITE', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'canRemoveAlias', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES('Ticket', 'invoiceTickets', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -1,10 +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`, `renewInterval`)
INSERT IGNORE INTO `salix`.`accessTokenConfig` (`id`, `renewPeriod`, `courtesyTime`, `renewInterval`)
VALUES
(1, 21600, 300);
(1, 21600, 5, 300);

View File

@ -1,9 +1,9 @@
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId)
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 (
CREATE OR REPLACE TABLE `vn`.`invoiceOutConfig` (
id INT UNSIGNED auto_increment NOT NULL,
parallelism int UNSIGNED DEFAULT 1 NOT NULL,
PRIMARY KEY (id)

View File

@ -0,0 +1,13 @@
create table `salix`.`authCode`
(
userFk int UNSIGNED not null,
code int not null,
expires bigint not null,
constraint authCode_pk
primary key (userFk),
constraint authCode_unique
unique (code),
constraint authCode_user_id_fk
foreign key (userFk) references `account`.`user` (id)
on update cascade on delete cascade
);

View File

@ -0,0 +1,89 @@
DROP PROCEDURE IF EXISTS `vn`.`clientCreate`;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`client_create`(
vFirstname VARCHAR(50),
vSurnames VARCHAR(50),
vFi VARCHAR(9),
vAddress TEXT,
vPostcode CHAR(5),
vCity VARCHAR(25),
vProvinceFk SMALLINT(5),
vCompanyFk SMALLINT(5),
vPhone VARCHAR(11),
vEmail VARCHAR(255),
vUserFk INT
)
BEGIN
/**
* Create new client
*
* @params vFirstname firstName
* @params vSurnames surnames
* @params vFi company code from accounting transactions
* @params vAddress address
* @params vPostcode postCode
* @params vCity city
* @params vProvinceFk province
* @params vCompanyFk company in which he has become a client
* @params vPhone telephone number
* @params vEmail email address
* @params vUserFk user id
*/
DECLARE vPayMethodFk INT;
DECLARE vDueDay INT;
DECLARE vDefaultCredit DECIMAL(10, 2);
DECLARE vIsTaxDataChecked TINYINT(1);
DECLARE vHasCoreVnl BOOLEAN;
DECLARE vMandateTypeFk INT;
SELECT defaultPayMethodFk,
defaultDueDay,
defaultCredit,
defaultIsTaxDataChecked,
defaultHasCoreVnl,
defaultMandateTypeFk
INTO vPayMethodFk,
vDueDay,
vDefaultCredit,
vIsTaxDataChecked,
vHasCoreVnl,
vMandateTypeFk
FROM clientConfig;
INSERT INTO `client`
SET id = vUserFk,
name = CONCAT(vFirstname, ' ', vSurnames),
street = vAddress,
fi = TRIM(vFi),
phone = vPhone,
email = vEmail,
provinceFk = vProvinceFk,
city = vCity,
postcode = vPostcode,
socialName = CONCAT(vSurnames, ' ', vFirstname),
payMethodFk = vPayMethodFk,
dueDay = vDueDay,
credit = vDefaultCredit,
isTaxDataChecked = vIsTaxDataChecked,
hasCoreVnl = vHasCoreVnl,
isEqualizated = FALSE
ON duplicate KEY UPDATE
payMethodFk = vPayMethodFk,
dueDay = vDueDay,
credit = vDefaultCredit,
isTaxDataChecked = vIsTaxDataChecked,
hasCoreVnl = vHasCoreVnl,
isActive = TRUE;
INSERT INTO mandate (clientFk, companyFk, mandateTypeFk)
SELECT vUserFk, vCompanyFk, vMandateTypeFk
WHERE NOT EXISTS (
SELECT id
FROM mandate
WHERE clientFk = vUserFk
AND companyFk = vCompanyFk
AND mandateTypeFk = vMandateTypeFk
);
END$$
DELIMITER ;

View File

@ -0,0 +1,17 @@
ALTER TABLE `vn`.`clientConfig` ADD defaultPayMethodFk tinyint(3) unsigned NULL;
ALTER TABLE `vn`.`clientConfig` ADD defaultDueDay int unsigned NULL;
ALTER TABLE `vn`.`clientConfig` ADD defaultCredit decimal(10, 2) NULL;
ALTER TABLE `vn`.`clientConfig` ADD defaultIsTaxDataChecked tinyint(1) NULL;
ALTER TABLE `vn`.`clientConfig` ADD defaultHasCoreVnl boolean NULL;
ALTER TABLE `vn`.`clientConfig` ADD defaultMandateTypeFk smallint(5) NULL;
ALTER TABLE `vn`.`clientConfig` ADD CONSTRAINT clientNewConfigPayMethod_FK FOREIGN KEY (defaultPayMethodFk) REFERENCES vn.payMethod(id);
ALTER TABLE `vn`.`clientConfig` ADD CONSTRAINT clientNewConfigMandateType_FK FOREIGN KEY (defaultMandateTypeFk) REFERENCES vn.mandateType(id);
UPDATE `vn`.`clientConfig`
SET defaultPayMethodFk = 4,
defaultDueDay = 5,
defaultCredit = 300.0,
defaultIsTaxDataChecked = 1,
defaultHasCoreVnl = 1,
defaultMandateTypeFk = 2
WHERE id = 1;

View File

@ -0,0 +1,24 @@
alter table `vn`.`department`
add `twoFactor` ENUM ('email') null comment 'Default user two-factor auth type';
drop trigger `vn`.`department_afterUpdate`;
DELIMITER $$
$$
create definer = root@localhost trigger `vn`.`department_afterUpdate`
after update
on department
for each row
BEGIN
IF !(OLD.parentFk <=> NEW.parentFk) THEN
UPDATE vn.department_recalc SET isChanged = TRUE;
END IF;
IF !(OLD.twoFactor <=> NEW.twoFactor) THEN
UPDATE account.user u
JOIN vn.workerDepartment wd ON wd.workerFk = u.id
SET u.twoFactor = NEW.twoFactor
WHERE wd.departmentFk = NEW.id;
END IF;
END;$$
DELIMITER ;

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

View File

@ -0,0 +1,10 @@
ALTER TABLE `vn`.`roadmap` COMMENT='Troncales diarios que se contratan';
ALTER TABLE `vn`.`roadmap` ADD price decimal(10,2) NULL;
ALTER TABLE `vn`.`roadmap` ADD driverName varchar(45) NULL;
ALTER TABLE `vn`.`roadmap` ADD name varchar(45) NOT NULL;
ALTER TABLE `vn`.`roadmap` CHANGE name name varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL AFTER id;
ALTER TABLE `vn`.`roadmap` MODIFY COLUMN etd datetime NOT NULL;
ALTER TABLE `vn`.`expeditionTruck` COMMENT='Distintas paradas que hacen los trocales';
ALTER TABLE `vn`.`expeditionTruck` DROP FOREIGN KEY expeditionTruck_FK_2;
ALTER TABLE `vn`.`expeditionTruck` ADD CONSTRAINT expeditionTruck_FK_2 FOREIGN KEY (roadmapFk) REFERENCES vn.roadmap(id) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,6 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Roadmap', '*', '*', 'ALLOW', 'ROLE', 'palletizerBoss'),
('Roadmap', '*', '*', 'ALLOW', 'ROLE', 'productionBoss'),
('ExpeditionTruck', '*', '*', 'ALLOW', 'ROLE', 'palletizerBoss'),
('ExpeditionTruck', '*', '*', 'ALLOW', 'ROLE', 'productionBoss');

View File

@ -0,0 +1,5 @@
alter table `account`.`user`
add `twoFactor` ENUM ('email') null comment 'Two-factor auth type';
DELETE FROM `salix`.`ACL`
WHERE model = 'VnUser' AND property = 'changePassword';

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn`.`item` ADD recycledPlastic INT NULL;
ALTER TABLE `vn`.`item` ADD nonRecycledPlastic INT NULL;

View File

@ -0,0 +1,7 @@
UPDATE `vn`.`ticket` t
JOIN `vn`.`ticketObservation` o ON o.ticketFk = t.id
SET t.weight = cast(REPLACE(o.description, ',', '.') as decimal(10,2))
WHERE o.observationTypeFk = 6;
DELETE FROM `vn`.`ticketObservation` WHERE observationTypeFk = 6;
DELETE FROM `vn`.`observationType` WHERE id = 6;

File diff suppressed because one or more lines are too long

View File

@ -77,7 +77,10 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `
ORDER BY id;
INSERT INTO `account`.`account`(`id`)
SELECT id FROM `account`.`user`;
SELECT `u`.`id`
FROM `account`.`user` `u`
JOIN `account`.`role` `r` ON `u`.`role` = `r`.`id`
WHERE `r`.`name` <> 'customer';
INSERT INTO `vn`.`educationLevel` (`id`, `name`)
VALUES
@ -144,17 +147,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
@ -185,13 +188,13 @@ INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`, `sectorFk`, `ipAd
UPDATE `vn`.`sector` SET mainPrinterFk = 1 WHERE id = 1;
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`, `sectorFk`, `labelerFk`)
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`)
VALUES
(1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106, NULL, NULL),
(1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107, NULL, NULL),
(1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108, 1, NULL),
(1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109, 1, NULL),
(1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110, 2, NULL);
(1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106),
(1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107),
(1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108),
(1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109),
(1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110);
INSERT INTO `vn`.`parking` (`id`, `column`, `row`, `sectorFk`, `code`, `pickingOrder`)
VALUES
@ -382,9 +385,16 @@ INSERT INTO `vn`.`clientManaCache`(`clientFk`, `mana`, `dated`)
(1103, 0, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(1104, -30, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH));
INSERT INTO `vn`.`clientConfig`(`riskTolerance`, `maxCreditRows`)
INSERT INTO `vn`.`mandateType`(`id`, `name`)
VALUES
(200, 10);
(1, 'B2B'),
(2, 'CORE'),
(3, 'LCR');
INSERT INTO `vn`.`clientConfig`(`id`, `riskTolerance`, `maxCreditRows`, `maxPriceIncreasingRatio`, `riskScope`, `defaultPayMethodFk`, `defaultDueDay`, `defaultCredit`, `defaultIsTaxDataChecked`, `defaultHasCoreVnl`, `defaultMandateTypeFk`)
VALUES
(1, 200, 10, 0.25, 2, 4, 5, 300.00, 1, 1, 2);
INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `phone`, `mobile`, `isActive`, `clientFk`, `agencyModeFk`, `longitude`, `latitude`, `isEqualizated`, `isDefaultAddress`)
VALUES
@ -552,8 +562,11 @@ INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`
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'),
(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'),
(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');
(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
@ -697,40 +710,40 @@ INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agen
(6, NULL, 57, util.VN_CURDATE(), 5, 7, 'sixth route', 1.7, 60, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 3),
(7, NULL, 57, util.VN_CURDATE(), 6, 8, 'seventh route', 0, 70, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 5);
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`)
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`, `weight`)
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)),
(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()),
(10, 1, 1, 5, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'Ingram Street', 2, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(11, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'NY roofs', 122, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(12, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(13, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(14, 1, 2, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1104, 'Malibu Point', 4, NULL, 0, 9, 5, 1, util.VN_CURDATE()),
(15, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1105, 'An incredibly long alias for testing purposes', 125, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(16, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(17, 1, 7, 2, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(18, 1, 4, 4, 4, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1108, 'Cerebro', 128, NULL, 0, 12, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +12 HOUR)),
(19, 1, 5, 5, NULL, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 1, NULL, 5, 1, util.VN_CURDATE()),
(20, 1, 5, 5, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH)),
(21, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Holland', 102, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH)),
(22, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Japan', 103, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH)),
(23, NULL, 8, 1, 7, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'address 21', 121, NULL, 0, 5, 5, 1, util.VN_CURDATE()),
(24 ,NULL, 8, 1, 7, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 5, 5, 1, util.VN_CURDATE()),
(25 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(26 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(28, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(29, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(30, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(31, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(32, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE());
(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), 1),
(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), 2),
(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), NULL),
(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), NULL),
(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), NULL),
(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), NULL),
(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(), NULL),
(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(), NULL),
(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(), NULL),
(10, 1, 1, 5, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'Ingram Street', 2, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(11, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'NY roofs', 122, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(12, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(13, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(14, 1, 2, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1104, 'Malibu Point', 4, NULL, 0, 9, 5, 1, util.VN_CURDATE(), NULL),
(15, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1105, 'An incredibly long alias for testing purposes', 125, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(16, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(17, 1, 7, 2, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(18, 1, 4, 4, 4, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1108, 'Cerebro', 128, NULL, 0, 12, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +12 HOUR), NULL),
(19, 1, 5, 5, NULL, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 1, NULL, 5, 1, util.VN_CURDATE(), NULL),
(20, 1, 5, 5, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), NULL),
(21, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Holland', 102, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), NULL),
(22, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Japan', 103, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), NULL),
(23, NULL, 8, 1, 7, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'address 21', 121, NULL, 0, 5, 5, 1, util.VN_CURDATE(), NULL),
(24 ,NULL, 8, 1, 7, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 5, 5, 1, util.VN_CURDATE(), NULL),
(25 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(26 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(28, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(29, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(30, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(31, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(32, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL);
INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`)
VALUES
@ -821,12 +834,6 @@ INSERT INTO `vn`.`greuge`(`id`, `clientFk`, `description`, `amount`, `shipped`,
(11, 1101, 'some heritage charges', -15.99, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 5, 1),
(12, 1101, 'some miscellaneous charges', 58.00, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 6, 1);
INSERT INTO `vn`.`mandateType`(`id`, `name`)
VALUES
(1, 'B2B'),
(2, 'CORE'),
(3, 'LCR');
INSERT INTO `vn`.`mandate`(`id`, `clientFk`, `companyFk`, `code`, `created`, `mandateTypeFk`)
VALUES
(1, 1102, 442, '1-1', util.VN_CURDATE(), 2);
@ -2572,6 +2579,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),
@ -2579,9 +2606,18 @@ INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
(3, 6, 5),
(4, 7, 1);
INSERT INTO `vn`.`expeditionTruck` (`id`, `ETD`, `description`)
INSERT INTO `vn`.`roadmap` (`id`, `name`, `tractorPlate`, `trailerPlate`, `phone`, `supplierFk`, `etd`, `observations`, `userFk`, `price`, `driverName`)
VALUES
(1, CONCAT(YEAR(DATE_ADD(util.VN_CURDATE(), INTERVAL +3 YEAR))), 'Best truck in fleet');
(1, 'val-algemesi', 'RE-001', 'PO-001', '111111111', 1, util.VN_NOW(), 'this is test observation', 1, 15, 'Batman'),
(2, 'alg-valencia', 'RE-002', 'PO-002', '111111111', 1, util.VN_NOW(), 'test observation', 1, 20, 'Robin'),
(3, 'alz-algemesi', 'RE-003', 'PO-003', '222222222', 2, DATE_ADD(util.VN_NOW(), INTERVAL 2 DAY), 'observations...', 2, 25, 'Driverman');
INSERT INTO `vn`.`expeditionTruck` (`id`, `roadmapFk`, `warehouseFk`, `eta`, `description`, `userFk`)
VALUES
(1, 1, 1, DATE_ADD(util.VN_NOW(), INTERVAL 1 DAY), 'Best truck in fleet', 1),
(2, 1, 2, DATE_ADD(util.VN_NOW(), INTERVAL '1 2' DAY_HOUR), 'Second truck in fleet', 1),
(3, 1, 3, DATE_ADD(util.VN_NOW(), INTERVAL '1 4' DAY_HOUR), 'Third truck in fleet', 1),
(4, 2, 1, DATE_ADD(util.VN_NOW(), INTERVAL 3 DAY), 'Truck red', 1);
INSERT INTO `vn`.`expeditionPallet` (`id`, `truckFk`, `built`, `position`, `isPrint`)
VALUES
@ -2825,8 +2861,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
('lilium', 'dev', 'http://localhost:9000/#/'),
('salix', 'dev', 'http://localhost:5000/#!/');
('lilium', 'development', 'http://localhost:9000/#/'),
('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
VALUES
@ -2842,7 +2878,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

File diff suppressed because it is too large Load Diff

View File

@ -312,8 +312,8 @@ export default {
clientDefaulter: {
anyClient: 'vn-client-defaulter tbody > tr',
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(3) > span',
firstObservation: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(8) > vn-textarea[ng-model="defaulter.observation"]',
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"]',
@ -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',
@ -550,6 +547,7 @@ export default {
moreMenuMakeInvoice: '.vn-menu [name="makeInvoice"]',
moreMenuRegenerateInvoice: '.vn-menu [name="regenerateInvoice"]',
moreMenuChangeShippedHour: '.vn-menu [name="changeShipped"]',
moreMenuSMSOptions: '.vn-menu [name="smsOptions"]',
moreMenuPaymentSMS: '.vn-menu [name="sendPaymentSms"]',
moreMenuSendImportSms: '.vn-menu [name="sendImportSms"]',
SMStext: 'textarea[name="message"]',
@ -667,15 +665,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',
@ -906,6 +895,18 @@ export default {
extension: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(5) > section > span',
},
department: {
firstDepartment: 'vn-worker-department-index vn-card > vn-treeview vn-treeview-childs vn-treeview-childs vn-treeview-childs a'
},
departmentSummary: {
header: 'vn-worker-department-summary h5',
name: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(1) > section > span',
code: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(2) > section > span',
chat: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(3) > section > span',
bossDepartment: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(4) > section > span',
email: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(5) > section > span',
clientFk: 'vn-worker-department-summary vn-horizontal > vn-one > vn-vertical > vn-label-value:nth-child(6) > section > span',
},
workerBasicData: {
name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]',
surname: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.lastName"]',
@ -913,6 +914,13 @@ export default {
locker: 'vn-worker-basic-data vn-input-number[ng-model="$ctrl.worker.locker"]',
saveButton: 'vn-worker-basic-data button[type=submit]'
},
departmentBasicData: {
Name: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.name"]',
Code: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.code"]',
Chat: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.chat"]',
Email: 'vn-worker-department-basic-data vn-textfield[ng-model="$ctrl.department.notificationEmail"]',
saveButton: 'vn-worker-department-basic-data button[type=submit]'
},
workerNotes: {
addNoteFloatButton: 'vn-worker-note vn-icon[icon="add"]',
note: 'vn-note-worker-create vn-textarea[ng-model="$ctrl.note.text"]',

View File

@ -16,6 +16,7 @@ describe('ChangePassword path', async() => {
await browser.close();
});
const badPassword = 'badpass';
const oldPassword = 'nightmare';
const newPassword = 'newPass.1234';
describe('Bad login', async() => {
@ -37,13 +38,22 @@ describe('ChangePassword path', async() => {
expect(message.text).toContain('Invalid current password');
// Bad attempt: password not meet requirements
message = await page.sendForm($.form, {
oldPassword: oldPassword,
newPassword: badPassword,
repeatPassword: badPassword
});
expect(message.text).toContain('Password does not meet requirements');
// Bad attempt: same password
message = await page.sendForm($.form, {
oldPassword: oldPassword,
newPassword: oldPassword,
repeatPassword: oldPassword
});
expect(message.text).toContain('Password does not meet requirements');
expect(message.text).toContain('You can not use the same password');
// Correct attempt: change password
message = await page.sendForm($.form, {

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

@ -0,0 +1,29 @@
import selectors from '../../../helpers/selectors.js';
import getBrowser from '../../../helpers/puppeteer';
describe('department summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('hr', 'worker');
await page.accessToSection('worker.department');
await page.doSearch('INFORMATICA');
await page.click(selectors.department.firstDepartment);
});
afterAll(async() => {
await browser.close();
});
it('should reach the employee summary section and check all properties', async() => {
expect(await page.waitToGetProperty(selectors.departmentSummary.header, 'innerText')).toEqual('INFORMATICA');
expect(await page.getProperty(selectors.departmentSummary.name, 'innerText')).toEqual('INFORMATICA');
expect(await page.getProperty(selectors.departmentSummary.code, 'innerText')).toEqual('it');
expect(await page.getProperty(selectors.departmentSummary.chat, 'innerText')).toEqual('informatica-cau');
expect(await page.getProperty(selectors.departmentSummary.bossDepartment, 'innerText')).toEqual('');
expect(await page.getProperty(selectors.departmentSummary.email, 'innerText')).toEqual('-');
expect(await page.getProperty(selectors.departmentSummary.clientFk, 'innerText')).toEqual('-');
});
});

View File

@ -0,0 +1,43 @@
import getBrowser from '../../../helpers/puppeteer';
import selectors from '../../../helpers/selectors.js';
const $ = {
form: 'vn-worker-department-basic-data form',
};
describe('department summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('hr', 'worker');
await page.accessToSection('worker.department');
await page.doSearch('INFORMATICA');
await page.click(selectors.department.firstDepartment);
});
beforeEach(async() => {
await page.accessToSection('worker.department.card.basicData');
});
afterAll(async() => {
await browser.close();
});
it(`should edit the department basic data and confirm the department data was edited`, async() => {
const values = {
Name: 'Informatica',
Code: 'IT',
Chat: 'informatica-cau',
Email: 'it@verdnatura.es',
};
await page.fillForm($.form, values);
const formValues = await page.fetchForm($.form, Object.keys(values));
const message = await page.sendForm($.form, values);
expect(message.isSuccess).toBeTrue();
expect(formValues).toEqual(values);
});
});

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

@ -124,6 +124,7 @@ describe('Ticket descriptor path', () => {
describe('SMS', () => {
it('should send the payment SMS using the descriptor menu', async() => {
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuSMSOptions);
await page.waitToClick(selectors.ticketDescriptor.moreMenuPaymentSMS);
await page.waitForSelector(selectors.ticketDescriptor.SMStext);
await page.waitPropertyLength(selectors.ticketDescriptor.SMStext, 'value', 128);
@ -134,8 +135,7 @@ describe('Ticket descriptor path', () => {
});
it('should send the import SMS using the descriptor menu', async() => {
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitForContentLoaded();
await page.waitToClick(selectors.ticketDescriptor.moreMenuSMSOptions);
await page.waitToClick(selectors.ticketDescriptor.moreMenuSendImportSms);
await page.waitForSelector(selectors.ticketDescriptor.SMStext);
await page.waitPropertyLength(selectors.ticketDescriptor.SMStext, 'value', 144);

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

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

@ -24,7 +24,7 @@ export default class Auth {
initialize() {
let criteria = {
to: state => {
const outLayout = ['login', 'recover-password', 'reset-password', 'change-password'];
const outLayout = ['login', 'recover-password', 'reset-password', 'change-password', 'validate-email'];
return !outLayout.some(ol => ol == state.name);
}
};
@ -59,12 +59,31 @@ export default class Auth {
password: password || undefined
};
return this.$http.post('VnUsers/signIn', params)
.then(json => this.onLoginOk(json, remember));
const now = new Date();
return this.$http.post('VnUsers/sign-in', params).then(
json => this.onLoginOk(json, now, remember));
}
onLoginOk(json, remember) {
this.vnToken.set(json.data.token, json.data.created, remember);
validateCode(user, password, code, remember) {
if (!user) {
let err = new UserError('Please enter your username');
err.code = 'EmptyLogin';
return this.$q.reject(err);
}
let params = {
user: user,
password: password || undefined,
code: code
};
const now = new Date();
return this.$http.post('VnUsers/validate-auth', params)
.then(json => this.onLoginOk(json, now, 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

@ -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,37 +6,116 @@ import ngModule from '../module';
* @property {String} token The current login token or %null
*/
export default class Token {
constructor() {
try {
this.token = sessionStorage.getItem('vnToken');
this.created = sessionStorage.getItem('vnTokenCreated');
if (!this.token) {
this.token = localStorage.getItem('vnToken');
this.created = localStorage.getItem('vnTokenCreated');
}
} catch (e) {}
}
set(token, created, remember) {
this.unset();
try {
if (remember) {
localStorage.setItem('vnToken', token);
localStorage.setItem('vnTokenCreated', created);
} else {
sessionStorage.setItem('vnToken', token);
sessionStorage.setItem('vnTokenCreated', created);
}
} catch (e) {}
constructor(vnInterceptor, $http, $rootScope) {
Object.assign(this, {
vnInterceptor,
$http,
$rootScope
});
this.token = token;
this.created = created;
try {
this.getStorage(sessionStorage);
this.remember = true;
if (!this.token) {
this.getStorage(localStorage);
this.remember = false;
}
} catch (e) {}
}
set(token, created, ttl, remember) {
this.unset();
Object.assign(this, {
token,
created,
ttl,
remember
});
this.vnInterceptor.setToken(token);
try {
if (remember)
this.setStorage(localStorage, token, created, ttl);
else
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);
});
}
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

@ -23,6 +23,15 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-trailer:before {
content: "\e967";
}
.icon-grafana:before {
content: "\e965";
}
.icon-trolley:before {
content: "\e95c";
}
.icon-agency-term:before {
content: "\e950";
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

@ -21,6 +21,14 @@
type="password"
autocomplete="false">
</vn-textfield>
<vn-textfield
ng-if="$ctrl.$state.params.twoFactor == 'true'"
label="Verification code"
ng-model="$ctrl.code"
vn-name="code"
autocomplete="false"
class="vn-mt-md">
</vn-textfield>
<div class="footer">
<vn-submit label="Change password" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">

View File

@ -26,11 +26,13 @@ export default class Controller {
submit() {
const userId = this.$state.params.userId;
const newPassword = this.newPassword;
const oldPassword = this.oldPassword;
const newPassword = this.newPassword;
const repeatPassword = this.repeatPassword;
const code = this.code;
if (!newPassword)
throw new UserError(`You must enter a new password`);
if (!oldPassword || !newPassword || !repeatPassword)
throw new UserError(`You must fill all the fields`);
if (newPassword != this.repeatPassword)
throw new UserError(`Passwords don't match`);
@ -38,11 +40,12 @@ export default class Controller {
Authorization: this.$state.params.id
};
this.$http.post('VnUsers/change-password',
this.$http.patch('Accounts/change-password',
{
id: userId,
oldPassword,
newPassword
newPassword,
code
},
{headers}
).then(() => {

View File

@ -2,6 +2,10 @@ Change password: Cambiar contraseña
Old password: Antigua contraseña
New password: Nueva contraseña
Repeat password: Repetir contraseña
Passwords don't match: Las contraseñas no coinciden
You must fill all the fields: Debes rellenar todos los campos
You can not use the same password: No puedes usar la misma contraseña
Verification code: Código de verificación
Password updated!: ¡Contraseña actualizada!
Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud,

View File

@ -9,6 +9,7 @@ import './login';
import './outLayout';
import './recover-password';
import './reset-password';
import './validate-email';
import './change-password';
import './module-card';
import './module-main';

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/>

View File

@ -3,14 +3,13 @@ import Component from 'core/lib/component';
import './style.scss';
export class Layout extends Component {
constructor($element, $, vnModules, vnToken) {
constructor($element, $, vnModules) {
super($element, $);
this.modules = vnModules.get();
}
$onInit() {
this.getUserData();
this.getAccessTokenConfig();
}
getUserData() {
@ -32,41 +31,11 @@ export class Layout extends Component {
window.location.reload();
}
getAccessTokenConfig() {
this.$http.get('AccessTokenConfigs').then(json => {
const firtsResult = json.data[0];
if (!firtsResult) return;
this.renewPeriod = firtsResult.renewPeriod;
this.renewInterval = firtsResult.renewInterval;
const intervalMilliseconds = firtsResult.renewInterval * 1000;
this.inservalId = setInterval(this.checkTokenValidity.bind(this), intervalMilliseconds);
});
}
checkTokenValidity() {
const now = new Date();
const differenceMilliseconds = now - new Date(this.vnToken.created);
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
if (differenceSeconds > this.renewPeriod) {
this.$http.post('VnUsers/renewToken')
.then(json => {
if (json.data.token) {
let remember = true;
if (window.sessionStorage.vnToken) remember = false;
this.vnToken.set(json.data.token, json.data.created, remember);
}
});
}
}
$onDestroy() {
clearInterval(this.inservalId);
this.vnToken.stopRenewer();
}
}
Layout.$inject = ['$element', '$scope', 'vnModules', 'vnToken'];
Layout.$inject = ['$element', '$scope', 'vnModules'];
ngModule.vnComponent('vnLayout', {
template: require('./index.html'),

View File

@ -37,49 +37,4 @@ describe('Component vnLayout', () => {
expect(url).not.toBeDefined();
});
});
describe('getAccessTokenConfig()', () => {
it(`should set the renewPeriod and renewInterval properties in localStorage`, () => {
const response = [{
renewPeriod: 100,
renewInterval: 5
}];
$httpBackend.expect('GET', `AccessTokenConfigs`).respond(response);
controller.getAccessTokenConfig();
$httpBackend.flush();
expect(controller.renewPeriod).toBe(100);
expect(controller.renewInterval).toBe(5);
expect(controller.inservalId).toBeDefined();
});
});
describe('checkTokenValidity()', () => {
it(`should not call renewToken and not set vnToken in the controller`, () => {
controller.renewPeriod = 100;
controller.vnToken.created = new Date();
controller.checkTokenValidity();
expect(controller.vnToken.token).toBeNull();
});
it(`should call renewToken and set vnToken properties in the controller`, () => {
const response = {
token: 999,
created: new Date()
};
controller.renewPeriod = 100;
const oneHourBefore = new Date(Date.now() - (60 * 60 * 1000));
controller.vnToken.created = oneHourBefore;
$httpBackend.expect('POST', `VnUsers/renewToken`).respond(response);
controller.checkTokenValidity();
$httpBackend.flush();
expect(controller.vnToken.token).toBe(999);
expect(controller.vnToken.created).toEqual(response.created);
});
});
});

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,34 +15,61 @@
<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 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="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>
<span class="model-id" ng-if="::modelLog.id">#{{::modelLog.id}}</span>
<span class="model-value" title="{{::modelLog.showValue}}">{{::modelLog.showValue}}</span>
</div>
<vn-card class="changes-log vn-mb-xs" ng-repeat="log in ::modelLog.logs">
<div class="change-info vn-pa-sm">
<div
class="action-date text-secondary text-caption vn-ml-sm"
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]"
@ -53,17 +78,7 @@
</vn-icon>
</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"
<div class="change-detail vn-pa-sm"
ng-class="{expanded: log.expand}"
ng-if="::log.props.length || log.description">
<vn-icon
@ -98,9 +113,12 @@
</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,72 +1,156 @@
@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;
& > .header {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
& > .action-model {
display: inline-flex;
}
}
.model-log {
& > .model-info {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-height: 22px;
& > .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;
@include mobile {
display: initial;
}
}
}
&:hover > .model-info > vn-icon[icon="filter_alt"] {
display: initial;
}
}
.changes-log {
position: relative;
max-width: 100%;
width: 100%;
border-radius: 2px;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
& > .change-info {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
& > .date {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-overflow: ellipsis;
}
}
& > .action-date {
& > div {
white-space: nowrap;
& > vn-icon.pit {
@extend %clickable-light;
vertical-align: middle;
font-size: 20px;
color: $color-font-secondary;
display: none;
@include mobile {
display: inline-block;
}
}
& > .action {
display: inline-flex;
align-items: center;
@ -92,41 +176,11 @@ vn-log {
}
}
}
}
& > .model {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-height: 18px;
& > vn-icon {
@extend %clickable-light;
vertical-align: middle;
padding: 2px;
margin: -2px;
font-size: 18px;
color: $color-font-secondary;
float: right;
display: none;
@include mobile {
display: initial;
&:hover vn-icon.pit {
display: inline-block;
}
}
& > .model-value {
font-style: italic;
}
& > .model-id {
color: $color-font-secondary;
font-size: .9rem;
}
}
&:hover > .model > vn-icon {
display: initial;
}
}
}
.changes {
& > .change-detail {
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
color: $color-font-light;
@ -161,7 +215,32 @@ vn-log {
}
}
}
vn-log-value > .id-value {
.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;
}
}
}

View File

@ -24,13 +24,23 @@ export default class Controller {
this.loading = false;
})
.catch(req => {
this.loading = false;
this.password = '';
this.focusUser();
if (req?.data?.error?.code === 'REQUIRES_2FA') {
this.outLayout.login = {
user: this.user,
password: this.password,
remember: this.remember
};
this.$state.go('validate-email');
return;
}
const err = req.data?.error;
if (err?.code == 'passExpired')
this.$state.go('change-password', err.details.token);
this.loading = false;
this.password = '';
this.focusUser();
throw req;
});
}
@ -44,5 +54,8 @@ Controller.$inject = ['$scope', '$element', '$state', 'vnAuth'];
ngModule.vnComponent('vnLogin', {
template: require('./index.html'),
controller: Controller
controller: Controller,
require: {
outLayout: '^vnOutLayout'
}
});

View File

@ -0,0 +1,10 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Enter verification code</h5>
<span translate>Please enter the verification code that we have sent to your email address within 5 minutes</span>
<vn-textfield label="Code" ng-model="$ctrl.code" type="text" vn-focus>
</vn-textfield>
<div class="footer">
<vn-submit label="Validate" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>

View File

@ -0,0 +1,43 @@
import ngModule from '../../module';
import './style.scss';
export default class Controller {
constructor($scope, $element, vnAuth, $state) {
Object.assign(this, {
$scope,
$element,
vnAuth,
user: localStorage.getItem('lastUser'),
remember: true,
$state
});
}
$onInit() {
this.loginData = this.outLayout.login;
if (!this.loginData)
this.$state.go('login');
}
submit() {
this.loading = true;
this.vnAuth.validateCode(this.loginData.user, this.loginData.password, this.code, this.loginData.remember)
.then(() => {
localStorage.setItem('lastUser', this.user);
this.loading = false;
})
.catch(error => {
this.loading = false;
throw error;
});
}
}
Controller.$inject = ['$scope', '$element', 'vnAuth', '$state'];
ngModule.vnComponent('vnValidateEmail', {
template: require('./index.html'),
controller: Controller,
require: {
outLayout: '^vnOutLayout'
}
});

View File

@ -0,0 +1,5 @@
Validate email auth: Autenticar email
Enter verification code: Introduce código de verificación
Code: Código
Please enter the verification code that we have sent to your email address within 5 minutes: Por favor, introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos
Validate: Validar

View File

@ -0,0 +1,24 @@
@import "variables";
vn-validate-email {
.footer {
margin-top: 32px;
text-align: center;
position: relative;
& > .vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}

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', {
@ -36,9 +37,15 @@ function config($stateProvider, $urlRouterProvider) {
description: 'Reset password',
template: '<vn-reset-password></vn-reset-password>'
})
.state('validate-email', {
parent: 'outLayout',
url: '/validate-email',
description: 'Validate email auth',
template: '<vn-validate-email></vn-validate-email>'
})
.state('change-password', {
parent: 'outLayout',
url: '/change-password?id&userId',
url: '/change-password?id&userId&twoFactor',
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

@ -39,7 +39,7 @@ module.exports = Self => {
return [html, 'text/html', `filename=${fileName}.pdf"`];
};
Self.sendTemplate = async function(ctx, templateName) {
Self.sendTemplate = async function(ctx, templateName, force) {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,
@ -52,6 +52,6 @@ module.exports = Self => {
const email = new Email(templateName, params);
return email.send();
return email.send({force: force});
};
};

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

@ -147,14 +147,12 @@
"Receipt's bank was not found": "Receipt's bank was not found",
"This receipt was not compensated": "This receipt was not compensated",
"Client's email was not found": "Client's email was not found",
"Tickets with associated refunds": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº {{id}}",
"Tickets with associated refunds": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº %d",
"It is not possible to modify tracked sales": "It is not possible to modify tracked sales",
"It is not possible to modify sales that their articles are from Floramondo": "It is not possible to modify sales that their articles are from Floramondo",
"It is not possible to modify cloned sales": "It is not possible to modify cloned sales",
"Valid priorities: 1,2,3": "Valid priorities: 1,2,3",
"Warehouse inventory not set": "Almacén inventario no está establecido",
"Component cost not set": "Componente coste no está estabecido",
"Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2",
"Description cannot be blank": "Description cannot be blank",
"company": "Company",
"country": "Country",
@ -176,5 +174,9 @@
"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}}",
"Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address"
"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",
"You can not use the same password": "You can not use the same password",
"Valid priorities": "Valid priorities: %d",
"Negative basis of tickets": "Negative basis of tickets: {{ticketsIds}}"
}

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",
@ -269,7 +268,7 @@
"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",
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº %d",
"Not exist this branch": "La rama no existe",
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
"Collection does not exist": "La colección no existe",
@ -277,6 +276,8 @@
"Insert a date range": "Inserte un rango de fechas",
"Added observation": "{{user}} añadió esta observacion: {{text}}",
"Comment added to client": "Observación añadida al cliente {{clientFk}}",
"Invalid auth code": "Código de verificación incorrecto",
"Invalid or expired verification code": "Código de verificación incorrecto o expirado",
"Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen",
"company": "Compañía",
"country": "País",
@ -294,9 +295,15 @@
"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",
"Authentication failed": "Autenticación fallida",
"You can't use the same password": "No puedes usar la misma contraseña",
"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"
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado",
"Valid priorities": "Prioridades válidas: %d",
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}",
"You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado"
}

View File

@ -0,0 +1,9 @@
module.exports = class ForbiddenError extends Error {
constructor(message, code, ...translateArgs) {
super(message);
this.name = 'ForbiddenError';
this.statusCode = 403;
this.code = code;
this.translateArgs = translateArgs;
}
};

View File

@ -1,15 +1,12 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('changePassword', {
Self.remoteMethodCtx('changePassword', {
description: 'Changes the user password',
accessType: 'WRITE',
accessScopes: ['changePassword'],
accepts: [
{
arg: 'id',
type: 'number',
description: 'The user id',
http: {source: 'path'}
}, {
arg: 'oldPassword',
type: 'string',
description: 'The old password',
@ -19,15 +16,35 @@ module.exports = Self => {
type: 'string',
description: 'The new password',
required: true
}, {
arg: 'code',
type: 'string',
description: 'The 2FA code'
}
],
http: {
path: `/:id/changePassword`,
path: `/change-password`,
verb: 'PATCH'
}
});
Self.changePassword = async function(id, oldPassword, newPassword) {
await Self.app.models.VnUser.changePassword(id, oldPassword, newPassword);
Self.changePassword = async function(ctx, oldPassword, newPassword, code, options) {
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const {VnUser} = Self.app.models;
const user = await VnUser.findById(userId, {fields: ['name', 'twoFactor']}, myOptions);
await user.hasPassword(oldPassword);
if (oldPassword == newPassword)
throw new UserError(`You can not use the same password`);
if (user.twoFactor)
await VnUser.validateCode(user.name, code, myOptions);
await VnUser.changePassword(userId, oldPassword, newPassword, myOptions);
};
};

View File

@ -1,5 +1,5 @@
module.exports = Self => {
Self.remoteMethod('login', {
Self.remoteMethodCtx('login', {
description: 'Login a user with username/email and password',
accepts: [
{
@ -23,5 +23,5 @@ module.exports = Self => {
}
});
Self.login = async(user, password) => Self.app.models.VnUser.signIn(user, password);
Self.login = async(ctx, user, password, options) => Self.app.models.VnUser.signIn(ctx, user, password, options);
};

View File

@ -21,7 +21,8 @@ module.exports = Self => {
}
});
Self.setPassword = async function(id, newPassword) {
await Self.app.models.VnUser.setPassword(id, newPassword);
Self.setPassword = async function(id, newPassword, options) {
options = typeof options == 'object' ? options : {};
await Self.app.models.VnUser.setPassword(id, newPassword, options);
};
};

View File

@ -1,22 +1,99 @@
const {models} = require('vn-loopback/server/server');
describe('account changePassword()', () => {
const ctx = {req: {accessToken: {userId: 70}}};
const unauthCtx = {
req: {
headers: {},
connection: {
remoteAddress: '127.0.0.1'
},
getLocale: () => 'en'
},
args: {}
};
describe('Without 2FA', () => {
it('should throw an error when old password is wrong', async() => {
const tx = await models.Account.beginTransaction({});
let error;
try {
await models.Account.changePassword(1, 'wrongPassword', 'nightmare.9999');
const options = {transaction: tx};
await models.Account.changePassword(ctx, 'wrongPassword', 'nightmare.9999', null, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e.message;
}
expect(error).toContain('Invalid current password');
});
it('should change password', async() => {
it('should throw an error when old and new password are the same', async() => {
const tx = await models.Account.beginTransaction({});
let error;
try {
await models.Account.changePassword(70, 'nightmare', 'nightmare.9999');
const options = {transaction: tx};
await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', null, options);
await models.Account.changePassword(ctx, 'nightmare.9999', 'nightmare.9999', null, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e.message;
}
expect(error).toContain('You can not use the same password');
});
it('should change password', async() => {
const tx = await models.Account.beginTransaction({});
try {
const options = {transaction: tx};
await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', null, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
expect(e).toBeUndefined();
}
});
});
describe('With 2FA', () => {
it('should change password when code is correct', async() => {
const tx = await models.Account.beginTransaction({});
const yesterday = Date.vnNew();
yesterday.setDate(yesterday.getDate() - 1);
const options = {transaction: tx};
try {
await models.VnUser.updateAll(
{id: 70},
{
twoFactor: 'email',
passExpired: yesterday
}
, options);
await models.VnUser.signIn(unauthCtx, 'trainee', 'nightmare', options);
} catch (e) {
if (e.message != 'Pass expired')
throw e;
}
try {
const authCode = await models.AuthCode.findOne({where: {userFk: 70}}, options);
await models.Account.changePassword(ctx, 'nightmare', 'nightmare.9999', authCode.code, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
expect(e).toBeUndefined();
}
});
});
});

View File

@ -8,8 +8,18 @@ describe('Account setPassword()', () => {
});
it('should update password when it passes requirements', async() => {
let req = models.Account.setPassword(1, 'Very$ecurePa22.');
const tx = await models.Account.beginTransaction({});
await expectAsync(req).toBeResolved();
let error;
try {
const options = {transaction: tx};
await models.Account.setPassword(1, 'Very$ecurePa22.', options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined();
});
});

View File

@ -37,6 +37,13 @@
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
"property": "changePassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -17,9 +17,7 @@
<vn-icon-button
icon="delete"
translate-attr="{title: 'Unsubscribe'}"
ng-click="removeConfirm.show(row)"
vn-acl="itManagement"
vn-acl-action="remove">
ng-click="removeConfirm.show(row)">
</vn-icon-button>
</vn-item-section>
</vn-item>
@ -32,9 +30,7 @@
translate-attr="{title: 'Add'}"
vn-bind="+"
ng-click="$ctrl.onAddClick()"
fixed-bottom-right
vn-acl="itManagement"
vn-acl-action="remove">
fixed-bottom-right>
</vn-float-button>
<vn-dialog
vn-id="dialog"

View File

@ -21,12 +21,11 @@ export default class Controller extends Section {
}
onAddClick() {
this.addData = {account: this.$params.id};
this.$.dialog.show();
}
onAddSave() {
return this.$http.post(`MailAliasAccounts`, this.addData)
return this.$http.post(`VnUsers/${this.$params.id}/addAlias`, this.addData)
.then(() => this.refresh())
.then(() => this.vnApp.showSuccess(
this.$t('Subscribed to alias!'))
@ -34,11 +33,12 @@ export default class Controller extends Section {
}
onRemove(row) {
return this.$http.delete(`MailAliasAccounts/${row.id}`)
.then(() => {
this.$.data.splice(this.$.data.indexOf(row), 1);
this.vnApp.showSuccess(this.$t('Unsubscribed from alias!'));
});
const params = {
mailAlias: row.mailAlias
};
return this.$http.post(`VnUsers/${this.$params.id}/removeAlias`, params)
.then(() => this.refresh())
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
}

View File

@ -25,8 +25,9 @@ describe('component vnUserAliases', () => {
describe('onAddSave()', () => {
it('should add the new row', () => {
controller.addData = {account: 1};
controller.$params = {id: 1};
$httpBackend.expectPOST('MailAliasAccounts').respond();
$httpBackend.expectPOST('VnUsers/1/addAlias').respond();
$httpBackend.expectGET('MailAliasAccounts').respond('foo');
controller.onAddSave();
$httpBackend.flush();
@ -41,12 +42,14 @@ describe('component vnUserAliases', () => {
{id: 1, alias: 'foo'},
{id: 2, alias: 'bar'}
];
controller.$params = {id: 1};
$httpBackend.expectDELETE('MailAliasAccounts/1').respond();
$httpBackend.expectPOST('VnUsers/1/removeAlias').respond();
$httpBackend.expectGET('MailAliasAccounts').respond(controller.$.data[1]);
controller.onRemove(controller.$.data[0]);
$httpBackend.flush();
expect(controller.$.data).toEqual([{id: 2, alias: 'bar'}]);
expect(controller.$.data).toEqual({id: 2, alias: 'bar'});
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});

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

@ -95,7 +95,7 @@ module.exports = Self => {
// When claimState has been changed
if (args.claimStateFk) {
const newState = await models.ClaimState.findById(args.claimStateFk, null, options);
const newState = await models.ClaimState.findById(args.claimStateFk, null, myOptions);
if (newState.hasToNotify) {
if (newState.code == 'incomplete')
notifyStateChange(ctx, salesPerson.id, claim, newState.code);

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

@ -28,7 +28,7 @@ module.exports = Self => {
const isAccount = await models.Account.findById(id);
if (isClient && !isAccount)
await models.Account.setPassword(id, newPassword);
await models.VnUser.setPassword(id, newPassword);
else
throw new UserError(`Modifiable password only via recovery or by an administrator`);
};

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

@ -89,7 +89,7 @@ module.exports = Self => {
};
const country = await Self.app.models.Country.findOne(filter);
const code = country ? country.code.toLowerCase() : null;
const countryCode = this.fi.toLowerCase().substring(0, 2);
const countryCode = this.fi?.toLowerCase().substring(0, 2);
if (!this.fi || !validateTin(this.fi, code) || (this.isVies && countryCode == code))
err();
@ -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 = {};
@ -406,17 +406,27 @@ module.exports = Self => {
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)
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

@ -68,7 +68,7 @@
</span>
</vn-td>
<vn-td number>{{::clientInforma.rating}}</vn-td>
<vn-td>{{::clientInforma.recommendedCredit | currency: 'EUR': 2}}</vn-td>
<vn-td number>{{::clientInforma.recommendedCredit | currency: 'EUR'}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>

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

@ -270,7 +270,7 @@
info="Invoices minus payments plus orders not yet invoiced">
</vn-label-value>
<vn-label-value label="Credit"
value="{{$ctrl.summary.credit | currency: 'EUR':2 }} "
value="{{$ctrl.summary.credit | currency: 'EUR'}} "
ng-class="{alert: $ctrl.summary.credit > $ctrl.summary.creditInsurance ||
($ctrl.summary.credit && $ctrl.summary.creditInsurance == null)}"
info="Verdnatura's maximum risk">
@ -296,6 +296,9 @@
value="{{$ctrl.summary.rating}}"
info="Value from 1 to 20. The higher the better value">
</vn-label-value>
<vn-label-value label="Recommended credit"
value="{{$ctrl.summary.recommendedCredit | currency: 'EUR'}}">
</vn-label-value>
</vn-one>
</vn-horizontal>
<vn-horizontal>

View File

@ -8,7 +8,7 @@ vn-client-summary .summary {
}
vn-horizontal h4 .grafana:after {
content: 'contact_support';
font-size: 17px;
font-family: 'salixfont' !important;
content: "\e965";
}
}

View File

@ -9,8 +9,7 @@ vn-entry-buy-index vn-card {
}
thead tr {
border-left: 1px solid white;
border-right: 1px solid white;
border: 1px solid white;;
}
tbody tr:nth-child(1),

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