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/), 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). 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 ### Added
@ -14,15 +14,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### 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 ## [2326.01] - 2023-06-29
### Added ### Added
- (Entradas -> Correo) Al cambiar el tipo de cambio enviará un correo a las personas designadas - (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 ### 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 ### Fixed
- - (General -> Históricos) Duplicidades eliminadas
- (Facturas -> Facturación global) Solucionados fallos que paran el proceso
## [2324.01] - 2023-06-15 ## [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) { Self.renewToken = async function(ctx) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const token = ctx.req.accessToken;
const created = ctx.req.accessToken.created;
const tokenId = ctx.req.accessToken.id;
const now = new Date(); const now = new Date();
const differenceMilliseconds = now - created; const differenceMilliseconds = now - token.created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000); 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) if (differenceSeconds < accessTokenConfig.renewPeriod - accessTokenConfig.courtesyTime)
throw new UserError(`The renew period has not been exceeded`); throw new UserError(`The renew period has not been exceeded`, 'periodNotExceeded');
await Self.logout(tokenId); await Self.logout(token.id);
const user = await Self.findById(userId); const user = await Self.findById(token.userId);
const accessToken = await user.createAccessToken(); 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": { "AccessTokenConfig": {
"dataSource": "vn", "dataSource": "vn",
"options": { "options": {
@ -10,6 +7,12 @@
} }
} }
}, },
"AccountingType": {
"dataSource": "vn"
},
"AuthCode": {
"dataSource": "vn"
},
"Bank": { "Bank": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -16,6 +16,10 @@
"type": "number", "type": "number",
"required": true "required": true
}, },
"courtesyTime": {
"type": "number",
"required": true
},
"renewInterval": { "renewInterval": {
"type": "number", "type": "number",
"required": true "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) { module.exports = function(Self) {
vnModel(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/acl')(Self);
require('../methods/vn-user/recover-password')(Self); require('../methods/vn-user/recover-password')(Self);
require('../methods/vn-user/validate-token')(Self); require('../methods/vn-user/validate-token')(Self);
require('../methods/vn-user/privileges')(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/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'); Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
@ -111,6 +114,18 @@ module.exports = function(Self) {
return email.send(); 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; const _setPassword = Self.prototype.setPassword;
Self.prototype.setPassword = async function(newPassword, options, cb) { Self.prototype.setPassword = async function(newPassword, options, cb) {
if (cb === undefined && typeof options === 'function') { if (cb === undefined && typeof options === 'function') {
@ -143,8 +158,9 @@ module.exports = function(Self) {
} }
}; };
Self.sharedClass._methods.find(method => method.name == 'changePassword') Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls =
.accessScopes = ['change-password']; Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
.filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761 // FIXME: https://redmine.verdnatura.es/issues/5761
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { // Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {

View File

@ -59,7 +59,10 @@
}, },
"passExpired": { "passExpired": {
"type": "date" "type": "date"
} },
"twoFactor": {
"type": "string"
}
}, },
"relations": { "relations": {
"role": { "role": {
@ -111,6 +114,13 @@
"principalId": "$authenticated", "principalId": "$authenticated",
"permission": "ALLOW" "permission": "ALLOW"
}, },
{
"property": "validateAuth",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{ {
"property": "privileges", "property": "privileges",
"accessType": "*", "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` ( CREATE TABLE `salix`.`accessTokenConfig` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`renewPeriod` int(10) unsigned DEFAULT NULL, `renewPeriod` int(10) unsigned DEFAULT NULL,
`courtesyTime` int(10) unsigned DEFAULT NULL,
`renewInterval` int(10) unsigned DEFAULT NULL, `renewInterval` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; ) 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 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 VALUES
('InvoiceOut','makePdfAndNotify','WRITE','ALLOW','ROLE','invoicing'), ('InvoiceOut','makePdfAndNotify','WRITE','ALLOW','ROLE','invoicing'),
('InvoiceOutConfig','*','READ','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, id INT UNSIGNED auto_increment NOT NULL,
parallelism int UNSIGNED DEFAULT 1 NOT NULL, parallelism int UNSIGNED DEFAULT 1 NOT NULL,
PRIMARY KEY (id) 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; ORDER BY id;
INSERT INTO `account`.`account`(`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`) INSERT INTO `vn`.`educationLevel` (`id`, `name`)
VALUES VALUES
@ -144,17 +147,17 @@ INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`)
(3, 'GBP', 'Libra', 1), (3, 'GBP', 'Libra', 1),
(4, 'JPY', 'Yen Japones', 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 VALUES
(1, 'España', 1, 'ES', 1, 24, 4, 0, 1, 1), (1, 'España', 1, 'ES', 1, 24, 4, 0, 1),
(2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1, 2), (2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1),
(3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1, 3), (3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1),
(4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1, 4), (4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1),
(5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1, 5), (5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1),
(8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1, 8), (8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1),
(13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2, 13), (13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2),
(19,'Francia', 1, 'FR', 1, 27, 4, 0, 1, 19), (19,'Francia', 1, 'FR', 1, 27, 4, 0, 1),
(30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2, 30); (30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2);
INSERT INTO `vn`.`warehouseAlias`(`id`, `name`) INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
VALUES VALUES
@ -185,13 +188,13 @@ INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`, `sectorFk`, `ipAd
UPDATE `vn`.`sector` SET mainPrinterFk = 1 WHERE id = 1; 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 VALUES
(1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106, NULL, NULL), (1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106),
(1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107, NULL, NULL), (1107, 'ANT', 'Hank' , 'Pym' , 1107, 19, 432978107),
(1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108, 1, NULL), (1108, 'DCX', 'Charles' , 'Xavier', 1108, 19, 432978108),
(1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109, 1, NULL), (1109, 'HLK', 'Bruce' , 'Banner', 1109, 19, 432978109),
(1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110, 2, NULL); (1110, 'JJJ', 'Jessica' , 'Jones' , 1110, 19, 432978110);
INSERT INTO `vn`.`parking` (`id`, `column`, `row`, `sectorFk`, `code`, `pickingOrder`) INSERT INTO `vn`.`parking` (`id`, `column`, `row`, `sectorFk`, `code`, `pickingOrder`)
VALUES VALUES
@ -382,9 +385,16 @@ INSERT INTO `vn`.`clientManaCache`(`clientFk`, `mana`, `dated`)
(1103, 0, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)), (1103, 0, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(1104, -30, 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 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`) INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `phone`, `mobile`, `isActive`, `clientFk`, `agencyModeFk`, `longitude`, `latitude`, `isEqualizated`, `isDefaultAddress`)
VALUES VALUES
@ -550,10 +560,13 @@ INSERT INTO `vn`.`supplierAddress`(`id`, `supplierFk`, `nickname`, `street`, `pr
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`, `commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`, `workerFk`, `supplierActivityFk`, `isPayMethodChecked`, `healthRegister`) INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`, `commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`, `workerFk`, `supplierActivityFk`, `isPayMethodChecked`, `healthRegister`)
VALUES 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'), (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'), (2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'), (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'),
(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'); (442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(567, 'Holland', 'Holland nick', 4000020567, 1, '14364089Z', 0, util.VN_CURDATE(), 1, 'supplier address 6', 'ASGARD', 3, 46600, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
(791, 'Bros SL', 'Bros nick', 5115000791, 1, '37718083S', 0, util.VN_CURDATE(), 1, 'supplier address 7', 'ASGARD', 3, 46600, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(1381, 'Ornamentales', 'Ornamentales', 7185001381, 1, '07972486L', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`) INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES 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), (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); (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 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)), (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, '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, 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, 'T2222222', 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)), (3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), 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, 'T3333333', 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH)), (4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), 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, 'T4444444', 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH)), (5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), 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, 'A1111111', 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)), (6 , 1, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Mountain Drive Gotham', 1, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 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()), (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()), (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()), (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()), (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()), (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()), (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()), (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()), (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()), (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()), (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()), (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)), (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()), (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)), (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)), (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)), (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()), (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()), (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()), (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()), (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()), (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()), (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()), (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()), (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()), (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()); (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`) INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`)
VALUES 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), (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); (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`) INSERT INTO `vn`.`mandate`(`id`, `clientFk`, `companyFk`, `code`, `created`, `mandateTypeFk`)
VALUES VALUES
(1, 1102, 442, '1-1', util.VN_CURDATE(), 2); (1, 1102, 442, '1-1', util.VN_CURDATE(), 2);
@ -2572,6 +2579,26 @@ INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
CALL `vn`.`ticket_doRecalc`(); 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`) INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
VALUES VALUES
(1, 1, 1), (1, 1, 1),
@ -2579,9 +2606,18 @@ INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
(3, 6, 5), (3, 6, 5),
(4, 7, 1); (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 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`) INSERT INTO `vn`.`expeditionPallet` (`id`, `truckFk`, `built`, `position`, `isPrint`)
VALUES VALUES
@ -2825,8 +2861,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES VALUES
('lilium', 'dev', 'http://localhost:9000/#/'), ('lilium', 'development', 'http://localhost:9000/#/'),
('salix', 'dev', 'http://localhost:5000/#!/'); ('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`) INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
VALUES VALUES
@ -2842,7 +2878,8 @@ INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk
INSERT INTO `vn`.`ticketRefund`(`refundTicketFk`, `originalTicketFk`) INSERT INTO `vn`.`ticketRefund`(`refundTicketFk`, `originalTicketFk`)
VALUES VALUES
(1, 12); (1, 12),
(8, 10);
INSERT INTO `vn`.`deviceProductionModels` (`code`) INSERT INTO `vn`.`deviceProductionModels` (`code`)
VALUES VALUES

File diff suppressed because it is too large Load Diff

View File

@ -312,8 +312,8 @@ export default {
clientDefaulter: { clientDefaulter: {
anyClient: 'vn-client-defaulter tbody > tr', anyClient: 'vn-client-defaulter tbody > tr',
firstClientName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(2) > span', 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', 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(8) > vn-textarea[ng-model="defaulter.observation"]', 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', allDefaulterCheckbox: 'vn-client-defaulter thead vn-multi-check',
addObservationButton: 'vn-client-defaulter vn-button[icon="icon-notes"]', addObservationButton: 'vn-client-defaulter vn-button[icon="icon-notes"]',
observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]', 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', 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' 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: { ticketSummary: {
header: 'vn-ticket-summary > vn-card > h5', header: 'vn-ticket-summary > vn-card > h5',
state: 'vn-ticket-summary vn-label-value[label="State"] > section > span', state: 'vn-ticket-summary vn-label-value[label="State"] > section > span',
@ -550,6 +547,7 @@ export default {
moreMenuMakeInvoice: '.vn-menu [name="makeInvoice"]', moreMenuMakeInvoice: '.vn-menu [name="makeInvoice"]',
moreMenuRegenerateInvoice: '.vn-menu [name="regenerateInvoice"]', moreMenuRegenerateInvoice: '.vn-menu [name="regenerateInvoice"]',
moreMenuChangeShippedHour: '.vn-menu [name="changeShipped"]', moreMenuChangeShippedHour: '.vn-menu [name="changeShipped"]',
moreMenuSMSOptions: '.vn-menu [name="smsOptions"]',
moreMenuPaymentSMS: '.vn-menu [name="sendPaymentSms"]', moreMenuPaymentSMS: '.vn-menu [name="sendPaymentSms"]',
moreMenuSendImportSms: '.vn-menu [name="sendImportSms"]', moreMenuSendImportSms: '.vn-menu [name="sendImportSms"]',
SMStext: 'textarea[name="message"]', SMStext: 'textarea[name="message"]',
@ -667,15 +665,6 @@ export default {
thirdRemoveRequestButton: 'vn-ticket-request-index vn-tr:nth-child(3) vn-icon[icon="delete"]', 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', 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]', 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: { ticketService: {
addServiceButton: 'vn-ticket-service vn-icon-button[vn-tooltip="Add service"] > button', 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', 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: { workerBasicData: {
name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]', name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]',
surname: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.lastName"]', 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"]', locker: 'vn-worker-basic-data vn-input-number[ng-model="$ctrl.worker.locker"]',
saveButton: 'vn-worker-basic-data button[type=submit]' 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: { workerNotes: {
addNoteFloatButton: 'vn-worker-note vn-icon[icon="add"]', addNoteFloatButton: 'vn-worker-note vn-icon[icon="add"]',
note: 'vn-note-worker-create vn-textarea[ng-model="$ctrl.note.text"]', 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(); await browser.close();
}); });
const badPassword = 'badpass';
const oldPassword = 'nightmare'; const oldPassword = 'nightmare';
const newPassword = 'newPass.1234'; const newPassword = 'newPass.1234';
describe('Bad login', async() => { describe('Bad login', async() => {
@ -37,13 +38,22 @@ describe('ChangePassword path', async() => {
expect(message.text).toContain('Invalid current password'); expect(message.text).toContain('Invalid current password');
// Bad attempt: password not meet requirements // 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, { message = await page.sendForm($.form, {
oldPassword: oldPassword, oldPassword: oldPassword,
newPassword: oldPassword, newPassword: oldPassword,
repeatPassword: 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 // Correct attempt: change password
message = await page.sendForm($.form, { message = await page.sendForm($.form, {

View File

@ -5,8 +5,8 @@ const $ = {
userName: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.name"]', userName: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.name"]',
email: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.email"]', email: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.email"]',
saveButton: 'vn-client-web-access button[type=submit]', saveButton: 'vn-client-web-access button[type=submit]',
nameValue: 'vn-client-log .change:nth-child(1) .basic-json:nth-child(2) vn-json-value', nameValue: 'vn-client-log .changes-log:nth-child(2) .basic-json:nth-child(2) vn-json-value',
activeValue: 'vn-client-log .change:nth-child(2) .basic-json:nth-child(1) 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', () => { describe('Client web access path', () => {
@ -39,9 +39,9 @@ describe('Client web access path', () => {
const userName = await page.getValue($.userName); const userName = await page.getValue($.userName);
const email = await page.getValue($.email); const email = await page.getValue($.email);
await page.accessToSection('client.card.log'); // await page.accessToSection('client.card.log');
const logName = await page.innerText($.nameValue); // const logName = await page.innerText($.nameValue);
const logActive = await page.innerText($.activeValue); // const logActive = await page.innerText($.activeValue);
expect(enableMessage.type).toBe('success'); expect(enableMessage.type).toBe('success');
expect(modifyMessage.type).toBe('success'); expect(modifyMessage.type).toBe('success');
@ -50,7 +50,7 @@ describe('Client web access path', () => {
expect(userName).toEqual('Legion'); expect(userName).toEqual('Legion');
expect(email).toEqual('legion@marvel.com'); expect(email).toEqual('legion@marvel.com');
expect(logName).toEqual('Legion'); // expect(logName).toEqual('Legion');
expect(logActive).toEqual('✗'); // 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() => { it(`should add the item tax to all countries`, async() => {
await page.autocompleteSearch(selectors.itemTax.firstClass, 'General VAT'); await page.autocompleteSearch(selectors.itemTax.firstClass, 'General VAT');
await page.autocompleteSearch(selectors.itemTax.secondClass, 'General VAT'); await page.autocompleteSearch(selectors.itemTax.secondClass, 'General VAT');
await page.autocompleteSearch(selectors.itemTax.thirdClass, 'General VAT');
await page.waitToClick(selectors.itemTax.submitTaxButton); await page.waitToClick(selectors.itemTax.submitTaxButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
@ -40,13 +39,6 @@ describe('Item edit tax path', () => {
expect(secondVatType).toEqual('General VAT'); 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() => { it(`should edit the first class without saving the form`, async() => {
await page.autocompleteSearch(selectors.itemTax.firstClass, 'Reduced VAT'); await page.autocompleteSearch(selectors.itemTax.firstClass, 'Reduced VAT');
const firstVatType = await page.waitToGetProperty(selectors.itemTax.firstClass, 'value'); const firstVatType = await page.waitToGetProperty(selectors.itemTax.firstClass, 'value');

View File

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

View File

@ -9,7 +9,7 @@ describe('Travel descriptor path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('buyer', 'travel'); 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.keyboard.press('Enter');
await page.waitForState('travel.card.summary'); await page.waitForState('travel.card.summary');
}); });
@ -23,7 +23,7 @@ describe('Travel descriptor path', () => {
await page.waitForState('travel.index'); await page.waitForState('travel.index');
const result = await page.countElement(selectors.travelIndex.anySearchResult); 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() => { 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'); await page.accessToSection('account.role.card.inherited');
const rolesCount = await page.countElement(selectors.accountRoleInheritance.anyResult); 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; display: block;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
height: 36px; height: 38px;
width: 36px; width: 38px;
font-size: 22px; font-size: 22px;
background-color: $color-main; background-color: $color-main;
position: relative; position: relative;

View File

@ -24,7 +24,7 @@ export default class Auth {
initialize() { initialize() {
let criteria = { let criteria = {
to: state => { 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); return !outLayout.some(ol => ol == state.name);
} }
}; };
@ -59,12 +59,31 @@ export default class Auth {
password: password || undefined password: password || undefined
}; };
return this.$http.post('VnUsers/signIn', params) const now = new Date();
.then(json => this.onLoginOk(json, remember)); return this.$http.post('VnUsers/sign-in', params).then(
json => this.onLoginOk(json, now, remember));
} }
onLoginOk(json, remember) { validateCode(user, password, code, remember) {
this.vnToken.set(json.data.token, json.data.created, 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(() => { return this.loadAcls().then(() => {
let continueHash = this.$state.params.continue; let continueHash = this.$state.params.continue;

View File

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

View File

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

View File

@ -1,402 +1,411 @@
@font-face { @font-face {
font-family: 'salixfont'; font-family: 'salixfont';
src: src:
url('./salixfont.ttf?wtrl3') format('truetype'), url('./salixfont.ttf?wtrl3') format('truetype'),
url('./salixfont.woff?wtrl3') format('woff'), url('./salixfont.woff?wtrl3') format('woff'),
url('./salixfont.svg?wtrl3#salixfont') format('svg'); url('./salixfont.svg?wtrl3#salixfont') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
[class^="icon-"], [class*=" icon-"] { [class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
font-family: 'salixfont' !important; font-family: 'salixfont' !important;
speak: none; speak: none;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
font-variant: normal; font-variant: normal;
text-transform: none; text-transform: none;
line-height: 1; line-height: 1;
/* Better Font Rendering =========== */ /* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-agency-term:before { .icon-trailer:before {
content: "\e950"; content: "\e967";
} }
.icon-defaulter:before { .icon-grafana:before {
content: "\e94b"; content: "\e965";
} }
.icon-100:before { .icon-trolley:before {
content: "\e95a"; content: "\e95c";
} }
.icon-clientUnpaid:before { .icon-agency-term:before {
content: "\e95b"; content: "\e950";
} }
.icon-history:before { .icon-defaulter:before {
content: "\e968"; content: "\e94b";
} }
.icon-Person:before { .icon-100:before {
content: "\e901"; content: "\e95a";
} }
.icon-accessory:before { .icon-clientUnpaid:before {
content: "\e90a"; content: "\e95b";
} }
.icon-account:before { .icon-history:before {
content: "\e92a"; content: "\e968";
} }
.icon-actions:before { .icon-Person:before {
content: "\e960"; content: "\e901";
} }
.icon-addperson:before { .icon-accessory:before {
content: "\e90e"; content: "\e90a";
} }
.icon-agency:before { .icon-account:before {
content: "\e938"; content: "\e92a";
} }
.icon-albaran:before { .icon-actions:before {
content: "\e94d"; content: "\e960";
} }
.icon-anonymous:before { .icon-addperson:before {
content: "\e930"; content: "\e90e";
} }
.icon-apps:before { .icon-agency:before {
content: "\e951"; content: "\e938";
} }
.icon-artificial:before { .icon-albaran:before {
content: "\e90b"; content: "\e94d";
} }
.icon-attach:before { .icon-anonymous:before {
content: "\e92e"; content: "\e930";
} }
.icon-barcode:before { .icon-apps:before {
content: "\e971"; content: "\e951";
} }
.icon-basket:before { .icon-artificial:before {
content: "\e914"; content: "\e90b";
} }
.icon-basketadd:before { .icon-attach:before {
content: "\e913"; content: "\e92e";
} }
.icon-bin:before { .icon-barcode:before {
content: "\e96f"; content: "\e971";
} }
.icon-botanical:before { .icon-basket:before {
content: "\e972"; content: "\e914";
} }
.icon-bucket:before { .icon-basketadd:before {
content: "\e97a"; content: "\e913";
} }
.icon-buscaman:before { .icon-bin:before {
content: "\e93b"; content: "\e96f";
} }
.icon-buyrequest:before { .icon-botanical:before {
content: "\e932"; content: "\e972";
} }
.icon-calc_volum .path1:before { .icon-bucket:before {
content: "\e915"; content: "\e97a";
} }
.icon-calc_volum .path2:before { .icon-buscaman:before {
content: "\e916"; content: "\e93b";
margin-left: -1em; }
} .icon-buyrequest:before {
.icon-calc_volum .path3:before { content: "\e932";
content: "\e917"; }
margin-left: -1em; .icon-calc_volum .path1:before {
} content: "\e915";
.icon-calc_volum .path4:before { }
content: "\e918"; .icon-calc_volum .path2:before {
margin-left: -1em; content: "\e916";
} margin-left: -1em;
.icon-calc_volum .path5:before { }
content: "\e919"; .icon-calc_volum .path3:before {
margin-left: -1em; content: "\e917";
} margin-left: -1em;
.icon-calc_volum .path6:before { }
content: "\e91a"; .icon-calc_volum .path4:before {
margin-left: -1em; content: "\e918";
} margin-left: -1em;
.icon-calendar:before { }
content: "\e93d"; .icon-calc_volum .path5:before {
} content: "\e919";
.icon-catalog:before { margin-left: -1em;
content: "\e937"; }
} .icon-calc_volum .path6:before {
.icon-claims:before { content: "\e91a";
content: "\e963"; margin-left: -1em;
} }
.icon-client:before { .icon-calendar:before {
content: "\e928"; content: "\e93d";
} }
.icon-clone:before { .icon-catalog:before {
content: "\e973"; content: "\e937";
} }
.icon-columnadd:before { .icon-claims:before {
content: "\e954"; content: "\e963";
} }
.icon-columndelete:before { .icon-client:before {
content: "\e953"; content: "\e928";
} }
.icon-components:before { .icon-clone:before {
content: "\e946"; content: "\e973";
} }
.icon-consignatarios:before { .icon-columnadd:before {
content: "\e93f"; content: "\e954";
} }
.icon-control:before { .icon-columndelete:before {
content: "\e949"; content: "\e953";
} }
.icon-credit:before { .icon-components:before {
content: "\e927"; content: "\e946";
} }
.icon-deletedTicket:before { .icon-consignatarios:before {
content: "\e935"; content: "\e93f";
} }
.icon-deleteline:before { .icon-control:before {
content: "\e955"; content: "\e949";
} }
.icon-delivery:before { .icon-credit:before {
content: "\e939"; content: "\e927";
} }
.icon-deliveryprices:before { .icon-deletedTicket:before {
content: "\e91c"; content: "\e935";
} }
.icon-details:before { .icon-deleteline:before {
content: "\e961"; content: "\e955";
} }
.icon-dfiscales:before { .icon-delivery:before {
content: "\e984"; content: "\e939";
} }
.icon-disabled:before { .icon-deliveryprices:before {
content: "\e921"; content: "\e91c";
} }
.icon-doc:before { .icon-details:before {
content: "\e977"; content: "\e961";
} }
.icon-entry:before { .icon-dfiscales:before {
content: "\e934"; content: "\e984";
} }
.icon-exit:before { .icon-disabled:before {
content: "\e92f"; content: "\e921";
} }
.icon-eye:before { .icon-doc:before {
content: "\e976"; content: "\e977";
} }
.icon-fixedPrice:before { .icon-entry:before {
content: "\e90d"; content: "\e934";
} }
.icon-flower:before { .icon-exit:before {
content: "\e90c"; content: "\e92f";
} }
.icon-frozen:before { .icon-eye:before {
content: "\e900"; content: "\e976";
} }
.icon-fruit:before { .icon-fixedPrice:before {
content: "\e903"; content: "\e90d";
} }
.icon-funeral:before { .icon-flower:before {
content: "\e904"; content: "\e90c";
} }
.icon-greenery:before { .icon-frozen:before {
content: "\e907"; content: "\e900";
} }
.icon-greuge:before { .icon-fruit:before {
content: "\e944"; content: "\e903";
} }
.icon-grid:before { .icon-funeral:before {
content: "\e980"; content: "\e904";
} }
.icon-handmade:before { .icon-greenery:before {
content: "\e909"; content: "\e907";
} }
.icon-handmadeArtificial:before { .icon-greuge:before {
content: "\e902"; content: "\e944";
} }
.icon-headercol:before { .icon-grid:before {
content: "\e958"; content: "\e980";
} }
.icon-info:before { .icon-handmade:before {
content: "\e952"; content: "\e909";
} }
.icon-inventory:before { .icon-handmadeArtificial:before {
content: "\e92b"; content: "\e902";
} }
.icon-invoice:before { .icon-headercol:before {
content: "\e923"; content: "\e958";
} }
.icon-invoice-in:before { .icon-info:before {
content: "\e911"; content: "\e952";
} }
.icon-invoice-in-create:before { .icon-inventory:before {
content: "\e912"; content: "\e92b";
} }
.icon-invoice-out:before { .icon-invoice:before {
content: "\e910"; content: "\e923";
} }
.icon-isTooLittle:before { .icon-invoice-in:before {
content: "\e91b"; content: "\e911";
} }
.icon-item:before { .icon-invoice-in-create:before {
content: "\e956"; content: "\e912";
} }
.icon-languaje:before { .icon-invoice-out:before {
content: "\e926"; content: "\e910";
} }
.icon-lines:before { .icon-isTooLittle:before {
content: "\e942"; content: "\e91b";
} }
.icon-linesprepaired:before { .icon-item:before {
content: "\e948"; content: "\e956";
} }
.icon-logout:before { .icon-languaje:before {
content: "\e936"; content: "\e926";
} }
.icon-mana:before { .icon-lines:before {
content: "\e96a"; content: "\e942";
} }
.icon-mandatory:before { .icon-linesprepaired:before {
content: "\e97b"; content: "\e948";
} }
.icon-net:before { .icon-logout:before {
content: "\e931"; content: "\e936";
} }
.icon-niche:before { .icon-mana:before {
content: "\e96c"; content: "\e96a";
} }
.icon-no036:before { .icon-mandatory:before {
content: "\e920"; content: "\e97b";
} }
.icon-noPayMethod:before { .icon-net:before {
content: "\e905"; content: "\e931";
} }
.icon-notes:before { .icon-niche:before {
content: "\e941"; content: "\e96c";
} }
.icon-noweb:before { .icon-no036:before {
content: "\e91f"; content: "\e920";
} }
.icon-onlinepayment:before { .icon-noPayMethod:before {
content: "\e91d"; content: "\e905";
} }
.icon-package:before { .icon-notes:before {
content: "\e978"; content: "\e941";
} }
.icon-payment:before { .icon-noweb:before {
content: "\e97e"; content: "\e91f";
} }
.icon-pbx:before { .icon-onlinepayment:before {
content: "\e93c"; content: "\e91d";
} }
.icon-pets:before { .icon-package:before {
content: "\e947"; content: "\e978";
} }
.icon-photo:before { .icon-payment:before {
content: "\e924"; content: "\e97e";
} }
.icon-plant:before { .icon-pbx:before {
content: "\e908"; content: "\e93c";
} }
.icon-polizon:before { .icon-pets:before {
content: "\e95e"; content: "\e947";
} }
.icon-preserved:before { .icon-photo:before {
content: "\e906"; content: "\e924";
} }
.icon-recovery:before { .icon-plant:before {
content: "\e97c"; content: "\e908";
} }
.icon-regentry:before { .icon-polizon:before {
content: "\e964"; content: "\e95e";
} }
.icon-reserva:before { .icon-preserved:before {
content: "\e959"; content: "\e906";
} }
.icon-revision:before { .icon-recovery:before {
content: "\e94a"; content: "\e97c";
} }
.icon-risk:before { .icon-regentry:before {
content: "\e91e"; content: "\e964";
} }
.icon-services:before { .icon-reserva:before {
content: "\e94c"; content: "\e959";
} }
.icon-settings:before { .icon-revision:before {
content: "\e979"; content: "\e94a";
} }
.icon-shipment-01:before { .icon-risk:before {
content: "\e929"; content: "\e91e";
} }
.icon-sign:before { .icon-services:before {
content: "\e95d"; content: "\e94c";
} }
.icon-sms:before { .icon-settings:before {
content: "\e975"; content: "\e979";
} }
.icon-solclaim:before { .icon-shipment-01:before {
content: "\e95f"; content: "\e929";
} }
.icon-solunion:before { .icon-sign:before {
content: "\e94e"; content: "\e95d";
} }
.icon-stowaway:before { .icon-sms:before {
content: "\e94f"; content: "\e975";
} }
.icon-splitline:before { .icon-solclaim:before {
content: "\e93e"; content: "\e95f";
} }
.icon-splur:before { .icon-solunion:before {
content: "\e970"; content: "\e94e";
} }
.icon-supplier:before { .icon-stowaway:before {
content: "\e925"; content: "\e94f";
} }
.icon-supplierfalse:before { .icon-splitline:before {
content: "\e90f"; content: "\e93e";
} }
.icon-tags:before { .icon-splur:before {
content: "\e96d"; content: "\e970";
} }
.icon-tax:before { .icon-supplier:before {
content: "\e940"; content: "\e925";
} }
.icon-thermometer:before { .icon-supplierfalse:before {
content: "\e933"; content: "\e90f";
} }
.icon-ticket:before { .icon-tags:before {
content: "\e96b"; content: "\e96d";
} }
.icon-ticketAdd:before { .icon-tax:before {
content: "\e945"; content: "\e940";
} }
.icon-traceability:before { .icon-thermometer:before {
content: "\e962"; content: "\e933";
} }
.icon-transaction:before { .icon-ticket:before {
content: "\e966"; content: "\e96b";
} }
.icon-treatments:before { .icon-ticketAdd:before {
content: "\e922"; content: "\e945";
} }
.icon-unavailable:before { .icon-traceability:before {
content: "\e92c"; content: "\e962";
} }
.icon-volume:before { .icon-transaction:before {
content: "\e96e"; content: "\e966";
} }
.icon-wand:before { .icon-treatments:before {
content: "\e93a"; content: "\e922";
} }
.icon-web:before { .icon-unavailable:before {
content: "\e982"; content: "\e92c";
} }
.icon-wiki:before { .icon-volume:before {
content: "\e92d"; content: "\e96e";
} }
.icon-worker:before { .icon-wand:before {
content: "\e957"; content: "\e93a";
} }
.icon-zone:before { .icon-web:before {
content: "\e943"; content: "\e982";
} }
.icon-wiki:before {
content: "\e92d";
}
.icon-worker:before {
content: "\e957";
}
.icon-zone:before {
content: "\e943";
}

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" type="password"
autocomplete="false"> autocomplete="false">
</vn-textfield> </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"> <div class="footer">
<vn-submit label="Change password" ng-click="$ctrl.submit()"></vn-submit> <vn-submit label="Change password" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper"> <div class="spinner-wrapper">

View File

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

View File

@ -2,6 +2,10 @@ Change password: Cambiar contraseña
Old password: Antigua contraseña Old password: Antigua contraseña
New password: Nueva contraseña New password: Nueva contraseña
Repeat password: Repetir 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 updated!: ¡Contraseña actualizada!
Password requirements: > Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud, La contraseña debe tener al menos {{ length }} caracteres de longitud,

View File

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

View File

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

View File

@ -3,14 +3,13 @@ import Component from 'core/lib/component';
import './style.scss'; import './style.scss';
export class Layout extends Component { export class Layout extends Component {
constructor($element, $, vnModules, vnToken) { constructor($element, $, vnModules) {
super($element, $); super($element, $);
this.modules = vnModules.get(); this.modules = vnModules.get();
} }
$onInit() { $onInit() {
this.getUserData(); this.getUserData();
this.getAccessTokenConfig();
} }
getUserData() { getUserData() {
@ -32,41 +31,11 @@ export class Layout extends Component {
window.location.reload(); 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() { $onDestroy() {
clearInterval(this.inservalId); this.vnToken.stopRenewer();
} }
} }
Layout.$inject = ['$element', '$scope', 'vnModules', 'vnToken']; Layout.$inject = ['$element', '$scope', 'vnModules'];
ngModule.vnComponent('vnLayout', { ngModule.vnComponent('vnLayout', {
template: require('./index.html'), template: require('./index.html'),

View File

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

View File

@ -3,7 +3,10 @@ import Section from '../section';
import {hashToColor} from 'core/lib/string'; import {hashToColor} from 'core/lib/string';
import './style.scss'; 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 { export default class Controller extends Section {
constructor($element, $) { constructor($element, $) {
@ -28,6 +31,20 @@ export default class Controller extends Section {
select: 'visibility' select: 'visibility'
}; };
this.filter = { this.filter = {
fields: [
'id',
'originFk',
'userFk',
'action',
'changedModel',
'oldInstance',
'newInstance',
'creationDate',
'changedModel',
'changedModelId',
'changedModelValue',
'description'
],
include: [{ include: [{
relation: 'user', relation: 'user',
scope: { scope: {
@ -48,6 +65,11 @@ export default class Controller extends Section {
this.today.setHours(0, 0, 0, 0); this.today.setHours(0, 0, 0, 0);
} }
$onInit() {
const match = this.url?.match(/(.*)Logs$/);
this.modelI18n = match && this.translateModel(match[1]);
}
$postLink() { $postLink() {
this.resetFilter(); this.resetFilter();
this.$.$watch( this.$.$watch(
@ -63,47 +85,75 @@ export default class Controller extends Section {
set logs(value) { set logs(value) {
this._logs = value; this._logs = value;
this.logTree = [];
if (!value) return; if (!value) return;
const empty = {}; const empty = {};
const validations = window.validations; 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 notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || empty; const olds = (notDelete ? log.oldInstance : null) || empty;
const vals = (notDelete ? log.newInstance : log.oldInstance) || 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)); let propNames = Object.keys(olds).concat(Object.keys(vals));
props = [...new Set(props)]; propNames = [...new Set(propNames)];
log.props = []; log.props = this.parseProps(propNames, locale, vals, olds);
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};
} }
} }
@ -114,17 +164,76 @@ export default class Controller extends Section {
set models(value) { set models(value) {
this._models = value; this._models = value;
if (!value) return; if (!value) return;
for (const model of value) { for (const model of value)
const name = model.changedModel; model.changedModelI18n = this.translateModel(model.changedModel);
model.changedModelI18n =
firstUpper(window.validations[name]?.locale?.name) || name;
}
} }
get showModelName() { get showModelName() {
return !(this.changedModel && this.changedModelId); 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) { castJsonValue(value) {
return typeof value === 'string' && validDate.test(value) return typeof value === 'string' && validDate.test(value)
? new Date(value) ? new Date(value)
@ -160,12 +269,11 @@ export default class Controller extends Section {
applyFilter() { applyFilter() {
const filter = this.$.filter; const filter = this.$.filter;
function getParam(prop, value) { const getParam = (prop, value) => {
if (value == null || value == '') return null; if (value == null || value == '') return null;
switch (prop) { switch (prop) {
case 'search': case 'search':
const or = []; if (/^\s*[0-9]+\s*$/.test(value) || this.byRecord)
if (/^\s*[0-9]+\s*$/.test(value))
return {changedModelId: value.trim()}; return {changedModelId: value.trim()};
else else
return {changedModelValue: {like: `%${value}%`}}; return {changedModelValue: {like: `%${value}%`}};
@ -177,72 +285,86 @@ export default class Controller extends Section {
]}; ]};
case 'who': case 'who':
switch (value) { switch (value) {
case 'all':
return null;
case 'user': case 'user':
return {userFk: {neq: null}}; return {userFk: {neq: null}};
case 'system': case 'system':
return {userFk: null}; return {userFk: null};
case 'all':
default:
return null;
} }
case 'actions': case 'actions': {
const inq = []; const inq = [];
for (const action in value) { for (const action in value) {
if (value[action]) if (value[action])
inq.push(action); inq.push(action);
} }
return inq.length ? {action: {inq}} : null; return inq.length ? {action: {inq}} : null;
}
case 'from': case 'from':
if (filter.to) { if (filter.to)
return {creationDate: {gte: value}}; return {creationDate: {gte: value}};
} else { else {
const to = new Date(value); const to = new Date(value);
to.setHours(23, 59, 59, 999); to.setHours(23, 59, 59, 999);
return {creationDate: {between: [value, to]}}; return {creationDate: {between: [value, to]}};
} }
case 'to': case 'to': {
const to = new Date(value); const to = new Date(value);
to.setHours(23, 59, 59, 999); to.setHours(23, 59, 59, 999);
return {creationDate: {lte: to}}; return {creationDate: {lte: to}};
}
case 'userFk': case 'userFk':
return filter.who != 'system' return filter.who != 'system'
? {[prop]: value} : null; ? {[prop]: value} : null;
default: default:
return {[prop]: value}; return {[prop]: value};
} }
} };
this.hasFilter = false;
const and = []; const and = [];
if (!filter.search || !filter.changedModel)
this.byRecord = false;
if (!this.byRecord)
and.push({originFk: this.originId});
for (const prop in filter) { for (const prop in filter) {
const param = getParam(prop, filter[prop]); 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; const lbFilter = and.length ? {where: {and}} : null;
return this.$.model.applyFilter(lbFilter); return this.$.model.applyFilter(lbFilter);
} }
filterByEntity(log) { filterByRecord(modelLog) {
this.byRecord = true;
this.$.filter = { this.$.filter = {
who: 'all', who: 'all',
search: log.changedModelId, search: modelLog.id,
changedModel: log.changedModel changedModel: modelLog.model
}; };
} }
searchUser(search) { searchUser(search) {
if (/^[0-9]+$/.test(search)) { if (/^[0-9]+$/.test(search))
return {id: search}; return {id: search};
} else { else {
return {or: [ return {or: [
{name: search}, {name: search},
{nickname: {like: `%${search}%`}} {nickname: {like: `%${search}%`}}
]} ]};
} }
} }
showWorkerDescriptor(event, log) { showWorkerDescriptor(event, userLog) {
if (log.user?.worker) if (userLog.user?.worker)
this.$.workerDescriptor.show(event.target, log.userFk); this.$.workerDescriptor.show(event.target, userLog.userFk);
} }
} }

View File

@ -24,4 +24,5 @@ Changes: Cambios
today: hoy today: hoy
yesterday: ayer yesterday: ayer
Show all record changes: Mostrar todos los cambios realizados en el registro 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 Quit filter: Quitar filtro

View File

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

View File

@ -24,13 +24,23 @@ export default class Controller {
this.loading = false; this.loading = false;
}) })
.catch(req => { .catch(req => {
this.loading = false; if (req?.data?.error?.code === 'REQUIRES_2FA') {
this.password = ''; this.outLayout.login = {
this.focusUser(); user: this.user,
password: this.password,
remember: this.remember
};
this.$state.go('validate-email');
return;
}
const err = req.data?.error; const err = req.data?.error;
if (err?.code == 'passExpired') if (err?.code == 'passExpired')
this.$state.go('change-password', err.details.token); this.$state.go('change-password', err.details.token);
this.loading = false;
this.password = '';
this.focusUser();
throw req; throw req;
}); });
} }
@ -44,5 +54,8 @@ Controller.$inject = ['$scope', '$element', '$state', 'vnAuth'];
ngModule.vnComponent('vnLogin', { ngModule.vnComponent('vnLogin', {
template: require('./index.html'), 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, abstract: true,
template: '<vn-layout></vn-layout>', template: '<vn-layout></vn-layout>',
resolve: { resolve: {
config: ['vnConfig', vnConfig => vnConfig.initialize()] config: ['vnConfig', vnConfig => vnConfig.initialize()],
token: ['vnToken', vnToken => vnToken.fetchConfig()]
} }
}) })
.state('outLayout', { .state('outLayout', {
@ -36,9 +37,15 @@ function config($stateProvider, $urlRouterProvider) {
description: 'Reset password', description: 'Reset password',
template: '<vn-reset-password></vn-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', { .state('change-password', {
parent: 'outLayout', parent: 'outLayout',
url: '/change-password?id&userId', url: '/change-password?id&userId&twoFactor',
description: 'Change password', description: 'Change password',
template: '<vn-change-password></vn-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"`]; 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 args = Object.assign({}, ctx.args);
const params = { const params = {
recipient: args.recipient, recipient: args.recipient,
@ -52,6 +52,6 @@ module.exports = Self => {
const email = new Email(templateName, params); 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); Self.super_.setup.call(this);
require('../methods/log/editors')(this); require('../methods/log/editors')(this);
require('../methods/log/models')(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", "Receipt's bank was not found": "Receipt's bank was not found",
"This receipt was not compensated": "This receipt was not compensated", "This receipt was not compensated": "This receipt was not compensated",
"Client's email was not found": "Client's email was not found", "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 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 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", "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", "Warehouse inventory not set": "Almacén inventario no está establecido",
"Component cost not set": "Componente coste no está estabecido", "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", "Description cannot be blank": "Description cannot be blank",
"company": "Company", "company": "Company",
"country": "Country", "country": "Country",
@ -175,6 +173,10 @@
"Pass expired": "The password has expired, change it from Salix", "Pass expired": "The password has expired, change it from Salix",
"Can't transfer claimed sales": "Can't transfer claimed sales", "Can't transfer claimed sales": "Can't transfer claimed sales",
"Invalid quantity": "Invalid quantity", "Invalid quantity": "Invalid quantity",
"Failed to upload delivery note": "Error to upload delivery note {{id}}", "Failed to upload delivery note": "Error to upload delivery note {{id}}",
"Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address" "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", "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", "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", "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 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}}*", "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", "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", "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", "Warehouse inventory not set": "El almacén inventario no está establecido",
"This locker has already been assigned": "Esta taquilla ya ha sido asignada", "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", "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", "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", "Collection does not exist": "La colección no existe",
@ -277,6 +276,8 @@
"Insert a date range": "Inserte un rango de fechas", "Insert a date range": "Inserte un rango de fechas",
"Added observation": "{{user}} añadió esta observacion: {{text}}", "Added observation": "{{user}} añadió esta observacion: {{text}}",
"Comment added to client": "Observación añadida al cliente {{clientFk}}", "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", "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", "company": "Compañía",
"country": "País", "country": "País",
@ -294,9 +295,15 @@
"Invalid NIF for VIES": "Invalid NIF for VIES", "Invalid NIF for VIES": "Invalid NIF for VIES",
"Ticket does not exist": "Este ticket no existe", "Ticket does not exist": "Este ticket no existe",
"Ticket is already signed": "Este ticket ya ha sido firmado", "Ticket is already signed": "Este ticket ya ha sido firmado",
"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", "Fecha fuera de rango": "Fecha fuera de rango",
"Error while generating PDF": "Error al generar PDF", "Error while generating PDF": "Error al generar PDF",
"Error when sending mail to client": "Error al enviar el correo al cliente", "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", "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 => { module.exports = Self => {
Self.remoteMethod('changePassword', { Self.remoteMethodCtx('changePassword', {
description: 'Changes the user password', description: 'Changes the user password',
accessType: 'WRITE', accessType: 'WRITE',
accessScopes: ['changePassword'],
accepts: [ accepts: [
{ {
arg: 'id',
type: 'number',
description: 'The user id',
http: {source: 'path'}
}, {
arg: 'oldPassword', arg: 'oldPassword',
type: 'string', type: 'string',
description: 'The old password', description: 'The old password',
@ -19,15 +16,35 @@ module.exports = Self => {
type: 'string', type: 'string',
description: 'The new password', description: 'The new password',
required: true required: true
}, {
arg: 'code',
type: 'string',
description: 'The 2FA code'
} }
], ],
http: { http: {
path: `/:id/changePassword`, path: `/change-password`,
verb: 'PATCH' verb: 'PATCH'
} }
}); });
Self.changePassword = async function(id, oldPassword, newPassword) { Self.changePassword = async function(ctx, oldPassword, newPassword, code, options) {
await Self.app.models.VnUser.changePassword(id, oldPassword, newPassword); 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 => { module.exports = Self => {
Self.remoteMethod('login', { Self.remoteMethodCtx('login', {
description: 'Login a user with username/email and password', description: 'Login a user with username/email and password',
accepts: [ 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) { Self.setPassword = async function(id, newPassword, options) {
await Self.app.models.VnUser.setPassword(id, newPassword); 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'); const {models} = require('vn-loopback/server/server');
describe('account changePassword()', () => { describe('account changePassword()', () => {
it('should throw an error when old password is wrong', async() => { const ctx = {req: {accessToken: {userId: 70}}};
let error; const unauthCtx = {
try { req: {
await models.Account.changePassword(1, 'wrongPassword', 'nightmare.9999'); headers: {},
} catch (e) { connection: {
error = e.message; 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({});
expect(error).toContain('Invalid current password'); let error;
try {
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 throw an error when old and new password are the same', async() => {
const tx = await models.Account.beginTransaction({});
let error;
try {
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();
}
});
}); });
it('should change password', async() => { describe('With 2FA', () => {
try { it('should change password when code is correct', async() => {
await models.Account.changePassword(70, 'nightmare', 'nightmare.9999'); const tx = await models.Account.beginTransaction({});
} catch (e) { const yesterday = Date.vnNew();
expect(e).toBeUndefined(); 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() => { 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", "principalType": "ROLE",
"principalId": "$authenticated", "principalId": "$authenticated",
"permission": "ALLOW" "permission": "ALLOW"
},
{
"property": "changePassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
} }
] ]
} }

View File

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

View File

@ -21,12 +21,11 @@ export default class Controller extends Section {
} }
onAddClick() { onAddClick() {
this.addData = {account: this.$params.id};
this.$.dialog.show(); this.$.dialog.show();
} }
onAddSave() { onAddSave() {
return this.$http.post(`MailAliasAccounts`, this.addData) return this.$http.post(`VnUsers/${this.$params.id}/addAlias`, this.addData)
.then(() => this.refresh()) .then(() => this.refresh())
.then(() => this.vnApp.showSuccess( .then(() => this.vnApp.showSuccess(
this.$t('Subscribed to alias!')) this.$t('Subscribed to alias!'))
@ -34,11 +33,12 @@ export default class Controller extends Section {
} }
onRemove(row) { onRemove(row) {
return this.$http.delete(`MailAliasAccounts/${row.id}`) const params = {
.then(() => { mailAlias: row.mailAlias
this.$.data.splice(this.$.data.indexOf(row), 1); };
this.vnApp.showSuccess(this.$t('Unsubscribed from alias!')); 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()', () => { describe('onAddSave()', () => {
it('should add the new row', () => { it('should add the new row', () => {
controller.addData = {account: 1}; controller.addData = {account: 1};
controller.$params = {id: 1};
$httpBackend.expectPOST('MailAliasAccounts').respond(); $httpBackend.expectPOST('VnUsers/1/addAlias').respond();
$httpBackend.expectGET('MailAliasAccounts').respond('foo'); $httpBackend.expectGET('MailAliasAccounts').respond('foo');
controller.onAddSave(); controller.onAddSave();
$httpBackend.flush(); $httpBackend.flush();
@ -41,12 +42,14 @@ describe('component vnUserAliases', () => {
{id: 1, alias: 'foo'}, {id: 1, alias: 'foo'},
{id: 2, alias: 'bar'} {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]); controller.onRemove(controller.$.data[0]);
$httpBackend.flush(); $httpBackend.flush();
expect(controller.$.data).toEqual([{id: 2, alias: 'bar'}]); expect(controller.$.data).toEqual({id: 2, alias: 'bar'});
expect(controller.vnApp.showSuccess).toHaveBeenCalled(); expect(controller.vnApp.showSuccess).toHaveBeenCalled();
}); });
}); });

View File

@ -77,14 +77,6 @@ module.exports = Self => {
if (salesPersonId) if (salesPersonId)
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); 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); const email = new Email('claim-pickup-order', params);
return email.send(); return email.send();

View File

@ -95,7 +95,7 @@ module.exports = Self => {
// When claimState has been changed // When claimState has been changed
if (args.claimStateFk) { 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.hasToNotify) {
if (newState.code == 'incomplete') if (newState.code == 'incomplete')
notifyStateChange(ctx, salesPerson.id, claim, newState.code); notifyStateChange(ctx, salesPerson.id, claim, newState.code);

View File

@ -19,9 +19,6 @@ module.exports = Self => {
}); });
Self.confirmTransaction = async(ctx, id, options) => { Self.confirmTransaction = async(ctx, id, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let tx; let tx;
const myOptions = {userId: ctx.req.accessToken.userId}; const myOptions = {userId: ctx.req.accessToken.userId};
@ -34,29 +31,8 @@ module.exports = Self => {
} }
try { try {
const oldTpvTransaction = await models.TpvTransaction.findById(id, null, myOptions);
const confirm = await Self.rawSql('CALL hedera.tpvTransaction_confirmById(?)', [id], 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(); if (tx) await tx.commit();
return confirm; return confirm;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); 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 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 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; return sms;
}; };
}; };

View File

@ -28,7 +28,7 @@ module.exports = Self => {
const isAccount = await models.Account.findById(id); const isAccount = await models.Account.findById(id);
if (isClient && !isAccount) if (isClient && !isAccount)
await models.Account.setPassword(id, newPassword); await models.VnUser.setPassword(id, newPassword);
else else
throw new UserError(`Modifiable password only via recovery or by an administrator`); 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 sms = await models.Client.sendSms(ctx, id, destination, message, options);
const createdLog = await models.ClientLog.findById(sms.logId, null, options); expect(sms).toBeDefined();
const json = JSON.parse(JSON.stringify(createdLog.newInstance));
expect(json.message).toEqual(message);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -144,20 +144,7 @@ module.exports = Self => {
if (taxDataChecked && !hasSageData) if (taxDataChecked && !hasSageData)
throw new UserError(`You need to fill sage information before you check verified data`); 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 // Remove unwanted properties
delete args.ctx; delete args.ctx;
delete args.id; delete args.id;

View File

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

View File

@ -14,7 +14,7 @@ module.exports = Self => {
Self.validatesPresenceOf('street', { Self.validatesPresenceOf('street', {
message: 'Street cannot be empty' message: 'Street cannot be empty'
}); });
Self.validatesPresenceOf('city', { Self.validatesPresenceOf('city', {
message: 'City cannot be empty' message: 'City cannot be empty'
}); });
@ -89,7 +89,7 @@ module.exports = Self => {
}; };
const country = await Self.app.models.Country.findOne(filter); const country = await Self.app.models.Country.findOne(filter);
const code = country ? country.code.toLowerCase() : null; 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)) if (!this.fi || !validateTin(this.fi, code) || (this.isVies && countryCode == code))
err(); err();
@ -282,7 +282,7 @@ module.exports = Self => {
await Self.changeCredit(ctx, finalState, changes); await Self.changeCredit(ctx, finalState, changes);
// Credit management changes // Credit management changes
if (orgData?.rating != changes.rating || orgData?.recommendedCredit != changes.recommendedCredit) if (changes?.rating || changes?.recommendedCredit)
await Self.changeCreditManagement(ctx, finalState, changes); await Self.changeCreditManagement(ctx, finalState, changes);
const oldInstance = {}; const oldInstance = {};
@ -401,22 +401,32 @@ module.exports = Self => {
Self.changeCredit = async function changeCredit(ctx, finalState, changes) { Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.options.accessToken.userId; const userId = ctx.options.accessToken.userId;
const accessToken = {req: {accessToken: ctx.options.accessToken} }; const accessToken = {req: {accessToken: ctx.options.accessToken}};
const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE'); const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE');
if (!canEditCredit) { if (!canEditCredit) {
const lastCredit = await models.ClientCredit.findOne({ const lastCredit = await models.ClientCredit.findOne({
field: ['workerFk', 'amount'],
where: { where: {
clientFk: finalState.id clientFk: finalState.id
}, },
order: 'id DESC' order: 'id DESC'
}, ctx.options); }, ctx.options);
const lastAmount = lastCredit && lastCredit.amount; if (lastCredit && lastCredit.amount == 0) {
const lastCreditIsNotEditable = !await models.ACL.checkAccessAcl(accessToken, 'Client', 'isNotEditableCredit', 'WRITE'); 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`); throw new UserError(`You can't change the credit set to zero from a financialBoss`);
}
const creditLimits = await models.ClientCreditLimit.find({ const creditLimits = await models.ClientCreditLimit.find({
fields: ['roleFk'], fields: ['roleFk'],
@ -479,21 +489,6 @@ module.exports = Self => {
hasChanges = oldData.name != changes.name || oldData.active != changes.active; hasChanges = oldData.name != changes.name || oldData.active != changes.active;
if (!hasChanges) return; 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 { try {
const options = {transaction: tx}; const options = {transaction: tx};
const context = {options}; const ctx = {options};
// Set credit to zero by a financialBoss // Set credit to zero by a financialBoss
const financialBoss = await models.VnUser.findOne({ const financialBoss = await models.VnUser.findOne({
where: {name: 'financialBoss'} where: {name: 'financialBoss'}
}, options); }, 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({ const salesAssistant = await models.VnUser.findOne({
where: {name: 'salesAssistant'} where: {name: 'salesAssistant'}
}, options); }, 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(); await tx.rollback();
} catch (e) { } catch (e) {
@ -93,14 +93,14 @@ describe('Client Model', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const context = {options}; const ctx = {options};
const salesAssistant = await models.VnUser.findOne({ const salesAssistant = await models.VnUser.findOne({
where: {name: 'salesAssistant'} where: {name: 'salesAssistant'}
}, options); }, 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(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -68,7 +68,7 @@
</span> </span>
</vn-td> </vn-td>
<vn-td number>{{::clientInforma.rating}}</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-tr>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>

View File

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

View File

@ -6,6 +6,7 @@ export default class Controller extends Section {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
this.defaulter = {}; this.defaulter = {};
this.defaulters = [];
this.checkedDefaulers = []; this.checkedDefaulers = [];
this.smartTableOptions = { this.smartTableOptions = {
@ -69,6 +70,18 @@ export default class Controller extends Section {
this.getBalanceDueTotal(); 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() { get checked() {
const clients = this.$.model.data || []; const clients = this.$.model.data || [];
const checkedLines = []; const checkedLines = [];

View File

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

View File

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

View File

@ -3,14 +3,13 @@
vn-entry-buy-index vn-card { vn-entry-buy-index vn-card {
max-width: $width-xl; max-width: $width-xl;
.dark-row { .dark-row {
background-color: lighten($color-marginal, 10%); background-color: lighten($color-marginal, 10%);
} }
thead tr { thead tr {
border-left: 1px solid white; border: 1px solid white;;
border-right: 1px solid white;
} }
tbody tr:nth-child(1), tbody tr:nth-child(1),
@ -22,7 +21,7 @@ vn-entry-buy-index vn-card {
tbody tr:nth-child(2) { tbody tr:nth-child(2) {
border-bottom: 1px solid $color-spacer; border-bottom: 1px solid $color-spacer;
} }
tbody{ tbody{
border-bottom: 1px solid $color-spacer; border-bottom: 1px solid $color-spacer;
} }
@ -40,4 +39,4 @@ vn-entry-buy-index vn-card {
} }
} }
$color-font-link-medium: lighten($color-font-link, 20%) $color-font-link-medium: lighten($color-font-link, 20%)

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