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

This commit is contained in:
Vicent Llopis 2023-07-11 11:50:15 +00:00
commit a831a1534b
97 changed files with 1883 additions and 395 deletions

View File

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

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, ttl: token.ttl};
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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,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

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;

View File

@ -77,7 +77,10 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `
ORDER BY id;
INSERT INTO `account`.`account`(`id`)
SELECT id FROM `account`.`user`;
SELECT `u`.`id`
FROM `account`.`user` `u`
JOIN `account`.`role` `r` ON `u`.`role` = `r`.`id`
WHERE `r`.`name` <> 'customer';
INSERT INTO `vn`.`educationLevel` (`id`, `name`)
VALUES
@ -382,9 +385,16 @@ INSERT INTO `vn`.`clientManaCache`(`clientFk`, `mana`, `dated`)
(1103, 0, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(1104, -30, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH));
INSERT INTO `vn`.`clientConfig`(`riskTolerance`, `maxCreditRows`)
INSERT INTO `vn`.`mandateType`(`id`, `name`)
VALUES
(200, 10);
(1, 'B2B'),
(2, 'CORE'),
(3, 'LCR');
INSERT INTO `vn`.`clientConfig`(`id`, `riskTolerance`, `maxCreditRows`, `maxPriceIncreasingRatio`, `riskScope`, `defaultPayMethodFk`, `defaultDueDay`, `defaultCredit`, `defaultIsTaxDataChecked`, `defaultHasCoreVnl`, `defaultMandateTypeFk`)
VALUES
(1, 200, 10, 0.25, 2, 4, 5, 300.00, 1, 1, 2);
INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `phone`, `mobile`, `isActive`, `clientFk`, `agencyModeFk`, `longitude`, `latitude`, `isEqualizated`, `isDefaultAddress`)
VALUES
@ -700,40 +710,40 @@ INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agen
(6, NULL, 57, util.VN_CURDATE(), 5, 7, 'sixth route', 1.7, 60, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 3),
(7, NULL, 57, util.VN_CURDATE(), 6, 8, 'seventh route', 0, 70, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 5);
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`)
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`, `weight`)
VALUES
(1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)),
(4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH)),
(5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH)),
(6 , 1, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Mountain Drive Gotham', 1, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
(7 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'Mountain Drive Gotham', 1, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(8 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'Bat cave', 121, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(9 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(10, 1, 1, 5, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'Ingram Street', 2, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(11, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'NY roofs', 122, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(12, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(13, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(14, 1, 2, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1104, 'Malibu Point', 4, NULL, 0, 9, 5, 1, util.VN_CURDATE()),
(15, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1105, 'An incredibly long alias for testing purposes', 125, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(16, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(17, 1, 7, 2, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE()),
(18, 1, 4, 4, 4, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1108, 'Cerebro', 128, NULL, 0, 12, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +12 HOUR)),
(19, 1, 5, 5, NULL, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 1, NULL, 5, 1, util.VN_CURDATE()),
(20, 1, 5, 5, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH)),
(21, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Holland', 102, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH)),
(22, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Japan', 103, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH)),
(23, NULL, 8, 1, 7, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'address 21', 121, NULL, 0, 5, 5, 1, util.VN_CURDATE()),
(24 ,NULL, 8, 1, 7, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 5, 5, 1, util.VN_CURDATE()),
(25 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(26 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(28, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(29, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(30, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(31, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(32, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE());
(1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2),
(3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL),
(4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL),
(5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL),
(6 , 1, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Mountain Drive Gotham', 1, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL),
(7 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'Mountain Drive Gotham', 1, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(8 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'Bat cave', 121, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(9 , NULL, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(10, 1, 1, 5, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'Ingram Street', 2, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(11, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1102, 'NY roofs', 122, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(12, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(13, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(14, 1, 2, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1104, 'Malibu Point', 4, NULL, 0, 9, 5, 1, util.VN_CURDATE(), NULL),
(15, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1105, 'An incredibly long alias for testing purposes', 125, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(16, 1, 7, 1, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(17, 1, 7, 2, 6, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1106, 'Many Places', 126, NULL, 0, 3, 5, 1, util.VN_CURDATE(), NULL),
(18, 1, 4, 4, 4, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1108, 'Cerebro', 128, NULL, 0, 12, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +12 HOUR), NULL),
(19, 1, 5, 5, NULL, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 1, NULL, 5, 1, util.VN_CURDATE(), NULL),
(20, 1, 5, 5, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Thailand', 129, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), NULL),
(21, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Holland', 102, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), NULL),
(22, NULL, 5, 5, 5, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL +1 MONTH), INTERVAL +1 DAY), 1109, 'Somewhere in Japan', 103, NULL, 0, 13, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL +1 MONTH), NULL),
(23, NULL, 8, 1, 7, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1101, 'address 21', 121, NULL, 0, 5, 5, 1, util.VN_CURDATE(), NULL),
(24 ,NULL, 8, 1, 7, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 5, 5, 1, util.VN_CURDATE(), NULL),
(25 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(26 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(28, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(29, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(30, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(31, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL),
(32, 1, 8, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL + 2 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE(), NULL);
INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`)
VALUES
@ -824,12 +834,6 @@ INSERT INTO `vn`.`greuge`(`id`, `clientFk`, `description`, `amount`, `shipped`,
(11, 1101, 'some heritage charges', -15.99, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 5, 1),
(12, 1101, 'some miscellaneous charges', 58.00, DATE_ADD(util.VN_CURDATE(), INTERVAL 1 MONTH), util.VN_CURDATE(), 6, 1);
INSERT INTO `vn`.`mandateType`(`id`, `name`)
VALUES
(1, 'B2B'),
(2, 'CORE'),
(3, 'LCR');
INSERT INTO `vn`.`mandate`(`id`, `clientFk`, `companyFk`, `code`, `created`, `mandateTypeFk`)
VALUES
(1, 1102, 442, '1-1', util.VN_CURDATE(), 2);
@ -2857,8 +2861,8 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
('lilium', 'dev', 'http://localhost:9000/#/'),
('salix', 'dev', 'http://localhost:5000/#!/');
('lilium', 'development', 'http://localhost:9000/#/'),
('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
VALUES

View File

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

View File

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

View File

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

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

View File

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

View File

@ -34,7 +34,6 @@ export default class Token {
remember
});
this.vnInterceptor.setToken(token);
try {
if (remember)
this.setStorage(localStorage, token, created, ttl);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -268,7 +268,7 @@
"Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite",
"Warehouse inventory not set": "El almacén inventario no está establecido",
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº %d",
"Not exist this branch": "La rama no existe",
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
"Collection does not exist": "La colección no existe",
@ -276,6 +276,8 @@
"Insert a date range": "Inserte un rango de fechas",
"Added observation": "{{user}} añadió esta observacion: {{text}}",
"Comment added to client": "Observación añadida al cliente {{clientFk}}",
"Invalid auth code": "Código de verificación incorrecto",
"Invalid or expired verification code": "Código de verificación incorrecto o expirado",
"Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen",
"company": "Compañía",
"country": "País",
@ -293,10 +295,13 @@
"Invalid NIF for VIES": "Invalid NIF for VIES",
"Ticket does not exist": "Este ticket no existe",
"Ticket is already signed": "Este ticket ya ha sido firmado",
"Authentication failed": "Autenticación fallida",
"You can't use the same password": "No puedes usar la misma contraseña",
"You can only add negative amounts in refund tickets": "Solo se puede añadir cantidades negativas en tickets abono",
"Fecha fuera de rango": "Fecha fuera de rango",
"Error while generating PDF": "Error al generar PDF",
"Error when sending mail to client": "Error al enviar el correo al cliente",
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado"
"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",
"Valid priorities": "Prioridades válidas: %d"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
let UserError = require('vn-loopback/util/user-error');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('new', {
@ -49,7 +49,7 @@ module.exports = Self => {
try {
const itemConfig = await models.ItemConfig.findOne({fields: ['validPriorities']}, myOptions);
if (!itemConfig.validPriorities.includes(params.priority))
throw new UserError(`Valid priorities: ${[...itemConfig.validPriorities]}`);
throw new UserError('Valid priorities', 'VALID_PRIORITIES', [...itemConfig.validPriorities]);
const provisionalName = params.provisionalName;
delete params.provisionalName;

View File

@ -50,7 +50,7 @@ module.exports = Self => {
fields: ['id']}
, myOptions);
if (ticketRefunds.length > 0)
throw new UserError($t('Tickets with associated refunds', {id: ticketRefunds[0].id}));
throw new UserError('Tickets with associated refunds', 'TICKET_REFUND', ticketRefunds[0].id);
// Check if has sales with shelving
const canDeleteTicketWithPartPrepared =

View File

@ -60,6 +60,9 @@
},
"totalWithoutVat": {
"type": "number"
},
"weight": {
"type": "number"
}
},
"relations": {

View File

@ -101,23 +101,33 @@
translate>
Change shipped hour
</vn-item>
<vn-item
ng-click="$ctrl.sendPaymentSms()"
name="sendPaymentSms"
<vn-item class="dropdown"
vn-click-stop="sendDeliveryNoteMenu.show($event, 'left');"
name="smsOptions"
translate>
SMS Pending payment
</vn-item>
<vn-item
ng-click="$ctrl.sendImportSms()"
name="sendImportSms"
translate>
SMS Minimum import
</vn-item>
<vn-item
ng-click="$ctrl.sendChangesSms()"
name="sendChangesSms"
translate>
SMS Notify changes
Send SMS...
<vn-menu vn-id="sendDeliveryNoteMenu">
<vn-list>
<vn-item
ng-click="$ctrl.sendPaymentSms()"
name="sendPaymentSms"
translate>
Pending payment
</vn-item>
<vn-item
ng-click="$ctrl.sendImportSms()"
name="sendImportSms"
translate>
Minimum import
</vn-item>
<vn-item
ng-click="$ctrl.sendChangesSms()"
name="sendChangesSms"
translate>
Notify changes
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item
ng-click="makeInvoiceConfirmation.show()"
@ -163,6 +173,12 @@
</vn-list>
</vn-menu>
</vn-item>
<vn-item
ng-click="setTicketWeight.show()"
vn-acl="deliveryBoss"
translate>
Set ticket weight
</vn-item>
</vn-list>
</vn-menu>
@ -348,3 +364,28 @@
question="Are you sure you want to replace this delivery note?"
message="Already exist signed delivery note">
</vn-confirm>
<!-- Set ticket weight -->
<vn-dialog
vn-id="setTicketWeight"
title="TicketWeight"
size="sm"
on-accept="$ctrl.setTicketWeight($ctrl.ticket.weight)">
<tpl-body>
<vn-input-number
label="Ticket weight"
ng-model="$ctrl.ticket.weight">
</vn-input-number>
</tpl-body>
<tpl-buttons>
<button response="accept" translate>Accept</button>
</tpl-buttons>
</vn-dialog>
<!-- Without weight confirmation dialog -->
<vn-confirm
vn-id="withoutWeightConfirmation"
on-accept="$ctrl.makeInvoice(true)"
question="This address has incoterms, you should set the weight before invoice it"
message="Are you sure you want to invoice this ticket?">
</vn-confirm>

View File

@ -249,16 +249,11 @@ class Controller extends Section {
this.$.sms.open();
}
makeInvoice() {
const params = {ticketsIds: [this.id]};
/*
This should call the notification sistem to insert a new notification
in te queue, yet to check how to handle user permissions,
as of 08-11-2022 every employee can insert a new notification in the queue
makeInvoice(force) {
if (this.ticket.address.incotermsFk && !this.ticket.weight && !force)
return this.$.withoutWeightConfirmation.show();
*/
const client = this.ticket.client;
if (client.hasElectronicInvoice) {
this.$http.post(`NotificationQueues`, {
notificationFk: 'invoice-electronic',
@ -275,7 +270,8 @@ class Controller extends Section {
});
}
return this.$http.post(`Tickets/makeInvoice`, params)
return this.$http.post(`Tickets/makeInvoice`, {ticketsIds: [this.id]})
.then(() => this.reload())
.then(() => this.reload())
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
}
@ -331,6 +327,15 @@ class Controller extends Section {
this.vnApp.showSuccess(this.$t('PDF sent!'));
});
}
setTicketWeight(weight) {
return this.$http.patch(`Tickets/${this.ticket.id}`, {weight})
.then(() => {
this.$.setTicketWeight.hide();
this.vnApp.showSuccess(this.$t('Data saved!'));
this.reload();
});
}
}
Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];

View File

@ -198,6 +198,17 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
expect(controller.reload).toHaveBeenCalledWith();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
it('should show withoutWeightConfirmation', () => {
controller.$.withoutWeightConfirmation = {show: () => {}};
jest.spyOn(controller.$.withoutWeightConfirmation, 'show');
controller.ticket.address.incotermsFk = true;
controller.makeInvoice();
controller.ticket.address.incotermsFk = false;
expect(controller.$.withoutWeightConfirmation.show).toHaveBeenCalled();
});
});
describe('createPdfInvoice()', () => {

View File

@ -16,8 +16,14 @@ without warehouse: sin almacén
Invoice sent: Factura enviada
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
Transfer client: Transferir cliente
SMS Notify changes: SMS Notificar cambios
Send SMS...: Enviar SMS...
Notify changes: Notificar cambios
Minimum import: Importe mínimo
Pending payment: Pago pendiente
PDF sent!: ¡PDF enviado!
Already exist signed delivery note: Ya existe albarán de entrega firmado
Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán de entrega?
Create a single ticket with all the content of the current ticket: Crea un ticket único con todo el contenido del ticket actual
Set ticket weight: Establecer peso al ticket
Ticket weight: Peso del ticket
This address has incoterms, you should set the weight before invoice it: Este consigatario tiene incoterms, deberías establecer el peso antes de facturar

View File

@ -17,8 +17,6 @@ Shipped hour updated: Hora de envio modificada
Deleted ticket: Ticket eliminado
Recalculate components: Recalcular componentes
Are you sure you want to recalculate the components?: ¿Seguro que quieres recalcular los componentes?
SMS Minimum import: 'SMS Importe minimo'
SMS Pending payment: 'SMS Pago pendiente'
Restore ticket: Restaurar ticket
You are going to restore this ticket: Vas a restaurar este ticket
Are you sure you want to restore this ticket?: ¿Seguro que quieres restaurar el ticket?

View File

@ -65,6 +65,11 @@
{{$ctrl.summary.refFk | dashIfEmpty}}
</span>
</vn-label-value>
<vn-label-value label="Weight">
<span>
{{$ctrl.summary.weight | dashIfEmpty}}
</span>
</vn-label-value>
</vn-one>
<vn-two>
<vn-label-value label="Shipped"

View File

@ -33,7 +33,6 @@ module.exports = Self => {
map.set(node.parentFk, []);
map.get(node.parentFk).push(node);
}
function setLeaves(nodes) {
if (!nodes) return;
for (let node of nodes) {
@ -43,6 +42,7 @@ module.exports = Self => {
}
let leaves = map.get(parentId);
setLeaves(leaves);
return leaves || [];

View File

@ -0,0 +1,10 @@
const models = require('vn-loopback/server/server').models;
describe('department getLeaves()', () => {
const ctx = {req: {accessToken: {userId: 9}}};
it('should return the department and the childs containing the search value', async() => {
let result = await models.Department.getLeaves(ctx, null, 'INFORMATICA');
expect(result.length).toEqual(1);
});
});

View File

@ -171,7 +171,7 @@ module.exports = Self => {
throw new UserError(`That payment method requires an IBAN`);
await models.Worker.rawSql(
'CALL vn.clientCreate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
'CALL vn.client_create(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
args.firstName,
args.lastNames,

View File

@ -8,8 +8,9 @@
},
"properties": {
"id": {
"type": "number",
"id": true,
"type": "number"
"description": "Identifier"
},
"code": {
"type": "string"
@ -37,6 +38,30 @@
},
"hasToMistake": {
"type": "number"
},
"isTeleworking": {
"type": "boolean"
},
"hasToRefill": {
"type": "boolean"
},
"hasToSendMail": {
"type": "boolean"
},
"isProduction": {
"type": "boolean"
}
},
"relations": {
"client": {
"type": "belongsTo",
"model": "Client",
"foreignKey": "clientFk"
},
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
}
}
}

View File

@ -60,7 +60,7 @@
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
"foreignKey": "id"
},
"boss": {
"type": "belongsTo",

View File

@ -0,0 +1,102 @@
<vn-watcher
vn-id="watcher"
data="$ctrl.department"
form="form"
url="Departments">
</vn-watcher>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
info="Search departments by id, name or code"
on-search="$ctrl.onSearch($params)"
base-state="worker.department">
</vn-searchbar>
</vn-portal>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-horizontal>
<vn-textfield
vn-one
label="Name"
ng-model="$ctrl.department.name"
vn-name="Name">
</vn-textfield>
<vn-textfield
vn-one
label="Code"
ng-model="$ctrl.department.code"
vn-name="Code">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Chat"
ng-model="$ctrl.department.chatName"
vn-name="Chat">
</vn-textfield>
<vn-textfield type="Email"
vn-one
label="Email"
ng-model="$ctrl.department.notificationEmail"
vn-name="Email">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
ng-model="$ctrl.department.workerFk"
url="Workers/activeWithInheritedRole"
show-field="nickname"
search-function="{firstName: $search}"
label="Boss department">
</vn-autocomplete>
<vn-autocomplete
ng-model="$ctrl.department.clientFk"
url="Clients/"
show-field="name"
search-function="{firstName: $search}"
label="Self-consumption customer">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-check
label="Telework"
ng-model="$ctrl.department.isTeleworking">
</vn-check>
<vn-check
label="Notify on errors"
ng-model="$ctrl.department.hasToMistake">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
label="worksInProduction"
ng-model="$ctrl.department.isProduction">
</vn-check>
<vn-check
label="Fill in days without physical check-ins"
ng-model="$ctrl.department.hasToRefill">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
label="Send check-ins by email"
ng-model="$ctrl.department.hasToSendMail">
</vn-check>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
<vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>

View File

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

View File

@ -0,0 +1,13 @@
Name: Nombre
Code: Código
Chat: Chat
Email: Email
Boss department: Jefe del departamento
Self-consumption customer: Cliente autoconsumo
Telecommutes: Teletrabaja
Notify on errors: Notificar errores
worksInProduction: Pertenece a producción
Fill in days without physical check-ins: Completar días sin registros físicos
Send check-ins by email: Enviar fichadas por email
Save: Guardar
Undo changes: Deshacer cambios

View File

@ -0,0 +1,5 @@
<vn-portal slot="menu">
<vn-worker-department-descriptor department="$ctrl.department"></vn-worker-department-descriptor>
<vn-left-menu source="department"></vn-left-menu>
</vn-portal>
<ui-view></ui-view>

View File

@ -0,0 +1,39 @@
import ngModule from '../../module';
import ModuleCard from 'salix/components/module-card';
class Controller extends ModuleCard {
reload() {
const filter = {
fields: ['id', 'name', 'code', 'workerFk', 'isProduction', 'chatName',
'isTeleworking', 'notificationEmail', 'hasToRefill', 'hasToSendMail', 'hasToMistake', 'clientFk'],
include: [
{
relation: 'client',
scope: {
fields: ['id', 'name']
}
}, {
relation: 'worker',
scope: {
fields: ['id', 'firstName', 'lastName'],
include: {
relation: 'user',
scope: {
fields: ['name']
}
}
}
}
]
};
this.$http.get(`Departments/${this.$params.id}`, {filter})
.then(res => this.department = res.data);
}
}
ngModule.vnComponent('vnWorkerDepartmentCard', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,4 @@
<slot-descriptor>
<vn-worker-department-descriptor>
</vn-worker-department-descriptor>
</slot-descriptor>

View File

@ -0,0 +1,9 @@
import ngModule from '../../module';
import DescriptorPopover from 'salix/components/descriptor-popover';
class Controller extends DescriptorPopover {}
ngModule.vnComponent('vnWorkerDepartmentDescriptorPopover', {
slotTemplate: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,49 @@
<vn-descriptor-content
module="worker"
description="$ctrl.department.name"
base-state="worker.department">
<slot-menu>
<vn-item
ng-click="deleteConfirmation.show()"
name="deleteDepartment"
translate>
Delete
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
<vn-label-value
label="Chat"
value="{{$ctrl.department.chatName}}">
</vn-label-value>
<vn-label-value
label="Email"
value="{{$ctrl.department.emailNotification}}">
</vn-label-value>
<vn-label-value
label="Self-consumption customer"
value="{{$ctrl.department.client.name}}">
</vn-label-value>
<vn-label-value
label="Boss department"
value="{{$ctrl.department.worker.firstName}}">
</vn-label-value>
</div>
<div class="quicklinks">
<div ng-transclude="btnOne">
<vn-quick-link vn-anchor="{state: 'worker.index', params: {q: {departmentFk: $ctrl.$params.id} } }"
vn-tooltip="Department workers"
icon="icon-worker">
</vn-quick-link>
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-popup vn-id="summary">
<vn-worker-summary worker="$ctrl.worker"></vn-worker-summary>
</vn-popup>
<vn-confirm
vn-id="deleteConfirmation"
on-accept="$ctrl.deleteDepartment()"
question="Are you sure you want to delete this department?">
</vn-confirm>

View File

@ -0,0 +1,45 @@
import ngModule from '../../module';
import Descriptor from 'salix/components/descriptor';
class Controller extends Descriptor {
constructor($element, $, $rootScope) {
super($element, $);
this.$rootScope = $rootScope;
}
get department() {
return this.entity;
}
set department(value) {
this.entity = value;
}
filterDepartments(department, event) {
if (event.defaultPrevented) return;
event.preventDefault();
event.stopPropagation();
this.$state.go(`worker.index`,
{q: JSON.stringify({departmentFk: department.id})});
}
deleteDepartment() {
return this.$http.delete(`Departments/${this.id}`)
.then(() => {
this.$state.go('worker.department');
this.vnApp.showSuccess(this.$t('Department deleted.'));
});
}
}
Controller.$inject = ['$element', '$scope', '$rootScope'];
ngModule.vnComponent('vnWorkerDepartmentDescriptor', {
template: require('./index.html'),
controller: Controller,
bindings: {
department: '<'
}
});

View File

@ -1,83 +1,7 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
$postLink() {
this.$.treeview.fetch();
}
onFetch(item) {
const params = item ? {parentId: item.id} : null;
return this.$.model.applyFilter({}, params).then(() => {
return this.$.model.data;
});
}
onSort(a, b) {
return a.name.localeCompare(b.name);
}
onDrop(dropped, dragged) {
const params = dropped ? {parentId: dropped.id} : null;
const query = `departments/${dragged.id}/moveChild`;
this.$http.post(query, params).then(() => {
this.$.treeview.move(dragged, dropped);
});
}
onCreate(parent) {
this.newChild = {
parent: parent,
name: ''
};
this.$.createNode.show();
}
onCreateDialogOpen() {
this.newChild.name = '';
}
onCreateResponse() {
try {
if (!this.newChild.name)
throw new Error(`Name can't be empty`);
const params = {name: this.newChild.name};
const parent = this.newChild.parent;
if (parent && parent.id)
params.parentId = parent.id;
const query = `departments/createChild`;
this.$http.post(query, params).then(res => {
const item = res.data;
item.parent = parent;
this.$.treeview.create(item);
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
onRemove(item) {
this.removedChild = item;
this.$.deleteNode.show();
}
onRemoveResponse() {
const childId = this.removedChild.id;
const path = `departments/${childId}/removeChild`;
this.$http.post(path).then(() => {
this.$.treeview.remove(this.removedChild);
});
}
}
ngModule.vnComponent('vnWorkerDepartment', {
template: require('./index.html'),
controller: Controller
});
import './main';
import './index/';
import './summary';
import './card';
import './descriptor';
import './basic-data';
import './descriptor-popover';

View File

@ -3,6 +3,14 @@
url="departments/getLeaves"
auto-load="false">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
info="Search departments by id, name or code"
on-search="$ctrl.onSearch($params)"
base-state="worker.department">
</vn-searchbar>
</vn-portal>
<form name="form" class="vn-w-md">
<vn-card class="vn-pa-md">
<vn-treeview vn-id="treeview" root-label="Departments" read-only="false"
@ -13,7 +21,12 @@
on-drop="$ctrl.onDrop($dropped, $dragged)"
on-drag-start="$ctrl.onDragStart(item)"
on-drag-end="$ctrl.onDragEnd(item)">
{{::item.name}}
<a
style="display: block; color: inherit;"
ui-sref="worker.department.card.summary({id: item.id})"
translate>
{{::item.name}}
</a>
</vn-treeview>
</vn-card>
</form>

View File

@ -0,0 +1,94 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
class Controller extends Section {
$postLink() {
this.$.treeview.fetch();
if (this.$params.q) {
const search = JSON.parse(this.$params.q);
this.onSearch(search);
}
}
onSearch(params) {
this.$.model.applyFilter({}, params).then(() => {
const data = this.$.model.data;
this.$.treeview.data = data;
});
}
onFetch(item) {
const params = item ? {parentId: item.id} : null;
return this.$.model.applyFilter({}, params).then(() => {
return this.$.model.data;
});
}
onSort(a, b) {
return a.name.localeCompare(b.name);
}
onDrop(dropped, dragged) {
const params = dropped ? {parentId: dropped.id} : null;
const query = `departments/${dragged.id}/moveChild`;
this.$http.post(query, params).then(() => {
this.$.treeview.move(dragged, dropped);
});
}
onCreate(parent) {
this.newChild = {
parent: parent,
name: ''
};
this.$.createNode.show();
}
onCreateDialogOpen() {
this.newChild.name = '';
}
onCreateResponse() {
try {
if (!this.newChild.name)
throw new Error(`Name can't be empty`);
const params = {name: this.newChild.name};
const parent = this.newChild.parent;
if (parent && parent.id)
params.parentId = parent.id;
const query = `departments/createChild`;
this.$http.post(query, params).then(res => {
const item = res.data;
item.parent = parent;
this.$.treeview.create(item);
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
onRemove(item) {
this.removedChild = item;
this.$.deleteNode.show();
}
onRemoveResponse() {
const childId = this.removedChild.id;
const path = `departments/${childId}/removeChild`;
this.$http.post(path).then(() => {
this.$.treeview.remove(this.removedChild);
});
}
}
ngModule.vnComponent('vnWorkerDepartmentIndex', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,4 +1,5 @@
New department: Nuevo departamento
Delete department: Eliminar departamento
Are you sure you want to delete it?: ¿Seguro que quieres eliminarlo?
Name can't be empty: El nombre esta vacio
Name can't be empty: El nombre esta vacio
Department workers: Trabajadores del departamento

View File

@ -0,0 +1,9 @@
<vn-crud-model
vn-id="model"
url="Departments/filter"
limit="20"
order="id">
</vn-crud-model>
<ui-view>
<vn-worker-department-index></vn-worker-department-index>
</ui-view>

View File

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

View File

@ -0,0 +1,91 @@
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
info="Search departments by id, name or code"
on-search="$ctrl.onSearch($params)"
base-state="worker.department">
</vn-searchbar>
</vn-portal>
<vn-card class="summary">
<h5>
<span>{{summary.name}}</span>
</h5>
<vn-horizontal class="vn-pa-md">
<vn-one class="expand">
<h4 ng-show="$ctrl.isHr">
<a
ui-sref="worker.department.card.basicData({id:summary.id})">
<span translate vn-tooltip="Go to">Basic data</span>
</a>
</h4>
<h4
translates
ng-show="!$ctrl.isHr">
Basic data
</h4>
</vn-one>
<vn-one>
<vn-vertical>
<vn-label-value label="Nombre"
value="{{summary.name}}">
</vn-label-value>
<vn-label-value label="Code"
value="{{summary.code}}">
</vn-label-value>
<vn-label-value label="Chat"
value="{{summary.chatName}}">
</vn-label-value>
<vn-label-value
label="Boss department">
<span
ng-click="workerDescriptor.show($event, summary.worker.id)"
class="link">
{{summary.worker.user.name}}
</span>
</vn-label-value>
<vn-label-value
label="Email"
value="{{summary.notificationEmail}}">
</vn-label-value>
<vn-label-value label="Self-consumption customer"
value="{{summary.client.name}}">
</vn-label-value>
</vn-vertical>
</vn-one>
<vn-one>
<vn-vertical>
<vn-check
label="Telework"
ng-model="summary.isTeleworking"
disabled="true">
</vn-check>
<vn-check
label="Notify on errors"
ng-model="summary.hasToMistake"
disabled="true">
</vn-check>
<vn-check
label="worksInProduction"
ng-model="summary.isProduction"
disabled="true">
</vn-check>
<vn-check
label="Fill in days without physical check-ins"
ng-model="summary.hasToRefill"
disabled="true">
</vn-check>
<vn-check
label="Send check-ins by email"
ng-model="summary.hasToSendMail"
disabled="true">
</vn-check>
</vn-vertical>
</vn-one>
</vn-horizontal>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-worker-department-descriptor-popover
vn-id="workerDepartmentDescriptor">
</vn-worker-department-descriptor-popover>

View File

@ -0,0 +1,27 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
class Controller extends Component {
set department(value) {
this._department = value;
this.$.summary = null;
if (!value) return;
this.$.summary = this.department;
}
get department() {
return this._department;
}
get isHr() {
return this.aclService.hasAny(['hr']);
}
}
ngModule.component('vnWorkerDepartmentSummary', {
template: require('./index.html'),
controller: Controller,
bindings: {
department: '<'
}
});

View File

@ -0,0 +1,12 @@
Name: Nombre
Code: Código
Chat: Chat
Boss department: Jefe de departamento
Email: Email
Self-consumption customer: Cliente autoconsumo
Telework: Teletrabaja
Notify on errors: Notificar errores
worksInProduction: Pertenece a producción
Fill in days without physical check-ins: Completar días sin registros físicos
Send check-ins by email: Enviar fichadas por mail
Are you sure you want to delete this department?: ¿Estás seguro de que quieres eliminar este departamento?

View File

@ -0,0 +1,8 @@
@import "./variables";
vn-worker-department-summary {
.expand {
min-width: 100%;
}
}

View File

@ -24,6 +24,9 @@
"description": "Wikipedia"
},
{"state": "worker.card.workerLog", "icon": "history"}
],
"department": [
{"state": "worker.department.card.basicData", "icon": "settings"}
]
},
"keybindings": [
@ -118,12 +121,35 @@
"worker": "$ctrl.worker"
}
}, {
"url" : "/department",
"url": "/department?q",
"state": "worker.department",
"component": "vn-worker-department",
"description": "Departments",
"acl": ["hr"]
"description":"Departments"
}, {
"url": "/:id",
"state": "worker.department.card",
"component": "vn-worker-department-card",
"abstract": true,
"description": "Detail"
}, {
"url" : "/summary",
"state": "worker.department.card.summary",
"component": "vn-worker-department-summary",
"description": "Summary",
"params": {
"department": "$ctrl.department"
}
},
{
"url": "/basic-data",
"state": "worker.department.card.basicData",
"component": "vn-worker-department-basic-data",
"description": "Basic data",
"params": {
"department": "$ctrl.department"
}
},
{
"url": "/dms",
"state": "worker.card.dms",
"abstract": true,

View File

@ -27,15 +27,19 @@
<vn-label-value label="Email" no-ellipsize
value="{{worker.user.emailUser.email}}">
</vn-label-value>
<vn-label-value label="Department"
value="{{worker.department.department.name}}">
<vn-label-value label="Department">
<span
ng-click="workerDepartmentDescriptor.show($event, worker.department.department.id)"
class="link">
{{worker.department.department.name}}
</span>
</vn-label-value>
<vn-label-value
label="Boss">
<span
ng-click="workerDescriptor.show($event, worker.boss.id)"
class="link">
{{::worker.boss.nickname}}
{{::worker.boss.name}}
</span>
</vn-label-value>
<vn-label-value label="Mobile extension"
@ -71,3 +75,6 @@
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-worker-department-descriptor-popover
vn-id="workerDepartmentDescriptor">
</vn-worker-department-descriptor-popover>

View File

@ -38,7 +38,7 @@ class Controller extends Summary {
},
{
relation: 'boss',
scope: {fields: ['id', 'nickname']}
scope: {fields: ['id', 'name']}
},
{
relation: 'sip',
@ -48,7 +48,8 @@ class Controller extends Summary {
relation: 'department',
scope: {
include: {
relation: 'department'
relation: 'department',
scope: {fields: ['id', 'code', 'name']}
}
}
}

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "salix-back",
"version": "23.26.01",
"version": "23.30.01",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "salix-back",
"version": "23.24.01",
"version": "23.26.01",
"license": "GPL-3.0",
"dependencies": {
"axios": "^1.2.2",

View File

@ -1,6 +1,6 @@
{
"name": "salix-back",
"version": "23.26.01",
"version": "23.30.01",
"author": "Verdnatura Levante SL",
"description": "Salix backend",
"license": "GPL-3.0",

View File

@ -84,7 +84,8 @@ class Email extends Component {
replyTo: this.args.replyTo || '',
subject: localeSubject,
html: rendered,
attachments: attachments
attachments: attachments,
force: options.force
};
return smtp.send(mailOptions);

View File

@ -10,16 +10,17 @@ module.exports = {
async send(options) {
options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
if (!process.env.NODE_ENV)
options.to = config.app.senderEmail;
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' && !options.force) {
const notProductionError = {message: 'This not production, this email not sended'};
await this.mailLog(options, notProductionError);
if (!config.smtp.auth.user)
return Promise.resolve(true);
options.to = config.app.senderEmail;
}
if (!config.smtp.auth.user)
return Promise.resolve(true);
let res;
let error;
try {

View File

@ -0,0 +1,13 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,5 @@
.code {
border: 2px dashed #8dba25;
border-radius: 3px;
text-align: center
}

View File

@ -0,0 +1,23 @@
<email-body v-bind="$props">
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{ $t('description') }}</p>
<p>
{{ $t('device') }}: <strong>{{ device }}</strong>
</p>
<p>
{{$t('ip')}}: <strong>{{ ip }}</strong>
</p>
</div>
</div>
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<p>{{ $t('Enter the following code to continue to your account') }}</p>
<div class="code vn-pa-sm vn-m-md">
{{ code }}
</div>
<p>{{ $t('It expires in 5 minutes') }}</p>
</div>
</div>
</email-body>

View File

@ -0,0 +1,21 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
module.exports = {
name: 'auth-code',
components: {
'email-body': emailBody.build(),
},
props: {
code: {
type: String,
required: true
},
device: {
type: String
},
ip: {
type: String
}
}
};

View File

@ -0,0 +1,7 @@
subject: Verification code
title: Verification code
description: Somebody did request a verification code for login. If you didn't request it, please ignore this email.
device: 'Device'
ip: 'IP'
Enter the following code to continue to your account: Enter the following code to continue to your account
It expires in 5 minutes: It expires in 5 minutes

View File

@ -0,0 +1,7 @@
subject: Código de verificación
title: Código de verificación
description: Alguien ha solicitado un código de verificación para poder iniciar sesión. Si no lo has solicitado tu, ignora este email.
device: 'Dispositivo'
ip: 'IP'
Enter the following code to continue to your account: Introduce el siguiente código para poder continuar con tu cuenta
It expires in 5 minutes: Expira en 5 minutos

View File

@ -0,0 +1,7 @@
subject: Code de vérification
title: Code de vérification
description: Quelqu'un a demandé un code de vérification pour se connecter. Si ce n'était pas toi, ignore cet email.
device: 'Appareil'
ip: 'IP'
Enter the following code to continue to your account: Entrez le code suivant pour continuer avec votre compte
It expires in 5 minutes: Il expire dans 5 minutes.

View File

@ -0,0 +1,7 @@
subject: Código de verificação
title: Código de verificação
description: Alguém solicitou um código de verificação para entrar. Se você não fez essa solicitação, ignore este e-mail.
device: 'Dispositivo'
ip: 'IP'
Enter the following code to continue to your account: Insira o seguinte código para continuar com sua conta.
It expires in 5 minutes: Expira em 5 minutos.

View File

@ -1,36 +1,36 @@
SELECT io.issued,
c.socialName,
c.street postalAddress,
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
SELECT io.issued,
c.socialName,
c.street postalAddress,
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
io.clientFk,
c.postcode,
c.city,
c.postcode,
c.city,
io.companyFk,
io.ref,
tc.code,
s.concept,
s.quantity,
s.price,
s.discount,
s.ticketFk,
tc.code,
s.concept,
s.quantity,
s.price,
s.discount,
s.ticketFk,
t.shipped,
t.refFk,
a.nickname,
t.refFk,
a.nickname,
s.itemFk,
s.id saleFk,
pm.name AS pmname,
sa.iban,
c.phone,
c.phone,
MAX(t.packages) packages,
a.incotermsFk,
ic.name incotermsName ,
sub.description weight,
t.observations,
t.weight,
t.observations,
ca.fiscalName customsAgentName,
ca.street customsAgentStreet,
ca.nif customsAgentNif,
ca.phone customsAgentPhone,
ca.email customsAgentEmail,
ca.email customsAgentEmail,
CAST(sub2.volume AS DECIMAL (10,2)) volume,
sub3.intrastat
FROM vn.invoiceOut io
@ -38,21 +38,16 @@ SELECT io.issued,
JOIN vn.client c ON c.id = io.clientFk
LEFT JOIN vn.province p ON p.id = c.provinceFk
JOIN vn.ticket t ON t.refFk = io.ref
LEFT JOIN (SELECT tob.ticketFk,tob.description
FROM vn.ticketObservation tob
LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk
WHERE ot.description = "Peso Aduana"
)sub ON sub.ticketFk = t.id
JOIN vn.address a ON a.id = t.addressFk
LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk
LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN (SELECT SUM(volume) volume
FROM vn.invoiceOut io
FROM vn.invoiceOut io
JOIN vn.ticket t ON t.refFk = io.ref
JOIN vn.saleVolume sv ON sv.ticketFk = t.id
JOIN vn.saleVolume sv ON sv.ticketFk = t.id
WHERE t.refFk = ?
) sub2 ON TRUE
) sub2 ON TRUE
JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE'
@ -61,11 +56,11 @@ SELECT io.issued,
JOIN vn.company co ON co.id=io.companyFk
JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk
LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat
FROM vn.ticket t
FROM vn.ticket t
JOIN vn.invoiceOut io ON io.ref = t.refFk
JOIN vn.sale s ON t.id = s.ticketFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.intrastat ir ON ir.id = i.intrastatFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.intrastat ir ON ir.id = i.intrastatFk
WHERE t.refFk = ?
)sub3 ON TRUE
WHERE t.refFk = ?